适用提交: 6c42f8f | 最后验证: 2026-09-18 | 何时会失效: 六阶段
LABELS的键集、merchant_onboarding_progress的表结构、或MerchantPaymentReadinessService.requireCollectingMerchant的闸门条件被改动
本文件回答三件事:商户怎么进件、**什么状态下能收款**、以及这条链路最容易再犯的坑。
一句话结论:**进件通过 ≠ 能收款**——能不能收款由「通道侧的收款能力快照」加上「微信业务开通与实名授权」共同决定。**【推断】**(判定与文案可逐行核对,通道侧证据与四个阶段见下文)
正文严格使用根目录 CONTEXT.md 的术语,下表同时给出**禁用同义词**(写错会被误读):
| 术语 | 含义 | 不要写成 |
|---|---|---|
| 商户进件 | 商户提交主体、经营与结算资料、申请成为汇付商户的过程;主体分企业(含个体工商户)与个人(小微) | 入驻、开户 |
| 微信支付开通 | 商户取得在平台小程序内接受微信付款所需业务能力的过程;**失败可单独重试**,不否定已通过的基本资料审核 | 「微信开通成功」(那句话包含实名,见下) |
| 微信待实名 | 微信支付开通过程中仍需完成微信实名认证与授权的阶段,**不属于开通失败** | 合规补课 |
| 收款能力 | 通道侧这家子商户当下是否可用的判定事实(通道商户状态、取现与结算开关)。它不由平台决定,会被汇付控台随时改掉 | 收款就绪(两者必须分开,见第 4 节) |
| 收款就绪 | 某一时点「商户能不能接受微信付款」的判定结果,是基本进件、结算银行卡、延迟入账、自动结算、**收款能力**、微信支付开通与**实名授权**的合取;不是进件的终态 | 进件完成 |
| 人才端 / 商户端 | 人才端=人才买商品并微信付款的小程序;商户端=办理进件、创建到店消费订单、核销的小程序 | 用户端 / 商家端 |
| 预支付单 | 向通道发起一次支付请求后,通道生成的、可被付款的凭证 | 支付单、预订单(会与本地订单混同) |
| 支付尝试 / 关单 / 支付结果不确定 / 悬账 | 定义见 CONTEXT.md;本链路只在「闸门说不能收款、但用户手上还有一张预支付单」这类交界处用到它们 |
取消、关闭订单、差错 |
| 延迟入账 / 交易确认 / 延迟户 | 未核销的钱停在通道延迟户(官方定义:不可结算、不可取现),核销成功才调交易确认分账到商户基本户 | 中间户、核销即到账 |
置信度四档(【官方原件】/【官方·外部】/【实测】/【推断】)的定义见 docs/huifu/README.md。
术语表与 CONTEXT.md 的表述一致,但它本身是**仓库术语表**、不是官方文档;其中「延迟户不可结算、不可取现」这类**官方定义**目前只是转述(仓库内没有对应官方页面原件,属【官方·外部】)。
本文没有任何一条结论以汇付官方确认为依据(2026-09-18 全仓核查:仓库里不存在汇付官方回复的留档):凡我方自测得到的结论一律标【实测】,并注明「自测结论,汇付侧未书面确认」。
sequenceDiagram
autonumber
participant M as 商户端(rencaika_merchant)
participant S as 后端
participant H as 斗拱/汇付
participant A as 后台(talent-card-admin)
M->>S: POST /api/merchant/onboarding (X-Idempotency-Key)
S->>S: 建进件(pending) + 银行卡(pending);已有 active 进件则只补门店照
alt 企业主体(含个体工商户)
S->>H: v2.merchant.basicdata.ent(先传 F07/F02/F03/F13… 取 fileId)
else 个人(小微)主体
S->>H: v2.merchant.basicdata.indv(先传 F40/F41/F13/F22)
end
H-->>S: huifu_id + apply_no(同步响应即可推进下一步,不等 token_no)
S->>H: v2.merchant.busi.open(wx_conf_list 场景 4/5/12、delay_flag=Y、forced_delay_flag=N)
H-->>S: 90000000「审核中」=已受理(不是失败)
Note over S: 配置的闸门取通道证据:qry_wx_conf_list 里真的出现线上场景才发下一条
S->>H: v2.merchant.busi.config(绑定人才端小程序 AppID)
H-->>S: 00000000 或驳回
H->>S: 回调 merchant-onboarding-notify / merchant-business-open-notify / merchant-wechat-config-notify
Note over S: 回调先 select ... for update 锁行进件,配置一律推迟到事务提交后(runAfterCommit)
M->>S: POST /api/merchant/onboarding/refresh
A->>S: POST /admin/management/merchant-settlement/onboardings/{id}/progress/refresh
S->>H: status.query + basicdata.query + busi.config.query + busi.realname.query
H-->>S: apply_status / qry_wx_conf_list / qry_settle_config_list / delay_flag / authorize_stat / qrcode_data
S->>S: 写 stages_json(六阶段)+ completionChecks(另两项核实)
M->>S: POST /api/merchant/onboarding/realname(或后台代为发起)
S->>H: v2.merchant.busi.realname(contact_type=LEGAL,走法人本人资料)
H-->>S: applyment_stat + qrcode_data(裸 base64)
Note over M: 授权只能由本人扫通道回传的小程序码完成,authorize_stat=1 才算授权成功
S->>H: v2.merchant.busi.modify(delay_flag=Y / forced_delay_flag=N)
S->>S: 六阶段 + bank + merchantNormal 全过 → approved → 提交后刷一次能力快照
兜底说明(图看不懂时按这段理解):进件一次收齐资料,之后**按阶段推进**——基本审核 → 微信支付业务开通 → 人才端 AppID 绑定 → 实名与授权 → 延迟入账 → 自动结算。推进只由**回调**与**人工刷新**两件事触发(ADR-0001,用户明确选择「不加定时轮询」);没有回调且没人刷新时,进度保持最后已知状态。**[实测]**(本链路每一步都在真实通道上跑过:企业路径走通过,个人路径 2026-09-16 逐字段修到受理成功)
flowchart TD
START["下单 / 发起支付"] --> G1{"有过 approved 的进件?<br/>initial_bank_card_id 非空"}
G1 -->|"否"| B1["拒: 商户进件尚未审核通过"]
G1 -->|"是"| G2{"该门店有 approved 卡<br/>且 provider_token_no 非空?"}
G2 -->|"否"| B2["拒: 结算银行卡尚未绑定成功"]
G2 -->|"是"| G3{"快照完整?<br/>merchant_stat、cash_switch_status、<br/>auto_settle_status、capability_verified_at 都非空"}
G3 -->|"否"| B3["拒: 商户收款能力待确认"]
G3 -->|"是"| G4{"cash_switch_status = 1 或 Y?"}
G4 -->|"否"| B4["拒: 商户取现能力不可用"]
G4 -->|"是"| G5{"merchant_stat = NORMAL?"}
G5 -->|"否"| B5["拒: 斗拱子商户状态异常<br/>文案里带上真实值"]
G5 -->|"是"| G6{"分阶段进度里<br/>wechat 与 realname 都 confirmed?"}
G6 -->|"否"| B6["拒: 微信收款能力未就绪,缺哪项报哪项"]
G6 -->|"是"| OK["放行: 返回 CollectingMerchant<br/>applicationId + storeId + huifuId"]
OK --> BIND["子商户号落到订单上<br/>collect_huifu_id,之后不再漂移"]
兜底说明:这是 MerchantPaymentReadinessService.requireCollectingMerchant 的唯一判定点,订单/支付侧多个入口共用它。顺序即「先报哪个原因」的优先级:**进件 → 结算卡 → 快照是否被核实过 → 取现开关 → 通道商户状态 → 微信开通与实名**。
快照的新鲜度由既有的 30 分钟定时核实(只挑超过 stale-hours=6 小时未核实的申请)与商户端「立即核实」(受 5 分钟冷却保护)维护——**所以通道侧被关停到我们发现之间最长有约 6 小时空窗**。**[实测]**(判定链与 6 小时窗口都在真实库/真实通道上核对过;另见第 5 节坑 2)
key(LABELS) |
展示名 | 「已确认」的通道证据 |
|---|---|---|
basic |
基础审核 | apply_status=Y 且 reg_status=S |
wechat |
微信支付 | qry_wx_conf_list 里 4/5/12 三场景齐全,且每场景 fee_rate=0.70、fee_min_amt 缺失或为 0、fee_charge_type=1、out_fee_flag=2、wx_mer_infos 里有 bank_mer_code 且 is_default=1 |
appId |
人才端绑定 | busi.config.query 的 atu_config_bo_list 中 fee_type=01 的 wx_config_list 里出现人才端 AppID;control_flag=Y 记 action_required |
realname |
实名与授权 | 只看 authorize_stat:=1 → confirmed;APPLYMENT_STATE_REJECTED → failed;NOAPPLY/CANCELED → missing;EDITTING/WAITTING_FOR_AUDIT → pending;其余 → action_required |
delay |
延迟入账 | 商户详情 delay_flag=Y 且 forced_delay_flag=N;不满足时由 v2.merchant.busi.modify 发起(落 merchant_onboarding_attempt,step='delay') |
settlement |
自动结算 | qry_settle_config_list 恰好 1 条,且 settle_status=1、settle_cycle=D1、settle_pattern=P0、min_amt=0、remained_amt=0 |
三处反直觉约束,都是踩过的:
LABELS 同时是展示列表与完成判定键集。 complete() 拿它的键集做「六项全 confirmed」判定,所以**加第 7 项阶段会两次破坏完成判定**:bank/merchantNormal 是布尔值,而六项必须是字符串 "confirmed"。这两项因此**不进 LABELS**,只通过 view() 的 completionChecks 补出(ADR-0002「考虑过但没选的」)。**[实测]**processing 不会被自动推进成 approved。 推进步骤的条件只认 blank/not_started/failed;而 approved 是能力快照刷新与「整单完成」的唯一入口。无回调的部署里若不按通道证据收口,商户明明开通了却永远推不到收款就绪。**[推断]**(代码逻辑可推;audit.md 记了当时「processing 永远不会被推进」这一现象)supports() 决定这条申请走分阶段模型还是旧单状态模型:要求 initial_bank_card_id 非空且 status != 'rejected'。被驳回的申请留在旧模型,商户端才有「重新提交」入口;反之分阶段申请**重复提交不新建进件**(不会在通道上多出第二家商户),只补空着的门店照。**[推断]**(代码与 MerchantExistingOnboardingTests 可核对)| 位置 | 存什么 | 谁写它 |
|---|---|---|
merchant_onboarding_application.status |
pending/approved/rejected(**整单**状态,≠ 六阶段) |
进度刷新(基本被驳回→rejected;六项+两项核实全过→approved;否则 pending),以及进件提交时的通道拒绝 |
同表 business_open_status / wechat_config_status |
not_started/processing/approved/failed |
主动推进(advanceBusinessOpen → advanceWechatConfig)与两个回调 |
同表 realname_status |
processing/approved/failed |
MerchantRealnameService.submit 与实名回调 |
| 同表 能力快照列 | merchant_stat/auto_settle_status/cash_switch_status/cash_type/capability_verified_at/capability_sync_until |
MerchantPaymentReadinessService.queryProvider(在**另一条连接**上写,见下) |
merchant_onboarding_progress.stages_json |
六阶段 + 两个布尔证据(bank/merchantNormal)+ merchantStat/realnameQr |
MerchantOnboardingProgressService.refresh |
merchant_onboarding_attempt |
延迟入账申请的流水号与结果(step='delay') |
advanceDelay |
merchant_bank_card |
卡的 status 与 provider_token_no |
绑卡回调;以及刷新时按「通道卡列表里有这张卡 且 它挂在结算配置上」收口 |
推进入口只有两个(ADR-0001):通道回调(merchant-onboarding-notify、merchant-business-open-notify、merchant-wechat-config-notify、绑卡回调 merchant-business-notify)与人工刷新(商户端「刷新进度」、后台刷新)。**没有定时轮询**——这是用户明确选的取舍,不要顺手加。
两处必须遵守的写法(写错会直接造成线上故障,都是踩过的):
select ... for update 锁住进件行,**后续配置必须 runAfterCommit 到事务提交后**再做。在事务里同步调汇付 HTTP=拿着行锁 + 一条数据库连接等一个不受控的网络响应,慢一点连接池就借光,**整张 merchant_onboarding_application 上的查询全部排队**(2026-09-15 后台全站不可用就是这个形态)。**[实测]**REQUIRES_NEW / PROPAGATION_NOT_SUPPORTED(另一条连接),且**不要**给该行加 for update:锁这一行挡不住并发刷新,却会和调用方已持有的行锁撞成 CannotAcquireLockException,结果是「进件通过却永远无法收款」。**[推断]**(代码与注释可核对)merchant_stat、取现开关、结算开关),随控台操作随时变化;进件通过不代表它一直正常。**[实测]**(被关停的子商户查结算记录时通道回「客户与产品关系已关闭」)confirmed。未完成实名前不允许收款——**这是收款就绪的真实缺项,不是可忽略的合规补课**(ADR-0001 2026-09-16 修订)。**[推断]**(判定与文案在 MerchantPaymentReadinessService 里可逐行核对)merchant_stat 等通道原值;页面文案与下单被拒的文案**逐字一致**(同源同文案,ADR-0002,有测试 merchantViewReusesTheGateMessage 钉住)。能力快照字段语义(列名在 merchant_onboarding_application 上,来源 v2.merchant.basicdata.query):
| 存储列 | 通道字段 | 就绪取值 | 当前闸门是否仍要求 |
|---|---|---|---|
merchant_stat |
mer_stat |
NORMAL(其它值一律拦) |
是(【实测】) |
cash_switch_status |
qry_cash_config_list[].switch_state |
1(或 Y)可用 |
是(【实测】) |
auto_settle_status |
qry_settle_config_list[].settle_status |
历史上要求 0(自动结算关闭) |
否——该规则已删除(见坑 1)。今天它只参与「快照是否完整」:字段缺失即判「商户收款能力待确认」(【实测】,值 1=开启是真实通道语义,D1 自动结算生效时读回就是 1) |
cash_type |
qry_cash_config_list[].cash_type |
D0/D1/T1/DM |
否,只落库备查(【官方·外部】,取值域来自汇付字段表,仓库内无原件) |
capability_verified_at |
查询时间戳 | 只有查询成功才推进 | 是(【实测】,空=快照不可信) |
capability_sync_until |
无(本地列,V151) | 商户端「立即核实」的冷却截止 | 是——**不能用 capability_verified_at 当冷却**:查询失败时那列不推进,按钮会被连点(【实测】) |
app.payment.huifu.enabled=false(本地/测试)时不发任何通道请求,只校验「有过 approved 进件 + 有 approved 结算卡」,外加已存快照里能读到的取现开关。**[推断]**
排序按「会动钱 > 卡死流程 > 体验」。每条都给全「现象 + 根因」,
commit/文件引用只是补充。
其中涉及通道行为的部分,**汇付侧均无书面确认**(2026-09-18 全仓核查:仓库里没有任何汇付官方回复的留档)。
现象:延迟入账上线后,所有新支付都被闸门拦住,报「自动结算尚未关闭」——一类支付全废。
根因:「自动结算必须关闭才能收款」是延迟分账之前立的规则;延迟户已经把未核销的钱兜住(官方定义:不可结算、不可取现),这条规则不但多余,还把支付路径直接堵死。已在 723b756 删除。**同类风险仍在**:闸门是合取式的,任何一条历史前提残留下来都会让「所有人都收不了款」,而报错只会指出被卡的那一条。
置信档:【实测】(提交正文 + MerchantPaymentReadinessService 的现判定)
现象:汇付控台关停商户后,进件页依旧显示「已完成、六项全已确认」;商户真正的感知是「下一次下单被拒」。
根因:能力定时核实 30 分钟一轮,但只扫 capability_verified_at 超过 stale-hours(默认 6 小时)的行;空窗内的失败要等用户真的发起支付才由通道拒绝。且 approved 之后商户端「刷新进度」按钮被隐藏,全仓四个 @Scheduled 没有一个刷进件进度快照。
置信档:【实测】(6 小时=application.yml:143-146 的默认值;现象见 huifu-merchant-closure-impact 记录)
接手提示:stale-hours 调小可缩短空窗,代价是按商户数成比例增加通道查询量;把这个取舍和业务对齐,不要顺手改。
现象:门槛一旦落地,**所有没有进度记录的存量商户都收不了款**。
根因:门槛要求「微信开通 + 实名/授权」双 confirmed,而这两个 confirmed 来自 merchant_onboarding_progress。存量商户不刷新进度就永远没有这行记录;missingWechatCapabilities 对「查不到进度记录」的处理是**两项都算缺**(查不到证据就不能认为能收款)。
置信档:【推断】(代码逻辑;门槛与存量数据的组合没有在真实库上整体验证过)
待确认:存量商户要不要给一次批量进度刷新,还是明确告知必须逐个刷?——[待确认:批量刷新的范围与执行方式]
现象:配置被驳「根据渠道号或微信子商户号未查到入驻记录」,紧接着微信支付、人才端绑定、实名全部连坐;商户永远停在 not_started。
根因:两层。①**顺序反了**:微信子商户号是业务开通创建的,配置必须先等它;配置一失败旧实现就 return,业务开通永远轮不到。②**闸门取错了依据**:业务开通被受理(90000000)不等于子商户号已可查——微信入驻是**异步**的,闸门必须取通道证据(qry_wx_conf_list 里真的出现线上场景),不能取本地状态。
置信档:【实测】(新商户真实报文:配置驳回原因逐字如此;改成通道证据后受理成功)
为什么以前没暴露:只验证过存量/参考商户,它们的微信业务早就开通了,配置有东西可挂。
LABELS 里加第 7 项阶段,进件会永远无法 approved现象:进件页标题说「请按待办理项补齐缺失能力」,六项却全绿,一个待办理项都没有;反向改法(把 bank/merchantNormal 塞进阶段列表)会让进件**永远无法 approved**。
根因:complete() 拿 LABELS 的键集做完成判定,且这两项是布尔值、其余六项必须是字符串 "confirmed"——加进去「两次」破坏完成判定。正确的可见性做法是 completionChecks:不动键集,只把另外两项完成条件和它们失败时的通道原值(如状态异常 CLOSED)补出来。
置信档:【实测】(ADR-0002 已把它写成「考虑过但没选的」;MerchantOnboardingProgressService 的注释与判定一致)
rencaika_merchant/pages/merchant-onboarding/merchant-onboarding.uvue(提交资料,个人主体多两张门店照)、merchant-onboarding-pending.uvue(六阶段 + 二维码 + 刷新/实名)、payment-capability.uvue(「收款能力」页);api 在 rencaika_merchant/services/settlement.uts;config/api.uts 里是端点常量。POST /api/merchant/onboarding、POST /api/merchant/onboarding/refresh、POST /api/merchant/onboarding/realname、GET /api/merchant/onboarding/status、GET /api/merchant/payment-readiness、POST /api/merchant/payment-readiness/verify。talent-card-admin/src/views/MerchantSettlementView.vue(商户进件 / 银行卡申请两个标签)、MerchantOnboardingDetailView.vue(详情、刷新进度、代为发起实名、刷新收款能力);api 在 talent-card-admin/src/api/admin.ts。GET /api/admin/management/merchant-settlement/onboardings、.../onboardings/{id}、.../{id}/progress、POST .../{id}/progress/refresh、POST .../{id}/realname、POST .../{id}/payment-capability/refresh。rencaika)不参与进件;它只在支付时受收款闸门影响(商户不可收款时下单被拒,文案与商户端页面同一句)。以上为**逐文件核对**结果(【推断】)——注意 uvue 客户端没有可编译的反馈回路(本机没有 HBuilderX / uni CLI),改这两端之后只能在真机/开发者工具上验证,别把「代码看起来对」当成验收。
契约冲突时**默认先怀疑字段层**(本链路 2026-09-11~18 修掉的缺陷绝大多数是「文档/对照表写了、代码没照做」或「代码和自己的注释不一致」),再怀疑权限与商户侧材料:
| 现象 | 先看哪里 | 多半是哪边 |
|---|---|---|
查询一律 20002 数据权限认证失败 |
签名身份有没有该商户的数据权限;与商户层级(head 字段)**无关** | 我方凭据/权限。【实测】 |
业务开通回 90000000 |
这是「审核中=已受理」,**不是失败** | 我方判定(曾把它记成 failed)。【实测】 |
wx_conf_list格式错误 |
逐字段比对层级与类型:wx_real_name_info 是**嵌套对象**不是字符串;小微/个体工商户必须带 mcc |
我方报文。【实测】 |
微信费率规则号填写不正确 |
CID 按「场景 × 业务类型 × 主体类型」三维定位(企业 756 / 个体户 761 / 小微 775) | 我方配置。【官方原件】(官方 Excel 全表在 docs/huifu/reference/official/wx-fee-rule-id.xlsx) |
阶段永远「未确认」而 sync_error 是空的 |
通道查询其实成功了,是**判定条件**没通过(典型:不设最低收费时通道**不返回** fee_min_amt,缺失必须按 0 处理) |
我方判定。【实测】(真实回执里该字段整个缺席) |
| 商户端没有授权二维码 | 通道回的是**裸 base64 的 JPEG**,别写死 PNG magic;官方示例却是 data:image/png;base64,iVBOR… |
我方解析.【实测】(通道回 /9j/)+官方示例属【官方原件】(reference/official/huifu-api_shjj_wxsmrzztcx.md)——**两者不一致,不要照抄官方示例** |
authorize_stat=1 却显示待办理 |
授权**只看 authorize_stat**;APPLYMENT_STATE_FREEZED(主体已完成过入驻)也可能是已授权成功 |
我方判定。【实测】 |
| 进件通过后页面一直「审核中」 | 通道侧可能早就 apply_status=Y 了,是本地没刷新(无回调环境尤其如此) |
我方刷新路径。【实测】 |
以上凡属「通道行为」的结论都是**自测结论,汇付侧未书面确认**。照此实现新对接前,建议就「字段名/层级/缺失语义」向汇付要一条书面依据,而不是先怀疑自己。