chore: initialize V2 backend foundation

This commit is contained in:
2026-06-22 18:19:59 +08:00
parent 3176954a1d
commit a6157fcaf8
41 changed files with 819 additions and 5 deletions
+7
View File
@@ -2,6 +2,11 @@ backend/.venv/
frontend/.next/
frontend/.next-dev-log/
frontend/node_modules/
services/*/.venv/
services/*/.pytest_cache/
apps/*/.next/
apps/*/node_modules/
.pytest_cache/
*.pyc
__pycache__/
*.egg-info/
@@ -26,6 +31,8 @@ backend/project-docs/qa-artifacts/
output/
frontend/next-env.d.ts
frontend/tsconfig.tsbuildinfo
apps/*/next-env.d.ts
apps/*/tsconfig.tsbuildinfo
# Temporary directories
temp-*/
+43
View File
@@ -0,0 +1,43 @@
# V2 Local Development
## Run Tests
```powershell
python -m pytest
```
## Run APIs Without Docker
Contract API:
```powershell
$env:PYTHONPATH='services/contract-api/src'
python -m uvicorn yuqei_contract_api.main:app --host 0.0.0.0 --port 8100
```
AI Platform API:
```powershell
$env:PYTHONPATH='services/ai-platform-api/src'
python -m uvicorn yuqei_ai_platform_api.main:app --host 0.0.0.0 --port 8101
```
Worker health:
```powershell
$env:PYTHONPATH='services/worker/src'
python -m yuqei_worker.main --health
```
## Run Infrastructure With Docker
```powershell
Copy-Item infra/compose/.env.example infra/compose/.env -ErrorAction SilentlyContinue
docker compose --env-file infra/compose/.env -f infra/compose/docker-compose.yml up --build
```
Health checks:
- Contract API: `http://localhost:8100/health`
- AI Platform API: `http://localhost:8101/health`
- MinIO Console: `http://localhost:9001`
+5
View File
@@ -0,0 +1,5 @@
POSTGRES_USER=yuqei
POSTGRES_PASSWORD=yuqei-local
POSTGRES_DB=yuqei_v2
MINIO_ROOT_USER=yuqei-minio
MINIO_ROOT_PASSWORD=yuqei-minio-local
-1
View File
@@ -1 +0,0 @@
+97
View File
@@ -0,0 +1,97 @@
name: yuqei-contract-v2
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER:-yuqei}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-yuqei-local}
POSTGRES_DB: ${POSTGRES_DB:-yuqei_v2}
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-yuqei} -d $${POSTGRES_DB:-yuqei_v2}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
minio:
image: minio/minio:latest
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-yuqei-minio}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-yuqei-minio-local}
ports:
- "9000:9000"
- "9001:9001"
volumes:
- minio-data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 10s
timeout: 5s
retries: 5
contract-api:
build:
context: ../..
dockerfile: infra/docker/contract-api.Dockerfile
environment:
CONTRACT_API_ENVIRONMENT: local
CONTRACT_API_DATABASE_URL: postgresql+psycopg://yuqei:yuqei-local@postgres:5432/yuqei_v2
CONTRACT_API_REDIS_URL: redis://redis:6379/0
CONTRACT_API_AI_PLATFORM_BASE_URL: http://ai-platform-api:8101
ports:
- "8100:8100"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
ai-platform-api:
build:
context: ../..
dockerfile: infra/docker/ai-platform-api.Dockerfile
environment:
AI_PLATFORM_ENVIRONMENT: local
AI_PLATFORM_DATABASE_URL: postgresql+psycopg://yuqei:yuqei-local@postgres:5432/yuqei_v2
AI_PLATFORM_REDIS_URL: redis://redis:6379/1
AI_PLATFORM_PROVIDER_CONFIG_SOURCE: database
ports:
- "8101:8101"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
worker:
build:
context: ../..
dockerfile: infra/docker/worker.Dockerfile
environment:
YUQEI_WORKER_ENVIRONMENT: local
YUQEI_WORKER_REDIS_URL: redis://redis:6379/2
YUQEI_WORKER_OBJECT_STORAGE_ENDPOINT: http://minio:9000
depends_on:
redis:
condition: service_healthy
minio:
condition: service_healthy
volumes:
postgres-data:
minio-data:
-1
View File
@@ -1 +0,0 @@
+15
View File
@@ -0,0 +1,15 @@
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/app/services/ai-platform-api/src
WORKDIR /app
COPY services/ai-platform-api/pyproject.toml /tmp/ai-platform-api-pyproject.toml
RUN pip install --no-cache-dir fastapi pydantic pydantic-settings sqlalchemy uvicorn
COPY services/ai-platform-api services/ai-platform-api
EXPOSE 8101
CMD ["python", "-m", "uvicorn", "yuqei_ai_platform_api.main:app", "--host", "0.0.0.0", "--port", "8101"]
+15
View File
@@ -0,0 +1,15 @@
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/app/services/contract-api/src
WORKDIR /app
COPY services/contract-api/pyproject.toml /tmp/contract-api-pyproject.toml
RUN pip install --no-cache-dir fastapi pydantic pydantic-settings sqlalchemy uvicorn
COPY services/contract-api services/contract-api
EXPOSE 8100
CMD ["python", "-m", "uvicorn", "yuqei_contract_api.main:app", "--host", "0.0.0.0", "--port", "8100"]
+14
View File
@@ -0,0 +1,14 @@
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/app/services/worker/src
WORKDIR /app
COPY services/worker/pyproject.toml /tmp/worker-pyproject.toml
RUN pip install --no-cache-dir pydantic pydantic-settings
COPY services/worker services/worker
CMD ["python", "-m", "yuqei_worker.main", "--health"]
@@ -0,0 +1,108 @@
# V2 后端基础工程与本地开发底座记录
日期:2026-06-22
## 1. 本轮目标
承接 V2 重构任务编排中的 T0-4 和 T0-6,先建立可运行、可测试、可部署演进的后端基础工程,避免直接进入业务页面和 AI 复杂能力开发。
## 2. 已完成内容
### Contract API
目录:`services/contract-api`
已建立:
- FastAPI 应用入口。
- `/health` 和 `/api/v1/health`。
- Pydantic Settings 配置加载。
- 基础日志配置。
- `.env.example`。
- 单元测试。
- 服务 README。
### AI Platform API
目录:`services/ai-platform-api`
已建立:
- FastAPI 应用入口。
- `/health` 和 `/api/v1/health`。
- Pydantic Settings 配置加载。
- 基础日志配置。
- `.env.example`。
- 单元测试。
- 服务 README。
### Worker
目录:`services/worker`
已建立:
- worker 配置加载。
- 基础日志配置。
- `--health` 健康输出。
- `.env.example`。
- 单元测试。
- 服务 README。
### 本地开发底座
已建立:
- 根目录 `pyproject.toml`,统一 pytest 入口。
- `infra/compose/docker-compose.yml`。
- `infra/compose/.env.example`。
- 三个服务 Dockerfile。
- `docs/architecture/local-development.md`。
- V2 目录相关 `.gitignore` 规则。
## 3. 验证结果
已通过:
```powershell
python -m pytest
```
结果:
```text
6 passed
```
已通过:
```powershell
git diff --check
```
说明:
- 当前机器未安装 Docker CLI,所以本轮未执行 `docker compose config` 或实际容器启动。
- Docker/Compose 文件已作为开发底座提交,后续在具备 Docker 的机器或服务器上执行启动验证。
## 4. 当前边界
本轮只建立工程底座,不包含:
- 数据库表结构和 Alembic migration。
- AI provider 真实调用。
- 法律知识库检索。
- 合同业务 API。
- 前端 Next.js 工程。
## 5. 下一步建议
继续执行 T0-5:初始化前端基础工程和设计系统底座。
优先顺序:
1. `apps/contract-web` 初始化 Next.js/React/TypeScript。
2. `packages/design-system` 建立 tokens、主导航、页面壳、工作台布局规范。
3. 建立前端本地启动、lint、typecheck、build 命令。
4. 前端健康页对接 `contract-api` 的 `/health`。
5. 再进入 S2/S3:AI Platform MVP 和 SDK MVP。
+12
View File
@@ -0,0 +1,12 @@
[tool.pytest.ini_options]
testpaths = [
"services/contract-api/tests",
"services/ai-platform-api/tests",
"services/worker/tests",
]
pythonpath = [
"services/contract-api/src",
"services/ai-platform-api/src",
"services/worker/src",
]
addopts = "-q --import-mode=importlib"
+5
View File
@@ -0,0 +1,5 @@
AI_PLATFORM_ENVIRONMENT=local
AI_PLATFORM_LOG_LEVEL=INFO
AI_PLATFORM_DATABASE_URL=postgresql+psycopg://ai:ai@localhost:5432/yuqei_ai
AI_PLATFORM_REDIS_URL=redis://localhost:6379/1
AI_PLATFORM_PROVIDER_CONFIG_SOURCE=database
-1
View File
@@ -1 +0,0 @@
+23
View File
@@ -0,0 +1,23 @@
# AI Platform API
FastAPI service for V2 shared AI capabilities.
## Local Run
```powershell
cd services/ai-platform-api
python -m uvicorn yuqei_ai_platform_api.main:app --app-dir src --reload --port 8101
```
Health endpoints:
- `GET /health`
- `GET /api/v1/health`
## Test
From repository root:
```powershell
python -m pytest services/ai-platform-api/tests
```
+16
View File
@@ -0,0 +1,16 @@
[project]
name = "yuqei-ai-platform-api"
version = "0.1.0"
description = "Yuqei shared AI platform V2 API"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115",
"pydantic>=2.10",
"pydantic-settings>=2.7",
"sqlalchemy>=2.0",
"uvicorn>=0.34",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
@@ -0,0 +1 @@
__version__ = "0.1.0"
@@ -0,0 +1,25 @@
from functools import lru_cache
from typing import Literal
from pydantic_settings import BaseSettings, SettingsConfigDict
class AiPlatformSettings(BaseSettings):
service_name: str = "ai-platform-api"
environment: Literal["local", "test", "staging", "production"] = "local"
log_level: str = "INFO"
api_prefix: str = "/api/v1"
database_url: str = "postgresql+psycopg://ai:ai@localhost:5432/yuqei_ai"
redis_url: str = "redis://localhost:6379/1"
provider_config_source: Literal["database", "environment"] = "database"
model_config = SettingsConfigDict(
env_file=".env",
env_prefix="AI_PLATFORM_",
extra="ignore",
)
@lru_cache
def get_settings() -> AiPlatformSettings:
return AiPlatformSettings()
@@ -0,0 +1,27 @@
import logging
from logging.config import dictConfig
def configure_logging(level: str) -> None:
dictConfig(
{
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "%(asctime)s %(levelname)s %(name)s %(message)s",
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "default",
}
},
"root": {
"handlers": ["console"],
"level": level.upper(),
},
}
)
logging.getLogger("uvicorn.access").setLevel(level.upper())
@@ -0,0 +1,47 @@
from typing import Literal
from fastapi import FastAPI
from pydantic import BaseModel
from yuqei_ai_platform_api import __version__
from yuqei_ai_platform_api.config import AiPlatformSettings, get_settings
from yuqei_ai_platform_api.logging import configure_logging
class HealthResponse(BaseModel):
status: Literal["ok"] = "ok"
service: str
version: str
environment: str
provider_config_source: str
def create_app(settings: AiPlatformSettings | None = None) -> FastAPI:
resolved_settings = settings or get_settings()
configure_logging(resolved_settings.log_level)
app = FastAPI(
title="Yuqei AI Platform API",
version=__version__,
docs_url="/docs",
redoc_url="/redoc",
openapi_url="/openapi.json",
)
@app.get("/health", response_model=HealthResponse, tags=["system"])
def health() -> HealthResponse:
return HealthResponse(
service=resolved_settings.service_name,
version=__version__,
environment=resolved_settings.environment,
provider_config_source=resolved_settings.provider_config_source,
)
@app.get(f"{resolved_settings.api_prefix}/health", response_model=HealthResponse, tags=["system"])
def api_health() -> HealthResponse:
return health()
return app
app = create_app()
@@ -0,0 +1 @@
"""AI Platform API test package."""
@@ -0,0 +1,30 @@
from fastapi.testclient import TestClient
from yuqei_ai_platform_api.config import AiPlatformSettings
from yuqei_ai_platform_api.main import create_app
def test_health_endpoint_returns_ai_platform_status() -> None:
app = create_app(AiPlatformSettings(environment="test"))
client = TestClient(app)
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {
"status": "ok",
"service": "ai-platform-api",
"version": "0.1.0",
"environment": "test",
"provider_config_source": "database",
}
def test_versioned_health_endpoint_matches_ai_platform_prefix() -> None:
app = create_app(AiPlatformSettings(environment="test"))
client = TestClient(app)
response = client.get("/api/v1/health")
assert response.status_code == 200
assert response.json()["service"] == "ai-platform-api"
+5
View File
@@ -0,0 +1,5 @@
CONTRACT_API_ENVIRONMENT=local
CONTRACT_API_LOG_LEVEL=INFO
CONTRACT_API_DATABASE_URL=postgresql+psycopg://contract:contract@localhost:5432/yuqei_contract
CONTRACT_API_REDIS_URL=redis://localhost:6379/0
CONTRACT_API_AI_PLATFORM_BASE_URL=http://localhost:8101
-1
View File
@@ -1 +0,0 @@
+23
View File
@@ -0,0 +1,23 @@
# Contract API
FastAPI service for V2 contract workflows.
## Local Run
```powershell
cd services/contract-api
python -m uvicorn yuqei_contract_api.main:app --app-dir src --reload --port 8100
```
Health endpoints:
- `GET /health`
- `GET /api/v1/health`
## Test
From repository root:
```powershell
python -m pytest services/contract-api/tests
```
+16
View File
@@ -0,0 +1,16 @@
[project]
name = "yuqei-contract-api"
version = "0.1.0"
description = "Yuqei AI Contract Platform V2 contract API"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115",
"pydantic>=2.10",
"pydantic-settings>=2.7",
"sqlalchemy>=2.0",
"uvicorn>=0.34",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
@@ -0,0 +1 @@
__version__ = "0.1.0"
@@ -0,0 +1,25 @@
from functools import lru_cache
from typing import Literal
from pydantic_settings import BaseSettings, SettingsConfigDict
class ContractApiSettings(BaseSettings):
service_name: str = "contract-api"
environment: Literal["local", "test", "staging", "production"] = "local"
log_level: str = "INFO"
api_prefix: str = "/api/v1"
database_url: str = "postgresql+psycopg://contract:contract@localhost:5432/yuqei_contract"
redis_url: str = "redis://localhost:6379/0"
ai_platform_base_url: str = "http://localhost:8101"
model_config = SettingsConfigDict(
env_file=".env",
env_prefix="CONTRACT_API_",
extra="ignore",
)
@lru_cache
def get_settings() -> ContractApiSettings:
return ContractApiSettings()
@@ -0,0 +1,27 @@
import logging
from logging.config import dictConfig
def configure_logging(level: str) -> None:
dictConfig(
{
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "%(asctime)s %(levelname)s %(name)s %(message)s",
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "default",
}
},
"root": {
"handlers": ["console"],
"level": level.upper(),
},
}
)
logging.getLogger("uvicorn.access").setLevel(level.upper())
@@ -0,0 +1,45 @@
from typing import Literal
from fastapi import FastAPI
from pydantic import BaseModel
from yuqei_contract_api import __version__
from yuqei_contract_api.config import ContractApiSettings, get_settings
from yuqei_contract_api.logging import configure_logging
class HealthResponse(BaseModel):
status: Literal["ok"] = "ok"
service: str
version: str
environment: str
def create_app(settings: ContractApiSettings | None = None) -> FastAPI:
resolved_settings = settings or get_settings()
configure_logging(resolved_settings.log_level)
app = FastAPI(
title="Yuqei Contract API",
version=__version__,
docs_url="/docs",
redoc_url="/redoc",
openapi_url="/openapi.json",
)
@app.get("/health", response_model=HealthResponse, tags=["system"])
def health() -> HealthResponse:
return HealthResponse(
service=resolved_settings.service_name,
version=__version__,
environment=resolved_settings.environment,
)
@app.get(f"{resolved_settings.api_prefix}/health", response_model=HealthResponse, tags=["system"])
def api_health() -> HealthResponse:
return health()
return app
app = create_app()
+1
View File
@@ -0,0 +1 @@
"""Contract API test package."""
@@ -0,0 +1,29 @@
from fastapi.testclient import TestClient
from yuqei_contract_api.config import ContractApiSettings
from yuqei_contract_api.main import create_app
def test_health_endpoint_returns_contract_api_status() -> None:
app = create_app(ContractApiSettings(environment="test"))
client = TestClient(app)
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {
"status": "ok",
"service": "contract-api",
"version": "0.1.0",
"environment": "test",
}
def test_versioned_health_endpoint_matches_contract_api_prefix() -> None:
app = create_app(ContractApiSettings(environment="test"))
client = TestClient(app)
response = client.get("/api/v1/health")
assert response.status_code == 200
assert response.json()["service"] == "contract-api"
+5
View File
@@ -0,0 +1,5 @@
YUQEI_WORKER_ENVIRONMENT=local
YUQEI_WORKER_LOG_LEVEL=INFO
YUQEI_WORKER_REDIS_URL=redis://localhost:6379/2
YUQEI_WORKER_QUEUE_NAME=yuqei-v2
YUQEI_WORKER_OBJECT_STORAGE_ENDPOINT=http://localhost:9000
-1
View File
@@ -1 +0,0 @@
+27
View File
@@ -0,0 +1,27 @@
# Worker
Python worker process for V2 background tasks.
Initial responsibilities:
- Document extraction
- Knowledge indexing
- Export generation
- Reminder jobs
- Long-running AI jobs
## Local Health Check
```powershell
cd services/worker
$env:PYTHONPATH='src'
python -m yuqei_worker.main --health
```
## Test
From repository root:
```powershell
python -m pytest services/worker/tests
```
+13
View File
@@ -0,0 +1,13 @@
[project]
name = "yuqei-worker"
version = "0.1.0"
description = "Yuqei AI Contract Platform V2 worker"
requires-python = ">=3.12"
dependencies = [
"pydantic>=2.10",
"pydantic-settings>=2.7",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
@@ -0,0 +1 @@
__version__ = "0.1.0"
@@ -0,0 +1,24 @@
from functools import lru_cache
from typing import Literal
from pydantic_settings import BaseSettings, SettingsConfigDict
class WorkerSettings(BaseSettings):
service_name: str = "worker"
environment: Literal["local", "test", "staging", "production"] = "local"
log_level: str = "INFO"
redis_url: str = "redis://localhost:6379/2"
queue_name: str = "yuqei-v2"
object_storage_endpoint: str = "http://localhost:9000"
model_config = SettingsConfigDict(
env_file=".env",
env_prefix="YUQEI_WORKER_",
extra="ignore",
)
@lru_cache
def get_settings() -> WorkerSettings:
return WorkerSettings()
@@ -0,0 +1,26 @@
import logging
from logging.config import dictConfig
def configure_logging(level: str) -> None:
dictConfig(
{
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "%(asctime)s %(levelname)s %(name)s %(message)s",
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "default",
}
},
"root": {
"handlers": ["console"],
"level": level.upper(),
},
}
)
+45
View File
@@ -0,0 +1,45 @@
import argparse
import json
from typing import Literal, TypedDict
from yuqei_worker import __version__
from yuqei_worker.config import WorkerSettings, get_settings
from yuqei_worker.logging import configure_logging
class WorkerHealth(TypedDict):
status: Literal["ok"]
service: str
version: str
environment: str
queue_name: str
def health_snapshot(settings: WorkerSettings | None = None) -> WorkerHealth:
resolved_settings = settings or get_settings()
return {
"status": "ok",
"service": resolved_settings.service_name,
"version": __version__,
"environment": resolved_settings.environment,
"queue_name": resolved_settings.queue_name,
}
def main() -> None:
parser = argparse.ArgumentParser(description="Yuqei V2 worker")
parser.add_argument("--health", action="store_true", help="Print worker health JSON and exit.")
args = parser.parse_args()
settings = get_settings()
configure_logging(settings.log_level)
if args.health:
print(json.dumps(health_snapshot(settings), ensure_ascii=False))
return
raise SystemExit("Worker loop is not implemented yet. Use --health for the current V2 baseline.")
if __name__ == "__main__":
main()
+1
View File
@@ -0,0 +1 @@
"""Worker test package."""
+14
View File
@@ -0,0 +1,14 @@
from yuqei_worker.config import WorkerSettings
from yuqei_worker.main import health_snapshot
def test_worker_health_snapshot_returns_worker_status() -> None:
health = health_snapshot(WorkerSettings(environment="test", queue_name="test-queue"))
assert health == {
"status": "ok",
"service": "worker",
"version": "0.1.0",
"environment": "test",
"queue_name": "test-queue",
}