dev2 · 2026-08-20 · 所有落点均已物理 grep 实证 · 云机环境·非验收证据
| # | 事 | 难度 | 动库 |
|---|---|---|---|
| ① | 已提交机构列表状态不刷新(显示「已关闭」) | 中 · 真因比报障更严重 | 否 |
| ② | 进度页底部文案居中 | 低 · 有复用陷阱 | 否 |
| ③ | 「原价(500席后)」「现价(余202席)」 | 中 | 是 |
| ④ | 渠道密码改 6 位数字 | 低 · 核心侧不用动 | 否 |
| ⑤ | 摘临时清理码(长官 8-20 令捎带) | 低 | 否 |
【实测】显示逻辑早就处理了已关闭:
settlement/web/app/store/minicup/signup/page.tsx:113
if (body.store.status === "已关闭" || body.store.status === "已停用") return "已关闭";
真因在刷新那一步 —— 前端漏传后端必填字段:
后端 settlement/backend/app/api/settlement/store_signup.py:256-261(2026-08-17 手机号验证方案甲)
contact_phone = (payload.contact_phone or "").strip()
if not contact_phone:
raise HTTPException(401, detail={"code": "phone_required", ...})
前端 signup/page.tsx:249-252
body: JSON.stringify({ review_token: entry.reviewToken })
↑ 只传了 review_token,没传 contact_phone
⇒ 每次刷新必定 401 → if (!exchangeResponse.ok) return entry;(page.tsx:255)
静默保留旧值 ⇒ 状态永远停在提交那一刻。
🔴 影响面比长官报的大:不只已关闭显示错,资料通过 / 驳回 / 已开通全都不会刷新。
长官恰好挑了个已关闭的例子,看起来像关闭态的问题。
这是 2026-08-17 后端加必填守门时漏改调用方——
正是项目铁律「协议 C 第 13 项 · client/server 契约反向 grep」要防的那类。
【实测】store-progress-foot 命中 6 处,跨两端:
门店端 settlement/web/app/store/minicup/review/page.tsx:1046 ← 长官要改的
渠道端 settlement/web/app/channel/minicup/store-progress/page.tsx:625 ← 不能动
CSS settlement/web/app/channel/minicup/_components/channel-h5.css:872 / 3108 / 3319
⇒ 不能直接改那个 class 的 CSS,否则渠道端一起变。
改哪
| 文件 | 改什么 |
|---|---|
settlement/web/app/store/minicup/signup/page.tsx:249-252 |
调 review-exchange 时补传 contact_phone: entry.contactPhone |
| 同上 · 列表渲染处(约 :524) | 状态为「已关闭」时,「继续查看」按钮置灰不可点 |
为什么一行就能修:列表条目里本来就存着手机号
(【实测】page.tsx:58-65 StoreSignupResumeEntry.contactPhone,页面上也显示出来了)。
计数口径:长官定 A —— 「N 个申请」仍显示总数,已关闭的照样计入。
第一版写「401 即停」是错的。【实测】store_signup.py:775-822 真实错误码:
| 情况 | 返码 | 说明 |
|---|---|---|
| 没传手机号 | 401 phone_required |
补传后不再出现 |
| 手机号不对 | 400「手机号不正确」 | ← 真正要防的是这个,按 401 停等于白写 |
| 已被锁 | 429 | 锁 10 分钟 |
| 机构没留手机号 | 409 | 老档案,永远刷不出状态 |
风险性质更正:失败计数【实测】是 profile.review_link_failed_attempts,
按机构分别记在各自那行上,不是全局共用;且【实测】:822 成功一次即清零。
⇒ 8 条一起刷不会连撞 8 次,而是各记各的 1 次。
真实风险是:某机构手机号对不上 → 每进一次页面 +1 → 进 5 次页面就锁 10 分钟。
(第一版把风险说重了,但风险确实存在。)
采用改法:
- 收到 400 → 标记该条「状态暂不可用」,本次不再重试该机构,其余机构照常刷
- 收到 409 → 列表显示「需联系平台补录手机号」,不重试(这类机构永远刷不出来,
不能装作没事)
- 收到 429 → 已被锁,显示「稍后再试」,不重试
- 不再用 Promise.all 无差别并发重试
配套(必做):反查所有调 review-exchange 的地方,确认没有第二处漏传。
不做:不动 8-17 那套手机号验证逻辑本身(它是对的,漏的是调用方);
不放宽 REVIEW_LINK_MAX_ATTEMPTS(那是防穷举的,不能为了刷新去削弱它)。
改哪:settlement/web/app/store/minicup/review/page.tsx:1046
怎么改:不改公共 class。给门店那一处单加一个修饰类(如
store-progress-foot store-progress-foot--center),只给新类加居中。
渠道端 store-progress/page.tsx:625 一个字不动。
第一版没写清落点。【实测】门店 review 页套的就是渠道那套根样式:
settlement/web/app/store/minicup/review/page.tsx:304 / :746 / :1059
<main className="channel-h5-root">
⇒ 新的修饰类必须写进 settlement/web/app/channel/minicup/_components/channel-h5.css
(跟现有 .channel-h5-root .store-progress-foot 同一处),不是新建门店 CSS 文件。
两端共用这一份样式表是既有事实,不在本次改动范围内动它。
长官已定:
- 语义就是「前 N 席现价,之后恢复原价」,但不实现任何自动涨价逻辑,纯展示文案
- 两个数字后台手填,填多少显示多少,不做自动计算
- 没填就不显示括号那部分
- (余N席) (N席后) 中的「余…席」「…席后」是固定文案
- 只在签约第一页显示,支付页不动
改哪
| 层 | 文件 | 改什么 |
|---|---|---|
| 库 | 新建迁移 settlement/backend/migrations/versions/phase72_000_price_seat_labels.py |
service_prices 加两列 |
| 模型 | settlement/backend/app/models/settlement.py:793 ServicePrice |
加两个字段 |
| 接口 | 价格管理读写端点 | 带上这两个字段 |
| 后台 | settlement/web/app/admin/minicup/prices/page.tsx 编辑弹框 |
加两个输入框「原价席位数」「现价席位数」 |
| 门店 | settlement/web/app/store/minicup/signup/page.tsx:584 / :588 |
原价/现价后面拼括号文案 |
建议列名:original_price_seats / current_price_seats(整数,可空)
🔴 迁移必须手写,禁止 autogenerate —— 台账实跑坐实过 autogenerate 会生成
DROP 45 张表的脚本。
【实测】phase72_* 在结算迁移目录确实不存在(命中 0),最新是 phase71_000_support_qrcodes,
所以新迁移挂在 phase71 之后。
注意:【实测】signup/page.tsx:574 注释写明「只有原价是写死常量」,
现价取后端实际值。所以两个席位数都要从后端带下来,原价那侧是新增数据通路。
长官已定:方案 A —— 初始密码 6 位纯数字 + 改密规则也放宽成 6 位纯数字;
历史密码不管;只改渠道端。
我第一版方案说「核心侧一行都不用动」,只看了建号那条路,漏了重置密码那条。
【实测】渠道密码有两条产生路径,都在核心侧:
app/api/internal_settlement.py:183 建号 password = payload.password or _generate_one_time_password()
app/api/internal_settlement.py:273 重置 temporary_password = _generate_one_time_password()
建号那条结算侧能传密码进去;重置那条传不了 ——
【实测】ChannelPasswordOpPayload(:234-237)只有 op / old_password / new_password,
reset 分支没有可传入密码的字段,结算侧调用也确实没传
(channels.py:1088 core_channel_password_op(account.id, op="reset"))。
⇒ 不改核心,平台重置出来的仍是 14 位,用户拿到 14 位却被要求改成 6 位,自相矛盾。
直接改核心侧 _generate_one_time_password → 生成 6 位纯数字。
为什么安全:【实测】该函数全仓只有 2 个调用点,且都是渠道账号专用端点
(建号 + 重置),不影响席位/平台/门店任何角色。
这样结算侧连生成逻辑都不用写,建号与重置两条路一次改齐。
| 侧 | 文件 | 改什么 |
|---|---|---|
| 核心 | app/api/internal_settlement.py:86 _generate_one_time_password |
改为生成 6 位纯数字 |
| 结算 | settlement/backend/app/services/settlement/channels.py:496 validate_channel_password |
规则改为正好 6 位纯数字 |
| 前端 | settlement/web/app/channel/minicup/_components/channel-h5-shell.tsx:577 |
校验文案同步 |
| 前端 | settlement/web/app/admin/minicup/channels/page.tsx 新建账号弹框 |
按钮下加一行小字 |
⚠️ 连带影响:动了核心侧 ⇒ 这批改动会同时触发核心 test 链和结算 test 链,
验收两条链都要看(不是只看 settle-test)。
| 位置 | 文案 |
|---|---|
| 设置密码输入框 | 请设置 6 位数字密码 |
| 校验不过 | 密码必须为 6 位数字 |
| 新建账号弹框 | 系统将生成 6 位数字初始密码,首次登录后需修改 |
安全性:【实测】登录有渐进冷却(app/services/auth_service.py:58-70 ·
15 分钟窗口失败 ≥15 次 → 冷却 60s,每再失败翻倍,封顶 300s)。
按此速率爆破 6 位数字单账号平均需数年,风险可控。
保留:must_change_password=True 不动(首次登录仍强制改密)。
【实测】4 个文件、生产与 test 都还在:
settlement/backend/app/api/settlement/service_management.py:497 legacy-pending-amount 端点
settlement/backend/app/services/settlement/service_orders.py:60 LEGACY_CLEANUP_STATUSES 常量
:664 / :675 / :785-786 引用处
settlement/backend/tests/contract/settlement/test_service_order_auto_pricing.py:272-276
settlement/web/app/admin/minicup/stores/page.tsx:200 / :254 机构签约页橙色清理条
前置条件早已满足:生产「待确认金额」= 0 · 迁移已到 phase71(> phase70)。
一个功能分支 · 五件分五个 commit
↓ 一次 MR 到 test(test + settle-test 两条链自动部署)
↓ 长官在 test 上逐件验
↓ 生产发这一个 MR
为什么生产不拆开发:同一 MR 内容天然完整,不会重演 #476 那种
「依赖的东西没发过去 → cherry-pick 冲突」。test 上分件验,生产一次发。
🎉 结算部署是自动的:【实测 8-20】settle-prod run#13 与 main run#146
同一分钟触发(14:12)⇒ 合 main 会自动带结算一起部署,③ 的迁移会自动落库,
不用麻烦 root。
openapi_url=None,只能用这招)自检查出 3 处要改 + 1 处虚惊,已全部落进上面各节:
| 项 | 结论 |
|---|---|
| ④ 漏了重置密码路径 | 🔴 真漏洞 · 已改(核心侧要动 1 个函数,更正「核心不用动」的说法) |
| ① 补传会撞防穷举阈值 | 🔴 真风险 · 已改(并发改串行 + 401 即停) |
| ② 新样式加在哪没写清 | ⚠️ 落点已写死(加进 channel-h5.css) |
| ⑤ 摘码会不会让存量单没法取消 | ✅ 虚惊 · 实测两边都 0 条 |
| ① 停止条件用 401(第一版) | 🔴 写错了 · 手机号不对返的是 400 · 第二轮自检改正 |
| ① 老档案没留手机号(409) | 🔴 第一版完全没考虑 · 这类机构永远刷不出状态 · 已补 |
| ③ 改价守门会不会挡住只改席位数 | ✅ 虚惊 · 价格没变直接放行 |
⑤ 实测数据(两边都没有「待确认金额」):
test: 已完成 105 · 已取消 6 · 待付款 5
生产: 已完成 10 · 已取消 2
③ 另查一项:改席位数不会污染改价历史 ✅ ——
【实测】service_prices.py:211 写的是 if price_changed: 才写 ServicePriceChange,
只改席位数不动价格不会触发。
store-progress 页面channels.profile_status(红线 · 与机构状态是两张表两列)云机环境 · 非验收证据。