chore: 初始化项目与后端基础工程
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
APP_NAME="财务 AI 助手 API"
|
||||
APP_VERSION=1.0.0
|
||||
DEBUG=true
|
||||
SECRET_KEY=change-me-in-production
|
||||
ALLOWED_ORIGINS=http://localhost:3000
|
||||
|
||||
DATABASE_URL=postgresql+asyncpg://s2f_user:s2f_password@localhost:5432/s2f_db
|
||||
|
||||
JWT_SECRET_KEY=change-me-in-production
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=60
|
||||
|
||||
OPENAI_API_KEY=
|
||||
OPENAI_MODEL=gpt-4-turbo-preview
|
||||
ZHIPU_API_KEY=
|
||||
AI_PROVIDER=zhipu
|
||||
AI_API_KEY=
|
||||
|
||||
UPLOAD_DIR=./uploads
|
||||
MAX_UPLOAD_SIZE=10485760
|
||||
|
||||
LOG_LEVEL=INFO
|
||||
@@ -0,0 +1,51 @@
|
||||
from functools import lru_cache
|
||||
from typing import Any
|
||||
|
||||
from pydantic import field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
case_sensitive=False,
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
app_name: str = "财务 AI 助手 API"
|
||||
app_version: str = "1.0.0"
|
||||
debug: bool = True
|
||||
secret_key: str = "change-me-in-production"
|
||||
allowed_origins: list[str] = ["http://localhost:3000"]
|
||||
|
||||
database_url: str = "postgresql+asyncpg://s2f_user:s2f_password@localhost:5432/s2f_db"
|
||||
|
||||
jwt_secret_key: str = "change-me-in-production"
|
||||
jwt_algorithm: str = "HS256"
|
||||
jwt_access_token_expire_minutes: int = 60
|
||||
|
||||
openai_api_key: str = ""
|
||||
openai_model: str = "gpt-4-turbo-preview"
|
||||
zhipu_api_key: str = ""
|
||||
ai_provider: str = "zhipu"
|
||||
ai_api_key: str = ""
|
||||
|
||||
upload_dir: str = "./uploads"
|
||||
max_upload_size: int = 10_485_760
|
||||
|
||||
log_level: str = "INFO"
|
||||
|
||||
@field_validator("allowed_origins", mode="before")
|
||||
@classmethod
|
||||
def parse_allowed_origins(cls, value: Any) -> list[str]:
|
||||
if isinstance(value, str):
|
||||
return [origin.strip() for origin in value.split(",") if origin.strip()]
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
return ["http://localhost:3000"]
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
@@ -0,0 +1,17 @@
|
||||
import logging
|
||||
|
||||
import structlog
|
||||
|
||||
|
||||
def configure_logging(log_level: str = "INFO") -> None:
|
||||
level = getattr(logging, log_level.upper(), logging.INFO)
|
||||
|
||||
structlog.configure(
|
||||
processors=[
|
||||
structlog.processors.TimeStamper(fmt="iso"),
|
||||
structlog.processors.add_log_level,
|
||||
structlog.processors.JSONRenderer(ensure_ascii=False),
|
||||
],
|
||||
wrapper_class=structlog.make_filtering_bound_logger(level),
|
||||
cache_logger_on_first_use=True,
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.core.config import get_settings
|
||||
from app.core.logging import configure_logging
|
||||
|
||||
settings = get_settings()
|
||||
configure_logging(settings.log_level)
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.app_name,
|
||||
version=settings.app_version,
|
||||
debug=settings.debug,
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.allowed_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/health", tags=["health"])
|
||||
async def health_check() -> dict[str, str]:
|
||||
return {
|
||||
"status": "ok",
|
||||
"service": settings.app_name,
|
||||
"version": settings.app_version,
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
[tool.poetry]
|
||||
name = "s2f-backend"
|
||||
version = "0.1.0"
|
||||
description = "财务 AI 助手后端服务,当前第一模块为薪酬财务对账 MVP。"
|
||||
authors = ["S2F Team"]
|
||||
readme = "../README.md"
|
||||
packages = [{ include = "app" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
fastapi = "0.110.0"
|
||||
uvicorn = { version = "0.27.0", extras = ["standard"] }
|
||||
pydantic = "2.6.0"
|
||||
pydantic-settings = "2.1.0"
|
||||
sqlalchemy = "2.0.25"
|
||||
asyncpg = "0.29.0"
|
||||
alembic = "1.13.0"
|
||||
python-jose = { version = "3.3.0", extras = ["cryptography"] }
|
||||
passlib = { version = "1.7.4", extras = ["bcrypt"] }
|
||||
python-multipart = "0.0.6"
|
||||
openpyxl = "3.1.2"
|
||||
pandas = "2.2.0"
|
||||
openai = "1.10.0"
|
||||
python-dotenv = "1.0.0"
|
||||
structlog = "24.1.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "7.4.4"
|
||||
pytest-asyncio = "0.23.4"
|
||||
httpx = "0.26.0"
|
||||
ruff = "0.1.15"
|
||||
black = "23.12.1"
|
||||
mypy = "1.8.0"
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ["py311"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py311"
|
||||
select = ["E", "F", "I", "UP", "B"]
|
||||
ignore = []
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py"]
|
||||
pythonpath = ["."]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
@@ -0,0 +1,21 @@
|
||||
fastapi==0.110.0
|
||||
uvicorn[standard]==0.27.0
|
||||
pydantic==2.6.0
|
||||
pydantic-settings==2.1.0
|
||||
sqlalchemy==2.0.25
|
||||
asyncpg==0.29.0
|
||||
alembic==1.13.0
|
||||
python-jose[cryptography]==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
python-multipart==0.0.6
|
||||
openpyxl==3.1.2
|
||||
pandas==2.2.0
|
||||
openai==1.10.0
|
||||
python-dotenv==1.0.0
|
||||
structlog==24.1.0
|
||||
pytest==7.4.4
|
||||
pytest-asyncio==0.23.4
|
||||
httpx==0.26.0
|
||||
ruff==0.1.15
|
||||
black==23.12.1
|
||||
mypy==1.8.0
|
||||
@@ -0,0 +1,12 @@
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.main import app
|
||||
|
||||
|
||||
def test_health_check() -> None:
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/api/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "ok"
|
||||
Reference in New Issue
Block a user