路径调整
This commit is contained in:
parent
08931d5892
commit
a31dd4b74b
@ -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>
|
||||
|
@ -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<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<>();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
// }
|
||||
//}
|
594
ruoyi-admin/src/main/java/com/ruoyi/api/util/TransferToUser.java
Normal file
594
ruoyi-admin/src/main/java/com/ruoyi/api/util/TransferToUser.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -78,6 +78,10 @@ public class WechatPayConfig {
|
||||
* 微信支付V3-url前缀
|
||||
*/
|
||||
private String baseUrl;
|
||||
private String mchKey;
|
||||
private String keyPath;
|
||||
private String privateCertPath;
|
||||
private String zhuanNotifyUrl;
|
||||
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user