yeg
2026-09-16 c18b9fac0edf5ebe3aecd74c93ec3f841fe5bf9e
fix(merchant): 修正存量商户微信补开的请求组装与审核中判定

2026-09-15 对指定存量商户 6666000241335656 做真实补开时逐项撞出来的缺陷:
前两次请求被通道拒绝、第三次受理。修复后应用自己发出的请求与探针逐字相同。

- media_mer_common_flag 从顶层移进 online_media_info_list 元素。它是媒介元素的字段,
放顶层会被拒:`基础参数校验失败:【media_merCommon_flag-媒介主体与商户主体是否一致】不能为空,或格式非法`。
之前那次「补上顶层字段」的修复从未真正生效——当时没有可用权限去验证它。
- 线上三个场景一起开(4 线上公众号/5 线上小程序/12 线上反扫),原先只发场景 5,
另外两条支付路径在通道侧就没有配置。场景 12 在汇付费率规则表里没有独立行,与 4/5 共用同一线上规则。
- 企业微信开通使用 fee_rule_id(cid)代替 mcc:官方《微信费率规则ID》表里企业线上只有 756 一条,
细分行业含「就业信息平台/其他信息服务平台」;费率默认由 0.60 改为 0.70。
- 去掉 use_chains_flag=3。它要求上级本身有商户级微信配置,而我们的上级是渠道商
(查询返回 huifuId不存在),实测被拒:`失败:复用交易信息,上级商户号为空`。
不下发该字段时,子商户在渠道 00016904 下取得自己的微信子商户号。
- 运营媒介改为按 merchant-online-media-name 下发、默认留空:文档写明它只在开通快捷/网银/
余额支付Pro/分账/大额转账/代扣时才需要,且声明 media_mer_common_flag=N 会连带索要
授权或开发证明材料,而平台是一个小程序服务多个商户。
- 回执把 90000000(审核中)计入已受理,与两个进件网关的判定保持一致。真实补开返回的正是它,
照原判定会被记成 business_open_status=failed,两端页面会把已开好的商户显示成失败。

验证:真实补开受理(apply_no=2026091530554624),三个场景 wx_conf_status=S、入驻成功,
delay_flag 由 N 变 Y,人才端 AppID 绑定读回成功。定向测试 29 项全绿;全量 198 项仅
docs/agents/testing.md 所列的 3 项基线失败,无新增。
1个文件已添加
5个文件已修改
250 ■■■■■ 已修改文件
talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuMerchantBusinessOpenGateway.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuPaymentProperties.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/resources/application.yml 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/test/java/com/talentcard/server/business/payment/huifu/HuifuBusinessOpenRequiredFieldsTests.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/test/java/com/talentcard/server/business/payment/huifu/HuifuBusinessOpenResultTests.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/test/java/com/talentcard/server/business/payment/huifu/HuifuOnboardingMerchantLevelTests.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuMerchantBusinessOpenGateway.java
@@ -8,6 +8,7 @@
import com.talentcard.server.common.ErrorCode;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -53,33 +54,45 @@
        return new HuifuMerchantBusinessOpenGateway(properties).buildRequest(requestNo, huifuId, shortName, mcc, individualMerchant);
    }
    private Map<String, Object> buildRequest(String requestNo, String huifuId, String shortName, String mcc, boolean individualMerchant) {
        Map<String, Object> wechat = new LinkedHashMap<>();
        wechat.put("pay_scene", "5");
        // 企业微信业务使用汇付确认的 fee_rule_id;仅在未配置规则 ID 时兼容旧的 mcc 路径。
        // 不得把参考商户的规则 ID 猜测套用到其它行业。
        if (!properties.getMerchantWechatFeeRuleId().isBlank()) {
            wechat.put("fee_rule_id", properties.getMerchantWechatFeeRuleId());
        } else {
            // mcc:文档注明「个体工商户、小微商户必填」——个人主体属于小微商户,所以必须带上。
            wechat.put("mcc", mcc);
        }
        wechat.put("fee_rate", properties.getMerchantWechatFeeRate().trim());
        // 实名认证由通道在业务开通成功后另行发起;取 N 表示本次不发起。
        // 改成 Y 之前要先确认实名所需的材料(含 F22/F24 照片)已就绪。
        wechat.put("is_check_real_name", "N");
        if (individualMerchant) {
            String microBizType = properties.getMerchantMicroBusinessType();
            if (!microBizType.isBlank()) {
                Map<String, Object> realName = new LinkedHashMap<>();
                realName.put("micro_biz_type", microBizType);
                wechat.put("wx_real_name_info", JSON.toJSONString(realName));
            }
        }
    /**
     * 线上微信业务场景:4=线上公众号、5=线上小程序、12=线上反扫。
     *
     * <p>三个场景一起开:平台的小程序、H5 公众号与反扫共用同一个子商户号,只开小程序会让另外两条
     * 支付路径在通道侧没有配置。汇付《微信费率规则ID》表里场景 12 没有独立行,它与 4/5 共用同一条
     * 线上规则——参考商户 {@code 6666000226048687} 线上三场景费率同为 0.70% 且真实生效。
     */
    private static final List<String> ONLINE_WECHAT_SCENES = List.of("4", "5", "12");
        Map<String, Object> media = new LinkedHashMap<>();
        media.put("media_type", "S3");
        media.put("media_name", properties.getMerchantOnlineMediaName().trim());
    private Map<String, Object> buildRequest(String requestNo, String huifuId, String shortName, String mcc, boolean individualMerchant) {
        String feeRuleId = properties.getMerchantWechatFeeRuleId();
        List<Map<String, Object>> wechatList = new ArrayList<>();
        for (String scene : ONLINE_WECHAT_SCENES) {
            Map<String, Object> wechat = new LinkedHashMap<>();
            wechat.put("pay_scene", scene);
            // 企业微信业务使用汇付确认的 fee_rule_id;仅在未配置规则 ID 时兼容旧的 mcc 路径。
            // 不得把参考商户的规则 ID 猜测套用到其它行业。
            if (!feeRuleId.isBlank()) {
                wechat.put("fee_rule_id", feeRuleId);
            } else {
                // mcc:文档注明「个体工商户、小微商户必填」——个人主体属于小微商户,所以必须带上。
                wechat.put("mcc", mcc);
            }
            wechat.put("fee_rate", properties.getMerchantWechatFeeRate().trim());
            // 最低收取手续费留空:通道按不设最低收费处理,详情查询读回 fee_min_amt=0.00
            // (参考商户线上三场景实测就是 0.00)。该字段可选且写明「大于 0」,不要主动送 0。
            // 实名认证由通道在业务开通成功后另行发起;取 N 表示本次不发起。
            // 改成 Y 之前要先确认实名所需的材料(含 F22/F24 照片)已就绪。
            wechat.put("is_check_real_name", "N");
            if (individualMerchant) {
                String microBizType = properties.getMerchantMicroBusinessType();
                if (!microBizType.isBlank()) {
                    Map<String, Object> realName = new LinkedHashMap<>();
                    realName.put("micro_biz_type", microBizType);
                    wechat.put("wx_real_name_info", JSON.toJSONString(realName));
                }
            }
            wechatList.add(wechat);
        }
        Map<String, Object> agreement = new LinkedHashMap<>();
        agreement.put("agreement_type", "3");
@@ -91,12 +104,19 @@
        request.put("huifu_id", huifuId);
        request.put("upper_huifu_id", properties.getOnboardingUpperHuifuId().trim());
        request.put("short_name", shortName);
        request.put("wx_conf_list", JSON.toJSONString(List.of(wechat)));
        request.put("wx_conf_list", JSON.toJSONString(wechatList));
        request.put("online_busi_type", properties.getMerchantOnlineBusinessType().trim());
        request.put("online_media_info_list", JSON.toJSONString(List.of(media)));
        // 运营媒介主体与商户主体是否一致。平台用一个运营媒介为众多子商户开通,主体天然不一致,
        // 而且它是顶层必填——不传会被通道拒(实测报「media_merCommon_flag…不能为空,或格式非法」)。
        putIfPresent(request, "media_mer_common_flag", properties.getMerchantMediaSubjectSame());
        // 运营媒介只在开通快捷/网银/余额支付Pro/分账/大额转账/代扣时才需要,本网关只开线上微信,
        // 因此默认不下发。要下发就把 merchant-online-media-name 配上;注意声明
        // media_mer_common_flag=N(媒介主体与商户主体不一致)时,通道会一并索要授权或开发证明材料。
        String mediaName = properties.getMerchantOnlineMediaName().trim();
        if (!mediaName.isBlank()) {
            Map<String, Object> media = new LinkedHashMap<>();
            media.put("media_type", "S3");
            media.put("media_name", mediaName);
            putIfPresent(media, "media_mer_common_flag", properties.getMerchantMediaSubjectSame());
            request.put("online_media_info_list", JSON.toJSONString(List.of(media)));
        }
        request.put("agreement_info", JSON.toJSONString(agreement));
        // 进件时就把延迟入账能力配好:先付钱后核销的订单必须把钱留在通道「待确认」,
        // 核销确认后才分配给商户。forced_delay_flag 保持关闭——支付即核销的到店消费订单不该被延迟。
@@ -112,7 +132,7 @@
    private void ensureConfigured() {
        if (!properties.isEnabled()) throw new BusinessException(ErrorCode.CONFLICT, "Huifu payment is disabled");
        if (blank(properties.getSystemId()) || blank(properties.getProductId()) || blank(properties.getOnboardingUpperHuifuId()) || blank(properties.getMerchantWechatFeeRate()) || blank(properties.getMerchantOnlineBusinessType()) || blank(properties.getMerchantOnlineMediaName()) || blank(properties.getMerchantAgreementUrl()) || blank(properties.getPrivateKey()) || blank(properties.getPlatformPublicKey())) throw new BusinessException(ErrorCode.CONFLICT, "Huifu merchant business opening configuration is incomplete");
        if (blank(properties.getSystemId()) || blank(properties.getProductId()) || blank(properties.getOnboardingUpperHuifuId()) || blank(properties.getMerchantWechatFeeRate()) || blank(properties.getMerchantOnlineBusinessType()) || blank(properties.getMerchantAgreementUrl()) || blank(properties.getPrivateKey()) || blank(properties.getPlatformPublicKey())) throw new BusinessException(ErrorCode.CONFLICT, "Huifu merchant business opening configuration is incomplete");
    }
    private void initializeSdk() throws Exception {
@@ -138,7 +158,9 @@
            String code = value(data, "sub_resp_code", "resp_code", "code");
            return new Result(success(code), value(data, "apply_no", "applyNo"), code, value(data, "sub_resp_desc", "resp_desc", "message"));
        }
        private static boolean success(String code) { return "00000000".equals(code) || "00000100".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code); }
        // 90000000 = 审核中:业务开通已被受理、微信子商户号当场就入驻成功,但整张申请单仍在审核。
        // 它与两个进件网关的判定保持一致;漏掉它会把一次成功受理记成 business_open_status=failed。
        private static boolean success(String code) { return "00000000".equals(code) || "00000100".equals(code) || "90000000".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code); }
        private static String value(Map<String, Object> data, String... keys) { for (String key : keys) if (data.get(key) != null) return String.valueOf(data.get(key)).trim(); return ""; }
    }
}
talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuPaymentProperties.java
@@ -37,11 +37,12 @@
    /**
     * 商户业务开通的 {@code use_chains_flag}:是否用上级(渠道商)的微信商户号发起交易。
     *
     * <p>我们是平台自有的一个小程序服务多个商户,没有实现「微信特约商户进件」给每个商户单独
     * 申请微信商户号,所以保持 {@code 3}——仅微信、复用上级商户号。留空则不下发该字段,
     * 由商户使用自己的微信商户号。
     * <p>留空(默认)即不下发该字段,子商户会在渠道下拿到自己的微信子商户号。
     * 不要下发 {@code 3}:那是「复用上级微信商户号」,要求上级本身有商户级微信配置,而我们的上级是
     * 渠道商(没有商户级配置,查询返回「huifuId不存在」),实测会被通道拒:
     * 「失败:复用交易信息,上级商户号为空」。
     */
    private String merchantUseChainsFlag = "3";
    private String merchantUseChainsFlag = "";
    private String merchantWechatFeeRate = "0.60";
    /**
     * 企业微信业务开通使用的汇付费率规则 ID。企业场景应优先填写该值,
talent-card-server/src/main/resources/application.yml
@@ -88,15 +88,23 @@
      # 进件只做「渠道商 → 普通商户」两级:不再用 head_office_flag=0 + head_huifu_id 把商户
      # 建成平台自己的下级商户。实测那条路没有收益(建完照样 20002 数据权限认证失败),
      # 还会按总部客群类型索要 F725/F726/F727 材料。
      # 商户业务开通是否复用上级(渠道商)的微信商户号:3=仅微信;留空=不下发该字段。
      merchant-use-chains-flag: ${HUIFU_MERCHANT_USE_CHAINS_FLAG:3}
      merchant-wechat-fee-rate: ${HUIFU_MERCHANT_WECHAT_FEE_RATE:0.60}
      # 企业微信开通应填写汇付按实际行业确认的规则 ID;留空时沿用 mcc 兼容路径。
      merchant-wechat-fee-rule-id: ${HUIFU_MERCHANT_WECHAT_FEE_RULE_ID:}
      # 是否复用上级(渠道商)的微信商户号:1=微信+支付宝,2=不使用,3=仅微信,4=支付宝;留空=不下发。
      # 渠道商 6666000220591054 本身没有商户级微信配置(查询返回「huifuId不存在」),实测下发 3 会被拒:
      # 「失败:复用交易信息,上级商户号为空」。不下发时,子商户在渠道 00016904 下拿到自己的微信
      # 子商户号(实测 sub_mer_id=W15737921333057628112,sub_mer_name 即商户注册名)。
      merchant-use-chains-flag: ${HUIFU_MERCHANT_USE_CHAINS_FLAG:}
      merchant-wechat-fee-rate: ${HUIFU_MERCHANT_WECHAT_FEE_RATE:0.70}
      # 企业微信业务的费率规则号(cid)。756 是汇付《微信费率规则ID》表里「企业 + fee_type=01(线上)」
      # 唯一的一条规则,细分行业含「就业信息平台/其他信息服务平台」,与本平台业务对应;
      # 场景 12 在该表没有独立行,与线上公众号/小程序共用这一条。留空则退回 mcc 兼容路径。
      merchant-wechat-fee-rule-id: ${HUIFU_MERCHANT_WECHAT_FEE_RULE_ID:756}
      merchant-online-business-type: ${HUIFU_MERCHANT_ONLINE_BUSINESS_TYPE:H5311L}
      merchant-online-media-name: ${HUIFU_MERCHANT_ONLINE_MEDIA_NAME:东区人才卡}
      # 运营媒介主体与商户主体是否一致。平台用一个运营媒介给众多子商户开通,主体天然不一致。
      # 这是商户业务开通的顶层必填,缺了会被通道拒(实测报 media_merCommon_flag 不能为空)。
      # 运营媒介(online_media_info_list)。文档写明它只在开通快捷/网银/余额支付Pro/分账/
      # 大额转账/代扣时才需要,且是「基本信息入驻接口」的必填项,不是「商户业务开通」的必填项;
      # 本平台只开线上微信,所以默认留空即不下发。要下发时再配媒介名称。
      merchant-online-media-name: ${HUIFU_MERCHANT_ONLINE_MEDIA_NAME:}
      # 媒介主体与商户主体是否一致;它是 online_media_info_list 的**元素字段**,不是顶层参数。
      # 声明 N(不一致)时通道会一并索要授权或开发证明材料,因此不下发媒介时它也用不上。
      merchant-media-subject-same: ${HUIFU_MERCHANT_MEDIA_SUBJECT_SAME:N}
      # 小微经营类型,个人商户开通微信业务时必填;三种取值都要求 F22 门头照 + F24 内景照。
      merchant-micro-business-type: ${HUIFU_MERCHANT_MICRO_BUSINESS_TYPE:MICRO_TYPE_ONLINE}
talent-card-server/src/test/java/com/talentcard/server/business/payment/huifu/HuifuBusinessOpenRequiredFieldsTests.java
@@ -2,6 +2,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -14,9 +15,9 @@
 * <p>以汇付《商户业务开通》(v2.merchant.busi.open) 的请求规则为准逐项核对后发现两处缺口:
 *
 * <ul>
 *   <li>{@code media_mer_common_flag}(顶层):运营媒介主体与商户主体是否一致。平台用一个运营媒介
 *       给众多子商户开通,主体天然不一致。实测缺它会被拒:
 *       「【media_merCommon_flag-媒介主体与商户主体是否一致】不能为空,或格式非法」。</li>
 *   <li>{@code media_mer_common_flag}({@code online_media_info_list} 的元素字段):运营媒介主体与商户主体
 *       是否一致。平台用一个运营媒介给众多子商户开通,主体天然不一致,取 N。它属于媒介**元素**而不是顶层参数,
 *       放顶层会被拒:「【media_merCommon_flag-媒介主体与商户主体是否一致】不能为空,或格式非法」。</li>
 *   <li>{@code micro_biz_type}(wx_conf_list 元素内的 wx_real_name_info):个人商户开通微信业务必填,
 *       且三种取值都要求 F22 店铺门头照 + F24 店铺内景照。</li>
 * </ul>
@@ -43,11 +44,30 @@
    }
    @Test
    void theMediaSubjectFlagIsAlwaysSent() {
    void theMediaSubjectFlagIsInsideTheMediaElement() {
        Map<String, Object> request = request(false);
        // 平台媒介主体与商户主体不一致,取 N;关键是必须出现,否则通道判参数非法。
        assertThat(request).containsEntry("media_mer_common_flag", "N");
        // 平台媒介主体与商户主体不一致,取 N。它是 online_media_info_list 的**元素字段**,
        // 不是顶层参数——放顶层会被通道判「不能为空,或格式非法」。
        assertThat(request).doesNotContainKey("media_mer_common_flag");
        Map<String, Object> media = onlineMedia(request);
        assertThat(media).containsEntry("media_mer_common_flag", "N");
    }
    /**
     * 运营媒介只在开通快捷/网银/余额支付Pro/分账/大额转账/代扣时才需要,本平台只开线上微信,
     * 默认不下发;不下发媒介,通道也就不会再索要「授权或开发证明材料」。
     */
    @Test
    void aBlankMediaNameKeepsTheMediaDeclarationOutOfTheRequest() {
        HuifuPaymentProperties properties = properties();
        properties.setMerchantOnlineMediaName(" ");
        Map<String, Object> request = HuifuMerchantBusinessOpenGateway.buildRequestForTest(
                properties, "BO-TEST-1", "6666000241390821", "阳野", "5812", false);
        assertThat(request).doesNotContainKey("online_media_info_list");
    }
    @Test
@@ -55,9 +75,9 @@
        Map<String, Object> request = request(true);
        List<Map<String, Object>> wechat = wechatConfig(request);
        assertThat(wechat).hasSize(1);
        // 个人主体属于小微商户,mcc 也是必填。
        assertThat(wechat.get(0)).containsKeys("mcc", "wx_real_name_info");
        assertThat(wechat).hasSize(3);
        // 个人主体属于小微商户,mcc 也是必填;三个线上场景都要带。
        assertThat(wechat).allSatisfy(entry -> assertThat(entry).containsKeys("mcc", "wx_real_name_info"));
        // 层级:micro_biz_type 在 wx_real_name_info 这个嵌套 jsonObject 里,不在元素顶层。
        assertThat(wechat.get(0)).doesNotContainKey("micro_biz_type");
@@ -71,7 +91,7 @@
        Map<String, Object> request = request(false);
        List<Map<String, Object>> wechat = wechatConfig(request);
        assertThat(wechat.get(0)).doesNotContainKey("wx_real_name_info");
        assertThat(wechat).allSatisfy(entry -> assertThat(entry).doesNotContainKey("wx_real_name_info"));
    }
    @Test
@@ -82,8 +102,32 @@
        List<Map<String, Object>> wechat = wechatConfig(HuifuMerchantBusinessOpenGateway.buildRequestForTest(
                properties, "BO-TEST-RULE", "6666000241335656", "成都喜望科技", "7372", false));
        assertThat(wechat.get(0)).containsEntry("fee_rule_id", "756");
        assertThat(wechat.get(0)).doesNotContainKey("mcc");
        assertThat(wechat).allSatisfy(entry -> {
            assertThat(entry).containsEntry("fee_rule_id", "756");
            assertThat(entry).doesNotContainKey("mcc");
        });
    }
    /**
     * 线上公众号、小程序、反扫三个场景一起开。只开小程序会让另外两条支付路径在通道侧缺配置,
     * 而平台的小程序、H5 公众号与反扫共用同一个子商户号。
     */
    @Test
    void theOnlineWeChatScenesAreOpenedTogether() {
        List<Map<String, Object>> wechat = wechatConfig(request(false));
        List<String> scenes = new ArrayList<>();
        for (Map<String, Object> entry : wechat) scenes.add(String.valueOf(entry.get("pay_scene")));
        assertThat(scenes).containsExactly("4", "5", "12");
        assertThat(wechat).allSatisfy(entry -> {
            assertThat(entry).containsEntry("fee_rate", "0.60");
            // 最低收取手续费可选、文档写明「大于 0」:留空让通道按不设最低收费处理,
            // 详情查询读回 fee_min_amt=0.00(参考商户线上三场景实测即 0.00),不要主动送 0。
            assertThat(entry).doesNotContainKey("fee_min_amt");
            // 不填子渠道号即入驻到渠道默认配置的微信渠道号下。
            assertThat(entry).doesNotContainKey("pay_channel_id");
        });
    }
    /**
@@ -100,4 +144,12 @@
    private List<Map<String, Object>> wechatConfig(Map<String, Object> request) {
        return JSON.parseObject(String.valueOf(request.get("wx_conf_list")), List.class);
    }
    @SuppressWarnings("unchecked")
    private Map<String, Object> onlineMedia(Map<String, Object> request) {
        List<Map<String, Object>> media =
                JSON.parseObject(String.valueOf(request.get("online_media_info_list")), List.class);
        assertThat(media).hasSize(1);
        return media.get(0);
    }
}
talent-card-server/src/test/java/com/talentcard/server/business/payment/huifu/HuifuBusinessOpenResultTests.java
New file
@@ -0,0 +1,36 @@
package com.talentcard.server.business.payment.huifu;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Map;
import org.junit.jupiter.api.Test;
/**
 * 商户业务开通的回执判定。
 *
 * <p>{@code 90000000=审核中} 表示申请已被受理(微信子商户号当场就入驻成功),整张申请单仍在审核,
 * 它与两个进件网关的判定一致。漏掉它会把一次成功受理记成 {@code business_open_status=failed},
 * 两端页面就会把已经开好的商户显示成失败。
 */
class HuifuBusinessOpenResultTests {
    @Test
    void aSubmittedApplicationUnderReviewCountsAsAccepted() {
        HuifuMerchantBusinessOpenGateway.Result result = HuifuMerchantBusinessOpenGateway.Result.from(
                Map.of("resp_code", "90000000", "resp_desc", "审核中", "apply_no", "2026091530554624"));
        assertThat(result.accepted()).isTrue();
        assertThat(result.applyNo()).isEqualTo("2026091530554624");
        assertThat(result.code()).isEqualTo("90000000");
    }
    @Test
    void aRejectedApplicationIsNotAccepted() {
        HuifuMerchantBusinessOpenGateway.Result result = HuifuMerchantBusinessOpenGateway.Result.from(
                Map.of("resp_code", "00000001", "resp_desc", "基础参数校验失败"));
        assertThat(result.accepted()).isFalse();
        assertThat(result.applyNo()).isEmpty();
    }
}
talent-card-server/src/test/java/com/talentcard/server/business/payment/huifu/HuifuOnboardingMerchantLevelTests.java
@@ -47,26 +47,29 @@
    }
    /**
     * 商户自己没有微信商户号(我们没实现「微信特约商户进件」),开通时仍然复用上级商户号发起交易,
     * 否则子商户根本没有可用的微信商户号。留空该配置即不下发字段。
     * 默认不下发 {@code use_chains_flag}:子商户在渠道下取得自己的微信子商户号。
     *
     * <p>下发 {@code 3}(复用上级微信商户号)曾是默认值,但上级是渠道商、没有商户级微信配置,
     * 通道会拒:「失败:复用交易信息,上级商户号为空」。2026-09-15 的真实补开在不带该字段时被受理,
     * 线上公众号/小程序/反扫三场景各自入驻成功并返回自己的 {@code sub_mer_id}。
     */
    @Test
    void businessOpenReusesTheSuperiorWeChatMerchantNumber() {
    void businessOpenLeavesTheUseChainsFlagOutByDefault() {
        Map<String, Object> request = HuifuMerchantBusinessOpenGateway.buildRequestForTest(
                properties(), "BO-TEST-1", "6666000241335656", "成都喜望科技", "5812", false);
        assertThat(request).containsEntry("use_chains_flag", "3");
        assertThat(request).doesNotContainKey("use_chains_flag");
    }
    @Test
    void aBlankUseChainsFlagKeepsTheFieldOutOfTheRequest() {
    void aConfiguredUseChainsFlagIsSentAsIs() {
        HuifuPaymentProperties properties = properties();
        properties.setMerchantUseChainsFlag(" ");
        properties.setMerchantUseChainsFlag("3");
        Map<String, Object> request = HuifuMerchantBusinessOpenGateway.buildRequestForTest(
                properties, "BO-TEST-1", "6666000241335656", "成都喜望科技", "5812", false);
        assertThat(request).doesNotContainKey("use_chains_flag");
        assertThat(request).containsEntry("use_chains_flag", "3");
    }
    private static HuifuPaymentProperties properties() {