From 7dfc5b78d616eef0e263c177f41858c8d95880a5 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期一, 14 九月 2026 20:35:49 +0800
Subject: [PATCH] feat(merchant): 进件把商户建成平台自己的下级商户,进件后自动配置才做得成

---
 talent-card-server/src/test/java/com/talentcard/server/business/payment/huifu/HuifuOnboardingParentMerchantTests.java |   51 +++++++++++++++++
 talent-card-server/src/main/resources/application.yml                                                                 |    5 +
 talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantSettlementService.java               |    6 +
 talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuEnterpriseOnboardingGateway.java   |    2 
 talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingCommand.java   |    4 +
 talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuParentMerchant.java                |   36 ++++++++++++
 talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuEnterpriseOnboardingCommand.java   |    4 +
 talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuMerchantBusinessOpenGateway.java   |    4 +
 talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingGateway.java   |    2 
 talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuPaymentProperties.java             |   21 +++++++
 10 files changed, 131 insertions(+), 4 deletions(-)

diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantSettlementService.java b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantSettlementService.java
index 4f6e128..03ee861 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantSettlementService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantSettlementService.java
@@ -152,7 +152,8 @@
                     privateSettlement ? "1" : "0", privateSettlement ? request.subjectName().trim() : request.merchantName().trim(), request.bankCardNumber().trim(), request.bankProvinceCode().trim(), request.bankCityCode().trim(), clean(branch.get("headBankCode")), clean(branch.get("clearingCode")), clean(branch.get("branchName")),
                     privateSettlement ? request.subjectIdNumber().trim().toUpperCase() : "", privateSettlement ? certificateValidityType(request.subjectCertificateValidityType()) : "", privateSettlement ? certificateBeginDate(request.subjectCertificateBeginDate()) : "", privateSettlement ? certificateEndDate(request.subjectCertificateEndDate()) : "", privateSettlement ? request.contactMobile().trim() : "",
                     registrationAccountPictureId, settlementCardPictureId, settlementCertFrontPictureId, settlementCertBackPictureId,
-                    certificateBeginDate(request.establishedDate()), normalizeRegisteredCapital(request.companyType(), request.registeredCapital())));
+                    certificateBeginDate(request.establishedDate()), normalizeRegisteredCapital(request.companyType(), request.registeredCapital()),
+                    huifuPaymentProperties.getOnboardingParentHuifuId()));
             applyOnboardingResult(applicationNo, bankCardId, result.accepted(), result.huifuId(), result.applyNo(), result.tokenNo(), result.code(), result.message());
         } catch (HuifuPaymentGatewayException exception) {
             jdbc.update("update merchant_onboarding_application set reject_reason=? where application_no=? and status='pending'", "Huifu submission outcome is pending confirmation", applicationNo);
@@ -167,7 +168,8 @@
             HuifuIndividualOnboardingGateway.Result result = individualOnboardingGateway.create(new HuifuIndividualOnboardingCommand(
                     applicationNo, request.subjectName().trim(), request.subjectIdNumber().trim().toUpperCase(), certificateValidityType(request.subjectCertificateValidityType()), certificateBeginDate(request.subjectCertificateBeginDate()), certificateEndDate(request.subjectCertificateEndDate()), request.contactMobile().trim(), clean(request.registeredAddress()), request.administratorEmail().trim(),
                     mcc(principal.merchantId()), clean(request.registeredProvinceCode()), clean(request.registeredCityCode()), clean(request.registeredDistrictCode()), legalFrontPictureId, legalBackPictureId,
-                    request.bankCardNumber().trim(), request.bankProvinceCode().trim(), request.bankCityCode().trim(), clean(branch.get("headBankCode")), settlementCardPictureId));
+                    request.bankCardNumber().trim(), request.bankProvinceCode().trim(), request.bankCityCode().trim(), clean(branch.get("headBankCode")), settlementCardPictureId,
+                    huifuPaymentProperties.getOnboardingParentHuifuId()));
             applyOnboardingResult(applicationNo, bankCardId, result.accepted(), result.huifuId(), result.applyNo(), result.tokenNo(), result.code(), result.message());
         } catch (HuifuPaymentGatewayException exception) {
             jdbc.update("update merchant_onboarding_application set reject_reason=? where application_no=? and status='pending'", "Huifu submission outcome is pending confirmation", applicationNo);
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuEnterpriseOnboardingCommand.java b/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuEnterpriseOnboardingCommand.java
index 262b75c..0d6f678 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuEnterpriseOnboardingCommand.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuEnterpriseOnboardingCommand.java
@@ -44,5 +44,7 @@
         String settlementCertificateFrontPictureId,
         String settlementCertificateBackPictureId,
         String foundDate,
-        String registeredCapital
+        String registeredCapital,
+        /** 平台自己的汇付号:非空则把该商户建成平台的下级商户(head_office_flag=0 + head_huifu_id)。 */
+        String parentHuifuId
 ) {}
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuEnterpriseOnboardingGateway.java b/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuEnterpriseOnboardingGateway.java
index ed8d2b9..6db27b1 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuEnterpriseOnboardingGateway.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuEnterpriseOnboardingGateway.java
@@ -53,6 +53,8 @@
         request.put("req_seq_id", command.applicationNo());
         request.put("req_date", LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE));
         request.put("upper_huifu_id", properties.getOnboardingUpperHuifuId().trim());
+        // 建成平台自己的下级商户:只有挂在自己名下,平台才有权为该商户配置微信支付与业务开通。
+        HuifuParentMerchant.apply(request, command.parentHuifuId());
         request.put("reg_name", command.companyName());
         request.put("short_name", command.shortName());
         request.put("receipt_name", command.shortName());
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingCommand.java b/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingCommand.java
index 79b87d6..06be76c 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingCommand.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingCommand.java
@@ -20,5 +20,7 @@
         String bankProvinceCode,
         String bankCityCode,
         String bankCode,
-        String settlementCardFrontPictureId
+        String settlementCardFrontPictureId,
+        /** 平台自己的汇付号:非空则把该商户建成平台的下级商户(head_office_flag=0 + head_huifu_id)。 */
+        String parentHuifuId
 ) {}
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingGateway.java b/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingGateway.java
index 3d7c5ef..f9e6eed 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingGateway.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingGateway.java
@@ -49,6 +49,8 @@
         request.put("req_seq_id", command.applicationNo());
         request.put("req_date", LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE));
         request.put("upper_huifu_id", properties.getOnboardingUpperHuifuId().trim());
+        // 建成平台自己的下级商户:只有挂在自己名下,平台才有权为该商户配置微信支付与业务开通。
+        HuifuParentMerchant.apply(request, command.parentHuifuId());
         request.put("reg_name", command.name());
         request.put("short_name", command.name());
         request.put("mcc", command.mcc());
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuMerchantBusinessOpenGateway.java b/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuMerchantBusinessOpenGateway.java
index e961fe9..e870d34 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuMerchantBusinessOpenGateway.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuMerchantBusinessOpenGateway.java
@@ -53,6 +53,10 @@
         // 核销确认后才分配给商户。forced_delay_flag 保持关闭——支付即核销的到店消费订单不该被延迟。
         request.put("delay_flag", "Y");
         request.put("forced_delay_flag", "N");
+        // 子商户复用平台的微信商户号发起交易,不必为每个商户单独申请微信商户号。
+        // 前提是上级(平台)已开通微信功能——平台的 wx_conf_list 三个 payScene 都是 openFlag=Y。
+        // 3 = 仅微信;文档:使用上级微信、支付宝商户号需对应开通微信、支付宝功能。
+        if (properties.isOnboardingUnderParentMerchant()) request.put("use_chains_flag", "3");
         putIfPresent(request, "async_return_url", properties.getMerchantBusinessOpenNotifyUrl());
         putIfPresent(request, "busi_async_return_url", properties.getMerchantBusinessOpenNotifyUrl());
         try {
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuParentMerchant.java b/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuParentMerchant.java
new file mode 100644
index 0000000..81b09d6
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuParentMerchant.java
@@ -0,0 +1,36 @@
+package com.talentcard.server.business.payment.huifu;
+
+import java.util.Map;
+
+/**
+ * 进件请求里「把新商户建成平台下级商户」的公共组装逻辑。
+ *
+ * <p>汇付的进件接口用两个字段表达商户归属:
+ * <ul>
+ *   <li>{@code head_office_flag=0} —— 本笔进件是一个下级商户;</li>
+ *   <li>{@code head_huifu_id} —— 它的上级商户汇付号({@code head_office_flag=0} 时必填)。</li>
+ * </ul>
+ *
+ * <p>这两个字段必须成对出现。只传 {@code upper_huifu_id}(渠道商号)时商户归属渠道商,
+ * 平台自己的签名身份随后无权操作它——业务开通与微信支付配置都会返回
+ * {@code 20002 数据权限认证失败},商户就只能自己去汇付控台配置。
+ *
+ * <p>企业进件与个人进件共用同一规则,所以抽在这里,避免两条路各写一遍后走偏。
+ */
+final class HuifuParentMerchant {
+
+    private HuifuParentMerchant() {
+    }
+
+    /**
+     * 当 {@code parentHuifuId} 非空时,把请求标成该上级的下级商户;为空则保持普通商户进件。
+     *
+     * <p>前置条件:上级汇付号必须已通过「开通下级商户权限配置接口」
+     * ({@code v2/merchant/busi/head/config})取得开通下级商户的权限,否则通道会拒绝本笔进件。
+     */
+    static void apply(Map<String, Object> request, String parentHuifuId) {
+        if (parentHuifuId == null || parentHuifuId.isBlank()) return;
+        request.put("head_office_flag", "0");
+        request.put("head_huifu_id", parentHuifuId.trim());
+    }
+}
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuPaymentProperties.java b/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuPaymentProperties.java
index 4643736..300506c 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuPaymentProperties.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuPaymentProperties.java
@@ -19,6 +19,20 @@
     private String merchantWechatConfigNotifyUrl = "";
     private String merchantBusinessOpenNotifyUrl = "";
     private String onboardingUpperHuifuId = "";
+    /**
+     * 进件时把新商户挂到这个汇付号下,作为它的下级商户({@code head_office_flag=0} + {@code head_huifu_id})。
+     *
+     * <p>留空则按普通商户进件,商户归属由 {@code upper_huifu_id}(渠道商号)决定,
+     * 而平台自己的身份**无权操作**这类商户——实测对它们调用商户业务开通、微信配置、
+     * 商户信息查询一律返回 {@code 20002 数据权限认证失败},进件后的自动配置全部做不了。
+     *
+     * <p>填成平台自己的汇付号({@code merchant-id})后,商户建在平台名下,平台就能自动完成
+     * 「微信小程序支付配置 + 商户业务开通」,商户端无需参与。
+     *
+     * <p>前置条件:该汇付号必须先通过「开通下级商户权限配置接口」
+     * ({@code v2/merchant/busi/head/config})获得开通下级商户的权限。
+     */
+    private String onboardingParentHuifuId = "";
     private String merchantWechatFeeRate = "0.60";
     private String merchantOnlineBusinessType = "H5311L";
     private String merchantOnlineMediaName = "东区人才卡";
@@ -68,6 +82,13 @@
         return merchantId;
     }
     public void setOnboardingUpperHuifuId(String onboardingUpperHuifuId) { this.onboardingUpperHuifuId = onboardingUpperHuifuId; }
+    public String getOnboardingParentHuifuId() { return onboardingParentHuifuId == null ? "" : onboardingParentHuifuId.trim(); }
+    public void setOnboardingParentHuifuId(String onboardingParentHuifuId) { this.onboardingParentHuifuId = onboardingParentHuifuId; }
+    /**
+     * 是否把新商户建成平台的下级商户。同时配好「上级商户号发起微信交易」,
+     * 让子商户复用平台的微信商户号,不必逐个单独申请。
+     */
+    public boolean isOnboardingUnderParentMerchant() { return !getOnboardingParentHuifuId().isBlank(); }
     public String getMerchantWechatFeeRate() { return merchantWechatFeeRate; }
     public void setMerchantWechatFeeRate(String merchantWechatFeeRate) { this.merchantWechatFeeRate = merchantWechatFeeRate; }
     public String getMerchantOnlineBusinessType() { return merchantOnlineBusinessType; }
diff --git a/talent-card-server/src/main/resources/application.yml b/talent-card-server/src/main/resources/application.yml
index 46ce2b3..df2c603 100644
--- a/talent-card-server/src/main/resources/application.yml
+++ b/talent-card-server/src/main/resources/application.yml
@@ -74,6 +74,11 @@
       merchant-wechat-config-notify-url: ${HUIFU_MERCHANT_WECHAT_CONFIG_NOTIFY_URL:http://221.182.45.100:11451/api/payments/huifu/merchant-wechat-config-notify}
       merchant-business-open-notify-url: ${HUIFU_MERCHANT_BUSINESS_OPEN_NOTIFY_URL:http://221.182.45.100:11451/api/payments/huifu/merchant-business-open-notify}
       onboarding-upper-huifu-id: ${HUIFU_ONBOARDING_UPPER_HUIFU_ID:6666000220591054}
+      # 把新商户建成平台自己的下级商户(head_office_flag=0 + head_huifu_id)。
+      # 不建在自己名下的话,平台无权操作该商户,进件后的「微信支付配置 + 业务开通」全部
+      # 会返回 20002 数据权限认证失败,商户就得自己去汇付控台配置 —— 那正是要避免的。
+      # 置空则退回普通进件(归属由 upper-huifu-id 决定),仅用于排查。
+      onboarding-parent-huifu-id: ${HUIFU_ONBOARDING_PARENT_HUIFU_ID:6666000226048687}
       merchant-wechat-fee-rate: ${HUIFU_MERCHANT_WECHAT_FEE_RATE:0.60}
       merchant-online-business-type: ${HUIFU_MERCHANT_ONLINE_BUSINESS_TYPE:H5311L}
       merchant-online-media-name: ${HUIFU_MERCHANT_ONLINE_MEDIA_NAME:东区人才卡}
diff --git a/talent-card-server/src/test/java/com/talentcard/server/business/payment/huifu/HuifuOnboardingParentMerchantTests.java b/talent-card-server/src/test/java/com/talentcard/server/business/payment/huifu/HuifuOnboardingParentMerchantTests.java
new file mode 100644
index 0000000..bb6a82e
--- /dev/null
+++ b/talent-card-server/src/test/java/com/talentcard/server/business/payment/huifu/HuifuOnboardingParentMerchantTests.java
@@ -0,0 +1,51 @@
+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 head_office_flag=0} + {@code head_huifu_id}),商户就归属到渠道商名下,
+ * 平台随后对它的**一切**操作都会返回 {@code 20002 数据权限认证失败**——实测
+ * {@code v2.merchant.busi.open}(业务开通)和 {@code v2.merchant.busi.config}(微信小程序
+ * 支付配置)两个接口、带不带 {@code product_id} 都一样。结果就是进件通过了,但微信支付
+ * 永远配不上,商户只能自己去汇付控台点——这正是进件功能要消灭的事情。
+ *
+ * <p>所以这两个字段的有无必须被钉住。
+ */
+class HuifuOnboardingParentMerchantTests {
+
+    @Test
+    void anOnboardingWithoutAParentStaysAnOrdinaryMerchant() {
+        Map<String, Object> request = new java.util.LinkedHashMap<>();
+
+        HuifuParentMerchant.apply(request, "");
+
+        assertThat(request).doesNotContainKeys("head_office_flag", "head_huifu_id");
+    }
+
+    @Test
+    void aBlankParentIsTreatedAsNoParent() {
+        Map<String, Object> request = new java.util.LinkedHashMap<>();
+
+        HuifuParentMerchant.apply(request, "   ");
+
+        assertThat(request).doesNotContainKeys("head_office_flag", "head_huifu_id");
+    }
+
+    @Test
+    void aParentTurnsTheMerchantIntoItsDirectSubMerchant() {
+        Map<String, Object> request = new java.util.LinkedHashMap<>();
+
+        HuifuParentMerchant.apply(request, "6666000226048687");
+
+        // 0 = 下级商户;上级商户号必填,平台之后才有权为它配置微信支付与业务开通。
+        assertThat(request).containsEntry("head_office_flag", "0");
+        assertThat(request).containsEntry("head_huifu_id", "6666000226048687");
+    }
+}

--
Gitblit v1.7.1