# 商户端可见「我现在能不能收款」 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, retryAfterSeconds}`(手动核实另有 `refreshed`,且每条返回路径都必须带上它),`reason` 与下单被拒文案逐字一致;不返回通道原始值、不返回逐项前提清单 | 只读接口不查通道,避免第二个判定口径;`retryAfterSeconds` 让客户端在冷却结束时就地恢复按钮,不必自己解析数据库时间戳;`refreshed` 说明"这次到底查没查通道",通道校验关掉时不能凭空宣称刚核实过 | | 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` 的键映射进响应。 - `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`**。 - 全仓没有任何限流/节流工具;唯一的窗口模式是 `merchant_onboarding_progress` 的 `sync_owner`/`sync_until` 5 分钟租约。 - 商户端接口约定:`ApiResponse>`、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`、商户中心自查页、以及一次扫码下单,确认三处口径一致。 ## 人工验收(在共享开发库上,样本值 2026-09-17 校对) 前置:后端重启到含本轮改动的版本;商户端在 HBuilderX 重新编译运行(开发者工具默认连 `http://192.168.110.26:8080`,与共享开发库 `192.168.110.13:3306/talent_card` 配套)。改库前记下原值, 验完按最后一节还原。 可用样本(`merchant_operator.account` → 申请): | 账号 | merchant / store | 申请 | 当前状态 | | --- | --- | --- | --- | | `yangye134`(个人进件验证商户) | 632 / 573 | `application 452` | approved,能力快照 NORMAL/取现 1/自动结算 1,六项阶段全 confirmed → 收款就绪 | | `verify_merchant`(存量商户验证专用) | 631 / 572 | `application 448` | approved 且快照正常,但**没有进度记录** → 卡在微信开通与实名 | | `onboarde001`(企业进件验证商户) | 633 / 574 | `application 449` | pending(`wechat=missing`)→ 用于"进件未通过"那一类 | ### A. 进件页不再自相矛盾(票 01、04) ```sql update merchant_onboarding_progress set stages_json = json_set(stages_json, '$.merchantNormal', false, '$.merchantStat', 'CLOSED') where application_id = 452; ``` 用 `yangye134` 打开商户中心 →「商户进件」,应看到:标题「进件办理中」(原来是「收款能力开通中」); 六项阶段仍全部「已确认」;多出一段「完成条件核实」,其中「通道商户状态」是「失败」+ 「斗拱子商户状态异常(CLOSED),请联系汇付或后台处理」;顶部说明是「进件阶段已全部确认,但通道商户 状态未通过核实…」,**不再**是「请按待办理项补齐缺失能力」。把 `$.bank` 改成 `false` 可看到 「结算银行卡核实」那一条。 注意:此时点页面上的「刷新进度」会真去查通道、把 `merchantNormal` 写回真实值,这一页就恢复成 approved——想看这个场景就别点刷新,或看完再点(那也是"以通道为准"的正常表现)。 ### B. 收款能力自查(票 02、05) `yangye134` 打开商户中心 →「收款能力」:显示「现在可以收款」+ 最近核实时间。 - **只读验证**:记下 `capability_verified_at`,反复进出这个页面,该值不应变化(加载不查通道、不写库)。 - `verify_merchant`:显示「暂时无法收款 · 微信收款能力未就绪:微信支付业务开通、微信实名与授权,暂时无法收款」, 按钮仍是可点的「立即核实」(它缺的是微信开通与实名,不是通道侧的事,所以允许重新核实)。 - `onboarde001`:显示「暂时无法收款 · 商户进件尚未审核通过,暂时无法收款」,按钮是「**暂不可核实**」, 且没有「下次可核实」那一行——没有冷却,只是没有可核实的对象。 ### C. 手动核实与冷却(票 03) `yangye134`: 1. 点「立即核实」→ 后端日志出现一次汇付能力查询,最近核实时间刷新,`capability_verified_at` 更新。 2. 立刻再点:按钮变成「4:5x 后可再次核实」并逐秒递减;此时强点会提示「刚核实过,请稍后再试」。 3. **留在页面上**等倒计时归零:按钮应自己变回「立即核实」(本轮新加的行为,不必退出重进)。 4. 不想等:`update merchant_onboarding_application set capability_sync_until = date_sub(now(), interval 1 minute) where id = 452;` 再点一次即可再查。 ### D. 页面文案 == 下单被拒文案(票 02 的核心) ```sql update merchant_onboarding_application set merchant_stat = 'CLOSED' where id = 452; ``` 1. 「收款能力」页应显示「暂时无法收款 · 斗拱子商户状态异常(CLOSED),暂时无法收款」。 2. 用同一商户在商户端扫码下一笔到店消费单(首页扫码 → 提交):被拒的 toast 应与上面那句**逐字一致**。 (等价性另有集成测试 `merchantViewReusesTheGateMessage` 直接拿闸门抛出的异常消息比对,人工这步是端到端确认。) 3. 再点一次「立即核实」:汇付侧其实没关这家商户,快照会被真实查询纠正回 `NORMAL`,页面回到可收款。 ### E. 还原 ```sql update merchant_onboarding_application set merchant_stat = 'NORMAL', capability_sync_until = null where id = 452; update merchant_onboarding_progress set stages_json = json_remove(json_set(stages_json, '$.merchantNormal', true), '$.merchantStat') where application_id = 452; ``` ## Out of Scope - 缩短能力快照 6 小时窗口、下单前实时校验、提高定时核实频率。 - 下游「下单被拒」的 toast 文案改动(人才端与商户端)。 - 核销后交易确认在关闭商户下能否成功(属支付链路,需实测)。 - 商户端/人才端的任何前端自动化测试基建。 - 人才端(`rencaika`)页面改动、核销实现、交易下单与退款实现。 - 给进件进度新增定时轮询或页面自动轮询(ADR-0001 已由用户选择不做)。 ## Further Notes - 留档文档里「怎么验」的第 1、3 步(控台关户 → 后台点能力刷新 → 查库看通道把哪些字段改成了什么)仍然有效,只是本轮不依赖真实控台操作也能验收:直接改测试库即可造出同样的快照。 - `MerchantPaymentReadinessService` 里那句「微信收款能力未就绪:…」在术语上指的是**收款就绪**的前提,属存量措辞;本轮不改这段文案(会牵动既有测试断言),仅在此记录,留待下次收口。 - `AGENTS.md` 指向 `CONTEXT.md` 查「各业务域的代码位置」,而 `CONTEXT.md` 里没有代码位置(该指针悬空)。这是仓库级的小缺口,涉及所有业务域,本轮不动,另行提请。