| ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/CourseController.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/WxPayController.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| ruoyi-applet/src/main/resources/application-test.yml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentRefundModel.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxPayConfig.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxRefundUtil.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxV3Pay.java | ●●●●● 补丁 | 查看 | 原始文档 | 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); }