谷粒随享
学习目标:
需求:当用户进行订单、充值 微信支付为每笔交易产生一条本地交易记录,将来用于对账。
YAPI接口地址:http://192.168.200.6: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;
}
}
YAPI接口地址:http://192.168.200.6: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.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<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;
}
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: 订单编号
* @return
*/
public PaymentInfo savePaymentInfo(String paymentType, String orderNo);
}
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.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<PaymentInfoMapper, PaymentInfo> 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<PaymentInfo> 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;
}
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/102
异步回调需要在微信端配置,并且只能配置一个接口地址,而现在这个接口地址有其他项目在使用。所以我们在前端模拟了一下回调功能,主动查询一下结果!
接收前端传递的支付类型以及订单编号
先在配置文件中添加支付相关信息配置:
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
apiclient_key.pem 商户API私钥路径 的私钥要放入指定位置,让程序读取!
注意:RSAAutoCertificateConfig 对象必须在配置文件中注入到spring 容器中,不要直接使用原生的,否则会报错!
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();
}
}
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<Map<String, String>> createJsapi(@PathVariable String paymentType, @PathVariable String orderNo) {
Map<String, String> map = wxPayService.createJsapi(paymentType, orderNo);
return Result.ok(map);
}
}
package com.atguigu.tingshu.payment.service;
import java.util.Map;
public interface WxPayService {
/**
* 对接微信支付返回小程序拉起微信支付页面所需参数
*
* @param paymentType 支付类型:1301-订单 1302-充值
* @param orderNo 订单号(订单、充值)
* @return {}包含五项参数
*/
Map<String, String> createJsapi(String paymentType, String orderNo);
}
微信支付:小程序调起支付需要返回的参数
支付文档入口:https://developers.weixin.qq.com/miniprogram/dev/api/payment/wx.requestPayment.html 对接方式:https://github.com/wechatpay-apiv3/wechatpay-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<String, String> 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<String, String> 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);
}
}
}
需求:当用户微信付款后,在小程序端需要通过查询微信端交易状态(轮询查询交易状态),如果用户已支付,给小程序端响应结果,展示支付成功页面(展示给用户)。
微信支付接口说明: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
WxPayApiController控制器:
/**
* 根据商户订单编号查询微信支付结果
*
* @param orderNo
* @return
*/
@GuiGuLogin
@Operation(summary = "根据商户订单编号查询微信支付结果")
@GetMapping("/wxPay/queryPayStatus/{orderNo}")
public Result<Boolean> queryPayStatus(@PathVariable String orderNo) {
Boolean flag = wxPayService.queryPayStatus(orderNo);
return Result.ok(flag);
}
WxPayService接口:
/**
* 根据商户订单编号查询微信支付结果
*
* @param orderNo
* @return
*/
Boolean queryPayStatus(String orderNo);
WxPayServiceImpl实现类:
/**
* 查询订单支付状态:调用微信支付接口查询支付状态
*
* @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;
}
大家实现(没有办法支付,故在同步回调中模拟支付成功)-代码:
/**
* 根据商户订单编号查询微信支付结果
*
* @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;
}
微信异步回调说明:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_5.shtml
内网穿透工具:https://natapp.cn/
注册账户,实名认证
购买免费隧道,设置域名映射本地IP及端口
修改netapp程序配置文件
#将本文件放置于natapp同级目录 程序将读取 [default] 段
#在命令行参数模式如 natapp -authtoken=xxx 等相同参数将会覆盖掉此配置
#命令行参数 -config= 可以指定任意config.ini文件
[default]
authtoken=改为自己申请隧道对应的authtoken #对应一条隧道的authtoken
启动netapp程序
修改Nacos中支付系统配置文件中异步回调地址改为最新域名
启动支付系统,拉取最新配置
创建购买订单或充值记录,产生微信交易
在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-order
模块新增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.200.6:8848 # nacos服务地址
group: DEFAULT_GROUP # 默认服务分组
namespace: "" # 默认命名空间
cluster: default # 默认TC集群名称
微信异步回调说明:https://pay.weixin.qq.com/doc/v3/merchant/4012791861
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/104
WxPayApiController控制器:
/**
* 提供给微信支付调用:用户微信付款成功后,微信通知商户支付结果
* @param request
* @return {code:"SUCCESS",message:"成功"}
*/
@Operation(summary = "提供给微信支付调用:用户微信付款成功后,微信通知商户支付结果")
@PostMapping("/wxPay/notify")
public Map<String, String> notifyPayResult(HttpServletRequest request) {
return wxPayService.notifyPayResult(request);
}
WxPayService接口:
/**
* 提供给微信支付调用:用户微信付款成功后,微信通知商户支付结果
* @param request
* @return {code:"SUCCESS",message:"成功"}
*/
Map<String, String> notifyPayResult(HttpServletRequest request);
WxPayServiceImpl实现类:
@Autowired
private RedisTemplate redisTemplate;
/**
* 提供给微信支付调用:用户微信付款成功后,微信通知商户支付结果
*
* @param request
* @return
*/
@Override
public Map<String, String> 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<String, String> 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;
}
PaymentInfoService
/**
* 用户付款成功后,更新本地交易记录及关联的订单或充值相关业务处理
* @param transaction
*/
void handlerPaymentInfo(Transaction transaction);
PaymentInfoServiceImpl
/**
* 用户付款成功后,更新本地交易记录及关联的订单或充值相关业务处理
*
* @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<PaymentInfo>().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("远程调用-充值异常");
}
}
}
}
YAPI接口地址:http://192.168.200.6: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
* @return
*/
void orderPaySuccess(String orderNo);
OrderInfoServiceImpl
/**
* 用户付款成功后,处理订单相关业务
*
* @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<OrderDetail> orderInfoOrderDetailList = orderInfo.getOrderDetailList();
if (CollectionUtil.isNotEmpty(orderInfoOrderDetailList)) {
List<Long> 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远程调用方法
/**
* 用户付款成功后,处理订单相关业务
* @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;
}
见第2小节。
点击我的---->我的钱包
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/105
前端传递充值金额与充值方式,所以我们需要封装一个实体类RechargeInfoVo接受前端传递的数据.
service-account
模块中控制器:RechargeInfoApiController
/**
* 保存充值记录
* @param rechargeInfoVo
* @return {orderNo:"充值订单编号"}
*/
@GuiGuLogin
@Operation(summary = "保存充值记录")
@PostMapping("/rechargeInfo/submitRecharge")
public Result<Map<String, String>> submitRecharge(@RequestBody RechargeInfoVo rechargeInfoVo){
Map<String, String> map = rechargeInfoService.submitRecharge(rechargeInfoVo);
return Result.ok(map);
}
RechargeInfoService接口:
/**
* 保存充值记录
* @param rechargeInfoVo
* @return {orderNo:"充值订单编号"}
*/
Map<String, String> submitRecharge(RechargeInfoVo rechargeInfoVo);
实现类:
/**
* 保存充值记录
*
* @param rechargeInfoVo
* @return {orderNo:"充值订单编号"}
*/
@Override
public Map<String, String> 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<String, String> map = new HashMap<>();
map.put("orderNo", orderNo);
//4.TODO 发送延迟消息 延迟关闭充值记录
return map;
}
在支付成功之后,会在支付服务中处理异步回调处理支付成功业务。
service-account
模块添加处理充值成功的充值订单业务Restful接口即可YAPI接口文档地址:http://192.168.200.6:3000/project/11/interface/api/154
在这个RechargeInfoApiController类中添加数据
/**
* 用户付款后处理充值成功业务
*
* @param orderNo
* @return
*/
@Operation(summary = "用户付款后处理充值成功业务")
@GetMapping("/rechargeInfo/rechargePaySuccess/{orderNo}")
public Result rechargePaySuccess(@PathVariable String orderNo) {
rechargeInfoService.rechargePaySuccess(orderNo);
return Result.ok();
}
RechargeInfoService接口
/**
* 用户付款后处理充值成功业务
*
* @param orderNo
*/
void rechargePaySuccess(String orderNo);
RechargeInfoServiceImpl实现
@Autowired
private UserAccountService userAccountService;
/**
* 用户付款后处理充值成功业务
*
* @param orderNo
*/
@Override
public void rechargePaySuccess(String orderNo) {
//1.修改充值状态
//1.1. 根据充值订单编号查询充值记录
LambdaQueryWrapper<RechargeInfo> 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<UserAccount> 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);
}
}
在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;
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/106
在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) {
Long userId = AuthContextHolder.getUserId();
Page<UserAccountDetail> pageInfo = new Page<>(page, limit);
rechargeInfoService.getUserAccountDetailPage(pageInfo, SystemConstant.ACCOUNT_TRADE_TYPE_DEPOSIT, userId);
return Result.ok(pageInfo);
}
UserAccountService接口:
/**
* 根据交易类型分页查询账户变动日志
* @param pageInfo
* @param tradeType
* @param userId
* @return
*/
void getUserAccountDetailPage(Page<UserAccountDetail> pageInfo, String tradeType, Long userId);
UserAccountServiceImpl实现类:
/**
* 根据交易类型分页查询账户变动日志
*
* @param pageInfo
* @param tradeType
* @param userId
* @return
*/
@Override
public void getUserAccountDetailPage(Page<UserAccountDetail> pageInfo, String tradeType, Long userId) {
//1.创建分页查询条件对象
LambdaQueryWrapper<UserAccountDetail> 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);
}
消费记录
YAPI接口地址:http://192.168.200.6: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) {
Long userId = AuthContextHolder.getUserId();
Page<UserAccountDetail> pageInfo = new Page<>(page, limit);
rechargeInfoService.getUserAccountDetailPage(pageInfo, SystemConstant.ACCOUNT_TRADE_TYPE_MINUS, userId);
return Result.ok(pageInfo);
}
测试:充值一百元之后,查看余额与充值记录: