WxPayServiceImpl.java 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. package com.yingpai.gupiao.service.impl;
  2. import com.wechat.pay.java.core.Config;
  3. import com.wechat.pay.java.core.RSAAutoCertificateConfig;
  4. import com.wechat.pay.java.core.RSAPublicKeyConfig;
  5. import com.wechat.pay.java.core.notification.NotificationConfig;
  6. import com.wechat.pay.java.core.notification.NotificationParser;
  7. import com.wechat.pay.java.core.notification.RequestParam;
  8. import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
  9. import com.wechat.pay.java.service.payments.jsapi.model.*;
  10. import com.wechat.pay.java.service.payments.model.Transaction;
  11. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  12. import com.yingpai.gupiao.config.WxConfig;
  13. import com.yingpai.gupiao.domain.po.User;
  14. import com.yingpai.gupiao.domain.vo.WxPayVO;
  15. import com.yingpai.gupiao.mapper.UserMapper;
  16. import com.yingpai.gupiao.service.WxPayConfigService;
  17. import com.yingpai.gupiao.service.WxPayService;
  18. import lombok.RequiredArgsConstructor;
  19. import lombok.extern.slf4j.Slf4j;
  20. import org.springframework.core.io.ClassPathResource;
  21. import org.springframework.stereotype.Service;
  22. import java.io.BufferedReader;
  23. import java.io.ByteArrayInputStream;
  24. import java.io.InputStream;
  25. import java.io.InputStreamReader;
  26. import java.nio.charset.StandardCharsets;
  27. import java.security.cert.CertificateFactory;
  28. import java.security.cert.X509Certificate;
  29. import java.util.stream.Collectors;
  30. /**
  31. * 微信支付服务实现(微信支付SDK)
  32. */
  33. @Slf4j
  34. @Service
  35. @RequiredArgsConstructor
  36. public class WxPayServiceImpl implements WxPayService {
  37. private final WxConfig wxConfig;
  38. private final WxPayConfigService wxPayConfigService;
  39. private final UserMapper userMapper;
  40. private Config config;
  41. private JsapiServiceExtension jsapiService;
  42. private NotificationParser notificationParser;
  43. // 缓存当前配置的hash,用于检测配置变化
  44. private String currentConfigHash = "";
  45. /**
  46. * 获取配置hash,用于检测配置是否变化
  47. */
  48. private String getConfigHash() {
  49. String mchId = wxPayConfigService.getMchId();
  50. String apiV3Key = wxPayConfigService.getApiV3Key();
  51. String privateKeyPath = wxPayConfigService.getPrivateKeyPath();
  52. String certPath = wxPayConfigService.getCertPath();
  53. String publicKeyPath = wxPayConfigService.getPublicKeyPath();
  54. String publicKeyId = wxPayConfigService.getPublicKeyId();
  55. return mchId + "|" + apiV3Key + "|" + privateKeyPath + "|" + certPath + "|" + publicKeyPath + "|" + publicKeyId;
  56. }
  57. /**
  58. * 确保SDK已初始化,配置变化时自动重新初始化
  59. */
  60. private synchronized void ensureInitialized() {
  61. String newHash = getConfigHash();
  62. if (!newHash.equals(currentConfigHash)) {
  63. log.info("检测到支付配置变化,重新初始化SDK");
  64. init();
  65. currentConfigHash = newHash;
  66. }
  67. }
  68. private void init() {
  69. try {
  70. String mchId = wxPayConfigService.getMchId();
  71. String apiV3Key = wxPayConfigService.getApiV3Key();
  72. String privateKeyPath = wxPayConfigService.getPrivateKeyPath();
  73. String certPath = wxPayConfigService.getCertPath();
  74. String publicKeyPath = wxPayConfigService.getPublicKeyPath();
  75. String publicKeyId = wxPayConfigService.getPublicKeyId();
  76. if (mchId.isEmpty() || apiV3Key.isEmpty() || privateKeyPath.isEmpty()) {
  77. log.warn("微信支付配置不完整,跳过初始化");
  78. config = null;
  79. jsapiService = null;
  80. notificationParser = null;
  81. return;
  82. }
  83. // 读取私钥
  84. String privateKey = loadPrivateKey(privateKeyPath);
  85. // 从证书文件自动读取序列号
  86. String mchSerialNo = loadCertSerialNo(certPath);
  87. log.info("从证书读取序列号: {}", mchSerialNo);
  88. // 判断使用公钥模式还是自动证书模式
  89. if (!publicKeyPath.isEmpty() && !publicKeyId.isEmpty()) {
  90. // 使用微信支付公钥模式(新商户)
  91. log.info("使用微信支付公钥模式初始化,公钥路径: {}", publicKeyPath);
  92. String publicKey = loadPublicKey(publicKeyPath);
  93. config = new RSAPublicKeyConfig.Builder()
  94. .merchantId(mchId)
  95. .privateKey(privateKey)
  96. .merchantSerialNumber(mchSerialNo)
  97. .apiV3Key(apiV3Key)
  98. .publicKey(publicKey)
  99. .publicKeyId(publicKeyId)
  100. .build();
  101. } else {
  102. // 使用自动下载平台证书模式(老商户)
  103. log.info("使用自动证书模式初始化");
  104. config = new RSAAutoCertificateConfig.Builder()
  105. .merchantId(mchId)
  106. .privateKey(privateKey)
  107. .merchantSerialNumber(mchSerialNo)
  108. .apiV3Key(apiV3Key)
  109. .build();
  110. }
  111. // 初始化JSAPI服务
  112. jsapiService = new JsapiServiceExtension.Builder().config(config).build();
  113. // 初始化回调解析器
  114. notificationParser = new NotificationParser((NotificationConfig) config);
  115. log.info("微信支付SDK初始化成功,商户号: {}", mchId);
  116. } catch (Exception e) {
  117. log.error("微信支付SDK初始化失败", e);
  118. config = null;
  119. jsapiService = null;
  120. notificationParser = null;
  121. }
  122. }
  123. @Override
  124. public WxPayVO createPrepayOrder(String orderNo, String openid, Integer amount, String description) {
  125. log.info("创建预支付订单,orderNo: {}, openid: {}, amount: {}分", orderNo, openid, amount);
  126. // 确保SDK已初始化,配置变化时自动重新初始化
  127. ensureInitialized();
  128. if (jsapiService == null) {
  129. throw new RuntimeException("微信支付未初始化,请检查配置");
  130. }
  131. // 判断openid类型,选择对应的appid
  132. String appid = determineAppid(openid);
  133. log.info("使用appid: {}", appid);
  134. String notifyUrl = wxPayConfigService.getNotifyUrl();
  135. // 构建下单请求
  136. PrepayRequest request = new PrepayRequest();
  137. request.setAppid(appid);
  138. request.setMchid(wxPayConfigService.getMchId());
  139. request.setDescription(description);
  140. request.setOutTradeNo(orderNo);
  141. request.setNotifyUrl(notifyUrl);
  142. Amount amountObj = new Amount();
  143. amountObj.setTotal(amount);
  144. amountObj.setCurrency("CNY");
  145. request.setAmount(amountObj);
  146. Payer payer = new Payer();
  147. payer.setOpenid(openid);
  148. request.setPayer(payer);
  149. // 调用JSAPI下单并获取调起支付参数
  150. PrepayWithRequestPaymentResponse response = jsapiService.prepayWithRequestPayment(request);
  151. log.info("预支付成功,orderNo: {}", orderNo);
  152. return WxPayVO.builder()
  153. .orderNo(orderNo)
  154. .appId(appid)
  155. .timeStamp(response.getTimeStamp())
  156. .nonceStr(response.getNonceStr())
  157. .packageValue(response.getPackageVal())
  158. .signType(response.getSignType())
  159. .paySign(response.getPaySign())
  160. .totalFee(amount)
  161. .build();
  162. }
  163. /**
  164. * 根据openid判断应该使用哪个appid
  165. * @param openid 用户的openid(可能是H5的openid或小程序的mini_openid)
  166. * @return 对应的appid
  167. */
  168. private String determineAppid(String openid) {
  169. // 查询User表,判断openid是存储在openid字段还是mini_openid字段
  170. LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
  171. wrapper.eq(User::getMiniOpenid, openid);
  172. User userByMiniOpenid = userMapper.selectOne(wrapper);
  173. if (userByMiniOpenid != null) {
  174. // 是小程序openid,使用小程序appid
  175. log.info("检测到小程序openid,使用小程序appid");
  176. return wxConfig.getMiniappAppid();
  177. }
  178. // 否则是H5 openid,使用H5 appid
  179. log.info("检测到H5 openid,使用H5公众号appid");
  180. return wxConfig.getH5Appid();
  181. }
  182. @Override
  183. public String verifyAndDecryptNotify(String serialNumber, String nonce, String timestamp,
  184. String signature, String body) {
  185. log.info("验证支付回调,serialNumber: {}", serialNumber);
  186. // 确保SDK已初始化
  187. ensureInitialized();
  188. if (notificationParser == null) {
  189. throw new RuntimeException("微信支付未初始化");
  190. }
  191. // 构建回调参数
  192. RequestParam requestParam = new RequestParam.Builder()
  193. .serialNumber(serialNumber)
  194. .nonce(nonce)
  195. .timestamp(timestamp)
  196. .signature(signature)
  197. .body(body)
  198. .build();
  199. // 解析并验证回调(SDK自动验签和解密)
  200. Transaction transaction = notificationParser.parse(requestParam, Transaction.class);
  201. log.info("回调验证成功,orderNo: {}, transactionId: {}, state: {}",
  202. transaction.getOutTradeNo(), transaction.getTransactionId(), transaction.getTradeState());
  203. return transaction.getOutTradeNo() + "|" + transaction.getTransactionId() + "|" + transaction.getTradeState();
  204. }
  205. @Override
  206. public String queryPaymentStatus(String orderNo) {
  207. log.info("查询支付状态,orderNo: {}", orderNo);
  208. // 确保SDK已初始化
  209. ensureInitialized();
  210. if (jsapiService == null) {
  211. throw new RuntimeException("微信支付未初始化");
  212. }
  213. QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
  214. request.setMchid(wxPayConfigService.getMchId());
  215. request.setOutTradeNo(orderNo);
  216. Transaction transaction = jsapiService.queryOrderByOutTradeNo(request);
  217. return transaction.getTradeState().name();
  218. }
  219. @Override
  220. public String queryOrderByOutTradeNo(String orderNo) {
  221. log.info("查询微信订单,orderNo: {}", orderNo);
  222. // 确保SDK已初始化
  223. ensureInitialized();
  224. if (jsapiService == null) {
  225. throw new RuntimeException("微信支付未初始化");
  226. }
  227. QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
  228. request.setMchid(wxPayConfigService.getMchId());
  229. request.setOutTradeNo(orderNo);
  230. try {
  231. Transaction transaction = jsapiService.queryOrderByOutTradeNo(request);
  232. if (transaction.getTradeState() == Transaction.TradeStateEnum.SUCCESS) {
  233. return transaction.getTransactionId();
  234. }
  235. } catch (Exception e) {
  236. log.error("查询微信订单失败,orderNo: {}", orderNo, e);
  237. }
  238. return null;
  239. }
  240. @Override
  241. public boolean refund(String orderNo, String refundNo, Integer totalAmount,
  242. Integer refundAmount, String reason) {
  243. throw new UnsupportedOperationException("不支持退款");
  244. }
  245. /**
  246. * 加载私钥文件
  247. */
  248. private String loadPrivateKey(String path) {
  249. try {
  250. if (path.startsWith("classpath:")) {
  251. String resourcePath = path.replace("classpath:", "");
  252. ClassPathResource resource = new ClassPathResource(resourcePath);
  253. try (BufferedReader reader = new BufferedReader(
  254. new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
  255. return reader.lines().collect(Collectors.joining("\n"));
  256. }
  257. } else {
  258. return java.nio.file.Files.readString(java.nio.file.Paths.get(path));
  259. }
  260. } catch (Exception e) {
  261. log.error("加载私钥失败: {}", path, e);
  262. throw new RuntimeException("加载私钥失败", e);
  263. }
  264. }
  265. /**
  266. * 加载微信支付公钥文件
  267. */
  268. private String loadPublicKey(String path) {
  269. try {
  270. if (path.startsWith("classpath:")) {
  271. String resourcePath = path.replace("classpath:", "");
  272. ClassPathResource resource = new ClassPathResource(resourcePath);
  273. try (BufferedReader reader = new BufferedReader(
  274. new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
  275. return reader.lines().collect(Collectors.joining("\n"));
  276. }
  277. } else {
  278. return java.nio.file.Files.readString(java.nio.file.Paths.get(path));
  279. }
  280. } catch (Exception e) {
  281. log.error("加载公钥失败: {}", path, e);
  282. throw new RuntimeException("加载公钥失败", e);
  283. }
  284. }
  285. /**
  286. * 从证书文件读取序列号
  287. */
  288. private String loadCertSerialNo(String certPath) {
  289. try {
  290. String certContent;
  291. if (certPath.startsWith("classpath:")) {
  292. String resourcePath = certPath.replace("classpath:", "");
  293. ClassPathResource resource = new ClassPathResource(resourcePath);
  294. try (BufferedReader reader = new BufferedReader(
  295. new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
  296. certContent = reader.lines().collect(Collectors.joining("\n"));
  297. }
  298. } else {
  299. certContent = java.nio.file.Files.readString(java.nio.file.Paths.get(certPath));
  300. }
  301. // 解析PEM格式证书
  302. String certPem = certContent
  303. .replace("-----BEGIN CERTIFICATE-----", "")
  304. .replace("-----END CERTIFICATE-----", "")
  305. .replaceAll("\\s", "");
  306. byte[] certBytes = java.util.Base64.getDecoder().decode(certPem);
  307. CertificateFactory cf = CertificateFactory.getInstance("X.509");
  308. try (InputStream is = new ByteArrayInputStream(certBytes)) {
  309. X509Certificate cert = (X509Certificate) cf.generateCertificate(is);
  310. // 获取序列号并转为16进制大写
  311. String serialNo = cert.getSerialNumber().toString(16).toUpperCase();
  312. log.info("证书序列号: {}", serialNo);
  313. return serialNo;
  314. }
  315. } catch (Exception e) {
  316. log.error("读取证书序列号失败: {}", certPath, e);
  317. throw new RuntimeException("读取证书序列号失败", e);
  318. }
  319. }
  320. }