yeg
2 天以前 ff4525af5b4178a38ca310719e4c7af9af3e742c
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantOnboardingProgressService.java
@@ -3,6 +3,7 @@
import com.alibaba.fastjson.JSON;
import com.talentcard.server.business.DatabaseRows;
import com.talentcard.server.business.payment.huifu.HuifuOnboardingProgressGateway;
import com.talentcard.server.business.payment.huifu.HuifuPaymentProperties;
import com.talentcard.server.common.BusinessException;
import com.talentcard.server.common.ErrorCode;
import java.util.ArrayList;
@@ -24,7 +25,8 @@
    }
    private final JdbcTemplate jdbc;
    private final HuifuOnboardingProgressGateway gateway;
    public MerchantOnboardingProgressService(JdbcTemplate jdbc, HuifuOnboardingProgressGateway gateway) { this.jdbc=jdbc; this.gateway=gateway; }
    private final HuifuPaymentProperties huifuPaymentProperties;
    public MerchantOnboardingProgressService(JdbcTemplate jdbc, HuifuOnboardingProgressGateway gateway, HuifuPaymentProperties huifuPaymentProperties) { this.jdbc=jdbc; this.gateway=gateway; this.huifuPaymentProperties=huifuPaymentProperties; }
    /**
     * 分阶段进度适用于「已关联结算银行卡、且未被整体拒绝」的申请。
@@ -42,6 +44,21 @@
    }
    public Map<String,Object> view(Long id) { return view(application(id)); }
    /** 进件来源标记:后台纳管进来的外部进件商户。取值与 V158 的 {@code source} 列一致。 */
    public static final String SOURCE_MANAGED = "managed";
    /**
     * 这条进件是不是「后台纳管的外部进件」。
     *
     * <p>它决定这条进件能不能**下发**通道改动:纳管全程只查询、不下发(外部商户的口径已由业务要求
     * 保持一致,只需核实;而补开微信、绑人才端、改支付场景、开启延迟入账这些下发操作都不幂等,
     * 连锁多店共用同一个汇付商户号时各自下发还会重复下发)。判定只留这一处定义,刷新链路与纳管/解绑
     * 都读它,避免同一个来源标记在几处各自比较字符串。
     */
    public static boolean isManaged(Map<String,Object> application) {
        return SOURCE_MANAGED.equals(text(application.get("source")).trim());
    }
    @org.springframework.transaction.annotation.Transactional(propagation = org.springframework.transaction.annotation.Propagation.NOT_SUPPORTED)
    public Map<String,Object> refresh(Long applicationId, boolean retry) {
@@ -80,8 +97,13 @@
                    }
                    if (capabilities.containsKey("qry_wx_conf_list")) {
                        List<Map<String,Object>> wechat = configurations(capabilities.get("qry_wx_conf_list"));
                        boolean valid = List.of("4", "5", "12").stream().allMatch(scene -> wechat.stream().anyMatch(item ->
                                scene.equals(text(item.get("pay_scene"))) && decimal(item.get("fee_rate"), "0.70")
                        // 口径必须与业务开通的下发取同一份配置(场景、费率、业务开通类型都来自 properties):
                        // 判定跟着下发走,否则会出现「通道已开通、阶段永远 missing」,而它下游的收款闸门
                        // 会把商户挡在下单之外。2026-09-24 前这里是硬编码的线上 4/5/12 + 0.70。
                        List<String> expectedScenes = huifuPaymentProperties.merchantWechatSceneList();
                        String expectedFeeRate = huifuPaymentProperties.getMerchantWechatFeeRate().trim();
                        boolean valid = !expectedScenes.isEmpty() && expectedScenes.stream().allMatch(scene -> wechat.stream().anyMatch(item ->
                                scene.equals(text(item.get("pay_scene"))) && decimal(item.get("fee_rate"), expectedFeeRate)
                                // fee_min_amt 是可选字段,且文档写明「大于 0」——不设最低收费时我们**不下发**它,
                                // 通道也就不返回它。缺失必须按 0 处理,否则这条判定永远不成立:商户微信业务
                                // 明明已经开好(三场景、费率、内扣、子商户号都对),阶段却永远停在未确认
@@ -114,10 +136,13 @@
                Map<String,Object> appConfig = gateway.queryWechatConfig(text(application.get("providerHuifuId")));
                if (appConfig != null && "00000000".equals(text(appConfig.get("resp_code")))) {
                    boolean bound = false, controlled = false;
                    // 业务开通类型也取配置:绑定写在哪个 fee_type 下就得在哪个下面找,两边必须同一个值。
                    String expectedFeeType = huifuPaymentProperties.getMerchantWechatFeeType();
                    String expectedAppId = huifuPaymentProperties.getMiniProgramAppId().trim();
                    for (Map<String,Object> config : configurations(appConfig.getOrDefault("atu_config_bo_list", List.of()))) {
                        if (!"01".equals(text(config.get("fee_type")))) continue;
                        if (!expectedFeeType.equals(text(config.get("fee_type")))) continue;
                        for (Map<String,Object> entry : configurations(config.getOrDefault("wx_config_list", List.of()))) {
                            if ("1".equals(text(entry.get("config_type"))) && "wxcfee477acb54d179".equals(text(entry.get("config_value")))) {
                            if ("1".equals(text(entry.get("config_type"))) && expectedAppId.equals(text(entry.get("config_value")))) {
                                bound = true;
                                controlled |= "Y".equals(text(entry.get("control_flag")));
                            }
@@ -137,7 +162,7 @@
                        if (!"confirmed".equals(stages.get("realname"))) stages.put("realname", "missing");
                    } else jdbc.update("update merchant_onboarding_progress set sync_error=? where application_id=? and sync_owner=?",queryError(realname),applicationId,owner);
                }
                if ("Y".equals(audit) && "S".equals(text(response.get("reg_status"))) && capabilities != null
                if (!isManaged(application) && "Y".equals(audit) && "S".equals(text(response.get("reg_status"))) && capabilities != null
                        && "00000000".equals(text(capabilities.get("resp_code"))) && "NORMAL".equals(text(capabilities.get("mer_stat")))
                        && capabilities.containsKey("delay_flag") && capabilities.containsKey("forced_delay_flag")) {
                    advanceDelay(applicationId,text(application.get("providerHuifuId")),owner,stages,retry);