From a31dd4b74b0b47c408029757b10989e1eb555c4f Mon Sep 17 00:00:00 2001
From: 13405411873 <1994398261@qq.com>
Date: Mon, 28 Apr 2025 19:23:49 +0800
Subject: [PATCH] =?UTF-8?q?=E8=B7=AF=E5=BE=84=E8=B0=83=E6=95=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ruoyi-admin/pom.xml | 19 +
.../src/main/java/com/ruoyi/api/PayApi.java | 110 +++-
.../com/ruoyi/api/util/ShopBeanConfig.java | 43 ++
.../com/ruoyi/api/util/TransferToUser.java | 594 ++++++++++++++++++
.../controller/MemberPointsController.java | 5 +-
.../service/impl/MemberPointsServiceImpl.java | 3 +-
.../com/ruoyi/payConfig/WechatPayConfig.java | 4 +
7 files changed, 771 insertions(+), 7 deletions(-)
create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/api/util/ShopBeanConfig.java
create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/api/util/TransferToUser.java
diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml
index 4333424..d000cc5 100644
--- a/ruoyi-admin/pom.xml
+++ b/ruoyi-admin/pom.xml
@@ -28,11 +28,30 @@
wechatpay-apache-httpclient
0.4.7
+
+ com.github.binarywang
+ weixin-java-pay
+ 4.7.2.B
+
com.github.wechatpay-apiv3
wechatpay-java
0.2.17
+
+
+ com.squareup.okhttp3
+ okhttp
+ 4.10.0
+
+
+
+
+ com.squareup.okio
+ okio
+ 3.0.0
+
+
org.springframework.boot
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/api/PayApi.java b/ruoyi-admin/src/main/java/com/ruoyi/api/PayApi.java
index 9e8ba68..41dceb5 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/api/PayApi.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/api/PayApi.java
@@ -5,18 +5,31 @@ import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.TypeReference;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.github.binarywang.wxpay.bean.transfer.TransferBillsRequest;
+import com.github.binarywang.wxpay.bean.transfer.TransferBillsResult;
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
+import com.ruoyi.api.util.TransferToUser;
import com.ruoyi.common.annotation.Anonymous;
+import com.ruoyi.common.config.WxAppConfig;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.member.domain.MemberOrder;
+import com.ruoyi.member.domain.MemberPoints;
import com.ruoyi.member.service.IMemberOrderService;
+import com.ruoyi.member.service.IMemberPointsService;
import com.ruoyi.payConfig.WechatPayConfig;
+import com.ruoyi.system.service.ISysUserService;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import com.wechat.pay.java.core.Config;
+import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.RSAPublicKeyConfig;
+import com.wechat.pay.java.core.http.*;
import com.wechat.pay.java.service.payments.jsapi.JsapiService;
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
import com.wechat.pay.java.service.payments.jsapi.model.*;
@@ -24,6 +37,7 @@ import com.wechat.pay.java.service.payments.nativepay.NativePayService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.RequestBody;
import javax.annotation.Resource;
import java.io.IOException;
@@ -31,9 +45,7 @@ import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.security.*;
-import java.util.Base64;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.*;
@RestController
@RequestMapping("/payApi")
@@ -43,9 +55,18 @@ public class PayApi {
@Autowired
private IMemberOrderService memberOrderService;
+ @Autowired
+ private TransferToUser transferToUser;
+ @Autowired
+ private IMemberPointsService pointsService;
+ @Autowired
+ private ISysUserService sysUserService;
+ @Autowired
+ private WxAppConfig appConfig;
+
/**
* type:h5、jsapi、app、native、sub_jsapi
- * @param type
+
* @return
*/
@ApiOperation(value = "统一下单-统一接口", notes = "统一下单-统一接口")
@@ -88,6 +109,55 @@ public class PayApi {
return service.prepayWithRequestPayment(request);
}
+
+
+
+
+ @RequestMapping("/entPay")
+ @ApiOperation(value = "商家转账给用户")
+ public JSONObject entPay(String orderNo){
+ LambdaQueryWrapper queryWrapper =new LambdaQueryWrapper<>();
+ queryWrapper.eq(MemberPoints::getOrderNo,orderNo).last("limit 1");
+ MemberPoints memberPoints = pointsService.getOne(queryWrapper);
+ if (!memberPoints.getUserId().equals(SecurityUtils.getUserId())){
+ throw new RuntimeException("非本人账户");
+ }
+ SysUser sysUser = sysUserService.selectUserById(memberPoints.getUserId());
+ // 初始化商户配置
+ TransferToUser.TransferToUserRequest userRequest = new TransferToUser.TransferToUserRequest();
+ userRequest.appid = appConfig.getAppId();
+ userRequest.openid=sysUser.getOpenId();
+ // 将元转换为分
+ double pointsInYuan = memberPoints.getPoints();
+ userRequest.transferAmount = BigDecimal.valueOf(pointsInYuan).multiply(BigDecimal.valueOf(100)).setScale(0, RoundingMode.HALF_UP).longValue();
+
+ userRequest.notifyUrl = wechatPayConfig.getZhuanNotifyUrl();
+ userRequest.outBillNo = orderNo;
+ userRequest.transferSceneId = "1000";
+ userRequest.transferRemark = "通告快接积分提现";
+ List transferSceneReportInfos = new ArrayList<>();
+ TransferToUser.TransferSceneReportInfo transferSceneReportInfo = new TransferToUser.TransferSceneReportInfo();
+ transferSceneReportInfo.infoContent = "发布通告得积分";
+ transferSceneReportInfo.infoType = "活动名称";
+ TransferToUser.TransferSceneReportInfo transferSceneReportInfo2 = new TransferToUser.TransferSceneReportInfo();
+ transferSceneReportInfo2.infoContent = "发布通告得积分";
+ transferSceneReportInfo2.infoType = "奖励说明";
+ transferSceneReportInfos.add(transferSceneReportInfo);
+ transferSceneReportInfos.add(transferSceneReportInfo2);
+ userRequest.transferSceneReportInfos = transferSceneReportInfos;
+ TransferToUser.TransferToUserResponse run = transferToUser.run(userRequest);
+ JSONObject res = new JSONObject();
+ res.put("runData",run);
+ JSONObject config = new JSONObject();
+ config.put("appId",appConfig.getAppId());
+ config.put("mchId",wechatPayConfig.getMchId());
+ res.put("config",config);
+ return res;
+
+ }
+
+
+
@ApiOperation(value = "支付回调", notes = "支付回调")
@PostMapping("/payNotify")
@Anonymous
@@ -110,6 +180,38 @@ public class PayApi {
}
+ @ApiOperation(value = "提现支付回调", notes = "提现支付回调")
+ @PostMapping("/zhuanNotify")
+ @Anonymous
+ public Map zhuanNotify(@RequestBody JSONObject jsonObject) throws GeneralSecurityException, IOException {
+ String key = wechatPayConfig.getApiV3Key();
+ String json = jsonObject.toString();
+ String associated_data = (String) JSONUtil.getByPath(JSONUtil.parse(json), "resource.associated_data");
+ String ciphertext = (String) JSONUtil.getByPath(JSONUtil.parse(json), "resource.ciphertext");
+ String nonce = (String) JSONUtil.getByPath(JSONUtil.parse(json), "resource.nonce");
+ String decryptData = new AesUtil(key.getBytes(StandardCharsets.UTF_8)).decryptToString(associated_data.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext);
+ //验签成功
+ JSONObject decryptDataObj = JSONObject.parseObject(decryptData, JSONObject.class);
+ String orderNo = decryptDataObj.get("out_bill_no").toString();
+ String state = decryptDataObj.get("state").toString();
+ //调用业务系统
+ if(state.equals("SUCCESS")){
+ pointsService.payoutCallback(orderNo,"success");
+ Map res = new HashMap<>();
+ res.put("code", "SUCCESS");
+ res.put("message", "成功");
+ return res;
+ } else if (state.equals("FAIL")) {
+ pointsService.payoutCallback(orderNo,"fail");
+
+ Map res = new HashMap<>();
+ res.put("code", "SUCCESS");
+ res.put("message", "成功");
+ return res;
+ }
+ return new HashMap<>();
+
+ }
}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/api/util/ShopBeanConfig.java b/ruoyi-admin/src/main/java/com/ruoyi/api/util/ShopBeanConfig.java
new file mode 100644
index 0000000..7088a60
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/api/util/ShopBeanConfig.java
@@ -0,0 +1,43 @@
+//package com.ruoyi.api.util;
+//
+//import com.github.binarywang.wxpay.config.WxPayConfig;
+//import com.github.binarywang.wxpay.service.WxPayService;
+//import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
+//import com.ruoyi.common.utils.StringUtils;
+//import com.ruoyi.payConfig.WechatPayConfig;
+//import lombok.AllArgsConstructor;
+//import org.springframework.beans.factory.annotation.Autowired;
+//import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+//import org.springframework.context.annotation.Bean;
+//import org.springframework.context.annotation.Configuration;
+//
+//@Configuration
+//@AllArgsConstructor
+//@ConditionalOnClass(WxPayService.class)
+//public class ShopBeanConfig {
+// @Autowired
+// private WechatPayConfig wechatPayConfig;
+//
+//
+//
+// @Bean
+// public WxPayService wxService() {
+// WxPayConfig payConfig = new WxPayConfig();
+// payConfig.setAppId(StringUtils.trimToNull(wechatPayConfig.getAppId()));
+// payConfig.setMchId(StringUtils.trimToNull(wechatPayConfig.getMchId()));
+// payConfig.setMchKey(StringUtils.trimToNull(wechatPayConfig.getMchKey()));
+// payConfig.setKeyPath(StringUtils.trimToNull(wechatPayConfig.getKeyPath()));
+// payConfig.setApiV3Key(StringUtils.trimToNull(wechatPayConfig.getApiV3Key()));
+// payConfig.setCertSerialNo(StringUtils.trimToNull(wechatPayConfig.getSerialNo()));
+// payConfig.setPrivateKeyPath(StringUtils.trimToNull(wechatPayConfig.getPrivateKeyPath()));
+// payConfig.setPrivateCertPath(StringUtils.trimToNull(wechatPayConfig.getPrivateCertPath()));
+// payConfig.setNotifyUrl(StringUtils.trimToNull(wechatPayConfig.getZhuanNotifyUrl()));
+//
+// // 可以指定是否使用沙箱环境
+// payConfig.setUseSandboxEnv(false);
+//
+// WxPayService wxPayService = new WxPayServiceImpl();
+// wxPayService.setConfig(payConfig);
+// return wxPayService;
+// }
+//}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/api/util/TransferToUser.java b/ruoyi-admin/src/main/java/com/ruoyi/api/util/TransferToUser.java
new file mode 100644
index 0000000..8e69646
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/api/util/TransferToUser.java
@@ -0,0 +1,594 @@
+package com.ruoyi.api.util;
+
+
+import com.google.gson.ExclusionStrategy;
+import com.google.gson.FieldAttributes;
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.internal.Primitives;
+import java.io.IOException;
+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.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.Signature;
+import java.security.SignatureException;
+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.Base64;
+import java.util.List;
+import java.util.Objects;
+import javax.annotation.Resource;
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+
+import com.ruoyi.payConfig.WechatPayConfig;
+import okhttp3.Headers;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okio.BufferedSource;
+import org.springframework.stereotype.Component;
+
+/**
+ * 发起转账
+ */
+@Component
+public class TransferToUser {
+ private static final String host = "https://api.mch.weixin.qq.com";
+ private static final String path = "/v3/fund-app/mch-transfer/transfer-bills";
+ private static final String method = "POST";
+
+
+
+
+ @Resource
+ private WechatPayConfig wechatPayConfig;
+
+
+
+ /**
+ * https://pay.weixin.qq.com/doc/v3/merchant/4012716434
+ *
+ * 商家转账用户确认模式下,用户申请收款时,商户可通过此接口申请创建转账单
+ *
+ * * 接口返回的HTTP状态码及错误码,仅代表本次请求的结果,不能代表订单状态。
+ * * 接口返回的HTTP状态码为200,且赔付状态为ACCEPT时,可认为发起商家转账成功。
+ * * **接口返回的HTTP状态码不为200时,请商户务必不要立即更换商户订单单号重试**。可根据错误码列表中的描述和接口返回的信息进行处理,并在查询原订单结果为失败或者联系客服确认情况后,再更换商户订单号进行重试。否则会有重复转账的资金风险。
+ *
+ * 注:单个商户的接口频率限制为100次/s
+ *
+ * @return TransferToUserResponse
+ */
+ public TransferToUserResponse run(TransferToUserRequest request) {
+ // 请求参数验证
+
+
+ // 构造HTTP请求
+ String uri = path;
+ String body = this.createRequestBody(request);
+
+ // 发送HTTP请求
+ try (Response httpResponse = this.sendHttpRequest(uri, body)) {
+ // 应答结果检查
+ String respBody = validateResponse(httpResponse);
+
+ // 从HTTP应答报文构建返回数据
+ return parseResponse(respBody);
+ } catch (IOException e) {
+ throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
+ }
+ }
+
+ /**
+ * 商家转账-发起转账-验证请求参数
+ *
+ * 参数列表:
+ * - appid string(32): [必填] **【商户AppID】** 是微信开放平台和微信公众平台为开发者的应用程序(APP、小程序、公众号、企业号corpid即为此AppID)提供的一个唯一标识。此处,可以填写这四种类型中的任意一种APPID,但请确保该appid与商户号有绑定关系。详见:[普通商户模式开发必要参数说明](https://iwiki.woa.com/p/4013070756)。
+ * - out_bill_no string(32): [必填] **【商户单号】** 商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一
+ * - transfer_scene_id string(36): [必填] **【转账场景ID】** 该笔转账使用的转账场景,可前往“商户平台-产品中心-商家转账”中申请。如:1001-现金营销
+ * - openid string(64): [必填] **【收款用户OpenID】** 用户在商户appid下的唯一标识。发起转账前需获取到用户的OpenID,获取方式详见 [参数说明](https://iwiki.woa.com/p/4012068676)。
+ * - user_name string: [选填] **【收款用户姓名】** 收款方真实姓名。需要加密传入,支持标准RSA算法和国密算法,公钥由微信侧提供。
转账金额 >= 2,000元时,该笔明细必须填写
若商户传入收款用户姓名,微信支付会校验收款用户与输入姓名是否一致,并提供电子回单
+ * - transfer_amount integer: [必填] **【转账金额】** 转账金额单位为“分”。
+ * - transfer_remark string(32): [必填] **【转账备注】** 转账备注,用户收款时可见该备注信息,UTF8编码,最多允许32个字符
+ * - notify_url string(256): [选填] **【通知地址】** 异步接收微信支付结果通知的回调地址,通知url必须为公网可访问的URL,必须为HTTPS,不能携带参数。
+ * - user_recv_perception string: [选填] **【用户收款感知】** 用户收款时感知到的收款原因将根据转账场景自动展示默认内容。如有其他展示需求,可在本字段传入。各场景展示的默认内容和支持传入的内容,可查看产品文档了解。
+ * - transfer_scene_report_infos array[TransferSceneReportInfo]: [选填] **【转账场景报备信息】** 各转账场景下需报备的内容,商户需要按照所属转账场景规则传参,详见[转账场景报备信息字段说明](https://iwiki.woa.com/p/4013774588)。
+ * - transfer_scene_report_infos.info_type string(15): [必填] **【信息类型】** 不能超过15个字符,商户所属转账场景下的信息类型,此字段内容为固定值,需严格按照 [转账场景报备信息字段说明](https://iwiki.woa.com/p/4013774588) 传参。
+ * - transfer_scene_report_infos.info_content string(32): [必填] **【信息内容】** 不能超过32个字符,商户所属转账场景下的信息内容,商户可按实际业务场景自定义传参,需严格按照 [转账场景报备信息字段说明](https://iwiki.woa.com/p/4013774588) 传参。
+ *
+ * 请根据上述字段列表提供的参数的必要性、最长长度限制,并根据描述中所述的关联,编写参数检查逻辑,每个字段以驼峰命名Public访问,无需Getter。
+ * @param request 请求参数
+ */
+ private void validateRequestParameters(TransferToUserRequest request) {
+ // 商家转账-发起转账-验证请求参数 生成示例,需选中该注释,通过 @demo 指令触发
+ throw new UnsupportedOperationException("Method not implemented yet");
+ }
+
+ /**
+ * 商家转账-发起转账-构造HTTP请求包体
+ *
+ * 包体参数列表:
+ * - appid string(32): [必填] **【商户AppID】** 是微信开放平台和微信公众平台为开发者的应用程序(APP、小程序、公众号、企业号corpid即为此AppID)提供的一个唯一标识。此处,可以填写这四种类型中的任意一种APPID,但请确保该appid与商户号有绑定关系。详见:[普通商户模式开发必要参数说明](https://iwiki.woa.com/p/4013070756)。
+ * - out_bill_no string(32): [必填] **【商户单号】** 商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一
+ * - transfer_scene_id string(36): [必填] **【转账场景ID】** 该笔转账使用的转账场景,可前往“商户平台-产品中心-商家转账”中申请。如:1001-现金营销
+ * - openid string(64): [必填] **【收款用户OpenID】** 用户在商户appid下的唯一标识。发起转账前需获取到用户的OpenID,获取方式详见 [参数说明](https://iwiki.woa.com/p/4012068676)。
+ * - user_name string: [选填] **【收款用户姓名】** 收款方真实姓名。需要加密传入,支持标准RSA算法和国密算法,公钥由微信侧提供。
转账金额 >= 2,000元时,该笔明细必须填写
若商户传入收款用户姓名,微信支付会校验收款用户与输入姓名是否一致,并提供电子回单
+ * - transfer_amount integer: [必填] **【转账金额】** 转账金额单位为“分”。
+ * - transfer_remark string(32): [必填] **【转账备注】** 转账备注,用户收款时可见该备注信息,UTF8编码,最多允许32个字符
+ * - notify_url string(256): [选填] **【通知地址】** 异步接收微信支付结果通知的回调地址,通知url必须为公网可访问的URL,必须为HTTPS,不能携带参数。
+ * - user_recv_perception string: [选填] **【用户收款感知】** 用户收款时感知到的收款原因将根据转账场景自动展示默认内容。如有其他展示需求,可在本字段传入。各场景展示的默认内容和支持传入的内容,可查看产品文档了解。
+ * - transfer_scene_report_infos array[TransferSceneReportInfo]: [选填] **【转账场景报备信息】** 各转账场景下需报备的内容,商户需要按照所属转账场景规则传参,详见[转账场景报备信息字段说明](https://iwiki.woa.com/p/4013774588)。
+ * - transfer_scene_report_infos.info_type string(15): [必填] **【信息类型】** 不能超过15个字符,商户所属转账场景下的信息类型,此字段内容为固定值,需严格按照 [转账场景报备信息字段说明](https://iwiki.woa.com/p/4013774588) 传参。
+ * - transfer_scene_report_infos.info_content string(32): [必填] **【信息内容】** 不能超过32个字符,商户所属转账场景下的信息内容,商户可按实际业务场景自定义传参,需严格按照 [转账场景报备信息字段说明](https://iwiki.woa.com/p/4013774588) 传参。
+ *
+ * 请参考上述字段列表从 request 中构造 TransferToUserRequest 类型,并根据描述对特定字段进行加密,加密函数使用 Utility.encrypt(this.wechatpayPublicKey, text)
+ * @return HTTP 请求报文String
+ */
+ private String createRequestBody(TransferToUserRequest request) {
+ TransferToUserRequest body = new TransferToUserRequest();
+ body.appid = request.appid;
+ body.outBillNo = request.outBillNo;
+ body.transferSceneId = request.transferSceneId;
+ body.openid = request.openid;
+ if (request.userName != null) {
+ body.userName = request.userName; // TODO: 需要加密
+ }
+ body.transferAmount = request.transferAmount;
+ body.transferRemark = request.transferRemark;
+ if (request.notifyUrl != null) {
+ body.notifyUrl = request.notifyUrl;
+ }
+ if (request.userRecvPerception != null) {
+ body.userRecvPerception = request.userRecvPerception;
+ }
+ if (request.transferSceneReportInfos != null) {
+ body.transferSceneReportInfos = request.transferSceneReportInfos;
+ }
+ return Utility.toJson(body);
+ }
+
+ private String buildAuthorization(String method, String uri, String body) {
+ return Utility.buildAuthorization(wechatPayConfig.getMchId(), wechatPayConfig.getSerialNo(), Utility.loadPrivateKeyFromPath(wechatPayConfig.getPrivateKeyPath()), method, uri, body);
+ }
+
+ private Response sendHttpRequest(String uri, String body) throws IOException {
+ Request.Builder builder = new Request.Builder().url(host + uri);
+ builder.addHeader("Accept", "application/json");
+ builder.addHeader("Wechatpay-Serial", wechatPayConfig.getPublicKeyId());
+ builder.addHeader("Authorization", buildAuthorization(method, uri, body));
+ builder.addHeader("Content-Type", "application/json");
+ RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), body);
+ builder.method(method, requestBody);
+
+ Request request = builder.build();
+ OkHttpClient client = new OkHttpClient.Builder().build();
+ return client.newCall(request).execute();
+ }
+
+ private String validateResponse(Response response) {
+ String body = "";
+ if (response.body() != null) {
+ try {
+ BufferedSource source = response.body().source();
+ body = source.readUtf8();
+ } catch (IOException e) {
+ throw new RuntimeException(String.format("An error occurred during reading response body. Status: %d", response.code()), e);
+ }
+ }
+
+ if (response.code() >= 200 && response.code() < 300) {
+ // 2XX 成功,继续验证应答签名
+ Headers headers = response.headers();
+ Utility.validateResponse(wechatPayConfig.getPublicKeyId(),Utility.loadPublicKeyFromPath(wechatPayConfig.getPublicKeyPath()) , headers, body);
+ return body;
+ }
+
+ // TODO: 根据错误码执行不同的处理
+ throw new UnsupportedOperationException(String.format("接口请求错误,StatusCode: %d, Body: %s",
+ response.code(), body));
+ }
+ /**
+ * 商家转账-发起转账-解析回包结果
+ * - out_bill_no string(32): [必填] **【商户单号】** 商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一
+ * - transfer_bill_no string(64): [必填] **【微信转账单号】** 微信转账单号,微信商家转账系统返回的唯一标识
+ * - create_time string: [必填] **【单据创建时间】** 单据受理成功时返回,按照使用rfc3339所定义的格式,格式为yyyy-MM-DDThh:mm:ss+TIMEZONE
+ * - state string: [必填] **【单据状态】** 商家转账订单状态
+ * - fail_reason string: [选填] **【失败原因】** 订单已失败或者已退资金时,会返回 [订单失败原因](https://iwiki.woa.com/p/4013774966)
+ * - package_info string: [选填] **【跳转领取页面的package信息】** 跳转微信支付收款页的package信息,[APP调起用户确认收款](https://iwiki.woa.com/p/4012719576) 或者 [JSAPI调起用户确认收款](https://iwiki.woa.com/p/4012716430) 时需要使用的参数。
单据创建后,**用户24小时内不领取将过期关闭**,建议拉起用户确认收款页面前,先查单据状态:如单据状态为待收款用户确认,可用之前的package信息拉起;单据到终态时需更换单号重新发起转账。
+ *
+ * @return TransferToUserResponse
+ */
+ private TransferToUserResponse parseResponse(String body) {
+ // 商家转账-发起转账-解析回包结果 生成示例,需选中该注释,通过 @demo 指令触发
+ TransferToUserResponse response = Utility.fromJson(body, TransferToUserResponse.class);
+ return response;
+ }
+
+ static class Utility {
+ 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();
+
+ public static String toJson(Object object) { return gson.toJson(object); }
+
+ public static T fromJson(String json, Class classOfT) throws JsonSyntaxException {
+ return gson.fromJson(json, classOfT);
+ }
+
+ 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);
+ }
+ }
+
+ 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);
+ }
+ }
+
+ public static PrivateKey loadPrivateKeyFromPath(String keyPath) {
+ return loadPrivateKeyFromString(readKeyStringFromPath(keyPath));
+ }
+
+ 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);
+ }
+ }
+
+ public static PublicKey loadPublicKeyFromPath(String keyPath) {
+ return loadPublicKeyFromString(readKeyStringFromPath(keyPath));
+ }
+
+ 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);
+ }
+
+ 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 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);
+ }
+
+ 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);
+ }
+ }
+
+ 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);
+ }
+
+ public static String urlEncode(String content) {
+ try {
+ return URLEncoder.encode(content, StandardCharsets.UTF_8.name());
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void validateResponse(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 http response,timestamp[%s] of httpResponse is expires, "
+ + "request-id[%s]",
+ timestamp, headers.get("Request-ID")));
+ }
+ } catch (DateTimeException | NumberFormatException e) {
+ throw new IllegalArgumentException(
+ String.format("Validate http response,timestamp[%s] of httpResponse is invalid, request-id[%s]", timestamp,
+ headers.get("Request-ID")));
+ }
+ String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"), body == null ? "" : body);
+ String serialNumber = headers.get("Wechatpay-Serial");
+ if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) {
+ throw new IllegalArgumentException(
+ String.format("Invalid Wechatpay-Serial, Local: %s, Remote: %s", wechatpayPublicKeyId, serialNumber));
+ }
+ String signature = headers.get("Wechatpay-Signature");
+
+ 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));
+ }
+ }
+ }
+
+ /**
+ * TransferToUserResponse
+ */
+ public static class TransferToUserResponse {
+
+ /**
+ * 商户单号
+ * 说明:商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一
+ */
+ @SerializedName("out_bill_no")
+ public String outBillNo;
+
+ /**
+ * 微信转账单号
+ * 说明:微信转账单号,微信商家转账系统返回的唯一标识
+ */
+ @SerializedName("transfer_bill_no")
+ public String transferBillNo;
+
+ /**
+ * 单据创建时间
+ * 说明:单据受理成功时返回,按照使用rfc3339所定义的格式,格式为yyyy-MM-DDThh:mm:ss+TIMEZONE
+ */
+ @SerializedName("create_time")
+ public String createTime;
+
+ /**
+ * 单据状态
+ * 说明:商家转账订单状态
+ */
+ @SerializedName("state")
+ public TransferBillStatus state;
+
+ /**
+ * 失败原因
+ * 说明:订单已失败或者已退资金时,会返回 [订单失败原因](https://iwiki.woa.com/p/4013774966)
+ */
+ @SerializedName("fail_reason")
+ public String failReason;
+
+ /**
+ * 跳转领取页面的package信息
+ * 说明:跳转微信支付收款页的package信息,[APP调起用户确认收款](https://iwiki.woa.com/p/4012719576) 或者 [JSAPI调起用户确认收款](https://iwiki.woa.com/p/4012716430) 时需要使用的参数。 单据创建后,**用户24小时内不领取将过期关闭**,建议拉起用户确认收款页面前,先查单据状态:如单据状态为待收款用户确认,可用之前的package信息拉起;单据到终态时需更换单号重新发起转账。
+ */
+ @SerializedName("package_info")
+ public String packageInfo;
+ }
+ /**
+ * TransferBillStatus
+ */
+ public enum TransferBillStatus {
+
+ @SerializedName("ACCEPTED")
+ ACCEPTED,
+
+ @SerializedName("PROCESSING")
+ PROCESSING,
+
+ @SerializedName("WAIT_USER_CONFIRM")
+ WAIT_USER_CONFIRM,
+
+ @SerializedName("TRANSFERING")
+ TRANSFERING,
+
+ @SerializedName("SUCCESS")
+ SUCCESS,
+
+ @SerializedName("FAIL")
+ FAIL,
+
+ @SerializedName("CANCELING")
+ CANCELING,
+
+ @SerializedName("CANCELLED")
+ CANCELLED
+ }
+
+ /**
+ * TransferSceneReportInfo
+ */
+ public static class TransferSceneReportInfo {
+
+ /**
+ * 信息类型
+ * 说明:不能超过15个字符,商户所属转账场景下的信息类型,此字段内容为固定值,需严格按照 [转账场景报备信息字段说明](https://iwiki.woa.com/p/4013774588) 传参。
+ */
+ @SerializedName("info_type")
+ public String infoType;
+
+ /**
+ * 信息内容
+ * 说明:不能超过32个字符,商户所属转账场景下的信息内容,商户可按实际业务场景自定义传参,需严格按照 [转账场景报备信息字段说明](https://iwiki.woa.com/p/4013774588) 传参。
+ */
+ @SerializedName("info_content")
+ public String infoContent;
+ }
+
+ /**
+ * TransferToUserRequest
+ */
+ public static class TransferToUserRequest {
+
+ /**
+ * 商户AppID
+ * 说明:是微信开放平台和微信公众平台为开发者的应用程序(APP、小程序、公众号、企业号corpid即为此AppID)提供的一个唯一标识。此处,可以填写这四种类型中的任意一种APPID,但请确保该appid与商户号有绑定关系。详见:[普通商户模式开发必要参数说明](https://iwiki.woa.com/p/4013070756)。
+ */
+ @SerializedName("appid")
+ public String appid;
+
+ /**
+ * 商户单号
+ * 说明:商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一
+ */
+ @SerializedName("out_bill_no")
+ public String outBillNo;
+
+ /**
+ * 转账场景ID
+ * 说明:该笔转账使用的转账场景,可前往“商户平台-产品中心-商家转账”中申请。如:1001-现金营销
+ */
+ @SerializedName("transfer_scene_id")
+ public String transferSceneId;
+
+ /**
+ * 收款用户OpenID
+ * 说明:用户在商户appid下的唯一标识。发起转账前需获取到用户的OpenID,获取方式详见 [参数说明](https://iwiki.woa.com/p/4012068676)。
+ */
+ @SerializedName("openid")
+ public String openid;
+
+ /**
+ * 收款用户姓名
+ * 说明:收款方真实姓名。需要加密传入,支持标准RSA算法和国密算法,公钥由微信侧提供。 转账金额 >= 2,000元时,该笔明细必须填写 若商户传入收款用户姓名,微信支付会校验收款用户与输入姓名是否一致,并提供电子回单
+ */
+ @SerializedName("user_name")
+ public String userName;
+
+ /**
+ * 转账金额
+ * 说明:转账金额单位为“分”。
+ */
+ @SerializedName("transfer_amount")
+ public Long transferAmount;
+
+ /**
+ * 转账备注
+ * 说明:转账备注,用户收款时可见该备注信息,UTF8编码,最多允许32个字符
+ */
+ @SerializedName("transfer_remark")
+ public String transferRemark;
+
+ /**
+ * 通知地址
+ * 说明:异步接收微信支付结果通知的回调地址,通知url必须为公网可访问的URL,必须为HTTPS,不能携带参数。
+ */
+ @SerializedName("notify_url")
+ public String notifyUrl;
+
+ /**
+ * 用户收款感知
+ * 说明:用户收款时感知到的收款原因将根据转账场景自动展示默认内容。如有其他展示需求,可在本字段传入。各场景展示的默认内容和支持传入的内容,可查看产品文档了解。
+ */
+ @SerializedName("user_recv_perception")
+ public String userRecvPerception;
+
+ /**
+ * 转账场景报备信息
+ * 说明:各转账场景下需报备的内容,商户需要按照所属转账场景规则传参,详见[转账场景报备信息字段说明](https://iwiki.woa.com/p/4013774588)。
+ */
+ @SerializedName("transfer_scene_report_infos")
+ public List transferSceneReportInfos;
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/member/controller/MemberPointsController.java b/ruoyi-admin/src/main/java/com/ruoyi/member/controller/MemberPointsController.java
index 2b8176a..fe10f6f 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/member/controller/MemberPointsController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/member/controller/MemberPointsController.java
@@ -1,5 +1,6 @@
package com.ruoyi.member.controller;
+import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.annotation.Log;
@@ -75,10 +76,12 @@ public class MemberPointsController extends BaseController {
public AjaxResult payout(@RequestBody MemberPoints memberPoints) {
try {
memberPointsService.payout(memberPoints);
+ JSONObject res =new JSONObject();
+ res.put("orderNo",memberPoints.getOrderNo());
+ return success(res);
}catch (Exception e) {
return error(e.getMessage());
}
- return success();
}
/**
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/member/service/impl/MemberPointsServiceImpl.java b/ruoyi-admin/src/main/java/com/ruoyi/member/service/impl/MemberPointsServiceImpl.java
index 2176ca2..ea9e52e 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/member/service/impl/MemberPointsServiceImpl.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/member/service/impl/MemberPointsServiceImpl.java
@@ -100,7 +100,6 @@ public class MemberPointsServiceImpl extends ServiceImpl list = list(lambdaQueryWrapper);
if (list.isEmpty()){
@@ -203,6 +202,6 @@ public class MemberPointsServiceImpl extends ServiceImpl