yeg
2026-09-14 7217e095eda8326e24aaef7b72c11d16f88e8742
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
package com.talentcard.server.business.payment.huifu;
 
import com.alibaba.fastjson.JSON;
import com.huifu.bspay.sdk.opps.client.BasePayClient;
import com.huifu.bspay.sdk.opps.core.BasePay;
import com.huifu.bspay.sdk.opps.core.config.MerConfig;
import com.talentcard.server.common.BusinessException;
import com.talentcard.server.common.ErrorCode;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
 
@Service
public class HuifuIndividualOnboardingGateway {
    private static final Object SDK_LOCK = new Object();
    private static final Logger log = LoggerFactory.getLogger(HuifuIndividualOnboardingGateway.class);
    private final HuifuPaymentProperties properties;
 
    public HuifuIndividualOnboardingGateway(HuifuPaymentProperties properties) { this.properties = properties; }
 
    public Result create(HuifuIndividualOnboardingCommand command) {
        ensureConfigured();
        Map<String, Object> card = new LinkedHashMap<>();
        card.put("card_type", "1");
        card.put("card_name", command.name());
        card.put("card_no", command.cardNumber());
        card.put("prov_id", command.bankProvinceCode());
        card.put("area_id", command.bankCityCode());
        card.put("bank_code", command.bankCode());
        card.put("cert_type", "00");
        card.put("cert_no", command.idCardNo());
        card.put("cert_validity_type", command.certificateValidityType());
        card.put("cert_begin_date", command.certificateBeginDate());
        if (!blank(command.certificateEndDate())) card.put("cert_end_date", command.certificateEndDate());
        card.put("mp", command.mobileNo());
        card.put("is_settle_default", "Y");
 
        Map<String, Object> cash = new LinkedHashMap<>();
        cash.put("cash_type", "DM");
        cash.put("fix_amt", "0.00");
        cash.put("fee_rate", "");
 
        Map<String, Object> request = new LinkedHashMap<>();
        request.put("req_seq_id", command.applicationNo());
        request.put("req_date", LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE));
        request.put("upper_huifu_id", properties.getOnboardingUpperHuifuId().trim());
        request.put("reg_name", command.name());
        request.put("short_name", command.name());
        request.put("mcc", command.mcc());
        request.put("scene_type", "ONLINE");
        request.put("prov_id", command.provinceCode());
        request.put("area_id", command.cityCode());
        request.put("district_id", command.districtCode());
        request.put("detail_addr", command.address());
        request.put("legal_cert_no", command.idCardNo());
        request.put("legal_cert_validity_type", command.certificateValidityType());
        request.put("legal_cert_begin_date", command.certificateBeginDate());
        putIfPresent(request, "legal_cert_end_date", command.certificateEndDate());
        request.put("legal_addr", command.address());
        request.put("legal_cert_front_pic", command.legalFrontPictureId());
        request.put("legal_cert_back_pic", command.legalBackPictureId());
        request.put("contact_name", command.name());
        request.put("contact_mobile_no", command.mobileNo());
        request.put("contact_email", command.email());
        request.put("login_name", loginName(command.applicationNo()));
        request.put("card_info", JSON.toJSONString(card));
        request.put("settle_card_front_pic", command.settlementCardFrontPictureId());
        request.put("cash_config", JSON.toJSONString(List.of(cash)));
        // 与商户主体一致:进件即打开自动结算,核销确认后的钱由斗拱按结算周期自动划到结算卡。
        request.put("settle_config", JSON.toJSONString(HuifuEnterpriseOnboardingGateway.settleConfig()));
        putIfPresent(request, "async_return_url", properties.getMerchantOnboardingNotifyUrl());
        try {
            synchronized (SDK_LOCK) {
                initializeSdk();
                Map<String, Object> response = BasePayClient.request(request, "v2.merchant.basicdata.indv", false);
                log.info("Huifu individual merchant onboarding response: applicationNo={}, upperHuifuId={}, response={}", command.applicationNo(), properties.getOnboardingUpperHuifuId(), JSON.toJSONString(response));
                return Result.from(response);
            }
        } catch (BusinessException exception) {
            throw exception;
        } catch (Exception exception) {
            log.error("Huifu individual merchant onboarding request error: applicationNo={}, upperHuifuId={}", command.applicationNo(), properties.getOnboardingUpperHuifuId(), exception);
            throw new HuifuPaymentGatewayException("Huifu individual merchant onboarding request failed", outcomeUnknown(exception), exception);
        }
    }
 
    private void ensureConfigured() {
        if (!properties.isEnabled()) throw new BusinessException(ErrorCode.CONFLICT, "Huifu payment is disabled");
        if (blank(properties.getSystemId()) || blank(properties.getProductId()) || blank(properties.getOnboardingUpperHuifuId()) || blank(properties.getPrivateKey()) || blank(properties.getPlatformPublicKey())) throw new BusinessException(ErrorCode.CONFLICT, "Huifu merchant onboarding configuration is incomplete");
    }
 
    private void initializeSdk() throws Exception {
        BasePay.debug = false;
        BasePay.prodMode = "prod".equalsIgnoreCase(properties.getEnvironment()) ? BasePay.MODE_PROD : BasePay.MODE_TEST;
        MerConfig config = new MerConfig();
        config.setSysId(properties.getSystemId().trim());
        config.setProductId(properties.getProductId().trim());
        config.setRsaPrivateKey(properties.getPrivateKey().trim());
        config.setRsaPublicKey(properties.getPlatformPublicKey().trim());
        BasePay.initWithMerConfig(config);
    }
 
    private static String loginName(String applicationNo) { return ("tc" + applicationNo).replaceAll("[^0-9A-Za-z_]", ""); }
    private static void putIfPresent(Map<String, Object> request, String key, String value) { if (!blank(value)) request.put(key, value.trim()); }
    private static boolean blank(String value) { return value == null || value.isBlank(); }
    private boolean outcomeUnknown(Exception exception) { String message = exception.getMessage() == null ? "" : exception.getMessage().toLowerCase(); return message.contains("timeout") || message.contains("timed out") || message.contains("no response"); }
 
    @SuppressWarnings("unchecked")
    public record Result(boolean accepted, String huifuId, String applyNo, String tokenNo, String code, String message) {
        static Result from(Map<String, Object> response) {
            Object raw = response.get("data");
            Map<String, Object> data = raw instanceof Map<?, ?> value ? (Map<String, Object>) value : raw instanceof String json && !json.isBlank() ? JSON.parseObject(json, Map.class) : response;
            String code = value(data, "sub_resp_code", "resp_code", "code");
            return new Result(success(code), value(data, "huifu_id", "huifuId"), value(data, "apply_no", "applyNo"), value(data, "token_no", "tokenNo"), code, value(data, "sub_resp_desc", "resp_desc", "message"));
        }
        /**
         * Merchant onboarding return codes: {@code 00000000} means processed, {@code 90000000} means
         * the application was accepted and is under provider review. Treating the review status as a
         * failure would reject an application the provider actually accepted, so both are accepted
         * here; an accepted application without a merchant number or card token stays pending.
         */
        private static boolean success(String code) { return "00000000".equals(code) || "00000100".equals(code) || "90000000".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code); }
        private static String value(Map<String, Object> data, String... keys) { for (String key : keys) if (data.get(key) != null) return String.valueOf(data.get(key)).trim(); return ""; }
    }
}