Merge branch 'test' into prod

This commit is contained in:
gong
2025-12-23 13:35:54 +08:00
6 changed files with 405 additions and 275 deletions

View File

@@ -1,11 +1,14 @@
package com.czg.system.service; package com.czg.system.service;
import com.czg.exception.CzgException;
import com.czg.resp.CzgResult; import com.czg.resp.CzgResult;
import com.czg.system.dto.SysParamsDTO; import com.czg.system.dto.SysParamsDTO;
import com.czg.system.entity.SysParams; import com.czg.system.entity.SysParams;
import com.mybatisflex.core.service.IService; import com.mybatisflex.core.service.IService;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set;
/** /**
* 服务层。 * 服务层。
@@ -23,6 +26,7 @@ public interface SysParamsService extends IService<SysParams> {
* @return 参数列表 * @return 参数列表
*/ */
CzgResult<List<SysParamsDTO>> getParamsByType(Integer type); CzgResult<List<SysParamsDTO>> getParamsByType(Integer type);
/** /**
* 新增参数 * 新增参数
* *
@@ -62,4 +66,15 @@ public interface SysParamsService extends IService<SysParams> {
*/ */
String getSysParamValue(String code); 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;
} }

View File

@@ -1,16 +1,21 @@
package com.czg.service.market.service.impl; 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.constants.ParamCodeCst;
import com.czg.exception.CzgException;
import com.czg.service.RedisService; import com.czg.service.RedisService;
import com.czg.system.service.SysParamsService; import com.czg.system.service.SysParamsService;
import com.ijpay.core.kit.RsaKit; import com.ijpay.core.kit.RsaKit;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference; import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Set;
/** /**
* 微信支付service * 微信支付service
* @author Administrator * @author Administrator
@@ -23,29 +28,59 @@ public class AppWxServiceImpl extends BaseWx {
@DubboReference @DubboReference
private SysParamsService paramsService; 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) { public AppWxServiceImpl(@Autowired RedisService autoRedisService) {
this.redisService = autoRedisService; this.redisService = autoRedisService;
config = new Config(); config = new Config();
} }
@PostConstruct @Override
public void init() { 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 // 小程序id
config.appId = paramsService.getSysParamValue(ParamCodeCst.Wechat.Mini.USER_WX_APP_ID); config.appId = userMiniKeyMap.get(ParamCodeCst.Wechat.Mini.USER_WX_APP_ID);
log.info("小程序id:{}", config.appId);
// 小程序secrete // 小程序secrete
config.appSecret = paramsService.getSysParamValue(ParamCodeCst.Wechat.Mini.USER_WX_SECRETE); config.appSecret = userMiniKeyMap.get(ParamCodeCst.Wechat.Mini.USER_WX_SECRETE);
log.info("小程序secrete:{}", config.appSecret);
config.certPath = ""; config.certPath = "";
// 微信支付公钥 // 微信支付公钥
config.pubKey = paramsService.getSysParamValue(ParamCodeCst.Wechat.Pay.WX_PUB_KEY); config.pubKey = payKeyMap.get(ParamCodeCst.Wechat.Pay.WX_PUB_KEY);
log.info("微信支付公钥:{}", config.pubKey);
// api支付证书私钥 // api支付证书私钥
config.apiCertKey = paramsService.getSysParamValue(ParamCodeCst.Wechat.Pay.WX_API_CLIENT_KEY); config.apiCertKey = payKeyMap.get(ParamCodeCst.Wechat.Pay.WX_API_CLIENT_KEY);
log.info("api支付证书私钥:{}", config.apiCertKey);
// api支付证书公钥 // api支付证书公钥
config.apiCert = paramsService.getSysParamValue(ParamCodeCst.Wechat.Pay.WX_API_CLIENT_CERT); config.apiCert = payKeyMap.get(ParamCodeCst.Wechat.Pay.WX_API_CLIENT_CERT);
log.info("api支付证书公钥:{}", config.apiCert);
try { try {
config.privateKey = RsaKit.loadPrivateKey(config.apiCertKey); config.privateKey = RsaKit.loadPrivateKey(config.apiCertKey);
} catch (Exception e) { } catch (Exception e) {
@@ -56,18 +91,14 @@ public class AppWxServiceImpl extends BaseWx {
// 平台证书编号 // 平台证书编号
config.platformCertNo = ""; config.platformCertNo = "";
// 商户号 // 商户号
config.mchId = paramsService.getSysParamValue(ParamCodeCst.Wechat.Pay.WX_MCH_ID); config.mchId = payKeyMap.get(ParamCodeCst.Wechat.Pay.WX_MCH_ID);
log.info("商户号:{}", config.mchId);
// v3密钥 // v3密钥
config.apiV3Key = paramsService.getSysParamValue(ParamCodeCst.Wechat.Pay.WX_V3_KEY); config.apiV3Key = payKeyMap.get(ParamCodeCst.Wechat.Pay.WX_V3_KEY);
log.info("v3密钥:{}", config.apiV3Key);
config.apiV2Key = ""; config.apiV2Key = "";
// 回调地址 // 回调地址
config.notifyUrl = paramsService.getSysParamValue(ParamCodeCst.System.NATIVE_NOTIFY_URL) + "/wx/pay"; config.notifyUrl = payKeyMap.get(ParamCodeCst.System.NATIVE_NOTIFY_URL) + "/wx/pay";
log.info("回调地址:{}", config.notifyUrl);
config.refundNotifyUrl = ""; config.refundNotifyUrl = "";
config.transferNotifyUrl = paramsService.getSysParamValue(ParamCodeCst.System.NATIVE_NOTIFY_URL) + "/wx/transfer"; config.transferNotifyUrl = payKeyMap.get(ParamCodeCst.System.NATIVE_NOTIFY_URL) + "/wx/transfer";
log.info("转账回调地址:{}", config.transferNotifyUrl);
} }
public BaseWx getAppService() { public BaseWx getAppService() {

View File

@@ -5,6 +5,7 @@ import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpUtil; import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.czg.constants.ParamCodeCst;
import com.czg.exception.CzgException; import com.czg.exception.CzgException;
import com.czg.service.RedisService; import com.czg.service.RedisService;
import com.ijpay.core.IJPayHttpResponse; import com.ijpay.core.IJPayHttpResponse;
@@ -20,8 +21,7 @@ import com.ijpay.wxpay.enums.v3.BasePayApiEnum;
import com.ijpay.wxpay.model.v3.*; import com.ijpay.wxpay.model.v3.*;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.Data; import lombok.Data;
import org.slf4j.Logger; import lombok.extern.slf4j.Slf4j;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@@ -35,36 +35,61 @@ import java.security.cert.X509Certificate;
import java.util.Base64; import java.util.Base64;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set;
/** /**
* 微信支付相关 * 微信支付相关
*
* @author Administrator * @author Administrator
*/ */
@Slf4j
public abstract class BaseWx { public abstract class BaseWx {
public Config config; protected Config config;
public RedisService redisService; public RedisService redisService;
public Logger log = LoggerFactory.getLogger(BaseWx.class); /**
@Data * 微信 AccessToken 接口地址
public static class Config { */
public String appId; protected static final String WX_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token";
public String appSecret; /**
public String certPath; * AccessToken 默认过期时间微信官方默认7200秒
public String pubKey; */
public String apiCertKey; protected static final Long DEFAULT_EXPIRES_IN = 7200L;
public PrivateKey privateKey; /**
public String apiCert; * 缓存过期偏移量提前200秒过期避免接口调用时刚好过期
public String platformCertPath; */
public String platformCertNo; protected static final Long EXPIRES_OFFSET = 200L;
public String mchId;
public String apiV3Key;
public String apiV2Key;
public String notifyUrl;
public String refundNotifyUrl;
public String transferNotifyUrl;
}
/**
* 支付参数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 requestBody = JSONObject.toJSONString(Map.of("code", code));
String response = HttpUtil.post("https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + getAccessToken(false), requestBody); String response = HttpUtil.post("https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + getAccessToken(false), requestBody);
log.info("获取手机号响应: {}", response); log.info("获取手机号响应: {}", response);
@@ -79,129 +104,10 @@ public abstract class BaseWx {
throw new RuntimeException("获取手机号失败"); 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", 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); log.info("获取openId响应: {}", response);
JSONObject jsonObject = JSONObject.parseObject(response); JSONObject jsonObject = JSONObject.parseObject(response);
@@ -213,7 +119,7 @@ public abstract class BaseWx {
} }
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 = "订单支付"; if (desc == null) desc = "订单支付";
UnifiedOrderModel model = new UnifiedOrderModel(); UnifiedOrderModel model = new UnifiedOrderModel();
model.setAppid(config.getAppId()); model.setAppid(config.getAppId());
@@ -254,6 +160,102 @@ public abstract class BaseWx {
} }
} }
/**
* 使用微信支付平台证书公钥加密敏感信息OAEP
*/
String encryptByPlatformCert(String content, String pubKey) {
return PayKit.encryptData(content, pubKey);
}
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> v2Pay(String openId, BigDecimal amount, String desc, String tradeNo) {
@@ -288,9 +290,55 @@ public abstract class BaseWx {
return WxPayKit.prepayIdCreateSign(prepayId, config.appId, config.appSecret, SignType.MD5); return WxPayKit.prepayIdCreateSign(prepayId, config.appId, config.appSecret, SignType.MD5);
} }
/**
* 提现
*/
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(
"appid", config.appId,
"out_bill_no", billNo,
"transfer_scene_id", "1000",
"openid", openId,
"transfer_amount", amount.multiply(BigDecimal.valueOf(100)).intValue(),
"transfer_remark", remark,
"notify_url", config.transferNotifyUrl,
"transfer_scene_report_infos", new Object[]{
Map.of("info_type", "活动名称", "info_content", "分销奖励"),
Map.of("info_type", "奖励说明", "info_content", "用户提现")
}
));
if (amount.compareTo(BigDecimal.valueOf(0.3)) >= 0) {
params.put("user_name", rsaEncryptOAEP(name, config.pubKey));
}
log.info("转账到零钱参数: {}", JSONObject.toJSONString(params));
IJPayHttpResponse response = null;
try {
response = WxPayApi.v3(
RequestMethodEnum.POST,
WxDomainEnum.CHINA.toString(),
"/v3/fund-app/mch-transfer/transfer-bills",
config.mchId,
getSerialNumberFromPem(config.apiCert),
null,
config.privateKey,
JSONObject.toJSONString(params)
);
} catch (Exception e) {
throw new RuntimeException(e);
}
log.info("转账到零钱响应 {}", response.getBody());
JSONObject resp = JSONObject.parseObject(response.getBody());
if (resp.containsKey("code")) {
throw new CzgException("转账到零钱失败" + resp.getString("message") + ", 响应: " + resp.toJSONString());
}
return resp;
}
String refund(String tradeNo, String refundTradeNo, BigDecimal amount) { String refund(String tradeNo, String refundTradeNo, BigDecimal amount) {
init();
int finalAmount = amount.multiply(new BigDecimal(100)).intValueExact(); int finalAmount = amount.multiply(new BigDecimal(100)).intValueExact();
RefundModel model = new RefundModel(); RefundModel model = new RefundModel();
model.setOut_trade_no(tradeNo); model.setOut_trade_no(tradeNo);
@@ -307,8 +355,8 @@ public abstract class BaseWx {
WxDomainEnum.CHINA.toString(), WxDomainEnum.CHINA.toString(),
BasePayApiEnum.REFUND.toString(), BasePayApiEnum.REFUND.toString(),
config.mchId, config.mchId,
getSerialNumber(), getSerialNumber(config.getCertPath(), config.mchId),
getSerialNumber(), getSerialNumber(config.getCertPath(), config.mchId),
config.apiCertKey, config.apiCertKey,
info info
); );
@@ -342,6 +390,7 @@ public abstract class BaseWx {
} }
return "data:image/png;base64," + Base64.getEncoder().encodeToString(bodyBytes); return "data:image/png;base64," + Base64.getEncoder().encodeToString(bodyBytes);
} }
public static X509Certificate loadCertificate(String certStr) throws Exception { public static X509Certificate loadCertificate(String certStr) throws Exception {
// 去掉 PEM 头尾 // 去掉 PEM 头尾
String pem = certStr String pem = certStr
@@ -355,10 +404,10 @@ public abstract class BaseWx {
} }
public String rsaEncryptOAEP(String content) { public String rsaEncryptOAEP(String content, String pubKey) {
try { try {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding"); Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
cipher.init(Cipher.ENCRYPT_MODE, RsaKit.loadPublicKey(config.pubKey)); cipher.init(Cipher.ENCRYPT_MODE, RsaKit.loadPublicKey(pubKey));
byte[] dataByte = content.getBytes(StandardCharsets.UTF_8); byte[] dataByte = content.getBytes(StandardCharsets.UTF_8);
byte[] cipherData = cipher.doFinal(dataByte); byte[] cipherData = cipher.doFinal(dataByte);
return cn.hutool.core.codec.Base64.encode(cipherData); return cn.hutool.core.codec.Base64.encode(cipherData);
@@ -367,50 +416,23 @@ public abstract class BaseWx {
} }
} }
@Data
public JSONObject transferBalance(String openId, String name, BigDecimal amount, String remarkTxt, String billNoTxt) { protected static class Config {
public String appId;
String remark = remarkTxt == null ? "佣金" : remarkTxt; public String appSecret;
String billNo = billNoTxt == null ? IdUtil.simpleUUID() : billNoTxt; public String certPath;
Map<String, Object> params = new java.util.HashMap<>(Map.of( public String pubKey;
"appid", config.appId, public String apiCertKey;
"out_bill_no", billNo, public PrivateKey privateKey;
"transfer_scene_id", "1000", public String apiCert;
"openid", openId, public String platformCertPath;
"transfer_amount", amount.multiply(BigDecimal.valueOf(100)).intValue(), public String platformCertNo;
"transfer_remark", remark, public String mchId;
"notify_url", config.transferNotifyUrl, public String apiV3Key;
"transfer_scene_report_infos", new Object[]{ public String apiV2Key;
Map.of("info_type", "活动名称", "info_content", "分销奖励"), public String notifyUrl;
Map.of("info_type", "奖励说明", "info_content", "用户提现") public String refundNotifyUrl;
public String transferNotifyUrl;
} }
));
if (amount.compareTo(BigDecimal.valueOf(0.3)) >= 0) {
params.put("user_name", rsaEncryptOAEP(name));
}
log.info("转账到零钱参数: {}", JSONObject.toJSONString(params));
IJPayHttpResponse response = null;
try {
response = WxPayApi.v3(
RequestMethodEnum.POST,
WxDomainEnum.CHINA.toString(),
"/v3/fund-app/mch-transfer/transfer-bills",
config.mchId,
getSerialNumberFromPem(config.apiCert),
null,
config.privateKey,
JSONObject.toJSONString(params)
);
} catch (Exception e) {
throw new RuntimeException(e);
}
log.info("转账到零钱响应 {}", response.getBody());
JSONObject resp = JSONObject.parseObject(response.getBody());
if (resp.containsKey("code")) {
throw new CzgException("转账到零钱失败" + resp.getString("message") + ", 响应: " + resp.toJSONString());
}
return resp;
}
} }

View File

@@ -1,20 +1,23 @@
package com.czg.service.market.service.impl; 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.constants.ParamCodeCst;
import com.czg.service.RedisService; import com.czg.service.RedisService;
import com.czg.system.service.SysParamsService; import com.czg.system.service.SysParamsService;
import com.ijpay.core.kit.RsaKit; import com.ijpay.core.kit.RsaKit;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference; import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Set;
/** /**
* 微信支付service * 微信支付service
*
* @author Administrator * @author Administrator
*/ */
@Component @Component
@@ -24,27 +27,59 @@ public class WxServiceImpl extends BaseWx {
@DubboReference @DubboReference
private SysParamsService paramsService; 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) { public WxServiceImpl(@Autowired RedisService redisService) {
this.redisService = redisService; this.redisService = redisService;
config = new Config(); 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() { public void init() {
// 商户小程序参数
Map<String, String> shopMiniKeyMap = paramsService.getParamsByMap("shopMiniKeys", shopMiniKeys);
// 小程序id // 小程序id
config.appId = paramsService.getSysParamValue(ParamCodeCst.Wechat.Mini.SHOP_WX_APP_ID); config.appId = shopMiniKeyMap.get(ParamCodeCst.Wechat.Mini.SHOP_WX_APP_ID);
// 小程序secrete // 小程序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.certPath = "";
// 微信支付公钥 // 微信支付公钥
config.pubKey = paramsService.getSysParamValue(ParamCodeCst.Wechat.Pay.WX_PUB_KEY); config.pubKey = payKeyMap.get(ParamCodeCst.Wechat.Pay.WX_PUB_KEY);
// api支付证书私钥 // api支付证书私钥
config.apiCertKey = paramsService.getSysParamValue(ParamCodeCst.Wechat.Pay.WX_API_CLIENT_KEY); config.apiCertKey = payKeyMap.get(ParamCodeCst.Wechat.Pay.WX_API_CLIENT_KEY);
// api支付证书公钥 // api支付证书公钥
config.apiCert = paramsService.getSysParamValue(ParamCodeCst.Wechat.Pay.WX_API_CLIENT_CERT); config.apiCert = payKeyMap.get(ParamCodeCst.Wechat.Pay.WX_API_CLIENT_CERT);
try { try {
config.privateKey = RsaKit.loadPrivateKey(config.apiCertKey); config.privateKey = RsaKit.loadPrivateKey(config.apiCertKey);
} catch (Exception e) { } catch (Exception e) {
@@ -55,14 +90,14 @@ public class WxServiceImpl extends BaseWx {
// 平台证书编号 // 平台证书编号
config.platformCertNo = ""; config.platformCertNo = "";
// 商户号 // 商户号
config.mchId = paramsService.getSysParamValue(ParamCodeCst.Wechat.Pay.WX_MCH_ID); config.mchId = payKeyMap.get(ParamCodeCst.Wechat.Pay.WX_MCH_ID);
// v3密钥 // v3密钥
config.apiV3Key = paramsService.getSysParamValue(ParamCodeCst.Wechat.Pay.WX_V3_KEY); config.apiV3Key = payKeyMap.get(ParamCodeCst.Wechat.Pay.WX_V3_KEY);
config.apiV2Key = ""; 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.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() { public BaseWx getAppService() {

View File

@@ -19,9 +19,15 @@
<if test="shopId != null"> <if test="shopId != null">
and o.shop_id = #{shopId} and o.shop_id = #{shopId}
</if> </if>
<if test="userId != null"> <choose>
and o.user_id = #{userId} and o.status != 'ing' <!-- userId为空时拼接 and o.status != 'ing' -->
</if> <when test="userId == null or userId == ''">
and o.status != 'ing'
</when>
<otherwise>
and o.user_id = #{userId}
</otherwise>
</choose>
<if test="param.orderNo != null"> <if test="param.orderNo != null">
and o.order_no = #{param.orderNo} and o.order_no = #{param.orderNo}
</if> </if>

View File

@@ -1,6 +1,8 @@
package com.czg.service.system.service.impl; package com.czg.service.system.service.impl;
import cn.hutool.core.bean.BeanUtil; 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.resp.CzgResult;
import com.czg.sa.StpKit; import com.czg.sa.StpKit;
import com.czg.service.system.mapper.SysParamsMapper; 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 com.mybatisflex.spring.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboService; import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict; 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.*;
import java.util.List;
/** /**
* 服务层实现。 * 服务层实现。
@@ -25,7 +27,6 @@ import java.util.List;
*/ */
@Slf4j @Slf4j
@DubboService @DubboService
@CacheConfig(cacheNames = "params")
public class SysParamsServiceImpl extends ServiceImpl<SysParamsMapper, SysParams> implements SysParamsService { public class SysParamsServiceImpl extends ServiceImpl<SysParamsMapper, SysParams> implements SysParamsService {
@Override @Override
@@ -62,7 +63,10 @@ public class SysParamsServiceImpl extends ServiceImpl<SysParamsMapper, SysParams
} }
@Override @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) { public CzgResult<String> updateParams(SysParamsDTO paramsDTO) {
// 查询 paramCode 是否存在 // 查询 paramCode 是否存在
SysParams sysParams = getOne(new QueryWrapper().eq(SysParams::getParamCode, paramsDTO.getParamCode()) SysParams sysParams = getOne(new QueryWrapper().eq(SysParams::getParamCode, paramsDTO.getParamCode())
@@ -90,7 +94,10 @@ public class SysParamsServiceImpl extends ServiceImpl<SysParamsMapper, SysParams
} }
@Override @Override
@CacheEvict(key = "#code") @Caching(evict = {
@CacheEvict(cacheNames = "params:entity", allEntries = true),
@CacheEvict(cacheNames = "params", key = "#code")
})
public CzgResult<Boolean> deleteParams(String code) { public CzgResult<Boolean> deleteParams(String code) {
SysParams sysParams = getById(code); SysParams sysParams = getById(code);
if (sysParams == null) { if (sysParams == null) {
@@ -130,4 +137,18 @@ public class SysParamsServiceImpl extends ServiceImpl<SysParamsMapper, SysParams
} }
return sysParam.getParamValue(); 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;
}
} }