WxPayServiceImpl.java 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. package com.kym.service.wechat.impl;
  2. import cn.dev33.satoken.stp.StpUtil;
  3. import cn.hutool.core.date.DateTime;
  4. import cn.hutool.core.date.DateUtil;
  5. import cn.hutool.core.date.LocalDateTimeUtil;
  6. import cn.hutool.core.io.IoUtil;
  7. import cn.hutool.core.util.RandomUtil;
  8. import com.alibaba.fastjson2.JSONObject;
  9. import com.kym.common.config.WxPayConfig;
  10. import com.kym.common.constant.ResponseEnum;
  11. import com.kym.common.exception.BusinessException;
  12. import com.kym.common.utils.CommUtil;
  13. import com.kym.common.utils.LambadaTools;
  14. import com.kym.common.utils.OrderUtils;
  15. import com.kym.entity.Account;
  16. import com.kym.entity.*;
  17. import com.kym.service.*;
  18. import com.kym.service.wechat.WxPayService;
  19. import com.wechat.pay.java.core.Config;
  20. import com.wechat.pay.java.core.RSAAutoCertificateConfig;
  21. import com.wechat.pay.java.core.exception.ValidationException;
  22. import com.wechat.pay.java.core.http.DefaultHttpClientBuilder;
  23. import com.wechat.pay.java.core.http.okhttp.OkHttpClientAdapter;
  24. import com.wechat.pay.java.core.notification.NotificationConfig;
  25. import com.wechat.pay.java.core.notification.NotificationParser;
  26. import com.wechat.pay.java.core.notification.RequestParam;
  27. import com.wechat.pay.java.service.payments.jsapi.JsapiService;
  28. import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
  29. import com.wechat.pay.java.service.payments.jsapi.model.Amount;
  30. import com.wechat.pay.java.service.payments.jsapi.model.*;
  31. import com.wechat.pay.java.service.payments.model.Transaction;
  32. import com.wechat.pay.java.service.refund.RefundService;
  33. import com.wechat.pay.java.service.refund.model.*;
  34. import jakarta.annotation.PostConstruct;
  35. import jakarta.servlet.ServletInputStream;
  36. import jakarta.servlet.http.HttpServletRequest;
  37. import lombok.SneakyThrows;
  38. import org.slf4j.Logger;
  39. import org.slf4j.LoggerFactory;
  40. import org.springframework.core.io.ClassPathResource;
  41. import org.springframework.http.HttpStatus;
  42. import org.springframework.http.ResponseEntity;
  43. import org.springframework.stereotype.Service;
  44. import org.springframework.transaction.annotation.Transactional;
  45. import java.io.ByteArrayOutputStream;
  46. import java.io.File;
  47. import java.io.FileInputStream;
  48. import java.io.IOException;
  49. import java.math.BigDecimal;
  50. import java.math.RoundingMode;
  51. import java.nio.charset.StandardCharsets;
  52. import java.time.LocalDateTime;
  53. import java.util.ArrayList;
  54. import java.util.Map;
  55. import java.util.concurrent.atomic.AtomicInteger;
  56. /**
  57. * @author skyline
  58. * @description 微信支付
  59. * @date 2023-08-10 14:03
  60. */
  61. @Service
  62. public class WxPayServiceImpl implements WxPayService {
  63. private static final Logger LOGGER = LoggerFactory.getLogger(WxPayServiceImpl.class);
  64. public static JsapiService jsapiService;
  65. public static RefundService refundService;
  66. public static Config config;
  67. private final WxPayConfig conf;
  68. private final WalletDetailService walletDetailService;
  69. private final PayLogService payLogService;
  70. private final AccountService accountService;
  71. private final RefundLogService refundLogService;
  72. private final ActivityService activityService;
  73. private final UserRechargeRightsService userRechargeRightsService;
  74. private final RechargeConfigService rechargeConfigService;
  75. private final StationAccountService stationAccountService;
  76. private final SplitRecordService splitRecordService;
  77. /**
  78. * 微信支付专用,支持自动签名验签解密等
  79. */
  80. private OkHttpClientAdapter wxHttpClient;
  81. public WxPayServiceImpl(WxPayConfig conf, WalletDetailService walletDetailService,
  82. PayLogService payLogService, AccountService accountService,
  83. RefundLogService refundLogService,
  84. ActivityService activityService, UserRechargeRightsService userRechargeRightsService,
  85. RechargeConfigService rechargeConfigService, StationAccountService stationAccountService, SplitRecordService splitRecordService) {
  86. this.conf = conf;
  87. this.walletDetailService = walletDetailService;
  88. this.payLogService = payLogService;
  89. this.accountService = accountService;
  90. this.refundLogService = refundLogService;
  91. this.activityService = activityService;
  92. this.userRechargeRightsService = userRechargeRightsService;
  93. this.rechargeConfigService = rechargeConfigService;
  94. this.stationAccountService = stationAccountService;
  95. this.splitRecordService = splitRecordService;
  96. }
  97. /**
  98. * 商户订单号查询订单
  99. */
  100. public static Transaction queryOrderByOutTradeNo() {
  101. QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
  102. // 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
  103. // 调用接口
  104. return jsapiService.queryOrderByOutTradeNo(request);
  105. }
  106. /**
  107. * 初始化
  108. *
  109. * @throws IOException
  110. */
  111. @PostConstruct
  112. void init() throws IOException {
  113. String privateKey;
  114. File file = new File(conf.getKeyPath());
  115. if (file.exists()) {
  116. privateKey = IoUtil.read(new FileInputStream(file), StandardCharsets.UTF_8);
  117. } else {
  118. ClassPathResource resource = new ClassPathResource(conf.getKeyPath());
  119. privateKey = IoUtil.read(resource.getInputStream(), StandardCharsets.UTF_8);
  120. }
  121. config = new RSAAutoCertificateConfig.Builder()
  122. .merchantId(conf.getMchid()) // 商户号
  123. .privateKey(privateKey)//商户API私钥路径
  124. .merchantSerialNumber(conf.getMchsn()) // 商户证书序列号
  125. .apiV3Key(conf.getV3key()) // 商户APIV3密钥
  126. .build();
  127. jsapiService = new JsapiService.Builder().config(config).build();
  128. refundService = new RefundService.Builder().config(config).build();
  129. wxHttpClient = (OkHttpClientAdapter) new DefaultHttpClientBuilder().newInstance().config(config).build();
  130. // // 初始化微信电子发票开发配置,主要是回调地址 todo 区块链电子发票开通后放开
  131. // devConfig();
  132. }
  133. /**
  134. * 回调签名验证失败排查指引 (body中字段顺序变化造成验签失败是个大坑)
  135. * https://developers.weixin.qq.com/community/pay/article/doc/0004a879f60928d89340b8b9f64c13
  136. * https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient/issues/152
  137. *
  138. * @param request
  139. * @return
  140. */
  141. @SneakyThrows
  142. Object[] handleWxNotify(HttpServletRequest request) {
  143. var no = RandomUtil.randomInt(1000, 9999);
  144. var signature = request.getHeader("Wechatpay-Signature");
  145. var serial = request.getHeader("Wechatpay-Serial");
  146. var nonce = request.getHeader("Wechatpay-Nonce");
  147. var timestamp = request.getHeader("Wechatpay-Timestamp");
  148. var signatureType = request.getHeader("Wechatpay-Signature-Type");
  149. // 应对探测流量
  150. if (signature.contains("SIGNTEST")) {
  151. throw new BusinessException("接收到签名探测流量");
  152. }
  153. LOGGER.info("微信支付回调{}: Request参数: signature:{},serial:{},nonce:{},timestamp:{},signatureType:{}", no, signature, serial, nonce, timestamp, signatureType);
  154. ServletInputStream inputStream = request.getInputStream();
  155. ByteArrayOutputStream result = new ByteArrayOutputStream();
  156. byte[] buffer = new byte[1024];
  157. for (int lenght; (lenght = inputStream.read(buffer)) != -1; ) {
  158. result.write(buffer, 0, lenght);
  159. }
  160. var body = result.toString();
  161. LOGGER.info("微信支付回调{}:\nBody数据:\n{}", no, result);
  162. // 构造 RequestParam
  163. RequestParam requestParam = new RequestParam.Builder()
  164. .serialNumber(serial)
  165. .nonce(nonce) // 随机数
  166. .signature(signature)
  167. .timestamp(timestamp)
  168. .body(body)
  169. .build();
  170. LOGGER.info("微信支付回调{}:构造 RequestParam完毕", no);
  171. // 如果已经初始化了 RSAAutoCertificateConfig,可直接使用
  172. // 初始化 NotificationParser
  173. NotificationParser parser = new NotificationParser((NotificationConfig) config);
  174. return new Object[]{requestParam, parser, no};
  175. }
  176. /**
  177. * JSAPI支付下单
  178. */
  179. @Override
  180. @Transactional
  181. public PrepayWithRequestPaymentResponse wxPay(Long rechargeConfigId, String stationId) {
  182. // 充值配置
  183. var rechargeConfig = rechargeConfigService.getById(rechargeConfigId);
  184. if ((rechargeConfig == null) || rechargeConfig.getRechargeAmount() <= 0) {
  185. throw new BusinessException(ResponseEnum.WX_PAY_AMOUNT_ERROR);
  186. }
  187. var rechargeAmount = rechargeConfig.getRechargeAmount();
  188. var openid = StpUtil.getSession().getString("openid");
  189. var userId = StpUtil.getLoginIdAsLong();
  190. // 生成订单号
  191. String outTradeNo = OrderUtils.getOrderNo();
  192. // 创建钱包流水
  193. var walletDetail = new WalletDetail()
  194. .setType(WalletDetail.TYPE_充值)
  195. .setStatus(WalletDetail.STATUS_待确认)
  196. .setUserId(userId)
  197. .setAmount(rechargeAmount)
  198. .setOrderNo(outTradeNo);
  199. walletDetailService.save(walletDetail);
  200. // request.setXxx(val)设置所需参数,具体参数可见Request定义
  201. PrepayRequest request = new PrepayRequest();
  202. Amount amount = new Amount();
  203. // 传入金额单位为分
  204. amount.setTotal(rechargeAmount);
  205. request.setAmount(amount);
  206. request.setAppid(conf.getAppid());
  207. request.setMchid(conf.getMchid());
  208. request.setDescription("超级进化车生活充值");
  209. request.setNotifyUrl(conf.getNotifyUrl());
  210. request.setOutTradeNo(outTradeNo);
  211. // 把stationId传给微信,微信回调时携带再取出,记录是充值到哪个站点
  212. request.setAttach(stationId);
  213. Payer payer = new Payer();
  214. payer.setOpenid(openid);
  215. request.setPayer(payer);
  216. JsapiServiceExtension service = new JsapiServiceExtension.Builder().config(config).build();
  217. // response包含了调起支付所需的所有参数,可直接用于前端调起支付
  218. return service.prepayWithRequestPayment(request);
  219. }
  220. /**
  221. * 微信支付订单号查询订单
  222. */
  223. public Transaction queryOrderById(String transactionId) {
  224. QueryOrderByIdRequest request = new QueryOrderByIdRequest();
  225. // 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
  226. request.setMchid(conf.getMchid());
  227. request.setTransactionId(transactionId);
  228. // 调用接口
  229. return jsapiService.queryOrderById(request);
  230. }
  231. /**
  232. * 关闭订单
  233. *
  234. * @param outTradeNo
  235. */
  236. public void closeOrder(String outTradeNo) {
  237. CloseOrderRequest request = new CloseOrderRequest();
  238. // 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
  239. request.setMchid(conf.getMchid());
  240. request.setOutTradeNo(outTradeNo);
  241. // 调用接口
  242. jsapiService.closeOrder(request);
  243. }
  244. /**
  245. * 微信支付结果通知(充值完成)
  246. *
  247. * @param request
  248. * @return
  249. */
  250. @SneakyThrows
  251. @Override
  252. @Transactional(rollbackFor = Exception.class)
  253. public ResponseEntity<Object> wxNotify(HttpServletRequest request) {
  254. try {
  255. var notifyRes = handleWxNotify(request);
  256. // 以支付通知回调为例,验签、解密并转换成 Transaction
  257. Transaction transaction = ((NotificationParser) notifyRes[1]).parse((RequestParam) notifyRes[0], Transaction.class);
  258. LOGGER.info("微信支付回调{}:验签解密完毕,数据:{}", notifyRes[2], transaction);
  259. // 判断是否已经接收处理过通知
  260. if (payLogService.lambdaQuery().eq(PayLog::getOutTradeNo, transaction.getOutTradeNo()).one() != null) {
  261. return ResponseEntity.status(HttpStatus.OK).build();
  262. }
  263. DateTime dt = DateUtil.parse(transaction.getSuccessTime());
  264. LocalDateTime successTime = LocalDateTimeUtil.of(dt);
  265. // 钱包流水
  266. var walletDetail = walletDetailService.getWalletDetailByOrderNo(transaction.getOutTradeNo(), WalletDetail.TYPE_充值);
  267. if (walletDetail != null) {
  268. // 更新余额
  269. var account = accountService.getAccountByUserId(walletDetail.getUserId());
  270. accountService.lambdaUpdate().setSql("balance = (balance + %d)".formatted(transaction.getAmount().getTotal()))
  271. .eq(Account::getUserId, walletDetail.getUserId()).update();
  272. walletDetail.setStatus(WalletDetail.STATUS_已确认); //已确认
  273. walletDetail.setCurrency(transaction.getAmount().getCurrency());
  274. walletDetail.setAmount(transaction.getAmount().getTotal());
  275. walletDetail.setBeforeBalance(account.getBalance());
  276. walletDetail.setAfterBalance(account.getBalance() + walletDetail.getAmount());
  277. walletDetail.setTransactionTime(successTime);
  278. walletDetailService.updateById(walletDetail);
  279. // 异步处理充值服务费打折权益活动相关逻辑
  280. activityService.handleRechargeActivity(walletDetail.getUserId(), transaction.getAmount().getTotal());
  281. // 支付记录
  282. var payLog = new PayLog();
  283. payLog.setUserId(walletDetail.getUserId());
  284. payLog.setOpenid(transaction.getPayer().getOpenid());
  285. payLog.setBankType(transaction.getBankType());
  286. payLog.setMchId(transaction.getMchid());
  287. payLog.setOutTradeNo(transaction.getOutTradeNo());
  288. payLog.setTransactionId(transaction.getTransactionId());
  289. payLog.setSuccessTime(successTime);
  290. payLog.setTradeType(transaction.getTradeType().name());
  291. payLog.setTradeState(transaction.getTradeState().name());
  292. payLog.setAttach(transaction.getAttach());
  293. var totalAmount = transaction.getAmount().getTotal();
  294. payLog.setTotal(totalAmount);
  295. payLog.setCurrency(transaction.getAmount().getCurrency());
  296. payLog.setPayerTotal(transaction.getAmount().getPayerTotal());
  297. payLog.setPayerCurrency(transaction.getAmount().getPayerCurrency());
  298. payLogService.save(payLog);
  299. // 用户(此时要知道用户归属的站点)充值的资金先进到洗车站商户账户的基本户和冻结户,然后在消费时再将冻结户金额进行分润
  300. var stationId = transaction.getAttach();
  301. // 70%进入站点商户基本户,30%进入站点商户冻结户 todo 后面将比例进行配置化(需要考虑历史数据处理)
  302. var stationBasicAmount = (int) (totalAmount * 0.7);
  303. var stationFreezeAmount = totalAmount - stationBasicAmount;
  304. stationAccountService.lambdaUpdate()
  305. .setSql("balance = (balance + %d), frozen_amount = (frozen_amount + %d)".formatted(stationBasicAmount, stationFreezeAmount))
  306. .eq(StationAccount::getStationId, stationId)
  307. .update();
  308. // 分账记录
  309. var splitRecord = new SplitRecord()
  310. .setFromStationId(stationId)
  311. .setToStationId(stationId)
  312. .setTradeNo(transaction.getTransactionId())
  313. .setAmount(totalAmount)
  314. .setType(SplitRecord.TYPE_RECHARGE);
  315. splitRecordService.save(splitRecord);
  316. LOGGER.info("微信支付回调{}:业务处理结束", notifyRes[2]);
  317. return ResponseEntity.status(HttpStatus.OK).build();
  318. } else {
  319. LOGGER.error("微信支付通知处理异常,资金流水为空,回调信息:{}", transaction);
  320. return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("code", HttpStatus.INTERNAL_SERVER_ERROR, "message", "资金流水为空"));
  321. }
  322. } catch (Exception e) {
  323. if (e instanceof ValidationException) {
  324. // 签名验证失败,返回 401 UNAUTHORIZED 状态码
  325. LOGGER.error("微信支付通知验签失败", e);
  326. }
  327. if (e instanceof BusinessException) {
  328. LOGGER.error("业务异常", e);
  329. }
  330. return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("code", HttpStatus.UNAUTHORIZED, "message", "验签失败"));
  331. }
  332. }
  333. /**
  334. * 申请退款
  335. */
  336. @Override
  337. @Transactional(rollbackFor = Exception.class)
  338. public void applyWxRefund(String reason) {
  339. var userId = StpUtil.getLoginIdAsLong();
  340. // var chargeOrder = chargeOrderService.getChargingOrderByUserId(userId);
  341. // if (chargeOrder != null) {
  342. // throw new BusinessException("存在未完结的订单,请等待所有订单完结之后重试");
  343. // }
  344. // todo 洗车未完结订单校验
  345. var account = accountService.getAccountByUserId(userId);
  346. if (account.getBalance() <= 0) {
  347. throw new BusinessException("账户余额不足,无需退款");
  348. }
  349. // 校验余额大于优惠金额
  350. if (account.getBalance() <= account.getDiscountAmount()) {
  351. throw new BusinessException("不可退金额高于钱包余额,不支持退款");
  352. }
  353. // 将余额转移至冻结余额
  354. accountService.lambdaUpdate().setSql(" frozen_amount = (frozen_amount + balance) ,balance = 0").eq(Account::getUserId, userId).update();
  355. // 退款时,充值权益失效(权益余额转入冻结余额,权益状态设置为失效)
  356. userRechargeRightsService.lambdaUpdate()
  357. .setSql("frozen_balance = (rights_balance + frozen_balance) , rights_balance = 0")
  358. .set(UserRechargeRights::getStatus, UserRechargeRights.STATUS_无效)
  359. .eq(UserRechargeRights::getUserId, userId)
  360. .eq(UserRechargeRights::getStatus, UserRechargeRights.STATUS_有效)
  361. .update();
  362. // 余减去优惠金额作为退款金额
  363. AtomicInteger refundAmount = new AtomicInteger(account.getBalance() - account.getDiscountAmount());
  364. var originalRefundAmount = BigDecimal.valueOf(refundAmount.get());
  365. // 充值记录
  366. var payLogs = payLogService.lambdaQuery().eq(PayLog::getUserId, userId).eq(PayLog::getTradeState, PayLog.STATUS_充值成功).orderByDesc(PayLog::getSuccessTime).list();
  367. // 历史退款金额
  368. var refundsAmount = refundLogService.lambdaQuery().eq(RefundLog::getUserId, userId).list().stream().mapToInt(RefundLog::getRefund).sum();
  369. // 充值金额-历史退款金额不足以支付余额退款
  370. if (!CommUtil.isEmptyOrNull(payLogs)) {
  371. var total = payLogs.stream().mapToInt(PayLog::getTotal).sum();
  372. if ((total - refundsAmount) < account.getBalance()) {
  373. LOGGER.error("用户:{},历史充值金额:{},不足以支付余额退款金额:{}", userId, total - refundsAmount, account.getBalance());
  374. throw new BusinessException("充值金额不足以支付余额退款");
  375. }
  376. }
  377. // 最后一次的充值金额可以覆盖退款金额
  378. if (!CommUtil.isEmptyOrNull(payLogs) && payLogs.get(0).getTotal() >= refundAmount.get()) {
  379. // 退款日志
  380. var refundLog = new RefundLog().setUserId(payLogs.get(0).getUserId()).setOutTradeNo(payLogs.get(0).getOutTradeNo())
  381. .setTotal(payLogs.get(0).getTotal())
  382. .setRefund(refundAmount.get())
  383. .setDiscountAmount(account.getDiscountAmount())
  384. .setOutRefundNo(OrderUtils.getOrderNo());
  385. refundLogService.save(refundLog);
  386. } else if (!CommUtil.isEmptyOrNull(payLogs) && payLogs.get(0).getTotal() < refundAmount.get()) {
  387. // 最后一次的充值金额不能覆盖退款金额,拆分成多笔退款
  388. int amount = 0;
  389. // 用来退款的充值记录
  390. var refundPayLogs = new ArrayList<PayLog>();
  391. for (PayLog payLog : payLogs) {
  392. amount += payLog.getTotal();
  393. refundPayLogs.add(payLog);
  394. if (amount >= refundAmount.get()) {
  395. break;
  396. }
  397. }
  398. // 需要退款的记录数量
  399. var size = refundPayLogs.size();
  400. var refundLogList = new ArrayList<RefundLog>(size);
  401. var newRefundLogList = new ArrayList<RefundLog>(size);
  402. refundPayLogs.forEach(LambadaTools.forEachWithIndex((item, index) -> {
  403. // 如果不是最后一笔,金额全退,最后一笔退剩余的金额
  404. if (index < size - 1) {
  405. refundLogList.add(new RefundLog().setUserId(item.getUserId()).setOutTradeNo(item.getOutTradeNo())
  406. .setTotal(item.getTotal()).setRefund(item.getTotal()).setOutRefundNo(OrderUtils.getOrderNo()));
  407. refundAmount.addAndGet(-item.getTotal());
  408. } else {
  409. refundLogList.add(new RefundLog().setUserId(item.getUserId()).setOutTradeNo(item.getOutTradeNo())
  410. .setTotal(item.getTotal()).setRefund(refundAmount.get()).setOutRefundNo(OrderUtils.getOrderNo()));
  411. }
  412. }));
  413. // 不可退金额按退款金额比例放入每笔退款中
  414. refundLogList.forEach(LambadaTools.forEachWithIndex((refundLog, index) -> {
  415. int discountAmount;
  416. if (index < size - 1) {
  417. discountAmount = BigDecimal.valueOf(account.getDiscountAmount()).multiply((BigDecimal.valueOf(refundLog.getRefund()).divide(originalRefundAmount, 2, RoundingMode.HALF_UP))).intValue();
  418. } else {
  419. // 前面存在精度误差,最后一个元素用总数相减最精确
  420. discountAmount = account.getDiscountAmount() - newRefundLogList.stream().mapToInt(RefundLog::getDiscountAmount).sum();
  421. }
  422. refundLog.setDiscountAmount(discountAmount);
  423. refundLog.setReason(CommUtil.isEmptyOrNull(reason) ? reason : JSONObject.parseObject(reason).getString("reason"));
  424. newRefundLogList.add(index, refundLog);
  425. }));
  426. refundLogService.saveBatch(newRefundLogList);
  427. } else {
  428. // 退款异常
  429. LOGGER.error("退款异常:userId:{},退款金额:{},payLogs:{}", userId, originalRefundAmount, payLogs);
  430. throw new BusinessException("退款异常");
  431. }
  432. }
  433. /**
  434. * 处理退款
  435. *
  436. * @return
  437. */
  438. @Override
  439. public void wxRefund(long refundLogId) {
  440. // 通过退款申请id获取退款申请记录
  441. var refundLog = refundLogService.getById(refundLogId);
  442. // 钱包流水
  443. var walletDetail = new WalletDetail()
  444. .setType(WalletDetail.TYPE_提现)
  445. .setStatus(WalletDetail.STATUS_待确认)
  446. .setUserId(refundLog.getUserId())
  447. .setAmount(refundLog.getRefund())
  448. .setOrderNo(refundLog.getOutRefundNo());
  449. walletDetailService.save(walletDetail);
  450. CreateRequest request = new CreateRequest();
  451. // 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
  452. // 原始第三方订单号
  453. request.setOutTradeNo(refundLog.getOutTradeNo());
  454. // 退款订单号
  455. request.setOutRefundNo(refundLog.getOutRefundNo());
  456. // 退款原因
  457. request.setReason("用户申请退款");
  458. // 回调通知地址
  459. request.setNotifyUrl(conf.getRefundNotifyUrl());
  460. var amount = new AmountReq();
  461. // 退款金额
  462. amount.setRefund((long) refundLog.getRefund());
  463. amount.setTotal((long) refundLog.getTotal());
  464. amount.setCurrency(refundLog.getCurrency());
  465. request.setAmount(amount);
  466. var refund = refundService.create(request);
  467. refundLog.setChannel(refund.getChannel().name());
  468. refundLog.setFundsAccount(refund.getFundsAccount().name());
  469. refundLog.setCurrency(refund.getAmount().getCurrency());
  470. refundLog.setAdminUserId(StpUtil.getLoginIdAsLong());
  471. refundLog.setAdminUsername(StpUtil.getSession().getString("username"));
  472. refundLog.setStatus(RefundLog.STATUS_退款处理中);
  473. refundLogService.updateById(refundLog);
  474. }
  475. /**
  476. * 查询单笔退款(通过商户退款单号)
  477. *
  478. * @return
  479. */
  480. @Override
  481. public Refund queryByOutRefundNo(String outRefundNo) {
  482. QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest();
  483. // 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
  484. request.setOutRefundNo(outRefundNo);
  485. // 调用接口
  486. return refundService.queryByOutRefundNo(request);
  487. }
  488. /**
  489. * 微信退款结果通知
  490. *
  491. * @param request
  492. * @return
  493. */
  494. @Override
  495. @Transactional(rollbackFor = Exception.class)
  496. public ResponseEntity<Object> wxRefundNotify(HttpServletRequest request) {
  497. var notifyRes = handleWxNotify(request);
  498. try {
  499. // 以支付通知回调为例,验签、解密并转换成 RefundNotification
  500. RefundNotification refundNotification = ((NotificationParser) notifyRes[1]).parse((RequestParam) notifyRes[0], RefundNotification.class);
  501. LOGGER.info("微信退款回调{}:验签解密完毕,数据:\n{}", notifyRes[2], refundNotification);
  502. //退款日志在申请时插入,接收通知时更新
  503. var refundLog = refundLogService.lambdaQuery().eq(RefundLog::getOutRefundNo, refundNotification.getOutRefundNo()).one();
  504. // 防止重复处理消息
  505. if (RefundLog.STATUS_退款成功.equals(refundLog.getStatus())) {
  506. return ResponseEntity.status(HttpStatus.OK).build();
  507. }
  508. DateTime dt = DateUtil.parse(refundNotification.getSuccessTime());
  509. LocalDateTime successTime = LocalDateTimeUtil.of(dt);
  510. refundLogService.lambdaUpdate()
  511. .set(RefundLog::getRefundId, refundNotification.getRefundId())
  512. .set(RefundLog::getTransactionId, refundNotification.getTransactionId())
  513. .set(RefundLog::getUserReceivedAccount, refundNotification.getUserReceivedAccount())
  514. .set(RefundLog::getSuccessTime, successTime)
  515. .set(RefundLog::getStatus, refundNotification.getRefundStatus().name())
  516. .set(RefundLog::getTotal, refundNotification.getAmount().getTotal().intValue())
  517. .set(RefundLog::getRefund, refundNotification.getAmount().getRefund().intValue())
  518. .eq(RefundLog::getId, refundLog.getId()).update();
  519. if (RefundLog.STATUS_退款成功.equals(refundNotification.getRefundStatus().name())) {
  520. // 冻结金额扣减此次(退款金额+优惠金额),优惠金额字段减去申请退款时的优惠金额
  521. var account = accountService.getAccountByUserId(refundLog.getUserId());
  522. accountService.lambdaUpdate().setSql("frozen_amount = (frozen_amount - (%d + %d)) , discount_amount = (discount_amount - %d)"
  523. .formatted(refundNotification.getAmount().getRefund().intValue(), refundLog.getDiscountAmount(), refundLog.getDiscountAmount()))
  524. .eq(Account::getUserId, refundLog.getUserId()).update();
  525. // 更新资金流水
  526. var walletDetail = walletDetailService.getWalletDetailByOrderNo(refundNotification.getOutRefundNo(), WalletDetail.TYPE_提现);
  527. walletDetailService.lambdaUpdate()
  528. .set(WalletDetail::getStatus, WalletDetail.STATUS_已确认)
  529. .set(WalletDetail::getTransactionId, refundNotification.getTransactionId())
  530. .set(WalletDetail::getTransactionTime, successTime)
  531. .set(WalletDetail::getAmount, refundNotification.getAmount().getRefund().intValue())
  532. .set(WalletDetail::getBeforeBalance, account.getBalance())
  533. .set(WalletDetail::getAfterBalance, account.getBalance() - refundNotification.getAmount().getRefund().intValue())
  534. .set(WalletDetail::getTransactionTime, successTime)
  535. .eq(WalletDetail::getId, walletDetail.getId()).update();
  536. LOGGER.info("微信退款回调{}:业务处理结束", notifyRes[2]);
  537. return ResponseEntity.status(HttpStatus.OK).build();
  538. } else {
  539. // 退款失败
  540. LOGGER.error("微信退款失败,用户id:{},退款状态:{} \n 退款结果通知详情:{}", refundLog.getUserId(), refundNotification.getRefundStatus().name(), refundNotification);
  541. return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("code", HttpStatus.INTERNAL_SERVER_ERROR, "message", "退款处理异常"));
  542. }
  543. } catch (ValidationException e) {
  544. // 签名验证失败,返回 401 UNAUTHORIZED 状态码
  545. LOGGER.error("微信退款通知验签失败", e);
  546. return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("code", HttpStatus.UNAUTHORIZED, "message", "验签失败"));
  547. }
  548. }
  549. //================================================================发票=====================================================================
  550. }