**谷粒随享** ## 第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) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(RechargeInfo::getOrderNo, orderNo); return rechargeInfoMapper.selectOne(queryWrapper); } } ``` 远程调用模块`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) { //0.根据订单/充值编号查询交易记录 存在直接返回,不存在新增 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(PaymentInfo::getOrderNo, orderNo); PaymentInfo paymentInfo = this.getOne(queryWrapper); if (paymentInfo != null) { return paymentInfo; }else{ //1.构建本地交易记录对象 paymentInfo = new PaymentInfo(); //1.1 封装对象中简单属性 Long userId = AuthContextHolder.getUserId(); paymentInfo.setUserId(userId); paymentInfo.setPaymentType(paymentType); paymentInfo.setOrderNo(orderNo); paymentInfo.setPayWay(SystemConstant.ORDER_PAY_WAY_WEIXIN); paymentInfo.setPaymentStatus(SystemConstant.PAYMENT_STATUS_UNPAID); //1.2 封装本地交易记录中金额跟内容-远程调用其他服务获取 //1.3 判断支付类型:订单 if (SystemConstant.PAYMENT_TYPE_ORDER.equals(paymentType)) { //1.3.1 远程调用订单服务获取订单信息 OrderInfo orderInfo = orderFeignClient.getOrderInfo(orderNo).getData(); Assert.notNull(orderInfo, "订单不存在"); //1.3.2 校验订单状态是否为:未支付 boolean flag = SystemConstant.ORDER_STATUS_UNPAID.equals(orderInfo.getOrderStatus()); Assert.state(flag, "订单状态有误!"); //1.3.3 封装本地交易记录中交易内容跟金额 paymentInfo.setAmount(orderInfo.getOrderAmount()); paymentInfo.setContent(orderInfo.getOrderTitle()); } else if (SystemConstant.PAYMENT_TYPE_RECHARGE.equals(paymentType)) { //1.4 判断支付类型:充值 //1.4.1 远程调用账户服务获取充值信息 RechargeInfo rechargeInfo = accountFeignClient.getRechargeInfo(orderNo).getData(); Assert.notNull(rechargeInfo, "充值记录不存在"); //1.4.2 校验充值状态是否为:未支付 boolean flag = SystemConstant.ORDER_STATUS_UNPAID.equals(rechargeInfo.getRechargeStatus()); Assert.state(flag, "充值状态有误!"); //1.4.3 封装本地交易记录中交易内容跟金额 paymentInfo.setAmount(rechargeInfo.getRechargeAmount()); paymentInfo.setContent("用户充值:" + rechargeInfo.getRechargeAmount()); } //2.保存本地交易记录对象 this.save(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 {}包含五项参数 */ @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 {}包含五项参数 */ 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 {}包含五项参数 */ @Override public Map createJsapi(String paymentType, String orderNo) { try { //1.新增本地交易记录 PaymentInfo paymentInfo = paymentInfoService.savePaymentInfo(paymentType, orderNo); if (SystemConstant.PAYMENT_STATUS_PAID.equals(paymentInfo.getPaymentStatus())) { throw new RuntimeException("该笔交易已支付"); } //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 (RuntimeException e) { log.error("创建微信支付JSAPI参数失败", e); throw new RuntimeException(e); } } } ``` ## 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 flag = wxPayService.queryPayStatus(orderNo); return Result.ok(flag); } ``` **WxPayService**接口: ```java /** * 根据商户订单编号查询微信支付结果 * * @param orderNo * @return */ Boolean queryPayStatus(String orderNo); ``` **WxPayServiceImpl**实现类: ```java /** * 查询订单支付状态:调用微信支付接口查询支付状态 * * @param orderNo * @return */ @Override @GlobalTransactional(rollbackFor = Exception.class) public Boolean queryPayStatus(String orderNo) { //1 JSAPI支付和APP支付业务类对象 JsapiServiceExtension service = new JsapiServiceExtension.Builder().config(rsaAutoCertificateConfig).build(); //2.创建商户订单查询支付结果请求对象 QueryOrderByOutTradeNoRequest queryRequest = new QueryOrderByOutTradeNoRequest(); queryRequest.setMchid(wxPayV3Config.getMerchantId()); queryRequest.setOutTradeNo(orderNo); try { //3.执行查询 Transaction transaction = service.queryOrderByOutTradeNo(queryRequest); if (transaction != null) { if (transaction.getTradeState() == Transaction.TradeStateEnum.SUCCESS) { Integer payerTotal = transaction.getAmount().getPayerTotal(); if (1 == payerTotal) { return true; } } } } catch (Exception e) { // API返回失败, 例如ORDER_NOT_EXISTS log.error("[支付系统]同步查询支付状态异常:{}", e); } return false; } ``` **大家实现(没有办法支付,故在同步回调中模拟支付成功)-代码**: ```java /** * 根据商户订单编号查询微信支付结果 * * @param orderNo 商户订单编号 * @return */ @Override public Boolean queryPayStatus(String orderNo) { //TODO 模拟用户已经完成付款(商户入账),处理本地交易记录及订单或充值业务 Transaction transaction = new Transaction(); transaction.setOutTradeNo(orderNo); transaction.setTransactionId("wx"+ IdUtil.getSnowflakeNextId()); paymentInfoService.handlerPaymentInfo(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 {code:"SUCCESS",message:"成功"} */ @Operation(summary = "提供给微信支付调用:用户微信付款成功后,微信通知商户支付结果") @PostMapping("/wxPay/notify") public Map notifyPayResult(HttpServletRequest request) { return wxPayService.notifyPayResult(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 notifyPayResult(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.幂等性处理:微信会多次重复发送通知共计时间24h4m if (transaction != null) { //3.1 微信支付系统生成的订单号 String transactionId = transaction.getTransactionId(); //3.2 采用Redis进行幂等性处理 String key = "pay:notify:" + transactionId; Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, transactionId, 25, TimeUnit.HOURS); if (flag) { try { //4.校验通知的信息是否与商户侧的信息一致:校验应付金额跟实付金额是否一致 if (transaction.getAmount().getTotal().intValue() == 1) { //5.TODO 核心业务:更新本地交易及订单或者充值记录状态-存在分布式事务问题 paymentInfoService.handlerPaymentInfo(transaction); //6.返回微信正确应答 Map map = new HashMap<>(); map.put("code", "SUCCESS"); map.put("message", "成功"); return map; } } catch (Exception e) { //如果业务发生异常无法正确应答微信,删除key,等待微信下次通知 redisTemplate.delete(key); throw new RuntimeException(e); } } } return null; } ``` #### 1.4.3.1 更新本地交易记录 **PaymentInfoService** ```java /** * 用户付款成功后,更新本地交易记录及关联的订单或充值相关业务处理 * @param transaction */ void handlerPaymentInfo(Transaction transaction); ``` **PaymentInfoServiceImpl** ```java /** * 用户付款成功后,更新本地交易记录及关联的订单或充值相关业务处理 * * @param transaction */ @Override @GlobalTransactional(rollbackFor = Exception.class) public void handlerPaymentInfo(Transaction transaction) { //1.核心业务1:更新本地交易-支付状态、回调内容、回调时间、微信端交易编号 //1.1 获取交易对应商户订单编号 String orderNo = transaction.getOutTradeNo(); //1.2 根据订单编号查询本地交易记录 PaymentInfo paymentInfo = paymentInfoMapper.selectOne(new LambdaQueryWrapper().eq(PaymentInfo::getOrderNo, orderNo)); if (paymentInfo != null && SystemConstant.PAYMENT_STATUS_UNPAID.equals(paymentInfo.getPaymentStatus())) { //1.3 获取到微信交易订单编号 String transactionId = transaction.getTransactionId(); paymentInfo.setPaymentStatus(SystemConstant.PAYMENT_STATUS_PAID); paymentInfo.setOutTradeNo(transactionId); paymentInfo.setCallbackTime(new Date()); paymentInfo.setCallbackContent(transaction.toString()); paymentInfoMapper.updateById(paymentInfo); //支付类型:1301-订单 1302-充值 String paymentType = paymentInfo.getPaymentType(); //2.核心业务2-如果交易类型是订单,远程调用订单服务更新订单并且完成虚拟物品发货 if (SystemConstant.PAYMENT_TYPE_ORDER.equals(paymentType)) { //2.1 远程调用订单服务修改订单状态:已支付 Result result = orderFeignClient.orderPaySuccess(orderNo); if (!result.getCode().equals(200)) { throw new RuntimeException("远程调用-更新订单状态异常"); } } //3.TODO 核心业务3-如果交易类型是充值,远程调用账户服务完成充值业务 if (SystemConstant.PAYMENT_TYPE_RECHARGE.equals(paymentType)) { Result result = accountFeignClient.rechargePaySuccess(orderNo); if (!result.getCode().equals(200)) { throw new RuntimeException("远程调用-充值异常"); } } } } ``` #### 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 * @return */ @Override public void orderPaySuccess(String orderNo) { //1.修改订单状态改为已支付 OrderInfo orderInfo = this.getOrderInfo(orderNo); orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_PAID); orderInfoMapper.updateById(orderInfo); //2.远程调用用户服务完成虚拟物品发货 //2.1 构建虚拟物品发货VO对象 UserPaidRecordVo userPaidRecordVo = new UserPaidRecordVo(); userPaidRecordVo.setOrderNo(orderInfo.getOrderNo()); userPaidRecordVo.setUserId(orderInfo.getUserId()); //2.1.1 购买项目类型:付款项目类型: 1001-专辑 1002-声音 1003-vip会员' userPaidRecordVo.setItemType(orderInfo.getItemType()); //2.1.2 从订单中获取订单明细得到购买项目ID列表 List orderInfoOrderDetailList = orderInfo.getOrderDetailList(); if (CollectionUtil.isNotEmpty(orderInfoOrderDetailList)) { List itemIdList = orderInfoOrderDetailList.stream().map(OrderDetail::getItemId).collect(Collectors.toList()); userPaidRecordVo.setItemIdList(itemIdList); //2.2 判断业务状态码 Result result = userFeignClient.savePaidRecord(userPaidRecordVo); if (!result.getCode().equals(200)) { throw new RuntimeException("远程调用-虚拟物品发货异常"); } } } ``` 在`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) { //1.构建充值记录对象 RechargeInfo rechargeInfo = new RechargeInfo(); rechargeInfo.setUserId(AuthContextHolder.getUserId()); rechargeInfo.setRechargeStatus(SystemConstant.ORDER_STATUS_UNPAID); rechargeInfo.setRechargeAmount(rechargeInfoVo.getAmount()); rechargeInfo.setPayWay(SystemConstant.ORDER_PAY_WAY_WEIXIN); //生成充值订单编号 CZ+日期+雪花算法 String orderNo = "CZ" + DateUtil.today().replace("-", "") + IdUtil.getSnowflakeNextId(); rechargeInfo.setOrderNo(orderNo); //2.执行保存充值记录 rechargeInfoMapper.insert(rechargeInfo); //3.封装响应对象中订单编号 Map map = new HashMap<>(); map.put("orderNo", orderNo); //4.TODO 发送延迟消息 延迟关闭充值记录 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 */ void rechargePaySuccess(String orderNo); ``` **RechargeInfoServiceImpl实现** ```java @Autowired private UserAccountService userAccountService; /** * 用户付款后处理充值成功业务 * * @param orderNo */ @Override public void rechargePaySuccess(String orderNo) { //1.修改充值状态 //1.1. 根据充值订单编号查询充值记录 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(RechargeInfo::getOrderNo, orderNo); RechargeInfo rechargeInfo = rechargeInfoMapper.selectOne(queryWrapper); if (SystemConstant.ORDER_STATUS_UNPAID.equals(rechargeInfo.getRechargeStatus())) { //1.2 修改充值状态:已支付 rechargeInfo.setRechargeStatus(SystemConstant.ORDER_STATUS_PAID); rechargeInfoMapper.updateById(rechargeInfo); //2.余额充值 LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); BigDecimal amount = rechargeInfo.getRechargeAmount(); updateWrapper.setSql("total_amount = total_amount + " + amount + ", available_amount = available_amount + " + amount + ", total_income_amount = total_income_amount + " + amount); updateWrapper.eq(UserAccount::getUserId, rechargeInfo.getUserId()); userAccountService.update(updateWrapper); //3.新增账户变动日志 userAccountService.saveUserAccountDetail(rechargeInfo.getUserId(), "充值:" + amount, 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)