交付出件箱

← 返回出件箱

五件改动 · 完整方案(待长官过目)

dev2 · 2026-08-20 · 所有落点均已物理 grep 实证 · 云机环境·非验收证据

摘要

# 事 难度 动库
① 已提交机构列表状态不刷新(显示「已关闭」) 中 · 真因比报障更严重 否
② 进度页底部文案居中 低 · 有复用陷阱 否
③ 「原价(500席后)」「现价(余202席)」 中 是
④ 渠道密码改 6 位数字 低 · 核心侧不用动 否
⑤ 摘临时清理码(长官 8-20 令捎带) 低 否

零、两个查证发现(直接改变方案)

发现 1:① 的真因不是「关闭态没映射」,是刷新链路整个是废的 🔴

【实测】显示逻辑早就处理了已关闭:

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」要防的那类。

发现 2:② 那个样式名渠道端也在用,直接改会波及渠道端 ⚠️

【实测】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 个申请」仍显示总数,已关闭的照样计入。

🔴 更正 2(第二轮自检):错误码搞错了,且风险性质与第一版描述不同

第一版写「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 文件。
两端共用这一份样式表是既有事实,不在本次改动范围内动它。


三、③ 「原价(500席后)」「现价(余202席)」

长官已定:
- 语义就是「前 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 注释写明「只有原价是写死常量」,
现价取后端实际值。所以两个席位数都要从后端带下来,原价那侧是新增数据通路。


四、④ 渠道密码改 6 位数字

长官已定:方案 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 位,自相矛盾。

采用改法(最小 diff)

直接改核心侧 _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。


七、验收方式(项目铁律:no-mock · e2e 真打)


七点五、自检结论(长官 2026-08-20 令「认真自检」)

自检查出 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,
只改席位数不动价格不会触发。


八、我不做的(避免扩大范围)


云机环境 · 非验收证据。