Browse Source

1、提现接口处理

master
Aug 3 weeks ago
parent
commit
5200100186
6 changed files with 774 additions and 61 deletions
  1. +199
    -28
      module-common/src/main/java/org/jeecg/api/service/impl/AppletMoneyLogServiceImpl.java
  2. +148
    -0
      module-common/src/main/java/org/jeecg/api/transfer/TransferToUser.java
  3. +381
    -0
      module-common/src/main/java/org/jeecg/api/transfer/WXPayUtility.java
  4. +26
    -26
      module-system/src/main/resources/apiclient_key.pem
  5. +11
    -7
      module-system/src/main/resources/application-dev.yml
  6. +9
    -0
      module-system/src/main/resources/pub_key.pem

+ 199
- 28
module-common/src/main/java/org/jeecg/api/service/impl/AppletMoneyLogServiceImpl.java View File

@ -1,10 +1,14 @@
package org.jeecg.api.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.map.HashedMap;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.api.bean.PageBean;
import org.jeecg.api.service.AppletMoneyLogService;
import org.jeecg.api.transfer.TransferToUser;
import org.jeecg.common.api.vo.Result;
import org.jeecg.config.shiro.ShiroRealm;
import org.jeecg.modules.commonBook.entity.CommonBook;
@ -12,14 +16,38 @@ import org.jeecg.modules.commonMoneyLog.entity.CommonMoneyLog;
import org.jeecg.modules.commonMoneyLog.service.ICommonMoneyLogService;
import org.jeecg.modules.hanHaiMember.entity.HanHaiMember;
import org.jeecg.modules.hanHaiMember.service.IHanHaiMemberService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Map;
@Slf4j
@Service
public class AppletMoneyLogServiceImpl implements AppletMoneyLogService {
/*************************************************************************************/
//微信小程序的 AppID
@Value("${wechat.mpAppId}")
private String appid;
//商户号
@Value("${wechat.merchantId}")
private String mchid;
//商户API私钥路径
@Value("${wechat.privateKeyPath}")
private String privateKeyFilePath;
//商户API公钥路径
@Value("${wechat.publicKeyPath}")
private String wechatPayPublicKeyFilePath;
//商户API公钥ID
@Value("${wechat.publicKeyId}")
private String wechatPayPublicKeyId;
//商户证书序列号
@Value("${wechat.merchantSerialNumber}")
private String certiticateSerialNo;
//权限
@Resource
private ShiroRealm shiroRealm;
@ -27,6 +55,7 @@ public class AppletMoneyLogServiceImpl implements AppletMoneyLogService {
private ICommonMoneyLogService commonMoneyLogService;
@Resource
private IHanHaiMemberService hanHaiMemberService;
/*************************************************************************************/
@ -45,40 +74,182 @@ public class AppletMoneyLogServiceImpl implements AppletMoneyLogService {
}
@Override
public Result<?> withdraw(String token, CommonMoneyLog log) {
public Result<?> withdraw(String token, CommonMoneyLog commonMoneyLog) {
//权限验证
HanHaiMember member = shiroRealm.checkUserTokenIsEffectHanHaiOpenId(token);
//HanHaiMember member = hanHaiMemberService.getById("1928663563891728385");
// 检查用户余额是否足够
if (member.getMoney() == null || member.getMoney().compareTo(log.getMoney()) < 0) {
return Result.error("余额不足,无法提现");
}
//提现结果
String massage = "提现申请失败";
try{
//0基础约束
BigDecimal balance = member.getMoney();//用户余额
//金额不能为空
if(null == commonMoneyLog.getMoney()){
log.info("金额为空,请填写大于0的整数金额");
return Result.error("金额小于0,请填写大于0的整数金额");
}
if(commonMoneyLog.getMoney().compareTo(balance)>0){
//提现金额大于推广佣金
log.info("推广佣金不足,当前推广佣金:{}", balance);
return Result.error("推广佣金不足");
}
//提现金额要为整数
if(commonMoneyLog.getMoney().scale()>0){
log.info("请填写大于0的整数金额,当前输入金额:{}", commonMoneyLog.getMoney());
return Result.error("请填写大于0的整数金额");
}
//提现金额大于0的整数
if(commonMoneyLog.getMoney().compareTo(new BigDecimal(0))<=0){
log.info("请填写大于0的整数金额,当前输入金额:{}", commonMoneyLog.getMoney());
return Result.error("请填写大于0的整数金额");
}
if(org.apache.commons.lang.StringUtils.isEmpty(commonMoneyLog.getUserName())){
log.info("用户姓名未填写");
return Result.error("用户姓名未填写");
}
//1.微信提现基础参数
Map map = getMap();
//变化的用户信息参数
map.put("openid", member.getAppletOpenid());//用户openid(小程序)
map.put("userName", commonMoneyLog.getUserName());//用户真实姓名
map.put("transferAmount", commonMoneyLog.getMoney());//提现金额, 单位为
String idStr = "H" + IdWorker.getIdStr();
map.put("outBillNo", idStr);//商户单号
TransferToUser client = new TransferToUser(
map.get("mchid").toString(), // 商户号是由微信支付系统生成并分配给每个商户的唯一标识符商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
map.get("certiticateSerialNo").toString(), // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
map.get("privateKeyFilePath").toString(), // 商户API证书私钥文件路径本地文件路径
map.get("wechatPayPublicKeyId").toString(), // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
map.get("wechatPayPublicKeyFilePath").toString() // 微信支付公钥文件路径本地文件路径
);
//2场景信息
TransferToUser.TransferToUserRequest request = new TransferToUser.TransferToUserRequest();
request.appid = map.get("appid").toString();
request.outBillNo = map.get("outBillNo").toString();
request.transferSceneId = map.get("transferSceneId").toString();
request.openid = map.get("openid").toString();
request.userName = client.encrypt(map.get("userName").toString());
request.transferAmount = commonMoneyLog.getMoney().longValue()*100;//单位为分
request.transferRemark = map.get("transferRemark").toString();
request.notifyUrl = map.get("notifyUrl").toString();
request.userRecvPerception = map.get("userRecvPerception").toString();
request.transferSceneReportInfos = new ArrayList<>();
{
TransferToUser.TransferSceneReportInfo item0 = new TransferToUser.TransferSceneReportInfo();
item0.infoType = map.get("infoType1").toString();
item0.infoContent = map.get("infoContent1").toString();
request.transferSceneReportInfos.add(item0);
}
//3执行提现
TransferToUser.TransferToUserResponse response = client.run(request, map);
log.info("提现发起成功,outBillNo:"+response.outBillNo + ",transferBillNo:" +response.transferBillNo + ",state:" +response.state+ ",packageInfo:" + response.packageInfo);
switch (response.state){
case ACCEPTED:
log.info("转账已受理");
massage = "转账已受理";
break;
case PROCESSING:
log.info("转账锁定资金中。如果一直停留在该状态,建议检查账户余额是否足够,如余额不足,可充值后再原单重试");
massage = "转账锁定资金中。如果一直停留在该状态,建议检查账户余额是否足够,如余额不足,可充值后再原单重试";
break;
case WAIT_USER_CONFIRM:
log.info("待收款用户确认,可拉起微信收款确认页面进行收款确认");
massage = "待收款用户确认,可拉起微信收款确认页面进行收款确认";
break;
case TRANSFERING:
log.info("转账中,可拉起微信收款确认页面再次重试确认收款");
massage = "转账中,可拉起微信收款确认页面再次重试确认收款";
break;
case SUCCESS:
log.info("转账成功");
massage = "转账成功";
break;
case FAIL:
log.info("转账失败");
massage = "转账失败";
break;
case CANCELING:
log.info("商户撤销请求受理成功,该笔转账正在撤销中");
massage = "商户撤销请求受理成功,该笔转账正在撤销中";
break;
case CANCELLED:
log.info("转账撤销完成");
massage = "转账撤销完成";
break;
}
log.info("提现结果:" + massage);
//4业务处理
// 设置提现记录信息
commonMoneyLog.setTitle("提现记录");
commonMoneyLog.setUserId(member.getId());
commonMoneyLog.setType(1); // 支出类型
commonMoneyLog.setState(0); // 待处理状态
commonMoneyLog.setIsBrokerage("N"); // 不是佣金
commonMoneyLog.setOldMoney(member.getMoney()); // 记录原始余额
// 更新用户余额
BigDecimal newMoney = member.getMoney().subtract(commonMoneyLog.getMoney());
member.setMoney(newMoney);
// 更新累计提现金额
if (member.getIntegerPrice() == null) {
member.setIntegerPrice(commonMoneyLog.getMoney());
} else {
member.setIntegerPrice(member.getIntegerPrice().add(commonMoneyLog.getMoney()));
}
// 保存用户信息和提现记录
hanHaiMemberService.updateById(member);
commonMoneyLogService.save(commonMoneyLog);
//5返回信息
return Result.OK(massage, response);
}catch (Exception e){
e.printStackTrace();
log.info("提现失败:" + e.getMessage());
return Result.error(e.getMessage());
// 设置提现记录信息
log.setTitle("提现记录");
log.setUserId(member.getId());
log.setType(1); // 支出类型
log.setState(0); // 待处理状态
log.setIsBrokerage("N"); // 不是佣金
log.setOldMoney(member.getMoney()); // 记录原始余额
// 更新用户余额
BigDecimal newMoney = member.getMoney().subtract(log.getMoney());
member.setMoney(newMoney);
// 更新累计提现金额
if (member.getIntegerPrice() == null) {
member.setIntegerPrice(log.getMoney());
} else {
member.setIntegerPrice(member.getIntegerPrice().add(log.getMoney()));
}
// 保存用户信息和提现记录
hanHaiMemberService.updateById(member);
commonMoneyLogService.save(log);
}
/**
* 微信提现基础参数
* @return
*/
public Map getMap(){
Map<String, Object> map = new HashedMap();//转账接口所需参数
map.put("host", "https://api.mch.weixin.qq.com");//请求地址
map.put("method", "POST");//请求类型
map.put("path", "/v3/fund-app/mch-transfer/transfer-bills");//提现接口
map.put("notifyUrl", "https://www.yurangongfang.com/massage-admin/massage/cash/cashoutNotify/");//回调接口
//微信商户参数
map.put("appid", appid);//小程序appid
map.put("mchid", mchid);//商户号
map.put("certiticateSerialNo", certiticateSerialNo);//商户序列号
map.put("privateKeyFilePath", privateKeyFilePath);//商户私钥证书
map.put("wechatPayPublicKeyId", wechatPayPublicKeyId);//商户公钥id
map.put("wechatPayPublicKeyFilePath", wechatPayPublicKeyFilePath);//商户公钥证书
map.put("transferSceneId", "1010");//商户转账场景ID 1010-二手回收
map.put("transferRemark", "二手衣物回收");//商户转账场景ID 1010-佣金报酬
map.put("userRecvPerception", "");//商户转账场景ID 1010-佣金报酬
//转账场景报备信息ransfer_scene_report_infos为数组类型参数
map.put("infoType1","回收商品名称");
map.put("infoContent1","衣服");
// TODO 后续对接微信转账
return map;
return Result.OK("提现申请已提交");
}
@Override


+ 148
- 0
module-common/src/main/java/org/jeecg/api/transfer/TransferToUser.java View File

@ -0,0 +1,148 @@
package org.jeecg.api.transfer;
import com.google.gson.annotations.SerializedName;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.List;
import java.util.Map;
/**
* 发起转账
*/
@Slf4j
public class TransferToUser {
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public TransferToUser(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public TransferToUserResponse run(TransferToUserRequest request, Map map) {
String uri = map.get("path").toString();
String host = map.get("host").toString();
String method = map.get("method").toString();
String reqBody = WXPayUtility.toJson(request);
Request.Builder reqBuilder = new Request.Builder().url(host + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, method, uri, reqBody));
reqBuilder.addHeader("Content-Type", "application/json");
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
reqBuilder.method(method, requestBody);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, TransferToUserResponse.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
public String encrypt(String plainText) {
return WXPayUtility.encrypt(this.wechatPayPublicKey, plainText);
}
public static class TransferToUserResponse {
@SerializedName("out_bill_no")
public String outBillNo;
@SerializedName("transfer_bill_no")
public String transferBillNo;
@SerializedName("create_time")
public String createTime;
@SerializedName("state")
public TransferBillStatus state;
@SerializedName("package_info")
public String packageInfo;
}
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
}
public static class TransferSceneReportInfo {
@SerializedName("info_type")
public String infoType;
@SerializedName("info_content")
public String infoContent;
}
public static class TransferToUserRequest {
@SerializedName("appid")
public String appid;
@SerializedName("out_bill_no")
public String outBillNo;
@SerializedName("transfer_scene_id")
public String transferSceneId;
@SerializedName("openid")
public String openid;
@SerializedName("user_name")
public String userName;
@SerializedName("transfer_amount")
public Long transferAmount;
@SerializedName("transfer_remark")
public String transferRemark;
@SerializedName("notify_url")
public String notifyUrl;
@SerializedName("user_recv_perception")
public String userRecvPerception;
@SerializedName("transfer_scene_report_infos")
public List<TransferSceneReportInfo> transferSceneReportInfos;
}
}

+ 381
- 0
module-common/src/main/java/org/jeecg/api/transfer/WXPayUtility.java View File

@ -0,0 +1,381 @@
package org.jeecg.api.transfer;
import com.google.gson.*;
import com.google.gson.annotations.Expose;
import okhttp3.Headers;
import okhttp3.Response;
import okio.BufferedSource;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
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.*;
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.Map;
import java.util.Objects;
public class WXPayUtility {
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();
/**
* Object 转换为 JSON 字符串
*/
public static String toJson(Object object) {
return gson.toJson(object);
}
/**
* JSON 字符串解析为特定类型的实例
*/
public static <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
return gson.fromJson(json, classOfT);
}
/**
* 从公私钥文件路径中读取文件内容
*
* @param keyPath 文件路径
* @return 文件内容
*/
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);
}
}
/**
* 读取 PKCS#8 格式的私钥字符串并加载为私钥对象
*
* @param keyString 私钥文件内容 -----BEGIN PRIVATE KEY----- 开头
* @return PrivateKey 对象
*/
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);
}
}
/**
* PKCS#8 格式的私钥文件中加载私钥
*
* @param keyPath 私钥文件路径
* @return PrivateKey 对象
*/
public static PrivateKey loadPrivateKeyFromPath(String keyPath) {
return loadPrivateKeyFromString(readKeyStringFromPath(keyPath));
}
/**
* 读取 PKCS#8 格式的公钥字符串并加载为公钥对象
*
* @param keyString 公钥文件内容 -----BEGIN PUBLIC KEY----- 开头
* @return PublicKey 对象
*/
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);
}
}
/**
* PKCS#8 格式的公钥文件中加载公钥
*
* @param keyPath 公钥文件路径
* @return PublicKey 对象
*/
public static PublicKey loadPublicKeyFromPath(String keyPath) {
return loadPublicKeyFromString(readKeyStringFromPath(keyPath));
}
/**
* 创建指定长度的随机字符串字符集为[0-9a-zA-Z]可用于安全相关用途
*/
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);
}
/**
* 使用公钥按照 RSA_PKCS1_OAEP_PADDING 算法进行加密
*
* @param publicKey 加密用公钥对象
* @param plaintext 待加密明文
* @return 加密后密文
*/
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);
}
}
/**
* 使用私钥按照指定算法进行签名
*
* @param message 待签名串
* @param algorithm 签名算法 SHA256withRSA
* @param privateKey 签名用私钥对象
* @return 签名结果
*/
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);
}
/**
* 使用公钥按照特定算法验证签名
*
* @param message 待签名串
* @param signature 待验证的签名内容
* @param algorithm 签名算法SHA256withRSA
* @param publicKey 验签用公钥对象
* @return 签名验证是否通过
*/
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);
}
}
/**
* 根据微信支付APIv3请求签名规则构造 Authorization 签名
*
* @param mchid 商户号
* @param certificateSerialNo 商户API证书序列号
* @param privateKey 商户API证书私钥
* @param method 请求接口的HTTP方法请使用全大写表述 GETPOSTPUTDELETE
* @param uri 请求接口的URL
* @param body 请求接口的Body
* @return 构造好的微信支付APIv3 Authorization
*/
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);
}
/**
* 对参数进行 URL 编码
*
* @param content 参数内容
* @return 编码后的内容
*/
public static String urlEncode(String content) {
try {
return URLEncoder.encode(content, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* 对参数Map进行 URL 编码生成 QueryString
*
* @param params Query参数Map
* @return QueryString
*/
public static String urlEncode(Map<String, Object> params) {
if (params == null || params.isEmpty()) {
return "";
}
int index = 0;
StringBuilder result = new StringBuilder();
for (Map.Entry<String, Object> entry : params.entrySet()) {
result.append(entry.getKey())
.append("=")
.append(urlEncode(entry.getValue().toString()));
index++;
if (index < params.size()) {
result.append("&");
}
}
return result.toString();
}
/**
* 从应答中提取 Body
*
* @param response HTTP 请求应答对象
* @return 应答中的Body内容Body为空时返回空字符串
*/
public static String extractBody(Response response) {
if (response.body() == null) {
return "";
}
try {
BufferedSource source = response.body().source();
return source.readUtf8();
} catch (IOException e) {
throw new RuntimeException(String.format("An error occurred during reading response body. Status: %d", response.code()), e);
}
}
/**
* 根据微信支付APIv3应答验签规则对应答签名进行验证验证不通过时抛出异常
*
* @param wechatpayPublicKeyId 微信支付公钥ID
* @param wechatpayPublicKey 微信支付公钥对象
* @param headers 微信支付应答 Header 列表
* @param body 微信支付应答 Body
*/
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));
}
}
/**
* 微信支付API错误异常发送HTTP请求成功但返回状态码不是 2XX 时抛出本异常
*/
public static class ApiException extends RuntimeException {
public final int statusCode;
public final String body;
public final Headers headers;
public ApiException(int statusCode, String body, Headers headers) {
super(String.format("微信支付API访问失败,StatusCode: %s, Body: %s", statusCode, body));
this.statusCode = statusCode;
this.body = body;
this.headers = headers;
}
}
}

+ 26
- 26
module-system/src/main/resources/apiclient_key.pem View File

@ -1,28 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDlD79uHUwjr8r
hSlk7gyCvmWAOH9w1nvl6vjStN0UEbGkPYEtZhAKpykwK5uPV0CXHJI8a7Ilv+qr
C0Y1m+MQ63SZ8AHb+e5wd+iGkXSXX4T/y2jBHBGVxJkwPB7bN7nh7UYq4OZ1PB5x
KXvYwYgrTtmDKxMrwbaodJryLbi6/WzC4QcKLSQTwJ6O9ehHVrUWbBXizYKZX/QH
j9BQY73GtfNhj/ppVVh48dPkfYy20pGS5tOP6VDHslNQe1FHsULz2QKr9fqUcGKl
9ChnpbcUAkDtHzlI2NDYiVsidRJZjajJA4zpYyn3Sb9dl6VG/BsVgLTASPDoh8li
nXKksmuBAgMBAAECggEAf4/cee3qeY5RP9hthEgDXu9CEpxG+tjaHL7iJcQTgfh8
bcwzyeGMyvX2VlXK83YMScM32jLAEgEX1RHYbDTNqAZ6mcDB5bEhBLggsEyEyApk
G9aW74UYLx/4bk54LbEuCx6QKn1fss1QaayN+3VXFDAsjHH24g5JzZuoSBbsKwDl
WVP3wHh87x4eiXnDtR9hBG5ul5AXRhuE24ka1cpYE0N3/7cm+D+lNfvXz/A427z9
6+El2sBTutrfIzFnJjz2KUX1RqFHXqBWVwCxUPH0SSp6l59qy/kWOkIW1nlOjo8x
fPoLEd6y6x5YgaNSLG3ur4hhxp7h/2beVqwfmSSxJQKBgQDwuG8LTpB+xDKjmE3C
sX82n1yvE6I0UQo4mc/5LkS/PxsKAE9zo6yX96WsFThY8S43Ep6gg6SdMVGINHBx
vPudiWgIrhGMocJ+9PHapJQBtUAxem64gNVxSXFrfyphjO+GRtM4ji9+71fcUwFD
lS0T0wKm5MVCbSzbUWIdGgWM5wKBgQDP/kqqP/SalLD89dQa4jaJ5jOQJTiYq7lB
9BIsy23dQEv5mNXJS6pcOjayuE1iaiYdA0Ri0Rueu4xZ+hNaAdSHcH1Pgt/0qMI7
+SuaHjPFxffNFtePkmHg6dS9JzXwQ/xub6ZOf+E/qUf1BTv6ja0o69ApWl0RFtAF
xL/Z6kIPVwKBgETsujbllvAFI26+ND2z7vXn6XTjzUTnk2Kjf+4cNmkAG7DgZ993
lPqqWRCNvuWQoSf5t9vD9cVgkrTKNwwKDY2NA3HAzZuT0YnifsGY8BwRFsFUChHg
Kb1XRxd9gNgPr6Gl8+K0q5rP0ztttOXx98c+WvsIdAbSFc7yXYJxqfcvAoGAAorI
HNaVRcJle2IByqZTJlJS9QMPcwY+SGkUQ8nkuNyNUSqmCkTLez8W5g5Mm9RSTO56
Sn7lyIXgTEU7MVFuaI1earddx168qQD9oG+YEGXABpit38pZOeeBuyIcjag3EJ56
uODlPuLxxzPeLMzIfgSL0cWR96CAwGFMOvya/BcCgYBpOOie5LdFOLh0kCRWk0sg
1/pIJYr9hXmG+QEFSELZ56mWl0ujPfZ8g7bh211RZ5FWaddD3ba7nZR6u884bRwN
9Tg/BKz5rXd7FkKtaodxs1/ti1WLGdrC/mnCjIq0LfnVK6Zg5dbOSGdE2qQaVyPY
ZUrwYWW/VDgsAyeMuWWdEQ==
MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQCd78AKWRvl0n0b
BSKQl0lm59RW2grUHewY9CBnKiy6K+h7gZyORuglyeGplq9BTMLYiOvd5AIzoaOF
BM9JY4bp3XPCzquDCTOW6jEz9exn2Shp5pQD/qsZQXlC617ZVEQxW2Y7HRc9yP1x
w/mlbIJA+OwX4S15E7dC9uKQhQ95xZASzIkLdrUQkgAd72Dlq6n7mCcNwvUluLhM
7q0QDrfyIXlZuLVt+J39FlCUd1gR5yve5oQ4NsW407ZXriIo1sCdzhcdPEo+6Bkc
XV6gs4I9e/IEYeQgodi4t6aOBsqzujkyMgzvhV495h+O/GDI1xNmX/U5huXhmqgu
3e8DH+2VAgMBAAECggEBAJyDwum+YyI/lZGYZieqdaiQaGLl1ENfn+Ee1ZnHNuoM
p2j2CmMJK/h+965r0SRCYPnbvvt3gSMPtZAWMyJEiqfquVS+IURLcx/E2Jvow64q
i51fTyIiWAdAVl6WCLoRgz7yl/5PFwA41cnPb/Ca9v5ScQ49LiZF/qBTMDYOV0bR
jyUjkkWXQvLsccY5dwp6nEAU6eQXBxQlyHiwPJaYV8WVep2MexWVXqNg/S8hPPmi
3+Jg6FwWPbbF7482bl+bHxmoUjpVZLqcgpOxymNeY+nHlKT1t8M5rqNTjCVm2d/O
/O4gX1D5cDqPSEydVG08P4uzmCkS2Vc7M83AyKny/eECgYEAyzf1GFRH3vHhsMwu
oaM29p0ZHNRiJ2wcP0gBWxICosiM4Gt0GYroHht1cW1/xPfymxUgfy9PbTN9EMgZ
pplvXZnczht3IzaAb+etONCQEjYYjPaXcCtb29db5xAUHMKEtwtonuyPpmLXcNPu
DbMzJq0pXWkZO4rn3eN/yvgYTZkCgYEAxvT63M21II3Cd2VKddeYL0fl4d6G2xs5
MfwFHnitS7Yoya9p5gyhrrGVtOajJL0pN2fe+bbc/ON/o4rJdt+VXhnbSH/y1xZ2
xMbrOaehdbJCCOArH6ddSqI+WWEPN1Khy46P5C+BdCdKkahEd50e9gWoVaWF8Mat
4/yGJq5dxV0CgYEAj0Rs83D/lkqanSTlvyis6CILAUstw7MxGt/trMG+v/p5bwHy
QMuwJwGew4+54ygWjoTPezrkWQ1Sn5EinFRRsgB2LYwpatbSp5ICiipnV59xoXd7
fiXjRdLHRNgOBdSQ8ecGI/yMrXXDgxS2IEx0xFCR9/vPQS4Fl2X6wyIzKAkCgYEA
suPteIHh/Z5ZH5/M3r52kPS1x2ydlv8YY1A2jhp1v2DPteDqeDYdXoVAlcLXKB6J
o87al2+LrjskNjmBQkhw9oaLO9oH0MvbCL3PHS0TqU8Zvv9I5xJGeOf+5cC2vMio
v+20hbkNThJSIzvILEDzaMTXggLttvv9uMQDH16i4+UCgYEAhrLdn9gA9AFuArYG
Qi82NLJ3QDaT2/RiJ2jhGywqK3Dx0n7fp1uQ21gzdV8d9aT8TL5xLd3eRhFpGRpd
J7uMwadmNIBjBGxWcfPWOcgynAIDvuRweB7HckUjZcNjGnYwkvPrA5+pWQkWi6n6
Y2FtmLanHI/B3S+G851jkXfzzbg=
-----END PRIVATE KEY-----

+ 11
- 7
module-system/src/main/resources/application-dev.yml View File

@ -330,15 +330,19 @@ third-app:
client-secret: ??
agent-id: ??
##配置微信##爱简收旧衣按件回收小程序
wechat:
mpAppId: wx2bd656ae9704dbee
mpAppSecret: b07f82b16e9598bc23de604f49c57e23
mchId:
mchKey:
keyPath:
notifyUrl:
mpAppId: wx2bd656ae9704dbee # 微信小程序appid
mpAppSecret: b07f82b16e9598bc23de604f49c57e23 # 微信小程序密钥
merchantId: 1701841654 # 商户号
privateKeyPath: module-system/src/main/resources/apiclient_key.pem #本地私钥路径
publicKeyPath: module-system/src/main/resources/pub_key.pem #本地公钥路径
# privateKeyPath: /data/app-test/hly/cerFile/apiclient_key.pem #线上私钥路径
# publicKeyPath: /data/app-test/hly/cerFile/pub_key.pem #线上公钥路径
publicKeyId: PUB_KEY_ID_0117018416542025022100395100001649 #公钥
merchantSerialNumber: 525971050851A99F315A970D3055192779652E03 # 商户API证书序列号
apiV3Key: # 商户APIV3密钥
refundNotifyUrl: # 退款通知地址(正式环境)

+ 9
- 0
module-system/src/main/resources/pub_key.pem View File

@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8qX7Ko/dx5/MDNj3wwzc
vqt5cSA85mVQH63vkQm/ffimHGixzwvcyq7QldyaLPLMzCG3jKsVpY7CKXyAANMT
WT4tAj4Kzi55ZkjJXWCH9I3zuLFFA27zrY0xd5PVbzxnNWaMq2S1Lkn+ul9y77Jb
VcI26MXqvFxkjT9Yxg/rEF2sh989/xM7tUpyJg8O8ZyMkmvjtdEmZxcr+SOkbHQz
qcDPIBWWElB5pocMHATvro9G8VHzCZONB7DyQq/w+ck58BU/4s6KGTZ6NIHjS/ex
C24tdJdMRxDcacUmjsH45m+QoG66gfUi8XuNodMl8Xkmbxq93xM03ophFjnVstH6
1wIDAQAB
-----END PUBLIC KEY-----

Loading…
Cancel
Save