商户端可见「我现在能不能收款」

Status: ready-for-agent
更新日期:2026-09-17
规格来源:本会话 grilling 已确认的决策,以及只读代码核查(MerchantOnboardingProgressService、MerchantPaymentReadinessService、MerchantPaymentCapabilityScheduler、两端页面)。
相关留档:汇付控台关闭商户后我们这边会怎样(结论证据不变,其中「遗留一/遗留二」由本规格落实)。
术语:见根 CONTEXT.md 的「商户进件/收款能力/收款就绪」三词;判定依据见 ADR 0002 与 ADR 0001。

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<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、商户中心自查页、以及一次扫码下单,确认三处口径一致。

人工验收(在共享开发库上,样本值 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)

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 的核心)

update merchant_onboarding_application set merchant_stat = 'CLOSED' where id = 452;
  1. 「收款能力」页应显示「暂时无法收款 · 斗拱子商户状态异常(CLOSED),暂时无法收款」。
  2. 用同一商户在商户端扫码下一笔到店消费单(首页扫码 → 提交):被拒的 toast 应与上面那句**逐字一致**。
    (等价性另有集成测试 merchantViewReusesTheGateMessage 直接拿闸门抛出的异常消息比对,人工这步是端到端确认。)
  3. 再点一次「立即核实」:汇付侧其实没关这家商户,快照会被真实查询纠正回 NORMAL,页面回到可收款。

E. 还原

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 里没有代码位置(该指针悬空)。这是仓库级的小缺口,涉及所有业务域,本轮不动,另行提请。