liujie
2026-08-04 e9e42b621afdf6ac94f5327ee843b32b10a401b4
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
192
193
194
195
196
197
198
199
200
201
202
203
204
package com.tourguide.common.huifu.biz;
 
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.huifu.bspay.sdk.opps.core.net.BasePayRequest;
import com.tourguide.common.huifu.properties.HuiFuProperties;
import com.tourguide.config.properties.WechatMiniProgramProperties;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
 
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Duration;
import java.util.Base64;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
 
/**
 * 斗拱支付工具。
 *
 * 备注:这里只负责创建支付参数和调用斗拱,不能直接把本地订单改为已支付;订单状态必须以后端验签回调为准。
 */
@Component
public class DouGongPayUtil {
    private static final String PAYMENT_CREATE_URL = "https://api.huifu.com/v4/trade/payment/create";
    private static final String PAYMENT_SUCCESS_CODE = "00000100";
    private static final Duration WX_PAY_CACHE_TTL = Duration.ofMinutes(5);
 
    private final HuiFuProperties huiFuProperties;
    private final WechatMiniProgramProperties wechatMiniProgramProperties;
    private final StringRedisTemplate redisTemplate;
    private final RestTemplate restTemplate;
 
    public DouGongPayUtil(HuiFuProperties huiFuProperties,
                          WechatMiniProgramProperties wechatMiniProgramProperties,
                          StringRedisTemplate redisTemplate,
                          @Qualifier("huifuRestTemplate") RestTemplate restTemplate) {
        this.huiFuProperties = huiFuProperties;
        this.wechatMiniProgramProperties = wechatMiniProgramProperties;
        this.redisTemplate = redisTemplate;
        this.restTemplate = restTemplate;
    }
 
    public JSONObject wxPayOrder(BigDecimal amount, String openId, String goodsDesc,
                                  String reqSeqId, String reqDate) {
        validateConfiguration();
        String cacheKey = "dg_wx_pay:" + reqSeqId;
        String cached = redisTemplate.opsForValue().get(cacheKey);
        if (StringUtils.hasText(cached)) return JSONObject.parseObject(cached);
        JSONObject response = createPayment(amount.toString(), goodsDesc, 0, 0,
                "T_MINIAPP", methodExpand(openId), false, reqSeqId, reqDate);
        if (response != null && PAYMENT_SUCCESS_CODE.equals(response.getString("resp_code"))) {
            redisTemplate.opsForValue().set(cacheKey, response.toJSONString(), WX_PAY_CACHE_TTL);
        }
        return response;
    }
 
    public JSONObject aliPayOrder(String money, String buyerId, String goodsDesc, int type, String orderId, int callType) {
        return createPayment(money, goodsDesc, type, callType, "A_NATIVE", null, true,
                orderId + "_" + type + "_" + System.currentTimeMillis() + DouGongRequestSupport.randomNumber(10),
                DouGongRequestSupport.todayForHuiFu());
    }
 
    public Map<String, Object> payFromUser(String reqSeqId, String reqDate, String ordAmt, String receiverHuifuId) throws Exception {
        DouGongRequestSupport.initBasePay(huiFuProperties);
        BigDecimal amount = new BigDecimal(ordAmt).setScale(2, RoundingMode.HALF_UP);
 
        Map<String, Object> paramsInfo = new HashMap<>();
        paramsInfo.put("req_seq_id", reqSeqId);
        paramsInfo.put("req_date", reqDate);
        paramsInfo.put("out_huifu_id", huiFuProperties.getHuifuId());
        paramsInfo.put("ord_amt", amount.toString());
        paramsInfo.put("acct_split_bunch", acctSplitBunch(amount, receiverHuifuId));
        paramsInfo.put("risk_check_data", riskCheckData());
        paramsInfo.put("trans_fee_take_flag", "OUT");
        return BasePayRequest.requestBasePay("v2/trade/acctpayment/pay", paramsInfo, null, false);
    }
 
    private JSONObject createPayment(String money, String goodsDesc, int type, int callType,
                                     String tradeType, String methodExpand, boolean alipay,
                                     String reqSeqId, String reqDate) {
        BigDecimal amount = new BigDecimal(money).setScale(2, RoundingMode.HALF_UP);
 
        TreeMap<String, Object> data = new TreeMap<>();
        data.put("goods_desc", goodsDesc);
        data.put("huifu_id", huiFuProperties.getHuifuId());
        data.put("notify_url", callbackBaseUrl() + notifyPath(callType, alipay));
        data.put("remark", "TourGuide");
        data.put("req_date", reqDate);
        data.put("req_seq_id", reqSeqId);
        data.put("trade_type", tradeType);
        data.put("trans_amt", amount.toString());
        if (methodExpand != null) {
            data.put("method_expand", methodExpand);
        }
 
        // 备注:斗拱要求按业务 data 生成 RSA 签名,签名失败直接中断,避免发出不可追踪的支付请求。
        String sign = sign(JSON.toJSONString(data), huiFuProperties.getRsaPrivateKey());
        if (sign == null) {
            throw new IllegalStateException("DouGong payment sign failed");
        }
 
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("sys_id", huiFuProperties.getSysId());
        payload.put("product_id", huiFuProperties.getProductId());
        payload.put("sign", sign);
        payload.put("data", data);
 
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        String paymentCreateUrl = StringUtils.hasText(huiFuProperties.getPaymentCreateUrl())
                ? huiFuProperties.getPaymentCreateUrl() : PAYMENT_CREATE_URL;
        ResponseEntity<String> response = restTemplate.postForEntity(paymentCreateUrl,
                new HttpEntity<>(JSON.toJSONString(payload), headers), String.class);
        JSONObject result = JSONObject.parseObject(response.getBody());
        result.put("req_seq_id", reqSeqId);
        return result;
    }
 
    private void validateConfiguration() {
        requireConfiguration(huiFuProperties.getProductId(), "HUIFU_PRODUCT_ID");
        requireConfiguration(huiFuProperties.getSysId(), "HUIFU_SYS_ID");
        requireConfiguration(huiFuProperties.getRsaPrivateKey(), "HUIFU_RSA_PRIVATE_KEY");
        requireConfiguration(huiFuProperties.getRsaPublicKey(), "HUIFU_RSA_PUBLIC_KEY");
        requireConfiguration(huiFuProperties.getHuifuId(), "HUIFU_ID");
        requireConfiguration(huiFuProperties.getCallbackPath(), "HUIFU_CALLBACK_PATH");
        requireConfiguration(wechatMiniProgramProperties.getAppId(), "WECHAT_MINI_APP_ID");
    }
 
    private void requireConfiguration(String value, String configurationName) {
        if (!StringUtils.hasText(value)) {
            throw new IllegalStateException("汇付支付配置缺少 " + configurationName);
        }
    }
 
    private String callbackBaseUrl() {
        String callbackPath = huiFuProperties.getCallbackPath().trim();
        while (callbackPath.endsWith("/")) callbackPath = callbackPath.substring(0, callbackPath.length() - 1);
        return callbackPath;
    }
 
    private String methodExpand(String openId) {
        JSONObject methodExpand = new JSONObject();
        methodExpand.put("sub_appid", wechatMiniProgramProperties.getAppId());
        methodExpand.put("sub_openid", openId);
        return methodExpand.toJSONString();
    }
 
    private String acctSplitBunch(BigDecimal amount, String huifuId) {
        JSONObject acctInfo = new JSONObject();
        acctInfo.put("div_amt", amount.toString());
        acctInfo.put("huifu_id", huifuId);
 
        JSONArray acctInfos = new JSONArray();
        acctInfos.add(acctInfo);
 
        JSONObject root = new JSONObject();
        root.put("acct_infos", acctInfos);
        return root.toJSONString();
    }
 
    private String riskCheckData() {
        JSONObject data = new JSONObject();
        data.put("sub_product", "1");
        data.put("transfer_type", "05");
        return data.toJSONString();
    }
 
    private String notifyPath(int callType, boolean alipay) {
        String channel = alipay ? "ALIPAY" : "WECHAT";
        // 备注:统一回调入口便于后续按 channel/callType 分发,安全配置已放行 /base/dougong/**。
        return "/base/dougong/pay-notify?channel=" + channel + "&callType=" + callType;
    }
 
    public static String sign(String data, String privateKeyBase64) {
        try {
            byte[] bytes = Base64.getDecoder().decode(privateKeyBase64);
            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
            Signature signature = Signature.getInstance("SHA256WithRSA");
            signature.initSign(privateKey);
            signature.update(data.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(signature.sign());
        } catch (Exception e) {
            return null;
        }
    }
}