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 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 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 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 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 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/** 及 /api/base/dougong/** 均免 Token。 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; } } }