.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 关闭:重试入口暂不做,行业不提供补改入口、由系统动态匹配,见第六节。) --- 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)); 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");