PayController.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. package com.ylx.web.controller.massage;
  2. import cn.hutool.core.date.DatePattern;
  3. import cn.hutool.core.date.DateUtil;
  4. import cn.hutool.core.io.file.FileWriter;
  5. import cn.hutool.core.util.StrUtil;
  6. import cn.hutool.http.ContentType;
  7. import cn.hutool.json.JSONArray;
  8. import cn.hutool.json.JSONObject;
  9. import cn.hutool.json.JSONUtil;
  10. import com.ijpay.core.IJPayHttpResponse;
  11. import com.ijpay.core.enums.AuthTypeEnum;
  12. import com.ijpay.core.enums.RequestMethodEnum;
  13. import com.ijpay.core.kit.AesUtil;
  14. import com.ijpay.core.kit.HttpKit;
  15. import com.ijpay.core.kit.PayKit;
  16. import com.ijpay.core.kit.WxPayKit;
  17. import com.ijpay.core.utils.DateTimeZoneUtil;
  18. import com.ijpay.wxpay.WxPayApi;
  19. import com.ijpay.wxpay.WxPayApiConfigKit;
  20. import com.ijpay.wxpay.enums.WxDomainEnum;
  21. import com.ijpay.wxpay.enums.v3.BasePayApiEnum;
  22. import com.ijpay.wxpay.enums.v3.CertAlgorithmTypeEnum;
  23. import com.ijpay.wxpay.enums.v3.TransferApiEnum;
  24. import com.ijpay.wxpay.model.v3.*;
  25. import com.ylx.common.config.WxPayConfig;
  26. import com.ylx.common.core.domain.R;
  27. import com.ylx.massage.domain.TRecharge;
  28. import com.ylx.massage.enums.BillTypeEnum;
  29. import com.ylx.massage.service.TOrderService;
  30. import com.ylx.massage.service.TRechargeService;
  31. import io.swagger.annotations.Api;
  32. import io.swagger.annotations.ApiOperation;
  33. import lombok.extern.slf4j.Slf4j;
  34. import org.springframework.beans.factory.annotation.Autowired;
  35. import org.springframework.web.bind.annotation.*;
  36. import javax.annotation.Resource;
  37. import javax.servlet.http.HttpServletRequest;
  38. import javax.servlet.http.HttpServletResponse;
  39. import java.io.ByteArrayInputStream;
  40. import java.math.BigDecimal;
  41. import java.nio.charset.StandardCharsets;
  42. import java.security.cert.X509Certificate;
  43. import java.util.*;
  44. import static com.ylx.common.constant.HttpStatus.SUCCESS;
  45. /**
  46. * @author jianlong
  47. * @date 2024-04-03 15:27
  48. */
  49. @RestController
  50. @Slf4j
  51. @RequestMapping("/wx/pay")
  52. @Api(tags = {"微信支付"})
  53. public class PayController {
  54. @Autowired
  55. private WxPayConfig wxPayProperties;
  56. @Resource
  57. private TRechargeService rechargeService;
  58. @Resource
  59. private TOrderService orderService;
  60. String serialNo;
  61. String platSerialNo;
  62. /**
  63. * 小程序微信支付的第一步,统一下单
  64. */
  65. @PostMapping("/pay")
  66. @ApiOperation("AIPV3微信支付充值")
  67. public R createUnifiedOrder(@RequestBody TRecharge recharge) throws Exception {
  68. TRecharge rechargeResp = rechargeService.recharge(recharge);
  69. return rechargeService.getPay(rechargeResp.getRechargeNo(), recharge.getdMoney(), recharge.getcOpenId(), BillTypeEnum.RECHARGE.getInfo(), BillTypeEnum.RECHARGE.getCode().toString());
  70. }
  71. public R<String> getPay(String setOutTradeNo, BigDecimal amount, String openId, String description) throws Exception {
  72. String timeExpire = DateTimeZoneUtil.dateToTimeZone(System.currentTimeMillis() + 1000 * 60 * 3);
  73. UnifiedOrderModel unifiedOrderModel = new UnifiedOrderModel()
  74. .setAppid(wxPayProperties.getAppId())
  75. .setMchid(wxPayProperties.getMchId())
  76. //商品描述
  77. .setDescription(description)
  78. //订单号
  79. .setOut_trade_no(setOutTradeNo)
  80. //交易结束时间
  81. .setTime_expire(timeExpire)
  82. //附加数据
  83. .setAttach("夜来香")
  84. //通知地址异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 公网域名必须为https,如果是走专线接入,使用专线NAT IP或者私有回调域名可使用http
  85. //示例值:https://www.weixin.qq.com/wxpay/pay.php
  86. .setNotify_url(wxPayProperties.getNotifyUrl())
  87. //支付金额以分为单位
  88. .setAmount(new Amount().setTotal(amount.multiply(new BigDecimal(100)).intValue()))
  89. //交易人
  90. .setPayer(new Payer().setOpenid(openId));
  91. log.info("统一下单参数 {}", JSONUtil.toJsonStr(unifiedOrderModel));
  92. IJPayHttpResponse response = WxPayApi.v3(
  93. RequestMethodEnum.POST,
  94. WxDomainEnum.CHINA.toString(),
  95. BasePayApiEnum.JS_API_PAY.toString(),
  96. wxPayProperties.getMchId(),
  97. getSerialNumber(),
  98. null,
  99. wxPayProperties.getCertKeyPath(),
  100. JSONUtil.toJsonStr(unifiedOrderModel)
  101. );
  102. log.info("统一下单响应 {}", response);
  103. // 根据证书序列号查询对应的证书来验证签名结果
  104. boolean verifySignature = WxPayKit.verifySignature(response, wxPayProperties.getPlatFormPath());
  105. log.info("verifySignature: {}", verifySignature);
  106. if (response.getStatus() == SUCCESS && verifySignature) {
  107. String body = response.getBody();
  108. JSONObject jsonObject = JSONUtil.parseObj(body);
  109. String prepayId = jsonObject.getStr("prepay_id");
  110. Map<String, String> map = WxPayKit.jsApiCreateSign(wxPayProperties.getAppId(), prepayId, wxPayProperties.getCertKeyPath());
  111. log.info("唤起支付参数:{}", map);
  112. return R.ok(JSONUtil.toJsonStr(map));
  113. }
  114. return R.ok(JSONUtil.toJsonStr(response));
  115. }
  116. private String getSerialNumber() {
  117. if (StrUtil.isEmpty(serialNo)) {
  118. // 获取证书序列号
  119. X509Certificate certificate = PayKit.getCertificate(wxPayProperties.getCertPath());
  120. if (null != certificate) {
  121. serialNo = certificate.getSerialNumber().toString(16).toUpperCase();
  122. // 提前两天检查证书是否有效
  123. boolean isValid = PayKit.checkCertificateIsValid(certificate, wxPayProperties.getMchId(), -2);
  124. log.info("证书是否可用 {} 证书有效期为 {}", isValid, DateUtil.format(certificate.getNotAfter(), DatePattern.NORM_DATETIME_PATTERN));
  125. }
  126. // System.out.println("输出证书信息:\n" + certificate.toString());
  127. // // 输出关键信息,截取部分并进行标记
  128. // System.out.println("证书序列号:" + certificate.getSerialNumber().toString(16));
  129. // System.out.println("版本号:" + certificate.getVersion());
  130. // System.out.println("签发者:" + certificate.getIssuerDN());
  131. // System.out.println("有效起始日期:" + certificate.getNotBefore());
  132. // System.out.println("有效终止日期:" + certificate.getNotAfter());
  133. // System.out.println("主体名:" + certificate.getSubjectDN());
  134. // System.out.println("签名算法:" + certificate.getSigAlgName());
  135. // System.out.println("签名:" + certificate.getSignature().toString());
  136. }
  137. System.out.println("serialNo:" + serialNo);
  138. return serialNo;
  139. }
  140. @RequestMapping(value = "/payNotify", method = {org.springframework.web.bind.annotation.RequestMethod.POST, org.springframework.web.bind.annotation.RequestMethod.GET})
  141. @ResponseBody
  142. @ApiOperation("微信支付回调接口")
  143. public void payNotify(HttpServletRequest request, HttpServletResponse response) {
  144. log.info("微信支付回调接口====================================>>>>微信支付回调接口");
  145. Map<String, String> map = new HashMap<>(12);
  146. try {
  147. String timestamp = request.getHeader("Wechatpay-Timestamp");
  148. String nonce = request.getHeader("Wechatpay-Nonce");
  149. String serialNo = request.getHeader("Wechatpay-Serial");
  150. String signature = request.getHeader("Wechatpay-Signature");
  151. log.info("timestamp:{} nonce:{} serialNo:{} signature:{}", timestamp, nonce, serialNo, signature);
  152. String result = HttpKit.readData(request);
  153. log.info("支付通知密文 {}", result);
  154. // 需要通过证书序列号查找对应的证书,verifyNotify 中有验证证书的序列号
  155. String plainText = WxPayKit.verifyNotify(serialNo, result, signature, nonce, timestamp,
  156. wxPayProperties.getMchKey(), wxPayProperties.getPlatFormPath());
  157. log.info("支付通知明文 {}", plainText);
  158. if (StrUtil.isNotEmpty(plainText)) {
  159. response.setStatus(200);
  160. map.put("code", "SUCCESS");
  161. map.put("message", "SUCCESS");
  162. // 处理业务逻辑
  163. JSONObject jsonObject = new JSONObject(plainText);
  164. if (jsonObject.get("attach").equals(BillTypeEnum.WX_PAY.getCode().toString())) {
  165. // 订单支付成功
  166. orderService.payNotifyOrder(jsonObject.get("out_trade_no").toString());
  167. } else {
  168. TRecharge outTradeNo = rechargeService.increaseAmount(jsonObject.get("out_trade_no").toString());
  169. }
  170. } else {
  171. response.setStatus(500);
  172. map.put("code", "ERROR");
  173. map.put("message", "签名错误");
  174. }
  175. response.setHeader("Content-type", ContentType.JSON.toString());
  176. response.getOutputStream().write(JSONUtil.toJsonStr(map).getBytes(StandardCharsets.UTF_8));
  177. response.flushBuffer();
  178. } catch (Exception e) {
  179. log.error("系统异常", e);
  180. }
  181. }
  182. @RequestMapping(value = "/test", method = {org.springframework.web.bind.annotation.RequestMethod.POST, org.springframework.web.bind.annotation.RequestMethod.GET})
  183. @ResponseBody
  184. @ApiOperation("测试")
  185. public void test(HttpServletRequest request, HttpServletResponse response) {
  186. System.out.println("test=======================>");
  187. }
  188. @RequestMapping("/get")
  189. @ResponseBody
  190. public String v3Get() throws Exception {
  191. // 获取平台证书列表
  192. try {
  193. IJPayHttpResponse response = WxPayApi.v3(
  194. RequestMethodEnum.GET,
  195. WxDomainEnum.CHINA.toString(),
  196. CertAlgorithmTypeEnum.getCertSuffixUrl(CertAlgorithmTypeEnum.RSA.getCode()),
  197. wxPayProperties.getMchId(),
  198. getSerialNumber(),
  199. null,
  200. wxPayProperties.getCertKeyPath(),
  201. "",
  202. AuthTypeEnum.RSA.getCode()
  203. );
  204. Map<String, List<String>> headers = response.getHeaders();
  205. log.info("请求头: {}", headers);
  206. String timestamp = response.getHeader("Wechatpay-Timestamp");
  207. String nonceStr = response.getHeader("Wechatpay-Nonce");
  208. String serialNumber = response.getHeader("Wechatpay-Serial");
  209. String signature = response.getHeader("Wechatpay-Signature");
  210. String body = response.getBody();
  211. int status = response.getStatus();
  212. log.info("serialNumber: {}", serialNumber);
  213. log.info("status: {}", status);
  214. log.info("body: {}", body);
  215. int isOk = 200;
  216. if (status == isOk) {
  217. JSONObject jsonObject = JSONUtil.parseObj(body);
  218. JSONArray dataArray = jsonObject.getJSONArray("data");
  219. // 默认认为只有一个平台证书
  220. JSONObject encryptObject = dataArray.getJSONObject(0);
  221. JSONObject encryptCertificate = encryptObject.getJSONObject("encrypt_certificate");
  222. String associatedData = encryptCertificate.getStr("associated_data");
  223. String cipherText = encryptCertificate.getStr("ciphertext");
  224. String nonce = encryptCertificate.getStr("nonce");
  225. String algorithm = encryptCertificate.getStr("algorithm");
  226. String serialNo = encryptObject.getStr("serial_no");
  227. final String platSerialNo = savePlatformCert(associatedData, nonce, cipherText, algorithm, wxPayProperties.getPlatFormPath());
  228. log.info("平台证书序列号: {} serialNo: {}", platSerialNo, serialNo);
  229. // 根据证书序列号查询对应的证书来验证签名结果
  230. boolean verifySignature = WxPayKit.verifySignature(response, wxPayProperties.getPlatFormPath());
  231. log.info("verifySignature:{}", verifySignature);
  232. }
  233. return body;
  234. } catch (Exception e) {
  235. log.error("获取平台证书列表异常", e);
  236. return null;
  237. }
  238. }
  239. private String savePlatformCert(String associatedData, String nonce, String cipherText, String algorithm, String certPath) {
  240. try {
  241. String key3 = wxPayProperties.getMchKey();
  242. String publicKey;
  243. if (StrUtil.equals(algorithm, AuthTypeEnum.SM2.getPlatformCertAlgorithm())) {
  244. publicKey = PayKit.sm4DecryptToString(key3, cipherText, nonce, associatedData);
  245. } else {
  246. AesUtil aesUtil = new AesUtil(wxPayProperties.getMchKey().getBytes(StandardCharsets.UTF_8));
  247. // 平台证书密文解密
  248. // encrypt_certificate 中的 associated_data nonce ciphertext
  249. publicKey = aesUtil.decryptToString(
  250. associatedData.getBytes(StandardCharsets.UTF_8),
  251. nonce.getBytes(StandardCharsets.UTF_8),
  252. cipherText
  253. );
  254. }
  255. if (StrUtil.isNotEmpty(publicKey)) {
  256. // 保存证书
  257. FileWriter writer = new FileWriter(certPath);
  258. writer.write(publicKey);
  259. // 获取平台证书序列号
  260. X509Certificate certificate = PayKit.getCertificate(new ByteArrayInputStream(publicKey.getBytes()));
  261. return certificate.getSerialNumber().toString(16).toUpperCase();
  262. }
  263. return "";
  264. } catch (Exception e) {
  265. log.error("保存平台证书异常", e);
  266. return e.getMessage();
  267. }
  268. }
  269. @RequestMapping("/batchTransfer")
  270. @ApiOperation("微信批量提现")
  271. @ResponseBody
  272. public String batchTransfer(@RequestParam(value = "openId", required = false, defaultValue = "o-_-itxuXeGW3O1cxJ7FXNmq8Wf8") String openId) {
  273. try {
  274. BatchTransferModel batchTransferModel = new BatchTransferModel()
  275. .setAppid(wxPayProperties.getAppId())
  276. .setOut_batch_no(PayKit.generateStr())
  277. .setBatch_name("IJPay 测试微信转账到零钱")
  278. .setBatch_remark("IJPay 测试微信转账到零钱")
  279. .setTotal_amount(1)
  280. .setTotal_num(1)
  281. .setTransfer_detail_list(Collections.singletonList(
  282. new TransferDetailInput()
  283. .setOut_detail_no(PayKit.generateStr())
  284. .setTransfer_amount(1)
  285. .setTransfer_remark("IJPay 测试微信转账到零钱")
  286. .setOpenid(openId)));
  287. log.info("发起商家转账请求参数 {}", JSONUtil.toJsonStr(batchTransferModel));
  288. IJPayHttpResponse response = WxPayApi.v3(
  289. RequestMethodEnum.POST,
  290. WxDomainEnum.CHINA.toString(),
  291. TransferApiEnum.TRANSFER_BATCHES.toString(),
  292. wxPayProperties.getMchId(),
  293. getSerialNumber(),
  294. null,
  295. wxPayProperties.getCertKeyPath(),
  296. JSONUtil.toJsonStr(batchTransferModel)
  297. );
  298. log.info("发起商家转账响应 {}", response);
  299. // 根据证书序列号查询对应的证书来验证签名结果
  300. boolean verifySignature = WxPayKit.verifySignature(response, wxPayProperties.getPlatFormPath());
  301. log.info("verifySignature: {}", verifySignature);
  302. if (response.getStatus() == SUCCESS && verifySignature) {
  303. return response.getBody();
  304. }
  305. return JSONUtil.toJsonStr(response);
  306. } catch (Exception e) {
  307. log.error("系统异常", e);
  308. return e.getMessage();
  309. }
  310. }
  311. @RequestMapping("/refund")
  312. @ResponseBody
  313. public String refund(@RequestParam(required = false) String outRefundNo, @RequestParam(required = false) String transactionId, @RequestParam(required = false) String outTradeNo) {
  314. try {
  315. log.info("商户退款单号: {}", outRefundNo);
  316. //退款商品
  317. List<RefundGoodsDetail> list = new ArrayList<>();
  318. RefundGoodsDetail refundGoodsDetail = new RefundGoodsDetail()
  319. //商户侧商品编码
  320. .setMerchant_goods_id("123")
  321. //商品名称
  322. .setGoods_name("IJPay 测试")
  323. //商品单价
  324. .setUnit_price(1)
  325. //商品退款金额
  326. .setRefund_amount(1)
  327. //商品退货数量
  328. .setRefund_quantity(1);
  329. list.add(refundGoodsDetail);
  330. RefundModel refundModel = new RefundModel()
  331. //商户退款单号
  332. .setOut_refund_no(outRefundNo)
  333. //退款原因
  334. .setReason("IJPay 测试退款")
  335. //回调地址
  336. .setNotify_url(wxPayProperties.getRefundNotify())
  337. //金额信息
  338. .setAmount(new RefundAmount().setRefund(1).setTotal(1).setCurrency("CNY"))
  339. //退款商品
  340. .setGoods_detail(list);
  341. //商户订单号
  342. if (StrUtil.isNotEmpty(transactionId)) {
  343. refundModel.setTransaction_id(transactionId);
  344. }
  345. if (StrUtil.isNotEmpty(outTradeNo)) {
  346. refundModel.setOut_trade_no(outTradeNo);
  347. }
  348. log.info("退款参数 {}", JSONUtil.toJsonStr(refundModel));
  349. IJPayHttpResponse response = WxPayApi.v3(
  350. RequestMethodEnum.POST,
  351. WxDomainEnum.CHINA.toString(),
  352. BasePayApiEnum.REFUND.toString(),
  353. wxPayProperties.getMchId(),
  354. getSerialNumber(),
  355. null,
  356. wxPayProperties.getCertKeyPath(),
  357. JSONUtil.toJsonStr(refundModel)
  358. );
  359. // 根据证书序列号查询对应的证书来验证签名结果
  360. boolean verifySignature = WxPayKit.verifySignature(response, wxPayProperties.getPlatFormPath());
  361. log.info("verifySignature: {}", verifySignature);
  362. log.info("退款响应 {}", response);
  363. if (verifySignature) {
  364. return response.getBody();
  365. }
  366. } catch (Exception e) {
  367. log.error("系统异常", e);
  368. return e.getMessage();
  369. }
  370. return null;
  371. }
  372. /**
  373. * 退款通知
  374. */
  375. @RequestMapping(value = "/refundNotify", method = {RequestMethod.POST, RequestMethod.GET})
  376. @ResponseBody
  377. @ApiOperation("微信退款回调接口")
  378. public String refundNotify(HttpServletRequest request) {
  379. String xmlMsg = HttpKit.readData(request);
  380. log.info("退款通知=" + xmlMsg);
  381. Map<String, String> params = WxPayKit.xmlToMap(xmlMsg);
  382. String returnCode = params.get("return_code");
  383. // 注意重复通知的情况,同一订单号可能收到多次通知,请注意一定先判断订单状态
  384. if (WxPayKit.codeIsOk(returnCode)) {
  385. String reqInfo = params.get("req_info");
  386. String decryptData = WxPayKit.decryptData(reqInfo, WxPayApiConfigKit.getWxPayApiConfig().getPartnerKey());
  387. log.info("退款通知解密后的数据=" + decryptData);
  388. // 更新订单信息
  389. // 发送通知等
  390. Map<String, String> xml = new HashMap<String, String>(2);
  391. xml.put("return_code", "SUCCESS");
  392. xml.put("return_msg", "OK");
  393. return WxPayKit.toXml(xml);
  394. }
  395. return null;
  396. }
  397. }