Selaa lähdekoodia

加入重构的微信app支付

zhangchen 1 kuukausi sitten
vanhempi
commit
5e40e8d83e

+ 425 - 0
alien-store/src/main/java/shop/alien/store/strategy/merchantPayment/impl/MerchantWechatPaymentStrategyImpl.java

@@ -0,0 +1,425 @@
+package shop.alien.store.strategy.merchantPayment.impl;
+
+import com.alibaba.fastjson.JSON;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Component;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.MerchantPaymentOrder;
+import shop.alien.entity.store.RefundRecord;
+import shop.alien.entity.store.StorePaymentConfig;
+import shop.alien.entity.store.UserReservationOrder;
+import shop.alien.store.service.MerchantPaymentOrderService;
+import shop.alien.store.service.RefundRecordService;
+import shop.alien.store.service.StorePaymentConfigService;
+import shop.alien.store.service.UserReservationOrderService;
+import shop.alien.store.strategy.merchantPayment.MerchantPaymentStrategy;
+import shop.alien.store.strategy.payment.impl.WeChatPaymentStrategyImpl;
+import shop.alien.store.util.WXPayUtility;
+import shop.alien.util.common.UniqueRandomNumGenerator;
+import shop.alien.util.common.constant.PaymentEnum;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 商户微信支付策略(预订订单,使用 StorePaymentConfig 按门店配置,业务逻辑与支付宝一致)
+ *
+ * @author system
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrategy {
+
+    private static final String POSTMETHOD = "POST";
+    private static final String GETMETHOD = "GET";
+    private static final String REDIS_PREPAY_KEY_PREFIX = "merchant:wechat:prepay:order:";
+    private static final long REDIS_PREPAY_EXPIRE_SECONDS = 15 * 60;
+
+    @Value("${payment.wechatPay.host:https://api.mch.weixin.qq.com}")
+    private String wechatPayApiHost;
+    @Value("${payment.wechatPay.prePayPath:/v3/pay/transactions/app}")
+    private String prePayPath;
+    @Value("${payment.wechatPay.searchOrderByOutTradeNoPath:/v3/pay/transactions/out-trade-no/{out_trade_no}}")
+    private String searchOrderByOutTradeNoPath;
+    @Value("${payment.wechatPay.refundPath:/v3/refund/domestic/refunds}")
+    private String refundPath;
+    @Value("${payment.wechatPay.business.prePayNotifyUrl:}")
+    private String prePayNotifyUrl;
+    @Value("${payment.wechatPay.business.refundNotifyUrl:}")
+    private String refundNotifyUrl;
+
+    private final StorePaymentConfigService storePaymentConfigService;
+    private final UserReservationOrderService userReservationOrderService;
+    private final MerchantPaymentOrderService merchantPaymentOrderService;
+    private final RefundRecordService refundRecordService;
+    private final StringRedisTemplate stringRedisTemplate;
+
+    @Override
+    public R<Map<String, Object>> createPrePay(Integer storeId, Integer orderId, String amountYuan, String subject, Integer userId) {
+        if (storeId == null || orderId == null) {
+            return R.fail("门店ID和订单ID不能为空");
+        }
+        if (StringUtils.isBlank(amountYuan) || new BigDecimal(amountYuan).compareTo(BigDecimal.ZERO) <= 0) {
+            return R.fail("支付金额必须大于0");
+        }
+        if (StringUtils.isBlank(subject)) {
+            return R.fail("订单描述不能为空");
+        }
+        StorePaymentConfig config = storePaymentConfigService.getByStoreId(storeId);
+        if (config == null) {
+            return R.fail("该门店未配置支付参数");
+        }
+        if (StringUtils.isBlank(config.getWechatAppId()) || StringUtils.isBlank(config.getWechatMchId())
+                || StringUtils.isBlank(config.getMerchantSerialNumber()) || StringUtils.isBlank(config.getApiV3Key())
+                || StringUtils.isBlank(config.getWechatPayPublicKeyId()) || StringUtils.isBlank(config.getWechatPrivateKeyFile())
+                || StringUtils.isBlank(config.getWechatPayPublicKeyFile())) {
+            return R.fail("门店微信支付配置不完整");
+        }
+        UserReservationOrder order = userReservationOrderService.getById(orderId);
+        if (order == null) {
+            return R.fail("预订订单不存在");
+        }
+        if (!order.getStoreId().equals(storeId)) {
+            return R.fail("订单与门店不匹配");
+        }
+        if (order.getPaymentStatus() != null && order.getPaymentStatus() == 1) {
+            return R.fail("订单已支付");
+        }
+
+        String redisKey = REDIS_PREPAY_KEY_PREFIX + orderId;
+        String cached = stringRedisTemplate.opsForValue().get(redisKey);
+        if (StringUtils.isNotBlank(cached)) {
+            try {
+                @SuppressWarnings("unchecked")
+                Map<String, Object> data = JSON.parseObject(cached, Map.class);
+                if (data != null && !data.isEmpty()) {
+                    log.info("商户预订订单微信预支付命中缓存,orderId={}", orderId);
+                    return R.data(data);
+                }
+            } catch (Exception e) {
+                log.warn("解析微信预支付缓存失败,将重新发起,orderId={}", orderId, e);
+            }
+        }
+
+        int deleted = merchantPaymentOrderService.logicDeleteByOrderId(orderId);
+        if (deleted > 0) {
+            log.info("未命中缓存,已逻辑删除该订单下 {} 条支付单,orderId={}", deleted, orderId);
+        }
+
+        String outTradeNo = UniqueRandomNumGenerator.generateUniqueCode(19);
+        BigDecimal amount = new BigDecimal(amountYuan);
+
+        MerchantPaymentOrder paymentOrder = new MerchantPaymentOrder();
+        paymentOrder.setPaymentNo(merchantPaymentOrderService.generatePaymentNo());
+        paymentOrder.setOrderType("reservation_order");
+        paymentOrder.setOrderId(order.getId());
+        paymentOrder.setOrderSn(order.getOrderSn());
+        paymentOrder.setStoreId(storeId);
+        paymentOrder.setPayType(PaymentEnum.WECHAT_PAY.getType());
+        paymentOrder.setOutTradeNo(outTradeNo);
+        paymentOrder.setPayAmount(amount);
+        paymentOrder.setPayStatus(0);
+        paymentOrder.setPayerUserId(userId);
+        paymentOrder.setSubject(subject);
+        paymentOrder.setCreatedTime(new Date());
+        paymentOrder.setUpdatedTime(new Date());
+        merchantPaymentOrderService.save(paymentOrder);
+
+        try {
+            PrivateKey privateKey = WXPayUtility.loadPrivateKeyFromString(config.getWechatPrivateKeyFile());
+
+            WeChatPaymentStrategyImpl.CommonPrepayRequest request = new WeChatPaymentStrategyImpl.CommonPrepayRequest();
+            request.appid = config.getWechatAppId();
+            request.mchid = config.getWechatMchId();
+            request.description = subject;
+            request.outTradeNo = outTradeNo;
+            request.notifyUrl = StringUtils.isNotBlank(prePayNotifyUrl) ? prePayNotifyUrl : "";
+            request.amount = new WeChatPaymentStrategyImpl.CommonAmountInfo();
+            request.amount.total = amount.multiply(new BigDecimal(100)).longValue();
+            request.amount.currency = "CNY";
+
+            WeChatPaymentStrategyImpl.DirectAPIv3AppPrepayResponse response = prePayOrderRun(config, privateKey, request);
+            if (response == null || StringUtils.isBlank(response.prepayId)) {
+                return R.fail("微信预支付失败");
+            }
+
+            long timestamp = System.currentTimeMillis() / 1000;
+            String nonce = WXPayUtility.createNonce(32);
+            String message = String.format("%s\n%s\n%s\n%s\n", config.getWechatAppId(), timestamp, nonce, response.prepayId);
+            Signature sign = Signature.getInstance("SHA256withRSA");
+            sign.initSign(privateKey);
+            sign.update(message.getBytes(StandardCharsets.UTF_8));
+            String signStr = Base64.getEncoder().encodeToString(sign.sign());
+
+            Map<String, Object> data = new HashMap<>();
+            data.put("outTradeNo", outTradeNo);
+            data.put("orderSn", order.getOrderSn());
+            data.put("orderId", order.getId());
+            data.put("paymentNo", paymentOrder.getPaymentNo());
+            data.put("prepayId", response.prepayId);
+            data.put("appId", config.getWechatAppId());
+            data.put("mchId", config.getWechatMchId());
+            data.put("sign", signStr);
+            data.put("timestamp", String.valueOf(timestamp));
+            data.put("nonce", nonce);
+            data.put("orderStr", "");
+
+            String cacheJson = JSON.toJSONString(data);
+            stringRedisTemplate.opsForValue().set(redisKey, cacheJson != null ? cacheJson : "{}", REDIS_PREPAY_EXPIRE_SECONDS, TimeUnit.SECONDS);
+            log.info("商户预订订单微信预支付成功并写入缓存,storeId={}, orderSn={}, outTradeNo={}", storeId, order.getOrderSn(), outTradeNo);
+            return R.data(data);
+        } catch (WXPayUtility.ApiException e) {
+            log.error("商户预订订单微信预支付异常,storeId={}, orderId={}", storeId, orderId, e);
+            return R.fail("预支付失败:" + e.getMessage());
+        } catch (Exception e) {
+            log.error("商户微信预支付异常,storeId={}", storeId, e);
+            return R.fail("预支付失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<Object> queryPayStatus(Integer storeId, String outTradeNo) {
+        if (storeId == null || StringUtils.isBlank(outTradeNo)) {
+            return R.fail("门店ID和商户订单号不能为空");
+        }
+        StorePaymentConfig config = storePaymentConfigService.getByStoreId(storeId);
+        if (config == null) {
+            return R.fail("该门店未配置支付参数");
+        }
+        MerchantPaymentOrder paymentOrder = merchantPaymentOrderService.getByOutTradeNo(outTradeNo);
+        if (paymentOrder == null) {
+            return R.fail("支付单不存在");
+        }
+        UserReservationOrder order = userReservationOrderService.getById(paymentOrder.getOrderId());
+        if (order == null) {
+            return R.fail("预订订单不存在");
+        }
+        try {
+            PrivateKey privateKey = WXPayUtility.loadPrivateKeyFromString(config.getWechatPrivateKeyFile());
+            PublicKey wechatPayPublicKey = WXPayUtility.loadPublicKeyFromString(config.getWechatPayPublicKeyFile());
+
+            WeChatPaymentStrategyImpl.QueryByWxTradeNoRequest req = new WeChatPaymentStrategyImpl.QueryByWxTradeNoRequest();
+            req.transactionId = outTradeNo;
+            req.mchid = config.getWechatMchId();
+            WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse response = searchOrderRun(config, privateKey, wechatPayPublicKey, req);
+            if (response == null) {
+                return R.fail("查询失败");
+            }
+            if ("SUCCESS".equals(response.tradeState)) {
+                Date now = new Date();
+                paymentOrder.setPayStatus(1);
+                paymentOrder.setTradeNo(response.transactionId);
+                paymentOrder.setPayTime(now);
+                paymentOrder.setUpdatedTime(now);
+                merchantPaymentOrderService.updateById(paymentOrder);
+
+                order.setPaymentStatus(1);
+                order.setPayTime(now);
+                order.setPaymentMethod("微信支付");
+                order.setOrderStatus(1);
+                if (StringUtils.isBlank(order.getVerificationCode())) {
+                    order.setVerificationCode("YS" + UniqueRandomNumGenerator.generateUniqueCode(10));
+                }
+                order.setUpdatedTime(now);
+                userReservationOrderService.updateById(order);
+                return R.success("支付成功");
+            }
+            if ("CLOSED".equals(response.tradeState)) {
+                return R.fail("交易已关闭");
+            }
+            if ("NOTPAY".equals(response.tradeState) || "USERPAYING".equals(response.tradeState)) {
+                return R.fail("等待用户付款");
+            }
+            return R.fail("订单状态:" + (response.tradeStateDesc != null ? response.tradeStateDesc : response.tradeState));
+        } catch (WXPayUtility.ApiException e) {
+            log.error("查询商户微信订单状态异常,outTradeNo={}", outTradeNo, e);
+            return R.fail("查询异常:" + e.getMessage());
+        } catch (Exception e) {
+            log.error("商户微信查询支付状态异常,storeId={}", storeId, e);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<String> refund(Integer storeId, String outTradeNo, String refundAmount, String refundReason) {
+        if (storeId == null || StringUtils.isBlank(outTradeNo)) {
+            return R.fail("门店ID和商户订单号不能为空");
+        }
+        if (StringUtils.isBlank(refundAmount) || new BigDecimal(refundAmount).compareTo(BigDecimal.ZERO) <= 0) {
+            return R.fail("退款金额必须大于0");
+        }
+        StorePaymentConfig config = storePaymentConfigService.getByStoreId(storeId);
+        if (config == null) {
+            return R.fail("该门店未配置支付参数");
+        }
+        MerchantPaymentOrder paymentOrder = merchantPaymentOrderService.getByOutTradeNo(outTradeNo);
+        if (paymentOrder == null) {
+            return R.fail("支付单不存在");
+        }
+        UserReservationOrder order = userReservationOrderService.getById(paymentOrder.getOrderId());
+        if (order == null) {
+            return R.fail("预订订单不存在");
+        }
+        if (order.getPaymentStatus() == null || order.getPaymentStatus() != 1) {
+            return R.fail("订单未支付或已退款,无法退款");
+        }
+        try {
+            PrivateKey privateKey = WXPayUtility.loadPrivateKeyFromString(config.getWechatPrivateKeyFile());
+            PublicKey wechatPayPublicKey = WXPayUtility.loadPublicKeyFromString(config.getWechatPayPublicKeyFile());
+
+            WeChatPaymentStrategyImpl.CreateRequest request = new WeChatPaymentStrategyImpl.CreateRequest();
+            request.outTradeNo = outTradeNo;
+            request.outRefundNo = UniqueRandomNumGenerator.generateUniqueCode(19);
+            request.reason = StringUtils.isNotBlank(refundReason) ? refundReason : "用户申请退款";
+            request.notifyUrl = StringUtils.isNotBlank(refundNotifyUrl) ? refundNotifyUrl : "";
+            request.amount = new WeChatPaymentStrategyImpl.AmountReq();
+            request.amount.refund = new BigDecimal(refundAmount).multiply(new BigDecimal(100)).longValue();
+            request.amount.total = order.getTotalAmount() != null ? order.getTotalAmount().multiply(new BigDecimal(100)).longValue() : request.amount.refund;
+            request.amount.currency = "CNY";
+
+            WeChatPaymentStrategyImpl.Refund response = refundRun(config, privateKey, wechatPayPublicKey, request);
+            if (response == null) {
+                return R.fail("退款失败");
+            }
+            String status = response.status != null ? response.status.name() : "";
+            if (!"SUCCESS".equals(status) && !"PROCESSING".equals(status)) {
+                return R.fail("退款失败:" + status);
+            }
+
+            Date now = new Date();
+            BigDecimal refundAmountDecimal = new BigDecimal(refundAmount);
+            paymentOrder.setPayStatus(3);
+            paymentOrder.setUpdatedTime(now);
+            merchantPaymentOrderService.updateById(paymentOrder);
+
+            order.setPaymentStatus(2);
+            order.setRefundAmount(refundAmountDecimal);
+            order.setRefundTime(now);
+            order.setRefundReason(refundReason);
+            order.setUpdatedTime(now);
+            userReservationOrderService.updateById(order);
+
+            RefundRecord record = new RefundRecord();
+            record.setPayType(PaymentEnum.WECHAT_PAY.getType());
+            record.setOutTradeNo(outTradeNo);
+            record.setTransactionId(response.transactionId);
+            record.setOutRefundNo(response.outRefundNo != null ? response.outRefundNo : request.outRefundNo);
+            record.setRefundId(response.refundId);
+            record.setRefundStatus("SUCCESS");
+            if (order.getTotalAmount() != null) {
+                record.setTotalAmount(order.getTotalAmount().multiply(new BigDecimal(100)).longValue());
+            }
+            record.setRefundAmount(refundAmountDecimal.multiply(new BigDecimal(100)).longValue());
+            record.setRefundReason(refundReason);
+            record.setOrderId(String.valueOf(order.getId()));
+            record.setStoreId(storeId);
+            record.setUserId(order.getUserId());
+            record.setCreatedTime(now);
+            record.setDeleteFlag(0);
+            refundRecordService.save(record);
+
+            log.info("商户预订订单微信退款成功,outTradeNo={}", outTradeNo);
+            return R.data("退款成功");
+        } catch (WXPayUtility.ApiException e) {
+            log.error("商户预订订单微信退款异常,outTradeNo={}", outTradeNo, e);
+            return R.fail("退款失败:" + e.getMessage());
+        } catch (Exception e) {
+            log.error("商户微信退款异常,storeId={}", storeId, e);
+            return R.fail("退款失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    public String getType() {
+        return PaymentEnum.WECHAT_PAY.getType();
+    }
+
+    private WeChatPaymentStrategyImpl.DirectAPIv3AppPrepayResponse prePayOrderRun(StorePaymentConfig config,
+                                                                                  PrivateKey privateKey,
+                                                                                  WeChatPaymentStrategyImpl.CommonPrepayRequest request) throws IOException {
+        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", config.getWechatPayPublicKeyId());
+        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(config.getWechatMchId(), config.getMerchantSerialNumber(), privateKey, POSTMETHOD, uri, reqBody));
+        reqBuilder.addHeader("Content-Type", "application/json");
+        MediaType jsonMediaType = MediaType.parse("application/json; charset=utf-8");
+        RequestBody body = RequestBody.create(reqBody, jsonMediaType);
+        reqBuilder.method(POSTMETHOD, body);
+        OkHttpClient client = new OkHttpClient.Builder().build();
+        try (Response httpResponse = client.newCall(reqBuilder.build()).execute()) {
+            String respBody = WXPayUtility.extractBody(httpResponse);
+            if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
+                PublicKey publicKey = WXPayUtility.loadPublicKeyFromString(config.getWechatPayPublicKeyFile());
+                WXPayUtility.validateResponse(config.getWechatPayPublicKeyId(), publicKey, httpResponse.headers(), respBody);
+                return WXPayUtility.fromJson(respBody, WeChatPaymentStrategyImpl.DirectAPIv3AppPrepayResponse.class);
+            }
+            throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
+        }
+    }
+
+    private WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse searchOrderRun(StorePaymentConfig config,
+                                                                               PrivateKey privateKey,
+                                                                               PublicKey wechatPayPublicKey,
+                                                                               WeChatPaymentStrategyImpl.QueryByWxTradeNoRequest request) throws IOException {
+        String uri = searchOrderByOutTradeNoPath.replace("{out_trade_no}", WXPayUtility.urlEncode(request.transactionId));
+        Map<String, Object> args = new HashMap<>();
+        args.put("mchid", config.getWechatMchId());
+        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", config.getWechatPayPublicKeyId());
+        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(config.getWechatMchId(), config.getMerchantSerialNumber(), privateKey, GETMETHOD, uri, null));
+        reqBuilder.method(GETMETHOD, null);
+        OkHttpClient client = new OkHttpClient.Builder().build();
+        try (Response httpResponse = client.newCall(reqBuilder.build()).execute()) {
+            String respBody = WXPayUtility.extractBody(httpResponse);
+            if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
+                WXPayUtility.validateResponse(config.getWechatPayPublicKeyId(), wechatPayPublicKey, httpResponse.headers(), respBody);
+                return WXPayUtility.fromJson(respBody, WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse.class);
+            }
+            throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
+        }
+    }
+
+    private WeChatPaymentStrategyImpl.Refund refundRun(StorePaymentConfig config,
+                                                       PrivateKey privateKey,
+                                                       PublicKey wechatPayPublicKey,
+                                                       WeChatPaymentStrategyImpl.CreateRequest request) throws IOException {
+        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", config.getWechatPayPublicKeyId());
+        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(config.getWechatMchId(), config.getMerchantSerialNumber(), privateKey, POSTMETHOD, uri, reqBody));
+        reqBuilder.addHeader("Content-Type", "application/json");
+        MediaType jsonMediaType = MediaType.parse("application/json; charset=utf-8");
+        RequestBody body = RequestBody.create(reqBody, jsonMediaType);
+        reqBuilder.method(POSTMETHOD, body);
+        OkHttpClient client = new OkHttpClient.Builder().build();
+        try (Response httpResponse = client.newCall(reqBuilder.build()).execute()) {
+            String respBody = WXPayUtility.extractBody(httpResponse);
+            if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
+                WXPayUtility.validateResponse(config.getWechatPayPublicKeyId(), wechatPayPublicKey, httpResponse.headers(), respBody);
+                return WXPayUtility.fromJson(respBody, WeChatPaymentStrategyImpl.Refund.class);
+            }
+            throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
+        }
+    }
+}