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;
|
}
|
}
|