Browse Source

Merge branch 'sit-OrderFood' into sit-OrderFood-LuTong

lutong 2 months ago
parent
commit
a3f8042d69

+ 0 - 41
alien-dining/src/main/java/shop/alien/dining/controller/DiningMenuController.java

@@ -1,41 +0,0 @@
-package shop.alien.dining.controller;
-
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiSort;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.web.bind.annotation.*;
-import shop.alien.entity.result.R;
-
-/**
- * 点餐菜单控制器
- *
- * @author ssk
- * @version 1.0
- * @date 2024/12/4
- */
-@Slf4j
-@Api(tags = {"微信点餐-菜单管理(用户端)"})
-@ApiSort(1)
-@CrossOrigin
-@RestController
-@RequestMapping("/dining/menu")
-@RequiredArgsConstructor
-public class DiningMenuController {
-
-    @ApiOperation(value = "获取菜单列表", notes = "获取点餐菜单列表")
-    @GetMapping("/list")
-    public R list() {
-        // TODO: 实现菜单列表查询逻辑
-        return R.ok("菜单列表功能待实现");
-    }
-
-    @ApiOperation(value = "获取菜单详情", notes = "根据菜单ID获取菜单详情")
-    @GetMapping("/{id}")
-    public R getById(@PathVariable Long id) {
-        // TODO: 实现菜单详情查询逻辑
-        return R.ok("菜单详情功能待实现");
-    }
-}
-

+ 0 - 13
alien-dining/src/main/java/shop/alien/dining/service/DiningMenuService.java

@@ -1,13 +0,0 @@
-package shop.alien.dining.service;
-
-/**
- * 点餐菜单服务接口
- *
- * @author ssk
- * @version 1.0
- * @date 2024/12/4
- */
-public interface DiningMenuService {
-    // TODO: 定义菜单相关业务方法
-}
-

+ 0 - 21
alien-dining/src/main/java/shop/alien/dining/service/impl/DiningMenuServiceImpl.java

@@ -1,21 +0,0 @@
-package shop.alien.dining.service.impl;
-
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-import shop.alien.dining.service.DiningMenuService;
-
-/**
- * 点餐菜单服务实现类
- *
- * @author ssk
- * @version 1.0
- * @date 2024/12/4
- */
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class DiningMenuServiceImpl implements DiningMenuService {
-    // TODO: 实现菜单相关业务逻辑
-}
-

+ 49 - 11
alien-dining/src/main/java/shop/alien/dining/strategy/payment/PaymentStrategy.java

@@ -1,6 +1,8 @@
 package shop.alien.dining.strategy.payment;
 
-import java.math.BigDecimal;
+import shop.alien.entity.result.R;
+
+import java.util.Map;
 
 /**
  * 支付策略接口
@@ -10,22 +12,58 @@ import java.math.BigDecimal;
  * @date 2024/12/4
  */
 public interface PaymentStrategy {
+    /** 生成预支付订单
+     *
+     * @param price 订单金额
+     * @param subject 订单标题
+     * @return 预支付订单信息
+     * @throws Exception 生成异常
+     */
+    R createPrePayOrder(String price, String subject) throws Exception;
+
+
+    /**
+     * 处理支付通知
+     *
+     * @param notifyData 支付通知数据
+     * @return 处理结果
+     * @throws Exception 处理异常
+     */
+    R handleNotify(String notifyData) throws Exception;
+
     /**
-     * 支付
+     * 查询订单状态
      *
-     * @param amount 支付金额
-     * @param orderId 订单ID
-     * @return 支付结果
+     * @param transactionId 交易订单号
+     * @return 订单状态信息
+     * @throws Exception 查询异常
      */
-    String pay(BigDecimal amount, Long orderId);
+    R searchOrderByOutTradeNoPath(String transactionId) throws Exception;
+
+    /**
+     * 处理退款请求
+     *
+     * @param params 退款请求参数
+     * @return 处理结果
+     * @throws Exception 处理异常
+     */
+    String handleRefund(Map<String,String> params) throws Exception;
+
+    /**
+     * 查询退款记录
+     *
+     * @param outRefundNo 退款订单号
+     * @return 退款记录信息
+     * @throws Exception 查询异常
+     */
+    R searchRefundRecordByOutRefundNo(String outRefundNo ) throws Exception;
+
 
     /**
-     * 退款
+     * 获取策略类型字符串
      *
-     * @param amount 退款金额
-     * @param orderId 订单ID
-     * @return 退款结果
+     * @return 策略类型字符串
      */
-    String refund(BigDecimal amount, Long orderId);
+    String getType();
 }
 

+ 64 - 0
alien-dining/src/main/java/shop/alien/dining/strategy/payment/PaymentStrategyFactory.java

@@ -0,0 +1,64 @@
+package shop.alien.dining.strategy.payment;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 支付策略工厂
+ *
+ * @author lyx
+ * @date 2025/11/20
+ */
+@Slf4j
+@Component
+public class PaymentStrategyFactory {
+
+    @Autowired
+    private List<PaymentStrategy> paymentStrategies;
+
+    private final Map<String, PaymentStrategy> strategyMap = new HashMap<>();
+
+        /**
+         * 初始化策略映射
+         */
+        @PostConstruct
+        public void init() {
+            if (paymentStrategies != null && !paymentStrategies.isEmpty()) {
+                for (PaymentStrategy strategy : paymentStrategies) {
+                    strategyMap.put(strategy.getType(), strategy);
+                    log.info("注册支付策略: {} -> {}", strategy.getType(), strategy.getClass().getSimpleName());
+                }
+            }
+        }
+
+    /**
+     * 根据类型获取OCR策略
+     *
+     * @param type OCR类型
+     * @return OCR策略实例
+     * @throws IllegalArgumentException 如果类型不存在
+     */
+    public PaymentStrategy getStrategy(String type) {
+        PaymentStrategy strategy = strategyMap.get(type);
+        if (strategy == null) {
+            throw new IllegalArgumentException("不支持的支付类型: " + type);
+        }
+        return strategy;
+    }
+
+    /**
+     * 检查是否支持指定的OCR类型
+     *
+     * @param type OCR类型
+     * @return 是否支持
+     */
+    public boolean supports(String type) {
+        return strategyMap.containsKey(type);
+    }
+}

+ 656 - 0
alien-dining/src/main/java/shop/alien/dining/strategy/payment/impl/WeChatPaymentMininProgramStrategyImpl.java

@@ -0,0 +1,656 @@
+package shop.alien.dining.strategy.payment.impl;
+
+
+import com.google.gson.annotations.Expose;
+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.dining.strategy.payment.PaymentStrategy;
+import shop.alien.dining.util.WXPayUtility;
+import shop.alien.entity.result.R;
+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.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;
+
+    @Value("${payment.wechatPay.searchOrderByOutTradeNoPath}")
+    private String searchOrderByOutTradeNoPath;
+
+
+    private static String POSTMETHOD = "POST";
+    private static String GETMETHOD = "GET";
+
+    private PrivateKey privateKey;
+    private PublicKey wechatPayPublicKey;
+
+
+    @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 {
+        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());
+        }
+    }
+
+    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, 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, DirectAPIv3QueryResponse.class);
+            } else {
+                throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
+            }
+        } catch (IOException e) {
+            throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
+        }
+    }
+
+    @Override
+    public String handleRefund(Map<String, String> params) throws Exception {
+        return null;
+    }
+
+    @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;
+    }
+
+    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;
+    }
+}
+

+ 0 - 34
alien-dining/src/main/java/shop/alien/dining/strategy/payment/impl/WeChatPaymentStrategyImpl.java

@@ -1,34 +0,0 @@
-package shop.alien.dining.strategy.payment.impl;
-
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-import shop.alien.dining.strategy.payment.PaymentStrategy;
-
-import java.math.BigDecimal;
-
-/**
- * 微信支付策略实现
- *
- * @author ssk
- * @version 1.0
- * @date 2024/12/4
- */
-@Slf4j
-@Component
-public class WeChatPaymentStrategyImpl implements PaymentStrategy {
-
-    @Override
-    public String pay(BigDecimal amount, Long orderId) {
-        // TODO: 实现微信支付逻辑
-        log.info("微信支付: 订单ID={}, 金额={}", orderId, amount);
-        return "微信支付成功";
-    }
-
-    @Override
-    public String refund(BigDecimal amount, Long orderId) {
-        // TODO: 实现微信退款逻辑
-        log.info("微信退款: 订单ID={}, 金额={}", orderId, amount);
-        return "微信退款成功";
-    }
-}
-

+ 856 - 0
alien-dining/src/main/java/shop/alien/dining/util/WXPayUtility.java

@@ -0,0 +1,856 @@
+package shop.alien.dining.util;
+
+import com.google.gson.*;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+import okhttp3.Headers;
+import okhttp3.Response;
+import okio.BufferedSource;
+import org.bouncycastle.crypto.digests.SM3Digest;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.*;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.time.DateTimeException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.*;
+import java.util.Map.Entry;
+
+public class WXPayUtility {
+    private static final Gson gson = new GsonBuilder()
+            .disableHtmlEscaping()
+            .addSerializationExclusionStrategy(new ExclusionStrategy() {
+                @Override
+                public boolean shouldSkipField(FieldAttributes fieldAttributes) {
+                    final Expose expose = fieldAttributes.getAnnotation(Expose.class);
+                    return expose != null && !expose.serialize();
+                }
+
+                @Override
+                public boolean shouldSkipClass(Class<?> aClass) {
+                    return false;
+                }
+            })
+            .addDeserializationExclusionStrategy(new ExclusionStrategy() {
+                @Override
+                public boolean shouldSkipField(FieldAttributes fieldAttributes) {
+                    final Expose expose = fieldAttributes.getAnnotation(Expose.class);
+                    return expose != null && !expose.deserialize();
+                }
+
+                @Override
+                public boolean shouldSkipClass(Class<?> aClass) {
+                    return false;
+                }
+            })
+            .create();
+    private static final char[] SYMBOLS =
+            "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
+    private static final SecureRandom random = new SecureRandom();
+
+    /**
+     * 将 Object 转换为 JSON 字符串
+     */
+    public static String toJson(Object object) {
+        return gson.toJson(object);
+    }
+
+    /**
+     * 将 JSON 字符串解析为特定类型的实例
+     */
+    public static <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
+        return gson.fromJson(json, classOfT);
+    }
+
+    /**
+     * 从公私钥文件路径中读取文件内容
+     *
+     * @param keyPath 文件路径
+     * @return 文件内容
+     */
+    private static String readKeyStringFromPath(String keyPath) {
+        try {
+            return new String(Files.readAllBytes(Paths.get(keyPath)), StandardCharsets.UTF_8);
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    /**
+     * 读取 PKCS#8 格式的私钥字符串并加载为私钥对象
+     *
+     * @param keyString 私钥文件内容,以 -----BEGIN PRIVATE KEY----- 开头
+     * @return PrivateKey 对象
+     */
+    public static PrivateKey loadPrivateKeyFromString(String keyString) {
+        try {
+            keyString = keyString.replace("-----BEGIN PRIVATE KEY-----", "")
+                    .replace("-----END PRIVATE KEY-----", "")
+                    .replaceAll("\\s+", "");
+            return KeyFactory.getInstance("RSA").generatePrivate(
+                    new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString)));
+        } catch (NoSuchAlgorithmException e) {
+            throw new UnsupportedOperationException(e);
+        } catch (InvalidKeySpecException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * 从 PKCS#8 格式的私钥文件中加载私钥
+     *
+     * @param keyPath 私钥文件路径
+     * @return PrivateKey 对象
+     */
+    public static PrivateKey loadPrivateKeyFromPath(String keyPath) {
+        return loadPrivateKeyFromString(readKeyStringFromPath(keyPath));
+    }
+
+    /**
+     * 读取 PKCS#8 格式的公钥字符串并加载为公钥对象
+     *
+     * @param keyString 公钥文件内容,以 -----BEGIN PUBLIC KEY----- 开头
+     * @return PublicKey 对象
+     */
+    public static PublicKey loadPublicKeyFromString(String keyString) {
+        try {
+            keyString = keyString.replace("-----BEGIN PUBLIC KEY-----", "")
+                    .replace("-----END PUBLIC KEY-----", "")
+                    .replaceAll("\\s+", "");
+            return KeyFactory.getInstance("RSA").generatePublic(
+                    new X509EncodedKeySpec(Base64.getDecoder().decode(keyString)));
+        } catch (NoSuchAlgorithmException e) {
+            throw new UnsupportedOperationException(e);
+        } catch (InvalidKeySpecException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * 从 PKCS#8 格式的公钥文件中加载公钥
+     *
+     * @param keyPath 公钥文件路径
+     * @return PublicKey 对象
+     */
+    public static PublicKey loadPublicKeyFromPath(String keyPath) {
+        return loadPublicKeyFromString(readKeyStringFromPath(keyPath));
+    }
+
+    /**
+     * 创建指定长度的随机字符串,字符集为[0-9a-zA-Z],可用于安全相关用途
+     */
+    public static String createNonce(int length) {
+        char[] buf = new char[length];
+        for (int i = 0; i < length; ++i) {
+            buf[i] = SYMBOLS[random.nextInt(SYMBOLS.length)];
+        }
+        return new String(buf);
+    }
+
+    /**
+     * 使用公钥按照 RSA_PKCS1_OAEP_PADDING 算法进行加密
+     *
+     * @param publicKey 加密用公钥对象
+     * @param plaintext 待加密明文
+     * @return 加密后密文
+     */
+    public static String encrypt(PublicKey publicKey, String plaintext) {
+        final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
+
+        try {
+            Cipher cipher = Cipher.getInstance(transformation);
+            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
+            return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)));
+        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+            throw new IllegalArgumentException("The current Java environment does not support " + transformation, e);
+        } catch (InvalidKeyException e) {
+            throw new IllegalArgumentException("RSA encryption using an illegal publicKey", e);
+        } catch (BadPaddingException | IllegalBlockSizeException e) {
+            throw new IllegalArgumentException("Plaintext is too long", e);
+        }
+    }
+
+    public static String aesAeadDecrypt(byte[] key, byte[] associatedData, byte[] nonce,
+                                        byte[] ciphertext) {
+        final String transformation = "AES/GCM/NoPadding";
+        final String algorithm = "AES";
+        final int tagLengthBit = 128;
+
+        try {
+            Cipher cipher = Cipher.getInstance(transformation);
+            cipher.init(
+                    Cipher.DECRYPT_MODE,
+                    new SecretKeySpec(key, algorithm),
+                    new GCMParameterSpec(tagLengthBit, nonce));
+            if (associatedData != null) {
+                cipher.updateAAD(associatedData);
+            }
+            return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
+        } catch (InvalidKeyException
+                 | InvalidAlgorithmParameterException
+                 | BadPaddingException
+                 | IllegalBlockSizeException
+                 | NoSuchAlgorithmException
+                 | NoSuchPaddingException e) {
+            throw new IllegalArgumentException(String.format("AesAeadDecrypt with %s Failed",
+                    transformation), e);
+        }
+    }
+
+    /**
+     * 使用私钥按照指定算法进行签名
+     *
+     * @param message    待签名串
+     * @param algorithm  签名算法,如 SHA256withRSA
+     * @param privateKey 签名用私钥对象
+     * @return 签名结果
+     */
+    public static String sign(String message, String algorithm, PrivateKey privateKey) {
+        byte[] sign;
+        try {
+            Signature signature = Signature.getInstance(algorithm);
+            signature.initSign(privateKey);
+            signature.update(message.getBytes(StandardCharsets.UTF_8));
+            sign = signature.sign();
+        } catch (NoSuchAlgorithmException e) {
+            throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e);
+        } catch (InvalidKeyException e) {
+            throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e);
+        } catch (SignatureException e) {
+            throw new RuntimeException("An error occurred during the sign process.", e);
+        }
+        return Base64.getEncoder().encodeToString(sign);
+    }
+
+    /**
+     * 使用公钥按照特定算法验证签名
+     *
+     * @param message   待签名串
+     * @param signature 待验证的签名内容
+     * @param algorithm 签名算法,如:SHA256withRSA
+     * @param publicKey 验签用公钥对象
+     * @return 签名验证是否通过
+     */
+    public static boolean verify(String message, String signature, String algorithm,
+                                 PublicKey publicKey) {
+        try {
+            Signature sign = Signature.getInstance(algorithm);
+            sign.initVerify(publicKey);
+            sign.update(message.getBytes(StandardCharsets.UTF_8));
+            return sign.verify(Base64.getDecoder().decode(signature));
+        } catch (SignatureException e) {
+            return false;
+        } catch (InvalidKeyException e) {
+            throw new IllegalArgumentException("verify uses an illegal publickey.", e);
+        } catch (NoSuchAlgorithmException e) {
+            throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e);
+        }
+    }
+
+    /**
+     * 根据微信支付APIv3请求签名规则构造 Authorization 签名
+     *
+     * @param mchid               商户号
+     * @param certificateSerialNo 商户API证书序列号
+     * @param privateKey          商户API证书私钥
+     * @param method              请求接口的HTTP方法,请使用全大写表述,如 GET、POST、PUT、DELETE
+     * @param uri                 请求接口的URL
+     * @param body                请求接口的Body
+     * @return 构造好的微信支付APIv3 Authorization 头
+     */
+    public static String buildAuthorization(String mchid, String certificateSerialNo,
+                                            PrivateKey privateKey,
+                                            String method, String uri, String body) {
+        String nonce = createNonce(32);
+        long timestamp = Instant.now().getEpochSecond();
+
+        String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce,
+                body == null ? "" : body);
+
+        String signature = sign(message, "SHA256withRSA", privateKey);
+
+        return String.format(
+                "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",signature=\"%s\"," +
+                        "timestamp=\"%d\",serial_no=\"%s\"",
+                mchid, nonce, signature, timestamp, certificateSerialNo);
+    }
+
+    /**
+     * 计算输入流的哈希值
+     *
+     * @param inputStream 输入流
+     * @param algorithm   哈希算法名称,如 "SHA-256", "SHA-1"
+     * @return 哈希值的十六进制字符串
+     */
+    private static String calculateHash(InputStream inputStream, String algorithm) {
+        try {
+            MessageDigest digest = MessageDigest.getInstance(algorithm);
+            byte[] buffer = new byte[8192];
+            int bytesRead;
+            while ((bytesRead = inputStream.read(buffer)) != -1) {
+                digest.update(buffer, 0, bytesRead);
+            }
+            byte[] hashBytes = digest.digest();
+            StringBuilder hexString = new StringBuilder();
+            for (byte b : hashBytes) {
+                String hex = Integer.toHexString(0xff & b);
+                if (hex.length() == 1) {
+                    hexString.append('0');
+                }
+                hexString.append(hex);
+            }
+            return hexString.toString();
+        } catch (NoSuchAlgorithmException e) {
+            throw new UnsupportedOperationException(algorithm + " algorithm not available", e);
+        } catch (IOException e) {
+            throw new RuntimeException("Error reading from input stream", e);
+        }
+    }
+
+    /**
+     * 计算输入流的 SHA256 哈希值
+     *
+     * @param inputStream 输入流
+     * @return SHA256 哈希值的十六进制字符串
+     */
+    public static String sha256(InputStream inputStream) {
+        return calculateHash(inputStream, "SHA-256");
+    }
+
+    /**
+     * 计算输入流的 SHA1 哈希值
+     *
+     * @param inputStream 输入流
+     * @return SHA1 哈希值的十六进制字符串
+     */
+    public static String sha1(InputStream inputStream) {
+        return calculateHash(inputStream, "SHA-1");
+    }
+
+    /**
+     * 计算输入流的 SM3 哈希值
+     *
+     * @param inputStream 输入流
+     * @return SM3 哈希值的十六进制字符串
+     */
+    public static String sm3(InputStream inputStream) {
+        // 确保Bouncy Castle Provider已注册
+        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+            Security.addProvider(new BouncyCastleProvider());
+        }
+
+        try {
+            SM3Digest digest = new SM3Digest();
+            byte[] buffer = new byte[8192];
+            int bytesRead;
+            while ((bytesRead = inputStream.read(buffer)) != -1) {
+                digest.update(buffer, 0, bytesRead);
+            }
+            byte[] hashBytes = new byte[digest.getDigestSize()];
+            digest.doFinal(hashBytes, 0);
+
+            StringBuilder hexString = new StringBuilder();
+            for (byte b : hashBytes) {
+                String hex = Integer.toHexString(0xff & b);
+                if (hex.length() == 1) {
+                    hexString.append('0');
+                }
+                hexString.append(hex);
+            }
+            return hexString.toString();
+        } catch (IOException e) {
+            throw new RuntimeException("Error reading from input stream", e);
+        }
+    }
+
+    /**
+     * 对参数进行 URL 编码
+     *
+     * @param content 参数内容
+     * @return 编码后的内容
+     */
+    public static String urlEncode(String content) {
+        try {
+            return URLEncoder.encode(content, StandardCharsets.UTF_8.name());
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 对参数Map进行 URL 编码,生成 QueryString
+     *
+     * @param params Query参数Map
+     * @return QueryString
+     */
+    public static String urlEncode(Map<String, Object> params) {
+        if (params == null || params.isEmpty()) {
+            return "";
+        }
+
+        StringBuilder result = new StringBuilder();
+        for (Entry<String, Object> entry : params.entrySet()) {
+            if (entry.getValue() == null) {
+                continue;
+            }
+
+            String key = entry.getKey();
+            Object value = entry.getValue();
+            if (value instanceof List) {
+                List<?> list = (List<?>) entry.getValue();
+                for (Object temp : list) {
+                    appendParam(result, key, temp);
+                }
+            } else {
+                appendParam(result, key, value);
+            }
+        }
+        return result.toString();
+    }
+
+    /**
+     * 将键值对 放入返回结果
+     *
+     * @param result 返回的query string
+     * @param key 属性
+     * @param value 属性值
+     */
+    private static void appendParam(StringBuilder result, String key, Object value) {
+        if (result.length() > 0) {
+            result.append("&");
+        }
+
+        String valueString;
+        // 如果是基本类型、字符串或枚举,直接转换;如果是对象,序列化为JSON
+        if (value instanceof String || value instanceof Number ||
+                value instanceof Boolean || value instanceof Enum) {
+            valueString = value.toString();
+        } else {
+            valueString = toJson(value);
+        }
+
+        result.append(key)
+                .append("=")
+                .append(urlEncode(valueString));
+    }
+
+    /**
+     * 从应答中提取 Body
+     *
+     * @param response HTTP 请求应答对象
+     * @return 应答中的Body内容,Body为空时返回空字符串
+     */
+    public static String extractBody(Response response) {
+        if (response.body() == null) {
+            return "";
+        }
+
+        try {
+            BufferedSource source = response.body().source();
+            return source.readUtf8();
+        } catch (IOException e) {
+            throw new RuntimeException(String.format("An error occurred during reading response body. " +
+                    "Status: %d", response.code()), e);
+        }
+    }
+
+    /**
+     * 根据微信支付APIv3应答验签规则对应答签名进行验证,验证不通过时抛出异常
+     *
+     * @param wechatpayPublicKeyId 微信支付公钥ID
+     * @param wechatpayPublicKey   微信支付公钥对象
+     * @param headers              微信支付应答 Header 列表
+     * @param body                 微信支付应答 Body
+     */
+    public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey,
+                                        Headers headers,
+                                        String body) {
+        String timestamp = headers.get("Wechatpay-Timestamp");
+        String requestId = headers.get("Request-ID");
+        try {
+            Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp));
+            // 拒绝过期请求
+            if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) {
+                throw new IllegalArgumentException(
+                        String.format("Validate response failed, timestamp[%s] is expired, request-id[%s]",
+                                timestamp, requestId));
+            }
+        } catch (DateTimeException | NumberFormatException e) {
+            throw new IllegalArgumentException(
+                    String.format("Validate response failed, timestamp[%s] is invalid, request-id[%s]",
+                            timestamp, requestId));
+        }
+        String serialNumber = headers.get("Wechatpay-Serial");
+        if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) {
+            throw new IllegalArgumentException(
+                    String.format("Validate response failed, Invalid Wechatpay-Serial, Local: %s, Remote: " +
+                            "%s", wechatpayPublicKeyId, serialNumber));
+        }
+
+        String signature = headers.get("Wechatpay-Signature");
+        String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"),
+                body == null ? "" : body);
+
+        boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey);
+        if (!success) {
+            throw new IllegalArgumentException(
+                    String.format("Validate response failed,the WechatPay signature is incorrect.%n"
+                                    + "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]",
+                            headers.get("Request-ID"), headers, body));
+        }
+    }
+
+    /**
+     * 根据微信支付APIv3通知验签规则对通知签名进行验证,验证不通过时抛出异常
+     * @param wechatpayPublicKeyId 微信支付公钥ID
+     * @param wechatpayPublicKey 微信支付公钥对象
+     * @param headers 微信支付通知 Header 列表
+     * @param body 微信支付通知 Body
+     */
+    public static void validateNotification(String wechatpayPublicKeyId,
+                                            PublicKey wechatpayPublicKey, Headers headers,
+                                            String body) {
+        String timestamp = headers.get("Wechatpay-Timestamp");
+        try {
+            Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp));
+            // 拒绝过期请求
+            if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) {
+                throw new IllegalArgumentException(
+                        String.format("Validate notification failed, timestamp[%s] is expired", timestamp));
+            }
+        } catch (DateTimeException | NumberFormatException e) {
+            throw new IllegalArgumentException(
+                    String.format("Validate notification failed, timestamp[%s] is invalid", timestamp));
+        }
+        String serialNumber = headers.get("Wechatpay-Serial");
+        if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) {
+            throw new IllegalArgumentException(
+                    String.format("Validate notification failed, Invalid Wechatpay-Serial, Local: %s, " +
+                                    "Remote: %s",
+                            wechatpayPublicKeyId,
+                            serialNumber));
+        }
+
+        String signature = headers.get("Wechatpay-Signature");
+        String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"),
+                body == null ? "" : body);
+
+        boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey);
+        if (!success) {
+            throw new IllegalArgumentException(
+                    String.format("Validate notification failed, WechatPay signature is incorrect.\n"
+                                    + "responseHeader[%s]\tresponseBody[%.1024s]",
+                            headers, body));
+        }
+    }
+
+    /**
+     * 对微信支付通知进行签名验证、解析,同时将业务数据解密。验签名失败、解析失败、解密失败时抛出异常
+     * @param apiv3Key 商户的 APIv3 Key
+     * @param wechatpayPublicKeyId 微信支付公钥ID
+     * @param wechatpayPublicKey   微信支付公钥对象
+     * @param headers              微信支付请求 Header 列表
+     * @param body                 微信支付请求 Body
+     * @return 解析后的通知内容,解密后的业务数据可以使用 Notification.getPlaintext() 访问
+     */
+    public static Notification parseNotification(String apiv3Key, String wechatpayPublicKeyId,
+                                                 PublicKey wechatpayPublicKey, Headers headers,
+                                                 String body) {
+        validateNotification(wechatpayPublicKeyId, wechatpayPublicKey, headers, body);
+        Notification notification = gson.fromJson(body, Notification.class);
+        notification.decrypt(apiv3Key);
+        return notification;
+    }
+
+    /**
+     * 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常
+     */
+    public static class ApiException extends RuntimeException {
+        private static final long serialVersionUID = 2261086748874802175L;
+
+        private final int statusCode;
+        private final String body;
+        private final Headers headers;
+        private final String errorCode;
+        private final String errorMessage;
+
+        public ApiException(int statusCode, String body, Headers headers) {
+            super(String.format("微信支付API访问失败,StatusCode: [%s], Body: [%s], Headers: [%s]", statusCode,
+                    body, headers));
+            this.statusCode = statusCode;
+            this.body = body;
+            this.headers = headers;
+
+            if (body != null && !body.isEmpty()) {
+                JsonElement code;
+                JsonElement message;
+
+                try {
+                    JsonObject jsonObject = gson.fromJson(body, JsonObject.class);
+                    code = jsonObject.get("code");
+                    message = jsonObject.get("message");
+                } catch (JsonSyntaxException ignored) {
+                    code = null;
+                    message = null;
+                }
+                this.errorCode = code == null ? null : code.getAsString();
+                this.errorMessage = message == null ? null : message.getAsString();
+            } else {
+                this.errorCode = null;
+                this.errorMessage = null;
+            }
+        }
+
+        /**
+         * 获取 HTTP 应答状态码
+         */
+        public int getStatusCode() {
+            return statusCode;
+        }
+
+        /**
+         * 获取 HTTP 应答包体内容
+         */
+        public String getBody() {
+            return body;
+        }
+
+        /**
+         * 获取 HTTP 应答 Header
+         */
+        public Headers getHeaders() {
+            return headers;
+        }
+
+        /**
+         * 获取 错误码 (错误应答中的 code 字段)
+         */
+        public String getErrorCode() {
+            return errorCode;
+        }
+
+        /**
+         * 获取 错误消息 (错误应答中的 message 字段)
+         */
+        public String getErrorMessage() {
+            return errorMessage;
+        }
+    }
+
+    public static class Notification {
+        @SerializedName("id")
+        private String id;
+        @SerializedName("create_time")
+        private String createTime;
+        @SerializedName("event_type")
+        private String eventType;
+        @SerializedName("resource_type")
+        private String resourceType;
+        @SerializedName("summary")
+        private String summary;
+        @SerializedName("resource")
+        private Resource resource;
+        private String plaintext;
+
+        public String getId() {
+            return id;
+        }
+
+        public String getCreateTime() {
+            return createTime;
+        }
+
+        public String getEventType() {
+            return eventType;
+        }
+
+        public String getResourceType() {
+            return resourceType;
+        }
+
+        public String getSummary() {
+            return summary;
+        }
+
+        public Resource getResource() {
+            return resource;
+        }
+
+        /**
+         * 获取解密后的业务数据(JSON字符串,需要自行解析)
+         */
+        public String getPlaintext() {
+            return plaintext;
+        }
+
+        private void validate() {
+            if (resource == null) {
+                throw new IllegalArgumentException("Missing required field `resource` in notification");
+            }
+            resource.validate();
+        }
+
+        /**
+         * 使用 APIv3Key 对通知中的业务数据解密,解密结果可以通过 getPlainText 访问。
+         * 外部拿到的 Notification 一定是解密过的,因此本方法没有设置为 public
+         * @param apiv3Key 商户APIv3 Key
+         */
+        private void decrypt(String apiv3Key) {
+            validate();
+
+            plaintext = aesAeadDecrypt(
+                    apiv3Key.getBytes(StandardCharsets.UTF_8),
+                    resource.associatedData.getBytes(StandardCharsets.UTF_8),
+                    resource.nonce.getBytes(StandardCharsets.UTF_8),
+                    Base64.getDecoder().decode(resource.ciphertext)
+            );
+        }
+
+        public static class Resource {
+            @SerializedName("algorithm")
+            private String algorithm;
+
+            @SerializedName("ciphertext")
+            private String ciphertext;
+
+            @SerializedName("associated_data")
+            private String associatedData;
+
+            @SerializedName("nonce")
+            private String nonce;
+
+            @SerializedName("original_type")
+            private String originalType;
+
+            public String getAlgorithm() {
+                return algorithm;
+            }
+
+            public String getCiphertext() {
+                return ciphertext;
+            }
+
+            public String getAssociatedData() {
+                return associatedData;
+            }
+
+            public String getNonce() {
+                return nonce;
+            }
+
+            public String getOriginalType() {
+                return originalType;
+            }
+
+            private void validate() {
+                if (algorithm == null || algorithm.isEmpty()) {
+                    throw new IllegalArgumentException("Missing required field `algorithm` in Notification" +
+                            ".Resource");
+                }
+                if (!Objects.equals(algorithm, "AEAD_AES_256_GCM")) {
+                    throw new IllegalArgumentException(String.format("Unsupported `algorithm`[%s] in " +
+                            "Notification.Resource", algorithm));
+                }
+
+                if (ciphertext == null || ciphertext.isEmpty()) {
+                    throw new IllegalArgumentException("Missing required field `ciphertext` in Notification" +
+                            ".Resource");
+                }
+
+                if (associatedData == null || associatedData.isEmpty()) {
+                    throw new IllegalArgumentException("Missing required field `associatedData` in " +
+                            "Notification.Resource");
+                }
+
+                if (nonce == null || nonce.isEmpty()) {
+                    throw new IllegalArgumentException("Missing required field `nonce` in Notification" +
+                            ".Resource");
+                }
+
+                if (originalType == null || originalType.isEmpty()) {
+                    throw new IllegalArgumentException("Missing required field `originalType` in " +
+                            "Notification.Resource");
+                }
+            }
+        }
+    }
+    /**
+     * 根据文件名获取对应的Content-Type
+     * @param fileName 文件名
+     * @return Content-Type字符串
+     */
+    public static String getContentTypeByFileName(String fileName) {
+        if (fileName == null || fileName.isEmpty()) {
+            return "application/octet-stream";
+        }
+
+        // 获取文件扩展名
+        String extension = "";
+        int lastDotIndex = fileName.lastIndexOf('.');
+        if (lastDotIndex > 0 && lastDotIndex < fileName.length() - 1) {
+            extension = fileName.substring(lastDotIndex + 1).toLowerCase();
+        }
+
+        // 常见文件类型映射
+        Map<String, String> contentTypeMap = new HashMap<>();
+        // 图片类型
+        contentTypeMap.put("png", "image/png");
+        contentTypeMap.put("jpg", "image/jpeg");
+        contentTypeMap.put("jpeg", "image/jpeg");
+        contentTypeMap.put("gif", "image/gif");
+        contentTypeMap.put("bmp", "image/bmp");
+        contentTypeMap.put("webp", "image/webp");
+        contentTypeMap.put("svg", "image/svg+xml");
+        contentTypeMap.put("ico", "image/x-icon");
+
+        // 文档类型
+        contentTypeMap.put("pdf", "application/pdf");
+        contentTypeMap.put("doc", "application/msword");
+        contentTypeMap.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
+        contentTypeMap.put("xls", "application/vnd.ms-excel");
+        contentTypeMap.put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        contentTypeMap.put("ppt", "application/vnd.ms-powerpoint");
+        contentTypeMap.put("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation");
+
+        // 文本类型
+        contentTypeMap.put("txt", "text/plain");
+        contentTypeMap.put("html", "text/html");
+        contentTypeMap.put("css", "text/css");
+        contentTypeMap.put("js", "application/javascript");
+        contentTypeMap.put("json", "application/json");
+        contentTypeMap.put("xml", "application/xml");
+        contentTypeMap.put("csv", "text/csv");
+
+        // 音视频类型
+        contentTypeMap.put("mp3", "audio/mpeg");
+        contentTypeMap.put("wav", "audio/wav");
+        contentTypeMap.put("mp4", "video/mp4");
+        contentTypeMap.put("avi", "video/x-msvideo");
+        contentTypeMap.put("mov", "video/quicktime");
+
+        // 压缩文件类型
+        contentTypeMap.put("zip", "application/zip");
+        contentTypeMap.put("rar", "application/x-rar-compressed");
+        contentTypeMap.put("7z", "application/x-7z-compressed");
+
+
+        return contentTypeMap.getOrDefault(extension, "application/octet-stream");
+    }
+}

+ 8 - 0
alien-store/src/main/java/shop/alien/store/strategy/payment/PaymentStrategy.java

@@ -50,6 +50,14 @@ public interface PaymentStrategy {
      */
      String handleRefund(Map<String,String> params) throws Exception;
 
+     /**
+     * 查询退款记录
+     *
+     * @param outRefundNo 退款订单号
+     * @return 退款记录信息
+     * @throws Exception 查询异常
+     */
+    R searchRefundRecordByOutRefundNo(String outRefundNo ) throws Exception;
 
 
     /**

+ 5 - 0
alien-store/src/main/java/shop/alien/store/strategy/payment/impl/AlipayPaymentStrategyImpl.java

@@ -347,6 +347,11 @@ public class AlipayPaymentStrategyImpl implements PaymentStrategy {
     }
 
     @Override
+    public R searchRefundRecordByOutRefundNo(String outRefundNo) throws Exception {
+        return null;
+    }
+
+    @Override
     public String getType() {
         return PaymentEnum.ALIPAY.getType();
     }

+ 639 - 0
alien-store/src/main/java/shop/alien/store/strategy/payment/impl/WeChatPaymentMininProgramStrategyImpl.java

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

+ 9 - 12
alien-store/src/main/java/shop/alien/store/strategy/payment/impl/WeChatPaymentStrategyImpl.java

@@ -28,11 +28,7 @@ import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.time.OffsetDateTime;
 import java.time.format.DateTimeFormatter;
-import java.util.Base64;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 /**
  * 微信支付策略
@@ -213,10 +209,6 @@ public class WeChatPaymentStrategyImpl implements PaymentStrategy {
             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;
@@ -417,6 +409,11 @@ public class WeChatPaymentStrategyImpl implements PaymentStrategy {
         }
     }
 
+    @Override
+    public R searchRefundRecordByOutRefundNo(String outRefundNo) throws Exception {
+        return null;
+    }
+
 
     @Override
     public String getType() {
@@ -981,7 +978,7 @@ public class WeChatPaymentStrategyImpl implements PaymentStrategy {
     /**
      * 从微信支付退款响应构建RefundRecord对象(成功情况)
      */
-    private RefundRecord buildRefundRecordFromWeChatResponse(Refund response, CreateRequest request, Map<String, String> params) {
+    public RefundRecord buildRefundRecordFromWeChatResponse(Refund response, CreateRequest request, Map<String, String> params) {
         try {
             RefundRecord record = new RefundRecord();
             
@@ -1083,7 +1080,7 @@ public class WeChatPaymentStrategyImpl implements PaymentStrategy {
     /**
      * 从微信支付退款错误响应构建RefundRecord对象(失败情况)
      */
-    private RefundRecord buildRefundRecordFromWeChatError(Refund response, CreateRequest request, Map<String, String> params, String status) {
+    public RefundRecord buildRefundRecordFromWeChatError(Refund response, CreateRequest request, Map<String, String> params, String status) {
         try {
             RefundRecord record = new RefundRecord();
             
@@ -1155,7 +1152,7 @@ public class WeChatPaymentStrategyImpl implements PaymentStrategy {
     /**
      * 从微信支付退款异常构建RefundRecord对象(异常情况)
      */
-    private RefundRecord buildRefundRecordFromWeChatException(CreateRequest request, Map<String, String> params, Exception e) {
+    public RefundRecord buildRefundRecordFromWeChatException(CreateRequest request, Map<String, String> params, Exception e) {
         try {
             RefundRecord record = new RefundRecord();
             

+ 2 - 0
alien-util/src/main/java/shop/alien/util/common/constant/PaymentEnum.java

@@ -11,6 +11,8 @@ public enum PaymentEnum {
     ALIPAY("alipay"),
     /** 微信支付 */
     WECHAT_PAY("wechatPay"),
+    /** 微信支付小程序 */
+    WECHAT_PAY_MININ_PROGRAM("wechatPayMininProgram"),
     /** 银联支付 */
     UNION_PAY("unionPay");