feat(server): 工程骨架后端——上传 → 解析 → 清单最小链路

- core:配置(prod 拒绝默认密钥 / dev 短信 / 假模型)、租户事务 + 行级安全策略、
  problem+json 错误码、信封加密与签名下载、对象存储、事务内入队、Valkey stream 事件流、权限点
- identity:手机号 HMAC 存储、注册、登录(挤下线需确认,API3)、服务端会话
- billing:额度流水只追加,预扣 / 结算 / 退回,一笔预扣只有一个结局
- document:分片上传、按文件头识别格式、Gotenberg 转换、文字层与稳定句子编号
- checklist:标注规则识别、模型抽取、三道校验、标注类条款规则兜底、重跑对齐
- ai:提示词注册表(extract.substantive v1 来自 S4 v2)、假模型、OpenAI 兼容供应商、ai_call 流水
- 迁移:data-model 基线、套餐、Procrastinate、会话状态函数
- 测试 30 个:租户隔离、额度、失败退回、对象加密、契约一致性
- Compose(含 migrate)、镜像、CI

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
YANG JIANKUAN
2026-11-03 18:48:10 +08:00
parent 5747a523bb
commit ad7fba6021
106 changed files with 8094 additions and 0 deletions

91
server/pyproject.toml Normal file
View File

@@ -0,0 +1,91 @@
[project]
name = "bid-doctor"
version = "0.1.0"
description = "标博士服务端:业务接口(api)和后台任务(worker)是同一套代码、两种进程"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115",
"uvicorn[standard]>=0.32",
"pydantic>=2.9",
"pydantic-settings>=2.6",
"sqlalchemy[asyncio]>=2.0.36",
"psycopg[binary,pool]>=3.2",
"alembic>=1.14",
"procrastinate>=3.10,<4", # core/queue.py 直接调用它的 SQL 函数,升级大版本前要核对
"redis>=5.2",
"httpx>=0.28",
"pypdfium2>=4.30",
"pdfplumber>=0.11",
"python-multipart>=0.0.18",
"cryptography>=44",
]
[dependency-groups]
dev = [
"pytest>=8.3",
"pytest-asyncio>=0.25",
"ruff>=0.8",
"pyright>=1.1.390",
"import-linter>=2.1",
"pyyaml>=6",
]
[project.scripts]
bid-doctor = "bid_doctor.cli:main"
[build-system]
requires = ["hatchling>=1.26"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/bid_doctor"]
[tool.ruff]
line-length = 120
target-version = "py312"
src = ["src", "tests"]
[tool.ruff.lint]
select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF", "ASYNC", "S", "BLE", "PT"]
ignore = [
"RUF001", "RUF002", "RUF003", # 中文全角标点是有意的
"S101", # assert 用来表达不变量
]
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S", "PT011", "PT018"]
"migrations/**" = ["E402"]
[tool.pyright]
include = ["src"]
pythonVersion = "3.12"
venvPath = ".."
venv = ".venv"
typeCheckingMode = "strict"
reportMissingTypeStubs = false
reportUnknownMemberType = false
reportUnknownVariableType = false
reportUnknownArgumentType = false
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "session"
asyncio_default_test_loop_scope = "session"
testpaths = ["tests"]
pythonpath = ["."]
[tool.importlinter]
root_package = "bid_doctor"
[[tool.importlinter.contracts]]
name = "core 不依赖业务模块"
type = "forbidden"
source_modules = ["bid_doctor.core"]
forbidden_modules = ["bid_doctor.identity", "bid_doctor.billing", "bid_doctor.project", "bid_doctor.document",
"bid_doctor.checklist", "bid_doctor.ai", "bid_doctor.audit", "bid_doctor.notification"]
[[tool.importlinter.contracts]]
name = "业务模块不依赖 api 层的组合模块"
type = "forbidden"
source_modules = ["bid_doctor.document", "bid_doctor.checklist", "bid_doctor.billing", "bid_doctor.identity.service"]
forbidden_modules = ["bid_doctor.project.workbench", "bid_doctor.app"]