Merge branch 'test' into prod
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
package com.czg.system.service;
|
||||
|
||||
import com.czg.exception.CzgException;
|
||||
import com.czg.resp.CzgResult;
|
||||
import com.czg.system.dto.SysParamsDTO;
|
||||
import com.czg.system.entity.SysParams;
|
||||
import com.mybatisflex.core.service.IService;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 服务层。
|
||||
@@ -23,6 +26,7 @@ public interface SysParamsService extends IService<SysParams> {
|
||||
* @return 参数列表
|
||||
*/
|
||||
CzgResult<List<SysParamsDTO>> getParamsByType(Integer type);
|
||||
|
||||
/**
|
||||
* 新增参数
|
||||
*
|
||||
@@ -62,4 +66,15 @@ public interface SysParamsService extends IService<SysParams> {
|
||||
*/
|
||||
String getSysParamValue(String code);
|
||||
|
||||
|
||||
/**
|
||||
* 根据参数类型获取参数
|
||||
* dubbo 调用需要 显式抛出 异常类型
|
||||
*
|
||||
* @param type 参数类型
|
||||
* userMiniKeys 用户小程序参数 appId appSecret
|
||||
* shopMiniKeys 商家小程序参数 appId appSecret
|
||||
* payKeys 微信支付参数
|
||||
*/
|
||||
Map<String, String> getParamsByMap(String type, Set<String> keyList) throws CzgException;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
package com.czg.service.market.service.impl;
|
||||
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.czg.constants.ParamCodeCst;
|
||||
import com.czg.exception.CzgException;
|
||||
import com.czg.service.RedisService;
|
||||
import com.czg.system.service.SysParamsService;
|
||||
import com.ijpay.core.kit.RsaKit;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.dubbo.config.annotation.DubboReference;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 微信支付service
|
||||
* @author Administrator
|
||||
@@ -23,29 +28,59 @@ public class AppWxServiceImpl extends BaseWx {
|
||||
@DubboReference
|
||||
private SysParamsService paramsService;
|
||||
|
||||
private static final Set<String> USER_MINI_KEYS = Set.of(ParamCodeCst.Wechat.Mini.USER_WX_APP_ID, ParamCodeCst.Wechat.Mini.USER_WX_SECRETE);
|
||||
|
||||
public AppWxServiceImpl(@Autowired RedisService autoRedisService) {
|
||||
this.redisService = autoRedisService;
|
||||
config = new Config();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
@Override
|
||||
public String getAccessToken(boolean refresh) {
|
||||
init();
|
||||
Object token = redisService.get("wx:user:access_token");
|
||||
if (!refresh && token instanceof String) {
|
||||
return (String) token;
|
||||
}
|
||||
|
||||
String response = HttpUtil.get(WX_ACCESS_TOKEN_URL,
|
||||
Map.of("grant_type", "client_credential", "appid", config.appId, "secret", config.appSecret)
|
||||
);
|
||||
|
||||
log.info("获取access_token响应: {}", response);
|
||||
JSONObject jsonObject = JSONObject.parseObject(response);
|
||||
String accessToken = jsonObject.getString("access_token");
|
||||
if (accessToken == null) {
|
||||
throw new RuntimeException("获取access_token失败");
|
||||
}
|
||||
Long expiresIn = jsonObject.getLong("expires_in");
|
||||
if (expiresIn == null) {
|
||||
expiresIn = DEFAULT_EXPIRES_IN;
|
||||
}
|
||||
redisService.set("wx:user:access_token", accessToken, expiresIn - EXPIRES_OFFSET);
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() throws CzgException {
|
||||
// 用户小程序参数
|
||||
Map<String, String> userMiniKeyMap = paramsService.getParamsByMap("userMiniKeys", USER_MINI_KEYS);
|
||||
|
||||
// 微信支付参数
|
||||
Map<String, String> payKeyMap = paramsService.getParamsByMap("payKeys", PAY_KEYS);
|
||||
|
||||
// 小程序id
|
||||
config.appId = paramsService.getSysParamValue(ParamCodeCst.Wechat.Mini.USER_WX_APP_ID);
|
||||
log.info("小程序id:{}", config.appId);
|
||||
config.appId = userMiniKeyMap.get(ParamCodeCst.Wechat.Mini.USER_WX_APP_ID);
|
||||
// 小程序secrete
|
||||
config.appSecret = paramsService.getSysParamValue(ParamCodeCst.Wechat.Mini.USER_WX_SECRETE);
|
||||
log.info("小程序secrete:{}", config.appSecret);
|
||||
config.appSecret = userMiniKeyMap.get(ParamCodeCst.Wechat.Mini.USER_WX_SECRETE);
|
||||
|
||||
config.certPath = "";
|
||||
// 微信支付公钥
|
||||
config.pubKey = paramsService.getSysParamValue(ParamCodeCst.Wechat.Pay.WX_PUB_KEY);
|
||||
log.info("微信支付公钥:{}", config.pubKey);
|
||||
config.pubKey = payKeyMap.get(ParamCodeCst.Wechat.Pay.WX_PUB_KEY);
|
||||
// api支付证书私钥
|
||||
config.apiCertKey = paramsService.getSysParamValue(ParamCodeCst.Wechat.Pay.WX_API_CLIENT_KEY);
|
||||
log.info("api支付证书私钥:{}", config.apiCertKey);
|
||||
config.apiCertKey = payKeyMap.get(ParamCodeCst.Wechat.Pay.WX_API_CLIENT_KEY);
|
||||
// api支付证书公钥
|
||||
config.apiCert = paramsService.getSysParamValue(ParamCodeCst.Wechat.Pay.WX_API_CLIENT_CERT);
|
||||
log.info("api支付证书公钥:{}", config.apiCert);
|
||||
config.apiCert = payKeyMap.get(ParamCodeCst.Wechat.Pay.WX_API_CLIENT_CERT);
|
||||
try {
|
||||
config.privateKey = RsaKit.loadPrivateKey(config.apiCertKey);
|
||||
} catch (Exception e) {
|
||||
@@ -56,18 +91,14 @@ public class AppWxServiceImpl extends BaseWx {
|
||||
// 平台证书编号
|
||||
config.platformCertNo = "";
|
||||
// 商户号
|
||||
config.mchId = paramsService.getSysParamValue(ParamCodeCst.Wechat.Pay.WX_MCH_ID);
|
||||
log.info("商户号:{}", config.mchId);
|
||||
config.mchId = payKeyMap.get(ParamCodeCst.Wechat.Pay.WX_MCH_ID);
|
||||
// v3密钥
|
||||
config.apiV3Key = paramsService.getSysParamValue(ParamCodeCst.Wechat.Pay.WX_V3_KEY);
|
||||
log.info("v3密钥:{}", config.apiV3Key);
|
||||
config.apiV3Key = payKeyMap.get(ParamCodeCst.Wechat.Pay.WX_V3_KEY);
|
||||
config.apiV2Key = "";
|
||||
// 回调地址
|
||||
config.notifyUrl = paramsService.getSysParamValue(ParamCodeCst.System.NATIVE_NOTIFY_URL) + "/wx/pay";
|
||||
log.info("回调地址:{}", config.notifyUrl);
|
||||
config.notifyUrl = payKeyMap.get(ParamCodeCst.System.NATIVE_NOTIFY_URL) + "/wx/pay";
|
||||
config.refundNotifyUrl = "";
|
||||
config.transferNotifyUrl = paramsService.getSysParamValue(ParamCodeCst.System.NATIVE_NOTIFY_URL) + "/wx/transfer";
|
||||
log.info("转账回调地址:{}", config.transferNotifyUrl);
|
||||
config.transferNotifyUrl = payKeyMap.get(ParamCodeCst.System.NATIVE_NOTIFY_URL) + "/wx/transfer";
|
||||
}
|
||||
|
||||
public BaseWx getAppService() {
|
||||
|
||||
@@ -5,6 +5,7 @@ import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.czg.constants.ParamCodeCst;
|
||||
import com.czg.exception.CzgException;
|
||||
import com.czg.service.RedisService;
|
||||
import com.ijpay.core.IJPayHttpResponse;
|
||||
@@ -20,8 +21,7 @@ import com.ijpay.wxpay.enums.v3.BasePayApiEnum;
|
||||
import com.ijpay.wxpay.model.v3.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.Data;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import java.io.ByteArrayInputStream;
|
||||
@@ -35,36 +35,61 @@ import java.security.cert.X509Certificate;
|
||||
import java.util.Base64;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 微信支付相关
|
||||
*
|
||||
* @author Administrator
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class BaseWx {
|
||||
public Config config;
|
||||
protected Config config;
|
||||
public RedisService redisService;
|
||||
public Logger log = LoggerFactory.getLogger(BaseWx.class);
|
||||
@Data
|
||||
public static class Config {
|
||||
public String appId;
|
||||
public String appSecret;
|
||||
public String certPath;
|
||||
public String pubKey;
|
||||
public String apiCertKey;
|
||||
public PrivateKey privateKey;
|
||||
public String apiCert;
|
||||
public String platformCertPath;
|
||||
public String platformCertNo;
|
||||
public String mchId;
|
||||
public String apiV3Key;
|
||||
public String apiV2Key;
|
||||
public String notifyUrl;
|
||||
public String refundNotifyUrl;
|
||||
public String transferNotifyUrl;
|
||||
}
|
||||
/**
|
||||
* 微信 AccessToken 接口地址
|
||||
*/
|
||||
protected static final String WX_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token";
|
||||
/**
|
||||
* AccessToken 默认过期时间(秒):微信官方默认7200秒
|
||||
*/
|
||||
protected static final Long DEFAULT_EXPIRES_IN = 7200L;
|
||||
/**
|
||||
* 缓存过期偏移量(秒):提前200秒过期,避免接口调用时刚好过期
|
||||
*/
|
||||
protected static final Long EXPIRES_OFFSET = 200L;
|
||||
|
||||
/**
|
||||
* 支付参数KEY
|
||||
*/
|
||||
protected static final Set<String> PAY_KEYS = Set.of(
|
||||
ParamCodeCst.Wechat.Pay.WX_PUB_KEY,
|
||||
ParamCodeCst.Wechat.Pay.WX_API_CLIENT_KEY,
|
||||
ParamCodeCst.Wechat.Pay.WX_API_CLIENT_CERT,
|
||||
ParamCodeCst.Wechat.Pay.WX_MCH_ID,
|
||||
ParamCodeCst.Wechat.Pay.WX_V3_KEY,
|
||||
ParamCodeCst.System.NATIVE_NOTIFY_URL
|
||||
);
|
||||
|
||||
String getPhone(String code) {
|
||||
/**
|
||||
* 初始化配置信息
|
||||
*/
|
||||
protected abstract void init();
|
||||
|
||||
/**
|
||||
* 获取微信 AccessToken
|
||||
*
|
||||
* @param refresh 是否强制刷新(true=忽略缓存,重新获取;false=优先用缓存)
|
||||
* @return 有效的 AccessToken
|
||||
*/
|
||||
public abstract String getAccessToken(boolean refresh);
|
||||
|
||||
/**
|
||||
* 通过code 获取用户手机号
|
||||
*
|
||||
* @param code 微信code
|
||||
*/
|
||||
public String getPhone(String code) {
|
||||
String requestBody = JSONObject.toJSONString(Map.of("code", code));
|
||||
String response = HttpUtil.post("https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + getAccessToken(false), requestBody);
|
||||
log.info("获取手机号响应: {}", response);
|
||||
@@ -79,129 +104,10 @@ public abstract class BaseWx {
|
||||
throw new RuntimeException("获取手机号失败");
|
||||
}
|
||||
|
||||
String getAccessToken(boolean refresh) {
|
||||
Object token = redisService.get("access_token");
|
||||
if (!refresh && token instanceof String) {
|
||||
return (String) token;
|
||||
}
|
||||
|
||||
String response = HttpUtil.get("https://api.weixin.qq.com/cgi-bin/token",
|
||||
Map.of("grant_type", "client_credential", "appid", config.appId, "secret", config.appSecret)
|
||||
);
|
||||
|
||||
log.info("获取access_token响应: {}", response);
|
||||
JSONObject jsonObject = JSONObject.parseObject(response);
|
||||
String accessToken = jsonObject.getString("access_token");
|
||||
if (accessToken == null) {
|
||||
throw new RuntimeException("获取access_token失败");
|
||||
}
|
||||
Long expiresIn = jsonObject.getLong("expires_in");
|
||||
if (expiresIn == null) {
|
||||
expiresIn = 7200L;
|
||||
}
|
||||
redisService.set("access_token", accessToken, expiresIn - 200);
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用微信支付平台证书公钥加密敏感信息(OAEP)
|
||||
*/
|
||||
String encryptByPlatformCert(String content) {
|
||||
return PayKit.encryptData(content, config.pubKey);
|
||||
}
|
||||
|
||||
String getSerialNumberFromPem(String certContent) {
|
||||
try {
|
||||
// 去掉 PEM 头尾并清理空格换行
|
||||
String pem = certContent
|
||||
.replace("-----BEGIN CERTIFICATE-----", "")
|
||||
.replace("-----END CERTIFICATE-----", "")
|
||||
.replaceAll("\\s+", "");
|
||||
|
||||
// Base64 解码
|
||||
byte[] certBytes = Base64.getDecoder().decode(pem);
|
||||
|
||||
try (ByteArrayInputStream bis = new ByteArrayInputStream(certBytes)) {
|
||||
X509Certificate certificate = PayKit.getCertificate(bis);
|
||||
|
||||
if (certificate != null) {
|
||||
String serialNo = certificate.getSerialNumber()
|
||||
.toString(16)
|
||||
.toUpperCase(Locale.getDefault());
|
||||
|
||||
System.out.println("证书序列号:" + serialNo);
|
||||
|
||||
return serialNo;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException("无效的证书", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getSerialNumber() {
|
||||
|
||||
try (var certStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(config.getCertPath())) {
|
||||
X509Certificate certificate = PayKit.getCertificate(certStream);
|
||||
if (certificate != null) {
|
||||
String serialNo = certificate.getSerialNumber().toString(16).toUpperCase(Locale.getDefault());
|
||||
boolean isValid = PayKit.checkCertificateIsValid(certificate, config.getMchId(), -2);
|
||||
log.info("证书是否可用 {} 证书有效期为 {}", isValid, certificate.getNotAfter());
|
||||
log.info("证书序列号: {}", serialNo);
|
||||
return serialNo;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("读取证书失败", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public JSONObject verifySignature(HttpServletRequest request) {
|
||||
try {
|
||||
log.info("开始校验签名并解密");
|
||||
String timestamp = request.getHeader("Wechatpay-Timestamp");
|
||||
String nonce = request.getHeader("Wechatpay-Nonce");
|
||||
String serialNo = request.getHeader("Wechatpay-Serial");
|
||||
String signature = request.getHeader("Wechatpay-Signature");
|
||||
String result = request.getReader().lines().reduce((a, b) -> a + b).orElse("");
|
||||
log.info("参数信息: timestamp: {}, nonce: {}, serialNo: {}, signature: {}, result: {}", timestamp, nonce, serialNo, signature, result);
|
||||
boolean b = WxPayKit.verifySignature(signature, result, nonce, timestamp, config.pubKey);
|
||||
if (!b) {
|
||||
throw new CzgException("验签失败");
|
||||
}
|
||||
JSONObject jsonObject = JSONObject.parseObject(result);
|
||||
JSONObject resource = jsonObject.getJSONObject("resource");
|
||||
String associatedData = resource.getString("associated_data");
|
||||
String ciphertext = resource.getString("ciphertext");
|
||||
String nonceStr = resource.getString("nonce");
|
||||
|
||||
String plainText = decryptToString(associatedData, nonceStr, ciphertext);
|
||||
log.info("充值支付通知明文 {}", plainText);
|
||||
return JSONObject.parseObject(plainText);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public String decryptToString(String associatedData, String nonceStr, String ciphertext) {
|
||||
AesUtil aesUtil = new AesUtil(config.getApiV3Key().getBytes(StandardCharsets.UTF_8));
|
||||
try {
|
||||
return aesUtil.decryptToString(
|
||||
associatedData.getBytes(StandardCharsets.UTF_8),
|
||||
nonceStr.getBytes(StandardCharsets.UTF_8),
|
||||
ciphertext
|
||||
);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getOpenId(String code) {
|
||||
|
||||
public String getOpenId(String code) {
|
||||
init();
|
||||
String response = HttpUtil.get("https://api.weixin.qq.com/sns/jscode2session",
|
||||
Map.of("appid", config.getAppId() , "secret", config.getAppSecret(), "js_code", code, "grant_type", "authorization_code")
|
||||
Map.of("appid", config.getAppId(), "secret", config.getAppSecret(), "js_code", code, "grant_type", "authorization_code")
|
||||
);
|
||||
log.info("获取openId响应: {}", response);
|
||||
JSONObject jsonObject = JSONObject.parseObject(response);
|
||||
@@ -212,8 +118,8 @@ public abstract class BaseWx {
|
||||
throw new RuntimeException("获取openId失败");
|
||||
}
|
||||
|
||||
public Map<String, String> v3Pay(String openId, BigDecimal amount, String desc, String tradeNo, String type) {
|
||||
|
||||
public Map<String, String> v3Pay(String openId, BigDecimal amount, String desc, String tradeNo, String type) {
|
||||
init();
|
||||
if (desc == null) desc = "订单支付";
|
||||
UnifiedOrderModel model = new UnifiedOrderModel();
|
||||
model.setAppid(config.getAppId());
|
||||
@@ -254,8 +160,104 @@ public abstract class BaseWx {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用微信支付平台证书公钥加密敏感信息(OAEP)
|
||||
*/
|
||||
String encryptByPlatformCert(String content, String pubKey) {
|
||||
return PayKit.encryptData(content, pubKey);
|
||||
}
|
||||
|
||||
Map<String, String> v2Pay(String openId, BigDecimal amount, String desc, String tradeNo) {
|
||||
public JSONObject verifySignature(HttpServletRequest request) {
|
||||
try {
|
||||
init();
|
||||
log.info("开始校验签名并解密");
|
||||
String timestamp = request.getHeader("Wechatpay-Timestamp");
|
||||
String nonce = request.getHeader("Wechatpay-Nonce");
|
||||
String serialNo = request.getHeader("Wechatpay-Serial");
|
||||
String signature = request.getHeader("Wechatpay-Signature");
|
||||
String result = request.getReader().lines().reduce((a, b) -> a + b).orElse("");
|
||||
log.info("参数信息: timestamp: {}, nonce: {}, serialNo: {}, signature: {}, result: {}", timestamp, nonce, serialNo, signature, result);
|
||||
boolean b = WxPayKit.verifySignature(signature, result, nonce, timestamp, config.pubKey);
|
||||
if (!b) {
|
||||
throw new CzgException("验签失败");
|
||||
}
|
||||
JSONObject jsonObject = JSONObject.parseObject(result);
|
||||
JSONObject resource = jsonObject.getJSONObject("resource");
|
||||
String associatedData = resource.getString("associated_data");
|
||||
String ciphertext = resource.getString("ciphertext");
|
||||
String nonceStr = resource.getString("nonce");
|
||||
|
||||
String plainText = decryptToString(associatedData, nonceStr, ciphertext);
|
||||
log.info("充值支付通知明文 {}", plainText);
|
||||
return JSONObject.parseObject(plainText);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密
|
||||
*/
|
||||
public String decryptToString(String associatedData, String nonceStr, String ciphertext) {
|
||||
init();
|
||||
AesUtil aesUtil = new AesUtil(config.getApiV3Key().getBytes(StandardCharsets.UTF_8));
|
||||
try {
|
||||
return aesUtil.decryptToString(
|
||||
associatedData.getBytes(StandardCharsets.UTF_8),
|
||||
nonceStr.getBytes(StandardCharsets.UTF_8),
|
||||
ciphertext
|
||||
);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
String getSerialNumberFromPem(String certContent) {
|
||||
try {
|
||||
// 去掉 PEM 头尾并清理空格换行
|
||||
String pem = certContent
|
||||
.replace("-----BEGIN CERTIFICATE-----", "")
|
||||
.replace("-----END CERTIFICATE-----", "")
|
||||
.replaceAll("\\s+", "");
|
||||
|
||||
// Base64 解码
|
||||
byte[] certBytes = Base64.getDecoder().decode(pem);
|
||||
try (ByteArrayInputStream bis = new ByteArrayInputStream(certBytes)) {
|
||||
X509Certificate certificate = PayKit.getCertificate(bis);
|
||||
|
||||
if (certificate != null) {
|
||||
String serialNo = certificate.getSerialNumber()
|
||||
.toString(16)
|
||||
.toUpperCase(Locale.getDefault());
|
||||
return serialNo;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException("无效的证书", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getSerialNumber(String certPath, String mchId) {
|
||||
|
||||
try (var certStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(certPath)) {
|
||||
X509Certificate certificate = PayKit.getCertificate(certStream);
|
||||
if (certificate != null) {
|
||||
String serialNo = certificate.getSerialNumber().toString(16).toUpperCase(Locale.getDefault());
|
||||
boolean isValid = PayKit.checkCertificateIsValid(certificate, mchId, -2);
|
||||
log.info("证书是否可用 {} 证书有效期为 {}", isValid, certificate.getNotAfter());
|
||||
log.info("证书序列号: {}", serialNo);
|
||||
return serialNo;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("读取证书失败", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Map<String, String> v2Pay(String openId, BigDecimal amount, String desc, String tradeNo) {
|
||||
|
||||
Map<String, String> payModel = com.ijpay.wxpay.model.UnifiedOrderModel.builder()
|
||||
.appid(config.appId)
|
||||
@@ -288,88 +290,11 @@ public abstract class BaseWx {
|
||||
return WxPayKit.prepayIdCreateSign(prepayId, config.appId, config.appSecret, SignType.MD5);
|
||||
}
|
||||
|
||||
|
||||
String refund(String tradeNo, String refundTradeNo, BigDecimal amount) {
|
||||
|
||||
int finalAmount = amount.multiply(new BigDecimal(100)).intValueExact();
|
||||
RefundModel model = new RefundModel();
|
||||
model.setOut_trade_no(tradeNo);
|
||||
model.setOut_refund_no(refundTradeNo);
|
||||
model.setAmount(new RefundAmount(finalAmount, "CNY", finalAmount));
|
||||
model.setNotify_url(config.refundNotifyUrl);
|
||||
|
||||
String info = JSONObject.toJSONString(model);
|
||||
log.info("统一退款参数: {}", info);
|
||||
IJPayHttpResponse response;
|
||||
try {
|
||||
response = WxPayApi.v3(
|
||||
RequestMethodEnum.POST,
|
||||
WxDomainEnum.CHINA.toString(),
|
||||
BasePayApiEnum.REFUND.toString(),
|
||||
config.mchId,
|
||||
getSerialNumber(),
|
||||
getSerialNumber(),
|
||||
config.apiCertKey,
|
||||
info
|
||||
);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
log.info("统一退款响应 {}", response);
|
||||
String body = response.getBody();
|
||||
JSONObject jsonObject = JSONObject.parseObject(body);
|
||||
if ("ABNORMAL".equals(jsonObject.getString("status")) || response.getStatus() != 200) {
|
||||
throw new CzgException("退款异常," + jsonObject.getString("message"));
|
||||
}
|
||||
return jsonObject.getString("refund_id");
|
||||
}
|
||||
|
||||
public String genCode(String path, String scene) {
|
||||
Map<String, Object> params = Map.of(
|
||||
"scene", scene,
|
||||
"page", path,
|
||||
"width", 430
|
||||
);
|
||||
var response = HttpRequest.post("https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + getAccessToken(false))
|
||||
.body(JSONObject.toJSONString(params))
|
||||
.execute();
|
||||
|
||||
byte[] bodyBytes = response.bodyBytes();
|
||||
String str = new String(bodyBytes);
|
||||
if (str.contains("errmsg")) {
|
||||
JSONObject json = JSONObject.parseObject(str);
|
||||
throw new CzgException(json.getString("errmsg"));
|
||||
}
|
||||
return "data:image/png;base64," + Base64.getEncoder().encodeToString(bodyBytes);
|
||||
}
|
||||
public static X509Certificate loadCertificate(String certStr) throws Exception {
|
||||
// 去掉 PEM 头尾
|
||||
String pem = certStr
|
||||
.replace("-----BEGIN CERTIFICATE-----", "")
|
||||
.replace("-----END CERTIFICATE-----", "")
|
||||
.replaceAll("\\s+", "");
|
||||
|
||||
byte[] der = Base64.getDecoder().decode(pem);
|
||||
CertificateFactory factory = CertificateFactory.getInstance("X.509");
|
||||
return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(der));
|
||||
}
|
||||
|
||||
|
||||
public String rsaEncryptOAEP(String content) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, RsaKit.loadPublicKey(config.pubKey));
|
||||
byte[] dataByte = content.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] cipherData = cipher.doFinal(dataByte);
|
||||
return cn.hutool.core.codec.Base64.encode(cipherData);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public JSONObject transferBalance(String openId, String name, BigDecimal amount, String remarkTxt, String billNoTxt) {
|
||||
|
||||
/**
|
||||
* 提现
|
||||
*/
|
||||
public JSONObject transferBalance(String openId, String name, BigDecimal amount, String remarkTxt, String billNoTxt) {
|
||||
init();
|
||||
String remark = remarkTxt == null ? "佣金" : remarkTxt;
|
||||
String billNo = billNoTxt == null ? IdUtil.simpleUUID() : billNoTxt;
|
||||
Map<String, Object> params = new java.util.HashMap<>(Map.of(
|
||||
@@ -386,7 +311,7 @@ public abstract class BaseWx {
|
||||
}
|
||||
));
|
||||
if (amount.compareTo(BigDecimal.valueOf(0.3)) >= 0) {
|
||||
params.put("user_name", rsaEncryptOAEP(name));
|
||||
params.put("user_name", rsaEncryptOAEP(name, config.pubKey));
|
||||
}
|
||||
log.info("转账到零钱参数: {}", JSONObject.toJSONString(params));
|
||||
IJPayHttpResponse response = null;
|
||||
@@ -412,5 +337,102 @@ public abstract class BaseWx {
|
||||
return resp;
|
||||
}
|
||||
|
||||
String refund(String tradeNo, String refundTradeNo, BigDecimal amount) {
|
||||
init();
|
||||
int finalAmount = amount.multiply(new BigDecimal(100)).intValueExact();
|
||||
RefundModel model = new RefundModel();
|
||||
model.setOut_trade_no(tradeNo);
|
||||
model.setOut_refund_no(refundTradeNo);
|
||||
model.setAmount(new RefundAmount(finalAmount, "CNY", finalAmount));
|
||||
model.setNotify_url(config.refundNotifyUrl);
|
||||
|
||||
String info = JSONObject.toJSONString(model);
|
||||
log.info("统一退款参数: {}", info);
|
||||
IJPayHttpResponse response;
|
||||
try {
|
||||
response = WxPayApi.v3(
|
||||
RequestMethodEnum.POST,
|
||||
WxDomainEnum.CHINA.toString(),
|
||||
BasePayApiEnum.REFUND.toString(),
|
||||
config.mchId,
|
||||
getSerialNumber(config.getCertPath(), config.mchId),
|
||||
getSerialNumber(config.getCertPath(), config.mchId),
|
||||
config.apiCertKey,
|
||||
info
|
||||
);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
log.info("统一退款响应 {}", response);
|
||||
String body = response.getBody();
|
||||
JSONObject jsonObject = JSONObject.parseObject(body);
|
||||
if ("ABNORMAL".equals(jsonObject.getString("status")) || response.getStatus() != 200) {
|
||||
throw new CzgException("退款异常," + jsonObject.getString("message"));
|
||||
}
|
||||
return jsonObject.getString("refund_id");
|
||||
}
|
||||
|
||||
public String genCode(String path, String scene) {
|
||||
Map<String, Object> params = Map.of(
|
||||
"scene", scene,
|
||||
"page", path,
|
||||
"width", 430
|
||||
);
|
||||
var response = HttpRequest.post("https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + getAccessToken(false))
|
||||
.body(JSONObject.toJSONString(params))
|
||||
.execute();
|
||||
|
||||
byte[] bodyBytes = response.bodyBytes();
|
||||
String str = new String(bodyBytes);
|
||||
if (str.contains("errmsg")) {
|
||||
JSONObject json = JSONObject.parseObject(str);
|
||||
throw new CzgException(json.getString("errmsg"));
|
||||
}
|
||||
return "data:image/png;base64," + Base64.getEncoder().encodeToString(bodyBytes);
|
||||
}
|
||||
|
||||
public static X509Certificate loadCertificate(String certStr) throws Exception {
|
||||
// 去掉 PEM 头尾
|
||||
String pem = certStr
|
||||
.replace("-----BEGIN CERTIFICATE-----", "")
|
||||
.replace("-----END CERTIFICATE-----", "")
|
||||
.replaceAll("\\s+", "");
|
||||
|
||||
byte[] der = Base64.getDecoder().decode(pem);
|
||||
CertificateFactory factory = CertificateFactory.getInstance("X.509");
|
||||
return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(der));
|
||||
}
|
||||
|
||||
|
||||
public String rsaEncryptOAEP(String content, String pubKey) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, RsaKit.loadPublicKey(pubKey));
|
||||
byte[] dataByte = content.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] cipherData = cipher.doFinal(dataByte);
|
||||
return cn.hutool.core.codec.Base64.encode(cipherData);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
protected static class Config {
|
||||
public String appId;
|
||||
public String appSecret;
|
||||
public String certPath;
|
||||
public String pubKey;
|
||||
public String apiCertKey;
|
||||
public PrivateKey privateKey;
|
||||
public String apiCert;
|
||||
public String platformCertPath;
|
||||
public String platformCertNo;
|
||||
public String mchId;
|
||||
public String apiV3Key;
|
||||
public String apiV2Key;
|
||||
public String notifyUrl;
|
||||
public String refundNotifyUrl;
|
||||
public String transferNotifyUrl;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
package com.czg.service.market.service.impl;
|
||||
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.czg.constants.ParamCodeCst;
|
||||
import com.czg.service.RedisService;
|
||||
import com.czg.system.service.SysParamsService;
|
||||
import com.ijpay.core.kit.RsaKit;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.dubbo.config.annotation.DubboReference;
|
||||
import org.apache.dubbo.config.annotation.DubboService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
/**
|
||||
* 微信支付service
|
||||
*
|
||||
* @author Administrator
|
||||
*/
|
||||
@Component
|
||||
@@ -24,27 +27,59 @@ public class WxServiceImpl extends BaseWx {
|
||||
@DubboReference
|
||||
private SysParamsService paramsService;
|
||||
|
||||
private final Set<String> shopMiniKeys = Set.of(ParamCodeCst.Wechat.Mini.SHOP_WX_APP_ID, ParamCodeCst.Wechat.Mini.SHOP_WX_SECRETE);
|
||||
|
||||
|
||||
public WxServiceImpl(@Autowired RedisService redisService) {
|
||||
this.redisService = redisService;
|
||||
config = new Config();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
@Override
|
||||
public String getAccessToken(boolean refresh) {
|
||||
init();
|
||||
Object token = redisService.get("wx:shop:access_token");
|
||||
if (!refresh && token instanceof String) {
|
||||
return (String) token;
|
||||
}
|
||||
|
||||
String response = HttpUtil.get(WX_ACCESS_TOKEN_URL,
|
||||
Map.of("grant_type", "client_credential", "appid", config.appId, "secret", config.appSecret)
|
||||
);
|
||||
|
||||
log.info("获取access_token响应: {}", response);
|
||||
JSONObject jsonObject = JSONObject.parseObject(response);
|
||||
String accessToken = jsonObject.getString("access_token");
|
||||
if (accessToken == null) {
|
||||
throw new RuntimeException("获取access_token失败");
|
||||
}
|
||||
Long expiresIn = jsonObject.getLong("expires_in");
|
||||
if (expiresIn == null) {
|
||||
expiresIn = DEFAULT_EXPIRES_IN;
|
||||
}
|
||||
redisService.set("wx:shop:access_token", accessToken, expiresIn - EXPIRES_OFFSET);
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
// 商户小程序参数
|
||||
Map<String, String> shopMiniKeyMap = paramsService.getParamsByMap("shopMiniKeys", shopMiniKeys);
|
||||
|
||||
// 小程序id
|
||||
config.appId = paramsService.getSysParamValue(ParamCodeCst.Wechat.Mini.SHOP_WX_APP_ID);
|
||||
config.appId = shopMiniKeyMap.get(ParamCodeCst.Wechat.Mini.SHOP_WX_APP_ID);
|
||||
// 小程序secrete
|
||||
config.appSecret = paramsService.getSysParamValue(ParamCodeCst.Wechat.Mini.SHOP_WX_SECRETE);
|
||||
|
||||
config.appSecret = shopMiniKeyMap.get(ParamCodeCst.Wechat.Mini.SHOP_WX_SECRETE);
|
||||
|
||||
// 微信支付参数
|
||||
Map<String, String> payKeyMap = paramsService.getParamsByMap("payKeys", PAY_KEYS);
|
||||
config.certPath = "";
|
||||
// 微信支付公钥
|
||||
config.pubKey = paramsService.getSysParamValue(ParamCodeCst.Wechat.Pay.WX_PUB_KEY);
|
||||
config.pubKey = payKeyMap.get(ParamCodeCst.Wechat.Pay.WX_PUB_KEY);
|
||||
// api支付证书私钥
|
||||
config.apiCertKey = paramsService.getSysParamValue(ParamCodeCst.Wechat.Pay.WX_API_CLIENT_KEY);
|
||||
config.apiCertKey = payKeyMap.get(ParamCodeCst.Wechat.Pay.WX_API_CLIENT_KEY);
|
||||
// api支付证书公钥
|
||||
config.apiCert = paramsService.getSysParamValue(ParamCodeCst.Wechat.Pay.WX_API_CLIENT_CERT);
|
||||
config.apiCert = payKeyMap.get(ParamCodeCst.Wechat.Pay.WX_API_CLIENT_CERT);
|
||||
try {
|
||||
config.privateKey = RsaKit.loadPrivateKey(config.apiCertKey);
|
||||
} catch (Exception e) {
|
||||
@@ -55,14 +90,14 @@ public class WxServiceImpl extends BaseWx {
|
||||
// 平台证书编号
|
||||
config.platformCertNo = "";
|
||||
// 商户号
|
||||
config.mchId = paramsService.getSysParamValue(ParamCodeCst.Wechat.Pay.WX_MCH_ID);
|
||||
config.mchId = payKeyMap.get(ParamCodeCst.Wechat.Pay.WX_MCH_ID);
|
||||
// v3密钥
|
||||
config.apiV3Key = paramsService.getSysParamValue(ParamCodeCst.Wechat.Pay.WX_V3_KEY);
|
||||
config.apiV3Key = payKeyMap.get(ParamCodeCst.Wechat.Pay.WX_V3_KEY);
|
||||
config.apiV2Key = "";
|
||||
// 回调地址
|
||||
config.notifyUrl = paramsService.getSysParamValue(ParamCodeCst.System.NATIVE_NOTIFY_URL) + "/wx/pay";
|
||||
config.notifyUrl = payKeyMap.get(ParamCodeCst.System.NATIVE_NOTIFY_URL) + "/wx/pay";
|
||||
config.refundNotifyUrl = "";
|
||||
config.transferNotifyUrl = paramsService.getSysParamValue(ParamCodeCst.System.NATIVE_NOTIFY_URL) + "/wx/transfer";
|
||||
config.transferNotifyUrl = payKeyMap.get(ParamCodeCst.System.NATIVE_NOTIFY_URL) + "/wx/transfer";
|
||||
}
|
||||
|
||||
public BaseWx getAppService() {
|
||||
|
||||
@@ -19,9 +19,15 @@
|
||||
<if test="shopId != null">
|
||||
and o.shop_id = #{shopId}
|
||||
</if>
|
||||
<if test="userId != null">
|
||||
and o.user_id = #{userId} and o.status != 'ing'
|
||||
</if>
|
||||
<choose>
|
||||
<!-- userId为空时:拼接 and o.status != 'ing' -->
|
||||
<when test="userId == null or userId == ''">
|
||||
and o.status != 'ing'
|
||||
</when>
|
||||
<otherwise>
|
||||
and o.user_id = #{userId}
|
||||
</otherwise>
|
||||
</choose>
|
||||
<if test="param.orderNo != null">
|
||||
and o.order_no = #{param.orderNo}
|
||||
</if>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.czg.service.system.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.czg.exception.CzgException;
|
||||
import com.czg.resp.CzgResult;
|
||||
import com.czg.sa.StpKit;
|
||||
import com.czg.service.system.mapper.SysParamsMapper;
|
||||
@@ -11,11 +13,11 @@ import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.dubbo.config.annotation.DubboService;
|
||||
import org.springframework.cache.annotation.CacheConfig;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.cache.annotation.Caching;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 服务层实现。
|
||||
@@ -25,7 +27,6 @@ import java.util.List;
|
||||
*/
|
||||
@Slf4j
|
||||
@DubboService
|
||||
@CacheConfig(cacheNames = "params")
|
||||
public class SysParamsServiceImpl extends ServiceImpl<SysParamsMapper, SysParams> implements SysParamsService {
|
||||
|
||||
@Override
|
||||
@@ -62,7 +63,10 @@ public class SysParamsServiceImpl extends ServiceImpl<SysParamsMapper, SysParams
|
||||
}
|
||||
|
||||
@Override
|
||||
@CacheEvict(key = "#paramsDTO.paramCode")
|
||||
@Caching(evict = {
|
||||
@CacheEvict(cacheNames = "params:entity", allEntries = true),
|
||||
@CacheEvict(cacheNames = "params", key = "#paramsDTO.paramCode")
|
||||
})
|
||||
public CzgResult<String> updateParams(SysParamsDTO paramsDTO) {
|
||||
// 查询 paramCode 是否存在
|
||||
SysParams sysParams = getOne(new QueryWrapper().eq(SysParams::getParamCode, paramsDTO.getParamCode())
|
||||
@@ -90,7 +94,10 @@ public class SysParamsServiceImpl extends ServiceImpl<SysParamsMapper, SysParams
|
||||
}
|
||||
|
||||
@Override
|
||||
@CacheEvict(key = "#code")
|
||||
@Caching(evict = {
|
||||
@CacheEvict(cacheNames = "params:entity", allEntries = true),
|
||||
@CacheEvict(cacheNames = "params", key = "#code")
|
||||
})
|
||||
public CzgResult<Boolean> deleteParams(String code) {
|
||||
SysParams sysParams = getById(code);
|
||||
if (sysParams == null) {
|
||||
@@ -130,4 +137,18 @@ public class SysParamsServiceImpl extends ServiceImpl<SysParamsMapper, SysParams
|
||||
}
|
||||
return sysParam.getParamValue();
|
||||
}
|
||||
|
||||
@Cacheable(cacheNames = "params:entity", key = "#type")
|
||||
@Override
|
||||
public Map<String, String> getParamsByMap(String type, Set<String> keyList) throws CzgException{
|
||||
Map<String, String> map = new HashMap<>();
|
||||
for (String key : keyList) {
|
||||
SysParams sysParam = getSysParam(key);
|
||||
if (sysParam == null || StrUtil.isBlank(sysParam.getParamValue())) {
|
||||
throw new CzgException(key + "参数不存在");
|
||||
}
|
||||
map.put(key, sysParam.getParamValue());
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user