liujie
2025-12-04 8748b9ca3e0528824e2ab52682cbf7d5db6ed8f6
小程序支付
2个文件已添加
6个文件已修改
464 ■■■■■ 已修改文件
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/CourseController.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/WxPayController.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/resources/application-test.yml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentRefundModel.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxPayConfig.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxRefundUtil.java 354 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxV3Pay.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/CourseController.java
@@ -3,7 +3,6 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.google.common.collect.ImmutableMap;
import com.ruoyi.common.core.domain.BasePage;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.OrderNos;
@@ -11,18 +10,16 @@
import com.ruoyi.system.model.TAppUser;
import com.ruoyi.system.model.TCourse;
import com.ruoyi.system.model.TCourseOrder;
import com.ruoyi.system.model.TEmploymentInfo;
import com.ruoyi.system.query.CourseOrderListQuery;
import com.ruoyi.system.query.GetCourseListQuery;
import com.ruoyi.system.service.TAppUserService;
import com.ruoyi.system.service.TCourseOrderService;
import com.ruoyi.system.service.TCourseService;
import com.ruoyi.system.service.TEmploymentInfoService;
import com.ruoyi.system.task.base.QuartzManager;
import com.ruoyi.system.task.base.TimeJobType;
import com.ruoyi.system.task.jobs.OrderJob;
import com.ruoyi.system.wxPay.model.WeixinPayProperties;
import com.ruoyi.system.wxPay.model.WxPaymentRefundModel;
import com.ruoyi.system.wxPay.utils.WxRefundUtil;
import com.ruoyi.system.wxPay.utils.WxV3Pay;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -165,6 +162,10 @@
//            return R.fail("无权限操作");
//        }
        courseOrder1.setOrderNumber(OrderNos.getDid());
        courseOrder1.setRealName(courseOrder.getRealName());
        courseOrder1.setUserPhone(courseOrder.getUserPhone());
        courseOrder1.setIdCard(courseOrder.getIdCard());
        courseOrderService.updateById(courseOrder1);
        Map<String, Object> result = wxV3Pay.jsApi(courseOrder1.getOrderNumber(), courseOrder1.getPayMoney().multiply(BigDecimal.valueOf(100)).longValue(), "oz71-1_OevadwmESB_IFAW7PucWY","购买课程","");
        return R.ok(result);
@@ -178,7 +179,7 @@
        TCourseOrder courseOrder1 = courseOrderService.getById(courseOrder.getId());
        courseOrder1.setRefundReason(courseOrder.getRefundReason());
        courseOrder1.setRefundReasonPictures(courseOrder.getRefundReasonPictures());
        courseOrder1.setPayStatus(6);
        courseOrder1.setRequestRefundTime(LocalDateTime.now());
        courseOrder1.setRefundMoney(courseOrder1.getPayMoney());
@@ -188,6 +189,7 @@
        if(courseOrder1.getPayStatus()!=2){
            return R.fail("状态不允许退款");
        }
        courseOrder1.setPayStatus(6);
        LocalDateTime payTime = courseOrder1.getPayTime();
        if(payTime==null){
            return R.fail("状态不允许退款");
@@ -199,19 +201,29 @@
            courseOrder1.setRefundStatus(1);
            courseOrderService.updateById(courseOrder1);
        }else {
            WxPaymentRefundModel refundModel = new WxPaymentRefundModel();
            refundModel.setSub_mchid(weixinPayProperties.getMchId());
            refundModel.setSp_appid(weixinPayProperties.getAppId());
            refundModel.setTransaction_id(courseOrder1.getTransactionId());
            refundModel.setOut_trade_no(courseOrder1.getOrderNumber());
            refundModel.setOut_refund_no(OrderNos.getDid());
            refundModel.setReason(courseOrder1.getRefundReason());
            refundModel.setNotify_url("");
            refundModel.setFunds_account("AVAILABLE");
            refundModel.setAmount(new WxPaymentRefundModel.RefundAmount(courseOrder1.getPayMoney().multiply(BigDecimal.valueOf(100)).intValue(), "CNY", courseOrder1.getPayMoney().multiply(BigDecimal.valueOf(100)).intValue()));
            Map<String, Object> refund = wxV3Pay.refund(refundModel);
            String outRefundNo = OrderNos.getDid();
            courseOrder1.setRefundNumber(outRefundNo);
            courseOrderService.updateById(courseOrder1);
//            WxPaymentRefundModel refundModel = new WxPaymentRefundModel();
////            refundModel.setSub_mchid(weixinPayProperties.getMchId());
////            refundModel.setSp_appid(weixinPayProperties.getAppId());
////            refundModel.setTransaction_id(courseOrder1.getTransactionId());
//            refundModel.setOut_trade_no(courseOrder1.getOrderNumber());
//            refundModel.setOut_refund_no(OrderNos.getDid());
//            refundModel.setReason(courseOrder1.getRefundReason());
//            refundModel.setGoods_detail(new ArrayList<>());
//            refundModel.setAmount(new WxPaymentRefundModel.RefundAmount(courseOrder1.getPayMoney().multiply(BigDecimal.valueOf(100)).intValue(), "CNY", courseOrder1.getPayMoney().multiply(BigDecimal.valueOf(100)).intValue()));
//            Map<String, Object> refund = wxV3Pay.refund(refundModel);
//            System.out.println(refund);
            String result = wxRefundUtil.refund(courseOrder1.getPayMoney().multiply(BigDecimal.valueOf(100)).intValue(), courseOrder1.getOrderNumber(),outRefundNo);
            if(result!=null){
                return R.fail(result);
            }
        }
        return R.ok();
    }
    @Autowired
    private WxRefundUtil wxRefundUtil;
}
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/WxPayController.java
@@ -127,8 +127,25 @@
    @PostMapping("refund/notify")
    public void refundNotify(HttpServletRequest request) throws IOException {
        try {
            Map<String, Object> params = wxV3Pay.verifyNotify(request, new TypeReference<Map<String, Object>>() {
            });
            System.err.println("微信退款回调");
            BufferedReader reader = request.getReader();
            StringBuilder requestBody = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                requestBody.append(line);
            }
            System.err.println("全部请求体" + requestBody);
            JSONObject jsonObject = JSONObject.parseObject(requestBody.toString());
            JSONObject resource = jsonObject.getJSONObject("resource");
            AesUtil aesUtil = new AesUtil(weixinPayProperties.getV3().getApiKey().getBytes(StandardCharsets.UTF_8));
            String decryptedData = aesUtil.decryptToString(resource.getString("associated_data").getBytes(StandardCharsets.UTF_8), resource.getString("nonce").getBytes(StandardCharsets.UTF_8),
                    resource.getString("ciphertext"));
            System.err.println("微信解密的字符串信息" + decryptedData);
            JSONObject params = JSONObject.parseObject(decryptedData);
            // 商户订单号
            String out_trade_no = params.get("out_trade_no").toString();
            // 商户退款单号
ruoyi-applet/src/main/resources/application-test.yml
@@ -215,6 +215,6 @@
      # 证书序列号
      mchSerialNo: 5ED4C99FE5657DA2579BE5E7AE4255C9468627D9
      # 支付成功回调地址
      notifyPayUrl: http://221.182.45.100:9100/api/wx/pay/notify
      notifyPayUrl: http://221.182.45.100:9100/wx/pay/notify
      # 支付退款回调地址
      notifyRefundUrl: http://21.182.45.100:9100/api/wx/refund/notify
      notifyRefundUrl: http://221.182.45.100:9100/wx/refund/notify
ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
@@ -131,7 +131,7 @@
                        "/t-large-user/pageListLarge","/t-large-volunteer/pageListLarge",
                        "/pension/getAssistedDiningList",
                        "/pension/getAssistedDiningDetail",
                        "/statistics/**","/course/**"
                        "/statistics/**","/course/**","/wx/**"
                ).permitAll()
                // 静态资源,可匿名访问
                .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentRefundModel.java
@@ -2,6 +2,7 @@
import lombok.*;
import java.util.ArrayList;
import java.util.List;
/**
@@ -19,12 +20,12 @@
    /**
     * 子商户,二级商户号
     */
    private String sub_mchid;
    /**
     * 电商平台APPID
     */
    private String sp_appid;
    private String transaction_id;
//    private String sub_mchid;
//    /**
//     * 电商平台APPID
//     */
//    private String sp_appid;
//    private String transaction_id;
    private String out_trade_no;
    /**
     * 商户退款单号
@@ -38,7 +39,7 @@
    /**
     * 资金账户,否
     */
    private String funds_account;
//    private String funds_account;
    /**
     * 退款金额信息
     */
@@ -46,7 +47,7 @@
    /**
     * 退款商品
     */
    private List<RefundGoodsDetail> goods_detail;
    private List<RefundGoodsDetail> goods_detail=new ArrayList<>();
    @Builder
    @AllArgsConstructor
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxPayConfig.java
New file
@@ -0,0 +1,13 @@
package com.ruoyi.system.wxPay.utils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class WxPayConfig {
    @Bean
    public WxRefundUtil wxRefundUtil() {
        return new WxRefundUtil();
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxRefundUtil.java
New file
@@ -0,0 +1,354 @@
package com.ruoyi.system.wxPay.utils;
import com.alibaba.fastjson2.JSONObject;
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import com.ruoyi.common.utils.OrderNos;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * 退款申请
 */
//@Component
public class WxRefundUtil {
  private static String HOST = "https://api.mch.weixin.qq.com";
  private static String METHOD = "POST";
  private static String PATH = "/v3/refund/domestic/refunds";
  @Autowired
  private WxV3Pay wxV3Pay;
  public WxRefundUtil() {
  }
  public WxRefundUtil(String mchid, String certificateSerialNo, PrivateKey privateKey, String wechatPayPublicKeyId, PublicKey wechatPayPublicKey) {
    this.mchid = mchid;
    this.certificateSerialNo = certificateSerialNo;
    this.privateKey = privateKey;
    this.wechatPayPublicKeyId = wechatPayPublicKeyId;
    this.wechatPayPublicKey = wechatPayPublicKey;
  }
  public String refund(int money,String outTradeNo,String outRefundNo) {
    // TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
    WxRefundUtil client = new WxRefundUtil(
           this. wxV3Pay.getConfig().getMchId(),                    // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
            this. wxV3Pay.getConfig().getV3().getMchSerialNo(),         // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
            this. wxV3Pay.getConfig().getV3().getPrivateKeyPath() ,     // 商户API证书私钥文件路径,本地文件路径
            this. wxV3Pay.getConfig().getV3().getPublicKeyId() ,      // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
            this. wxV3Pay.getConfig().getV3().getPublicKeyPath()         // 微信支付公钥文件路径,本地文件路径
    );
    CreateRequest request = new CreateRequest();
    request.outTradeNo = outTradeNo;
    request.reason = "退款";
    request.notifyUrl =this.wxV3Pay.getConfig().getV3().getNotifyRefundUrl();
    request.fundsAccount = ReqFundsAccount.AVAILABLE;
    request.amount = new AmountReq();
    request.amount.refund = money;
    request.outRefundNo =outRefundNo;
    request.amount.total = money;
    request.amount.currency = "CNY";
    request.goodsDetail = new ArrayList<>();
    try {
      Refund response = client.run(request);
        // TODO: 请求成功,继续业务逻辑
        System.out.println(response);
        return null;
    } catch (WXPayUtility.ApiException e) {
      String body = e.getBody();
      JSONObject jsonObject = JSONObject.parseObject(body);
      String result = jsonObject.get("message").toString();
      return result;
      // TODO: 请求失败,根据状态码执行不同的逻辑
    }
  }
  public Refund run(CreateRequest request) {
    String uri = PATH;
    String reqBody = WXPayUtility.toJson(request);
    Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
    reqBuilder.addHeader("Accept", "application/json");
    reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
    reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
    reqBuilder.addHeader("Content-Type", "application/json");
    RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
    reqBuilder.method(METHOD, requestBody);
    Request httpRequest = reqBuilder.build();
    // 发送HTTP请求
    OkHttpClient client = new OkHttpClient.Builder().build();
    try (Response httpResponse = client.newCall(httpRequest).execute()) {
      String respBody = WXPayUtility.extractBody(httpResponse);
      if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
        // 2XX 成功,验证应答签名
        WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
            httpResponse.headers(), respBody);
        // 从HTTP应答报文构建返回数据
        return WXPayUtility.fromJson(respBody, Refund.class);
      } else {
        throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
      }
    } catch (IOException e) {
      throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
    }
  }
  private  String mchid;
  private  String certificateSerialNo;
  private  PrivateKey privateKey;
  private  String wechatPayPublicKeyId;
  private  PublicKey wechatPayPublicKey;
  public WxRefundUtil(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
    this.mchid = mchid;
    this.certificateSerialNo = certificateSerialNo;
    this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
    this.wechatPayPublicKeyId = wechatPayPublicKeyId;
    this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
  }
  public static class CreateRequest {
    @SerializedName("out_trade_no")
    public String outTradeNo;
    @SerializedName("out_refund_no")
    public String outRefundNo;
    @SerializedName("reason")
    public String reason;
    @SerializedName("notify_url")
    public String notifyUrl;
    @SerializedName("funds_account")
    public ReqFundsAccount fundsAccount;
    @SerializedName("amount")
    public AmountReq amount;
    @SerializedName("goods_detail")
    public List<GoodsDetail> goodsDetail;
  }
  public static class Refund {
    @SerializedName("refund_id")
    public String refundId;
    @SerializedName("out_refund_no")
    public String outRefundNo;
    @SerializedName("transaction_id")
    public String transactionId;
    @SerializedName("out_trade_no")
    public String outTradeNo;
    @SerializedName("channel")
    public Channel channel;
    @SerializedName("user_received_account")
    public String userReceivedAccount;
    @SerializedName("success_time")
    public String successTime;
    @SerializedName("create_time")
    public String createTime;
    @SerializedName("status")
    public Status status;
    @SerializedName("funds_account")
    public FundsAccount fundsAccount;
    @SerializedName("amount")
    public Amount amount;
    @SerializedName("promotion_detail")
    public List<Promotion> promotionDetail;
  }
  public enum ReqFundsAccount {
    @SerializedName("AVAILABLE")
    AVAILABLE,
    @SerializedName("UNSETTLED")
    UNSETTLED
  }
  public static class AmountReq {
    @SerializedName("refund")
    public Integer refund;
    @SerializedName("from")
    public List<FundsFromItem> from;
    @SerializedName("total")
    public Integer total;
    @SerializedName("currency")
    public String currency;
  }
  public static class GoodsDetail {
    @SerializedName("merchant_goods_id")
    public String merchantGoodsId;
    @SerializedName("wechatpay_goods_id")
    public String wechatpayGoodsId;
    @SerializedName("goods_name")
    public String goodsName;
    @SerializedName("unit_price")
    public Long unitPrice;
    @SerializedName("refund_amount")
    public Long refundAmount;
    @SerializedName("refund_quantity")
    public Long refundQuantity;
  }
  public enum Channel {
    @SerializedName("ORIGINAL")
    ORIGINAL,
    @SerializedName("BALANCE")
    BALANCE,
    @SerializedName("OTHER_BALANCE")
    OTHER_BALANCE,
    @SerializedName("OTHER_BANKCARD")
    OTHER_BANKCARD
  }
  public enum Status {
    @SerializedName("SUCCESS")
    SUCCESS,
    @SerializedName("CLOSED")
    CLOSED,
    @SerializedName("PROCESSING")
    PROCESSING,
    @SerializedName("ABNORMAL")
    ABNORMAL
  }
  public enum FundsAccount {
    @SerializedName("UNSETTLED")
    UNSETTLED,
    @SerializedName("AVAILABLE")
    AVAILABLE,
    @SerializedName("UNAVAILABLE")
    UNAVAILABLE,
    @SerializedName("OPERATION")
    OPERATION,
    @SerializedName("BASIC")
    BASIC,
    @SerializedName("ECNY_BASIC")
    ECNY_BASIC
  }
  public static class Amount {
    @SerializedName("total")
    public Long total;
    @SerializedName("refund")
    public Long refund;
    @SerializedName("from")
    public List<FundsFromItem> from;
    @SerializedName("payer_total")
    public Long payerTotal;
    @SerializedName("payer_refund")
    public Long payerRefund;
    @SerializedName("settlement_refund")
    public Long settlementRefund;
    @SerializedName("settlement_total")
    public Long settlementTotal;
    @SerializedName("discount_refund")
    public Long discountRefund;
    @SerializedName("currency")
    public String currency;
    @SerializedName("refund_fee")
    public Long refundFee;
  }
  public static class Promotion {
    @SerializedName("promotion_id")
    public String promotionId;
    @SerializedName("scope")
    public PromotionScope scope;
    @SerializedName("type")
    public PromotionType type;
    @SerializedName("amount")
    public Long amount;
    @SerializedName("refund_amount")
    public Long refundAmount;
    @SerializedName("goods_detail")
    public List<GoodsDetail> goodsDetail;
  }
  public static class FundsFromItem {
    @SerializedName("account")
    public Account account;
    @SerializedName("amount")
    public Integer amount;
  }
  public enum PromotionScope {
    @SerializedName("GLOBAL")
    GLOBAL,
    @SerializedName("SINGLE")
    SINGLE
  }
  public enum PromotionType {
    @SerializedName("CASH")
    CASH,
    @SerializedName("NOCASH")
    NOCASH
  }
  public enum Account {
    @SerializedName("AVAILABLE")
    AVAILABLE,
    @SerializedName("UNAVAILABLE")
    UNAVAILABLE
  }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxV3Pay.java
@@ -31,6 +31,7 @@
    private WeixinPayProperties config;
    @Getter
    private Verifier verifier;
    private WechatPayHttpClientBuilder builder;
    @Getter
    private CloseableHttpClient httpClient;
@@ -188,7 +189,7 @@
     */
    @Override
    public <T> T verifyNotify(HttpServletRequest request, TypeReference<T> valueTypeRef) throws Exception {
        return verifyNotify(request, this.verifier, this.config.getV3().getApiKey(), valueTypeRef);
        return verifyNotify(request,verifier, this.config.getV3().getApiKey(), valueTypeRef);
    }
    /**
@@ -215,8 +216,8 @@
     */
    @Override
    public Map<String, Object> refund(WxPaymentRefundModel refundModel) {
        refundModel.setNotify_url(this.config.getV3().getNotifyRefundUrl() + refundModel.getNotify_url());
        return refund(this.httpClient, "/v3/refund/domestic/refunds", this.config.getHttpReadTimeoutMs(), this.config.getHttpConnectTimeoutMs(), refundModel);
        refundModel.setNotify_url(this.config.getV3().getNotifyRefundUrl());
        return  refund(this.httpClient, "/v3/refund/domestic/refunds", this.config.getHttpReadTimeoutMs(), this.config.getHttpConnectTimeoutMs(), refundModel);
    }