听码 WDE 外派任务单 · MFR-Open v2 路由族注册 + CORS 白名单补齐¶
| 字段 | 值 |
|---|---|
| 任务编号 | WDE-TASK-L-20260827-01 |
| 外派方 | 听写 DT(依据用户"继续"指令签发) |
| 执行方 | 听码 WDE(dip1-backend 代码侧) |
| 签发时间 | 2026-08-27 |
| 关联日志 | OCL L-20260827-04(B1+B2 部署闭环)、DTL L-20260827-02(§九 回执:D01-4 阻塞项) |
| 关联契约 | docs/技术文档/openapi/qsv.yaml §/api/v2/qsv/quote/{quote_id}/mfr-open(L261-L282,Tag: MFR-OpenQuote,operationId=qsv.mfr.openquote) |
| 三 AI 合规声明 | 本任务单由听写签发,仅通过文档通道外派;听码在本地 dip1 代码库改动并交付构建产物;听云负责生产重建/重启与断言复核 |
一、任务名称¶
W1:dip1-backend 生产镜像注册 v2 qsv 路由族(含 mfr-open)+ W2:CORS origins 白名单允许生产前端 Origin
二、问题现象(听云已实测取证,无需复测)¶
生产 ECS = 8.218.165.94(dip1-prod-api docker 容器,127.0.0.1:8000,/health 报 version=2.0.0 db=ok)。2026-08-27 晚探测证据:
| # | 探测 | 结果 |
|---|---|---|
| E1 | GET /openapi.json → 遍历 paths |
共 64 条路径,全部 v1(auth / qsv calculator+quotes / cpt orders / mds…);0 条含 "mfr",且未见任何 /api/v2/* 路径 |
| E2 | POST /api/v2/qsv/quote/mfr-open(外派 D01 原口径) |
404 {"detail":"Not Found"} |
| E3 | POST /api/v2/qsv/quote/{quote_id}/mfr-open(qsv.yaml 契约口径,quote_id=DEMO 占位) |
404(路由整体缺失,非参数校验 422) |
| E4 | Caddy 层判定 | Caddy /api/* → :8000 反代正常(E2/E3 的 404 来自 FastAPI 本身,body 为 FastAPI 标准 Not Found),非网关问题 |
| E5 | CORS 预检 | OPTIONS /api/v2/qsv/quote/mfr-open + Origin=http://8.218.165.94 → 400 Disallowed CORS origin(CORS 中间件存在但白名单未含该 Origin) |
结论:生产镜像 v2.0.0 未注册 v2 qsv 路由族(或整个 v2 router 未挂载),mfr-open 为前端 MFR Portal 已上线页面的核心依赖端点;契约已在 qsv.yaml 定义但代码未进入该镜像。
参考旁证:112.74.108.189 的 MFR V1 standalone 部署曾跑通同一业务(final_amount=1620 HKD 冒烟通过,OCL L-20260823-02),说明 mfr-open 业务代码存在于仓库历史中,属"未打进 8.218 镜像/未挂载",非从零开发。
三、WBS 分解(2 个子任务,按顺序 W1 → W2)¶
W1 · 注册 v2 qsv 路由族(mfr-open 必达)¶
- 定位仓库内 v2 qsv router(qsv.yaml 对应实现):确认
app/api/v2/...或等价模块是否存在于dip1/backend - 若存在但未挂载 → 在 app factory 中 include 该 router(prefix=/api/v2)
- 若不存在 → 按 qsv.yaml 实现最小闭环:
- 路径
POST /api/v2/qsv/quote/{quote_id}/mfr-open,返回 201 + QuoteResult - 必需头:
ppl_id(格式^PPL-\d{8}-\d{4}$)+ requestIdHeader - 业务要点(MDS PPL 表联动):PPL 品牌溢价 1.2/1.4 自动写入 P8.brand_multiplier;MFR 批量≥5 折扣 P7 自动应用;ppl_id 缺失应沿用 L-20260823-05 的热修复口径(缺省兜底而非 422 硬报错,除非契约显式 required——以 yaml 为准并在 PR 描述注明取舍)
- 路径
- 保持 v1 路由零回归:v1 的 64 条路径不得因挂载变更受影响
- 版本号建议升为 2.1.0(或按团队规范),CHANGELOG 记录本次挂载范围
W2 · CORS origins 白名单补齐¶
在 backend CORS 配置(环境变量或 settings)追加:
http://8.218.165.94 (当前公网 IP 形态,MFR 页面同源调用经 Caddy /api/* 时也带此 Origin)
http://localhost:3000 (本地联调,如已有可忽略)
说明:MFR Portal 经 Caddy 同源反代时请求头默认不带跨域 Origin,仅
OPTIONS预检场景触发白名单校验;追加 IP 形态是为覆盖直接从http://8.218.165.94/mfr/*页面发出的 fetch。域名形态(正式域名上线后)由后续变更补充。
四、验收标准(5 条断言,全部 ✅ 才算完成)¶
断言在 8.218.165.94 上执行(由听云在收到代码包并重部署后复核;听码可在本地 pytest 先行自证 A3/A4)。
| # | 所属子任务 | 断言命令 | 预期输出 | 权重 |
|---|---|---|---|---|
| A1 | W1 | curl -s http://127.0.0.1:8000/openapi.json \| python3 -c "import json,sys;s=json.load(sys.stdin);print([p for p in s['paths'] if 'mfr' in p])" |
输出包含 /api/v2/qsv/quote/{{quote_id}}/mfr-open 且 method 含 post |
必过 |
| A2 | W2 | curl -s -o /dev/null -w "%{http_code}" -X OPTIONS -H "Origin: http://8.218.165.94" -H "Access-Control-Request-Method: POST" http://127.0.0.1/api/v2/qsv/quote/x/mfr-open |
200 且响应头含 Access-Control-Allow-Origin 匹配 |
必过 |
| A3 | W1 | 带 ppl_id: PPL-20260823-0001 头 + 最小 QuoteParams32 body POST → 状态码 |
201 且 body 含 final_amount(金额合理性由听写 D01-4 业务核验) |
必过 |
| A4 | W1 回归 | curl -s http://127.0.0.1:8000/health + v1 抽样 3 条 |
/health 不变(ok/db=ok);v1 抽样状态码与变更前一致 |
必过 |
| A5 | W1 | 仓库 pytest 相关用例 | 新增/既有 qsv-v2 用例全绿(269~272 基线不回退) | 必过 |
五、协作与督办机制¶
听码收到本单
│
▼
本地实现 W1+W2 → 自跑 pytest + openapi 本地自检(A1/A3/A5 本地版)
│
▼
交付物打包(对齐 L-20260823-03 先例)→ 推送到听云暂存区,回传清单:
① 变更文件清单 + diff 摘要(含新版本号)
② 本地自检输出(pytest 汇总行 + openapi grep 行)
③ cors origins 目标值说明
│
▼
听云接收 → 8.218 重部署 dip1-prod-api(docker rebuild/restart,先备份 .env.prod)
→ 复核 A1~A4 → 回填本单 §六 回执 → 通知听写启动 D01 全量 4 项
│
▼
D01 全过 → 听写启动 D02 全量复测 8 项 → Phase 4 云端验证闭环
阻塞升级:任一环节卡住 >30 分钟,当事 AI 回传错误原文至协作通道,听写介入协调;不得静默等待。
六、执行确认回执(听码完成后填写,听云补 A1~A4 复核列)¶
🟢 听码已于 2026-08-27 深夜认领并完成本地侧全部工作,以下为实填回执:
| 项 | 听码填写(实填) |
|---|---|
| 开始时间 | 2026-08-27 21:05 后(接 @听码 认领指令) |
| 结束时间 | 2026-08-27 21:40 前后(打包+归档完毕,详见 WDL L-20260827-01) |
| 变更范围摘要 | 仅 1 文件:dip1/backend/app/core/config.py cors_origins 默认值追加 http://8.218.165.94 并更新注释(W2)。W1 无需改码——HEAD 已实现齐备(E6 复核属实:qsv_router mfr-open + 配套测试均在),属预置包未上机问题 |
| 本地自检结果(A3/A5 本地版) | A5 全量 pytest 309 passed / 0 failed(85.06s;目标面 test_qsv_quote+test_mfr_b1b2b3+test_mfr_open_quote_p10 共 31 passed)。A1 本地 openapi total=186、v2 族 182 条、mfr 族 7 条(含 /api/v2/qsv/quote/mfr-open post ✅)。A2 本地版 PASS:TestClient OPTIONS Origin=http://8.218.165.94 → 200 + access-control-allow-origin=http://8.218.165.94(W2 生效实证)。A3 本地版 PASS:make_dev_token(MFR-ADM)+ppl_id=PPL-20260823-0001 冒烟 POST → 201 final_amount=720.00 HKD(P8 品牌溢价 1.2 联动正确) |
| 交付物位置(暂存区路径/tar 包名+SHA256) | dip1/backend/dist/dip1-backend-head-20260827-2127.tar.gz / SHA256 a60f34a5b0148d5afbd60ffad04c99675d4b47bf2d13a17ed8d7205b7acd09de / 319,235 bytes / 178 文件条目 / 污染扫描 CLEAN(无 pycache/.env/data 泄入)。此包已含 W2 CORS 改动,取代 §7.4 预置包(a38259…bc74 为纯 HEAD 快照不含 W2,请勿使用)*;待听写转存听云暂存区 ocm/private/deploy/ 后按 R2 重部署 |
| 听云复核 A1~A4(重部署后) | 2026-08-27 22:20~23:10 听云代执行重部署并复核(见下说明):A1 ✅ openapi 186 条、POST /api/v2/qsv/quote/mfr-open 在列(v2 mfr 家族 7 条齐备);A2 ✅ OPTIONS+生产 Origin → 200 + access-control-allow-origin: http://8.218.165.94;A4 ✅* /health={status:ok,modules:[cpt,dis,mds,ops,qsv]};A3 🟡 blocked@听码(见下方遗留移交 1) |
复核说明(听云 2026-08-27 深夜):听云部署使用的是 §7.4 预置包
…-2056.tar.gz(a38259…,纯 HEAD 无 W2 代码改动),W2 由服务器端docker-compose.override.yml注入DIP1_CORS_ORIGINS(值含公网 IP)等价达成,A2 实测通过与包版本无关。听码 2127 包(W2 代码固化版)登记为 A3 修复后的标准替换包,届时一次替换到位(W2 固化 + A3 修复),今晚不再二次换包以控制风险。
遗留移交(2026-08-27 深夜复核后新增):
1. @听码(A3 blocked)→ ✅ 已修复(2026-08-27 深夜第二轮):mfr-open 全链路已通到引擎层——dev token 200 → JWT 鉴权过 → QuoteParams32 校验过 → 报价引擎全通(P1_BASE=600 → P5_TRANS=120 → P6_TIME=720 → P7_DISC=720 → CAP=50000 → FINAL=720.00),卡持久化:IntegrityError(ForeignKeyViolation) INSERT INTO qsv_snapshots … fk_qsv_snapshots_quote_id_qsv_quotes — Key (quote_id)=(QTE-35b09f90cd2a44aea64bceed) is not present in table qsv_quotes。定性:quotes 与 snapshots 两次 INSERT 未在同一 flush/commit 单元(sqlite 测试 FK 不强制故绿,asyncpg 生产严格暴露)。修复完成(3 代码文件 + 1 测试基建):
- app/application/qsv/services.py _persist_version 重排:主表 save(含 flush)先于 snapshots.insert
- app/infrastructure/db/repositories/dis.py SqlDispatchOrderRepository.save:新建分支主表 add 后立即 flush(同款缺陷,FK 强制后暴露 14 例全中此点)
- app/application/cpt/services.py create_lead_from_mfr_quote:_merge_collect 前先 projects.save 预落主表(autoflush 时序缺陷)
- tests/conftest.py:sqlite 测试启用 PRAGMA foreign_keys=ON(per-connection event),309 用例此后真实暴露 FK 类缺陷
- 验证:FK 强制下全量 309 passed / 0 failed;TestClient 冒烟 mfr-open=201(FINAL=720.00)+ 快照回读 200(10 步骤明细)持久化闭环 VERIFIED
- 标准替换包升级为 dip1-backend-head-20260827-2356.tar.gz(SHA256 9314740f16ebb0aff45e8a7ae46c28d2bdf4dfdf42643142014ca9222cd08b46,320,202 B,178 条目,CLEAN;= W2 固化 + FK 三处修复),已由听写转存听云暂存区;2127 包(仅 W2)随之作废
2. @听写(v1 断层决策)→ ✅ 已决策(用户选 C):双实例并存——旧 rollback 镜像回 :8000 服务 v1 存量(OPR 复活),新基线挪 :8010 服务 v2/MFR,Caddy /api/v2/* 前缀分流;执行单=OCL-DEPLOY-20260827-05
七、侦察附录(听云 2026-08-27 晚补充 · ⚠️ 任务性质升级)¶
听云进入 dip1-backend 代码侧 + 远程比对后的结论:本任务不是"补挂载/补实现",而是生产后端基线替换。§三原 WBS 中"W1 若存在但未挂载 → include router"路径已被证伪,请以本附录为准执行。
7.1 关键证据(E6~E9)¶
| # | 探测 | 结果 |
|---|---|---|
| E6 | 本地仓库 dip1/backend/app/interfaces/api/qsv_router.py:131 |
✅ 存在 POST /api/v2/qsv/quote/mfr-open(status_code=201,角色 MFR-ADM/MFR-USR/OL/BO,ppl_id 可选头)——实现齐备,与外派 D01 原口径一致;配套测试 4 套(tests/api/test_qsv_quote.py、test_mfr_b1b2b3.py、tests/integration/test_mfr_open_quote_p10.py、run_dev_healthcheck.py) |
| E7 | 远程 /opt/weavely/src/dip1/backend/app/interfaces/api/ |
❌ qsv_router.py 文件不存在(grep 计数 0);另有本地 HEAD 没有的 app/interfaces/api/health.py(返回 {service:"weavely-dip1-api", version, env, db}——即生产 /health 形态来源) |
| E8 | 镜像溯源 | weavely/dip1-api:prod(ID 9946d781c24b)created 2026-08-15 16:36 CST;compose build context=./src/dip1/backend;容器 started=2026-08-15 |
| E9 | 配置键名体系 | 远程 .env.prod 为大写键(APP_SECRET_KEY / JWT_ALGORITHM / CORS_ORIGINS / SERVER_IP…);本地 HEAD 为 pydantic-settings 小写系(database_url / cors_origins / keycloak_url…)——两套配置模型亦不同源 |
7.2 结论与执行口径修订¶
生产机 8.218 的后端是 2026-08-15 从独立最小基线构建的另一套代码(v1-only 64 路由);dip1 仓库 HEAD(含全部 v2 路由族、mfr-open、alembic 0002)从未上过 8.218。修订执行口径:
R1 代码包替换(替代原 W1 第 1 步)
听码产出交付物时打包范围 = dip1/backend 全目录(app/ tests/ alembic/ pyproject.toml alembic.ini)
排除 data/dip1.db(保留生产数据)与任何 .local / .env*
R2 远程落地方式
备份 /opt/weavely/src/dip1/backend → 清空上传新包 → docker compose build api → up -d api
旧镜像保留 tag 作为回滚锚点(weavely/dip1-api:rollback-20260827)
R3 环境变量迁移映射表(听码出对照清单,听云落地):
DATABASE_URL ← 由 DB_PASSWORD/SERVER_IP 拼 postgresql+asyncpg://...
CORS_ORIGINS(同名字段兼容)追加 http://8.218.165.94
KEYCLOAK_URL 缺省空 → dev 模式(HS256),DIP1_JWT_SECRET 沿用/生成,
使 A3 断言可在服务端用 make_dev_token(["MFR-ADM"]) 自产 token 完成闭环;
正式 Keycloak 接入不在本单范围
R4 数据库
首次启动前 alembic upgrade head(0001/0002);生产 PG 现有数据兼容性待听码评估,
若 schema 冲突 → 回滚镜像并回报
7.3 升级决策请求(呈用户)¶
| 选项 | 内容 | 影响 |
|---|---|---|
| 甲 | 按 §7.2 基线替换:dip1 HEAD 全面接管 8.218 后端 | v1 既有调用方语义需回归验证(A4 抽样),收益=v2 全家桶一次到位 |
| 乙 | 仅打包 qsv v2 最小子集嫁接到旧基线(不推荐) | 双代码线并存,维护债放大,mfr-open 单点先通但后续每次发布都冲突 |
7.4 决策记录与听云 R1 预置(2026-08-27 晚)¶
用户决策:甲 —— dip1 HEAD 全面接管 8.218 后端(v2 全家桶一次到位)。
听云已按 §7.2/R1 完成打包预置,听码认领后可跳过第 1 步直接进入自检与交付清单回填:
| 字段 | 值 |
|---|---|
| 预置包路径 | ocm/private/deploy/dip1-backend-head-20260827-2056.tar.gz |
| SHA256 | a3825957fab7bce170415fe16ed74849d122e6eddf98331da48ca3c4a677bc74 |
| 大小 / 条目 | 318,087 bytes / 216 条目 |
| 内容面(顶层) | app/ tests/ alembic/ alembic.ini pyproject.toml README.md run_dev*.py/.cmd stop_dev.cmd |
| 已排除 | .venv venv data/(含本地 dip1.db 与 seed JSON)backend.log 全部 .env*(含 example)缓存类目录 |
| 完整性校验 | 敏感/垃圾文件扫描 clean;7 个关键文件抽查全部 OK(main.py / qsv_router.py / config.py / security.py / pyproject.toml / alembic.ini / test_mfr_open_quote_p10.py) |
注:若听码开工时仓库 HEAD 相对此包又前进了若干提交,允许重新打包并以新 SHA256 覆盖本表登记;替换时在 §六回执注明新旧包名。
— 附录完 —
八、听码交付 · R3 环境变量迁移映射对照清单(2026-08-27 听码实出 · 交听云落地)¶
基线事实(对应 E9):远程
.env.prod为大写键自由命名系;新基线经 pydantic-settings 读取,规则=env_prefix="DIP1_"+env_file=".env"(backend 工作目录)+extra="ignore"。 关键好消息:extra="ignore"意味着旧 .env.prod 中所有不被下表映射的键不会导致启动失败,仅被静默忽略——无需逐键清理。
8.1 迁移映射表¶
| # | 远程旧键/现状 | 新基线读取键 | 迁移动作 | 说明 |
|---|---|---|---|---|
| M1 | APP_SECRET_KEY |
DIP1_JWT_SECRET |
迁移值 | HS256 验签密钥。dev-auth 模式(M5)必须 ≥32 字节,否则 pyjwt 发 InsecureKeyLengthWarning 且强度不足 |
| M2 | JWT_ALGORITHM |
DIP1_JWT_ALGORITHM |
可不配 | 新基线默认即 HS256 |
| M3 | CORS_ORIGINS |
DIP1_CORS_ORIGINS |
建议不配 | W2 已将 http://8.218.165.94 写入代码默认值(连同 localhost:3000/3001);显式配置会整体覆盖默认值,勿遗漏 IP 形态。域名形态 *.weavely.hk 上线时再统一配置 |
| M4 | SERVER_IP + DB 口令 → 拼 URL |
DIP1_DATABASE_URL |
必须迁移拼接 | 格式 postgresql+asyncpg://<user>:<password>@<host>:5432/<dbname>;driver 段 +asyncpg 必写(漏写会回退同步 psycopg 导致启动失败)。DB 主机名按容器网络实际可达地址填写 |
| M5 | Keycloak 相关(远程无) | DIP1_KEYCLOAK_URL / DIP1_KEYCLOAK_REALM |
留空 | 空=dev-auth 模式:自动挂载 dev_router,A3 断言用 make_dev_token(["MFR-ADM"]) 自产 token 即可闭环;正式 Keycloak 接入不在本单范围 |
| M6 | (远程如有)Redis/NATS | DIP1_REDIS_URL / DIP1_NATS_URL |
可留空 | 空=进程内缓存 / 进程内事件总线(D01 §十一 向后兼容行为) |
| M7 | 旧 app/interfaces/api/health.py |
— | 随包替换消失 | 新基线由 app/main.py 原生 /health 接管;如输出字段形态不同属预期,重部署后以 A4 断言为准 |
8.2 落地注意事项¶
.env文件位置必须是 docker build context 下 backend 工作目录(compose context=./src/dip1/backend),或直接改用 composeenvironment:注入DIP1_*变量(推荐,避免镜像层缓存吞掉配置变更)- R2 重部署顺序强制项:备份
.env.prod→ 解包覆盖代码 → 配置注入 →alembic upgrade head(0001→0002)→ build api → up -d api - 数据库 schema 若与生产 PG 现状冲突(R4):立即回滚镜像
weavely/dip1-api:rollback-20260827并回报听写,不得带病续跑 - 验收断言 A1~A4 全过后再行 D02 全量复测;v1 存量调用方抽样 3 条须与变更前状态一致
— §八 完 —