谷粒随享
第10章 微信支付充值业务
学习目标:
-
微信支付(对接第三方支付平台)
-
基于微信支付完成订单付款
-
基于微信支付完成充值业务
1、微信支付
1.1 保存交易记录
需求:当用户进行订单、充值 微信支付为每笔交易产生一条本地交易记录,将来用于对账。
1.1.1 获取订单对象
YAPI接口地址:http://192.168.254.156:3000/project/11/interface/api/100
已有Restful接口实现。只需要在service-order-client
模块中新增Feign远程调用方法
OrderFeignClient
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; /** * <p> * 订单模块远程调用API接口 * </p> * * @author atguigu */ @FeignClient(value = "service-order", path = "api/order", fallback = OrderDegradeFeignClient.class) public interface OrderFeignClient { /** * 根据订单编号查询订单详情(订单商品明细、订单优惠明细) * * @param orderNo * @return */ @GetMapping("/orderInfo/getOrderInfo/{orderNo}") public Result<OrderInfo> getOrderInfo(@PathVariable String orderNo); }
OrderDegradeFeignClient熔断类:
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<OrderInfo> getOrderInfo(String orderNo) { log.error("[订单模块]提供远程调用getOrderInfo服务降级"); return null; } }
1.1.2 获取充值记录信息
YAPI接口地址:http://192.168.254.156:3000/project/11/interface/api/101
service-account
微服务RechargeInfoApiController控制器中添加
package com.atguigu.tingshu.account.api; import com.atguigu.tingshu.account.service.RechargeInfoService; 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<RechargeInfo> getRechargeInfo(@PathVariable String orderNo){ RechargeInfo rechargeInfo = rechargeInfoService.getRechargeInfo(orderNo); return Result.ok(rechargeInfo); } }
RechargeInfoService接口
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<RechargeInfo> { /** * 根据充值订单编号查询充值记录 * @param orderNo * @return */ RechargeInfo getRechargeInfo(String orderNo); }
RechargeInfoServiceImpl实现类
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<RechargeInfoMapper, RechargeInfo> implements RechargeInfoService { @Autowired private RechargeInfoMapper rechargeInfoMapper; /** * 根据充值订单编号查询充值记录 * @param orderNo * @return */ @Override public RechargeInfo getRechargeInfo(String orderNo) { LambdaQueryWrapper<RechargeInfo> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(RechargeInfo::getOrderNo, orderNo); return rechargeInfoMapper.selectOne(queryWrapper); } }
远程调用模块service-account-client
中AccountFeignClient增加远程调用方法
/** * 根据充值订单编号查询充值记录 * @param orderNo * @return */ @GetMapping("/rechargeInfo/getRechargeInfo/{orderNo}") public Result<RechargeInfo> getRechargeInfo(@PathVariable String orderNo);
AccountDegradeFeignClient熔断类:
@Override public Result<RechargeInfo> getRechargeInfo(String orderNo) { log.error("[账户服务]提供远程调用接口getRechargeInfo服务降级"); return null; }
1.1.3 保存本地交易记录
service-payment
模块中增加保存本地交易业务处理
PaymentInfoService
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<PaymentInfo> { /** * 保存本地交易记录 * * @param paymentType 支付类型 支付类型:1301-订单 1302-充值 * @param orderNo 订单编号 * @param userId 用户ID * @return */ PaymentInfo savePaymentInfo(String paymentType, String orderNo, Long userId); }
PaymentInfoServiceImpl实现类
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.execption.GuiguException; 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<PaymentInfoMapper, PaymentInfo> implements PaymentInfoService { @Autowired private OrderFeignClient orderFeignClient; @Autowired private AccountFeignClient accountFeignClient; /** * 保存本地交易记录 * * @param paymentType 支付类型 支付类型:1301-订单 1302-充值 * @param orderNo 订单编号 * @param userId 用户ID * @return */ @Override public PaymentInfo savePaymentInfo(String paymentType, String orderNo, Long userId) { //1.根据订单编号查询本地交易记录 如果存在则返回即可 LambdaQueryWrapper<PaymentInfo> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(PaymentInfo::getOrderNo, orderNo); PaymentInfo paymentInfoDB = this.getOne(queryWrapper); if (paymentInfoDB != null) { return paymentInfoDB; } //2.构建本地交易记录对象 paymentInfoDB = new PaymentInfo(); //2.1 基本属性赋值 用户ID、交易类型、订单编号、付款方式(微信)、交易状态:未支付(1401) paymentInfoDB.setUserId(userId); paymentInfoDB.setOrderNo(orderNo); paymentInfoDB.setPaymentType(paymentType); paymentInfoDB.setPayWay(SystemConstant.ORDER_PAY_WAY_WEIXIN); paymentInfoDB.setPaymentStatus(SystemConstant.PAYMENT_STATUS_UNPAID); //2.2 封装交易内容及金额(远程调用订单服务或者账户服务获取) if (SystemConstant.PAYMENT_TYPE_ORDER.equals(paymentType)) { //远程调用订单服务获取订单金额及购买项目(VIP会员,专辑、声音) OrderInfo orderInfo = orderFeignClient.getOrderInfo(orderNo).getData(); Assert.notNull(orderInfo, "订单不存在!"); if(!SystemConstant.ORDER_STATUS_UNPAID.equals(orderInfo.getOrderStatus())){ throw new GuiguException(211, "订单状态错误!"); } paymentInfoDB.setAmount(orderInfo.getOrderAmount()); paymentInfoDB.setContent(orderInfo.getOrderTitle()); } else if (SystemConstant.PAYMENT_TYPE_RECHARGE.equals(paymentType)) { //远程调用账户服务获取充值金额 RechargeInfo rechargeInfo = accountFeignClient.getRechargeInfo(orderNo).getData(); Assert.notNull(rechargeInfo, "充值记录不存在!"); if(!SystemConstant.ORDER_STATUS_UNPAID.equals(rechargeInfo.getRechargeStatus())){ throw new GuiguException(211, "充值订单状态错误!"); } paymentInfoDB.setAmount(rechargeInfo.getRechargeAmount()); paymentInfoDB.setContent("充值" + rechargeInfo.getRechargeAmount()); } //3.保存本地交易记录且返回 this.save(paymentInfoDB); return paymentInfoDB; } }
1.2 对接微信支付
微信支付小程序入口:小程序下单 - 小程序支付 | 微信支付商户文档中心
支付文档入口:wx.requestPayment(Object object) | 微信开放文档 对接方式:GitHub - wechatpay-apiv3/wechatpay-java: 微信支付 APIv3 的官方 Java Library
接收前端传递的支付类型以及订单编号
-
先在配置文件中添加支付相关信息配置:
spring.application.name=service-payment spring.profiles.active=dev spring.main.allow-bean-definition-overriding=true spring.cloud.nacos.discovery.server-addr=192.168.254.156:8848 spring.cloud.nacos.config.server-addr=192.168.254.156: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
-
apiclient_key.pem 商户API私钥路径 的私钥要放入指定位置,让程序读取!
-
注意:RSAAutoCertificateConfig 对象必须在配置文件中注入到spring 容器中,不要直接使用原生的,否则会报错!
GitHub - wechatpay-apiv3/wechatpay-java: 微信支付 APIv3 的官方 Java Library
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 支付控制器
YAPI接口地址:http://192.168.254.156:3000/project/11/interface/api/102
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.*; 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 小程序拉调用wx.requestPayment(Object object)需要参数 */ @GuiGuLogin @Operation(summary = "获取微信小程序拉起本地微信支付所需要参数") @PostMapping("/wxPay/createJsapi/{paymentType}/{orderNo}") public Result<Map<String, Object>> getWxPrePayParams(@PathVariable String paymentType, @PathVariable String orderNo) { Map<String, Object> mapResult = wxPayService.getWxPrePayParams(paymentType, orderNo); return Result.ok(mapResult); } }
1.2.2 支付接口
package com.atguigu.tingshu.payment.service; import java.util.Map; public interface WxPayService { /** * 获取微信小程序拉起本地微信支付所需要参数 * * @param paymentType 支付类型:支付类型:1301-订单 1302-充值 * @param orderNo 订单编号 * @return 小程序拉调用wx.requestPayment(Object object)需要参数 */ Map<String, Object> getWxPrePayParams(String paymentType, String orderNo); }
1.2.3 支付实现类
微信支付:小程序调起支付需要返回的参数
支付文档入口:wx.requestPayment(Object object) | 微信开放文档 对接方式:GitHub - wechatpay-apiv3/wechatpay-java: 微信支付 APIv3 的官方 Java Library
package com.atguigu.tingshu.payment.service.impl; import com.atguigu.tingshu.common.util.AuthContextHolder; 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.JsapiService; import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension; import com.wechat.pay.java.service.payments.jsapi.model.*; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; @Service @Slf4j public class WxPayServiceImpl implements WxPayService { @Autowired private PaymentInfoService paymentInfoService; @Autowired private RSAAutoCertificateConfig rsaAutoCertificateConfig; @Autowired private WxPayV3Config wxPayV3Config; /** * 获取微信小程序拉起本地微信支付所需要参数 * * @param paymentType 支付类型:支付类型:1301-订单 1302-充值 * @param orderNo 订单编号 * @return 小程序拉调用wx.requestPayment(Object object)需要参数 */ @Override public Map<String, Object> getWxPrePayParams(String paymentType, String orderNo) { Long userId = AuthContextHolder.getUserId(); //1.将本次交易记录保存到本地交易表 PaymentInfo paymentInfo = paymentInfoService.savePaymentInfo(paymentType, orderNo, userId); //2.调用微信SDK获取小程序支付所需参数 //2.1 构建调用微信接口业务对象 JsapiServiceExtension jsapiService = new JsapiServiceExtension.Builder().config(rsaAutoCertificateConfig).build(); //2.2 构建预支付请求对象 PrepayRequest prepayRequest = new PrepayRequest(); Amount amount = new Amount(); //2.2.1 设置预支付订单金额 单位:分 TODO开发接口暂时硬编码1分 实际应该从paymentInfo对象中获取 amount.setTotal(1); prepayRequest.setAmount(amount); prepayRequest.setAppid(wxPayV3Config.getAppid()); prepayRequest.setMchid(wxPayV3Config.getMerchantId()); prepayRequest.setDescription(paymentInfo.getContent()); prepayRequest.setNotifyUrl(wxPayV3Config.getNotifyUrl()); //2.3 TODO 目前小程序未发布,开发阶段必选设置付款人(真正付款只有应用开发者列表中微信账户才有权限) Payer payer = new Payer(); payer.setOpenid("odo3j4qp-wC3HVq9Z_D9C0cOr0Zs");//TODO同学改为自己微信账户OpenID 能拉起微信支付,但是付款会报错:当前用户不是开发者 prepayRequest.setPayer(payer); //2.2.2 商户端订单编号 prepayRequest.setOutTradeNo(paymentInfo.getOrderNo()); //2.3 调用下单方法(小程序所需参数),得到应答 PrepayWithRequestPaymentResponse response = jsapiService.prepayWithRequestPayment(prepayRequest); if (response != null) { Map<String, Object> mapResult = new HashMap<>(); mapResult.put("timeStamp", response.getTimeStamp()); mapResult.put("package", response.getPackageVal()); mapResult.put("paySign", response.getPaySign()); mapResult.put("signType", response.getSignType()); mapResult.put("nonceStr", response.getNonceStr()); return mapResult; } return null; } }
1.3 查询支付状态(同步)
需求:当用户微信付款后,在小程序端需要通过查询微信端交易状态(轮询查询交易状态),如果用户已支付,给小程序端响应结果,展示支付成功页面(展示给用户)。
微信支付接口说明:微信支付-开发者文档
YAPI接口地址:http://192.168.254.156:3000/project/11/interface/api/103
WxPayApiController控制器:
/** * 小程序轮询查询支付结果-根据商户订单编号查询交易状态 * * @param orderNo * @return */ @Operation(summary = "小程序轮询查询支付结果-根据商户订单编号查询交易状态") @GetMapping("/wxPay/queryPayStatus/{orderNo}") public Result<Boolean> queryPayStatus(@PathVariable String orderNo) { Boolean isPay = wxPayService.queryPayStatus(orderNo); return Result.ok(isPay); }
WxPayService接口:
/** * 根据商户订单编号查询交易状态 * @param orderNo * @return */ Boolean queryPayStatus(String orderNo);
WxPayServiceImpl实现类:
/** * 根据商户订单编号查询交易状态 * * @param orderNo * @return */ @Override public Boolean queryPayStatus(String orderNo) { //1.创建查询交易请求对象 QueryOrderByOutTradeNoRequest queryOrderByOutTradeNoRequest = new QueryOrderByOutTradeNoRequest(); queryOrderByOutTradeNoRequest.setMchid(wxPayV3Config.getMerchantId()); queryOrderByOutTradeNoRequest.setOutTradeNo(orderNo); //2.创建调用微信服务端业务对象 JsapiServiceExtension jsapiService = new JsapiServiceExtension.Builder().config(rsaAutoCertificateConfig).build(); //3.调用微信查询交易状态接口 Transaction transaction = jsapiService.queryOrderByOutTradeNo(queryOrderByOutTradeNoRequest); //4.解析响应结果返回交易状态 if (transaction != null) { Transaction.TradeStateEnum tradeState = transaction.getTradeState(); if (Transaction.TradeStateEnum.SUCCESS == tradeState) { //用户支付成功 return true; } } return false; }
大家实现(没有办法支付,故在同步回调中模拟支付成功)-代码:
/** * 根据商户订单编号查询交易状态 * * @param orderNo * @return */ @Override @GlobalTransactional(rollbackFor = Exception.class) public Boolean queryPayStatus(String orderNo) { //1.创建查询交易请求对象 //QueryOrderByOutTradeNoRequest queryOrderByOutTradeNoRequest = new QueryOrderByOutTradeNoRequest(); //queryOrderByOutTradeNoRequest.setMchid(wxPayV3Config.getMerchantId()); //queryOrderByOutTradeNoRequest.setOutTradeNo(orderNo); // 2.创建调用微信服务端业务对象 //JsapiServiceExtension jsapiService = new JsapiServiceExtension.Builder().config(rsaAutoCertificateConfig).build(); // 3.调用微信查询交易状态接口 //Transaction transaction = jsapiService.queryOrderByOutTradeNo(queryOrderByOutTradeNoRequest); // 4.解析响应结果返回交易状态 //if (transaction != null) { // Transaction.TradeStateEnum tradeState = transaction.getTradeState(); // if (Transaction.TradeStateEnum.SUCCESS == tradeState) { // //用户支付成功 // return true; // } //} //return false; //1.伪造微信交易对象 Transaction transaction = new Transaction(); transaction.setOutTradeNo(orderNo); transaction.setTransactionId("WX"+ IdUtil.getSnowflakeNextId()); //修改支付记录状态 paymentInfoService.updatePaymentInfoSuccess(transaction); return true; }
1.4 支付异步回调
微信异步回调说明:微信支付-开发者文档
1.4.1 内网穿透工具
内网穿透工具:NATAPP-内网穿透 基于ngrok的国内高速内网映射工具
-
注册账户,实名认证
-
购买免费隧道,设置域名映射本地IP及端口
-
修改netapp程序配置文件
#将本文件放置于natapp同级目录 程序将读取 [default] 段 #在命令行参数模式如 natapp -authtoken=xxx 等相同参数将会覆盖掉此配置 #命令行参数 -config= 可以指定任意config.ini文件 [default] authtoken=改为自己申请隧道对应的authtoken #对应一条隧道的authtoken
-
启动netapp程序
-
修改Nacos中支付系统配置文件中异步回调地址改为最新域名
-
启动支付系统,拉取最新配置
-
创建购买订单或充值记录,产生微信交易
1.4.2 支付服务集成Seata
-
在
tingshu_payment
数据库中新增undo_log日志表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;
-
在
service-payment
模块pom.xml中增加Seata启动依赖<!--seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <!-- 默认seata客户端版本比较低,排除后重新引入指定版本--> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> </dependency>
-
订单微服务
service-payment
模块新增Seata配置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.254.156:8848 # nacos服务地址 group: DEFAULT_GROUP # 默认服务分组 namespace: "" # 默认命名空间 cluster: default # 默认TC集群名称
1.4.3 支付服务-处理异步回调
微信异步回调说明:微信支付-开发者文档
YAPI接口地址:http://192.168.254.156:3000/project/11/interface/api/104
WxPayApiController控制器:
/** * 提供给微信支付进行异步回调接口 * * @param request * @return */ @Operation(summary = "提供给微信支付进行异步回调接口") @PostMapping("/wxPay/notify") public Map<String, String> paySuccessNotify(HttpServletRequest request) { Map<String, String> mapResult = wxPayService.paySuccessNotify(request); return mapResult; }
WxPayService接口:
/** * 用户付款成功后,处理微信支付异步回调 * @param request * @return */ Map<String, String> paySuccessNotify(HttpServletRequest request);
WxPayServiceImpl实现类:
/** * 用户付款成功后,处理微信支付异步回调 * * @param request * @return */ @Override @GlobalTransactional(rollbackFor = Exception.class) public Map<String, String> paySuccessNotify(HttpServletRequest request) { //1.从请求头中获取微信提交参数 String wechatPaySerial = request.getHeader("Wechatpay-Serial"); //签名 String nonce = request.getHeader("Wechatpay-Nonce"); //签名中的随机数 String timestamp = request.getHeader("Wechatpay-Timestamp"); //时间戳 String signature = request.getHeader("Wechatpay-Signature"); //签名类型 //HTTP 请求体 body。切记使用原始报文,不要用 JSON 对象序列化后的字符串,避免验签的 body 和原文不一致。 String requestBody = PayUtil.readData(request); //2.构建RequestParam请求参数对象 RequestParam requestParam = new RequestParam.Builder() .serialNumber(wechatPaySerial) .nonce(nonce) .signature(signature) .timestamp(timestamp) .body(requestBody) .build(); //3.// 初始化 NotificationParser 解析器对象 NotificationParser parser = new NotificationParser(rsaAutoCertificateConfig); //4. 调用解析器对象解析方法 验签、解密 并转换成 Transaction Transaction transaction = parser.parse(requestParam, Transaction.class); if (transaction != null) { //4.1 业务验证,验证付款状态以及用户实际付款金额跟商户侧金额是否一致 if (Transaction.TradeStateEnum.SUCCESS == transaction.getTradeState()) { Integer payerTotal = transaction.getAmount().getPayerTotal(); //todo 调试阶段支付金额为1分,后续改为动态从本地交易记录中获取实际金额 if (payerTotal.intValue() == 1) { //4.2 更新本地交易记录状态 paymentInfoService.updatePaymentInfoSuccess(transaction); Map<String, String> map = new HashMap<>(); map.put("code", "SUCCESS"); map.put("message", "SUCCESS"); return map; } } } return null; }
1.4.3.1 更新本地交易记录
PaymentInfoService
/** * 用户付款成功后,修改本地交易记录 * @param transaction 微信交易对象 */ void updatePaymentInfoSuccess(Transaction transaction);
PaymentInfoServiceImpl
/** * 用户付款成功后,修改本地交易记录 * * @param transaction 微信交易对象 */ @Override @Transactional(rollbackFor = Exception.class) public void updatePaymentInfoSuccess(Transaction transaction) { //1.根据订单编号查询本地交易记录状态 String orderNo = transaction.getOutTradeNo(); LambdaQueryWrapper<PaymentInfo> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(PaymentInfo::getOrderNo, orderNo); PaymentInfo paymentInfo = paymentInfoMapper.selectOne(queryWrapper); if (SystemConstant.PAYMENT_STATUS_PAID.equals(paymentInfo.getPaymentStatus())) { //如果已支付:返回即可 return; } //2.修改本地交易记录 //2.1 本地交易记录关联微信支付交易ID paymentInfo.setOutTradeNo(transaction.getTransactionId()); //2.2 更新回调时间,及回调内容 paymentInfo.setCallbackTime(new Date()); paymentInfo.setCallbackContent(transaction.toString()); //2.3 将本地交易支付状态:已支付 paymentInfo.setPaymentStatus(SystemConstant.PAYMENT_STATUS_PAID); paymentInfoMapper.updateById(paymentInfo); //3.todo 远程调用订单服务/账户服务 完成订单/充值状态变更:已支付 //3.1 判断支付类型:1301-订单 if (SystemConstant.PAYMENT_TYPE_ORDER.equals(paymentInfo.getPaymentType())) { Result result = orderFeignClient.orderPaySuccess(orderNo); if (200 != result.getCode()) { throw new GuiguException(500, "远程修改订单状态异常:" + orderNo); } } //3.2 TODO 判断支付类型:1302-充值 if (SystemConstant.PAYMENT_TYPE_RECHARGE.equals(paymentInfo.getPaymentType())) { Result result = accountFeignClient.rechargePaySuccess(orderNo); if (200 != result.getCode()) { throw new GuiguException(500, "远程修改余额异常:" + orderNo); } } }
1.4.3.2 订单服务-更新订单状态
YAPI接口地址:http://192.168.254.156:3000/project/11/interface/api/145
在service-order
模块OrderInfoApiController处理订单支付成功
/** * 用户支付成功后,修改订单状态 * @param orderNo 订单编号 * @return */ @Operation(summary = "用户支付成功后,修改订单状态") @GetMapping("/orderInfo/orderPaySuccess/{orderNo}") public Result orderPaySuccess(@PathVariable String orderNo){ orderInfoService.orderPaySuccess(orderNo); return Result.ok(); }
OrderInfoService
/** * 用户支付成功后,修改订单状态 * @param orderNo 订单编号 */ void orderPaySuccess(String orderNo);
OrderInfoServiceImpl
/** * 用户支付成功后,修改订单状态(远程调用用户服务-新增用户购买记录) * * @param orderNo 订单编号 */ @Override @Transactional(rollbackFor = Exception.class) public void orderPaySuccess(String orderNo) { //1.根据订单编号查询订单记录 判断订单状态 OrderInfo orderInfo = this.getOrderInfo(orderNo); if (orderInfo != null && SystemConstant.ORDER_STATUS_PAID.equals(orderInfo.getOrderStatus())) { return; } //2.修改订单支付状态:已支付 orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_PAID); orderInfoMapper.updateById(orderInfo); //3.远程调用“用户服务”新增用户购买记录(VIP,专辑) //3.1 构建用户购买记录VO对象 UserPaidRecordVo userPaidRecordVo = new UserPaidRecordVo(); userPaidRecordVo.setOrderNo(orderInfo.getOrderNo()); userPaidRecordVo.setUserId(orderInfo.getUserId()); userPaidRecordVo.setItemType(orderInfo.getItemType()); //遍历订单中订单商品明细封装购买项目ID List<Long> itemIdList = orderInfo.getOrderDetailList().stream() .map(OrderDetail::getItemId) .collect(Collectors.toList()); userPaidRecordVo.setItemIdList(itemIdList); //3.2 远程调用为用户新增购买记录 Result userResult = userFeignClient.savePaidRecord(userPaidRecordVo); if (200 != userResult.getCode()) { throw new GuiguException(500, "新增购买记录异常!"); } }
在service-order-client
模块OrderFeignClient中新增Feign远程调用方法
/** * 用户支付成功后,修改订单状态 * @param orderNo 订单编号 * @return */ @GetMapping("/orderInfo/orderPaySuccess/{orderNo}") public Result orderPaySuccess(@PathVariable String orderNo);
OrderDegradeFeignClient服务降级类
@Override public Result orderPaySuccess(String orderNo) { log.error("[订单服务]远程调用orderPaySuccess服务降级"); return null; }
1.4.3.3 账户服务-更新账户信息
见第2小节。
2、充值业务
2.1保存充值记录
点击我的---->我的钱包
YAPI接口地址:http://192.168.254.156:3000/project/11/interface/api/105
在service-account
模块中添加控制器
前端传递充值金额与充值方式,所以我们需要封装一个实体类接受前端传递的数据.
实体类RechargeInfoVo
@Data @Schema(description = "充值对象") public class RechargeInfoVo { @Schema(description = "充值金额") private BigDecimal amount; @Schema(description = "支付方式:1101-微信 1102-支付宝") private String payWay; }
service-account
模块中控制器:RechargeInfoApiController
/** * 新增充值记录 * @param rechargeInfoVo * @return {orderNo:"充值交易订单编号"} */ @Operation(summary = "新增充值记录") @GuiGuLogin @PostMapping("/rechargeInfo/submitRecharge") public Result<Map> submitRecharge(@RequestBody RechargeInfoVo rechargeInfoVo){ Map<String, String> mapResult = rechargeInfoService.submitRecharge(rechargeInfoVo); return Result.ok(mapResult); }
RechargeInfoService接口:
/** * 新增充值记录 * @param rechargeInfoVo * @return {orderNo:"充值交易订单编号"} */ Map<String, String> submitRecharge(RechargeInfoVo rechargeInfoVo);
实现类:
/** * 新增充值记录 * * @param rechargeInfoVo * @return {orderNo:"充值交易订单编号"} */ @Override public Map<String, String> submitRecharge(RechargeInfoVo rechargeInfoVo) { //1.构建充值记录对象 Long userId = AuthContextHolder.getUserId(); RechargeInfo rechargeInfo = new RechargeInfo(); rechargeInfo.setUserId(userId); //1.1 生成本次充值记录订单编号:CZ+日期+雪花算法ID String orderNo = "CZ" + DateUtil.today().replaceAll("-", "") + IdUtil.getSnowflakeNextId(); rechargeInfo.setOrderNo(orderNo); //1.2 充值记录状态:未支付 rechargeInfo.setRechargeStatus(SystemConstant.ORDER_STATUS_UNPAID); rechargeInfo.setRechargeAmount(rechargeInfoVo.getAmount()); rechargeInfo.setPayWay(rechargeInfoVo.getPayWay()); //2.保存充值记录 rechargeInfoMapper.insert(rechargeInfo); Map<String, String> mapResult = new HashMap<>(); mapResult.put("orderNo", orderNo); return mapResult; }
2.2充值成功业务处理
在支付成功之后,会在支付服务中提供异步回调处理支付成功业务。
-
在
service-account
模块添加处理充值成功的充值订单业务Restful接口即可
2.2.1 修改充值状态
YAPI接口文档地址:http://192.168.254.156:3000/project/11/interface/api/154
在这个UserAccountApiController类中添加数据
/** * 用户充值,支付成功后,充值业务处理 * @param orderNo * @return */ @Operation(summary = "用户充值,支付成功后,充值业务处理") @GetMapping("/rechargeInfo/rechargePaySuccess/{orderNo}") public Result rechargePaySuccess(@PathVariable String orderNo){ userAccountService.rechargePaySuccess(orderNo); return Result.ok(); }
UserAccountService接口
/** * 用户充值,支付成功后,充值业务处理 * @param orderNo * @return */ void rechargePaySuccess(String orderNo);
RechargeInfoServiceImpl实现
@Autowired private RechargeInfoService rechargeInfoService; /** * 用户充值,支付成功后,充值业务处理 * * @param orderNo * @return */ @Override @Transactional(rollbackFor = Exception.class) public void rechargePaySuccess(String orderNo) { //1.根据订单编号查询充值记录 判断状态:未支付才处理 RechargeInfo rechargeInfo = rechargeInfoService.getRechargeInfo(orderNo); if (rechargeInfo != null && SystemConstant.ORDER_STATUS_PAID.equals(rechargeInfo.getRechargeStatus())) { return; } //2.新增余额 int count = userAccountMapper.add(rechargeInfo.getUserId(), rechargeInfo.getRechargeAmount()); if (count == 0) { throw new GuiguException(400, "充值异常"); } //3.新增账户变动日志 this.saveUserAccountDetail( rechargeInfo.getUserId(), "充值:" + rechargeInfo.getRechargeAmount(), SystemConstant.ACCOUNT_TRADE_TYPE_DEPOSIT, rechargeInfo.getRechargeAmount(), orderNo ); //4.修改充值记录状态:已支付 rechargeInfo.setRechargeStatus(SystemConstant.ORDER_STATUS_PAID); rechargeInfoService.updateById(rechargeInfo); }
2.2.2 账户充值
UserAccountMapper
/** * 账户充值 * @param userId * @param rechargeAmount * @return */ int add(@Param("userId") Long userId, @Param("amount") BigDecimal rechargeAmount);
UserAccountMapper.xml
<!--账户充值--> <update id="add"> UPDATE user_account SET total_amount = total_amount + #{amount}, available_amount = available_amount + #{amount}, total_income_amount = total_income_amount + #{amount} WHERE user_id = #{userId} </update>a
2.2.3 提供Feign接口
在service-account-client
模块中提供远程调用Feign接口
AccountFeignClient
/** * 用户充值,支付成功后,充值业务处理 * @param orderNo * @return */ @GetMapping("/rechargeInfo/rechargePaySuccess/{orderNo}") public Result rechargePaySuccess(@PathVariable String orderNo);
AccountDegradeFeignClient服务降级类
@Override public Result rechargePaySuccess(String orderNo) { log.error("[账户服务]执行服务降级方法:rechargePaySuccess"); return null; }
2.3 充值记录
YAPI接口地址:http://192.168.254.156:3000/project/11/interface/api/106
用户充值记录回显信息保存到实体类:
package com.atguigu.tingshu.model.account; import com.atguigu.tingshu.model.base.BaseEntity; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.math.BigDecimal; @Data @Schema(description = "UserAccountDetail") @TableName("user_account_detail") public class UserAccountDetail extends BaseEntity { private static final long serialVersionUID = 1L; @Schema(description = "用户id") @TableField("user_id") private Long userId; @Schema(description = "交易标题") @TableField("title") private String title; @Schema(description = "交易类型:1201-充值 1202-锁定 1203-解锁 1204-消费") @TableField("trade_type") private String tradeType; @Schema(description = "金额") @TableField("amount") private BigDecimal amount; @Schema(description = "订单编号") @TableField("order_no") private String orderNo; }
在service-account
微服务 UserAccountApiController 控制器添加
/** * 分页查询当前用户充值记录 * * @param page * @param limit * @return */ @GuiGuLogin @Operation(summary = "分页查询当前用户充值记录") @GetMapping("/userAccount/findUserRechargePage/{page}/{limit}") public Result<Page<UserAccountDetail>> getUserRechargePage(@PathVariable int page, @PathVariable int limit) { Page<UserAccountDetail> pageInfo = new Page<>(page, limit); userAccountService.getUserAccountDetailPage(pageInfo, SystemConstant.ACCOUNT_TRADE_TYPE_DEPOSIT); return Result.ok(pageInfo); }
接口:
/** * 分页查询当前用户充值、消费记录 * * @param pageInfo 分页对象 * @param tradeType 交易类型:1201-充值 1204-消费 * @return */ void getUserAccountDetailPage(Page<UserAccountDetail> pageInfo, String tradeType);
实现类:
/** * 分页查询当前用户充值、消费记录 * * @param pageInfo 分页对象 * @param tradeType 交易类型:1201-充值 1204-消费 * @return */ @Override public void getUserAccountDetailPage(Page<UserAccountDetail> pageInfo, String tradeType) { //1.获取当前登录用户ID Long userId = AuthContextHolder.getUserId(); //2.调用持久层获取充值记录 pageInfo = userAccountMapper.getUserAccountDetailPage(pageInfo, userId, tradeType); }
package com.atguigu.tingshu.account.mapper; import com.atguigu.tingshu.model.account.UserAccountDetail; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @Mapper public interface UserAccountDetailMapper extends BaseMapper<UserAccountDetail> { /** * 分页查询当前用户充值记录 * @param pageInfo * @param userId * @return */ Page<UserAccountDetail> getUserAccountDetailPage(Page<UserAccountDetail> pageInfo, @Param("userId") Long userId, @Param("tradeType") String tradeType); }
xml 配置文件
<!--分页查询当前用户充值/消费记录--> <select id="getUserAccountDetailPage" resultType="com.atguigu.tingshu.model.account.UserAccountDetail"> select * from user_account_detail where user_id = #{userId} and trade_type = #{tradeType} and is_deleted = 0 order by id desc </select>
2.4消费记录
消费记录
YAPI接口地址:http://192.168.254.156:3000/project/11/interface/api/107
在service-account 微服务中添加
/** * 分页查询当前用户消费记录 * * @param page * @param limit * @return */ @GuiGuLogin @Operation(summary = "分页查询当前用户消费记录") @GetMapping("/userAccount/findUserConsumePage/{page}/{limit}") public Result<Page<UserAccountDetail>> getUserConsumePage(@PathVariable int page, @PathVariable int limit) { Page<UserAccountDetail> pageInfo = new Page<>(page, limit); userAccountService.getUserAccountDetailPage(pageInfo, SystemConstant.ACCOUNT_TRADE_TYPE_MINUS); return Result.ok(pageInfo); }
测试:充值一百元之后,查看余额与
充值记录: