Status: ready-for-agent
更新日期:2026-09-17
规格来源:本会话 grilling 已确认的决策,以及只读代码核查(MerchantOnboardingProgressService、MerchantPaymentReadinessService、MerchantPaymentCapabilityScheduler、两端页面)。
相关留档:汇付控台关闭商户后我们这边会怎样(结论证据不变,其中「遗留一/遗留二」由本规格落实)。
术语:见根 CONTEXT.md 的「商户进件/收款能力/收款就绪」三词;判定依据见 ADR 0002 与 ADR 0001。
汇付控台关闭商户后,我们的进件申请仍是 approved,进件页读的是本地快照、approved 之后商户又点不到「刷新进度」,于是页面一直显示「商户进件已完成 + 六项全已确认」,而商户其实已经收不了款。商户对环境变化的感知完全依赖「下一次下单被拒」。
同时存在一个已确认的矛盾态:一旦某次刷新发现通道商户状态异常,complete() 不成立、申请回到 pending,页面标题变成「请刷新进度核实各项能力,并按待办理项补齐缺失能力」,可展示的六项却全是「已确认」——因为完成判定还要求结算银行卡核实与通道商户状态这两项,而它们既不在 LABELS 里、view() 也不返回,商户看不到究竟缺什么,只会以为页面出错。
stages 保持六项、complete() 不动。| # | 决策 | 取舍理由 |
|---|---|---|
| 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,代码从不手写它。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 等)只做增量小改。MerchantPaymentReadinessServiceTests 的既有风格:@SpringBootTest + @ActiveProfiles("test") + @MockitoBean 通道 + 原始 SQL 造商户/门店/申请/银行卡,断言服务行为;本规格新增的派生判定与冷却都在这条边界上测,不新开测试专用接口。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、商户中心自查页、以及一次扫码下单,确认三处口径一致。前置:后端重启到含本轮改动的版本;商户端在 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)→ 用于"进件未通过"那一类 |
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——想看这个场景就别点刷新,或看完再点(那也是"以通道为准"的正常表现)。
yangye134 打开商户中心 →「收款能力」:显示「现在可以收款」+ 最近核实时间。
capability_verified_at,反复进出这个页面,该值不应变化(加载不查通道、不写库)。verify_merchant:显示「暂时无法收款 · 微信收款能力未就绪:微信支付业务开通、微信实名与授权,暂时无法收款」,onboarde001:显示「暂时无法收款 · 商户进件尚未审核通过,暂时无法收款」,按钮是「**暂不可核实**」,yangye134:
capability_verified_at 更新。update merchant_onboarding_application set capability_sync_until = date_sub(now(), interval 1 minute) where id = 452;update merchant_onboarding_application set merchant_stat = 'CLOSED' where id = 452;
merchantViewReusesTheGateMessage 直接拿闸门抛出的异常消息比对,人工这步是端到端确认。)NORMAL,页面回到可收款。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;
rencaika)页面改动、核销实现、交易下单与退款实现。MerchantPaymentReadinessService 里那句「微信收款能力未就绪:…」在术语上指的是**收款就绪**的前提,属存量措辞;本轮不改这段文案(会牵动既有测试断言),仅在此记录,留待下次收口。AGENTS.md 指向 CONTEXT.md 查「各业务域的代码位置」,而 CONTEXT.md 里没有代码位置(该指针悬空)。这是仓库级的小缺口,涉及所有业务域,本轮不动,另行提请。