yeg
2026-09-16 815d8a436a30d572ebf09817402bf35beb929aea
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantSettlementService.java
@@ -36,6 +36,7 @@
@Service
public class MerchantSettlementService {
    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MerchantSettlementService.class);
    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"),
@@ -57,12 +58,30 @@
    private MerchantOnboardingProgressService onboardingProgressService;
    @org.springframework.beans.factory.annotation.Autowired
    private MerchantRealnameService realnameService;
    @org.springframework.beans.factory.annotation.Autowired
    private com.talentcard.server.business.payment.huifu.HuifuOnboardingProgressGateway onboardingQueryGateway;
    public Map<String, Object> refreshOnboarding(MerchantPrincipal principal, boolean retry) {
        store(principal);
        List<Map<String,Object>> records=rows("select * from merchant_onboarding_application where merchant_id=? and store_id=? and initial_bank_card_id is not null order by id desc limit 1",principal.merchantId(),principal.storeId());
        if(records.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND,"Onboarding application not found");
        return onboardingProgressService.refresh(((Number)records.get(0).get("id")).longValue(),retry);
        return refreshProgress(((Number)records.get(0).get("id")).longValue(),retry);
    }
    /**
     * 刷新分阶段进度:**先推进已满足前置条件的开通步骤,再查询回报结果**。
     *
     * <p>分阶段模型下这条路径是商户与后台推进开通的唯一入口。渠道侧的微信商户配置与
     * 业务开通原本只由进件审核回调触发,而分阶段申请的回调走的是进度刷新——不在这里补上
     * 推进,新商户的微信开通就永远不会被发起。{@code continuePostOnboardingProvisioning}
     * 自身按状态判断是否还需要做,重复调用无害。
     */
    public Map<String, Object> refreshProgress(Long applicationId, boolean retry) {
        List<Map<String,Object>> records=rows("select * from merchant_onboarding_application where id=? and initial_bank_card_id is not null",applicationId);
        if(records.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND,"Onboarding application not found");
        String applicationNo=clean(records.get(0).get("applicationNo"));
        if (!applicationNo.isBlank()) continuePostOnboardingProvisioning(applicationNo);
        return onboardingProgressService.refresh(applicationId,retry);
    }
    /** 微信实名认证:商户主动推进的下一步。未决申请不会重复提交,见 MerchantRealnameService。 */
@@ -450,6 +469,12 @@
        if (huifuId.isBlank() || "rejected".equals(clean(application.get("status")))) return;
        String wechatStatus = clean(application.get("wechatConfigStatus"));
        // 本地状态可能与通道脱节(通道侧已经配好、本地还是 not_started)。微信商户配置是**添加**接口、
        // 成功后不支持修改删除(每种参数最多 5 组),配置已在通道生效时再提一次会留下重复配置。
        if (needsProvisioningStep(wechatStatus) && wechatConfigEffective(huifuId)) {
            jdbc.update("update merchant_onboarding_application set wechat_config_status='approved', wechat_config_response_message=? where id=?", "通道已有配置,按已配置收口", application.get("id"));
            wechatStatus = "approved";
        }
        if (wechatStatus.isBlank() || "not_started".equals(wechatStatus) || "failed".equals(wechatStatus)) {
            String requestNo = clean(application.get("wechatConfigRequestNo"));
            if (requestNo.isBlank()) requestNo = provisioningRequestNo(applicationNo, "WC");
@@ -475,6 +500,11 @@
        application = rows("select * from merchant_onboarding_application where id=? limit 1", application.get("id")).get(0);
        String businessStatus = clean(application.get("businessOpenStatus"));
        // 同理:通道侧已经开通微信业务时不再重新提交,否则会为同一商户开出第二份业务开通申请。
        if (needsProvisioningStep(businessStatus) && wechatBusinessEffective(huifuId)) {
            jdbc.update("update merchant_onboarding_application set business_open_status='approved', business_open_response_message=? where id=?", "通道已开通微信业务,按已开通收口", application.get("id"));
            businessStatus = "approved";
        }
        if (businessStatus.isBlank() || "not_started".equals(businessStatus) || "failed".equals(businessStatus)) {
            String requestNo = clean(application.get("businessOpenRequestNo"));
            if (requestNo.isBlank()) requestNo = provisioningRequestNo(applicationNo, "BO");
@@ -598,6 +628,57 @@
     * <p>这个异步通知只给申请单状态、不给授权状态,所以 {@code APPLYMENT_STATE_PASSED} 只能算
     * processing——授权是否完成要等查询到 {@code authorize_stat=1} 才能定,不能凭这一步就写 approved。
     */
    private static boolean needsProvisioningStep(String status) {
        return status.isBlank() || "not_started".equals(status) || "failed".equals(status);
    }
    /**
     * 微信小程序支付配置是否已经在通道生效:人才端 AppID 是否绑定在 {@code fee_type=01} 的配置项下。
     * 与分阶段进度用的是同一份证据。查询失败按「未生效」处理——宁可多提一次也不要漏配。
     */
    private boolean wechatConfigEffective(String huifuId) {
        try {
            Map<String, Object> config = onboardingQueryGateway.queryWechatConfig(huifuId);
            if (config == null || !"00000000".equals(clean(config.get("resp_code")))) return false;
            String appId = huifuPaymentProperties.getMiniProgramAppId().trim();
            for (Map<String, Object> entry : providerList(config.get("atu_config_bo_list"))) {
                if (!"01".equals(clean(entry.get("fee_type")))) continue;
                for (Map<String, Object> item : providerList(entry.get("wx_config_list"))) {
                    if ("1".equals(clean(item.get("config_type"))) && appId.equals(clean(item.get("config_value")))) return true;
                }
            }
            return false;
        } catch (RuntimeException exception) {
            log.warn("Huifu merchant WeChat config probe failed: huifuId={}", huifuId, exception);
            return false;
        }
    }
    /**
     * 微信业务是否已经在通道开通:商户详情的 {@code qry_wx_conf_list} 里出现线上三场景之一即视为已开通。
     * 业务开通是申请型接口,重复提交会给同一商户开出第二份申请,所以提交前必须先查。
     */
    private boolean wechatBusinessEffective(String huifuId) {
        try {
            Map<String, Object> detail = onboardingQueryGateway.queryCapabilities(huifuId);
            if (detail == null || !"00000000".equals(clean(detail.get("resp_code")))) return false;
            return providerList(detail.get("qry_wx_conf_list")).stream()
                    .anyMatch(item -> List.of("4", "5", "12").contains(clean(item.get("pay_scene"))));
        } catch (RuntimeException exception) {
            log.warn("Huifu merchant business open probe failed: huifuId={}", huifuId, exception);
            return false;
        }
    }
    /** 通道返回的列表字段可能是 JSON 字符串也可能是已解析的数组,两种都要能吃。 */
    @SuppressWarnings("unchecked")
    private static List<Map<String, Object>> providerList(Object raw) {
        if (raw == null) return List.of();
        Object parsed = raw instanceof String value ? (value.isBlank() ? null : com.alibaba.fastjson.JSON.parse(value)) : raw;
        if (!(parsed instanceof List<?> list)) return List.of();
        return list.stream().filter(Map.class::isInstance).map(item -> (Map<String, Object>) item).toList();
    }
    private static String realnameStatus(String applymentStat) {
        return switch (applymentStat) {
            case "APPLYMENT_STATE_REJECTED" -> "failed";