|
|
@@ -0,0 +1,1238 @@
|
|
|
+package shop.alien.lawyer.payment.impl;
|
|
|
+
|
|
|
+import com.google.gson.annotations.Expose;
|
|
|
+import com.google.gson.annotations.SerializedName;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import okhttp3.*;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+import shop.alien.entity.result.R;
|
|
|
+import shop.alien.entity.store.RefundRecord;
|
|
|
+
|
|
|
+import shop.alien.lawyer.payment.PaymentStrategy;
|
|
|
+import shop.alien.lawyer.service.RefundRecordService;
|
|
|
+import shop.alien.lawyer.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.text.ParseException;
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
+import java.time.OffsetDateTime;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.*;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 微信支付策略
|
|
|
+ *
|
|
|
+ * @author lyx
|
|
|
+ * @date 2025/11/20
|
|
|
+ */
|
|
|
+
|
|
|
+@Slf4j
|
|
|
+@Component
|
|
|
+@RequiredArgsConstructor
|
|
|
+public class WeChatPaymentStrategyImpl implements PaymentStrategy {
|
|
|
+
|
|
|
+ private final RefundRecordService refundRecordService;
|
|
|
+
|
|
|
+ @Value("${payment.wechatPay.host}")
|
|
|
+ private String wechatPayApiHost;
|
|
|
+
|
|
|
+ @Value("${payment.wechatPay.prePayPath}")
|
|
|
+ private String prePayPath;
|
|
|
+
|
|
|
+ @Value("${payment.wechatPay.searchOrderByTransactionIdPath}")
|
|
|
+ private String searchOrderByTransactionIdPath;
|
|
|
+
|
|
|
+ @Value("${payment.wechatPay.searchOrderByOutTradeNoPath}")
|
|
|
+ private String searchOrderByOutTradeNoPath;
|
|
|
+
|
|
|
+ @Value("${payment.wechatPay.refundPath}")
|
|
|
+ private String refundPath;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 微信支付应用id
|
|
|
+ */
|
|
|
+ @Value("${payment.wechatPay.business.appId}")
|
|
|
+ private String appId;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 微信支付商户id
|
|
|
+ */
|
|
|
+ @Value("${payment.wechatPay.business.mchId}")
|
|
|
+ private String mchId;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 微信支付商户私钥路径
|
|
|
+ */
|
|
|
+ @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.business.merchantSerialNumber}")
|
|
|
+ private String merchantSerialNumber;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 微信支付API V3密钥
|
|
|
+ */
|
|
|
+ @Value("${payment.wechatPay.business.apiV3key}")
|
|
|
+ private String apiV3key;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 微信支付公钥id
|
|
|
+ */
|
|
|
+ @Value("${payment.wechatPay.business.wechatPayPublicKeyId}")
|
|
|
+ private String wechatPayPublicKeyId;
|
|
|
+
|
|
|
+ @Value("${payment.wechatPay.business.prePayNotifyUrl}")
|
|
|
+ private String prePayNotifyUrl;
|
|
|
+
|
|
|
+ @Value("${payment.wechatPay.business.refundNotifyUrl}")
|
|
|
+ private String refundNotifyUrl;
|
|
|
+
|
|
|
+ private PrivateKey privateKey;
|
|
|
+ private PublicKey wechatPayPublicKey;
|
|
|
+
|
|
|
+ private static String POSTMETHOD = "POST";
|
|
|
+ private static String GETMETHOD = "GET";
|
|
|
+
|
|
|
+ @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 {
|
|
|
+ log.info("创建微信预支付订单,价格:{},描述:{}", price, subject);
|
|
|
+ // 参数验证
|
|
|
+ if (price == null || price.trim().isEmpty()) {
|
|
|
+ return R.fail("价格不能为空");
|
|
|
+ }
|
|
|
+ if (subject == null || subject.trim().isEmpty()) {
|
|
|
+ return R.fail("订单描述不能为空");
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ BigDecimal amount = new BigDecimal(price);
|
|
|
+ if (amount.compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
+ return R.fail("价格必须大于0");
|
|
|
+ }
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ return R.fail("价格格式不正确");
|
|
|
+ }
|
|
|
+
|
|
|
+ CommonPrepayRequest request = new CommonPrepayRequest();
|
|
|
+ request.appid = appId;
|
|
|
+ request.mchid = mchId;
|
|
|
+ request.description = subject;
|
|
|
+ request.outTradeNo = UniqueRandomNumGenerator.generateUniqueCode(19);
|
|
|
+
|
|
|
+// request.timeExpire = "2018-06-08T10:34:56+08:00"; 超时支付不传默认7天
|
|
|
+// request.attach = "自定义数据说明";
|
|
|
+ // 目前没用,但是必填
|
|
|
+ request.notifyUrl = prePayNotifyUrl;
|
|
|
+// request.goodsTag = "WXG";
|
|
|
+// request.supportFapiao = false;
|
|
|
+ request.amount = new CommonAmountInfo();
|
|
|
+ request.amount.total = new BigDecimal(price).multiply(new BigDecimal(100)).longValue();
|
|
|
+ request.amount.currency = "CNY";
|
|
|
+ /* 【优惠功能】 优惠功能
|
|
|
+ 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 {
|
|
|
+ DirectAPIv3AppPrepayResponse response = this.prePayOrderRun(request);
|
|
|
+ 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);
|
|
|
+ // 生成sign
|
|
|
+// appId
|
|
|
+
|
|
|
+// 随机字符串
|
|
|
+// prepay_id
|
|
|
+ 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("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 {
|
|
|
+ log.info("查询微信支付订单状态,交易订单号:{}", transactionId);
|
|
|
+ QueryByWxTradeNoRequest request = new QueryByWxTradeNoRequest();
|
|
|
+ request.transactionId = transactionId;
|
|
|
+ request.mchid = mchId;
|
|
|
+ try {
|
|
|
+ DirectAPIv3QueryResponse response = searchOrderRun(request);
|
|
|
+ return R.data(response);
|
|
|
+ } catch (WXPayUtility.ApiException e) {
|
|
|
+ log.error("查询微信支付订单状态失败,状态码:{},错误信息:{}", e.getErrorCode(), e.getMessage());
|
|
|
+ return R.fail(e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String handleRefund(Map<String,String> params) throws Exception {
|
|
|
+
|
|
|
+ CreateRequest request = new 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 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 {
|
|
|
+ Refund response = 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 = buildRefundRecordFromWeChatResponse(response, request, params);
|
|
|
+ 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 = buildRefundRecordFromWeChatError(response, request, params, status);
|
|
|
+ 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 = buildRefundRecordFromWeChatException(request, params, e);
|
|
|
+ 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 String getType() {
|
|
|
+ return PaymentEnum.WECHAT_PAY.getType();
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ public DirectAPIv3AppPrepayResponse prePayOrderRun(CommonPrepayRequest 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, merchantSerialNumber, 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(wechatPayPublicKeyId, wechatPayPublicKey,
|
|
|
+ httpResponse.headers(), respBody);
|
|
|
+
|
|
|
+ // 从HTTP应答报文构建返回数据
|
|
|
+ return WXPayUtility.fromJson(respBody, DirectAPIv3AppPrepayResponse.class);
|
|
|
+ } else {
|
|
|
+ throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
|
|
+ }
|
|
|
+ } catch (IOException e) {
|
|
|
+ throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public DirectAPIv3QueryResponse searchOrderRun(QueryByWxTradeNoRequest request) {
|
|
|
+ String uri = searchOrderByOutTradeNoPath;
|
|
|
+ uri = uri.replace("{out_trade_no}", WXPayUtility.urlEncode(request.transactionId));
|
|
|
+ Map<String, Object> args = new HashMap<>();
|
|
|
+ args.put("mchid", mchId);
|
|
|
+ String queryString = WXPayUtility.urlEncode(args);
|
|
|
+ if (!queryString.isEmpty()) {
|
|
|
+ uri = uri + "?" + queryString;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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, merchantSerialNumber, 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, DirectAPIv3QueryResponse.class);
|
|
|
+ } else {
|
|
|
+ throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
|
|
+ }
|
|
|
+ } catch (IOException e) {
|
|
|
+ throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public Refund refundRun(CreateRequest request) {
|
|
|
+ String uri = refundPath;
|
|
|
+ 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, merchantSerialNumber, 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, 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 static class CommonPrepayRequest {
|
|
|
+ @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("detail")
|
|
|
+ public CouponInfo detail;
|
|
|
+
|
|
|
+ @SerializedName("scene_info")
|
|
|
+ public CommonSceneInfo sceneInfo;
|
|
|
+
|
|
|
+ @SerializedName("settle_info")
|
|
|
+ public SettleInfo settleInfo;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class DirectAPIv3AppPrepayResponse {
|
|
|
+ @SerializedName("prepay_id")
|
|
|
+ public String prepayId;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class CommonAmountInfo {
|
|
|
+ @SerializedName("total")
|
|
|
+ public Long total;
|
|
|
+
|
|
|
+ @SerializedName("currency")
|
|
|
+ public String currency;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 StoreInfo {
|
|
|
+ @SerializedName("id")
|
|
|
+ public String id;
|
|
|
+
|
|
|
+ @SerializedName("name")
|
|
|
+ public String name;
|
|
|
+
|
|
|
+ @SerializedName("area_code")
|
|
|
+ public String areaCode;
|
|
|
+
|
|
|
+ @SerializedName("address")
|
|
|
+ public String address;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class QueryByWxTradeNoRequest {
|
|
|
+ @SerializedName("mchid")
|
|
|
+ @Expose(serialize = false)
|
|
|
+ public String mchid;
|
|
|
+
|
|
|
+ @SerializedName("transaction_id")
|
|
|
+ @Expose(serialize = false)
|
|
|
+ public String transactionId;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class DirectAPIv3QueryResponse {
|
|
|
+ @SerializedName("appid")
|
|
|
+ public String appid;
|
|
|
+
|
|
|
+ @SerializedName("mchid")
|
|
|
+ public String mchid;
|
|
|
+
|
|
|
+ @SerializedName("out_trade_no")
|
|
|
+ public String outTradeNo;
|
|
|
+
|
|
|
+ @SerializedName("transaction_id")
|
|
|
+ public String transactionId;
|
|
|
+
|
|
|
+ @SerializedName("trade_type")
|
|
|
+ public String tradeType;
|
|
|
+
|
|
|
+ @SerializedName("trade_state")
|
|
|
+ public String tradeState;
|
|
|
+
|
|
|
+ @SerializedName("trade_state_desc")
|
|
|
+ public String tradeStateDesc;
|
|
|
+
|
|
|
+ @SerializedName("bank_type")
|
|
|
+ public String bankType;
|
|
|
+
|
|
|
+ @SerializedName("attach")
|
|
|
+ public String attach;
|
|
|
+
|
|
|
+ @SerializedName("success_time")
|
|
|
+ public String successTime;
|
|
|
+
|
|
|
+ @SerializedName("payer")
|
|
|
+ public CommRespPayerInfo payer;
|
|
|
+
|
|
|
+ @SerializedName("amount")
|
|
|
+ public CommRespAmountInfo amount;
|
|
|
+
|
|
|
+ @SerializedName("scene_info")
|
|
|
+ public CommRespSceneInfo sceneInfo;
|
|
|
+
|
|
|
+ @SerializedName("promotion_detail")
|
|
|
+ public List<PromotionDetail> promotionDetail;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class CommRespPayerInfo {
|
|
|
+ @SerializedName("openid")
|
|
|
+ public String openid;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class CommRespAmountInfo {
|
|
|
+ @SerializedName("total")
|
|
|
+ public Long total;
|
|
|
+
|
|
|
+ @SerializedName("payer_total")
|
|
|
+ public Long payerTotal;
|
|
|
+
|
|
|
+ @SerializedName("currency")
|
|
|
+ public String currency;
|
|
|
+
|
|
|
+ @SerializedName("payer_currency")
|
|
|
+ public String payerCurrency;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class CommRespSceneInfo {
|
|
|
+ @SerializedName("device_id")
|
|
|
+ public String deviceId;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class PromotionDetail {
|
|
|
+ @SerializedName("coupon_id")
|
|
|
+ public String couponId;
|
|
|
+
|
|
|
+ @SerializedName("name")
|
|
|
+ public String name;
|
|
|
+
|
|
|
+ @SerializedName("scope")
|
|
|
+ public String scope;
|
|
|
+
|
|
|
+ @SerializedName("type")
|
|
|
+ public String type;
|
|
|
+
|
|
|
+ @SerializedName("amount")
|
|
|
+ public Long amount;
|
|
|
+
|
|
|
+ @SerializedName("stock_id")
|
|
|
+ public String stockId;
|
|
|
+
|
|
|
+ @SerializedName("wechatpay_contribute")
|
|
|
+ public Long wechatpayContribute;
|
|
|
+
|
|
|
+ @SerializedName("merchant_contribute")
|
|
|
+ public Long merchantContribute;
|
|
|
+
|
|
|
+ @SerializedName("other_contribute")
|
|
|
+ public Long otherContribute;
|
|
|
+
|
|
|
+ @SerializedName("currency")
|
|
|
+ public String currency;
|
|
|
+
|
|
|
+ @SerializedName("goods_detail")
|
|
|
+ public List<GoodsDetailInPromotion> goodsDetail;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class GoodsDetailInPromotion {
|
|
|
+ @SerializedName("goods_id")
|
|
|
+ public String goodsId;
|
|
|
+
|
|
|
+ @SerializedName("quantity")
|
|
|
+ public Long quantity;
|
|
|
+
|
|
|
+ @SerializedName("unit_price")
|
|
|
+ public Long unitPrice;
|
|
|
+
|
|
|
+ @SerializedName("discount_amount")
|
|
|
+ public Long discountAmount;
|
|
|
+
|
|
|
+ @SerializedName("goods_remark")
|
|
|
+ public String goodsRemark;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class CreateRequest {
|
|
|
+ @SerializedName("transaction_id")
|
|
|
+ public String transactionId;
|
|
|
+
|
|
|
+ @SerializedName("out_trade_no")
|
|
|
+ public String outTradeNo;
|
|
|
+
|
|
|
+ @SerializedName("out_refund_no")
|
|
|
+ public String outRefundNo;
|
|
|
+
|
|
|
+ @SerializedName("reason")
|
|
|
+ public String reason;
|
|
|
+
|
|
|
+ @SerializedName("notify_url")
|
|
|
+ public String notifyUrl;
|
|
|
+
|
|
|
+ @SerializedName("funds_account")
|
|
|
+ public ReqFundsAccount fundsAccount;
|
|
|
+
|
|
|
+ @SerializedName("amount")
|
|
|
+ public AmountReq amount;
|
|
|
+
|
|
|
+ @SerializedName("goods_detail")
|
|
|
+ public List<GoodsDetail> goodsDetail;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class Refund {
|
|
|
+ @SerializedName("refund_id")
|
|
|
+ public String refundId;
|
|
|
+
|
|
|
+ @SerializedName("out_refund_no")
|
|
|
+ public String outRefundNo;
|
|
|
+
|
|
|
+ @SerializedName("transaction_id")
|
|
|
+ public String transactionId;
|
|
|
+
|
|
|
+ @SerializedName("out_trade_no")
|
|
|
+ public String outTradeNo;
|
|
|
+
|
|
|
+ @SerializedName("channel")
|
|
|
+ public Channel channel;
|
|
|
+
|
|
|
+ @SerializedName("user_received_account")
|
|
|
+ public String userReceivedAccount;
|
|
|
+
|
|
|
+ @SerializedName("success_time")
|
|
|
+ public String successTime;
|
|
|
+
|
|
|
+ @SerializedName("create_time")
|
|
|
+ public String createTime;
|
|
|
+
|
|
|
+ @SerializedName("status")
|
|
|
+ public Status status;
|
|
|
+
|
|
|
+ @SerializedName("funds_account")
|
|
|
+ public FundsAccount fundsAccount;
|
|
|
+
|
|
|
+ @SerializedName("amount")
|
|
|
+ public Amount amount;
|
|
|
+
|
|
|
+ @SerializedName("promotion_detail")
|
|
|
+ public List<Promotion> promotionDetail;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ public enum ReqFundsAccount {
|
|
|
+ @SerializedName("AVAILABLE")
|
|
|
+ AVAILABLE,
|
|
|
+ @SerializedName("UNSETTLED")
|
|
|
+ UNSETTLED
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class AmountReq {
|
|
|
+ @SerializedName("refund")
|
|
|
+ public Long refund;
|
|
|
+
|
|
|
+ @SerializedName("from")
|
|
|
+ public List<FundsFromItem> from;
|
|
|
+
|
|
|
+ @SerializedName("total")
|
|
|
+ public Long total;
|
|
|
+
|
|
|
+ @SerializedName("currency")
|
|
|
+ public String currency;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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;
|
|
|
+
|
|
|
+ @SerializedName("refund_amount")
|
|
|
+ public Long refundAmount;
|
|
|
+
|
|
|
+ @SerializedName("refund_quantity")
|
|
|
+ public Long refundQuantity;
|
|
|
+ }
|
|
|
+
|
|
|
+ public enum Channel {
|
|
|
+ @SerializedName("ORIGINAL")
|
|
|
+ ORIGINAL,
|
|
|
+ @SerializedName("BALANCE")
|
|
|
+ BALANCE,
|
|
|
+ @SerializedName("OTHER_BALANCE")
|
|
|
+ OTHER_BALANCE,
|
|
|
+ @SerializedName("OTHER_BANKCARD")
|
|
|
+ OTHER_BANKCARD
|
|
|
+ }
|
|
|
+
|
|
|
+ public enum Status {
|
|
|
+ @SerializedName("SUCCESS")
|
|
|
+ SUCCESS,
|
|
|
+ @SerializedName("CLOSED")
|
|
|
+ CLOSED,
|
|
|
+ @SerializedName("PROCESSING")
|
|
|
+ PROCESSING,
|
|
|
+ @SerializedName("ABNORMAL")
|
|
|
+ ABNORMAL
|
|
|
+ }
|
|
|
+
|
|
|
+ public enum FundsAccount {
|
|
|
+ @SerializedName("UNSETTLED")
|
|
|
+ UNSETTLED,
|
|
|
+ @SerializedName("AVAILABLE")
|
|
|
+ AVAILABLE,
|
|
|
+ @SerializedName("UNAVAILABLE")
|
|
|
+ UNAVAILABLE,
|
|
|
+ @SerializedName("OPERATION")
|
|
|
+ OPERATION,
|
|
|
+ @SerializedName("BASIC")
|
|
|
+ BASIC,
|
|
|
+ @SerializedName("ECNY_BASIC")
|
|
|
+ ECNY_BASIC
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class Amount {
|
|
|
+ @SerializedName("total")
|
|
|
+ public Long total;
|
|
|
+
|
|
|
+ @SerializedName("refund")
|
|
|
+ public Long refund;
|
|
|
+
|
|
|
+ @SerializedName("from")
|
|
|
+ public List<FundsFromItem> from;
|
|
|
+
|
|
|
+ @SerializedName("payer_total")
|
|
|
+ public Long payerTotal;
|
|
|
+
|
|
|
+ @SerializedName("payer_refund")
|
|
|
+ public Long payerRefund;
|
|
|
+
|
|
|
+ @SerializedName("settlement_refund")
|
|
|
+ public Long settlementRefund;
|
|
|
+
|
|
|
+ @SerializedName("settlement_total")
|
|
|
+ public Long settlementTotal;
|
|
|
+
|
|
|
+ @SerializedName("discount_refund")
|
|
|
+ public Long discountRefund;
|
|
|
+
|
|
|
+ @SerializedName("currency")
|
|
|
+ public String currency;
|
|
|
+
|
|
|
+ @SerializedName("refund_fee")
|
|
|
+ public Long refundFee;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class Promotion {
|
|
|
+ @SerializedName("promotion_id")
|
|
|
+ public String promotionId;
|
|
|
+
|
|
|
+ @SerializedName("scope")
|
|
|
+ public PromotionScope scope;
|
|
|
+
|
|
|
+ @SerializedName("type")
|
|
|
+ public PromotionType type;
|
|
|
+
|
|
|
+ @SerializedName("amount")
|
|
|
+ public Long amount;
|
|
|
+
|
|
|
+ @SerializedName("refund_amount")
|
|
|
+ public Long refundAmount;
|
|
|
+
|
|
|
+ @SerializedName("goods_detail")
|
|
|
+ public List<GoodsDetail> goodsDetail;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class FundsFromItem {
|
|
|
+ @SerializedName("account")
|
|
|
+ public Account account;
|
|
|
+
|
|
|
+ @SerializedName("amount")
|
|
|
+ public Long amount;
|
|
|
+ }
|
|
|
+
|
|
|
+ public enum PromotionScope {
|
|
|
+ @SerializedName("GLOBAL")
|
|
|
+ GLOBAL,
|
|
|
+ @SerializedName("SINGLE")
|
|
|
+ SINGLE
|
|
|
+ }
|
|
|
+
|
|
|
+ public enum PromotionType {
|
|
|
+ @SerializedName("CASH")
|
|
|
+ CASH,
|
|
|
+ @SerializedName("NOCASH")
|
|
|
+ NOCASH
|
|
|
+ }
|
|
|
+
|
|
|
+ public enum Account {
|
|
|
+ @SerializedName("AVAILABLE")
|
|
|
+ AVAILABLE,
|
|
|
+ @SerializedName("UNAVAILABLE")
|
|
|
+ UNAVAILABLE
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从微信支付退款响应构建RefundRecord对象(成功情况)
|
|
|
+ */
|
|
|
+ private RefundRecord buildRefundRecordFromWeChatResponse(Refund response, CreateRequest request, Map<String, String> params) {
|
|
|
+ try {
|
|
|
+ RefundRecord record = new RefundRecord();
|
|
|
+
|
|
|
+ // 基本信息
|
|
|
+ record.setPayType(PaymentEnum.WECHAT_PAY.getType());
|
|
|
+ record.setOutTradeNo(response.outTradeNo != null ? response.outTradeNo : request.outTradeNo);
|
|
|
+ record.setTransactionId(response.transactionId);
|
|
|
+ record.setOutRefundNo(response.outRefundNo != null ? response.outRefundNo : request.outRefundNo);
|
|
|
+ record.setRefundId(response.refundId);
|
|
|
+ record.setRefundStatus(response.status != null ? response.status.name() : "UNKNOWN");
|
|
|
+
|
|
|
+ // 金额信息(微信返回的是分)
|
|
|
+ if (response.amount != null) {
|
|
|
+ record.setTotalAmount(response.amount.total);
|
|
|
+ record.setRefundAmount(response.amount.refund);
|
|
|
+ record.setActualRefundAmount(response.amount.refund); // 微信退款金额就是实际退款金额
|
|
|
+ if (response.amount.currency != null) {
|
|
|
+ record.setCurrency(response.amount.currency);
|
|
|
+ } else {
|
|
|
+ record.setCurrency("CNY");
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ record.setTotalAmount(request.amount.total);
|
|
|
+ record.setRefundAmount(request.amount.refund);
|
|
|
+ record.setActualRefundAmount(request.amount.refund);
|
|
|
+ record.setCurrency(request.amount.currency != null ? request.amount.currency : "CNY");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 退款原因
|
|
|
+ record.setRefundReason(request.reason);
|
|
|
+
|
|
|
+ // 退款渠道
|
|
|
+ if (response.channel != null) {
|
|
|
+ record.setRefundChannel(response.channel.name());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 退款资金来源
|
|
|
+ if (response.fundsAccount != null) {
|
|
|
+ record.setFundsAccount(response.fundsAccount.name());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 用户收到退款账户
|
|
|
+ record.setUserReceivedAccount(response.userReceivedAccount);
|
|
|
+
|
|
|
+ // 退款详情(如果有优惠信息)
|
|
|
+ if (response.promotionDetail != null && !response.promotionDetail.isEmpty()) {
|
|
|
+ record.setRefundDetail(WXPayUtility.toJson(response.promotionDetail));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 退款时间(微信返回的是ISO 8601格式字符串,需要转换为Date)
|
|
|
+ if (response.createTime != null) {
|
|
|
+ Date createTime = parseWeChatTime(response.createTime);
|
|
|
+ if (createTime != null) {
|
|
|
+ record.setRefundCreateTime(createTime);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (response.successTime != null) {
|
|
|
+ Date successTime = parseWeChatTime(response.successTime);
|
|
|
+ if (successTime != null) {
|
|
|
+ record.setRefundSuccessTime(successTime);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 业务信息(从params获取,如果有的话)
|
|
|
+ if (params != null) {
|
|
|
+ if (params.containsKey("userId")) {
|
|
|
+ try {
|
|
|
+ record.setUserId(Integer.parseInt(params.get("userId")));
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ log.warn("解析userId失败: {}", params.get("userId"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (params.containsKey("orderId")) {
|
|
|
+ record.setOrderId(params.get("orderId"));
|
|
|
+ }
|
|
|
+ if (params.containsKey("storeId")) {
|
|
|
+ try {
|
|
|
+ record.setStoreId(Integer.parseInt(params.get("storeId")));
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ log.warn("解析storeId失败: {}", params.get("storeId"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 响应数据
|
|
|
+ record.setResponseData(WXPayUtility.toJson(response));
|
|
|
+
|
|
|
+ // 标准字段
|
|
|
+ record.setDeleteFlag(0);
|
|
|
+ record.setCreatedTime(new Date());
|
|
|
+
|
|
|
+ return record;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("构建RefundRecord对象失败", e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从微信支付退款错误响应构建RefundRecord对象(失败情况)
|
|
|
+ */
|
|
|
+ private RefundRecord buildRefundRecordFromWeChatError(Refund response, CreateRequest request, Map<String, String> params, String status) {
|
|
|
+ try {
|
|
|
+ RefundRecord record = new RefundRecord();
|
|
|
+
|
|
|
+ // 基本信息
|
|
|
+ record.setPayType(PaymentEnum.WECHAT_PAY.getType());
|
|
|
+ record.setOutTradeNo(response.outTradeNo != null ? response.outTradeNo : request.outTradeNo);
|
|
|
+ record.setTransactionId(response.transactionId);
|
|
|
+ record.setOutRefundNo(response.outRefundNo != null ? response.outRefundNo : request.outRefundNo);
|
|
|
+ record.setRefundId(response.refundId);
|
|
|
+ record.setRefundStatus(status);
|
|
|
+
|
|
|
+ // 金额信息
|
|
|
+ if (response.amount != null) {
|
|
|
+ record.setTotalAmount(response.amount.total);
|
|
|
+ record.setRefundAmount(response.amount.refund);
|
|
|
+ record.setCurrency(response.amount.currency != null ? response.amount.currency : "CNY");
|
|
|
+ } else {
|
|
|
+ record.setTotalAmount(request.amount.total);
|
|
|
+ record.setRefundAmount(request.amount.refund);
|
|
|
+ record.setCurrency(request.amount.currency != null ? request.amount.currency : "CNY");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 退款原因
|
|
|
+ record.setRefundReason(request.reason);
|
|
|
+
|
|
|
+ // 退款时间
|
|
|
+ if (response.createTime != null) {
|
|
|
+ Date createTime = parseWeChatTime(response.createTime);
|
|
|
+ if (createTime != null) {
|
|
|
+ record.setRefundCreateTime(createTime);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 业务信息
|
|
|
+ if (params != null) {
|
|
|
+ if (params.containsKey("userId")) {
|
|
|
+ try {
|
|
|
+ record.setUserId(Integer.parseInt(params.get("userId")));
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ log.warn("解析userId失败: {}", params.get("userId"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (params.containsKey("orderId")) {
|
|
|
+ record.setOrderId(params.get("orderId"));
|
|
|
+ }
|
|
|
+ if (params.containsKey("storeId")) {
|
|
|
+ try {
|
|
|
+ record.setStoreId(Integer.parseInt(params.get("storeId")));
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ log.warn("解析storeId失败: {}", params.get("storeId"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 响应数据
|
|
|
+ record.setResponseData(WXPayUtility.toJson(response));
|
|
|
+
|
|
|
+ // 标准字段
|
|
|
+ record.setDeleteFlag(0);
|
|
|
+ record.setCreatedTime(new Date());
|
|
|
+
|
|
|
+ return record;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("构建RefundRecord对象失败", e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从微信支付退款异常构建RefundRecord对象(异常情况)
|
|
|
+ */
|
|
|
+ private RefundRecord buildRefundRecordFromWeChatException(CreateRequest request, Map<String, String> params, Exception e) {
|
|
|
+ try {
|
|
|
+ RefundRecord record = new RefundRecord();
|
|
|
+
|
|
|
+ // 基本信息
|
|
|
+ record.setPayType(PaymentEnum.WECHAT_PAY.getType());
|
|
|
+ record.setOutTradeNo(request.outTradeNo);
|
|
|
+ record.setOutRefundNo(request.outRefundNo);
|
|
|
+ record.setRefundStatus("ABNORMAL");
|
|
|
+
|
|
|
+ // 金额信息
|
|
|
+ record.setTotalAmount(request.amount.total);
|
|
|
+ record.setRefundAmount(request.amount.refund);
|
|
|
+ record.setCurrency(request.amount.currency != null ? request.amount.currency : "CNY");
|
|
|
+
|
|
|
+ // 退款原因
|
|
|
+ record.setRefundReason(request.reason);
|
|
|
+
|
|
|
+ // 错误信息
|
|
|
+ if (e instanceof WXPayUtility.ApiException) {
|
|
|
+ WXPayUtility.ApiException apiException = (WXPayUtility.ApiException) e;
|
|
|
+ record.setErrorCode(String.valueOf(apiException.getErrorCode()));
|
|
|
+ record.setErrorMsg(apiException.getMessage());
|
|
|
+ } else {
|
|
|
+ record.setErrorMsg(e.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 业务信息
|
|
|
+ if (params != null) {
|
|
|
+ if (params.containsKey("userId")) {
|
|
|
+ try {
|
|
|
+ record.setUserId(Integer.parseInt(params.get("userId")));
|
|
|
+ } catch (NumberFormatException ex) {
|
|
|
+ log.warn("解析userId失败: {}", params.get("userId"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (params.containsKey("orderId")) {
|
|
|
+ record.setOrderId(params.get("orderId"));
|
|
|
+ }
|
|
|
+ if (params.containsKey("storeId")) {
|
|
|
+ try {
|
|
|
+ record.setStoreId(Integer.parseInt(params.get("storeId")));
|
|
|
+ } catch (NumberFormatException ex) {
|
|
|
+ log.warn("解析storeId失败: {}", params.get("storeId"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 标准字段
|
|
|
+ record.setDeleteFlag(0);
|
|
|
+ record.setCreatedTime(new Date());
|
|
|
+ record.setRefundCreateTime(new Date());
|
|
|
+
|
|
|
+ return record;
|
|
|
+ } catch (Exception ex) {
|
|
|
+ log.error("构建RefundRecord对象失败", ex);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析微信支付返回的时间字符串(ISO 8601格式)
|
|
|
+ * 例如:2018-06-08T10:34:56+08:00
|
|
|
+ */
|
|
|
+ private Date parseWeChatTime(String timeStr) {
|
|
|
+ if (timeStr == null || timeStr.trim().isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ // 尝试解析ISO 8601格式
|
|
|
+ OffsetDateTime dateTime = OffsetDateTime.parse(timeStr, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
|
|
|
+ return Date.from(dateTime.toInstant());
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 如果ISO 8601解析失败,尝试其他格式
|
|
|
+ try {
|
|
|
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
|
|
+ return sdf.parse(timeStr);
|
|
|
+ } catch (ParseException ex) {
|
|
|
+ log.warn("解析微信支付时间失败: {}", timeStr, ex);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|