package com.ruoyi.wx.web.controller; import cn.hutool.core.date.DateField; import cn.hutool.core.date.DateUtil; import com.github.binarywang.wxpay.bean.notify.OriginNotifyResponse; import com.github.binarywang.wxpay.bean.notify.SignatureHeader; import com.github.binarywang.wxpay.bean.notify.WxPayNotifyV3Result; import com.github.binarywang.wxpay.bean.request.WxPayOrderCloseRequest; import com.github.binarywang.wxpay.bean.request.WxPayOrderQueryRequest; import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest; import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; import com.github.binarywang.wxpay.bean.result.WxPayOrderCloseResult; import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryResult; import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderResult; import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result; import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum; import com.github.binarywang.wxpay.exception.WxPayException; import com.github.binarywang.wxpay.service.WxPayService; import com.google.gson.GsonBuilder; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.entity.WxUser; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.framework.recovery.domain.WxRecharge; import com.ruoyi.framework.recovery.service.impl.WxRechargeServiceImpl; import com.ruoyi.framework.recovery.service.impl.WxUserServiceImpl; import com.ruoyi.wx.web.domain.dto.WxPayDto; import com.ruoyi.wx.web.unit.WeixinNotifyUtils; import io.swagger.annotations.ApiOperation; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.math.BigDecimal; import java.util.Date; import java.util.concurrent.locks.ReentrantLock; /** * * 支付接口 */ @Slf4j @RestController @RequestMapping("/wx/pay") public class WxPayController { @Autowired private WxPayService wxPayService; @Autowired private WxRechargeServiceImpl wxRechargeService; @Autowired private WxUserServiceImpl wxUserService; private final ReentrantLock orderLock = new ReentrantLock(); /** *
     * 查询订单(详见https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_2)
     * 该接口提供所有微信支付订单的查询,商户可以通过查询订单接口主动查询订单状态,完成下一步的业务逻辑。
     * 需要调用查询接口的情况:
     * ◆ 当商户后台、网络、服务器等出现异常,商户系统最终未接收到支付通知;
     * ◆ 调用支付接口后,返回系统错误或未知交易状态情况;
     * ◆ 调用被扫支付API,返回USERPAYING的状态;
     * ◆ 调用关单或撤销接口API之前,需确认支付状态;
     * 接口地址:https://api.mch.weixin.qq.com/pay/orderquery
     * 
* * @param transactionId 微信订单号 * @param outTradeNo 商户系统内部的订单号,当没提供transactionId时需要传这个。 */ @ApiOperation(value = "查询订单") @GetMapping("/query/order") public WxPayOrderQueryResult queryOrder(@RequestParam(required = false) String transactionId, @RequestParam(required = false) String outTradeNo) throws WxPayException { return this.wxPayService.queryOrder(transactionId, outTradeNo); } @ApiOperation(value = "查询订单") @PostMapping("/query/order") public WxPayOrderQueryResult queryOrder(@RequestBody WxPayOrderQueryRequest wxPayOrderQueryRequest) throws WxPayException { return this.wxPayService.queryOrder(wxPayOrderQueryRequest); } /** *
     * 关闭订单
     * 应用场景
     * 以下情况需要调用关单接口:
     * 1. 商户订单支付失败需要生成新单号重新发起支付,要对原订单号调用关单,避免重复支付;
     * 2. 系统下单后,用户支付超时,系统退出不再受理,避免用户继续,请调用关单接口。
     * 注意:订单生成后不能马上调用关单接口,最短调用时间间隔为5分钟。
     * 接口地址:https://api.mch.weixin.qq.com/pay/closeorder
     * 是否需要证书:   不需要。
     * 
* * @param outTradeNo 商户系统内部的订单号 */ @ApiOperation(value = "关闭订单") @GetMapping("/closeOrder/{outTradeNo}") public WxPayOrderCloseResult closeOrder(@PathVariable String outTradeNo) throws WxPayException { return this.wxPayService.closeOrder(outTradeNo); } @ApiOperation(value = "关闭订单") @PostMapping("/closeOrder") public WxPayOrderCloseResult closeOrder(@RequestBody WxPayOrderCloseRequest wxPayOrderCloseRequest) throws WxPayException { return this.wxPayService.closeOrder(wxPayOrderCloseRequest); } /** * 调用统一下单接口,并组装生成支付所需参数对象. * * @param dto 统一下单请求参数 * @return 返回 {@link com.github.binarywang.wxpay.bean.order}包下的类对象 */ @ApiOperation(value = "统一下单,并组装所需支付参数") @PostMapping("/createOrder") public AjaxResult createOrder(@RequestBody WxPayDto dto) throws WxPayException { log.info("开始下单:{}", dto); WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request(); request.setDescription("购买会员"); // 商品订单号 String orderNo = "ORDER_" + System.currentTimeMillis(); request.setOutTradeNo(orderNo); // 订单金额 WxPayUnifiedOrderV3Request.Amount amount = new WxPayUnifiedOrderV3Request.Amount(); amount.setCurrency("CNY"); BigDecimal total = new BigDecimal(dto.gettBal()); if (StringUtils.isNotEmpty(dto.getMoney())) { total = total.subtract(new BigDecimal(dto.getMoney())); } else if (StringUtils.isNotEmpty(dto.getDiscount())) { total = total.multiply(new BigDecimal(dto.getDiscount()).divide(new BigDecimal(10))); } log.info("用户:{},支付总金额为:{}, 订单Id为:{}", dto.getOpenId(), amount, orderNo); amount.setTotal(total.intValue() * 100); request.setAmount(amount); WxPayUnifiedOrderV3Request.Payer payer = new WxPayUnifiedOrderV3Request.Payer(); payer.setOpenid(dto.getOpenId()); request.setPayer(payer); WxPayUnifiedOrderV3Result.JsapiResult orderV3 = this.wxPayService.createOrderV3(TradeTypeEnum.JSAPI, request); AjaxResult result = new AjaxResult(); WxRecharge wxRecharge = new WxRecharge(); wxRecharge.setPoints(dto.getPoints()); wxRecharge.setPrice(total.divide(new BigDecimal(100))); wxRecharge.setPayTime(new Date()); WxUser wxUser = wxUserService.selectWxUSerByOpenid(dto.getOpenId()); wxRecharge.setUserId(wxUser.getId()); wxRecharge.setUserName(wxUser.getUsername()); wxRechargeService.insertWxRecharge(wxRecharge); wxUser.setUserLevel(dto.getVipLv()); wxUser.setVipStartTime(new Date()); wxUser.setVipEndTime(defDate(new Date(), dto.getVipLv())); wxUserService.updateWxUser(wxUser); result.put("appId", orderV3.getAppId()); result.put("timeStamp", orderV3.getTimeStamp()); result.put("nonceStr", orderV3.getNonceStr()); result.put("packageValue", orderV3.getPackageValue()); result.put("signType", orderV3.getSignType()); result.put("outTradeNo", orderNo); return result; } /** * 统一下单(详见https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1) * 在发起微信支付前,需要调用统一下单接口,获取"预支付交易会话标识" * 接口地址:https://api.mch.weixin.qq.com/pay/unifiedorder * * @param request 请求对象,注意一些参数如appid、mchid等不用设置,方法内会自动从配置对象中获取到(前提是对应配置中已经设置) */ @ApiOperation(value = "原生的统一下单接口") @PostMapping("/unifiedOrder") public WxPayUnifiedOrderResult unifiedOrder(@RequestBody WxPayUnifiedOrderRequest request) throws WxPayException { return this.wxPayService.unifiedOrder(request); } /** * 回调接口 */ @ApiOperation(value = "回调接口") @PostMapping("/notify") public AjaxResult notify(HttpServletRequest request, HttpServletResponse response, @RequestBody String notifyData) throws WxPayException { log.debug("======= 接收到通知 ========="); // 获取请求头信息 SignatureHeader signatureHeader = WeixinNotifyUtils.getSignatureHeader(request); // 将请求体json字符串转换为实体 OriginNotifyResponse notifyResponse = new GsonBuilder().create().fromJson(notifyData, OriginNotifyResponse.class); // 支付成功通知 if ("TRANSACTION.SUCCESS".equals(notifyResponse.getEventType())) { // 获取锁 if (orderLock.tryLock()) { try { // 解析支付结果通知 WxPayNotifyV3Result result = wxPayService.parseOrderNotifyV3Result(notifyData, signatureHeader); // TODO 此处根据返回结果处理订单信息 log.debug("支付成功 ---> orderNo:" + result.getResult().getOutTradeNo()); // 返回成功(无需数据,系统状态码为200即可) return AjaxResult.success(); } catch (Exception e) { // 支付结果解析异常/订单处理异常 log.error("支付通知处理异常:", e); response.setStatus(500);// 支付成功通知处理失败时需要将状态码修改为5xx/4xx,微信才会重新发送回调 return AjaxResult.error(); } finally { // 释放锁 orderLock.unlock(); } } else { // 锁获取失败,返回异常,等待下次消息 response.setStatus(503);// 支付成功通知处理失败时需要将状态码修改为5xx/4xx,微信才会重新发送回调 return AjaxResult.error(); } } return AjaxResult.success(); } private Date defDate(Date now, Integer offset) { Date defDate = new Date(); switch (offset) { case 1: defDate = DateUtil.offset(now, DateField.MONTH, 1); break; case 2: defDate = DateUtil.offset(now, DateField.MONTH, 3); break; case 3: defDate = DateUtil.offset(now, DateField.MONTH, 12); break; default: break; } return defDate; } }