2026-09-23-lsy-onboarding-B-交付说明.md 46 KB

交付说明 —— 批次三-B「乐收赢进件」(定稿)

日期 2026-09-23 | 分支 java | 未 push | BASE 663156f → HEAD 5290d84 (BASE→2663020 是 39 提交 / 39 文件 / +11109 −27;其后又落了本件 §1.2 后记、§1.3 后记与 §六-3 那三笔 只改注释/SQL 记录的提交,以及 5290d84 的 R-7b 代码修复 —— 各自的行数见对应小节,本行不汇总。 ★ 上面那个「39 提交 / +11109 −27」是 2663020 时点的读数,不是 HEAD 的读数。)

本件位置说明:本件故意放在 .claude/plan/,不放在 SDD 工作区 (.superpowers/sdd/2026-09-23-lsy-onboarding-B/)内 —— 工作区是 git-ignored 的临时件, 将来可能被清掉,而本件是要交给用户执行/裁定的东西,必须能活下来。 工作区里的 task-10-delivery-note.md 是草稿,本件是它 的订正与定稿。

★ 读数口径:凡标「我量」的,是我 2026-09-23 当天当场复量的; 凡引自其它件的,标出出处。本件不含任何我没量过的数。


一、★ 三个交付 SQL 的当前状态(我量,只读会话)

# 文件(java/src/main/resources/db/) 状态 依据
1 lsy-onboarding-menu.sql ✅ 已执行,且逐格复核通过 见 §二
2 lsy-merchant-mch-no-uk.sql ✅ 已执行(2026-09-23,见 §1.2 后记) 原读数(当时为真):uk_lsy_merchant_mch_no 在 pg_indexes 0 行;表上现有 4 个索引无一是 mch_no 唯一索引
3 lsy-review-status-comment.sql ✅ 已执行(2026-09-23,见 §1.3 后记) 原读数(当时为真):review_status 的列注释仍是旧文案「A 阶段默认 APPROVED;B 阶段由进件写入口与上游审核通知回写」,不含 PENDING/APPROVED/REJECTED

★ 状态 2、3 是今天新量的,此前无人量过(草稿只写了「需要执行」,没写「执行没执行」)。 ★ 状态 3 的判据是看注释内容、不是看列在不在(列与注释一直都在,只差「改了没改」);配了「故意写错的列名 ⇒ 0 行」的反对照。

1.2 SQL 2 的前置(我量,执行前必读)

  • pay_lsy_merchant 全表只有 2 行(正对照:非空 ⇒ 上面的 0 是真没有)。
  • mch_no='Y52211090004' 有 2 行,tenant_id 都是 2 ⇒ CREATE UNIQUE INDEX 必然失败。 脚本自带守闸(把失败换成一句人话),但它不替你删任何一行。
  • ★ 这不是卫生问题,是功能性阻塞:tenant 2 对 /commit Y52211090004 会命中 rows.size() > 1 ⇒ BusinessException(409);同一对行还占满 tenant 2 的两个唯一键槽位 ⇒ 对 tenant 2 调 /create 也撞 uk_lsy_merchant_tenant_level。admin(tenant 1)不受影响,联调路径通。
  • ⇒ 先决定删哪一行,再跑 SQL 2。

1.2 后记(★ 2026-09-23 追加:SQL 2 已执行完毕)

上面第 2 行的「❌ 未执行」与本节全文是当时的真读数,保留不改;这里记其后发生的事。

  • 裁定:用户 2026-09-23 授权「随便你删哪条」⇒ 由我裁定删候选 A(id=1)。 依据与「有没有发布过」无关:A 的 description 是数据自己写的处置说明(「正式进件(子项目 B)后应删除」), 而 B 占着 tenant 2 的 enterprise_id IS NULL 租户级槽位(受 uk_lsy_merchant_tenant_level 约束)。
  • 执行读数(全文落盘 logs/_lsy-dedup-exec.txt): 删除前逐字段断言通过 → DELETE 影响 1 行 → 重复分组 0 行、全表剩 1 行(= B) → 守闸 DO 块通过(未抛异常)→ CREATE UNIQUE INDEX 已提交 → pg_indexes 5 条(旧的非唯一 idx_lsy_merchant_mch_no 仍在,脚本要求它必须还在) → pg_constraint(contype='u') 0 条(正常,脚本第 2 步已解释)。
  • 通电验证(两向,均回滚不留数据):同 mch_no 插入 ⇒ UniqueViolation,报文点名 uk_lsy_merchant_mch_no; 换 mch_no 插入 ⇒ 成功(负对照)⇒ 拦下来的确实是这一条索引,不是别的约束或别的失败。 探针后复核:全表仍 1 行、id 仍是 B。
  • ⇒ 上面 §1.2 那条功能性阻塞已解除:该 mch_no 现在唯一,原先「按 mch_no 定位会命中多行」的后果不再可能触发。

1.3 后记(★ 2026-09-23 追加:SQL 3 已执行完毕)

上表第 3 行原读数「❌ 未执行」是当时的真读数,保留不改;这里记其后发生的事。

  • 只读预检(logs/_lsy-comment-readonly.py,自带两个对照):正对照全表 1 行; 反对照(故意写错的列名)col_description ⇒ None ⇒ 查询不是「怎么写都命中」; 注释确为旧文案、不含 PENDING/REJECTED;列 DDL = 'APPROVED'::character varying | NO | 32 (与脚本自己的第 2 条核对项逐字一致);值分布 APPROVED × 1(落在词表内)。
  • 执行读数(logs/_lsy-comment-exec.txt):COMMENT ON COLUMN 已提交 → 新注释里 PENDING / APPROVED / REJECTED 三个词全部命中 → 列 DDL 未变(默认值仍是 'APPROVED'::character varying / NO / 32)→ 值分布未变 ⇒ 确实只动了注释,没碰表结构也没碰数据。
  • ⇒ 三个交付 SQL 至此全部执行完毕。 剩下的不再有「待执行的 SQL」,只剩两类: 裁定(§四 那 4 项 + 1 条相关不对称,2026-09-23 已全部裁定完毕)与阻塞在凭据。

二、SQL 1(菜单)执行后的复核读数(我量,只读)

用该 SQL 自己尾部的只读核对语句,执行前基线 → 执行后实测:

核对 执行前 执行后(实测)
收付款目录 1 行 1 行(id=268, /payment/collection)
页面行「商户进件」 0 行 1 行 id=272, type=2, order=4, 权限 …:onboarding:list, route_name CollectionOnboarding, route_path /payment/collection/onboarding, component_path module_payment/collection/onboarding/index, uuid 36 字符
两个按钮行 0 行 2 行(273 建号 …:create / 274 提交入网 …:commit)
授权行 0 行 3 行,role_id 全是 1
反面对照(故意写错的权限串) — 0 行 ⇒ 上面不是恒真

★ 2.2 顺带验掉的最高风险接缝(五跳全通)

「库里有行」≠「页面能渲染」—— component_path 对不上时前端会静默回落 404 页。 故我把整条链走了一遍:

跳 读数
① 库 component_path = module_payment/collection/onboarding/index
② 前端解析 permission.store.ts:6 glob "../../views/**/**.vue";:206 查表键 modules['../../views/${component}.vue'];:207 兜底是 views/error/404.vue(⇒ 对不上不报错)
③ 真文件 frontend/src/views/module_payment/collection/onboarding/index.vue 存在,31470 B
④ 组件名 该文件 :242 defineOptions({ name: "CollectionOnboarding" }) = DB 的 route_name 逐字相等
⑤ 叶子判定 permission.store.ts:203 只有叶子才挂组件;页面下挂着 2 个 type=3 按钮 ⇒ 若按钮进路由树就不是叶子。实测判否:UserService.java:623 与 :636 两处都 .in(MenuEntity::getType, List.of(1, 2, 4)) ⇒ 按钮被排除在建树之外

★ 仍未证明的:页面在浏览器里渲染成功(需登录态 + 后端在跑)—— 那是 §五 的 Step 3b。


三、需要向客户经理索取的 4 项 + 上游需配置的 1 项

(a)4 项凭据(配置 + 环境变量,默认值全空,缺失时由 LeshouyingProperties.missingOnboardingCredentials() 一次列全挡在入口):

配置项 环境变量
leshouying.channel-id LESHOUYING_CHANNEL_ID
leshouying.bd-id LESHOUYING_BD_ID
leshouying.channel-type LESHOUYING_CHANNEL_TYPE
leshouying.rate-code LESHOUYING_RATE_CODE

(b)上游需配置的 1 项:进件审核通知地址 leshouying.onboarding-notify-url / LESHOUYING_ONBOARDING_NOTIFY_URL(默认空)。 我们的接收端是 POST /payment/collection/notify/onboarding,免登录(靠验签)。 ⇒ 交付时要给上游一个公网可达的地址。


四、裁定材料(2026-09-23 已全部裁定 —— 5 条都按现方案走)

用户原话:「裁定:5 条都按现方案走」(2026-09-23)。 所谓「现方案」=本件写下这一节时正在运行的那套行为:下面 4 项各自的现有实现, 加上 §七 里那条 account_type 派生/直取的不对称的现状。 ⇒ 本批次对此零代码改动、零配置改动、零 SQL 改动。 落笔时 HEAD = c8ae69f(本次裁定记录另起一次提交)。 ★ 本节的正文原样保留(那 4 段的「代价若判错」是裁定的依据材料), 只在每项末尾加一行「★ 裁定」标出终态 —— 这样读的人既能看到裁了什么,也能看到据什么裁的。 ★ 不属于这 5 条的一条:§八② 「进件是租户自助还是仅管理端」没有被覆盖,见 §八。

  1. §0.3d —— 通道条件必填字段本批次一律不收。 说明栏里按通道标注的条件必填 (reg_capital / fSettle_account_type / actual_addr / business_scope / check_code / lease_contract / p_cashier 等)没进必填矩阵;字段照常进 DTO、照常上送(不收 ≠ 放行)。 代价若判错:通道落定后操作者漏填会被上游拒(报错点在上游,请求体在日志里,可当场比对补)。 ★ 裁定这一项要用到的数据已备齐:这 6 个(加上另外 4 个更新腿独有的)字段的上游原文说明、 类型、必填标记与取值线索,以及上游文档自身的三处缺陷,都在 §七里(2026-09-23 取自上游 page 196236557)。同时那里记着一条相关的语义不对称待你一并裁:account_type 建号腿由类别派生、 更新腿可单独改。 ★ 裁定(2026-09-23):维持「本批不收」 —— 必填矩阵不加这 6 + 4 个字段;字段继续进 DTO、 继续上送(不收 ≠ 放行,这句不变)。终态。 ★ 连带裁定:account_type 的派生/直取不对称 —— 维持现状(更新腿仍可单独改)。 代价如实留着:更新腿能造出与商户类别不符的结算类型,而建号腿不能(详见 §七)。
  2. §0.7 —— /api/info/bank 选择器要不要补。 account_bank_code 已从「平台级凭据」改正为 商户自己的结算账户开户支行(逐商户填)。本批次不实现该接口,联行号手工录入。 ★ 裁定(2026-09-23):维持「不实现 + 手工录入」。终态。
  3. §0.8 —— rate_code 的归属未定:平台级还是逐商户。 现按平台级配置项实现(DTO 里留了可覆盖字段)。 代价若判错:若其实逐商户,操作者会用同一个值给所有商户进件 ⇒ 费率错。 ★ 裁定(2026-09-23):维持「平台级配置项」。终态。 ★ 这是 5 条里代价最高的一条,所以把「判错时要改什么」也一并钉住: 上游 rate_code 是请求级参数,我们两种实现都走同一条线上送(LsyOnboardingService:500 更新腿 putIfNotBlank / :1225 建号腿「空白则回落平台配置」)⇒ 万一其实该逐商户,要改的是「谁填这个值」 (加 UI 入口 / 改派生规则),不是接口形状、不是 DTO、不是 SQL —— DTO 里那个可覆盖字段一直在。
  4. Task 9 的验证台是否收编进 frontend/。 修复轮造出一台可执行验证台(真 @vue/compiler-sfc + 真 vue,驱动页面真 setup(),只桩 @/api 与上传子组件): .claude/plan/2026-09-23-lsy-onboarding-B-harness/task-9-fix1-harness.mjs(10796 B / sha16 7f07de4099c96a23) —— 2026-09-23 已挪出批次工作区(原路径 .superpowers/sdd/2026-09-23-lsy-onboarding-B/task-9-fix1-harness.mjs, 现已不存在)。新位置在 git 跟踪范围内 ⇒ git clean -fdx 清不掉它。 它钉的是行为不是文本(主控实测两向变异:改前 pass=16 fail=9 → 改后 pass=25 fail=0;重引同一缺陷仍红 4 条)。 三条路:① 收编为正式测试 ⇒ frontend/ 需引入测试入口(该目录目前无测试框架); ② 只留文件 ⇒ 它留在批次工作区里,而工作区可能被清;③ 随工作区丢弃 ⇒ 下次同类页面要重造。 局限(务必一并考虑):模板不渲染 ⇒ 只证明函数判定逻辑、不证明浏览器表现; exit code 恒 0 ⇒ 判成败只能读 SUMMARY 行。 ★ 注:该台子在工作区内,若采纳 ② 需先把它挪到工作区之外。 —— 2026-09-23 已挪,见上。 ★ 裁定(2026-09-23):按现方案走 ⇒ 不收编(排除 ①)、不丢弃(排除 ③),即 ② 只留文件。 终态 —— frontend/ 不引入测试入口。 ★ 2026-09-23 用户指示「把 Task 9 台子挪出工作区」⇒ 已执行。 落地走四步,缺一不可: ① cp 到新位置 → ② 核 sha256 逐字节相同(两侧都是 7f07de40…c095)→ ③ 从新位置实跑一遍(SUMMARY 搬运后复跑: pass=25 fail=0、rc=0,与搬家前同值)→ ④ 才删源。 ★ 敢搬的依据是实测不是推断:台子对路径零依赖 —— 全文件只有一处绝对常量 D:/project2/payment-platform/frontend/(:18)与 process.argv[2](:23); process.cwd / import.meta / __dirname / 相对 import / 任何写文件调用 全部零命中 (配了同一支 grep 的正控:readFileSync 命中 1)。 ★ 「工作区可能被清」那条残留 + 「挪出工作区是一条命令的事,要挪说一声」 均已作废。 ★ 仍留在工作区里的同类台子(本次未动) —— 同一个「被 git clean -fdx 清掉」的风险,要挪一并说: fe-update-harness.mjs(12783 B,见 §十)、fe-update-keycheck.mjs(6415 B,见 §七)、 fe-update-mutate.mjs(4197 B)。它们与 Task 9 台子不是同一件东西(那台子驱动进件页的真 setup(); 这三支是 R-7c 前端入口的验证),所以本次只搬被裁定的那一台,没顺手扩大范围。 ★ 若你要的其实是 ①「收编」(那是新工作:得先给 frontend/ 引入测试入口),说一声即可 —— 本行按「现方案走」读作 ②,是因为 ① 会改变仓库内容,与「按现方案走」相反。

五、阻塞项(两条,都不是缺陷)

5.1 Step 3b(浏览器交互验证)—— 阻塞在管理端登录

菜单 SQL 已执行(§二)⇒ 「页面 404」这个旧阻塞已解除。现在缺的是一个管理端登录。 ★ 我此前检索文档找登录凭据的尝试被权限层判为 [Credential Exploration] 并拒绝; 我已停止,且不会绕行。此项等用户提供登录方式(或由用户自己跑一遍)。

5.2 与上游的联调 —— 阻塞在凭据(§三)

按 GC7,凭据到位前不部署、不联调。代码侧已就绪。


六、仍然开着的窗口(不是「已修复」,逐条已复核)

  1. tenant 2 的功能性阻塞(§1.2) —— ★ 2026-09-23 已解除:重复行已去重(删 A id=1)、 uk_lsy_merchant_mch_no 已建成并通电验证。本条原读数是「今天仍开着(我量:那 2 行还在)」,那是当时的真读数;读数见 §1.2 后记。
  2. 「上游已建号、本地无行」 —— ★ 2026-09-23 结案:一半判「修不了」,另一半给出可执行的手工闭环。 这个状态有两个成因,代价与出路都不同,此前一直混在同一句话里:

    • (A) 上游那次 HTTP 建号失败(超时 / 连接错)⇒ 本地无从判断上游到底建没建。 这不是「没修」,是修不了 —— 判据所需的信息只在对面,本地无论怎么查都是猜。 出口已由 R-7b 闭掉(非 GET 回「不得当作失败直接重试」),不再是一个诱导重试的 500。 ⇒ 成因 (A) 到此为止,不再作为待办挂着。
    • (B) 上游已建号、本地落库失败 ⇒ 商户号已知(409 应答里带着,日志里也带着)。 出口由 R1-A 闭掉;补偿是手工的,而这条路是通的 —— 我实读代码核对过三个环节,不是推测: 管理端「商户映射」→ POST /payment/collection/merchant(权限串 …:leshouying:merchant:list) → LsyMerchantService.save 的新增分支。 ★ 一处尖角:save 只在 reviewStatus 空白时才默认成 APPROVED,而进件腿的自动落行写的是 PENDING。⇒ 补行时必须显式带上 review_status: "PENDING",否则这一行会声称「上游已审核」, 而该商户其实连提交入网都还没做。(save 只在空白时才兜默认值 ⇒ 显式传的值会被原样采纳。)

本批不做自动补偿 / 对账 / 重试:(A) 结构上做不到;(B) 要做就得新开一张孤儿表 + 一条对账腿 (新 schema ⇒ 又是一份要用户执行的 SQL),而现有的手工路径已经把商户号交到操作员眼前。

  1. 上游调用失败仍是 HTTP 500(R-7b) —— ★★ 2026-09-23 已修复(提交 5290d84, 测试 647 绿;两向变异 4 条逐条读数见 .superpowers/sdd/2026-09-23-lsy-onboarding-B 的加记 86)。 修法:在 LeshouyingOnboardingController 上加控制器局部的 @ExceptionHandler(LeshouyingApiException.class),不动全局 advice。回 HTTP 200 + 业务码 502(理由:业务失败在本仓的既有出口就是 HTTP 200 + 业务码,调用方按业务码取 msg 展示 —— 回 HTTP 502 反而让文案到不了操作员眼前;取 502 而非 500 是为了与兜底分支可分辨)。 ★★ 文案分两句,判据取 HTTP 语义而不是路径名单:本控制器七个端点里六个走上游 (/create 建号、/commit 提交、/upload-image 上传、/update 更新商户、/areas 与 /mcc 取参考数据;只有 /page 只读本地库)。GET(/areas、/mcc)是读 ⇒ 重试安全,回「数据获取未成功,请稍后重试」; 非 GET 是写 ⇒ 回「请求未能完成,无法确认是否已经生效(不得当作失败直接重试,请联系管理员核实)」。 若改用「哪些路径是写」的名单去枚举,将来往本控制器加端点就会漏,而漏的方向恰好是 「新写端点拿到可重试文案」—— 正是本条要防的那件事。 ★ 2026-09-23 这条判断被实数验证了一次:本条之后新增的 POST /update(提交 1af6ef2, §七 第 7 行)一个字都没改这个 handler 就自动拿到写腿那句防重试文案 —— 正是因为判据是 HTTP 方法而不是路径名单。若当初写成名单,这就是那个「漏」的实例(且方向正是有害的那一侧)。 ★★ 上面那条 GC1 警告被照办了:e.getMessage()(逐字形如「乐收赢接口失败[HTTP_504]: …」) 只进日志,一个字都不进应答;这一条由用例的两条 not(containsString(…)) 断言钉住, 并做过变异(把 message 拼进应答 ⇒ 那两条当场红)。 ★ 本条原来的读数(保留,那是当时的真读数):LeshouyingApiException 无 advice 分支 ⇒ 落 catch-all。 超时意味着上游可能已经建号,而 500 的语义就是「重试」⇒ 重试会再建一个上游商户。 ★★ (原)给将来修这条的人的 GC1 警告:LeshouyingResponseParser:103 的异常 message 含上游名号, 今天到不了租户眼前(catch-all 换了通用文案);若照 BusinessException 分支加一条回传 e.getMessage() 的分支,会当场违反 GC1 ⇒ 必须先消毒 message。 ★ (原)2026-09-23 复量:前提仍成立,落点已定位。 全仓唯一 advice 是 GlobalExceptionHandler (5 个分支,Exception 那条是 catch-all),没有 LeshouyingApiException 分支; LeshouyingOnboardingController 也没有自己的 @ExceptionHandler。 ⇒ /create 里 onboardingService.createMerchant(body)(:183)抛出的上游异常不被 :184 那个 try 覆盖 (它只包本地落库那一步)⇒ 逃逸成 catch-all 500。:181 之后、:183 之前无任何兜底。 ★ 现成先例别漏:LeshouyingOpenController:301-309 已有一条 @ExceptionHandler(LeshouyingApiException.class) (回 Result.of(500, "处理未成功,请稍后重试"))—— 但它是控制器局部的(类上是 @RestController, 不是 advice,见 :266 的注释解释了为何故意不用全局 advice)⇒ 只覆盖那 7 个 open API 端点。 修管理端时可照它的形状,但文案要换成防重试口径(open 侧那句「请稍后重试」在这里是有害的)。
  2. 更新腿的 mass-assignment 口未封(R-7c) —— ★★ 2026-09-23 已封盖(提交 1af6ef2, 与「开门」同一次提交,不是先开一扇裸门)。原登记逐字保留: 「LsyMerchantUpdateDTO 带 rateCode,更新腿会用它覆盖平台费率。今天不可达(生产代码无调用方、 无控制器绑定)⇒ 将来给更新腿开 HTTP 入口时必须加同样的封盖」—— 那个「将来」到了: 用户指示「把修改商户的入口开出来」。 实测(LeshouyingOnboardingController,逐行读;:242 就是那个新端点): :242 @PostMapping("/update") → :244 requireTenantId() → :245 requireByMchNoForTenant(…) → :246 rejectBodySuppliedTenantId(…) → :247 rejectBodySuppliedRateCode(…) → :248 updateMerchant(body)。 (rateCode 字段在 LsyMerchantUpdateDTO:199;端点绑的是本控制器的内部子类 UpdateMerchantRequest extends LsyMerchantUpdateDTO,:267 —— 即同一条更新腿。) ★ 封盖是实数上要装的、不是理论洁癖:两向变异 M-2 摘掉 rejectBodySuppliedRateCode 后, 带 rate_code 的请求 $.code = 0(一路走通);顺序也被四条独立断言钉住(M-3:把 updateMerchant 提到闸之前 ⇒ 4 红)。读数见工作区台账加记 87。 ⇒ 本条不再是开着的窗口。★ 也不是「一个已修复的旧缺陷」——它是登记过的将来项,现在到期并已处置。
  3. 409 文案拼入上游返回的 mchNo 原值(该方法对自己的日志做了消毒、对这个 message 没有)—— 需上游返回含换行的商户号才成立; 仓里已有同形先例。裁定:不修,届时随统一消毒一并处理。
  4. 一处「严格读不成立」的旧注释,裁定不修(LsyOnboardingService.java:363-365 那句 「它们只活在下面这段解析代码里」)—— 实测该串在本文件出现 **6 行 = 1 行代码(:396)
    • 5 行注释/javadoc(:160/:358/:391/:416/:442),故「只活在」严格读不成立**。 裁定不改:该句要证的目的(这一串不进对外文案 ⇒ 不触 GC1)已实测成立,且误导线方向无害; 要把「只活在」换成可证的说法,需先做全仓普查 —— 本批不做,登记在此。

七、端点清单(本批次共 7 条,权限串只有 3 个)

类级 @RequestMapping("/payment/collection/onboarding")。

# 端点 权限串
1 GET /page module_payment:leshouying:onboarding:list
2 POST /create …:onboarding:create
3 POST /commit …:onboarding:commit
4 GET /areas?level=&parentCode= 复用 …:create
5 POST /upload-image(multipart,部件名 file) 复用 …:create
6 GET /mcc?level=&parentId= 复用 …:create
7 POST /update 复用 …:create

★ 4/5/6/7 复用 create 串 ⇒ 菜单 SQL 一个字不用改。

★ 第 7 行是 2663020 之后追加的(提交 1af6ef2,2026-09-23):R-7c 登记的是 「LsyMerchantUpdateDTO.rateCode + LsyOnboardingService:500 是同一 mass-assignment 的更新腿, 今天不可达(updateMerchant 无生产调用方、无控制器绑定),将来开 HTTP 入口时需同样封盖」。 用户指示「把修改商户的入口开出来」⇒ 那扇门开了,封盖与门一起装(不是开一扇裸门): 闸与 /commit 同形(requireTenantId() → requireByMchNoForTenant),两道 reject* 排在闸后、 服务调用前。★ 三条权限串与 lsy-onboarding-menu.sql 里 INSERT 的三条逐字相等(我量:DB 里那 3 行对得上)。

★ 前端入口随后补上了(用户裁定「把前端的 /update 入口也补上」):不新开页面, 进件页顶部加一个「建号 / 更新」模式切换,两条腿共用同一张表单与同一套级联 —— 因为更新腿的字段集就是建号腿那张表减去两个(见下)。前端侧的要点:

  • onboarding.ts 新增 updateOnboardingMerchant(第 5 个调用)。body 是 mch_no + 白名单, 空白字段整个省掉 —— 上游更新是增量语义,留空 = 保持原值(不是清空)。
  • 两个键在更新腿绝不能发:merchant_name_business(上游逐字「创建后即固定,无法修改」)、 lease_contract(创建页有、更新页没有)。两者都不在 LsyMerchantUpdateDTO 上 ⇒ 发出去不是「被忽略」而是 HTTP 500(FAIL_ON_UNKNOWN_PROPERTIES)。 前端配了两道独立防线:更新模式下这两格不渲染(渲染出来 = 提供一个按下去必 500 的 入口),且键白名单里也没有它们。后者不能省 —— switchMode 有意不清空表单 (41 个框,误点一下不能毁掉已填内容),所以「建号模式下填过、再切到更新」的值仍留在 form 里,只靠「不渲染」挡不住。
  • 更新腿的 10 个上游独有字段前端仍无入口(license_stt / account_type / legal_cert_stt / account_idcard_stt / reg_capital / fSettle_account_type / actual_addr / business_scope / check_code / p_cashier)—— 不是漏做:后端那张中文标签表是创建页 62 个参数的表, 它的键集被一条 assertEquals 钉死,这 10 个键一个都不在里面。要补得先从更新页参数表逐个转录。 ★ 2026-09-23 裁定:本批不补这 10 个入口,两条理由: ① 其中 6 个正是 §0.3d 那一族通道条件字段(reg_capital / fSettle_account_type / actual_addr / business_scope / check_code / p_cashier),而 §0.3d 当时本身就是 §四 待裁定的第 1 项 ⇒ 先替用户把 UI 做出来,等于替他做了那个裁定; (★ 该前提已于 2026-09-23 消失 —— §0.3d 已裁定,见本节末尾 ★ ;结论不变,依据已换) ② 剩下 4 个各有一个必须先答的问题:三个 *_stt(license_stt / legal_cert_stt / account_idcard_stt)的语义在上游文档里自相矛盾(见下);account_type 在建号腿是从 category 派生的(实测 LsyOnboardingService:1190 p.put("account_type", dto.getCategory().getAccountType())),更新腿却是 DTO 直取 (:480 putIfNotBlank(p, "account_type", dto.getAccountType()))⇒ 更新腿能造出与商户类别不符 的结算类型,而建号腿不能。这是要不要允许的选择,不是一个默认值。 ★ 不补是安全的:上游更新是增量语义「有值则更新,无值则保持原数据」⇒ 没有入口 = 不改这一项, 不会把已有值抹掉。所以这是能力缺口,不是正确性缺口。 ★ 2026-09-23 后补(理由 ① 的前提消失,结论不变):§0.3d 已裁定为「维持本批不收」、 account_type 的不对称已裁定为「维持现状」(见 §四)⇒ 上面理由 ①「先替用户做裁定」不再成立。 结论(本批不补这 10 个入口)照旧,但依据要换成另外两条:② 仍成立(三个 *_stt 的语义在上游 文档里自相矛盾),加上「不补是安全的」这条独立成立。 ★ 这不等于裁定「将来也不补」 —— 补的时候权威数据就在下一段,是转录不是重新调研。 ★ 转录用的权威数据已取回(上游 page 196236557,2026-09-23 用 showdoc JSON 接口取全文)。 将来裁定「补」时不必再查文档,逐字如下:

    | 键 | 上游说明(逐字) | 类型列 | 必填列 | |---|---|---|---| | license_stt | 营业执照有效期(格式YYYYMMDD) | String(8) | | | account_type | 账户类型 | String(4) | 是 | | legal_cert_stt | 法人证件起始日(格式YYYYMMDD | String(30) | | | account_idcard_stt | 持卡人身份证起始日(格式YYYYMMDD) | String(8) | | | reg_capital | 注册资本 | String(25) | C | | fSettle_account_type | 结算账户类型 | String(2) | C | | actual_addr | 营业地址 | String(2) | C | | business_scope | 营业范围 | String(2) | C | | check_code | 安心签验证码 | String(2) | C | | p_cashier | 收银台照 | String(255) | |

    取值线索在同表的示例列(不是说明列):account_type = 1对公结算 2法人对私结算 3非法人对私结算; reg_capital = 1000000 单位元 平安收银宝必填;fSettle_account_type = 1-我行对公 2-我行对私 平安收银宝必填; actual_addr = 省市区+详细地址 平安收银宝必填;business_scope = 营业执照上的 平安收银宝必填; check_code = 485225 星驿付必填。 ★★ 上游文档本身有三处缺陷,转录前必须知道,否则会把缺陷抄成我们的标签: ① license_stt 的说明与 license_expire 逐字撞车(两个都写「营业执照有效期(格式YYYYMMDD)」,

    后者多一句「长期传20991231」)。按 `_stt` / `_expire` 的命名、以及另两个同族字段
    (`legal_cert_stt` = 法人证件**起始日**、`account_idcard_stt` = 持卡人身份证**起始日**)判断,
    `license_stt` 应当是**起始日** —— **但这是我的推断,不是文档写的**,转录前要向上游确认。
    

    ② legal_cert_stt 的说明缺右括号(「(格式YYYYMMDD」)—— 已按原文照录,未替它补。 ③ actual_addr / business_scope / check_code 的类型列都写 String(2),而它们的示例分别是

    「省市区+详细地址」「营业执照上的」「485225」⇒ 类型列与示例**互相矛盾**,`String(2)` 一个都装不下。
    ⇒ **不要照类型列做长度校验。**
    

    ⇒ 后端收 50 个,两支独立量法逐位同值:① updateMerchant 方法体内 putIfNotBlank 调用点 = 50(正控:全文件 putIfNotBlank( 出现 51 次 = 定义 1 处 + 调用 50 处, 且没有调用落在 updateMerchant 之外);② LsyMerchantUpdateDTO 的 private String 字段 51 个、去掉 mchNo = 50。 ⇒ 前端能发出 39 个(执行级台子实跑倒出,不是从注释抄)。差的 11 个 = 上面那 10 个无入口的 + rate_code(有意封盖,不是漏做 —— 前端连输入框都不该有)。 ⇒ 这中间的差是已知边界,不是缺陷。 ★ 这 39 个与后端那 50 个做过跨端键名核对(fe-update-keycheck.mjs,把两边键集放在 同一个 node 进程里做集合运算):前端键集 ⊆ 后端键集,多出 0 个。这条不能省 —— 前端键名拼错不会当场报错,只会变成线上 HTTP 500。 ★ 两处本轮新做的检查,以及一处对我此前说法的订正:

    • 模板的三处字段渲染点全部接了投影函数(:99 纯文本组 / :163 地址组 / :242 上传组 都是 v-for="f in visibleFields(s)",没有残留的 s.fields)。这条必须单看 —— 执行级台子驱动的是函数,模板有没有调用它,台子看不见:投影函数写得再对, 模板若仍遍历 s.fields,「更新模式不渲染那两个键」就是一句空话,而台子照样 43 绿。
    • 订正我此前的说法:我写过「模板本身未被验证」。实测不成立。 vue-tsc 会检查本文件的 模板表达式 —— 往模板里注入一处类型错,pnpm ts:check 从 287 变 289,两条都点名 onboarding/index.vue(99,52) 与 (163,52)、报的就是我注入的那个属性名。 ⇒ 那条「与本改动前逐字节相同的 287」因此顺带证明模板表达式类型干净 (无未定义标识符、无错键访问、无类型不符),而不是只证明了脚本部分。 仍未验的只剩运行时渲染(台子是 inlineTemplate: false)与浏览器表现(§5.1)。

八、安全口径(读这一节再决定放权)

① 进件页与 7 个端点今天只对 admin(超管)可用。 lsy-onboarding-menu.sql 只授 role_id = 1, 故意不授 role_id = 2 —— 因为 /commit 的租户归属闸依赖一张调用方自己可写的映射表, 而 role 2(活库实测 17 个用户)本就持有那条写入口所需的读权限(merchant:list)。 ⇒ 这是有意的收紧,不是漏授;租户用户看不到页面属预期行为。已在 §二 复核:授权行 3 行、role_id 全是 1。 ② 「进件是租户自助还是仅管理端」——★ 2026-09-23 那次「5 条都按现方案走」没有覆盖它 (那 5 条里不含本条)。现行为两边都成立(只授 role 1:不管将来判「仅管理端」还是「将来开放」, 今天这一步都不需要改)⇒ 它现在不需要任何动作,只在你决定对租户开放进件页时才变成决策。 ③ save / delete 由读权限 merchant:list 守着,而那张映射表调用方可写 —— 既存面,不在本批修,如实登记。


九、★ 一处理由的订正(结论不变)

草稿 §六-2 写「SingleImageUpload.vue 不在 Task 9 的 diff 内,改它要动共享组件 ⇒ 越界」。 「不在 diff 内」是假的(我量:git log 663156f..a9d1b83 -- <该文件> 只有 c48c253 一条,而它就是 Task 9; 整支包里文件头命中 1,配了正对照)。 但「不修」仍对:c48c253..HEAD 对该文件零改动,其全部内容是加法 (一个可选 prop uploader + 一段 if (props.uploader) 分支),catch/onError 路径一个字符没动 ⇒ 双提示确属既有行为,本批次既没引入也没改过。 ⇒ 正确理由:双提示落在既有错误路径上,去修它属于改动既有共享行为、超出本任务范围。 (「越界」不是文件级禁令 —— 本批次已经动过那个共享组件,且动得是加法。)


十、测试与验收读数

  • 全量 mvn clean test:645 绿(Tests run: 645, Failures: 0, Errors: 0, Skipped: 0),跑在三个提交全部落地之后。
  • ★ 全量 mvn clean test:647 绿(Tests run: 647, Failures: 0, Errors: 0, Skipped: 0), 跑在 5290d84(R-7b 修复)落地之后(clean ⇒ 报告目录是本次的,49 个 surefire 报告文件全部解析到汇总行、无失败类)。相对上面 645 的增量 = +2,正是 R-7b 新增的 两条用例(createEndpoint_upstreamCallFailure_isNotCatchAll500_andForbidsBlindRetry 与 areasEndpoint_upstreamCallFailure_saysRetryable_notTheWriteEndpointsAntiRetryCopy)。 ★ 上面那条「@Test 计数 646」是 2663020 时点用另一支量具(数注解)的读数, 本轮未用同一支量具复量 ⇒ 不要把它与 647 相加或相减(两者量法不同,本件不替它推数)。
  • ★ 全量 mvn clean test:652 绿(Tests run: 652, Failures: 0, Errors: 0, Skipped: 0、 BUILD SUCCESS、rc=0)。★ 本次是我在 2026-09-23 收尾轮自己跑的,不是转述。 两支独立量法同值:① Maven 控制台汇总行;② target/surefire-reports/*.xml 逐文件聚合 —— 49 个报告文件、tests 合计 652、failures / errors / skipped 各 0。 ★ 配了正控:每个 XML 确实带 tests= 属性(逐个命中 1)⇒ 那个 652 不是 「0 个文件求和得 0」那一型假读数。 相对上面 647 的增量 +5 已定位到提交:git show 1af6ef2 -- '*/src/test/*' | grep -c '^+.*@Test' = 5(R-7c 的 POST /update 那组用例);同法量 5290d84(R-7b)= 2,与 645→647 的 +2 对得上。 ★ 上面 645 / 647 两行保留不动 —— 它们是各自时点的真读数,本行只是把链子接到今天。
  • 两个注释修复提交 70bb22a、2663020 也都是 645 绿(BUILD SUCCESS)。 两者「逐字节无行为变化」的完整证据(2663020 这一版两侧的 class 读数都是我自己采的): ① 行数不变(CR=1485 / LF=1485,无孤立 CR/LF,行尾仍 CRLF)⇒ LineNumberTable 不变; ② LsyOnboardingService.class 的 sha256 改前改后完全相同(909cf5898485a6cc…f0e337c,25444 B); ③ 该 .class 的 mtime 晚于源码 mtime(70bb22a 那轮 +34.4s;2663020 这轮 +41.3s) ⇒ 是拿改后源码编的,不是陈旧产物(这一条是必须的:sha256 相同分不清「没变」与「没重编」); ④ ★ 「javadoc 文本不进 .class」已从推断升级为直接实测:在 class 原始字节里搜 8 条 javadoc 探针 全 0 命中,同时 3 条代码字面量正控命中 1/1/2 ⇒ 量具是活的、这个 0 不是空的。 ★ 一处不成立的理由已订正:70bb22a 那轮我写的链条是「行数不变 ⇒ javadoc 不进 class」—— 那是 non-sequitur(散查复评指出,我接受):行数不变保住的只是 LineNumberTable。 上面第 ④ 条才是这件事的直接证据。
  • @Test 计数 BASE = HEAD = 646;测试文件 49/49 ⇒ 收尾轮未增未删用例。
  • 收尾轮(0ddb002 → a9d1b83)净 diff 66 行,全部是注释(SQL -- 12 + javadoc * 34 + java // 20, 非注释 0)⇒ 无行为改动。三个来源、三支量法同判。
  • 工作区状态:有一处未提交改动 java/src/main/resources/application.yml 的 redis.host: redis → localhost (本地开发用,不属于本批内容;按本批实测结论 mvn test 不依赖 Redis ⇒ 影响 nil,且不会随交付出去)。 另有两个已跟踪的旧日志文件在工作区被删(logs/payment-platform.2026-07-1{5,6}.0.log.gz)—— 本批所为之外,会话开始前即如此。
  • 未 push(GC10)。

十一、最终的整支评审

已回件。结论:无需修复 —— 0 条接缝缺陷、0 条安全缺口、0 条规格漏做/多做, 只有 1 条 Low 注释一致性问题(已修,见 §11.1)。范围 663156f..a9d1b83 (37 提交 / 39 文件 / +11109 −27;评审包 759216 B / sha16 741317ea91daea47), 按「接缝 / 整批规格符合性 / 安全 / 一致性」四个方向独立复量。 报告落在 .superpowers/sdd/2026-09-23-lsy-onboarding-B/final-review-report.md。

11.1 那 1 条 Low 已修(提交 70bb22a)

LsyOnboardingService.java:342 的小标题写「③ 组装:三处取值不属于调用方」,被它自己引入的 第三条(:349「rate_code:DTO 有值以 DTO 为准」)推翻;实现 :1225 也是 DTO 优先 (isBlank(dto.getRateCode()) ? props.getRateCode() : dto.getRateCode())。 ⇒ 照小标题读会得出「rate_code 是平台级、调用方设不了」,进而删掉计划 §0.8 特意留在 DTO 上的覆盖口。

★ 同一段的第二处是我复量出来的,评审漏了并且主动开脱::352 写「其余 54 个字段原样透传」。 评审的开脱理由(「55 − 1 = 54,算得对」)自己就不自洽 —— 它给的分组 4 + 2 + 1 + 54 = 61 ≠ 60。 真值是 53,两支独立量法逐位相同:DTO 共 55 个字段,category 只用于派生那两个码值、自身不上送, rate_code 又已在上面单列 ⇒ 原样透传 = 55 − 1 − 1 = 53。 已一并改掉,并把 53 写成可自检的算式,以免下次再靠人脑心算。

11.2 ★ 评审自报没复现的 8 项(不得据这份报告认为这些已被验证)

  1. 评审包(759216 B)它没有读 —— 它用的是工作区原文与 git diff <range> -- <path>。
  2. 台账 progress.md 与 25 份 _addendum*.md 它没有通读;派发件里那份「已裁定、不要重复报」 的清单,它是照转述采信的,没有回台账逐条核对。
  3. 测试断言强度未逐条复核(只跑了全量 + 通读了 LeshouyingSensitiveTest 的 300–452 段)。
  4. LeshouyingSigner.verify 的签名原文规则只读了代码,没有独立构造向量做实证。
  5. 前端未做执行级验证(无 dev server、无 Playwright)。 ★ 补记(2026-09-23 晚):审评当时确实没做,这一行保留不动;但它指的那块空白后来 由另一条路补上了 —— 本轮新增 fe-update-harness.mjs 执行级台子(编译真 SFC、跑 frontend/ 里那份真 vue、驱动真函数、只桩掉 api 与上传组件)43 条全绿,外加 4 条两向变异与 2 条 ts:check 可见性变异。仍未做的是浏览器里的验证 (本仓 frontend/ 无测试框架、ts:check 有非零历史基线,「跑绿了」在这里不成立)。
  6. 没有连库、没有执行任何 SQL(GC6)⇒ 真库那两行重复、列数/索引清单都是从注释与 SQL 读来的。
  7. 只跑了默认 profile;LeshouyingProperties.validate() 的启动拒绝路径未实测。
  8. 租户拦截器那张四行主体行为表只读了代码分支,未实测。

11.3 散查复评(第一轮已回件:通过)

修复提交 70bb22a 的散查复评已回件,结论「通过」 —— 那 2 行改得对,没有把一处假话换成另一处。 它用第三种量法(按值表达式形态)独立算出 53,与我的两支同值;并逐键做集合比较确认 测试夹具的「54」与实现的 53 不矛盾(fixture DTOSEC − {rate_code} == 实现的 53 个透传键) ⇒ 与 §11.1 的结论一致,夹具不需要改。

★ 它同时报了 3 条我漏掉的同形缺陷,全部落在本轮未改动的行上(结论对、定语/因果句错)。 三条我都用自己的量具独立复现后才动手:

位置 原句错在哪 我复现的读数
:305–306 「后两个里的 inst_no 与 sign」 枚举序是 inst_no/trace_no/sign ⇒「后两个」= {trace_no,sign},点错了对象;LeshouyingClient.signedPayload:382–383 实测补的正是 inst_no+sign
:307–309 构成式 3+4+2+54 = 63 真值 = 60 个 map 键 + inst_no + sign = 62;rate_code 被同时算进「配置四项」与「DTO 的 54 个」,数了两遍 —— 且这与我上轮 :352 写的「上面单列的 rate_code」直接矛盾
:313–316 凭据闸写「四项」 实测是 五项(missingOnboardingCredentials():230/233/236/239/242 五条 isBlank),漏的是 channel_type;而它不在那 60 个键里(生产代码只有 mccList:749 读它)⇒「凭据缺失时连报文都补不全」在「只有 channelType 缺」下不成立
:352(我上轮自己写的) 「只用于派生的 category」 .getCategory() 生产读点实测 4 处,除 :1189/:1190 派生外还有 LsyMerchantRequiredFields:313(选必填集)⇒「只」在排他读法下偏

已一并修复,提交 2663020(1 文件 / 8 增 8 删 / 全注释行 / 行数不变 ⇒ .class 逐字节相同,见 §10)。 改法逐字::305「后两个里的」→「其中」;:307「配置四项(… / rate_code)」→「配置三项(…)+ rate_code(规则见 ③)」; :309 的 54 → 53(⇒ 3+3+1+2+53 = 62,同时消掉与 :352 的自相矛盾); :314 改为「凭据闸共五项,其中四项本身就是报文的一部分…(第五项 channel_type 只喂 MCC 那条腿)」; :352「只用于派生的」→「自身不上送的」。

11.4 停止规则(本批不再改注释)

2663020 是本批最后一次注释订正轮。 之后再发现同族问题(定语 / 因果半句 / 计数)只登记 + 裁定,不改 —— 这四条全部落在注释上、.class 逐字节未变,继续改只会让「已评审范围」反复失效而不改变任何行为。 该例外条款(「若复评说新句子本身有假话则照改」)已用掉且未触发:第二轮散查复评判「通过」, 四条原问题都真消掉、四个新句子逐句实测无新假话、算术式 = 62。 它另独立量到两条我改时没量的读数:① :314–315 的「其中四项」恰好等于 :307 的「配置三项」∪{rate_code}(两处不再打架); ② :309 的 53 与 :352 的 53 逐键集合相等(双向差集皆空),不是「数字对、集合不对」。 ⇒ 本范围内已闭合,不再改动(详见台账加记 82、83)。

★ 一件已知偏离(两轮同样):技能要求派发时显式指定模型(并点名末轮用最强), 但用户既有约束是派发不传 model(实测会 400 打死派发)⇒ 按意图执行、未传 model。 此项已写进台账加记 75 §二。