|
@@ -0,0 +1,639 @@
|
|
|
|
|
+package shop.alien.store.strategy.payment.impl;
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+import com.google.gson.annotations.SerializedName;
|
|
|
|
|
+import com.wechat.pay.java.service.refund.model.QueryByOutRefundNoRequest;
|
|
|
|
|
+import com.wechat.pay.java.service.refund.model.Refund;
|
|
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import okhttp3.*;
|
|
|
|
|
+import org.slf4j.Logger;
|
|
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
+import org.springframework.cloud.context.config.annotation.RefreshScope;
|
|
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
|
|
+import shop.alien.entity.result.R;
|
|
|
|
|
+import shop.alien.entity.store.RefundRecord;
|
|
|
|
|
+import shop.alien.store.service.RefundRecordService;
|
|
|
|
|
+import shop.alien.store.strategy.payment.PaymentStrategy;
|
|
|
|
|
+import shop.alien.store.util.WXPayUtility;
|
|
|
|
|
+import shop.alien.util.common.UniqueRandomNumGenerator;
|
|
|
|
|
+import shop.alien.util.common.constant.PaymentEnum;
|
|
|
|
|
+import shop.alien.util.system.OSUtil;
|
|
|
|
|
+
|
|
|
|
|
+import javax.annotation.PostConstruct;
|
|
|
|
|
+import java.io.IOException;
|
|
|
|
|
+import java.io.UncheckedIOException;
|
|
|
|
|
+import java.math.BigDecimal;
|
|
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
|
|
+import java.security.PrivateKey;
|
|
|
|
|
+import java.security.PublicKey;
|
|
|
|
|
+import java.security.Signature;
|
|
|
|
|
+import java.util.*;
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 微信支付小程序策略
|
|
|
|
|
+ *
|
|
|
|
|
+ * @author lyx
|
|
|
|
|
+ * @date 2025/11/20
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Component
|
|
|
|
|
+@RequiredArgsConstructor
|
|
|
|
|
+@RefreshScope
|
|
|
|
|
+public class WeChatPaymentMininProgramStrategyImpl implements PaymentStrategy {
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 微信支付api主机地址
|
|
|
|
|
+ */
|
|
|
|
|
+ @Value("${payment.wechatPay.host}")
|
|
|
|
|
+ private String wechatPayApiHost;
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 微信支付商户id
|
|
|
|
|
+ */
|
|
|
|
|
+ @Value("${payment.wechatPay.business.mchId}")
|
|
|
|
|
+ private String mchId;
|
|
|
|
|
+ // TODO:小程序未注册-> 把下面的所有默认的去掉然后从配置文件中读取
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 微信支付小程序应用id
|
|
|
|
|
+ */
|
|
|
|
|
+ @Value("${payment.wechatPay.business.mininprogram.appId:没有}")
|
|
|
|
|
+ private String appId;
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 微信支付商户证书序列号
|
|
|
|
|
+ */
|
|
|
|
|
+ @Value("${payment.wechatPay.business.merchantSerialNumber}")
|
|
|
|
|
+ private String certificateSerialNo;
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 微信支付公钥id
|
|
|
|
|
+ */
|
|
|
|
|
+ @Value("${payment.wechatPay.business.wechatPayPublicKeyId}")
|
|
|
|
|
+ private String wechatPayPublicKeyId;
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 微信支付商户私钥路径
|
|
|
|
|
+ */
|
|
|
|
|
+ @Value("${payment.wechatPay.business.win.privateKeyPath}")
|
|
|
|
|
+ private String privateWinKeyPath;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 微信支付商户私钥路径(Linux环境)
|
|
|
|
|
+ */
|
|
|
|
|
+ @Value("${payment.wechatPay.business.linux.privateKeyPath}")
|
|
|
|
|
+ private String privateLinuxKeyPath;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 微信支付公钥路径
|
|
|
|
|
+ */
|
|
|
|
|
+ @Value("${payment.wechatPay.business.win.wechatPayPublicKeyFilePath}")
|
|
|
|
|
+ private String wechatWinPayPublicKeyFilePath;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 微信支付公钥路径(Linux环境)
|
|
|
|
|
+ */
|
|
|
|
|
+ @Value("${payment.wechatPay.business.linux.wechatPayPublicKeyFilePath}")
|
|
|
|
|
+ private String wechatLinuxPayPublicKeyFilePath;
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 微信支付预支付路径
|
|
|
|
|
+ */
|
|
|
|
|
+ @Value("${payment.wechatPay.miniProgram.prePayPath:/v3/pay/transactions/jsapi}")
|
|
|
|
|
+ private String prePayPath;
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 微信支付预支付通知路径
|
|
|
|
|
+ */
|
|
|
|
|
+ @Value("${payment.wechatPay.miniProgram.prePayNotifyUrl:https://www.weixin.qq.com/wxpay/pay.php}")
|
|
|
|
|
+ private String prePayNotifyUrl;
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 微信支付退款通知路径
|
|
|
|
|
+ */
|
|
|
|
|
+ @Value("${payment.wechatPay.business.miniProgram.refundNotifyUrl:https://www.weixin.qq.com/wxpay/pay.php}")
|
|
|
|
|
+ private String refundNotifyUrl;
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 微信支付查询退款状态路径
|
|
|
|
|
+ */
|
|
|
|
|
+ @Value("${payment.wechatPay.searchRefundStatusByOutRefundNoPath:/v3/refund/domestic/refunds/{out_refund_no}}")
|
|
|
|
|
+ private String searchRefundStatusByOutRefundNoPath;
|
|
|
|
|
+
|
|
|
|
|
+ private static String POSTMETHOD = "POST";
|
|
|
|
|
+ private static String GETMETHOD = "GET";
|
|
|
|
|
+
|
|
|
|
|
+ private PrivateKey privateKey;
|
|
|
|
|
+ private PublicKey wechatPayPublicKey;
|
|
|
|
|
+
|
|
|
|
|
+ private final WeChatPaymentStrategyImpl weChatPaymentStrategy;
|
|
|
|
|
+ private final RefundRecordService refundRecordService;
|
|
|
|
|
+
|
|
|
|
|
+ @PostConstruct
|
|
|
|
|
+ public void setWeChatPaymentConfig() {
|
|
|
|
|
+ String privateKeyPath;
|
|
|
|
|
+ String wechatPayPublicKeyFilePath;
|
|
|
|
|
+ if ("windows".equals(OSUtil.getOsName())) {
|
|
|
|
|
+ privateKeyPath = privateWinKeyPath;
|
|
|
|
|
+ wechatPayPublicKeyFilePath = wechatWinPayPublicKeyFilePath;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ privateKeyPath = privateLinuxKeyPath;
|
|
|
|
|
+ wechatPayPublicKeyFilePath = wechatLinuxPayPublicKeyFilePath;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyPath);
|
|
|
|
|
+ this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public R createPrePayOrder(String price, String subject) throws Exception {
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ DirectAPIv3JsapiPrepayRequest request = new DirectAPIv3JsapiPrepayRequest();
|
|
|
|
|
+ request.appid = appId;
|
|
|
|
|
+ request.mchid = mchId;
|
|
|
|
|
+ request.description = subject;
|
|
|
|
|
+ request.outTradeNo = UniqueRandomNumGenerator.generateUniqueCode(19);
|
|
|
|
|
+// request.timeExpire = "2018-06-08T10:34:56+08:00";
|
|
|
|
|
+// request.attach = "自定义数据说明";
|
|
|
|
|
+ request.notifyUrl = prePayNotifyUrl;
|
|
|
|
|
+ request.goodsTag = "WXG";
|
|
|
|
|
+ request.supportFapiao = false;
|
|
|
|
|
+ request.amount = new CommonAmountInfo();
|
|
|
|
|
+ request.amount.total = 100L;
|
|
|
|
|
+ request.amount.currency = "CNY";
|
|
|
|
|
+ request.payer = new JsapiReqPayerInfo();
|
|
|
|
|
+ request.payer.openid = "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o";
|
|
|
|
|
+ request.detail = new CouponInfo();
|
|
|
|
|
+ request.detail.costPrice = 608800L;
|
|
|
|
|
+ request.detail.invoiceId = "微信123";
|
|
|
|
|
+ request.detail.goodsDetail = new ArrayList<>();
|
|
|
|
|
+ {
|
|
|
|
|
+ GoodsDetail goodsDetailItem = new GoodsDetail();
|
|
|
|
|
+ goodsDetailItem.merchantGoodsId = "1246464644";
|
|
|
|
|
+ goodsDetailItem.wechatpayGoodsId = "1001";
|
|
|
|
|
+ goodsDetailItem.goodsName = "iPhoneX 256G";
|
|
|
|
|
+ goodsDetailItem.quantity = 1L;
|
|
|
|
|
+ goodsDetailItem.unitPrice = 528800L;
|
|
|
|
|
+ request.detail.goodsDetail.add(goodsDetailItem);
|
|
|
|
|
+ };
|
|
|
|
|
+ request.sceneInfo = new CommonSceneInfo();
|
|
|
|
|
+ request.sceneInfo.payerClientIp = "14.23.150.211";
|
|
|
|
|
+ request.sceneInfo.deviceId = "013467007045764";
|
|
|
|
|
+ request.sceneInfo.storeInfo = new StoreInfo();
|
|
|
|
|
+ request.sceneInfo.storeInfo.id = "0001";
|
|
|
|
|
+ request.sceneInfo.storeInfo.name = "腾讯大厦分店";
|
|
|
|
|
+ request.sceneInfo.storeInfo.areaCode = "440305";
|
|
|
|
|
+ request.sceneInfo.storeInfo.address = "广东省深圳市南山区科技中一道10000号";
|
|
|
|
|
+ request.settleInfo = new SettleInfo();
|
|
|
|
|
+ request.settleInfo.profitSharing = false;
|
|
|
|
|
+ try {
|
|
|
|
|
+ DirectAPIv3JsapiPrepayResponse response = doCreatePrePayOrder(request);
|
|
|
|
|
+ // TODO: 请求成功,继续业务逻辑
|
|
|
|
|
+ log.info("微信预支付订单创建成功,预支付ID:{}", response.prepayId);
|
|
|
|
|
+ Map<String,String> result = new HashMap<>();
|
|
|
|
|
+ result.put("prepayId", response.prepayId);
|
|
|
|
|
+ result.put("appId", appId);
|
|
|
|
|
+ result.put("mchId", mchId);
|
|
|
|
|
+ result.put("orderNo", request.outTradeNo);
|
|
|
|
|
+ long timestamp = System.currentTimeMillis() / 1000; // 时间戳
|
|
|
|
|
+ String nonce = WXPayUtility.createNonce(32); // 随机字符串
|
|
|
|
|
+ String prepayId = response.prepayId;
|
|
|
|
|
+ String message = String.format("%s\n%s\n%s\n%s\n", appId, timestamp, nonce, prepayId);
|
|
|
|
|
+// String sign = WXPayUtility.sign(message, sha256withRSA, privateKey);
|
|
|
|
|
+
|
|
|
|
|
+ Signature sign = Signature.getInstance("SHA256withRSA");
|
|
|
|
|
+ sign.initSign(privateKey);
|
|
|
|
|
+ sign.update(message.getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
+ result.put("signType","RSA");
|
|
|
|
|
+ result.put("sign", Base64.getEncoder().encodeToString(sign.sign()));
|
|
|
|
|
+ result.put("timestamp", String.valueOf(timestamp));
|
|
|
|
|
+ result.put("nonce", nonce);
|
|
|
|
|
+ return R.data(result);
|
|
|
|
|
+ } catch (WXPayUtility.ApiException e) {
|
|
|
|
|
+ log.error("微信支付预支付失败,状态码:{},错误信息:{}", e.getErrorCode(), e.getMessage());
|
|
|
|
|
+ return R.fail(e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public R handleNotify(String notifyData) throws Exception {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public R searchOrderByOutTradeNoPath(String transactionId) throws Exception {
|
|
|
|
|
+ return weChatPaymentStrategy.searchOrderByOutTradeNoPath(transactionId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String handleRefund(Map<String, String> params) throws Exception {
|
|
|
|
|
+
|
|
|
|
|
+ WeChatPaymentStrategyImpl.CreateRequest request = new WeChatPaymentStrategyImpl.CreateRequest();
|
|
|
|
|
+ // 微信支付订单号和商户订单号必须二选一,不能同时为空,查询的时候使用的是商户订单号
|
|
|
|
|
+ //request.transactionId = "1217752501201407033233368018";
|
|
|
|
|
+ request.outTradeNo = params.get("outTradeNo");
|
|
|
|
|
+ // 退款订单号
|
|
|
|
|
+ request.outRefundNo = UniqueRandomNumGenerator.generateUniqueCode(19);
|
|
|
|
|
+ // 退款原因
|
|
|
|
|
+ request.reason = params.get("reason");
|
|
|
|
|
+ // 退款回调通知地址
|
|
|
|
|
+ request.notifyUrl = refundNotifyUrl;
|
|
|
|
|
+ // 退款资金来源选填
|
|
|
|
|
+ //request.fundsAccount = ReqFundsAccount.AVAILABLE;
|
|
|
|
|
+ // 金额信息
|
|
|
|
|
+ request.amount = new WeChatPaymentStrategyImpl.AmountReq();
|
|
|
|
|
+ request.amount.refund = new BigDecimal(params.get("refundAmount")).longValue();
|
|
|
|
|
+ // 退款出资账户及金额 目前不需要,需要的时候再看
|
|
|
|
|
+ /*
|
|
|
|
|
+ request.amount.from = new ArrayList<>();
|
|
|
|
|
+ {
|
|
|
|
|
+ FundsFromItem fromItem = new FundsFromItem();
|
|
|
|
|
+ fromItem.account = Account.AVAILABLE;
|
|
|
|
|
+ fromItem.amount = 444L;
|
|
|
|
|
+ request.amount.from.add(fromItem);
|
|
|
|
|
+ };*/
|
|
|
|
|
+ // 订单总金额
|
|
|
|
|
+ request.amount.total = new BigDecimal(params.get("totalAmount")).longValue();
|
|
|
|
|
+ // 退款币种 目前默认人民币 CNY
|
|
|
|
|
+ request.amount.currency = "CNY";
|
|
|
|
|
+ // 退款商品信息 目前不需要,需要的时候再看
|
|
|
|
|
+ /*
|
|
|
|
|
+ request.goodsDetail = new ArrayList<>();
|
|
|
|
|
+ {
|
|
|
|
|
+ GoodsDetail goodsDetailItem = new GoodsDetail();
|
|
|
|
|
+ goodsDetailItem.merchantGoodsId = "1217752501201407033233368018";
|
|
|
|
|
+ goodsDetailItem.wechatpayGoodsId = "1001";
|
|
|
|
|
+ goodsDetailItem.goodsName = "iPhone6s 16G";
|
|
|
|
|
+ goodsDetailItem.unitPrice = 528800L;
|
|
|
|
|
+ goodsDetailItem.refundAmount = 528800L;
|
|
|
|
|
+ goodsDetailItem.refundQuantity = 1L;
|
|
|
|
|
+ request.goodsDetail.add(goodsDetailItem);
|
|
|
|
|
+ };*/
|
|
|
|
|
+ // 记录退款请求信息
|
|
|
|
|
+ log.info("开始处理微信支付退款,商户订单号:{},商户退款单号:{},退款金额:{}分,订单总金额:{}分,退款原因:{}",
|
|
|
|
|
+ request.outTradeNo, request.outRefundNo, request.amount.refund, request.amount.total, request.reason);
|
|
|
|
|
+ String refundResult = "";
|
|
|
|
|
+ try {
|
|
|
|
|
+ WeChatPaymentStrategyImpl.Refund response = weChatPaymentStrategy.refundRun(request);
|
|
|
|
|
+ // 退款状态
|
|
|
|
|
+ String status = response.status != null ? response.status.name() : "UNKNOWN";
|
|
|
|
|
+ if ("SUCCESS".equals(status) || "PROCESSING".equals(status)) {
|
|
|
|
|
+ // refund_id 申请退款受理成功时,该笔退款单在微信支付侧生成的唯一标识。
|
|
|
|
|
+ String refundId = response.refundId;
|
|
|
|
|
+ // 商户申请退款时传的商户系统内部退款单号。
|
|
|
|
|
+ String outRefundNo = response.outRefundNo != null ? response.outRefundNo : request.outRefundNo;
|
|
|
|
|
+ // 微信支付订单号
|
|
|
|
|
+ String transactionId = response.transactionId;
|
|
|
|
|
+
|
|
|
|
|
+ // 退款金额信息
|
|
|
|
|
+ String refundAmount = response.amount != null && response.amount.refund != null
|
|
|
|
|
+ ? String.valueOf(response.amount.refund) : String.valueOf(request.amount.refund);
|
|
|
|
|
+ String totalAmount = response.amount != null && response.amount.total != null
|
|
|
|
|
+ ? String.valueOf(response.amount.total) : String.valueOf(request.amount.total);
|
|
|
|
|
+ // 退款成功时间
|
|
|
|
|
+ String successTime = response.successTime;
|
|
|
|
|
+ // 退款创建时间
|
|
|
|
|
+ String createTime = response.createTime;
|
|
|
|
|
+ // 退款渠道
|
|
|
|
|
+ String channel = response.channel != null ? response.channel.name() : "UNKNOWN";
|
|
|
|
|
+
|
|
|
|
|
+ // 记录退款成功详细信息
|
|
|
|
|
+ log.info("微信支付退款成功 - 商户订单号:{},微信支付订单号:{},商户退款单号:{},微信退款单号:{}," +
|
|
|
|
|
+ "退款状态:{},退款金额:{}分,订单总金额:{}分,退款渠道:{},创建时间:{},成功时间:{}",
|
|
|
|
|
+ request.outTradeNo, transactionId, outRefundNo, refundId, status, refundAmount,
|
|
|
|
|
+ totalAmount, channel, createTime, successTime != null ? successTime : "未完成");
|
|
|
|
|
+
|
|
|
|
|
+ // 保存到通用退款记录表
|
|
|
|
|
+ try {
|
|
|
|
|
+ RefundRecord refundRecord = weChatPaymentStrategy.buildRefundRecordFromWeChatResponse(response, request, params);
|
|
|
|
|
+ refundRecord.setPayType(PaymentEnum.WECHAT_PAY_MININ_PROGRAM.getType());
|
|
|
|
|
+ if (refundRecord != null) {
|
|
|
|
|
+ // 检查是否已存在,避免重复插入
|
|
|
|
|
+ long count = refundRecordService.lambdaQuery()
|
|
|
|
|
+ .eq(RefundRecord::getOutRefundNo, refundRecord.getOutRefundNo())
|
|
|
|
|
+ .count();
|
|
|
|
|
+ if (count == 0) {
|
|
|
|
|
+ refundRecordService.save(refundRecord);
|
|
|
|
|
+ log.info("微信支付退款记录已保存到RefundRecord表,商户退款单号:{}", refundRecord.getOutRefundNo());
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log.info("微信支付退款记录已存在,跳过插入,商户退款单号:{}", refundRecord.getOutRefundNo());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("保存微信支付退款记录到RefundRecord表失败", e);
|
|
|
|
|
+ // 不抛出异常,避免影响原有逻辑
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ refundResult = "调用成功";
|
|
|
|
|
+ return refundResult;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log.error("微信支付退款失败 - 商户订单号:{},商户退款单号:{},退款金额:{}分,订单总金额:{}分," +
|
|
|
|
|
+ "退款状态:{}",
|
|
|
|
|
+ request.outTradeNo, request.outRefundNo, request.amount.refund,
|
|
|
|
|
+ request.amount.total, status);
|
|
|
|
|
+
|
|
|
|
|
+ // 保存失败记录到通用退款记录表
|
|
|
|
|
+ try {
|
|
|
|
|
+ RefundRecord refundRecord = weChatPaymentStrategy.buildRefundRecordFromWeChatError(response, request, params, status);
|
|
|
|
|
+ refundRecord.setPayType(PaymentEnum.WECHAT_PAY_MININ_PROGRAM.getType());
|
|
|
|
|
+ if (refundRecord != null) {
|
|
|
|
|
+ // 检查是否已存在,避免重复插入
|
|
|
|
|
+ long count = refundRecordService.lambdaQuery()
|
|
|
|
|
+ .eq(RefundRecord::getOutRefundNo, refundRecord.getOutRefundNo())
|
|
|
|
|
+ .count();
|
|
|
|
|
+ if (count == 0) {
|
|
|
|
|
+ refundRecordService.save(refundRecord);
|
|
|
|
|
+ log.info("微信支付退款失败记录已保存到RefundRecord表,商户退款单号:{}", refundRecord.getOutRefundNo());
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log.info("微信支付退款失败记录已存在,跳过插入,商户退款单号:{}", refundRecord.getOutRefundNo());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("保存微信支付退款失败记录到RefundRecord表失败", e);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return "退款失败";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (Exception e) {
|
|
|
|
|
+ // 记录其他异常
|
|
|
|
|
+ log.error("微信支付退款异常 - 商户订单号:{},商户退款单号:{},退款金额:{}分,订单总金额:{}分," +
|
|
|
|
|
+ "退款原因:{},异常信息:{}",
|
|
|
|
|
+ request.outTradeNo, request.outRefundNo, request.amount.refund,
|
|
|
|
|
+ request.amount.total, request.reason, e.getMessage(), e);
|
|
|
|
|
+
|
|
|
|
|
+ // 保存异常记录到通用退款记录表
|
|
|
|
|
+ try {
|
|
|
|
|
+ RefundRecord refundRecord = weChatPaymentStrategy.buildRefundRecordFromWeChatException(request, params, e);
|
|
|
|
|
+ refundRecord.setPayType(PaymentEnum.WECHAT_PAY_MININ_PROGRAM.getType());
|
|
|
|
|
+ if (refundRecord != null) {
|
|
|
|
|
+ // 检查是否已存在,避免重复插入
|
|
|
|
|
+ long count = refundRecordService.lambdaQuery()
|
|
|
|
|
+ .eq(RefundRecord::getOutRefundNo, refundRecord.getOutRefundNo())
|
|
|
|
|
+ .count();
|
|
|
|
|
+ if (count == 0) {
|
|
|
|
|
+ refundRecordService.save(refundRecord);
|
|
|
|
|
+ log.info("微信支付退款异常记录已保存到RefundRecord表,商户退款单号:{}", refundRecord.getOutRefundNo());
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log.info("微信支付退款异常记录已存在,跳过插入,商户退款单号:{}", refundRecord.getOutRefundNo());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception ex) {
|
|
|
|
|
+ log.error("保存微信支付退款异常记录到RefundRecord表失败", ex);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ refundResult = "退款处理异常:" + e.getMessage();
|
|
|
|
|
+ return refundResult;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public R searchRefundRecordByOutRefundNo(String outRefundNo) throws Exception {
|
|
|
|
|
+ // 1. 初始化日志(推荐使用SLF4J,替代System.out.println)
|
|
|
|
|
+ Logger logger = LoggerFactory.getLogger(this.getClass());
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 参数校验(前置防御,避免无效请求)
|
|
|
|
|
+ if (outRefundNo == null || outRefundNo.trim().isEmpty()) {
|
|
|
|
|
+ logger.error("微信退款查询失败:外部退款单号为空");
|
|
|
|
|
+ return R.fail("外部退款单号不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest();
|
|
|
|
|
+ request.setOutRefundNo(outRefundNo);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ Refund response = doSearchRefundRecordByOutRefundNo(request);
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 空值校验(避免response为空导致空指针)
|
|
|
|
|
+ if (response == null) {
|
|
|
|
|
+ logger.error("微信退款查询失败:外部退款单号{},返回结果为空", outRefundNo);
|
|
|
|
|
+ return R.fail("微信支付查询退款记录失败:返回结果为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ logger.info("微信退款查询结果:外部退款单号{},退款状态{}",
|
|
|
|
|
+ outRefundNo, response.getStatus());
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 细化退款状态判断(覆盖微信支付核心退款状态)
|
|
|
|
|
+ String refundStatus = String.valueOf(response.getStatus());
|
|
|
|
|
+ switch (refundStatus) {
|
|
|
|
|
+ case "SUCCESS":
|
|
|
|
|
+ // 退款成功:执行成功业务逻辑(如更新订单状态、通知用户等)
|
|
|
|
|
+ logger.info("退款成功:外部退款单号{}", outRefundNo);
|
|
|
|
|
+ // TODO: 补充你的成功业务逻辑(例:updateOrderRefundStatus(outRefundNo, "SUCCESS");)
|
|
|
|
|
+ return R.data(response);
|
|
|
|
|
+
|
|
|
|
|
+ case "REFUNDCLOSE":
|
|
|
|
|
+ // 退款关闭:执行关闭逻辑(如记录关闭原因、人工介入等)
|
|
|
|
|
+ logger.warn("退款关闭:外部退款单号{},原因{}", outRefundNo);
|
|
|
|
|
+ return R.fail("微信支付退款已关闭");
|
|
|
|
|
+
|
|
|
|
|
+ case "PROCESSING":
|
|
|
|
|
+ // 退款处理中:执行等待逻辑(如提示用户等待、定时任务重试等)
|
|
|
|
|
+ logger.info("退款处理中:外部退款单号{}", outRefundNo);
|
|
|
|
|
+ return R.fail("微信支付退款处理中,请稍后再查");
|
|
|
|
|
+
|
|
|
|
|
+ case "CHANGE":
|
|
|
|
|
+ // 退款异常:执行异常处理(如记录异常、人工核查等)
|
|
|
|
|
+ logger.error("退款异常:外部退款单号{}", outRefundNo);
|
|
|
|
|
+ return R.fail("微信支付退款异常");
|
|
|
|
|
+
|
|
|
|
|
+ default:
|
|
|
|
|
+ // 未知状态:兜底处理
|
|
|
|
|
+ logger.error("退款状态未知:外部退款单号{},状态码{}", outRefundNo, refundStatus);
|
|
|
|
|
+ return R.fail("微信支付查询退款记录失败:未知状态码" + refundStatus);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ } catch (WXPayUtility.ApiException e) {
|
|
|
|
|
+ // 5. 异常处理:细化异常日志,便于排查
|
|
|
|
|
+ logger.error("微信退款查询API异常:外部退款单号{},错误码{},错误信息{}",
|
|
|
|
|
+ outRefundNo, e.getErrorCode(), e.getMessage(), e);
|
|
|
|
|
+ return R.fail("微信支付查询退款记录失败:" + e.getMessage() + "(错误码:" + e.getErrorCode() + ")");
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ // 6. 兜底异常:捕获非API异常(如空指针、网络异常等)
|
|
|
|
|
+ logger.error("微信退款查询系统异常:外部退款单号{}", outRefundNo, e);
|
|
|
|
|
+ return R.fail("微信支付查询退款记录失败:系统异常,请联系管理员");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String getType() {
|
|
|
|
|
+ return PaymentEnum.WECHAT_PAY_MININ_PROGRAM.getType();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Refund doSearchRefundRecordByOutRefundNo(QueryByOutRefundNoRequest request) {
|
|
|
|
|
+ String uri = searchRefundStatusByOutRefundNoPath;
|
|
|
|
|
+ uri = uri.replace("{out_refund_no}", WXPayUtility.urlEncode(request.getOutRefundNo()));
|
|
|
|
|
+ Request.Builder reqBuilder = new Request.Builder().url(wechatPayApiHost + uri);
|
|
|
|
|
+ reqBuilder.addHeader("Accept", "application/json");
|
|
|
|
|
+ reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
|
|
|
|
+ reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchId, certificateSerialNo, privateKey, GETMETHOD, uri, null));
|
|
|
|
|
+ reqBuilder.method(GETMETHOD, null);
|
|
|
|
|
+ Request httpRequest = reqBuilder.build();
|
|
|
|
|
+
|
|
|
|
|
+ // 发送HTTP请求
|
|
|
|
|
+ OkHttpClient client = new OkHttpClient.Builder().build();
|
|
|
|
|
+ try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
|
|
|
|
+ String respBody = WXPayUtility.extractBody(httpResponse);
|
|
|
|
|
+ if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
|
|
|
|
+ // 2XX 成功,验证应答签名
|
|
|
|
|
+ WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
|
|
|
|
+ httpResponse.headers(), respBody);
|
|
|
|
|
+
|
|
|
|
|
+ // 从HTTP应答报文构建返回数据
|
|
|
|
|
+ return WXPayUtility.fromJson(respBody, Refund.class);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
|
+ throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public DirectAPIv3JsapiPrepayResponse doCreatePrePayOrder(DirectAPIv3JsapiPrepayRequest request) {
|
|
|
|
|
+ String uri = prePayPath;
|
|
|
|
|
+ String reqBody = WXPayUtility.toJson(request);
|
|
|
|
|
+
|
|
|
|
|
+ Request.Builder reqBuilder = new Request.Builder().url(wechatPayApiHost + uri);
|
|
|
|
|
+ reqBuilder.addHeader("Accept", "application/json");
|
|
|
|
|
+ reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
|
|
|
|
+ reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchId, certificateSerialNo,privateKey, POSTMETHOD, uri, reqBody));
|
|
|
|
|
+ reqBuilder.addHeader("Content-Type", "application/json");
|
|
|
|
|
+ RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
|
|
|
|
|
+ reqBuilder.method(POSTMETHOD, requestBody);
|
|
|
|
|
+ Request httpRequest = reqBuilder.build();
|
|
|
|
|
+
|
|
|
|
|
+ // 发送HTTP请求
|
|
|
|
|
+ OkHttpClient client = new OkHttpClient.Builder().build();
|
|
|
|
|
+ try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
|
|
|
|
+ String respBody = WXPayUtility.extractBody(httpResponse);
|
|
|
|
|
+ if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
|
|
|
|
+ // 2XX 成功,验证应答签名
|
|
|
|
|
+ WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
|
|
|
|
+ httpResponse.headers(), respBody);
|
|
|
|
|
+
|
|
|
|
|
+ // 从HTTP应答报文构建返回数据
|
|
|
|
|
+ return WXPayUtility.fromJson(respBody, DirectAPIv3JsapiPrepayResponse.class);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
|
+ throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static class DirectAPIv3JsapiPrepayRequest {
|
|
|
|
|
+ @SerializedName("appid")
|
|
|
|
|
+ public String appid;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("mchid")
|
|
|
|
|
+ public String mchid;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("description")
|
|
|
|
|
+ public String description;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("out_trade_no")
|
|
|
|
|
+ public String outTradeNo;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("time_expire")
|
|
|
|
|
+ public String timeExpire;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("attach")
|
|
|
|
|
+ public String attach;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("notify_url")
|
|
|
|
|
+ public String notifyUrl;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("goods_tag")
|
|
|
|
|
+ public String goodsTag;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("support_fapiao")
|
|
|
|
|
+ public Boolean supportFapiao;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("amount")
|
|
|
|
|
+ public CommonAmountInfo amount;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("payer")
|
|
|
|
|
+ public JsapiReqPayerInfo payer;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("detail")
|
|
|
|
|
+ public CouponInfo detail;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("scene_info")
|
|
|
|
|
+ public CommonSceneInfo sceneInfo;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("settle_info")
|
|
|
|
|
+ public SettleInfo settleInfo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static class DirectAPIv3JsapiPrepayResponse {
|
|
|
|
|
+ @SerializedName("prepay_id")
|
|
|
|
|
+ public String prepayId;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static class CommonAmountInfo {
|
|
|
|
|
+ @SerializedName("total")
|
|
|
|
|
+ public Long total;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("currency")
|
|
|
|
|
+ public String currency;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static class JsapiReqPayerInfo {
|
|
|
|
|
+ @SerializedName("openid")
|
|
|
|
|
+ public String openid;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static class CouponInfo {
|
|
|
|
|
+ @SerializedName("cost_price")
|
|
|
|
|
+ public Long costPrice;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("invoice_id")
|
|
|
|
|
+ public String invoiceId;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("goods_detail")
|
|
|
|
|
+ public List<GoodsDetail> goodsDetail;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static class CommonSceneInfo {
|
|
|
|
|
+ @SerializedName("payer_client_ip")
|
|
|
|
|
+ public String payerClientIp;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("device_id")
|
|
|
|
|
+ public String deviceId;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("store_info")
|
|
|
|
|
+ public StoreInfo storeInfo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static class SettleInfo {
|
|
|
|
|
+ @SerializedName("profit_sharing")
|
|
|
|
|
+ public Boolean profitSharing;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static class GoodsDetail {
|
|
|
|
|
+ @SerializedName("merchant_goods_id")
|
|
|
|
|
+ public String merchantGoodsId;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("wechatpay_goods_id")
|
|
|
|
|
+ public String wechatpayGoodsId;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("goods_name")
|
|
|
|
|
+ public String goodsName;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("quantity")
|
|
|
|
|
+ public Long quantity;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("unit_price")
|
|
|
|
|
+ public Long unitPrice;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static class StoreInfo {
|
|
|
|
|
+ @SerializedName("id")
|
|
|
|
|
+ public String id;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("name")
|
|
|
|
|
+ public String name;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("area_code")
|
|
|
|
|
+ public String areaCode;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("address")
|
|
|
|
|
+ public String address;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+}
|
|
|
|
|
+
|