Billing implements reserve -> commit -> refund against bl_quota and bl_ledger with row-level locking and idempotency keys; the refund commits on its own transaction so a failed request cannot silently keep a user's quota. Task implements the unified task centre over the state machine. Identity issues and resolves users. Remaining domains are placeholders whose READMEs state what belongs there and what must not.
美搭 backend — 工程地基 (M1)
「美搭」智能穿搭美学助手的服务端。本仓库当前是 工程地基,不是业务实现: 骨架、横切能力、以及一条打通全链路的最小垂直切片。
设计文档是唯一事实来源,在 Windows 侧的
G:\meida\docs\: 《技术架构文档》《需求规格说明》《开发任务拆解》。本 README 只讲怎么跑起来。
现在有什么 / 现在没有什么
| 已实现 | 状态 |
|---|---|
| 仓库结构与包边界(架构 §16.1) | ✅ |
| ruff / mypy strict / import-linter / pytest / 密钥扫描 全部进 CI(§16.2) | ✅ |
| 统一响应与错误码 + 异常处理(§9.2, FR-SYS-02) | ✅ |
| 结构化日志 + 请求/追踪 ID + 日志脱敏(NFR-SEC-03) | ✅ |
| 异步 SQLAlchemy + Alembic(含 pgvector)+ 首个迁移 | ✅ |
OTel + Prometheus + /healthz /readyz /metrics |
✅ |
| LLM 网关:路由 / 故障转移 / 熔断 / 结构化输出 / Prompt 版本 / 缓存前缀纪律 / 成本归因 / 日预算硬熔断 | ✅ |
| 配额三段式 reserve → commit → refund(§7.4, FR-BILL-05) | ✅ |
统一任务中心 状态机 + Celery 四队列 + 一个贯通的 ping 任务 |
✅ |
垂直切片 POST /v1/agent/chat |
✅ |
| 刻意留白(占位) | 归属任务 |
|---|---|
| 华为账号登录、手机号验证码、双令牌撤销、游客合并、注销 | T-P1-12 ~ T-P1-16 |
| 统一合规出口(审核 / 水印 / 元数据 / 内容编号) | T-P1-22、T-P1-23(CR-05/CR-06 不可裁剪) |
| 知识库、混合检索、规则引擎、记忆系统 | M2 |
| 报告生成、换装、发型、视觉服务、抠图 | M3 / M4 |
| 智谱 GLM provider(仅结构占位) | 需先核实备案模型清单 |
| SSE 流式对话、意图识别、生成后校验 | FR-AGT-01/02/05 |
开始写业务功能前请先读 packages/README.md 与对应域的 README —— 里面写了
什么能放进去、什么绝对不能。
快速开始
前置:Python 3.12、Docker、uv。
# 1. 依赖(本仓库用 uv 管理;uv.lock 锁定全部版本)
make install
# 2. 配置。复制模板后至少填 DEEPSEEK_API_KEY 与 JWT_SECRET
cp .env.example .env
$EDITOR .env
# 3. 起本地基础设施(PostgreSQL 16 + pgvector,Redis 7)
make up
make migrate
# 4. 跑起来
make dev # API → http://localhost:8000/api/v1/docs
make worker # Celery worker(另开一个终端)
# 5. 真实链路验证:会真的调用 DeepSeek,花真钱
make smoke
包管理器:本仓库使用 uv(uv sync --extra dev,锁文件 uv.lock)。
没有 uv 时 make install 会退回 python -m venv + pip install -e ".[dev]",
但 CI 与 Docker 镜像都走 uv,因此以 uv.lock 为准。
常用命令
| 命令 | 作用 |
|---|---|
make lint |
ruff check + format check |
make typecheck |
mypy(packages/domain、packages/llm 为 strict) |
make importlint |
校验架构 §4.2 的域间依赖规则 |
make test |
上面三项 + pytest(本地质量门,与 CI 一致) |
make migrate / make makemigration m="..." |
Alembic |
make up / make down |
本地 PG + Redis |
make smoke |
真实 DeepSeek 端到端验证 |
垂直切片:POST /v1/agent/chat
一次请求依次经过:
JWT 鉴权
→ billing.reserve() 配额预扣,写 bl_ledger(status=reserved)
→ LLM 网关 场景路由 → 缓存前缀校验 → 日预算闸门 → DeepSeek
→ ob_ai_call 每次 provider 调用(含失败与重试)写一行
→ billing.commit() / refund() 成功确认扣除,失败自动退还
→ 统一响应 {code, message, data, request_id}
接口基础路径是架构 §9.1 规定的 /api/v1;/v1/* 作为等价别名同样可用
(内部脚本与部分文档用的是后者,见下方「与设计文档的偏差」)。
TOKEN=$(curl -s -XPOST localhost:8000/v1/auth/dev-token \
-H 'Content-Type: application/json' -d '{"device_key":"me"}' \
| python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["access_token"])')
curl -s -XPOST localhost:8000/v1/agent/chat \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"message":"面试穿什么?"}' | python3 -m json.tool
地基验收结果(M1 出口)
以下均在本机实跑通过,不是「应该可以」:
| 项 | 结果 |
|---|---|
make lint / make typecheck / make test |
全绿;147 unit + 3 integration,packages/llm + packages/domain 覆盖率 97% |
| import-linter | 6 条契约全部 KEPT(含 compliance / identity 两条禁止依赖) |
make up + make migrate |
8 张表 + alembic_version 建成,vector 扩展存在,配额规则已播种 |
alembic downgrade base → upgrade head |
可逆,往返成功 |
make smoke(真实 DeepSeek) |
通过,模型 deepseek-v4-flash;第二次调用 tokens_cached=256,证明缓存前缀纪律真的生效 |
| Celery 全链路 | pending → queued → running → succeeded,tk_task.result 落库 |
GET https://api.deepseek.com/v1/models 当前只返回两个模型:
deepseek-v4-flash 与 deepseek-v4-pro。默认用前者,通过 DEEPSEEK_MODEL 切换,
调用点不写死。
密钥
.env在.gitignore第一条,永远不要提交。.env.example记录每一个变量的含义,不含任何真实值。- pre-commit 与 CI 都跑密钥扫描(detect-secrets + gitleaks)。
- 生产环境的密钥来自 SSM/KMS,不来自文件(架构 §11.2)。
本地 GPU
开发机带一块 RTX 5090(32GB VRAM)。这意味着 BiRefNet 抠图(TD-07)与
RTMPose ONNX 人体关键点(TD-05/TD-06)可以完全在本地开发和调参,不需要租云
GPU。相关占位见 services/vision/ 与 services/matting/。
本仓库尚未实现它们。
与设计文档的偏差 / 文档待补
记录在此,以便回灌设计文档:
- 业务域包的位置:架构 §16.1 的目录树列出了
packages/{domain,llm,retrieval, rules,memory,compliance,shared},但 §4.1 定义了 11 个业务域。由于compliance同时出现在两处,本仓库按「业务域各自是packages/<域>/,packages/domain/只放纯逻辑」来落地。建议在 §16.1 补全域目录。 - 接口前缀:§9.1 规定
/api/v1,但多处任务描述使用/v1。本仓库以/api/v1为准,并在 ASGI 层把/v1/*重写为等价路径。建议文档统一。 bl_ledger缺status列:§6.2 的列清单没有status,但 §7.4 的三段式 明确要求reserved → committed / refunded。本仓库补了该列。ob_ai_call缺 Prompt 版本列:§8.1「可复现」要求记录 prompt 版本, §6.2 的列清单里没有。本仓库补了prompt_id/prompt_version。- 日成本熔断是「拒绝」而非「告警」:§13 写的是「超预算自动降级并告警」。
本仓库把它实现为硬闸门(非关键场景直接拒绝),因为重试风暴产生成本的速度
远快于人响应告警的速度。关键场景通过
SceneConfig.critical豁免。 - 单位成本费率是占位值:
.env.example里的LLM_COST_RATES_JSON需要用 供应商实际价目表校准,否则ob_ai_call.cost_cents只有相对意义。 .env里的 JSON 值必须用单引号包住。scripts/smoke.sh会source .env, 裸双引号会被 bash 剥掉,JSON 随即损坏且报错发生在 pydantic-settings 内部, 现场很难看出根因。模板里已经这么写了,改的时候别改回去。