**谷粒随享** ## 第8章 微信支付充值业务 **学习目标:** - 本地交易记录保存(用于对账) - 下单 - 充值 - 微信支付(对接第三方支付平台) - 基于微信支付完成付款 - 下单 - 充值 - 基于微信支付完成充值业务 # 1、微信支付 ![支付-微信支付](assets/支付-微信支付.gif) ## 1.1 保存交易记录 需求:当用户进行订单、充值 微信支付为每笔交易产生一条本地交易记录,将来用于对账。 ### 1.1.1 获取订单对象 > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/100 已有Restful接口实现。只需要在`service-order-client`模块中新增Feign远程调用方法 **OrderFeignClient** ```java package com.atguigu.tingshu.order.client; import com.atguigu.tingshu.common.result.Result; import com.atguigu.tingshu.model.order.OrderInfo; import com.atguigu.tingshu.order.client.impl.OrderDegradeFeignClient; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; /** *

* 订单模块远程调用API接口 *

* * @author atguigu */ @FeignClient(value = "service-order", path = "api/order", fallback = OrderDegradeFeignClient.class) public interface OrderFeignClient { /** * 根据订单编号查询订单信息 * @param orderNo * @return */ @GetMapping("/orderInfo/getOrderInfo/{orderNo}") public Result getOrderInfo(@PathVariable String orderNo); } ``` **OrderDegradeFeignClient**熔断类: ```java package com.atguigu.tingshu.order.client.impl; import com.atguigu.tingshu.common.result.Result; import com.atguigu.tingshu.model.order.OrderInfo; import com.atguigu.tingshu.order.client.OrderFeignClient; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @Slf4j @Component public class OrderDegradeFeignClient implements OrderFeignClient { @Override public Result getOrderInfo(String orderNo) { log.error("[订单服务]提供远程调用方法getOrderInfo执行服务降级"); return null; } } ``` ### 1.1.2 获取充值记录信息 > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/101 `service-account` 微服务**RechargeInfoApiController**控制器中添加 ```java package com.atguigu.tingshu.account.api; import com.atguigu.tingshu.account.service.RechargeInfoService; import com.atguigu.tingshu.common.login.GuiGuLogin; import com.atguigu.tingshu.common.result.Result; import com.atguigu.tingshu.model.account.RechargeInfo; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @Tag(name = "充值管理") @RestController @RequestMapping("api/account") @SuppressWarnings({"all"}) public class RechargeInfoApiController { @Autowired private RechargeInfoService rechargeInfoService; /** * 根据充值订单编号查询充值信息 * @param orderNo * @return */ @Operation(summary = "根据充值订单编号查询充值信息") @GetMapping("/rechargeInfo/getRechargeInfo/{orderNo}") public Result getRechargeInfo(@PathVariable String orderNo){ RechargeInfo rechargeInfo = rechargeInfoService.getRechargeInfo(orderNo); return Result.ok(rechargeInfo); } } ``` **RechargeInfoService**接口 ```java package com.atguigu.tingshu.account.service; import com.atguigu.tingshu.model.account.RechargeInfo; import com.baomidou.mybatisplus.extension.service.IService; public interface RechargeInfoService extends IService { /** * 根据充值订单编号查询充值记录 * @param orderNo * @return */ RechargeInfo getRechargeInfo(String orderNo); } ``` **RechargeInfoServiceImpl实现类** ```java package com.atguigu.tingshu.account.service.impl; import com.atguigu.tingshu.account.mapper.RechargeInfoMapper; import com.atguigu.tingshu.account.service.RechargeInfoService; import com.atguigu.tingshu.model.account.RechargeInfo; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service @SuppressWarnings({"all"}) public class RechargeInfoServiceImpl extends ServiceImpl implements RechargeInfoService { @Autowired private RechargeInfoMapper rechargeInfoMapper; /** * 根据充值订单编号查询充值信息 * * @param orderNo * @return */ @Override public RechargeInfo getRechargeInfo(String orderNo) { return rechargeInfoMapper.selectOne( new LambdaQueryWrapper() .eq(RechargeInfo::getOrderNo, orderNo) ); } } ``` 远程调用模块`service-account-client`中**AccountFeignClient**增加远程调用方法 ```java /** * 根据充值订单编号查询充值信息 * @param orderNo * @return */ @GetMapping("/rechargeInfo/getRechargeInfo/{orderNo}") public Result getRechargeInfo(@PathVariable String orderNo); ``` **AccountDegradeFeignClient**熔断类: ```java @Override public Result getRechargeInfo(String orderNo) { log.error("[账户服务]提供远程调用接口getRechargeInfo服务降级"); return null; } ``` ### 1.1.3 保存本地交易记录 `service-payment`模块中增加保存本地交易业务处理 **PaymentInfoService** ```java package com.atguigu.tingshu.payment.service; import com.atguigu.tingshu.model.payment.PaymentInfo; import com.baomidou.mybatisplus.extension.service.IService; public interface PaymentInfoService extends IService { /** * 保存本地交易记录 * * @param paymentType:支付类型 1301-订单 1302-充值 * @param orderNo: 订单编号 * @return */ public PaymentInfo savePaymentInfo(String paymentType, String orderNo); } ``` **PaymentInfoServiceImpl**实现类 ```java package com.atguigu.tingshu.payment.service.impl; import cn.hutool.core.lang.Assert; import com.atguigu.tingshu.account.AccountFeignClient; import com.atguigu.tingshu.common.constant.SystemConstant; import com.atguigu.tingshu.common.util.AuthContextHolder; import com.atguigu.tingshu.model.account.RechargeInfo; import com.atguigu.tingshu.model.order.OrderInfo; import com.atguigu.tingshu.model.payment.PaymentInfo; import com.atguigu.tingshu.order.client.OrderFeignClient; import com.atguigu.tingshu.payment.mapper.PaymentInfoMapper; import com.atguigu.tingshu.payment.service.PaymentInfoService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service @SuppressWarnings({"all"}) public class PaymentInfoServiceImpl extends ServiceImpl implements PaymentInfoService { @Autowired private OrderFeignClient orderFeignClient; @Autowired private AccountFeignClient accountFeignClient; /** * 保存本地交易记录 * * @param paymentType:支付类型 1301-订单 1302-充值 * @param orderNo: 订单编号 * @return */ @Override public PaymentInfo savePaymentInfo(String paymentType, String orderNo) { Long userId = AuthContextHolder.getUserId(); //1.根据订单编号查询本地交易记录,如果存在则返回即可 PaymentInfo paymentInfo = baseMapper.selectOne( new LambdaQueryWrapper() .eq(PaymentInfo::getOrderNo, orderNo) ); if (paymentInfo != null) { return paymentInfo; } //2.构建本地交易记录 paymentInfo = new PaymentInfo(); //2.1 封装直接赋值属性:用户ID,支付类型、订单号、付款方式、支付状态 paymentInfo.setUserId(userId); paymentInfo.setPaymentType(paymentType); paymentInfo.setOrderNo(orderNo); paymentInfo.setPayWay(SystemConstant.ORDER_PAY_WAY_WEIXIN); paymentInfo.setPaymentStatus(SystemConstant.PAYMENT_STATUS_UNPAID); //2.2 处理支付类型为:1301订单,封装交易金额跟内容 if (SystemConstant.PAYMENT_TYPE_ORDER.equals(paymentType)) { //2.2.1 远程调用订单服务获取订单信息 OrderInfo orderInfo = orderFeignClient.getOrderInfo(orderNo).getData(); Assert.notNull(orderInfo, "订单{}不存在", orderNo); //2.2.2 判断订单状态,未支付才能支付 订单状态:0901-未支付 0902-已支付 0903-已取消 String orderStatus = orderInfo.getOrderStatus(); if (!SystemConstant.ORDER_STATUS_UNPAID.equals(orderStatus)) { throw new GuiguException(500, "订单状态有误:" + orderStatus); } //2.2.3 封装交易金额跟内容 paymentInfo.setAmount(orderInfo.getOrderAmount()); paymentInfo.setContent(orderInfo.getOrderTitle()); } //2.3 处理支付类型为:1302充值,封装交易金额跟内容 if (SystemConstant.PAYMENT_TYPE_RECHARGE.equals(paymentType)) { //2.2.1 远程调用账户服务获取充值信息 RechargeInfo rechargeInfo = accountFeignClient.getRechargeInfo(orderNo).getData(); Assert.notNull(rechargeInfo, "充值{}不存在", orderNo); //2.2.2 判断充值状态,未支付才能支付 充值状态:0901-未支付 0902-已支付 0903-已取消 String rechargeStatus = rechargeInfo.getRechargeStatus(); if (!SystemConstant.ORDER_STATUS_UNPAID.equals(rechargeStatus)) { throw new GuiguException(500, "充值状态有误:" + rechargeStatus); } //2.3.3 封装交易金额跟内容 paymentInfo.setAmount(rechargeInfo.getRechargeAmount()); paymentInfo.setContent("充值:" + rechargeInfo.getRechargeAmount()); } //2.4 TODO 其他属性,等支付成功后异步回调获取 //paymentInfo.setOutTradeNo(); //paymentInfo.setCallbackTime(); //paymentInfo.setCallbackContent(); //3.保存本地交易记录 baseMapper.insert(paymentInfo); return paymentInfo; } } ``` ## 1.2 对接微信支付 > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/102 异步回调需要在微信端配置,并且只能配置一个接口地址,而现在这个接口地址有其他项目在使用。所以我们在前端模拟了一下回调功能,主动查询一下结果! ![](assets/tingshu041.png) 接收前端传递的**支付类型**以及**订单编号** 1. 先在配置文件中添加支付相关信息配置: ```properties spring.application.name=service-payment spring.profiles.active=dev spring.main.allow-bean-definition-overriding=true spring.cloud.nacos.discovery.server-addr=192.168.200.130:8848 spring.cloud.nacos.config.server-addr=192.168.200.130:8848 spring.cloud.nacos.config.prefix=${spring.application.name} spring.cloud.nacos.config.file-extension=yaml wechat.v3pay.appid=wxcc651fcbab275e33 wechat.v3pay.merchantId=1631833859 #TODO 确保能加载本地应用私钥文件 wechat.v3pay.privateKeyPath=D:\\tmp\\apiclient_key.pem wechat.v3pay.merchantSerialNumber=4AE80B52EBEAB2B96F68E02510A42801E952E889 wechat.v3pay.apiV3key=84dba6dd51cdaf779e55bcabae564b53 #TODO 改为公网域名能确保微信调用成功 wechat.v3pay.notifyUrl=http://127.0.0.1:8500/api/payment/wxPay/notify ``` 2. **apiclient_key.pem** 商户API私钥路径 的私钥要放入指定位置,让程序读取! 3. 注意:RSAAutoCertificateConfig 对象必须在配置文件中注入到spring 容器中,不要直接使用原生的,否则会报错! ```java package com.atguigu.tingshu.payment.config; import com.wechat.pay.java.core.RSAAutoCertificateConfig; import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @ConfigurationProperties(prefix="wechat.v3pay") //读取节点 @Data public class WxPayV3Config { private String appid; /** 商户号 */ public String merchantId; /** 商户API私钥路径 */ public String privateKeyPath; /** 商户证书序列号 */ public String merchantSerialNumber; /** 商户APIV3密钥 */ public String apiV3key; /** 回调地址 */ private String notifyUrl; @Bean public RSAAutoCertificateConfig rsaAutoCertificateConfig(){ return new RSAAutoCertificateConfig.Builder() .merchantId(this.merchantId) .privateKeyFromPath(privateKeyPath) .merchantSerialNumber(merchantSerialNumber) .apiV3Key(apiV3key) .build(); } } ``` ### 1.2.1 支付控制器 ```java package com.atguigu.tingshu.payment.api; import com.atguigu.tingshu.common.login.GuiGuLogin; import com.atguigu.tingshu.common.result.Result; import com.atguigu.tingshu.payment.service.WxPayService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Map; @Tag(name = "微信支付接口") @RestController @RequestMapping("api/payment") @Slf4j public class WxPayApiController { @Autowired private WxPayService wxPayService; /** * 下单或充值选择微信支付,返回小程序拉起微信支付所需参数 * @param paymentType 支付类型:1301下单 1302充值 * @param orderNo 订单/充值订单编号 * @return {"timeStamp":"","package":"","paySign":"","signType":"RSA","nonceStr":""} */ @GuiGuLogin @Operation(summary = "下单或充值选择微信支付,返回小程序拉起微信支付所需参数") @PostMapping("/wxPay/createJsapi/{paymentType}/{orderNo}") public Result> createJsapi(@PathVariable String paymentType, @PathVariable String orderNo){ Map map = wxPayService.createJsapi(paymentType, orderNo); return Result.ok(map); } } ``` ### 1.2.2 支付接口 ```java package com.atguigu.tingshu.payment.service; import java.util.Map; public interface WxPayService { /** * 下单或充值选择微信支付,返回小程序拉起微信支付所需参数 * @param paymentType 支付类型:1301下单 1302充值 * @param orderNo 订单/充值订单编号 * @return {{"timeStamp":"","package":"","paySign":"","signType":"RSA","nonceStr":""}}} */ Map createJsapi(String paymentType, String orderNo); } ``` ### 1.2.3 支付实现类 微信支付:小程序调起支付需要返回的参数 > 支付文档入口:https://developers.weixin.qq.com/miniprogram/dev/api/payment/wx.requestPayment.html > 对接方式:https://github.com/wechatpay-apiv3/wechatpay-java ![](assets/tingshu042.png) ```java package com.atguigu.tingshu.payment.service.impl; import com.atguigu.tingshu.common.constant.SystemConstant; import com.atguigu.tingshu.common.execption.GuiguException; import com.atguigu.tingshu.model.payment.PaymentInfo; import com.atguigu.tingshu.payment.config.WxPayV3Config; import com.atguigu.tingshu.payment.service.PaymentInfoService; import com.atguigu.tingshu.payment.service.WxPayService; import com.wechat.pay.java.core.RSAAutoCertificateConfig; import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension; import com.wechat.pay.java.service.payments.jsapi.model.Amount; import com.wechat.pay.java.service.payments.jsapi.model.Payer; import com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest; import com.wechat.pay.java.service.payments.jsapi.model.PrepayWithRequestPaymentResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.swing.plaf.ProgressBarUI; import java.util.HashMap; import java.util.Map; @Service @Slf4j public class WxPayServiceImpl implements WxPayService { @Autowired private PaymentInfoService paymentInfoService; @Autowired private WxPayV3Config wxPayV3Config; @Autowired private RSAAutoCertificateConfig rsaAutoCertificateConfig; /** * 下单或充值选择微信支付,返回小程序拉起微信支付所需参数 * * @param paymentType 支付类型:1301下单 1302充值 * @param orderNo 订单/充值订单编号 * @return {{"timeStamp":"","package":"","paySign":"","signType":"RSA","nonceStr":""}}} */ @Override public Map createJsapi(String paymentType, String orderNo) { try { //1.获取或保存本地交易记录,验证状态 PaymentInfo paymentInfo = paymentInfoService.savePaymentInfo(paymentType, orderNo); //支付状态:1401-未支付 1402-已支付 String paymentStatus = paymentInfo.getPaymentStatus(); if (!SystemConstant.PAYMENT_STATUS_UNPAID.equals(paymentStatus)) { throw new GuiguException(500, "本地交易记录状态有误" + paymentStatus); } //2.对接微信支付,获取小程序端拉起微信支付所需参数 //2.1 创建调用微信支付JSAPI业务对象 JsapiServiceExtension service = new JsapiServiceExtension.Builder().config(rsaAutoCertificateConfig).build(); //2.2 创建预下单参数请求对象 PrepayRequest request = new PrepayRequest(); Amount amount = new Amount(); //测试付款金额固定硬编码为1分 amount.setTotal(1); request.setAmount(amount); request.setAppid(wxPayV3Config.getAppid()); request.setMchid(wxPayV3Config.getMerchantId()); request.setDescription(paymentInfo.getContent()); request.setNotifyUrl(wxPayV3Config.getNotifyUrl()); //商户订单编号 request.setOutTradeNo(orderNo); //小程序目前还未上线,仅支持应用下的开发者用户进行付款 故这里需要设置开发者用户的openid Payer payer = new Payer(); payer.setOpenid("odo3j4qp-wC3HVq9Z_D9C0cOr0Zs"); request.setPayer(payer); //2.3 调用微信获取response包含了调起支付所需的所有参数,可直接用于前端调起支付 PrepayWithRequestPaymentResponse response = service.prepayWithRequestPayment(request); if (response != null) { String timeStamp = response.getTimeStamp(); String nonceStr = response.getNonceStr(); String packageVal = response.getPackageVal(); String signType = response.getSignType(); String paySign = response.getPaySign(); Map map = new HashMap<>(); map.put("timeStamp", timeStamp); map.put("nonceStr", nonceStr); map.put("package", packageVal); map.put("signType", signType); map.put("paySign", paySign); return map; } return null; } catch (Exception e) { log.error("微信下单失败"); throw new GuiguException(500, e.getMessage()); } } } ``` ## 1.3 查询支付状态(同步) 需求:当用户微信付款后,在小程序端需要通过查询微信端交易状态(轮询查询交易状态),如果用户已支付,给小程序端响应结果,展示支付成功页面(**展示给用户**)。 > 微信支付接口说明:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_2.shtml > > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/103 ![](assets/tingshu043.png) **WxPayApiController**控制器: ```java /** * 查询订单支付状态 * @param orderNo * @return */ @GuiGuLogin @Operation(summary = "商户端主动查询微信支付结果") @GetMapping("/wxPay/queryPayStatus/{orderNo}") public Result queryPayStatus(@PathVariable String orderNo){ boolean result = wxPayService.queryPayStatus(orderNo); return Result.ok(result); } ``` **WxPayService**接口: ```java /** * 根据商户订单编号查询微信支付结果 * * @param orderNo * @return */ Boolean queryPayStatus(String orderNo); ``` **WxPayServiceImpl**实现类: ```java /** * 查询订单支付状态 * * @param orderNo * @return */ @Override public boolean queryPayStatus(String orderNo) { /*//1.构建业务对象 JsapiServiceExtension service = new JsapiServiceExtension.Builder().config(rsaAutoCertificateConfig).build(); //2.构建根据商户订单号查询微信支付订单状态的请求参数 QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest(); request.setMchid(wxPayV3Config.getMerchantId()); request.setOutTradeNo(orderNo); //3.调用微信支付查询订单状态接口 Transaction transaction = service.queryOrderByOutTradeNo(request); if (transaction != null) { Transaction.TradeStateEnum tradeState = transaction.getTradeState(); if (Transaction.TradeStateEnum.SUCCESS == tradeState) { //交易成功,验证订单金额跟用户实付金额是否一致 Integer payerTotal = transaction.getAmount().getPayerTotal(); if (payerTotal.intValue() == 1) { return true; } } } return false;*/ //TODO 处理核心业务:更新本地交易记录状态 //1.伪造微信交易对象 封装:订单编号+微信交易号 Transaction transaction = new Transaction(); transaction.setOutTradeNo(orderNo); transaction.setTransactionId("WX"+ IdUtil.getSnowflakeNextId()); paymentInfoService.updatePaymentInfo(transaction); return true; } ``` **大家课后练习(没有办法支付,故在同步回调中模拟支付成功)** ## 1.4 支付异步回调 > 微信异步回调说明:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_5.shtml ### 1.4.1 内网穿透工具 内网穿透工具:https://natapp.cn/ 1. 注册账户,实名认证 2. 购买免费隧道,设置域名映射本地IP及端口 ![image-20231204105254982](assets/image-20231204105254982.png) 3. 修改netapp程序配置文件 ![image-20231204105336040](assets/image-20231204105336040.png) ```properties #将本文件放置于natapp同级目录 程序将读取 [default] 段 #在命令行参数模式如 natapp -authtoken=xxx 等相同参数将会覆盖掉此配置 #命令行参数 -config= 可以指定任意config.ini文件 [default] authtoken=改为自己申请隧道对应的authtoken #对应一条隧道的authtoken ``` 4. 启动netapp程序 ![image-20231204105456254](assets/image-20231204105456254.png) 5. 修改Nacos中支付系统配置文件中异步回调地址改为最新域名 ![image-20231204105544046](assets/image-20231204105544046.png) 6. 启动支付系统,拉取最新配置 7. 创建购买订单或充值记录,产生微信交易 ### 1.4.2 支付服务集成Seata 1. 在`tingshu_payment`数据库中新增undo_log日志表 ```sql CREATE TABLE `undo_log` ( `id` bigint NOT NULL AUTO_INCREMENT, `branch_id` bigint NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb3; ``` 2. 在`service-payment`模块pom.xml中增加Seata启动依赖 ```xml com.alibaba.cloud spring-cloud-starter-alibaba-seata io.seata seata-spring-boot-starter io.seata seata-spring-boot-starter ``` 3. 订单微服务`service-order`模块新增Seata配置 ```yaml seata: enabled: true tx-service-group: ${spring.application.name}-group # 事务组名称 service: vgroup-mapping: #指定事务分组至集群映射关系,集群名default需要与seata-server注册到Nacos的cluster保持一致 service-payment-group: default registry: type: nacos # 使用nacos作为注册中心 nacos: server-addr: 192.168.200.6:8848 # nacos服务地址 group: DEFAULT_GROUP # 默认服务分组 namespace: "" # 默认命名空间 cluster: default # 默认TC集群名称 ``` ### 1.4.3 支付服务-处理异步回调 > 微信异步回调说明:https://pay.weixin.qq.com/doc/v3/merchant/4012791861 > > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/104 **WxPayApiController**控制器: ```java /** * 微信支付成功后异步回调 * @param request * @return * @throws Exception */ @Operation(summary = "微信支付成功后异步回调") @PostMapping("/wxPay/notify") public Map wxPayNotify(HttpServletRequest request) throws Exception { return wxPayService.wxPayNotify(request); } ``` **WxPayService接口**: ```java /** * 提供给微信支付调用:用户微信付款成功后,微信通知商户支付结果 * @param request * @return {code:"SUCCESS",message:"成功"} */ Map notifyPayResult(HttpServletRequest request); ``` **WxPayServiceImpl实现类**: ```java @Autowired private RedisTemplate redisTemplate; /** * 提供给微信支付调用:用户微信付款成功后,微信通知商户支付结果 * * @param request * @return */ @Override public Map wxPayNotify(HttpServletRequest request) { //1.验签-避免出现虚假通知或支付结果在网络传输中被恶意篡改 //1.1 从请求头中获取封装请求参数对象数据 String signature = request.getHeader("Wechatpay-Signature"); String serial = request.getHeader("Wechatpay-Serial"); String nonce = request.getHeader("Wechatpay-Nonce"); String timestamp = request.getHeader("Wechatpay-Timestamp"); String signaureType = request.getHeader("Wechatpay-Signature-Type"); log.info("签名:{},序列号:{},随机数:{},时间戳:{},签名类型:{}", signature, serial, nonce, timestamp, signaureType); //1.2 获取请求体中参数 String body = PayUtil.readData(request); log.info("请求体:{}", body); //2.参数解密-解析获取请求体中业务参数(支付结果业务数据) //2.1 构造RequestParam RequestParam requestParam = new RequestParam.Builder() .serialNumber(serial) .nonce(nonce) .signature(signature) .timestamp(timestamp) .body(body) .build(); //2.2 初始化 NotificationParser解析器对象 NotificationParser parser = new NotificationParser(rsaAutoCertificateConfig); //2.3 解密并转换成 Transaction交易对象 Transaction transaction = parser.parse(requestParam, Transaction.class); //3.验证微信支付状态:必须是已支付,应付金额跟实付金额一致 if (transaction != null) { Transaction.TradeStateEnum tradeState = transaction.getTradeState(); if (Transaction.TradeStateEnum.SUCCESS == tradeState && transaction.getAmount().getPayerTotal().intValue() == 1) { //4.幂等性处理 同一笔交易仅需处理一次 //4.1 获取交易对象中微信支付订单号或者商户订单作为唯一标识 String redisKey = RedisConstant.BUSINESS_PREFIX + transaction.getOutTradeNo(); //4.2 利用Redis提供set k v ex nx 命令实现 Boolean flag = redisTemplate.opsForValue().setIfAbsent(redisKey, transaction.getOutTradeNo(), 25, TimeUnit.HOURS); if (flag) { try { //5.核心业务处理:更新本地交易记录 paymentInfoService.updatePaymentInfo(transaction); //6.返回微信正确应答 Map map = new HashMap<>(); map.put("code", "SUCCESS"); map.put("message", "成功"); } catch (Exception e) { redisTemplate.delete(redisKey); log.error("微信支付回调处理失败"); throw new RuntimeException(e); } } } } return null; } ``` #### 1.4.3.1 更新本地交易记录 **PaymentInfoService** ```java /** * 支付成功后,更新交易记录 * @param transaction 微信交易对象 */ void updatePaymentInfo(Transaction transaction); ``` **PaymentInfoServiceImpl** ```java /** * 支付成功后,更新交易记录 * * @param transaction 微信交易对象 */ @Override @GlobalTransactional(rollbackFor = Exception.class) public void updatePaymentInfo(Transaction transaction) { //1.根据微信交易对象中的商户订单号查询本地交易记录 如果已支付 忽略 String orderNo = transaction.getOutTradeNo(); PaymentInfo paymentInfo = baseMapper.selectOne( new LambdaQueryWrapper() .eq(PaymentInfo::getOrderNo, orderNo) ); //支付状态:1401-未支付 1402-已支付 String paymentStatus = paymentInfo.getPaymentStatus(); if (SystemConstant.PAYMENT_STATUS_UNPAID.equals(paymentStatus)) { //2.更新本地交易记录状态:已支付 更新:交易编号、回调时间、回调内容 paymentInfo.setPaymentStatus(SystemConstant.PAYMENT_STATUS_PAID); paymentInfo.setOutTradeNo(transaction.getTransactionId()); paymentInfo.setCallbackTime(new Date()); paymentInfo.setCallbackContent(transaction.toString()); baseMapper.updateById(paymentInfo); //3.处理支付类型:1301订单 订单状态更新及虚拟物品发货 String paymentType = paymentInfo.getPaymentType(); if (SystemConstant.PAYMENT_TYPE_ORDER.equals(paymentType)) { Result result = orderFeignClient.orderPaySuccess(orderNo); if (result.getCode().intValue() != 200) { throw new GuiguException(result.getCode(), result.getMessage()); } } //4.处理支付类型:1302充值 余额充值 if(SystemConstant.PAYMENT_TYPE_RECHARGE.equals(paymentType)){ //4.1 远程调用账户服务,修改充值状态,完车充值业务 Result result = accountFeignClient.rechargePaySuccess(orderNo); if (result.getCode().intValue() != 200) { throw new GuiguException(result.getCode(), result.getMessage()); } } } } ``` #### 1.4.3.2 订单服务-更新订单状态 > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/145 在`service-order`模块**OrderInfoApiController**处理订单支付成功 ```java /** * 用户付款成功后,处理订单相关业务 * @param orderNo * @return */ @Operation(summary = "用户付款成功后,处理订单相关业务") @GetMapping("/orderInfo/orderPaySuccess/{orderNo}") public Result orderPaySuccess(@PathVariable String orderNo){ orderInfoService.orderPaySuccess(orderNo); return Result.ok(); } ``` **OrderInfoService** ```java /** * 用户付款成功后,处理订单相关业务 * @param orderNo * @return */ void orderPaySuccess(String orderNo); ``` **OrderInfoServiceImpl** ```java /** * 用户支付成功后,修改订单状态:已支付 * * @param orderNo */ @Override public void orderPaySuccess(String orderNo) { //1.根据订单编号查询订单状态 OrderInfo orderInfo = orderInfoMapper.selectOne( new LambdaQueryWrapper() .eq(OrderInfo::getOrderNo, orderNo) ); //订单状态:0901-未支付 0902-已支付 0903-已取消 String orderStatus = orderInfo.getOrderStatus(); if (SystemConstant.ORDER_STATUS_UNPAID.equals(orderStatus)) { //2.如果订单状态是未支付:修改订单状态为已支付 orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_PAID); orderInfoMapper.updateById(orderInfo); //3.远程调用"用户服务"虚拟物品发货 //3.1 构建虚拟物品发货vo对象 UserPaidRecordVo userPaidRecordVo = new UserPaidRecordVo(); userPaidRecordVo.setOrderNo(orderNo); userPaidRecordVo.setUserId(orderInfo.getUserId()); userPaidRecordVo.setItemType(orderInfo.getItemType()); //查询订单明细得的购买项目ID列表 List orderDetailList = orderDetailService.list( new LambdaQueryWrapper().eq(OrderDetail::getOrderId, orderInfo.getId()) .select(OrderDetail::getItemId) ); List itemIdList = orderDetailList.stream().map(OrderDetail::getItemId).collect(Collectors.toList()); userPaidRecordVo.setItemIdList(itemIdList); //3.2 执行远程调用,一定要判断调用业务状态码 Result result = userFeignClient.savePaidRecord(userPaidRecordVo); if (result.getCode().intValue() != 200) { throw new GuiguException(result.getCode(), result.getMessage()); } } } ``` 在`service-order-client`模块**OrderFeignClient**中新增Feign远程调用方法 ```java /** * 用户付款成功后,处理订单相关业务 * @param orderNo * @return */ @GetMapping("/orderInfo/orderPaySuccess/{orderNo}") public Result orderPaySuccess(@PathVariable String orderNo); ``` **OrderDegradeFeignClient服务降级类** ```java @Override public Result orderPaySuccess(String orderNo) { log.error("[订单服务]提供远程调用orderPaySuccess方法执行服务降级"); return null; } ``` #### 1.4.3.3 账户服务-更新账户信息 见第2小节。 # 2、充值业务 ## 2.0 保存充值记录 点击我的---->我的钱包 ![支付-充值金额](assets/支付-充值金额.gif) > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/105 前端传递充值金额与充值方式,所以我们需要封装一个实体类**RechargeInfoVo**接受前端传递的数据. `service-account`模块中控制器:**RechargeInfoApiController** ```java /** * 保存充值 * @param rechargeInfoVo * @return {orderNo:"充值订单编号"} */ @GuiGuLogin @Operation(summary = "保存充值") @PostMapping("/rechargeInfo/submitRecharge") public Result> submitRecharge(@RequestBody RechargeInfoVo rechargeInfoVo){ Map map = rechargeInfoService.submitRecharge(rechargeInfoVo); return Result.ok(map); } ``` **RechargeInfoService**接口: ```java /** * 保存充值记录 * @param rechargeInfoVo * @return {orderNo:"充值订单编号"} */ Map submitRecharge(RechargeInfoVo rechargeInfoVo); ``` 实现类: ```java /** * 保存充值 * * @param rechargeInfoVo * @return {orderNo:"充值订单编号"} */ @Override public Map submitRecharge(RechargeInfoVo rechargeInfoVo) { RechargeInfo rechargeInfo = new RechargeInfo(); rechargeInfo.setUserId(AuthContextHolder.getUserId()); //生成充值订单编号 String orderNo = "CZ" + DateUtil.today().replace("-", "") + IdUtil.getSnowflakeNextId(); rechargeInfo.setOrderNo(orderNo); rechargeInfo.setRechargeStatus(SystemConstant.ORDER_STATUS_UNPAID); rechargeInfo.setRechargeAmount(rechargeInfoVo.getAmount()); rechargeInfo.setPayWay(rechargeInfoVo.getPayWay()); rechargeInfoMapper.insert(rechargeInfo); //TODO 超时未支付充值订单关闭,RabbitMQ延迟消息 Map map = new HashMap<>(); map.put("orderNo", orderNo); return map; } ``` ## 2.1 充值成功业务处理 在支付成功之后,会在**支付服务**中处理异步回调处理支付成功业务。 - 在`service-account` 模块添加处理充值成功的充值订单业务Restful接口即可 ### 2.1.1 修改充值状态 > YAPI接口文档地址:http://192.168.200.6:3000/project/11/interface/api/154 在这个**RechargeInfoApiController**类中添加数据 ```java /** * 微信支付成功后,处理充值业务 * @param orderNo * @return */ @Operation(summary = "微信支付成功后,处理充值业务") @GetMapping("/rechargeInfo/rechargePaySuccess/{orderNo}") public Result rechargePaySuccess(@PathVariable String orderNo){ rechargeInfoService.rechargePaySuccess(orderNo); return Result.ok(); } ``` **RechargeInfoService**接口 ```java /** * 微信支付成功后,处理充值业务 * @param orderNo * @return */ void rechargePaySuccess(String orderNo); ``` **RechargeInfoServiceImpl实现** ```java /** * 微信支付成功后,处理充值业务 * * @param orderNo * @return */ @Override public void rechargePaySuccess(String orderNo) { //1.根据充值订单编号查询充值记录 判断状态是否为未支付 RechargeInfo rechargeInfo = rechargeInfoMapper.selectOne( new LambdaQueryWrapper() .eq(RechargeInfo::getOrderNo, orderNo) ); String rechargeStatus = rechargeInfo.getRechargeStatus(); if (SystemConstant.ORDER_STATUS_UNPAID.equals(rechargeStatus)) { //2.更新充值记录:已支付 rechargeInfo.setRechargeStatus(SystemConstant.ORDER_STATUS_PAID); rechargeInfoMapper.updateById(rechargeInfo); //3.充值,对总金额、可用金额增加 BigDecimal amount = rechargeInfo.getRechargeAmount(); boolean flag = userAccountService.update( null, new LambdaUpdateWrapper() .setSql("total_amount = total_amount +" + amount) .setSql("available_amount = available_amount +" + amount) .setSql("total_income_amount = total_income_amount +" + amount) .eq(UserAccount::getUserId, rechargeInfo.getUserId()) ); if (!flag) { throw new GuiguException(500, "充值失败!"); } //4.新增账户变动日志 userAccountService.saveUserAccountDetail(rechargeInfo.getUserId(), "充值", SystemConstant.ACCOUNT_TRADE_TYPE_DEPOSIT, amount, orderNo); } } ``` ### 2.1.2 提供Feign接口 在`service-account-client`模块中提供远程调用Feign接口 **AccountFeignClient** ```java /** * 微信支付成功后,处理充值业务 * @param orderNo * @return */ @GetMapping("/rechargeInfo/rechargePaySuccess/{orderNo}") public Result rechargePaySuccess(@PathVariable String orderNo); ``` **AccountDegradeFeignClient服务降级类** ```java @Override public Result rechargePaySuccess(String orderNo) { log.error("[账户服务]提供远程调用rechargePaySuccess方法执行服务降级"); return null; } ``` ## 2.2 充值记录 ![](assets/tingshu045.png) > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/106 在`service-account` 微服务 UserAccountApiController 控制器添加 ```java /** * 查询用户充值记录 * * @param page * @param limit * @return */ @GuiGuLogin @Operation(summary = "查询用户充值记录") @GetMapping("/userAccount/findUserRechargePage/{page}/{limit}") public Result> getUserRechargePage(@PathVariable int page, @PathVariable int limit) { Long userId = AuthContextHolder.getUserId(); Page pageInfo = new Page<>(page, limit); rechargeInfoService.getUserAccountDetailPage(pageInfo, SystemConstant.ACCOUNT_TRADE_TYPE_DEPOSIT, userId); return Result.ok(pageInfo); } ``` UserAccountService接口: ```java /** * 根据交易类型分页查询账户变动日志 * @param pageInfo * @param tradeType * @param userId * @return */ void getUserAccountDetailPage(Page pageInfo, String tradeType, Long userId); ``` UserAccountServiceImpl实现类: ```java /** * 根据交易类型分页查询账户变动日志 * * @param pageInfo * @param tradeType * @param userId * @return */ @Override public void getUserAccountDetailPage(Page pageInfo, String tradeType, Long userId) { //1.创建分页查询条件对象 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(UserAccountDetail::getUserId, userId); queryWrapper.eq(UserAccountDetail::getTradeType, tradeType); queryWrapper.select(UserAccountDetail::getOrderNo, UserAccountDetail::getTradeType, UserAccountDetail::getTitle, UserAccountDetail::getAmount); userAccountDetailMapper.selectPage(pageInfo, queryWrapper); } ``` ## 2.3 消费记录 ![](assets/tingshu046.png) 消费记录 > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/107 在service-account 微服务中添加 ```java /** * 查询用户消费记录 * * @param page * @param limit * @return */ @GuiGuLogin @Operation(summary = "查询用户消费记录") @GetMapping("/userAccount/findUserConsumePage/{page}/{limit}") public Result> getUserConsumePage(@PathVariable int page, @PathVariable int limit) { Long userId = AuthContextHolder.getUserId(); Page pageInfo = new Page<>(page, limit); rechargeInfoService.getUserAccountDetailPage(pageInfo, SystemConstant.ACCOUNT_TRADE_TYPE_MINUS, userId); return Result.ok(pageInfo); } ``` 测试:充值一百元之后,查看余额与充值记录: ![](assets/tingshu047.png) ![](assets/tingshu048.png)