yeg
2026-09-17 a4328ed61653ae896a43bc92d8e4015496bd0fc8
docs(merchant): 立 ADR 0002,区分商户进件、收款能力与收款就绪

进件申请的六项阶段只回答「进件办到哪一步」,通道侧的商户可用性会随汇付控台操作随时变化,
两者不能互相代表。因此商户端凡是回答「我现在能不能收款」的地方一律以收款能力快照为准,
与下单闸门同源同文案;进件页只补出此前不可见的另外两项完成条件。

- CONTEXT.md:新增「收款能力」词条(通道侧事实),收紧「收款就绪」为某一时点的判定结果;
「微信待实名」末尾那半句随之由「收款能力的缺项」改指「收款就绪」。
- ADR 0002:写明与 ADR 0001 的边界(进件进度仍不自动轮询,能力核实另有链路),
并记下四条被否掉的替代方案。
- .scratch/merchant-capability-visibility/:本会话 grilling 的 12 条决策、实现约束与 5 张 ticket。
- 留档文档状态行加一句指向新规格,其余结论与证据不动。
7个文件已添加
2个文件已修改
238 ■■■■■ 已修改文件
.scratch/merchant-capability-visibility/issues/01-onboarding-completion-checks.md 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.scratch/merchant-capability-visibility/issues/02-merchant-payment-readiness-endpoint.md 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.scratch/merchant-capability-visibility/issues/03-manual-capability-verify-with-cooldown.md 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.scratch/merchant-capability-visibility/issues/04-onboarding-page-shows-completion-checks.md 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.scratch/merchant-capability-visibility/issues/05-merchant-center-payment-capability-page.md 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.scratch/merchant-capability-visibility/spec.md 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.scratch/merchant-closure-impact/issues/huifu-merchant-closure-impact.md 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
CONTEXT.md 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/adr/0002-merchant-capability-visibility.md 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.scratch/merchant-capability-visibility/issues/01-onboarding-completion-checks.md
New file
@@ -0,0 +1,25 @@
# 01 — 进件进度视图补出两项不可见的完成条件
**What to build:** 进件页现在会出现「标题说待办理、六项阶段却全绿」的矛盾态:完成判定 `MerchantOnboardingProgressService.complete()` 还要求 `bank`(原结算银行卡核实)与 `merchantNormal`(通道商户状态)为真,但这两项既不在 `LABELS` 里、`view()` 也不返回。本次在进件进度视图里补出这两项核实结果(`completionChecks`,形如 `[{key,label,status,reason}]`),**`stages` 保持六项、`complete()` 一字不动**。
为什么不能简单地加进 `LABELS`:`complete()` 用 `LABELS.keySet().stream().allMatch(...)` 做完成判定,而 `bank`/`merchantNormal` 是布尔值、其余六项必须是字符串 `"confirmed"`,加进去会两次破坏完成判定,进件将永远无法 approved。详见 [spec](../spec.md) 的「关键事实」与 [ADR 0002](../../../docs/adr/0002-merchant-capability-visibility.md)。
Blocked by: None
Status: 代码侧交付完成,待页面级人工验收
- [x] `view()` 返回新增字段,含 `bank` 与 `merchantNormal` 的当前核实结果与可读原因;两项都为真时不制造噪音(页面上不多出一行「已核实」)。
- [x] `stages` 仍恰好是 basic/wechat/appId/realname/delay/settlement 六项,`complete()` 判定与 `application.status` 的写入逻辑不变。
- [x] 文案用第二、三轮定的词汇:通道商户状态异常时写「斗拱子商户状态异常(<值>),请联系汇付或后台处理」,**不写「待办理」**——「待办理」只用于进件阶段。
- [x] `reason` 里那句「请刷新进度核实各项能力,并按待办理项补齐缺失能力」不再出现在六项全绿的情况下。
- [x] 不回归:`MerchantExistingOnboardingTests` 里 `onboardingStatus(...).get("stages")` 的列表相等断言仍通过。
- [ ] 页面级人工验收(见「验收」)。
## 验收
`GET /api/merchant/onboarding/status` 在 `merchantNormal=false`(或 `bank=false`)时状态仍为 `pending`,且响应能说清是哪一项没核实;商户端页面不再出现「六项全绿 + 标题待办理」的自相矛盾。
## 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 项基线失败。
.scratch/merchant-capability-visibility/issues/02-merchant-payment-readiness-endpoint.md
New file
@@ -0,0 +1,30 @@
# 02 — 商户端收款能力只读判定接口
**What to build:** 商户端目前没有任何接口暴露通道商户状态与最近核实时间,商户只能靠「下单被拒」感知。新增商户端只读接口 `GET /api/merchant/payment-readiness`:按登录 principal 的商户与门店解析当前申请,返回派生结果 `{ready, reason, verifiedAt, verifyAvailable, nextVerifyAt}`。
约定:
- `reason` 直接复用下单闸门 `MerchantPaymentReadinessService.requireCollectingMerchant` 抛出的同一句文案(即 `readiness(...).reason()`),**不新写一套判定**,否则页面与「下单被拒」会出现两套口径。
- 只返回派生结果,**不返回** `merchant_stat`/`cash_switch_status` 等通道原始值,也不返回逐项前提清单。
- 只读:接口**不触发任何汇付查询**(现有 `capability(Long)` 在 `capability_verified_at` 为空时会查通道,那是后台入口的语义,商户端只读不沿用它)。
- 落点沿用仓库约定:`MerchantWorkController`、`ApiResponse<Map<String,Object>>`、principal 经 `@RequestAttribute` 注入、门店取自 principal,不接受前端传入的申请号。
Blocked by: None
Status: 代码侧交付完成,待页面级人工验收
- [x] 未进件/待审核/已驳回的商户也能调用:判定为不可收款并给出对应原因(复用既有原因链,不新造文案)。
- [x] 已 approved 且快照正常的商户返回 `ready=true`,`verifiedAt` 为 `capability_verified_at`。
- [x] 快照从未核实(`unknown`)时返回不可收款与「待确认」原因,且**不查通道**。
- [x] 权限:只返回当前 principal 的商户与门店的判定;跨商户、跨门店不可见。
- [x] 新端点补进 `rencaika_merchant/docs/项目接口对接说明.md`。
- [ ] 页面级人工验收。
## 验收
造一个 `merchant_stat=NORMAL` 且能力列齐备的申请 → 接口返回 `ready=true`;把 `merchant_stat` 改成非 `NORMAL` → 返回不可收款,且 `reason` 与拿该商户扫码下单时拿到的 409 文案逐字一致。
## Comments
- 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`。
.scratch/merchant-capability-visibility/issues/03-manual-capability-verify-with-cooldown.md
New file
@@ -0,0 +1,32 @@
# 03 — 手动核实与冷却(capability_sync_until)
**What to build:** 新增 `POST /api/merchant/payment-readiness/verify`:触发一次通道查询刷新能力快照,受冷却保护,冷却期内直接返回当前快照(不查通道)并告知下次可核实时间。
冷却用新列 `capability_sync_until`:
- 迁移 `V148`,**并且**同步加到 `MerchantOnboardingSchemaInitializer`(本地/开发 Flyway 关闭,只有测试 profile 打开,新增列必须两处都写)。全仓当前最大版本 147。
- 查询成功与失败**都**推进冷却。不能用 `capability_verified_at`:查询失败时它不推进,通道查不通时按钮可被连点,等于没有节流。也不能用 `merchant_onboarding_application.updated_at`:那是 MySQL 的 `on update current_timestamp`,任何无关 UPDATE(例如进件进度刷新写 `status`)都会顺带重置它。
- 默认冷却 5 分钟,与进件刷新那套 5 分钟租约同量级;常量/配置项落点按仓库风格定。
Blocked by: 02
Status: 代码侧交付完成,待页面级人工验收
- [x] 冷却期内第二次调用不产生通道查询(用 mock 通道的调用次数断言)。
- [x] 查询失败(通道异常、结果未知)同样节流,失败信息按既有失败路径写 `capability_response_code`/`capability_response_message`,快照其余列不被覆盖。
- [x] 冷却结束后可再次核实,并推进 `capability_verified_at`。
- [x] 未 approved/缺商户号的申请按既有规则拒绝,不产生通道查询。
- [x] 权限与 02 相同:只按 principal 的商户与门店定位申请。
- [x] 测试沿用 `MerchantPaymentReadinessServiceTests` 风格(`@SpringBootTest` + mock 通道 + 直接造 `merchant_stat`)。
- [x] 迁移只追加、不改已执行迁移;跑测试用 `scripts/run-server-tests.ps1`。
- [x] 新端点补进 `rencaika_merchant/docs/项目接口对接说明.md`。
- [ ] 页面级人工验收。
## 验收
连点两次「立即核实」只产生一次通道查询;冷却后再次点击产生第二次并更新 `capability_verified_at`;把通道 mock 成抛异常,冷却同样生效且快照里的旧值不被清空。
## Comments
- 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` 的四个冷却用例(含"失败也节流"与"冷却过期后可再查")。
.scratch/merchant-capability-visibility/issues/04-onboarding-page-shows-completion-checks.md
New file
@@ -0,0 +1,26 @@
# 04 — 进件页展示两项完成条件
**What to build:** 商户端进件页(`rencaika_merchant/pages/merchant-onboarding-pending/merchant-onboarding-pending.uvue`)展示 01 补出的 `completionChecks`,消除「六项全绿 + 标题说待办理」的矛盾态;pending 时页面必须给出依据,不能只说一句笼统的话而不说缺什么。
同时按术语对齐文案:`statusTitle` 里 `pending` 分支那句「收款能力开通中」改成「进件办理中」——按 `CONTEXT.md` 的三层词义,「收款能力」只指通道侧的事实。
明确不做:**不**放开 approved 下「刷新进度」按钮。那一行前端改动会引入第二个口径不一致的入口(进件页刷新只写 `stages_json`、不碰能力快照),自查入口在 05。见 [ADR 0002](../../../docs/adr/0002-merchant-capability-visibility.md)。
Blocked by: 01
Status: 代码侧交付完成,待页面级人工验收
- [x] 进件页能显示 `bank`/`merchantNormal` 未核实或异常的原因,文案不含「待办理」(「待办理」只用于六项进件阶段)。
- [x] `stages` 仍渲染六项,渲染逻辑保持通用循环,不硬编码阶段名(现有仅 `realname` 例外)。
- [x] approved 时页面行为与现在一致(标题「商户进件已完成」、不出现刷新按钮)。
- [x] `services/settlement.uts` 里的 `SettlementOnboarding` 类型与映射补上新字段。
- [ ] 页面级人工验收。
## 验收
造 `merchantNormal=false` 的申请,进件页同时显示六项已确认与那两项完成条件的异常原因。
## Comments
- 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 分支同时改口径(见上),面板底部「缺失能力」也改成「缺失步骤」。
.scratch/merchant-capability-visibility/issues/05-merchant-center-payment-capability-page.md
New file
@@ -0,0 +1,25 @@
# 05 — 商户中心新增「收款能力」页
**What to build:** 商户中心(`rencaika_merchant/pages/merchant/merchant.uvue`)新增一行「收款能力」,进入只读页展示 `{ready, reason, verifiedAt}` 与一个「立即核实」按钮。
- 判定与文案来自 02 的接口,`reason` 与下单被拒的 toast 逐字一致;页面**不**展示 `merchant_stat` 等通道原始值。
- 「立即核实」调 03 的接口;冷却中按钮禁用并显示下次可核实时间;核实后刷新页面状态。
- 页面必须显示最近核实时间(`verifiedAt`)——它是这份快照对外唯一的时效证据,快照最迟 6 小时。
- 未进件/待审核/已驳回的商户进这一页也应得到可读结论(接口已覆盖,页面不要自行兜底成「可收款」)。
落点沿用仓库约定:新页面路径 + `pages.json` 注册(`navigationStyle: custom`);商户中心那一行是硬编码 markup + `handleMenuAction` 里的字符串分支(没有可复用列表组件);接口调用新增/扩展 `services/` 下模块,路径集中在 `config/api.uts` 的 `getApiEndpoint`。
Blocked by: 02, 03
Status: 代码侧交付完成,待页面级人工验收
- [x] 商户中心出现「收款能力」入口,可进入新页面。
- [x] `ready=true` 时显示可收款与最近核实时间;`ready=false` 时显示原因与最近核实时间。
- [x] 快照从未核实时显示待确认原因,且「立即核实」可点。
- [x] 冷却中按钮禁用并显示下次可核实时间;冷却结束自动恢复可点。
- [ ] 页面验收:把测试库 `merchant_stat` 改成非 `NORMAL`,页面与「扫码下单被拒」的文案一致;改回 `NORMAL` 并核实后恢复可收款。
## 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` 组件,与商户中心一致。
.scratch/merchant-capability-visibility/spec.md
New file
@@ -0,0 +1,70 @@
# 商户端可见「我现在能不能收款」
Status: ready-for-agent
更新日期:2026-09-17
规格来源:本会话 grilling 已确认的决策,以及只读代码核查(`MerchantOnboardingProgressService`、`MerchantPaymentReadinessService`、`MerchantPaymentCapabilityScheduler`、两端页面)。
相关留档:[汇付控台关闭商户后我们这边会怎样](../merchant-closure-impact/issues/huifu-merchant-closure-impact.md)(结论证据不变,其中「遗留一/遗留二」由本规格落实)。
术语:见根 `CONTEXT.md` 的「商户进件/收款能力/收款就绪」三词;判定依据见 [ADR 0002](../../docs/adr/0002-merchant-capability-visibility.md) 与 [ADR 0001](../../docs/adr/0001-merchant-onboarding-and-wechat-provisioning.md)。
## Problem Statement
汇付控台关闭商户后,我们的进件申请仍是 `approved`,进件页读的是本地快照、approved 之后商户又点不到「刷新进度」,于是页面一直显示「商户进件已完成 + 六项全已确认」,而商户其实已经收不了款。商户对环境变化的感知完全依赖「下一次下单被拒」。
同时存在一个已确认的矛盾态:一旦某次刷新发现通道商户状态异常,`complete()` 不成立、申请回到 `pending`,页面标题变成「请刷新进度核实各项能力,并按待办理项补齐缺失能力」,可展示的六项却全是「已确认」——因为完成判定还要求结算银行卡核实与通道商户状态这两项,而它们既不在 `LABELS` 里、`view()` 也不返回,商户看不到究竟缺什么,只会以为页面出错。
## Solution
1. **进件页只讲进件**:补出完成判定里此前不可见的两项核实结果(结算银行卡核实、通道商户状态),让「为什么还没完成」永远有对应的可见原因;`stages` 保持六项、`complete()` 不动。
2. **收款状态另有入口**:商户中心新增只读「收款能力」页,展示与下单闸门同源、同文案的判定结果与最近核实时间,并提供一次受冷却保护的手动核实。
3. **术语收敛**:区分「商户进件」(一次性流程)、「收款能力」(通道侧事实)、「收款就绪」(某一时点的判定结果),两端文案按此对齐。
## Implementation Decisions(本会话已确认)
| # | 决策 | 取舍理由 |
| --- | --- | --- |
| D1 | 本轮只做「遗留一 + 遗留二」;不缩短 6 小时窗口、不改下游下单被拒文案、不实测核销后的交易确认 | 后两者属支付链路的交易/确认路径,另开一轮 |
| D2 | 商户端「能不能收款」的真值来源=**能力快照**,与 `requireCollectingMerchant` 同源 | 与闸门同源才不会出现「页面说能收、下单被拒」 |
| D3 | 术语分三层:商户进件/收款能力/收款就绪 | 现在两词混用,矛盾态文案无处可落 |
| D4 | 先 ADR-0002 + ticket,再改代码 | ADR-0001 挡在「自动刷新」那条路前面,边界要先写清 |
| D5 | 进件页新增字段**只**承载 `bank` 与 `merchantNormal`(暂定名 `completionChecks`) | 它应当恰好是 `complete()` 里看不到的那两项,不能把「收款就绪」提前塞进进件页 |
| 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` 与下单被拒文案逐字一致;不返回通道原始值、不返回逐项前提清单 | 只读接口不查通道,避免第二个判定口径;客户端不需要自己解析时间戳判断冷却 |
| D10 | 不新增任何定时任务;进件进度仍不自动轮询 | 沿用 ADR-0001 由用户明确选择的取舍,本次不修改它 |
| D11 | 后端加集成测试(mock 通道 + 直接造 `merchant_stat`),前端人工验收 | 无法真的让汇付关商户,被关状态一律造出来 |
| D12 | 商户端只读当前 principal 的商户与门店,不信任前端传入的申请号 | 沿用仓库既有权限边界 |
## 关键事实(实现时必须遵守)
- `LABELS` 同时是展示列表与完成判定的键集:`MerchantOnboardingProgressService.complete()` 用 `LABELS.keySet().stream().allMatch(...)`,且 `bank`/`merchantNormal` 是**布尔值**而其余六项必须是字符串 `"confirmed"`。把它们加进 `LABELS` 会两次破坏完成判定。
- `view()` 已经读入整份 `stages_json`(其中就有 `bank`、`merchantNormal`、`realnameQr`),只是只把 `LABELS` 的键映射进响应。
- 能力刷新**没有任何去重或冷却**:`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`**。
- 全仓没有任何限流/节流工具;唯一的窗口模式是 `merchant_onboarding_progress` 的 `sync_owner`/`sync_until` 5 分钟租约。
- 商户端接口约定:`ApiResponse<Map<String,Object>>`、principal 经 `@RequestAttribute` 注入、门店取自 principal(`X-Merchant-Store-Id` 可选,缺省回落第一个门店)。
- 共享文件(`CONTEXT.md`、`application.yml`、`AdminManagementController` 等)只做增量小改。
## Testing Decisions
- 后端沿用 `MerchantPaymentReadinessServiceTests` 的既有风格:`@SpringBootTest` + `@ActiveProfiles("test")` + `@MockitoBean` 通道 + 原始 SQL 造商户/门店/申请/银行卡,断言服务行为;本规格新增的派生判定与冷却都在这条边界上测,不新开测试专用接口。
- 需要断言「冷却期内不产生通道查询」时,用 mock 通道的调用次数断言,不锁内部方法顺序。
- 前端不新增自动化:仓库目前没有商户端测试基建,页面验收走人工。
- 跑测试用 `talent-card-server/scripts/run-server-tests.ps1`,遵守 `docs/agents/testing.md`(已执行迁移只追加、不 `drop database`、同一 checkout 一次只跑一个 Maven 构建、3 项基线失败不重复验证)。
- 人工验收:改测试库把 `merchant_stat` 从 `NORMAL` 改成别的值(以及改回),走 `GET /api/merchant/onboarding/status`、商户中心自查页、以及一次扫码下单,确认三处口径一致。
## Out of Scope
- 缩短能力快照 6 小时窗口、下单前实时校验、提高定时核实频率。
- 下游「下单被拒」的 toast 文案改动(人才端与商户端)。
- 核销后交易确认在关闭商户下能否成功(属支付链路,需实测)。
- 商户端/人才端的任何前端自动化测试基建。
- 人才端(`rencaika`)页面改动、核销实现、交易下单与退款实现。
- 给进件进度新增定时轮询或页面自动轮询(ADR-0001 已由用户选择不做)。
## Further Notes
- 留档文档里「怎么验」的第 1、3 步(控台关户 → 后台点能力刷新 → 查库看通道把哪些字段改成了什么)仍然有效,只是本轮不依赖真实控台操作也能验收:直接改测试库即可造出同样的快照。
- `MerchantPaymentReadinessService` 里那句「微信收款能力未就绪:…」在术语上指的是**收款就绪**的前提,属存量措辞;本轮不改这段文案(会牵动既有测试断言),仅在此记录,留待下次收口。
- `AGENTS.md` 指向 `CONTEXT.md` 查「各业务域的代码位置」,而 `CONTEXT.md` 里没有代码位置(该指针悬空)。这是仓库级的小缺口,涉及所有业务域,本轮不动,另行提请。
.scratch/merchant-closure-impact/issues/huifu-merchant-closure-impact.md
@@ -1,6 +1,10 @@
# 在汇付合作伙伴控台关闭商户后,我们这边会怎样
状态:**仅留档,不改代码**。下面「遗留一 / 遗留二」是已确认的体验缺陷,用户明确本轮不动。
状态:**仅留档,不改代码**。下面「遗留一 / 遗留二」是已确认的体验缺陷,当时用户明确本轮不动。
**2026-09-17 更新**:这两个缺陷已另立规格落实,见
[商户端可见「我现在能不能收款」](../../merchant-capability-visibility/spec.md)(ADR 见
`docs/adr/0002-merchant-capability-visibility.md`)。本文档其余结论与证据不变,仅作留档。
汇付的说法:控台关闭商户后该商户的所有支付相关功能受限,无法继续收款,微信/支付宝等已开通能力不可用;
恢复需要在控台重新启用商户。
CONTEXT.md
@@ -45,6 +45,8 @@
**微信支付开通**:商户取得在平台小程序内接受微信付款所需业务能力的过程。开通失败可以单独重试,不否定已经通过的基本资料审核。
**微信待实名**:微信支付开通过程中,仍需完成微信实名认证与授权的阶段,不属于开通失败。实名认证走法人(`contact_type=LEGAL`),申请以法人信息发起;申请单审核通过(`APPLYMENT_STATE_PASSED`)不等于授权完成,还要由本人扫描通道回传的小程序码、并满足 `authorize_stat=1`。这是收款能力的真实缺项,不是可以忽略的合规补课。
**微信待实名**:微信支付开通过程中,仍需完成微信实名认证与授权的阶段,不属于开通失败。实名认证走法人(`contact_type=LEGAL`),申请以法人信息发起;申请单审核通过(`APPLYMENT_STATE_PASSED`)不等于授权完成,还要由本人扫描通道回传的小程序码、并满足 `authorize_stat=1`。这是**收款就绪**的真实缺项,不是可以忽略的合规补课。
**收款就绪**:商户可以接受微信付款的状态。除基本进件通过、结算银行卡可用、延迟入账与自动结算已核实外,还必须已开通微信支付业务并完成微信实名认证与授权;未完成实名前不允许收款。
**收款能力**:通道侧对这家子商户当下是否可用的判定事实,即通道商户状态(`merchant_stat`)与取现、结算开关。它不由平台决定,会随汇付控台的操作随时改变——进件通过不代表它一直正常,商户被通道停用后它会变成异常。它**不等于**收款就绪:这里只描述通道侧的事实,不含我们要求的微信支付开通与实名等前提,也不等同于进件阶段。
**收款就绪**:商户可以接受微信付款的状态,是某一时点的判定结果,不是进件的终态。除基本进件通过、结算银行卡可用、延迟入账与自动结算已核实、**收款能力**正常外,还必须已开通微信支付业务并完成微信实名认证与授权;未完成实名前不允许收款。收款能力一旦异常,收款就绪随之回退,所以它的对外可信度以最近核实时间为准。
docs/adr/0002-merchant-capability-visibility.md
New file
@@ -0,0 +1,18 @@
# 商户端可见的收款状态以能力快照为准
进件申请的六项阶段只回答「进件办到哪一步」,而通道侧的商户可用性(`merchant_stat`、取现与结算开关)会随汇付控台的操作随时变化,两者不能互相代表。因此商户端凡是回答「我现在能不能收款」的地方,一律以**收款能力快照**为准——与下单闸门 `MerchantPaymentReadinessService.requireCollectingMerchant` 读同一份数据、给出同一句文案;进件页只补出此前不可见的另外两项完成条件(结算银行卡核实、通道商户状态),**不把通道状态混进六项阶段**。
时效由既有的能力定时核实(30 分钟一轮、只扫超过 6 小时未核实的申请)与商户端「立即核实」的手动刷新共同保证。手动刷新带冷却(`capability_sync_until`,冷却期内直接回当前快照),**不新增任何定时任务**;进件进度仍只由回调与手动刷新推进。页面必须显示最近核实时间,因为它是这份快照对外唯一的时效证据。
商户端只看到派生结果(能否收款 + 原因 + 最近核实时间),不返回 `merchant_stat`、`cash_switch_status` 等通道原始值;原因文案与下单被拒时逐字一致,避免同一个事实出现两套口径。
## 与 ADR-0001 的关系
本条不改动 ADR-0001 的判定:进件与微信支付开通仍分阶段判定,「不增加定时查询」的取舍继续有效,本次不新增任何轮询。需要写明的是边界——能力快照的定时核实比 ADR-0001 更早存在(2026-09-12 落地),ADR-0001 约束的是**进件进度**,不是能力核实。写下这条边界,后来者才不会以为商户端展示收款状态是在绕开 ADR-0001。
## 考虑过但没选的
- **把 `bank`/`merchantNormal` 加进 `LABELS` 当第七、第八项阶段。** `complete()` 正是拿 `LABELS` 的键集做完成判定,且这两项是布尔值、其余六项是字符串 `confirmed`,加进去会两次破坏完成判定,进件将永远无法 approved。
- **商户端每次进页面实时查通道。** 违反 ADR-0001 由用户明确选择的取舍,并把通道查询量交给页面打开次数。
- **用 `capability_verified_at` 充当冷却。** 查询失败时该列不推进,通道查不通时按钮可被连点,等于没有节流。
- **复用进件进度表的 `sync_owner`/`sync_until`。** 会把进件与能力两条本该独立的链路绑在一起。