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;
|
}
|
}
|
}
|