WxPayServiceImpl.java 31 KB

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