路径调整

This commit is contained in:
13405411873 2025-04-28 19:23:49 +08:00
parent 08931d5892
commit a31dd4b74b
7 changed files with 771 additions and 7 deletions

View File

@ -28,11 +28,30 @@
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.7</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>4.7.2.B</version>
</dependency>
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.17</version>
</dependency>
<!-- OkHttp 核心依赖 -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.10.0</version> <!-- 请替换为最新版本号 -->
</dependency>
<!-- Okio 是 OkHttp 的底层依赖,通常会自动引入,但可以显式声明 -->
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>3.0.0</version> <!-- 请替换为最新版本号 -->
</dependency>
<!-- spring-boot-devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@ -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;
/**
* typeh5jsapiappnativesub_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<MemberPoints> 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<TransferToUser.TransferSceneReportInfo> 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<String, String> 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<String, String> res = new HashMap<>();
res.put("code", "SUCCESS");
res.put("message", "成功");
return res;
} else if (state.equals("FAIL")) {
pointsService.payoutCallback(orderNo,"fail");
Map<String, String> res = new HashMap<>();
res.put("code", "SUCCESS");
res.put("message", "成功");
return res;
}
return new HashMap<>();
}
}

View File

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

View File

@ -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算法和国密算法公钥由微信侧提供<br/>转账金额 >= 2,000元时该笔明细必须填写<br/>若商户传入收款用户姓名微信支付会校验收款用户与输入姓名是否一致并提供电子回单
* - 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算法和国密算法公钥由微信侧提供<br/>转账金额 >= 2,000元时该笔明细必须填写<br/>若商户传入收款用户姓名微信支付会校验收款用户与输入姓名是否一致并提供电子回单
* - 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) 时需要使用的参数<br/>单据创建后**用户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> T fromJson(String json, Class<T> 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<TransferSceneReportInfo> transferSceneReportInfos;
}
}

View File

@ -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();
}
/**

View File

@ -100,7 +100,6 @@ public class MemberPointsServiceImpl extends ServiceImpl<MemberPointsMapper, Mem
lambdaQueryWrapper
.eq(MemberPoints::getUserId,userId)
.eq(MemberPoints::getStatus,"01")
.eq(DlBaseEntity::getDelFlag,0)
.orderByDesc(DlBaseEntity::getCreateTime);
List<MemberPoints> list = list(lambdaQueryWrapper);
if (list.isEmpty()){
@ -203,6 +202,6 @@ public class MemberPointsServiceImpl extends ServiceImpl<MemberPointsMapper, Mem
// 生成范围在100到999之间的随机数
int randomNum = random.nextInt(900) + 100;
// 组合成订单编号
return "JFTX-" + timestamp + String.format("%03d", randomNum);
return "JFTX" + timestamp + String.format("%03d", randomNum);
}
}

View File

@ -78,6 +78,10 @@ public class WechatPayConfig {
* 微信支付V3-url前缀
*/
private String baseUrl;
private String mchKey;
private String keyPath;
private String privateCertPath;
private String zhuanNotifyUrl;
}