yeg
2026-09-15 9d994ea0eee51dbc87fe83ead985580545926b8a
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantOnboardingProgressService.java
@@ -29,6 +29,13 @@
    public boolean supports(Map<String,Object> application) { return RECOVERY_HUIFU_ID.equals(text(application.get("providerHuifuId"))); }
    public Map<String,Object> view(Long id) {
        Map<String,Object> application = application(id);
        if (!supports(application)) throw new BusinessException(ErrorCode.CONFLICT,"This recovery is limited to the designated existing merchant");
        return view(application);
    }
    @org.springframework.transaction.annotation.Transactional(propagation = org.springframework.transaction.annotation.Propagation.NOT_SUPPORTED)
    public Map<String,Object> refresh(Long applicationId, boolean retry) {
        Map<String,Object> application=application(applicationId);
        if (!supports(application)) throw new BusinessException(ErrorCode.CONFLICT,"This recovery is limited to the designated existing merchant");
@@ -48,6 +55,28 @@
                jdbc.update("update merchant_onboarding_progress set stages_json=?,last_synced_at=now(),sync_error='' where application_id=? and sync_owner=?",JSON.toJSONString(stages),applicationId,owner);
                Map<String,Object> capabilities=gateway.queryCapabilities(text(application.get("providerHuifuId")));
                if (capabilities != null && "00000000".equals(text(capabilities.get("resp_code")))) {
                    if (capabilities.containsKey("mer_stat")) stages.put("merchantNormal", "NORMAL".equals(text(capabilities.get("mer_stat"))));
                    if ("S".equals(text(response.get("bind_card_status"))) && capabilities.containsKey("qry_cash_card_info_list") && capabilities.containsKey("qry_settle_config_list")) {
                        stages.put("bank", confirmOriginalCard(application,capabilities,owner));
                    }
                    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")
                                && zero(item.get("fee_min_amt")) && "1".equals(text(item.get("fee_charge_type")))
                                && "2".equals(text(item.get("out_fee_flag"))) && item.containsKey("wx_mer_infos")
                                && configurations(item.get("wx_mer_infos")).stream().anyMatch(merchant ->
                                        !text(merchant.get("bank_mer_code")).isBlank() && "1".equals(text(merchant.get("is_default"))))));
                        stages.put("wechat", valid ? "confirmed" : "missing");
                    }
                    if (capabilities.containsKey("qry_settle_config_list")) {
                        List<Map<String,Object>> settlements = configurations(capabilities.get("qry_settle_config_list"));
                        boolean valid = settlements.size() == 1 && settlements.stream().allMatch(item ->
                                "1".equals(text(item.get("settle_status"))) && "D1".equals(text(item.get("settle_cycle")))
                                && "P0".equals(text(item.get("settle_pattern")))
                                && zero(item.get("min_amt")) && zero(item.get("remained_amt")));
                        stages.put("settlement", valid ? "confirmed" : "missing");
                    }
                    if (capabilities.containsKey("delay_flag") && capabilities.containsKey("forced_delay_flag")) {
                        stages.put("delay", "Y".equals(capabilities.get("delay_flag")) && "N".equals(capabilities.get("forced_delay_flag")) ? "confirmed" : "missing");
                    } else {
@@ -58,6 +87,46 @@
                    jdbc.update("update merchant_onboarding_progress set sync_error=? where application_id=? and sync_owner=?",queryError(capabilities),applicationId,owner);
                }
                jdbc.update("update merchant_onboarding_progress set last_synced_at=now() where application_id=? and sync_owner=?",applicationId,owner);
                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;
                    for (Map<String,Object> config : configurations(appConfig.getOrDefault("atu_config_bo_list", List.of()))) {
                        if (!"01".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")))) {
                                bound = true;
                                controlled |= "Y".equals(text(entry.get("control_flag")));
                            }
                        }
                    }
                    stages.put("appId", controlled ? "action_required" : bound ? "confirmed" : "missing");
                } else if (appConfig != null && !appConfig.isEmpty()) {
                    jdbc.update("update merchant_onboarding_progress set sync_error=? where application_id=? and sync_owner=?",queryError(appConfig),applicationId,owner);
                }
                Map<String,Object> realname = gateway.queryRealname(text(application.get("providerHuifuId")));
                if (realname != null && "00000000".equals(text(realname.get("resp_code")))) {
                    String status = text(realname.get("applyment_stat"));
                    boolean authorized = "APPLYMENT_STATE_PASSED".equals(status) && "1".equals(text(realname.get("authorize_stat")));
                    if (!status.isBlank()) stages.put("realname", authorized ? "confirmed" :
                            "APPLYMENT_STATE_REJECTED".equals(status) ? "failed" :
                            "APPLYMENT_STATE_WAITTING_FOR_AUDIT".equals(status) ? "pending" : "action_required");
                    stages.put("realnameQr", qrCode(text(realname.get("qrcode_data"))));
                } else if (realname != null && !realname.isEmpty()) {
                    if ("00000001".equals(text(realname.get("resp_code"))) && "AT入驻信息池信息不存在".equals(text(realname.get("resp_desc")))) {
                        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
                        && "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,owner,stages,retry);
                }
                jdbc.update("update merchant_onboarding_progress set stages_json=? where application_id=? and sync_owner=?",JSON.toJSONString(stages),applicationId,owner);
                jdbc.update("""
                        update merchant_onboarding_application a join merchant_onboarding_progress p on p.application_id=a.id
                        set a.status=?,a.reviewed_at=case when ? then now() else null end
                        where a.id=? and p.sync_owner=?
                        """,complete(stages) ? "approved" : "pending",complete(stages),applicationId,owner);
            }
        } catch (RuntimeException exception) {
            jdbc.update("update merchant_onboarding_progress set sync_error=? where application_id=? and sync_owner=?","查询结果暂未确认,已保留最后有效进度,请稍后刷新",applicationId,owner);
@@ -65,6 +134,75 @@
            jdbc.update("update merchant_onboarding_progress set sync_owner=null,sync_until=null where application_id=? and sync_owner=?",applicationId,owner);
        }
        return view(application(applicationId));
    }
    private boolean confirmOriginalCard(Map<String,Object> application, Map<String,Object> capabilities, String owner) {
        List<Map<String,Object>> original=rows("select * from merchant_bank_card where id=? and merchant_id=?",application.get("initialBankCardId"),application.get("merchantId"));
        if (original.size()!=1) return false;
        Map<String,Object> card=original.get(0);
        for (Map<String,Object> remote:configurations(capabilities.get("qry_cash_card_info_list"))) {
            String number=text(remote.get("card_no")), token=text(remote.get("token_no"));
            if (!number.matches("[0-9]{8,32}") || token.isBlank() || !sha256(number).equals(text(card.get("cardHash")))) continue;
            String previous=text(card.get("providerTokenNo"));
            if (!previous.isBlank() && !previous.equals(token)) continue;
            boolean linked=configurations(capabilities.get("qry_settle_config_list")).stream().anyMatch(settle -> token.equals(text(settle.get("token_no"))));
            if (!linked) continue;
            return jdbc.update("""
                    update merchant_bank_card b join merchant_onboarding_progress p on p.application_id=?
                    set b.status='approved',b.provider_token_no=?,b.reviewed_at=coalesce(b.reviewed_at,now())
                    where b.id=? and p.sync_owner=?
                    """,application.get("id"),token,card.get("id"),owner)>0;
        }
        return false;
    }
    private static String sha256(String value) {
        try { return java.util.HexFormat.of().formatHex(java.security.MessageDigest.getInstance("SHA-256").digest(value.getBytes(java.nio.charset.StandardCharsets.UTF_8))); }
        catch (java.security.NoSuchAlgorithmException exception) { throw new IllegalStateException(exception); }
    }
    private static boolean complete(Map<String,Object> stages) {
        return Boolean.TRUE.equals(stages.get("bank")) && Boolean.TRUE.equals(stages.get("merchantNormal"))
                && LABELS.keySet().stream().allMatch(key -> "confirmed".equals(stages.get(key)));
    }
    private void advanceDelay(Long id, String owner, Map<String,Object> stages, boolean retry) {
        List<Map<String,Object>> attempts=rows("select * from merchant_onboarding_attempt where application_id=? and step='delay' order by id desc limit 1",id);
        Map<String,Object> attempt=attempts.isEmpty() ? null : attempts.get(0);
        if ("confirmed".equals(stages.get("delay"))) {
            if (attempt != null) jdbc.update("update merchant_onboarding_attempt set status='confirmed' where id=?",attempt.get("id"));
            return;
        }
        if (attempt != null && !"failed".equals(text(attempt.get("status")))) {
            String applyNo=text(attempt.get("applyNo"));
            if (!applyNo.isBlank() && !List.of("succeeded","confirmed").contains(text(attempt.get("status")))) {
                Map<String,Object> result=gateway.queryApplication(RECOVERY_HUIFU_ID,applyNo);
                if (result != null && "00000000".equals(text(result.get("resp_code"))) && List.of("N","F").contains(text(result.get("apply_status")))) {
                    jdbc.update("update merchant_onboarding_attempt set status='failed',response_code=? where id=? and status<>'confirmed'",text(result.get("resp_code")),attempt.get("id"));
                    attempt.put("status","failed");
                }
            }
            if (!"failed".equals(text(attempt.get("status")))) { stages.put("delay","pending"); return; }
        }
        if (attempt != null && !retry) { stages.put("delay","failed"); return; }
        String requestNo=UUID.randomUUID().toString().replace("-", "");
        String date=java.time.LocalDate.now(java.time.ZoneId.of("Asia/Shanghai")).format(java.time.format.DateTimeFormatter.BASIC_ISO_DATE);
        int inserted=jdbc.update("""
                insert into merchant_onboarding_attempt(application_id,step,request_no,request_date,status)
                select ?, 'delay', ?, ?, 'unknown' from merchant_onboarding_progress
                where application_id=? and sync_owner=? and sync_until>now()
                """,id,requestNo,date,id,owner);
        if (inserted==0) return;
        stages.put("delay","pending");
        try {
            Map<String,Object> result=gateway.configureDelay(RECOVERY_HUIFU_ID,requestNo,date);
            String code=result==null ? "" : text(result.get("resp_code"));
            String status=code.isBlank() ? "unknown" : List.of("00000000","00000100","90000000").contains(code) ? "pending" : "failed";
            jdbc.update("update merchant_onboarding_attempt set status=?,apply_no=nullif(?,''),response_code=? where request_no=?",status,result==null ? "" : text(result.get("apply_no")),code,requestNo);
            if ("failed".equals(status)) stages.put("delay","failed");
        } catch (RuntimeException ignored) {
            // The durable unknown attempt blocks resubmission after a crash, timeout or invalid response.
        }
    }
    public Map<String,Object> view(Map<String,Object> application) {
@@ -81,9 +219,34 @@
            result.put("syncError",text(saved.get(0).get("syncError")));
        }
        List<Map<String,Object>> progress=new ArrayList<>();
        for (var entry:LABELS.entrySet()) progress.add(Map.of("key",entry.getKey(),"label",entry.getValue(),"status",text(stages.getOrDefault(entry.getKey(),"unknown")),"reason","尚未取得通道生效证据"));
        for (var entry:LABELS.entrySet()) {
            String status = text(stages.getOrDefault(entry.getKey(),"unknown"));
            progress.add(Map.of("key",entry.getKey(),"label",entry.getValue(),"status",status,"reason",stageReason(entry.getKey(),status),
                    "qrCode", "realname".equals(entry.getKey()) ? text(stages.get("realnameQr")) : ""));
        }
        result.put("stages",progress); result.put("reason","请刷新进度,核实原申请与各项能力");
        result.put("retryable", "failed".equals(stages.get("delay")));
        result.put("status",complete(stages) ? "approved" : "pending");
        result.put("reason",complete(stages) ? "收款所需能力已核实,沿用原商户号和结算银行卡" : "请刷新进度核实各项能力;微信行业规则仍待平台确认,暂不提交微信补开");
        return result;
    }
    /** Called only after the existing HTTP callback boundary has verified Huifu's signature. */
    public Long acceptDelayCallback(Map<String,Object> data) {
        List<Map<String,Object>> matches=rows("""
                select t.id,t.application_id from merchant_onboarding_attempt t
                join merchant_onboarding_application a on a.id=t.application_id
                where t.step='delay' and t.request_no=? and t.request_date=? and a.provider_huifu_id=?
                """,text(data.get("req_seq_id")),text(data.get("req_date")),RECOVERY_HUIFU_ID);
        if (matches.isEmpty() || !RECOVERY_HUIFU_ID.equals(text(data.get("huifu_id")))) return null;
        Map<String,Object> attempt=matches.get(0);
        String audit=text(data.get("audit_status"));
        String state="Y".equals(audit) ? "succeeded" : List.of("N","F","C").contains(audit) ? "failed" : "pending";
        jdbc.update("""
                update merchant_onboarding_attempt set status=?,apply_no=coalesce(apply_no,nullif(?,'')),response_code=?
                where id=? and status not in ('succeeded','confirmed')
                """,state,text(data.get("apply_no")),text(data.getOrDefault("sub_resp_code",data.get("resp_code"))),attempt.get("id"));
        return ((Number)attempt.get("applicationId")).longValue();
    }
    private Map<String,Object> application(Long id) {
@@ -97,6 +260,33 @@
    }
    private List<Map<String,Object>> rows(String sql,Object... args) { return DatabaseRows.normalize(jdbc.queryForList(sql,args)); }
    private static String text(Object value) { return value==null ? "" : String.valueOf(value); }
    private static String stageReason(String key, String status) {
        if ("confirmed".equals(status)) return "已取得通道生效证据";
        if ("realname".equals(key) && "action_required".equals(status)) return "请法人或授权联系人完成微信实名及授权,办理后刷新;没有二维码时请联系平台运营";
        if ("appId".equals(key) && "action_required".equals(status)) return "人才端绑定被微信管控,请联系平台运营解除后刷新";
        if ("delay".equals(key) && "missing".equals(status)) return "需开启延迟入账并关闭强制延迟";
        if ("settlement".equals(key) && "missing".equals(status)) return "需开启 D1 批次结算,起结金额与留存金额均为 0";
        if ("pending".equals(status)) return "通道正在审核,请稍后刷新";
        if ("failed".equals(status)) return "该步骤未通过,请联系平台运营核对资料后局部重试";
        return "尚未取得通道生效证据";
    }
    private static String qrCode(String raw) {
        String value = raw.replaceFirst("^data:image/png;base64,", "");
        if (value.length() > 200000 || !value.startsWith("iVBORw0KGgo") || !value.matches("[A-Za-z0-9+/=\\r\\n]+")) return "";
        return "data:image/png;base64," + value;
    }
    @SuppressWarnings("unchecked")
    private static List<Map<String,Object>> configurations(Object raw) {
        Object parsed = raw instanceof String value ? JSON.parse(value) : raw;
        if (!(parsed instanceof List<?> list)) throw new IllegalArgumentException("Invalid provider configuration list");
        return list.stream().map(item -> (Map<String,Object>)item).toList();
    }
    private static boolean zero(Object value) {
        return decimal(value, "0");
    }
    private static boolean decimal(Object value, String expected) {
        return value != null && new java.math.BigDecimal(text(value)).compareTo(new java.math.BigDecimal(expected)) == 0;
    }
    private static String queryError(Map<String,Object> response) {
        return response != null && "20002".equals(text(response.get("resp_code")))
                ? "汇付查询权限不足(20002),请联系平台运营核实接口权限"