From 0dbda61bac200d184b58ae69c59c3dc8d173050a Mon Sep 17 00:00:00 2001 From: wangw <1594593906@qq.com> Date: Tue, 23 Dec 2025 10:45:22 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E6=94=AF=E4=BB=98=20?= =?UTF-8?q?=E9=97=AE=E9=A2=98=20=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../czg/system/service/SysParamsService.java | 15 + .../market/service/impl/AppWxServiceImpl.java | 73 ++- .../service/market/service/impl/BaseWx.java | 484 +++++++++--------- .../market/service/impl/WxServiceImpl.java | 63 ++- .../service/impl/SysParamsServiceImpl.java | 33 +- 5 files changed, 396 insertions(+), 272 deletions(-) diff --git a/cash-common/cash-common-service/src/main/java/com/czg/system/service/SysParamsService.java b/cash-common/cash-common-service/src/main/java/com/czg/system/service/SysParamsService.java index 037790472..1fef96027 100644 --- a/cash-common/cash-common-service/src/main/java/com/czg/system/service/SysParamsService.java +++ b/cash-common/cash-common-service/src/main/java/com/czg/system/service/SysParamsService.java @@ -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 { * @return 参数列表 */ CzgResult> getParamsByType(Integer type); + /** * 新增参数 * @@ -62,4 +66,15 @@ public interface SysParamsService extends IService { */ String getSysParamValue(String code); + + /** + * 根据参数类型获取参数 + * dubbo 调用需要 显式抛出 异常类型 + * + * @param type 参数类型 + * userMiniKeys 用户小程序参数 appId appSecret + * shopMiniKeys 商家小程序参数 appId appSecret + * payKeys 微信支付参数 + */ + Map getParamsByMap(String type, Set keyList) throws CzgException; } diff --git a/cash-service/market-service/src/main/java/com/czg/service/market/service/impl/AppWxServiceImpl.java b/cash-service/market-service/src/main/java/com/czg/service/market/service/impl/AppWxServiceImpl.java index b6fcd7d50..f66574f03 100644 --- a/cash-service/market-service/src/main/java/com/czg/service/market/service/impl/AppWxServiceImpl.java +++ b/cash-service/market-service/src/main/java/com/czg/service/market/service/impl/AppWxServiceImpl.java @@ -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 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 userMiniKeyMap = paramsService.getParamsByMap("userMiniKeys", USER_MINI_KEYS); + + // 微信支付参数 + Map 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() { diff --git a/cash-service/market-service/src/main/java/com/czg/service/market/service/impl/BaseWx.java b/cash-service/market-service/src/main/java/com/czg/service/market/service/impl/BaseWx.java index 78f42542c..9dbf42247 100644 --- a/cash-service/market-service/src/main/java/com/czg/service/market/service/impl/BaseWx.java +++ b/cash-service/market-service/src/main/java/com/czg/service/market/service/impl/BaseWx.java @@ -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 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 v3Pay(String openId, BigDecimal amount, String desc, String tradeNo, String type) { - + public Map 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 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 v2Pay(String openId, BigDecimal amount, String desc, String tradeNo) { Map 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 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 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 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; + } } diff --git a/cash-service/market-service/src/main/java/com/czg/service/market/service/impl/WxServiceImpl.java b/cash-service/market-service/src/main/java/com/czg/service/market/service/impl/WxServiceImpl.java index dffd12128..9ca60da80 100644 --- a/cash-service/market-service/src/main/java/com/czg/service/market/service/impl/WxServiceImpl.java +++ b/cash-service/market-service/src/main/java/com/czg/service/market/service/impl/WxServiceImpl.java @@ -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 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 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 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() { diff --git a/cash-service/system-service/src/main/java/com/czg/service/system/service/impl/SysParamsServiceImpl.java b/cash-service/system-service/src/main/java/com/czg/service/system/service/impl/SysParamsServiceImpl.java index 12118c3cb..3fdf8484e 100644 --- a/cash-service/system-service/src/main/java/com/czg/service/system/service/impl/SysParamsServiceImpl.java +++ b/cash-service/system-service/src/main/java/com/czg/service/system/service/impl/SysParamsServiceImpl.java @@ -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 implements SysParamsService { @Override @@ -62,7 +63,10 @@ public class SysParamsServiceImpl extends ServiceImpl updateParams(SysParamsDTO paramsDTO) { // 查询 paramCode 是否存在 SysParams sysParams = getOne(new QueryWrapper().eq(SysParams::getParamCode, paramsDTO.getParamCode()) @@ -90,7 +94,10 @@ public class SysParamsServiceImpl extends ServiceImpl deleteParams(String code) { SysParams sysParams = getById(code); if (sysParams == null) { @@ -130,4 +137,18 @@ public class SysParamsServiceImpl extends ServiceImpl getParamsByMap(String type, Set keyList) throws CzgException{ + Map 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; + } } From ebe2da9163b04f66d83b8a703ef20c72211396a0 Mon Sep 17 00:00:00 2001 From: wangw <1594593906@qq.com> Date: Tue, 23 Dec 2025 11:00:26 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=A7=AF=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/PointsConfigController.java | 27 ++++++++++++++++--- .../controller/user/UPointsController.java | 4 +-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/cash-api/market-server/src/main/java/com/czg/controller/admin/PointsConfigController.java b/cash-api/market-server/src/main/java/com/czg/controller/admin/PointsConfigController.java index 0b2c411e4..2cf246171 100644 --- a/cash-api/market-server/src/main/java/com/czg/controller/admin/PointsConfigController.java +++ b/cash-api/market-server/src/main/java/com/czg/controller/admin/PointsConfigController.java @@ -5,6 +5,7 @@ import com.czg.annotation.SaAdminCheckPermission; import com.czg.market.dto.MkPointsConfigDTO; import com.czg.market.dto.MkPointsUserDTO; import com.czg.market.entity.MkPointsConfig; +import com.czg.market.entity.MkPointsUser; import com.czg.market.entity.MkPointsUserRecord; import com.czg.market.service.MkPointsConfigService; import com.czg.market.service.MkPointsUserRecordService; @@ -12,15 +13,16 @@ import com.czg.market.service.MkPointsUserService; import com.czg.resp.CzgResult; import com.czg.sa.StpKit; import com.czg.utils.CzgStrUtils; -import com.czg.validator.ValidatorUtil; -import com.czg.validator.group.DefaultGroup; -import com.czg.validator.group.InsertGroup; import com.mybatisflex.core.paginate.Page; +import com.mybatisflex.core.query.QueryWrapper; import jakarta.annotation.Resource; import jakarta.validation.constraints.Pattern; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.util.HashMap; +import java.util.Map; + /** * 积分配置 @@ -88,4 +90,23 @@ public class PointsConfigController { @RequestParam(required = false) Long id) { return CzgResult.success(userRecordService.pageByPointsUserId(page, size, id)); } + + /** + * 获取用户积分 包括配置信息 + * { + * "pointsConfig": 配置信息, + * "pointsUser": 用户积分信息 + * } + */ + @GetMapping("userPoints") + public CzgResult> userPoints(@RequestParam(required = false) Long shopUserId) { + Long shopId = StpKit.USER.getShopId(); + Map result = new HashMap<>(2); + MkPointsConfig pointsConfig = pointsConfigService.getById(shopId); + MkPointsUser pointsUser = pointsUserService.getOne(QueryWrapper.create().eq(MkPointsUser::getShopId, shopId).eq(MkPointsUser::getShopUserId, shopUserId)); + result.put("pointsConfig", pointsConfig == null ? "" : pointsConfig); + result.put("pointsUser", pointsUser == null ? "" : pointsUser); + return CzgResult.success(result); + } + } \ No newline at end of file diff --git a/cash-api/market-server/src/main/java/com/czg/controller/user/UPointsController.java b/cash-api/market-server/src/main/java/com/czg/controller/user/UPointsController.java index 6e487ff5d..914413918 100644 --- a/cash-api/market-server/src/main/java/com/czg/controller/user/UPointsController.java +++ b/cash-api/market-server/src/main/java/com/czg/controller/user/UPointsController.java @@ -60,10 +60,10 @@ public class UPointsController { Long shopId = StpKit.USER.getShopId(); Map result = new HashMap<>(2); MkPointsConfig pointsConfig = pointsConfigService.getById(shopId); - MkPointsUser pointsUser = pointsUserService.getOne(QueryWrapper.create().eq(MkPointsUser::getShopId, shopId).eq(MkPointsUser::getUserId, shopUserId)); + MkPointsUser pointsUser = pointsUserService.getOne(QueryWrapper.create().eq(MkPointsUser::getShopId, shopId).eq(MkPointsUser::getShopUserId, shopUserId)); result.put("pointsConfig", pointsConfig == null ? "" : pointsConfig); result.put("pointsUser", pointsUser == null ? "" : pointsUser); - return CzgResult.success(); + return CzgResult.success(result); } /** From f88612d1156215b09703f2043d5584932bc28605 Mon Sep 17 00:00:00 2001 From: wangw <1594593906@qq.com> Date: Tue, 23 Dec 2025 11:48:54 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=A7=AF=E5=88=86?= =?UTF-8?q?=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/market/service/impl/MkPointsUserServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cash-service/market-service/src/main/java/com/czg/service/market/service/impl/MkPointsUserServiceImpl.java b/cash-service/market-service/src/main/java/com/czg/service/market/service/impl/MkPointsUserServiceImpl.java index 1c56df230..ca68c3b0c 100644 --- a/cash-service/market-service/src/main/java/com/czg/service/market/service/impl/MkPointsUserServiceImpl.java +++ b/cash-service/market-service/src/main/java/com/czg/service/market/service/impl/MkPointsUserServiceImpl.java @@ -70,7 +70,7 @@ public class MkPointsUserServiceImpl extends ServiceImpl pointsShopList(Long userId, String shopName) { - return listAs(query().select(MkPointsUser::getId, MkPointsUser::getShopId).select(ShopInfo::getShopName, ShopInfo::getLogo, ShopInfo::getCoverImg) + return listAs(query().select(MkPointsUser::getId, MkPointsUser::getShopId, MkPointsUser::getPointBalance).select(ShopInfo::getShopName, ShopInfo::getLogo, ShopInfo::getCoverImg) .eq(MkPointsUser::getUserId, userId).leftJoin(ShopInfo.class).on(MkPointsUser::getShopId, ShopInfo::getId), PointsShopListVO.class); } From 56ab4d04038afeef4df4b2e38bc0d431b3a189fa Mon Sep 17 00:00:00 2001 From: wangw <1594593906@qq.com> Date: Tue, 23 Dec 2025 13:28:34 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=A7=AF=E5=88=86?= =?UTF-8?q?=E5=88=97=E8=A1=A82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/czg/account/vo/PointsShopListVO.java | 2 +- .../com/czg/market/service/MkPointsUserRecordService.java | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/cash-common/cash-common-service/src/main/java/com/czg/account/vo/PointsShopListVO.java b/cash-common/cash-common-service/src/main/java/com/czg/account/vo/PointsShopListVO.java index b83c450ac..a93a1b07d 100644 --- a/cash-common/cash-common-service/src/main/java/com/czg/account/vo/PointsShopListVO.java +++ b/cash-common/cash-common-service/src/main/java/com/czg/account/vo/PointsShopListVO.java @@ -11,5 +11,5 @@ public class PointsShopListVO { private String logo; private String coverImg; private Long shopId; - private Integer accountPoints; + private Integer pointBalance; } diff --git a/cash-common/cash-common-service/src/main/java/com/czg/market/service/MkPointsUserRecordService.java b/cash-common/cash-common-service/src/main/java/com/czg/market/service/MkPointsUserRecordService.java index 394985b26..875fcbc31 100644 --- a/cash-common/cash-common-service/src/main/java/com/czg/market/service/MkPointsUserRecordService.java +++ b/cash-common/cash-common-service/src/main/java/com/czg/market/service/MkPointsUserRecordService.java @@ -1,11 +1,8 @@ package com.czg.market.service; -import com.czg.account.vo.PointsShopListVO; +import com.czg.market.entity.MkPointsUserRecord; import com.mybatisflex.core.paginate.Page; import com.mybatisflex.core.service.IService; -import com.czg.market.entity.MkPointsUserRecord; - -import java.util.List; /** * 会员积分变动记录 服务层。 From 81e102ff4138d70c0cb4f30f05f995b2aa3694e3 Mon Sep 17 00:00:00 2001 From: gong <1157756119@qq.com> Date: Tue, 23 Dec 2025 13:35:42 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E8=AE=A2=E5=8D=95=E6=9F=A5=E8=AF=A2=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/resources/mapper/PpPackageOrderMapper.xml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/cash-service/market-service/src/main/resources/mapper/PpPackageOrderMapper.xml b/cash-service/market-service/src/main/resources/mapper/PpPackageOrderMapper.xml index 489fc33e2..d5f1b2419 100644 --- a/cash-service/market-service/src/main/resources/mapper/PpPackageOrderMapper.xml +++ b/cash-service/market-service/src/main/resources/mapper/PpPackageOrderMapper.xml @@ -19,9 +19,15 @@ and o.shop_id = #{shopId} - - and o.user_id = #{userId} and o.status != 'ing' - + + + + and o.status != 'ing' + + + and o.user_id = #{userId} + + and o.order_no = #{param.orderNo}