yeg
2026-09-17 e2c9202b578bfdce8d46085a7c3932bcaa3ba22b
docs(merchant): 标注 ADR-0001 的术语修订,ticket 改 ready-for-human

复审发现三处文档问题,这条修的是口径本身。

- ADR-0001 新增「2026-09-17 术语修订」一节:本轮把「收款能力」收窄为通道侧事实之后,
该文件标题句与「等待实名是收款能力的真实缺项」两处旧用词不再一致。按 docs/agents/domain.md
的要求显式标出并改读,声明判定一字未动。ADR 0002 的「与 ADR-0001 的关系」同步加一句指过去。
- 五张 ticket 的 Status 由中文说明改为 ready-for-human —— 代码侧交付完成,待两端页面人工验收;
仓库里此前的先例都是先给标签再加说明,只有这五张完全不带标签。
- ticket 01/03 的 Comments 补记本轮改动(证据键改名、retryAfterSeconds、refreshed 单点返回);
05 把「冷却结束自动恢复可点」这一条改写成如实的实现描述(原先勾了但没做);
04 补记「缺失能力」→「缺失步骤」这句顺带的口径统一。
- spec 的 D9 与「关键事实」相应更新:说明 stages_json 里能住证据(bank/merchantNormal/
realnameQr/merchantStat)不能住阶段,以及各派生字段的用途。
8个文件已修改
33 ■■■■■ 已修改文件
.scratch/merchant-capability-visibility/issues/01-onboarding-completion-checks.md 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.scratch/merchant-capability-visibility/issues/02-merchant-payment-readiness-endpoint.md 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.scratch/merchant-capability-visibility/issues/03-manual-capability-verify-with-cooldown.md 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.scratch/merchant-capability-visibility/issues/04-onboarding-page-shows-completion-checks.md 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.scratch/merchant-capability-visibility/issues/05-merchant-center-payment-capability-page.md 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.scratch/merchant-capability-visibility/spec.md 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/adr/0001-merchant-onboarding-and-wechat-provisioning.md 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/adr/0002-merchant-capability-visibility.md 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.scratch/merchant-capability-visibility/issues/01-onboarding-completion-checks.md
@@ -6,7 +6,7 @@
Blocked by: None
Status: 代码侧交付完成,待页面级人工验收
Status: ready-for-human —— 代码侧交付完成,待两端页面人工验收
- [x] `view()` 返回新增字段,含 `bank` 与 `merchantNormal` 的当前核实结果与可读原因;两项都为真时不制造噪音(页面上不多出一行「已核实」)。
- [x] `stages` 仍恰好是 basic/wechat/appId/realname/delay/settlement 六项,`complete()` 判定与 `application.status` 的写入逻辑不变。
@@ -22,4 +22,5 @@
## Comments
- 2026-09-17 由本会话 grilling 的 D5/Q9 决策立项(见 [spec](../spec.md))。字段名 `completionChecks` 为暂定,实现时可按仓库风格调整,但语义必须保持「完成判定里不在六项阶段内的那两项」。
- 2026-09-17 代码侧交付。字段定为 `completionChecks: [{key,label,status,reason}]`,`status` 由 `stages_json` 里那两个布尔值派生(`true → confirmed`、`false → failed`、没核实过 → `unknown`),全是字符串,客户端不需要再处理布尔到字符串的转换。另外**在 `stages_json` 里补存了 `merchantStatus` 原始值**:只说「通道商户状态异常」而不给 `CLOSED`/`FREEZEWITHDRAWAL` 这种值,商户和运营都没法拿去沟通,而这个值本来就在通道回执里。`reason` 分支同时修正:六项全绿但两项未通过时改成「进件阶段已全部确认,但<项名>未通过核实,见下方核实结果」。测试见 `MerchantOnboardingCompletionChecksTests`(5 项),全量 318 项里仅 `docs/agents/testing.md` 的 3 项基线失败。
- 2026-09-17 代码侧交付。字段定为 `completionChecks: [{key,label,status,reason}]`,`status` 由 `stages_json` 里那两个布尔值派生(`true → confirmed`、`false → failed`、没核实过 → `unknown`),全是字符串,客户端不需要再处理布尔到字符串的转换。另外**在 `stages_json` 里补存了 `merchantStat` 原始值**:只说「通道商户状态异常」而不给 `CLOSED`/`FREEZEWITHDRAWAL` 这种值,商户和运营都没法拿去沟通,而这个值本来就在通道回执里。`reason` 分支同时修正:六项全绿但两项未通过时改成「进件阶段已全部确认,但<项名>未通过核实,见下方核实结果」。测试见 `MerchantOnboardingCompletionChecksTests`(5 项),全量 318 项里仅 `docs/agents/testing.md` 的 3 项基线失败。
- 2026-09-17 复审修复(两轴 code review 的 Standards 4/Spec 3)。证据键由 `merchantStatus` 改名为 **`merchantStat`**:前者与阶段状态同形,容易被读成"另一个阶段";后者与申请行的通道快照列同名同义,读作证据更自然。改名的代价是老记录里没有这个键——已补回落逻辑(取申请行的 `merchant_stat`)并加测试 `fallsBackToTheApplicationSnapshotForTheChannelMerchantValue`,测试从 5 项变 6 项。同时把 `incompleteReason` 里全限定的 `java.util.stream.Collectors.joining` 换成 `String.join`。`stages_json` 里能住证据不能住阶段这件事,已写进 [spec](../spec.md) 的「关键事实」。
.scratch/merchant-capability-visibility/issues/02-merchant-payment-readiness-endpoint.md
@@ -11,7 +11,7 @@
Blocked by: None
Status: 代码侧交付完成,待页面级人工验收
Status: ready-for-human —— 代码侧交付完成,待两端页面人工验收
- [x] 未进件/待审核/已驳回的商户也能调用:判定为不可收款并给出对应原因(复用既有原因链,不新造文案)。
- [x] 已 approved 且快照正常的商户返回 `ready=true`,`verifiedAt` 为 `capability_verified_at`。
@@ -28,3 +28,4 @@
- 2026-09-17 由本会话 grilling 的 D2/D9/Q10 决策立项(见 [spec](../spec.md))。手动核实是独立的一票(03),本票只做只读。
- 2026-09-17 代码侧交付。比 spec 的 D9 多返回两个派生字段:`verifyAvailable`(冷却窗口是否已放开,页面据此禁用「立即核实」)与 `nextVerifyAt`。加它们是因为让客户端去解析数据库时间戳字符串判断冷却不可靠,而这两个值同样是从快照派生的、不含通道原始值,D9 已相应更新。落点按既有约定放在 `MerchantSettlementService`(复用 `store(principal)` 门店校验)再委托给 `MerchantPaymentReadinessService.merchantView`;控制器只加两个端点,没有给控制器引入新依赖。测试见 `MerchantPaymentReadinessMerchantViewTests` 的 `merchantViewNeverQueriesTheProvider`(用 mock 调用次数证明只读)、`merchantViewReusesTheGateMessage`(拿闸门抛出的原话比对)、`merchantViewBlocksWithoutAnApprovedApplication`。
- 2026-09-17 复审修复(Standards 5)。再加一个派生字段 **`retryAfterSeconds`**:冷却剩余秒数由服务端算(`coalesce(greatest(0, timestampdiff(second, now(), capability_sync_until)), 0)`),客户端拿它做倒计时就地恢复按钮,不必解析时间戳。它同时替掉了原来那个"另查一次窗口是否放开"的 select——读侧只有一个 `capabilityCooldownSeconds`,写侧只有一个 `acquireVerifyWindow`,"冷却判据写两遍"的问题随之消失。`verifyAvailable` 现在由 `retryAfterSeconds == 0` 推出,两条口径不会再分家。D9 已相应更新。
.scratch/merchant-capability-visibility/issues/03-manual-capability-verify-with-cooldown.md
@@ -10,7 +10,7 @@
Blocked by: 02
Status: 代码侧交付完成,待页面级人工验收
Status: ready-for-human —— 代码侧交付完成,待两端页面人工验收
- [x] 冷却期内第二次调用不产生通道查询(用 mock 通道的调用次数断言)。
- [x] 查询失败(通道异常、结果未知)同样节流,失败信息按既有失败路径写 `capability_response_code`/`capability_response_message`,快照其余列不被覆盖。
@@ -30,3 +30,4 @@
- 2026-09-17 由本会话 grilling 的 D7/D8/Q11 决策立项(见 [spec](../spec.md))。这是本轮唯一的新增迁移,编号前已确认 `db/migration/` 里最大版本是 147,本次为 `V148`。
- 2026-09-17 代码侧交付。冷却实现取"一个 UPDATE 兼做判定与占位":`update ... set capability_sync_until = now() + interval 5 minute where id = ? and (capability_sync_until is null or capability_sync_until < now())`,更新到行的那次才去查通道,并发点击直接拿当前快照。5 分钟按仓库既有 5 分钟租约写死在服务里(`MERCHANT_VERIFY_COOLDOWN_MINUTES`),没新开配置项——`stale-hours` 是运维口径,这个只是防连点。`merchantVerify` 刻意**不加** `@Transactional`:冷却窗口必须在通道查询之前落库,且通道查询不能夹在持有该行锁的事务里(这条在类注释里已写死)。未核实的异常路径(`!providerVerificationRequired() || !huifuPaymentProperties.isEnabled()`)直接返回当前快照、不查通道,与 `refreshQuietly` 的守卫一致。测试见 `MerchantPaymentReadinessMerchantViewTests` 的四个冷却用例(含"失败也节流"与"冷却过期后可再查")。
- 2026-09-17 复审修复(Spec 6)。原实现里"通道校验关掉/汇付未启用"那条早退分支**不返回 `refreshed`**,而 D9 说手动核实一定带它;客户端把缺失当 `false`,于是在那种配置下也会弹"刚核实过"。现在 `refreshed` 由唯一的返回点统一带上(`boolean refreshed = … && acquireVerifyWindow(...)`,早退已删除),结构上不可能再缺;前端文案也改成如实的"未重新查询通道,显示最近一次结果"。另把冷却的抢窗口逻辑提成 `acquireVerifyWindow`,与读侧的 `capabilityCooldownSeconds` 各管一侧。测试补了 `retryAfterSeconds` 的断言。
.scratch/merchant-capability-visibility/issues/04-onboarding-page-shows-completion-checks.md
@@ -8,7 +8,7 @@
Blocked by: 01
Status: 代码侧交付完成,待页面级人工验收
Status: ready-for-human —— 代码侧交付完成,待两端页面人工验收
- [x] 进件页能显示 `bank`/`merchantNormal` 未核实或异常的原因,文案不含「待办理」(「待办理」只用于六项进件阶段)。
- [x] `stages` 仍渲染六项,渲染逻辑保持通用循环,不硬编码阶段名(现有仅 `realname` 例外)。
@@ -24,3 +24,4 @@
- 2026-09-17 由本会话 grilling 的 D5/D6 决策立项(见 [spec](../spec.md))。
- 2026-09-17 代码侧交付。渲染放在进度面板内、六项阶段之后,并用一小段「完成条件核实」标题隔开——两项完成条件不是第七、第八个进件阶段,不加分隔会被读成阶段之一(这正是术语收敛要避免的那种混用)。已核实通过的那两项不渲染(`failedChecks` 过滤掉 `confirmed`),全绿时页面不多话。`completionChecks` 的 `status` 服务端就是字符串,所以在 `settlement.uts` 里直接 `stringValue(check.status)` 即可,不会出现早先布尔值被转成 `"true"` 那种问题(`OnboardingStage.qrCode` 的教训)。`statusTitle` 的 pending 分支同时改口径(见上),面板底部「缺失能力」也改成「缺失步骤」。
- 2026-09-17 复审修复(Spec 4)。「缺失能力」→「缺失步骤」这句不在本票字面要求里,属于顺带的口径统一(D3:能力只指通道侧事实),在此补记以免被当成无关改动。
.scratch/merchant-capability-visibility/issues/05-merchant-center-payment-capability-page.md
@@ -11,7 +11,7 @@
Blocked by: 02, 03
Status: 代码侧交付完成,待页面级人工验收
Status: ready-for-human —— 代码侧交付完成,待两端页面人工验收
- [x] 商户中心出现「收款能力」入口,可进入新页面。
- [x] `ready=true` 时显示可收款与最近核实时间;`ready=false` 时显示原因与最近核实时间。
@@ -22,4 +22,5 @@
## Comments
- 2026-09-17 由本会话 grilling 的 D2/D6/D9 决策立项(见 [spec](../spec.md))。这是「遗留二」的落点;approved 之后商户不再需要回头进进件页。
- 2026-09-17 代码侧交付。新页 `pages/payment-capability/payment-capability.uvue`,入口放在商户中心「商户进件」下方(图标 `hand-coins.svg`)。状态图标刻意不用红叉:进件还没通过时用与进件页一致的「等待」图标,只有 `reason` 指明通道商户状态异常时才由文案承担那份严重性。冷却期把按钮置灰并显示「下次可核实」,「冷却结束自动恢复可点」这一条目前靠重新进入页面(`onShow` 重新拉取)触发,没有做本地倒计时——验收时请按「退出再进」的方式看这一条,如果希望在不离开页面的情况下自动恢复,需要再加一个定时器,请届时说明。`loading/error` 复用 `page-feedback` 组件,与商户中心一致。
- 2026-09-17 代码侧交付。新页 `pages/payment-capability/payment-capability.uvue`,入口放在商户中心「商户进件」下方(图标 `hand-coins-gray.svg`:`hand-coins.svg` 是绿色,与商户中心那一列灰图标不一致,`7fb1821` 补了灰色变体)。状态图标刻意不用红叉:进件还没通过时用与进件页一致的「等待」图标,只有 `reason` 指明通道商户状态异常时才由文案承担那份严重性。`loading/error` 复用 `page-feedback` 组件,与商户中心一致。
- 2026-09-17 复审修复(Spec 1/Standards 3,以及 Spec 6、7)。**「冷却结束自动恢复可点」原先勾得不对**:代码只在 `onShow` 重新拉取,留在页面上按钮会一直停在"稍后可再次核实"。现在真的做了:服务端给 `retryAfterSeconds`,页面用 `setInterval` 递减并显示在按钮上("4:05 后可再次核实"),到点就地恢复并静默重拉一次(`loadReadiness(false)`,不切 loading,避免整屏闪),`onHide`/`onUnload` 清定时器。同时修了两处会误导人的文案:一是核实回来若 `refreshed=false` 只说"未重新查询通道,显示最近一次结果"(不再谎称刚核实过);二是两种"不能点"分开说——冷却中提示"刚核实过,请稍后再试",没有冷却却仍不可核实(例如进件未通过)则直接复用判定的 `reason`,按钮文案也从一律"稍后可再次核实"改为"暂不可核实"。图标名一处笔误(写成 `hand-coins.svg`)一并改正。
.scratch/merchant-capability-visibility/spec.md
@@ -30,7 +30,7 @@
| D6 | 入口放在商户中心新增页;进件页只修字段与文案,**不**放开 approved 下的「刷新进度」 | approved 后商户不会回头点进件页;且进件页刷新只写 `stages_json`、不碰能力快照,两个入口必然口径不一 |
| D7 | 手动核实带服务端冷却,默认 5 分钟(与进件刷新 5 分钟租约同量级) | 现有 `refresh()` 对同一申请零去重,等于给汇付开一个无节流的查询入口 |
| D8 | 冷却用新增列 `capability_sync_until`(迁移 `V148` + 同步加到 `MerchantOnboardingSchemaInitializer`);成功与失败都推进冷却 | `updated_at` 是 MySQL 的 on-update 列,任何无关 UPDATE 都会重置它;`capability_verified_at` 失败不推进,可被连点 |
| D9 | 自查接口只返回派生结果 `{ready, reason, verifiedAt}` 与冷却相关的 `{verifyAvailable, nextVerifyAt}`(手动核实另有 `refreshed`),`reason` 与下单被拒文案逐字一致;不返回通道原始值、不返回逐项前提清单 | 只读接口不查通道,避免第二个判定口径;客户端不需要自己解析时间戳判断冷却 |
| D9 | 自查接口只返回派生结果 `{ready, reason, verifiedAt}` 与冷却相关的 `{verifyAvailable, nextVerifyAt, retryAfterSeconds}`(手动核实另有 `refreshed`,且每条返回路径都必须带上它),`reason` 与下单被拒文案逐字一致;不返回通道原始值、不返回逐项前提清单 | 只读接口不查通道,避免第二个判定口径;`retryAfterSeconds` 让客户端在冷却结束时就地恢复按钮,不必自己解析数据库时间戳;`refreshed` 说明"这次到底查没查通道",通道校验关掉时不能凭空宣称刚核实过 |
| D10 | 不新增任何定时任务;进件进度仍不自动轮询 | 沿用 ADR-0001 由用户明确选择的取舍,本次不修改它 |
| D11 | 后端加集成测试(mock 通道 + 直接造 `merchant_stat`),前端人工验收 | 无法真的让汇付关商户,被关状态一律造出来 |
| D12 | 商户端只读当前 principal 的商户与门店,不信任前端传入的申请号 | 沿用仓库既有权限边界 |
@@ -39,6 +39,7 @@
- `LABELS` 同时是展示列表与完成判定的键集:`MerchantOnboardingProgressService.complete()` 用 `LABELS.keySet().stream().allMatch(...)`,且 `bank`/`merchantNormal` 是**布尔值**而其余六项必须是字符串 `"confirmed"`。把它们加进 `LABELS` 会两次破坏完成判定。
- `view()` 已经读入整份 `stages_json`(其中就有 `bank`、`merchantNormal`、`realnameQr`),只是只把 `LABELS` 的键映射进响应。
- `stages_json` 里除六个阶段外还住着三样**证据**:`bank`/`merchantNormal`(完成条件本身)、`realnameQr`(实名阶段的二维码)、以及本轮新增的 `merchantStat`(`merchantNormal` 的通道原值,用于写出「状态异常(CLOSED)」)。它们不是阶段:响应里的 `stages` 列表由 `LABELS` 生成,非 `LABELS` 的键不会进列表,完成判定也只认 `LABELS` 的六个键。旧记录没有 `merchantStat` 时回落到申请行的 `merchant_stat` 快照列。
- 能力刷新**没有任何去重或冷却**:`refresh(Long)` 无条件查通道,`capability(Long)` 只在 `capability_verified_at` 为空时查(不看多旧);唯一的新鲜度闸门是定时任务那条 SQL。
- `merchant_onboarding_application.updated_at` 是 MySQL `on update current_timestamp`,代码从不手写它。
- 迁移当前最大版本 **147**,下一个是 `V148`;本地/开发 Flyway 关闭(`application.yml`),只有测试 profile 打开,**新增列必须同时写迁移与 `MerchantOnboardingSchemaInitializer`**。
docs/adr/0001-merchant-onboarding-and-wechat-provisioning.md
@@ -15,3 +15,12 @@
**未完成微信实名认证与授权前,商户不允许收款。** “收款就绪”必须把微信支付业务开通与实名/授权纳入前提,不能只凭进件审核通过、结算银行卡与取现能力判定。这也意味着“等待实名”是收款能力的真实缺项,不是可以忽略的合规补课。
二维码展示不需要新做:商户端与后台详情页已经在渲染 `qrcode_data`。
## 2026-09-17 术语修订:收款能力与收款就绪分开
`CONTEXT.md` 现在把「收款能力」限定为**通道侧**的可用性事实(通道商户状态、取现与结算开关),把微信支付业务开通与实名/授权归入「收款就绪」。上文两处用词随之改读,**判定一字未动,只是两个词不再混用**:
- 标题句「不做“一次进件即完成收款能力”」里的“收款能力”指**收款就绪**;
- 「“等待实名”是收款能力的真实缺项」同理,指收款就绪的缺项。
这次修订由商户端「我现在能不能收款」的展示方式引出:同一页面上两种含义并排出现,商户无法分辨缺的是通道侧的事还是自己要办的事。缘由、真值来源与时效约定见 [ADR 0002](0002-merchant-capability-visibility.md)。
docs/adr/0002-merchant-capability-visibility.md
@@ -10,6 +10,8 @@
本条不改动 ADR-0001 的判定:进件与微信支付开通仍分阶段判定,「不增加定时查询」的取舍继续有效,本次不新增任何轮询。需要写明的是边界——能力快照的定时核实比 ADR-0001 更早存在(2026-09-12 落地),ADR-0001 约束的是**进件进度**,不是能力核实。写下这条边界,后来者才不会以为商户端展示收款状态是在绕开 ADR-0001。
另一处需要标注的是术语:本条把「收款能力」收窄成通道侧的事实之后,ADR-0001 里两处旧用词与它不再一致。已在 [ADR 0001](0001-merchant-onboarding-and-wechat-provisioning.md) 里以「2026-09-17 术语修订」一节显式标出并改读,判定未动。
## 考虑过但没选的
- **把 `bank`/`merchantNormal` 加进 `LABELS` 当第七、第八项阶段。** `complete()` 正是拿 `LABELS` 的键集做完成判定,且这两项是布尔值、其余六项是字符串 `confirmed`,加进去会两次破坏完成判定,进件将永远无法 approved。