yeg
2026-09-24 3dff4451ffe12e87d16792cdd8f744e1e29aad9c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
package com.talentcard.server.business.payment.huifu;
 
import org.springframework.boot.context.properties.ConfigurationProperties;
 
@ConfigurationProperties(prefix = "app.payment.huifu")
public class HuifuPaymentProperties {
 
    private boolean enabled;
    private String environment = "test";
    private String systemId = "";
    private String productId = "";
    private String merchantId = "";
    private String miniProgramAppId = "";
    private String privateKey = "";
    private String platformPublicKey = "";
    private String notifyUrl = "";
    private String merchantBusinessNotifyUrl = "";
    private String merchantOnboardingNotifyUrl = "";
    private String merchantWechatConfigNotifyUrl = "";
    private String merchantBusinessOpenNotifyUrl = "";
    /**
     * 进件时填在 {@code upper_huifu_id} 上的直属渠道号:新商户进件完成后归属这个渠道商。
     *
     * <p>层级只有两级——渠道商 → 商户,商户是**普通商户**,我们不再用
     * {@code head_office_flag=0} + {@code head_huifu_id} 把商户建成平台自己的下级商户。
     * 那条路实测没有任何收益:带着 head 字段进件成功后,平台对新建商户调用
     * {@code v2.merchant.busi.config} / {@code v2.merchant.busi.open} 仍然一律返回
     * {@code 20002 数据权限认证失败}(head/config 申请单已审核通过也一样);而渠道商直属的
     * 普通商户(如 {@code 6666000241335656})同样是这个回执,说明 20002 与层级无关,
     * 取决于签名身份本身有没有那些商户的数据权限。见
     * {@code .scratch/merchant-onboarding/NEXT-SESSION-PROMPT.md}。
     *
     * <p>反过来说:加 head 字段还会按总部客群类型索要 F725/F726/F727 材料,
     * 平白增加进件失败面,所以这里保持「只挂渠道商」。
     */
    private String onboardingUpperHuifuId = "";
    /**
     * 商户业务开通的 {@code use_chains_flag}:是否用上级(渠道商)的微信商户号发起交易。
     *
     * <p>留空(默认)即不下发该字段,子商户会在渠道下拿到自己的微信子商户号。
     * 不要下发 {@code 3}:那是「复用上级微信商户号」,要求上级本身有商户级微信配置,而我们的上级是
     * 渠道商(没有商户级配置,查询返回「huifuId不存在」),实测会被通道拒:
     * 「失败:复用交易信息,上级商户号为空」。
     */
    private String merchantUseChainsFlag = "";
    /**
     * 微信支付费率(%)。2026-09-24 起平台统一按**线下**场景报备,费率随之从线上的 0.70 改为线下的 0.30。
     * 通道侧真实生效值以商户详情 {@code qry_wx_conf_list[].fee_rate} 读回为准,不要以本配置为准。
     */
    private String merchantWechatFeeRate = "0.30";
    /**
     * 微信业务开通的支付场景({@code wx_conf_list[].pay_scene}),逗号分隔。
     *
     * <p>汇付的场景枚举:1=线下反扫、2=线下公众号、3=线下小程序、4=线上公众号、5=线上小程序、12=线上反扫。
     * 统一走线下三场景:它们共用同一个微信子商户号,只开小程序会让另外两条支付路径在通道侧没有配置
     * (线上时同理,见「场景 12 与 4/5 共用同一条线上规则」的实测)。
     *
     * <p>要回退线上,把它配成 {@code 4,5,12},并把下面三个费率规则号、费率、{@code merchant-wechat-fee-type}
     * 与进件场景一起换回线上——这四处是一套口径,只改一处会出现「业务开通成功但判定永远 missing」。
     */
    private String merchantWechatScenes = "1,2,3";
    /**
     * 微信业务开通类型({@code fee_type}):01=线上标准费率,02=线下。
     *
     * <p>它同时决定三件事,四处必须一致:微信 AppID 绑定({@code v2.merchant.busi.config} 的 {@code fee_type})、
     * 实名认证与实名查询的 {@code pay_scene}、以及通道回执({@code qry_wx_conf_list} /
     * {@code atu_config_bo_list})里按 {@code fee_type} 匹配配置项的口径。
     */
    private String merchantWechatFeeType = "02";
    /**
     * 企业微信业务开通使用的汇付费率规则 ID(CID)。企业场景应优先填写该值,
     * 由汇付按商户实际行业确认;留空时兼容旧的 mcc 请求方式。
     *
     * <p>官方《微信费率规则ID》表按「支付场景 × 业务开通类型 × 主体类型」定 CID。线下三场景(类型 02)
     * 下企业是 **758**;线上(类型 01)是 756。
     */
    private String merchantWechatFeeRuleId = "758";
    /**
     * 个人(小微)主体微信业务开通使用的费率规则 ID。
     *
     * <p>同一张表按**主体类型**分行:线下三场景(类型 02)下企业 758、个体户 762、**小微 770**;
     * 线上(类型 01)下企业 756、个体户 761、小微 775。所以这个号不能全平台共用一个——给小微发企业的
     * 号码会被通道回「微信费率规则号填写不正确,请检查!」(2026-09-16 个人商户实测)。
     */
    private String merchantWechatFeeRuleIdIndividual = "770";
    /** 个体工商户(走企业进件接口、对私结算)的费率规则 ID,同一张表线下口径是 762(线上是 761)。 */
    private String merchantWechatFeeRuleIdSoleProprietor = "762";
    /**
     * 线上业务类型编码({@code online_busi_type})。
     *
     * <p>官方口径:它是「基本信息入驻接口」在 {@code scene_type=ONLINE/ALL} 时的必填项。本平台此前把它
     * 发在**业务开通**请求里(错位),统一走线下后已停止下发,这个字段现在**没有任何调用点**。
     * 要回退线上,应把它加进两个进件网关(那里才是官方口径的位置),而不是加回业务开通。
     */
    private String merchantOnlineBusinessType = "H5311L";
    private String merchantOnlineMediaName = "东区人才卡";
    /**
     * 运营媒介主体与商户主体是否一致({@code media_mer_common_flag},商户业务开通的顶层参数)。
     *
     * <p>平台用同一个运营媒介为众多子商户开通业务,媒介主体(平台)与商户主体天然不一致,
     * 因此填 {@code N}。不填会被通道拒——实测报
     * 「【media_merCommon_flag-媒介主体与商户主体是否一致】不能为空,或格式非法」。
     */
    private String merchantMediaSubjectSame = "N";
    /**
     * 小微经营类型({@code micro_biz_type},个人商户开通微信业务时必填)。
     *
     * <p>MICRO_TYPE_STORE 门店场所 / MICRO_TYPE_MOBILE 流动经营 / MICRO_TYPE_ONLINE 线上商品服务交易。
     * 文档注明「个人商户需要填写」,且**三种取值都需要 F22 店铺门头照与 F24 店铺内景照**,
     * 补充资料挂在 {@code extended_material_list} 里。
     */
    private String merchantMicroBusinessType = "MICRO_TYPE_STORE";
    /**
     * 商户进件的 {@code scene_type}:ONLINE/OFFLINE/ALL。
     *
     * <p>统一走**线下**报备(实体经营场所口径),通道据此决定索要哪些经营场所材料。回退线上时配 {@code ONLINE}。
     */
    private String merchantOnboardingSceneType = "OFFLINE";
    private String merchantAgreementUrl = "https://cloudpnrcdn.oss-cn-shanghai.aliyuncs.com/opps/api/prod/download_file/PaymentServiceAgreement.htm";
    private Boolean merchantCapabilityRequired;
 
    public boolean isEnabled() { return enabled; }
    public void setEnabled(boolean enabled) { this.enabled = enabled; }
    public String getEnvironment() { return environment; }
    public void setEnvironment(String environment) { this.environment = environment; }
    public String getSystemId() { return systemId; }
    public void setSystemId(String systemId) { this.systemId = systemId; }
    public String getProductId() { return productId; }
    public void setProductId(String productId) { this.productId = productId; }
    public String getMerchantId() { return merchantId; }
    public void setMerchantId(String merchantId) { this.merchantId = merchantId; }
    public String getMiniProgramAppId() { return miniProgramAppId; }
    public void setMiniProgramAppId(String miniProgramAppId) { this.miniProgramAppId = miniProgramAppId; }
    public String getPrivateKey() { return privateKey; }
    public void setPrivateKey(String privateKey) { this.privateKey = privateKey; }
    public String getPlatformPublicKey() { return platformPublicKey; }
    public void setPlatformPublicKey(String platformPublicKey) { this.platformPublicKey = platformPublicKey; }
    public String getNotifyUrl() { return notifyUrl; }
    public void setNotifyUrl(String notifyUrl) { this.notifyUrl = notifyUrl; }
    public String getMerchantBusinessNotifyUrl() {
        if (merchantBusinessNotifyUrl != null && !merchantBusinessNotifyUrl.isBlank()) return merchantBusinessNotifyUrl;
        return callbackUrl("merchant-business-notify");
    }
    public void setMerchantBusinessNotifyUrl(String merchantBusinessNotifyUrl) { this.merchantBusinessNotifyUrl = merchantBusinessNotifyUrl; }
    public String getMerchantOnboardingNotifyUrl() {
        if (merchantOnboardingNotifyUrl != null && !merchantOnboardingNotifyUrl.isBlank()) return merchantOnboardingNotifyUrl;
        return callbackUrl("merchant-onboarding-notify");
    }
    public void setMerchantOnboardingNotifyUrl(String merchantOnboardingNotifyUrl) { this.merchantOnboardingNotifyUrl = merchantOnboardingNotifyUrl; }
    public String getMerchantWechatConfigNotifyUrl() {
        if (merchantWechatConfigNotifyUrl != null && !merchantWechatConfigNotifyUrl.isBlank()) return merchantWechatConfigNotifyUrl;
        return callbackUrl("merchant-wechat-config-notify");
    }
    public void setMerchantWechatConfigNotifyUrl(String merchantWechatConfigNotifyUrl) { this.merchantWechatConfigNotifyUrl = merchantWechatConfigNotifyUrl; }
    public String getMerchantBusinessOpenNotifyUrl() {
        if (merchantBusinessOpenNotifyUrl != null && !merchantBusinessOpenNotifyUrl.isBlank()) return merchantBusinessOpenNotifyUrl;
        return callbackUrl("merchant-business-open-notify");
    }
    public void setMerchantBusinessOpenNotifyUrl(String merchantBusinessOpenNotifyUrl) { this.merchantBusinessOpenNotifyUrl = merchantBusinessOpenNotifyUrl; }
    public String getOnboardingUpperHuifuId() {
        if (onboardingUpperHuifuId != null && !onboardingUpperHuifuId.isBlank()) return onboardingUpperHuifuId;
        return merchantId;
    }
    public void setOnboardingUpperHuifuId(String onboardingUpperHuifuId) { this.onboardingUpperHuifuId = onboardingUpperHuifuId; }
    public String getMerchantUseChainsFlag() { return merchantUseChainsFlag == null ? "" : merchantUseChainsFlag.trim(); }
    public void setMerchantUseChainsFlag(String merchantUseChainsFlag) { this.merchantUseChainsFlag = merchantUseChainsFlag; }
    public String getMerchantWechatFeeRate() { return merchantWechatFeeRate; }
    public void setMerchantWechatFeeRate(String merchantWechatFeeRate) { this.merchantWechatFeeRate = merchantWechatFeeRate; }
    public String getMerchantWechatScenes() { return merchantWechatScenes == null ? "" : merchantWechatScenes.trim(); }
    public void setMerchantWechatScenes(String merchantWechatScenes) { this.merchantWechatScenes = merchantWechatScenes; }
    /**
     * 支付场景列表(忽略空项)。刻意不叫 {@code getMerchantWechatScenes}:配置项是逗号分隔的字符串,
     * 让同一个属性的 getter/setter 一个是 String、一个是 List 会让 Spring 的绑定行为变得难以预料。
     */
    public java.util.List<String> merchantWechatSceneList() {
        java.util.List<String> scenes = new java.util.ArrayList<>();
        for (String scene : getMerchantWechatScenes().split(",")) if (!scene.isBlank()) scenes.add(scene.trim());
        return scenes;
    }
    public String getMerchantWechatFeeType() { return merchantWechatFeeType == null ? "" : merchantWechatFeeType.trim(); }
    public void setMerchantWechatFeeType(String merchantWechatFeeType) { this.merchantWechatFeeType = merchantWechatFeeType; }
    public String getMerchantOnboardingSceneType() { return merchantOnboardingSceneType == null ? "" : merchantOnboardingSceneType.trim(); }
    public void setMerchantOnboardingSceneType(String merchantOnboardingSceneType) { this.merchantOnboardingSceneType = merchantOnboardingSceneType; }
    public String getMerchantWechatFeeRuleId() { return merchantWechatFeeRuleId == null ? "" : merchantWechatFeeRuleId.trim(); }
    public void setMerchantWechatFeeRuleId(String merchantWechatFeeRuleId) { this.merchantWechatFeeRuleId = merchantWechatFeeRuleId; }
    public String getMerchantWechatFeeRuleIdIndividual() { return merchantWechatFeeRuleIdIndividual == null ? "" : merchantWechatFeeRuleIdIndividual.trim(); }
    public void setMerchantWechatFeeRuleIdIndividual(String merchantWechatFeeRuleIdIndividual) { this.merchantWechatFeeRuleIdIndividual = merchantWechatFeeRuleIdIndividual; }
    public String getMerchantWechatFeeRuleIdSoleProprietor() { return merchantWechatFeeRuleIdSoleProprietor == null ? "" : merchantWechatFeeRuleIdSoleProprietor.trim(); }
    public void setMerchantWechatFeeRuleIdSoleProprietor(String merchantWechatFeeRuleIdSoleProprietor) { this.merchantWechatFeeRuleIdSoleProprietor = merchantWechatFeeRuleIdSoleProprietor; }
    public String getMerchantOnlineBusinessType() { return merchantOnlineBusinessType; }
    public void setMerchantOnlineBusinessType(String merchantOnlineBusinessType) { this.merchantOnlineBusinessType = merchantOnlineBusinessType; }
    public String getMerchantOnlineMediaName() { return merchantOnlineMediaName; }
    public void setMerchantOnlineMediaName(String merchantOnlineMediaName) { this.merchantOnlineMediaName = merchantOnlineMediaName; }
    public String getMerchantMediaSubjectSame() { return merchantMediaSubjectSame == null ? "N" : merchantMediaSubjectSame.trim(); }
    public void setMerchantMediaSubjectSame(String merchantMediaSubjectSame) { this.merchantMediaSubjectSame = merchantMediaSubjectSame; }
    public String getMerchantMicroBusinessType() { return merchantMicroBusinessType == null ? "" : merchantMicroBusinessType.trim(); }
    public void setMerchantMicroBusinessType(String merchantMicroBusinessType) { this.merchantMicroBusinessType = merchantMicroBusinessType; }
    public String getMerchantAgreementUrl() { return merchantAgreementUrl; }
    public void setMerchantAgreementUrl(String merchantAgreementUrl) { this.merchantAgreementUrl = merchantAgreementUrl; }
    /** Provider-side capability confirmation is on whenever the provider integration is enabled. */
    public boolean isMerchantCapabilityRequired() {
        return merchantCapabilityRequired != null ? merchantCapabilityRequired : enabled;
    }
    public void setMerchantCapabilityRequired(boolean merchantCapabilityRequired) { this.merchantCapabilityRequired = merchantCapabilityRequired; }
 
    /** 由支付回调地址推导同一 host 与路径下的其它回调地址。 */
    private String callbackUrl(String endpoint) {
        if (notifyUrl == null || notifyUrl.isBlank()) return "";
        int slash = notifyUrl.lastIndexOf('/');
        return slash < 0 ? "" : notifyUrl.substring(0, slash + 1) + endpoint;
    }
}