From 667abfbc3b88e3ef17390ff08dcc4d5b3ee419da Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期三, 16 九月 2026 12:57:37 +0800
Subject: [PATCH] fix(merchant): 行业以系统档案为准,商户在进件表单选的行业只作兜底

---
 talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantOnboardingIndustryMccTests.java |   31 ++++++++++++---
 talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantSettlementService.java          |   36 ++++++++++++------
 .scratch/merchant-individual-onboarding/audit.md                                                                 |   26 ++++++++++---
 3 files changed, 68 insertions(+), 25 deletions(-)

diff --git a/.scratch/merchant-individual-onboarding/audit.md b/.scratch/merchant-individual-onboarding/audit.md
index 8f35832..e0de30e 100644
--- a/.scratch/merchant-individual-onboarding/audit.md
+++ b/.scratch/merchant-individual-onboarding/audit.md
@@ -209,21 +209,38 @@
 | 4 | `store_address_code` 传 6 位地市级行政区划编码(省 2 位 + 市 4 位) | **无需改动**:本来就取商户端"经营地区"选择器返回的 `registered_city_code`,形如 `510100`,正是省 2 位 + 市 4 位 |
 | 5 | 三种小微经营类型的 `store_header_pic`(F22)在进件时**都必填**,只是图片内容不同:线上=店铺首页截图、流动经营/便民服务=经营现场照、门店场所=实体门头照 | ①个人进件请求补上 `store_header_pic`(原先只有业务开通的 `extended_material_list`,进件侧完全没传);②商户端该字段改名「店铺照片」,提示按三种情况分别说明;③业务开通侧继续带 F22/F24 到 `extended_material_list` |
 
+### 2026-09-16 追加两条口径(第二批复核时确认)
+
+| # | 口径 | 落地情况 |
+| --- | --- | --- |
+| 6 | **行业由系统动态匹配,商户改不了**(已进件商户也不提供补改入口) | 改的是 MCC 的**取值优先级**:此前是"商户在进件表单选的行业类型"优先于商家档案,与这条口径相反。改为**系统档案优先**——经营类型是我们自己维护的、也是匹配微信行业类目与费率的依据,不该被表单上的一次选择盖掉;商户申报的行业类型只在档案给不出具体行业(未配或配成「其它」)时兜底,避免档案缺数据时一律落 7399。见 `MerchantSettlementService.preferConfiguredMcc` 与 `MerchantOnboardingIndustryMccTests` |
+| 7 | 「只认延迟失败的重试入口」这件事**暂时不做** | 不改代码。现状:`retryable` 仍等于「延迟入账失败」,业务开通/微信配置失败时两端没有专门的重试按钮,靠「刷新进度」隐式重试(刷新本身会重发失败的步骤) |
+
 第 5 条是这一轮里唯一**真实漏发**的字段:`store_header_pic` 在进件的官方说明里是必填,
 而项目此前一条都没传过。三处消费同一张图的路径现在都齐了——进件 `store_header_pic`、
 业务开通 `extended_material_list` 的 F22、实名 `store_header_copy`。
 
-原有测试全部随之复跑:新增 `HuifuIndividualOnboardingRequiredFieldsTests` 5 项钉住上面第 2/3/5 条。
+原有测试全部随之复跑;第 2/3/5 条由 `HuifuIndividualOnboardingRequiredFieldsTests` 5 项钉住,
+第 6 条由 `MerchantOnboardingIndustryMccTests` 5 项钉住。
 
 ---
 
 ## 七、仍需官方确认
 
-只剩一条,且不阻断开通:
+**只剩这一条**(其余此前列过的都已由业务答复并落地,见第六节的两张表):
 
 - 业务开通的 `wx_real_name_info.micro_biz_type` 与独立实名接口的 `micro_biz_type` 是否都需要:
   本项目 `is_check_real_name=N`(不在业务开通里发起实名),却仍在 `wx_conf_list` 里带 `micro_biz_type`。
   两处都不缺材料,保留即可;若确认业务开通侧不需要,去掉的是冗余而不是能力。
+
+> 这一条**未被业务答复过**,只是不阻断开通,所以留在这里。
+
+### 已定的两条(原「剩余待讨论」,2026-09-16 关闭)
+
+- ~~两端的「重试」入口只认延迟失败~~ → **暂时不做**(见第六节第 7 条)。
+- ~~已进件商户能否补改经营行业~~ → **不提供补改入口,行业由系统动态匹配**(见第六节第 6 条)。
+  先前记的「452 进件时选的「其它」与实际行业不一致」这条顾虑也随之消解:按新口径,取值以系统档案为准,
+  商户表单上的申报值只是档案缺数据时的兜底,本来就不该作为口径依据。
 
 ---
 
@@ -491,10 +508,7 @@
 
 ### 剩余待讨论
 
-- **两端的「重试」入口只认延迟失败**(`retryable = delay == 'failed'`),"业务开通失败"或
-  "微信配置失败"没有对应的重试动作,只能靠「刷新进度」隐式重试——商户看到失败原因却没有可点的操作。
-- **已进件商户能否补改经营行业**:452 进件时选的是「其它」(MCC 退回商家档案后是 `5812` 餐饮,
-  值是对的,但商户申报的行业与实际不一致)。要补救这类数据得先定这条路径。
+(无。原两条已于 2026-09-16 关闭:重试入口暂不做,行业不提供补改入口、由系统动态匹配,见第六节。)
 
 ---
 
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 83ccaa2..69b9b99 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
@@ -37,6 +37,8 @@
 @Service
 public class MerchantSettlementService {
     private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MerchantSettlementService.class);
+    /** 商家档案给不出可识别经营类型时的兜底 MCC(「其它」)。 */
+    private static final String FALLBACK_MCC = "7399";
     private static final Map<String, String> MCC_BY_MERCHANT_TYPE = Map.ofEntries(
             Map.entry("food", "5812"), Map.entry("scenic", "7991"), Map.entry("hotel", "7011"), Map.entry("apartment", "7011"),
             Map.entry("cinema", "7832"), Map.entry("fuel", "5541"), Map.entry("parking", "7523"), Map.entry("carWash", "7542"),
@@ -876,20 +878,30 @@
     /**
      * 进件主体所属行业的 MCC。
      *
-     * <p>个人(小微)主体在进件表单第一步单独选过「行业类型」(餐饮服务/酒店服务/景区服务…),
-     * 它跟着企业主体的公司类型一起落在 {@code company_type} 列上。原先个人进件的 MCC 只认商家档案上的
-     * 经营类型,等于把商户自己在进件时选的行业**静默丢掉**:档案没配好就一律落 7399(其它),
-     * 通道侧的微信行业类目和费率跟着一起错。所以个人主体优先用进件时选的行业,
-     * 行业识别不出来(选了「其它」)再退回商家档案。
+     * <p><b>行业以系统里的商家档案为准</b>(2026-09-16 业务口径:「行业改不了,只有我们系统动态地去匹配
+     * 行业」)。商家档案上的经营类型是我们自己维护的,也是匹配微信行业类目与费率的依据,
+     * 不该被进件表单上的一次选择盖掉;已进件商户也不提供补改行业的入口。
+     *
+     * <p>商户在个人进件表单第一步选的「行业类型」只在**档案给不出具体行业时**兜底
+     * (档案未配、或配成「其它」未识别 → {@link #FALLBACK_MCC}),避免档案缺数据时一律落「其它」。
      *
      * <p>{@code mcc} 是进件的必填项;业务开通只在没有 {@code fee_rule_id} 时才用它兜底。
      */
     private String mcc(Long merchantId, String subjectType, String industryType) {
-        if ("individual".equalsIgnoreCase(clean(subjectType))) {
-            String industry = mccForIndustry(industryType);
-            if (!industry.isBlank()) return industry;
-        }
-        return mcc(merchantId);
+        String configured = mcc(merchantId);
+        // 个体工商户走企业进件接口,表单上没有「行业类型」这一项,只有个人主体才有申报值。
+        if (!"individual".equalsIgnoreCase(clean(subjectType))) return configured;
+        return preferConfiguredMcc(configured, industryType);
+    }
+
+    /**
+     * 系统档案优先、商户申报兜底。取值为静态形式,便于直接断言这条规则本身
+     * (见 {@code MerchantOnboardingIndustryMccTests})。
+     */
+    static String preferConfiguredMcc(String configuredMcc, String declaredIndustry) {
+        if (!FALLBACK_MCC.equals(cleanStatus(configuredMcc))) return configuredMcc == null ? "" : configuredMcc.trim();
+        String declared = mccForIndustry(declaredIndustry);
+        return declared.isBlank() ? FALLBACK_MCC : declared;
     }
 
     /**
@@ -898,7 +910,7 @@
      */
     static String mccForIndustry(String industryType) {
         String industry = MerchantTypeCatalog.canonicalCode(cleanStatus(industryType));
-        return "other".equals(industry) ? "" : MCC_BY_MERCHANT_TYPE.getOrDefault(industry, "7399");
+        return "other".equals(industry) ? "" : MCC_BY_MERCHANT_TYPE.getOrDefault(industry, FALLBACK_MCC);
     }
 
     private String mcc(Long merchantId) {
@@ -906,7 +918,7 @@
         if (merchants.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "商家不存在");
         String code = clean(merchants.get(0).get("merchantTypeCode"));
         if (code.isBlank()) code = MerchantTypeCatalog.canonicalCode(clean(merchants.get(0).get("merchantType")));
-        return MCC_BY_MERCHANT_TYPE.getOrDefault(code, "7399");
+        return MCC_BY_MERCHANT_TYPE.getOrDefault(code, FALLBACK_MCC);
     }
     private String normalizeRegisteredCapital(String companyType, String value) {
         String text = clean(value); boolean required = List.of("国营企业", "私营企业", "外资企业", "其它组织", "事业单位").contains(clean(companyType));
diff --git a/talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantOnboardingIndustryMccTests.java b/talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantOnboardingIndustryMccTests.java
index a57a725..ce3230d 100644
--- a/talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantOnboardingIndustryMccTests.java
+++ b/talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantOnboardingIndustryMccTests.java
@@ -5,18 +5,35 @@
 import org.junit.jupiter.api.Test;
 
 /**
- * 个人(小微)主体的 MCC 取的是**商户在进件表单里选的行业**。
+ * 个人(小微)主体上报给汇付的 MCC 怎么取。
  *
- * <p>进件第一步让个人商户选「行业类型」(餐饮服务/酒店服务/景区服务/加油服务/停车服务/其它),
- * 它跟着企业主体的公司类型一起落在 {@code company_type} 列上。原先上报汇付的 {@code mcc}
- * 只认商家档案上的经营类型,等于把这一项**静默丢掉**:档案没配好就一律落 7399(其它),
- * 通道侧的微信行业类目和费率跟着一起错,而商户页面上明明选过行业。
+ * <p>业务口径(2026-09-16):**行业由系统动态匹配,商户改不了**。所以系统里的商家档案优先——
+ * 经营类型是我们自己维护的,也是匹配微信行业类目与费率的依据,不该被进件表单上的一次选择盖掉;
+ * 已进件商户也不提供补改行业的入口。商户在进件表单第一步选的「行业类型」只在档案给不出具体
+ * 行业时兜底(未配、或配成「其它」未识别),避免档案缺数据时一律落「其它」。
  *
- * <p>「其它」与识别不出来的取值返回空串,交给商家档案兜底——不要在这里拍一个 7399,
- * 那会把「档案里配了准确行业」的商户也降级成「其它」。
+ * <p>「其它」与识别不出来的取值返回空串(见 {@link #anUnrecognisedIndustryFallsBackToTheMerchantProfile}),
+ * 正是这个兜底生效的触发条件。
  */
 class MerchantOnboardingIndustryMccTests {
 
+    /** 系统档案能给出具体行业时,以档案为准——哪怕进件表单上选的是别的行业。 */
+    @Test
+    void theMerchantProfileWinsOverWhatWasPickedOnTheForm() {
+        assertThat(MerchantSettlementService.preferConfiguredMcc("7011", "餐饮服务")).isEqualTo("7011");
+        assertThat(MerchantSettlementService.preferConfiguredMcc("5812", "其它")).isEqualTo("5812");
+    }
+
+    /** 档案给不出具体行业(落兜底的「其它」)时,才用进件表单上申报的行业。 */
+    @Test
+    void theDeclaredIndustryIsOnlyAFallbackForAMissingProfileIndustry() {
+        assertThat(MerchantSettlementService.preferConfiguredMcc("7399", "餐饮服务")).isEqualTo("5812");
+        assertThat(MerchantSettlementService.preferConfiguredMcc("7399", "停车服务")).isEqualTo("7523");
+        // 两边都给不出具体行业时保持兜底值,不会变成空串。
+        assertThat(MerchantSettlementService.preferConfiguredMcc("7399", "其它")).isEqualTo("7399");
+        assertThat(MerchantSettlementService.preferConfiguredMcc("7399", "")).isEqualTo("7399");
+    }
+
     @Test
     void theIndustryPickedOnTheFormBecomesTheMcc() {
         assertThat(MerchantSettlementService.mccForIndustry("餐饮服务")).isEqualTo("5812");

--
Gitblit v1.7.1