yeg
2026-09-14 5364ba7487eb7abe0d4adc67f3393f511ed123a7
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
package com.talentcard.server.business.payment.huifu;
 
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
 
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.huifu.bspay.sdk.opps.core.request.V3TradePaymentJspayRequest;
import com.huifu.bspay.sdk.opps.core.request.V3TradePaymentScanpayQueryRequest;
import com.talentcard.server.common.BusinessException;
import com.talentcard.server.common.ErrorCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
 
@Service
public class HuifuMiniProgramPaymentGateway {
 
    private static final Object SDK_INIT_LOCK = new Object();
    private static final DateTimeFormatter REQUEST_DATE = DateTimeFormatter.ofPattern("yyyyMMdd");
    private static final Logger log = LoggerFactory.getLogger(HuifuMiniProgramPaymentGateway.class);
 
    private final HuifuPaymentProperties properties;
    private volatile boolean initialized;
 
    public HuifuMiniProgramPaymentGateway(HuifuPaymentProperties properties) {
        this.properties = properties;
    }
 
    public Map<String, Object> createMiniProgramPayment(HuifuMiniProgramPaymentCommand command) {
        ensureConfigured();
        ensureCollectingMerchant(command.huifuId());
        initializeSdk();
        V3TradePaymentJspayRequest request = new V3TradePaymentJspayRequest();
        request.setReqDate(REQUEST_DATE.format(LocalDateTime.now()));
        request.setReqSeqId(command.paymentNo());
        request.setHuifuId(command.huifuId().trim());
        request.setGoodsDesc(command.goodsDescription());
        request.setTransAmt(BigDecimal.valueOf(command.amountInCent(), 2).toPlainString());
        request.setTradeType("T_MINIAPP");
 
        Map<String, String> wxData = new LinkedHashMap<>();
        wxData.put("sub_openid", command.openId());
        wxData.put("sub_appid", properties.getMiniProgramAppId().trim());
        Map<String, Object> extendInfo = new LinkedHashMap<>();
        extendInfo.put("wx_data", JSON.toJSONString(wxData));
        extendInfo.put("notify_url", properties.getNotifyUrl().trim());
        // 是否延迟入账由订单类型决定,不再看全局配置:先付钱后核销的订单必须延迟,
        // 否则未核销的钱会直接落进商户账户,退款时收不回来。
        extendInfo.put("delay_acct_flag", command.delayAccount() ? "Y" : "N");
        extendInfo.put("time_expire", command.expiresAt().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));
        request.setExtendInfo(extendInfo);
        log.info("Huifu mini-program payment request: {}", logJson(Map.of(
                "req_date", REQUEST_DATE.format(LocalDateTime.now()),
                "req_seq_id", command.paymentNo(),
                "huifu_id", command.huifuId().trim(),
                "goods_desc", command.goodsDescription(),
                "trans_amt", BigDecimal.valueOf(command.amountInCent(), 2).toPlainString(),
                "trade_type", "T_MINIAPP",
                "system_id", properties.getSystemId().trim(),
                "product_id", properties.getProductId().trim(),
                "extend_info", extendInfo
        )));
        try {
            Map<String, Object> response = BasePayClient.request(request, false);
            log.info("Huifu mini-program payment response: {}", logJson(response));
            return response;
        } catch (Exception exception) {
            // 支付参数包含用户 OpenID 与签名材料,日志仅保留 SDK 错误类型及平台消息。
            log.warn("Huifu mini-program payment request failed: type={}, message={}",
                    exception.getClass().getSimpleName(), safeMessage(exception.getMessage()));
            throw new HuifuPaymentGatewayException(
                    "汇付支付创建失败,请稍后重试",
                    isOutcomeUnknown(exception),
                    exception
            );
        }
    }
 
    public Map<String, Object> queryMiniProgramPayment(HuifuPaymentQueryCommand command) {
        ensureConfigured();
        ensureCollectingMerchant(command.huifuId());
        initializeSdk();
        V3TradePaymentScanpayQueryRequest request = new V3TradePaymentScanpayQueryRequest();
        request.setHuifuId(command.huifuId().trim());
        request.setOrgReqDate(command.requestDate());
        request.setOrgReqSeqId(command.paymentNo());
        if (!blank(command.providerTransactionNo())) request.setOrgHfSeqId(command.providerTransactionNo());
        try {
            return BasePayClient.request(request, false);
        } catch (Exception exception) {
            log.warn("Huifu mini-program payment query failed: type={}, message={}",
                    exception.getClass().getSimpleName(), safeMessage(exception.getMessage()));
            throw new HuifuPaymentGatewayException("Huifu payment query failed", isOutcomeUnknown(exception), exception);
        }
    }
 
    private void ensureConfigured() {
        if (!properties.isEnabled()) throw new BusinessException(ErrorCode.CONFLICT, "汇付支付尚未启用");
        if (blank(properties.getSystemId()) || blank(properties.getProductId()) || blank(properties.getMerchantId())
                || blank(properties.getMiniProgramAppId()) || blank(properties.getPrivateKey()) || blank(properties.getPlatformPublicKey())
                || blank(properties.getNotifyUrl())) {
            throw new BusinessException(ErrorCode.CONFLICT, "汇付支付配置不完整");
        }
    }
 
    /** A payment must always be collected by the order's own sub-merchant, never by the platform. */
    private void ensureCollectingMerchant(String huifuId) {
        if (blank(huifuId)) throw new BusinessException(ErrorCode.CONFLICT, "订单缺少收款子商户,请重新下单");
    }
 
    private void initializeSdk() {
        if (initialized) return;
        synchronized (SDK_INIT_LOCK) {
            if (initialized) return;
            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());
            try {
                BasePay.initWithMerConfig(config);
                initialized = true;
            } catch (Exception exception) {
                throw new BusinessException(ErrorCode.CONFLICT, "汇付支付配置初始化失败");
            }
        }
    }
 
    private boolean blank(String value) {
        return value == null || value.isBlank();
    }
 
    private String safeMessage(String message) {
        if (message == null || message.isBlank()) return "";
        return message.length() > 500 ? message.substring(0, 500) : message;
    }
 
    private String logJson(Object value) {
        try {
            return JSON.toJSONString(sanitizeForLog(value));
        } catch (Exception ignored) {
            return "{\"log_error\":\"Unable to serialize payment payload\"}";
        }
    }
 
    private Object sanitizeForLog(Object value) {
        if (value instanceof Map<?, ?> source) {
            Map<String, Object> result = new LinkedHashMap<>();
            for (Map.Entry<?, ?> entry : source.entrySet()) {
                String key = String.valueOf(entry.getKey());
                result.put(key, isSensitiveKey(key) ? mask(String.valueOf(entry.getValue())) : sanitizeForLog(entry.getValue()));
            }
            return result;
        }
        if (value instanceof List<?> source) return source.stream().map(this::sanitizeForLog).toList();
        if (value instanceof String text && (text.startsWith("{") || text.startsWith("["))) {
            try {
                return sanitizeForLog(JSON.parse(text));
            } catch (Exception ignored) {
                return text;
            }
        }
        return value;
    }
 
    private boolean isSensitiveKey(String key) {
        String normalized = key == null ? "" : key.toLowerCase();
        return normalized.contains("private") || normalized.contains("secret") || normalized.contains("sign")
                || normalized.contains("openid") || normalized.contains("authorization") || normalized.contains("certificate");
    }
 
    private String mask(String value) {
        if (value == null || value.isBlank()) return "";
        if (value.length() <= 8) return "***";
        return value.substring(0, 4) + "***" + value.substring(value.length() - 4);
    }
 
    private boolean isOutcomeUnknown(Exception exception) {
        String message = safeMessage(exception.getMessage()).toLowerCase();
        return message.contains("no response") || message.contains("timeout") || message.contains("timed out");
    }
}