云岚到家 支付实现
小程序支付调研
小程序调起支付这里,微信会校验小程序的APPID与微信支付商户的ID是否绑定,微信支付商户的ID怎么获取呢?是需要注册商户上传企业资料及法人资料,微信审核通过后方可 注册成功,所以注册成为一个普通商户对大家有限制。
小程序不能通过拉起H5页面做jsapi支付,小程序内只能使用小程序支付
前期准备
注册商户
首先参照接入前的准备进行申请:
进行小程序运营的企业需要申请成为微信的普通商户,为企业提供支付服务的需要注册成为普通服务商。
步骤:
1、申请成为商户
点击“成为商户”填写法人、公司等信息等待审核。
2、申请AppID
对于普通商户,该社交载体可以是公众号(什么是公众号 (opens new window)),小程序(什么是小程序 (opens new window))或App。
本项目需要申请一个小程序账号,拿到AppID。
3、申请mchid(商户号)
进入商户平台申请:https://pay.weixin.qq.com/index.php/core/info
4、绑定AppID及mchid
AppID和mchid全部申请完毕后,需要建立两者之间的绑定关系。
开通微信支付
-
申请小程序开发者账号,进行微信认证,获取AppID登录《微信公众平台》 (opens new window),注册一个小程序的开发者账号。小程序账号申请指引(opens new window)
-
小程序开通微信支付,即申请或复用微信支付商户号,申请完小程序后,登录小程序后台 (opens new window)。点击左侧导航栏的微信支付,在页面中进行开通。
总体流程
前期准备工作完毕下边调研小程序支付接口。
接口地址:https://pay.weixin.qq.com/docs/merchant/products/mini-program-payment/apilist.html
API列表
上边表格加粗的表示必须对接的接口。
下边的流程是业务系统(凡是和微信支付对接的系统统称为业务系统)与微信支付接口的交互流程,列出了支付、支付结果查询、退款三个接口的交互流程。
业务系统请求微信支付下单要将业务系统自己的订单号和订单金额告诉微信支付,只要这样在支付完成后才可以根据业务系统的订单号去查询支付结果。
比如:用户在家政平台下单,然后进行支付,家政平台服务端程序需要调用 微信支付的下单接口,将家政平台的订单号与订单金额传给微信下单接口。
小程序下单
当点击支付时,业务系统向微信发起下单请求。
请求方式:【POST】/v3/pay/transactions/jsapi
请求域名:【主域名】https://api.mch.weixin.qq.com
请求方向:业务系统--->微信
请求参数
除了appid、mchid商户id等必要参数以外,与业务相关的最重要的就是out_trade_no和amount,它是业务系统中的订单号及订单金额。
业务系统请求微信支付下单要将业务系统自己的订单号和订单金额告诉微信支付,只要这样在支付完成后才可以根据业务系统的订单号去查询支付结果。
比如:用户在家政平台下单,然后进行支付,家政平台服务端程序需要调用 微信支付的下单接口,将家政平台的订单号与订单金额传给微信下单接口。
如何生成请求签名 - 通用规则 | 微信支付商户文档中心
签名认证
在head参数中有一个Authorization,它表示请求接口的签名认证信息,是按照微信要求生成的加密串和一些基本信息。
签名是对原始数据通过签名算法生成的一段数据(签名串),用于证明数据的真实性和完整性。签名通常使用密钥进行生成,这个密钥可以是对称密钥或非对称密钥。
验签是对签名串进行验证的过程,用于确认数据的真实性和完整性。验签的过程通常使用与签名过程中相对应的公钥进行解密。
签名和验签是为了防止内容被篡改。
如何生成请求签名:https://pay.wechatpay.cn/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml
过程如下:
按照规则生成原始信息
使用签名算法、私钥对原始信息进行哈希,得到哈希串
如何验证签名:https://pay.weixin.qq.com/docs/merchant/development/interface-rules/signature-verification.html
过程如下:
获取原始信息
使用签名算法、公钥对原始信息进行哈希,对比是否和签名串一致,一致则验证通过。
签名和验签是为了防止内容被篡改。
小程序调起支付
小程序下单成功后微信返回一个prepay_id即预支付会话标识,小程序使用微信支付提供的小程序方法调起小程序支付。
调用wx.requestPayment(OBJECT)发起微信支付
接口名称: wx.requestPayment,详见小程序API文档(opens new window)
Object请求参数说明:
-
timeStamp必填string(32)
-
时间戳,标准北京时间,时区为东八区,自1970年1月1日 0点0分0秒以来的秒数。注意:部分系统取到的值为毫秒级,需要转换成秒(10位数字)。
-
nonceStr必填string(32)
-
随机字符串,不长于32位。
-
package必填string(128)
-
小程序下单接口返回的
prepay_id
参数值,提交格式如:prepay_id=***
-
signType必填string(32)
-
签名类型,默认为RSA,仅支持RSA。
-
paySign必填string(512)
-
签名,使用字段
appid
、timeStamp
、nonceStr
、package
计算得出的签名值 签名所使用的appid
,为【小程序下单】时传入的appid
,微信支付会校验下单与调起支付所使用的appid
的一致性。
native 扫码支付
扫码支付的流程与小程序支付基本类似,只是小程序支付改为了用户扫码支付,流程如下:
暂时无法在飞书文档外展示此内容
加密与解密
加密是将原始的、可读的数据(称为明文)通过某种算法和密钥转换成不可读的数据(称为密文)。加密的目的是防止未经授权的访问者能够理解或识别被加密的信息。加密算法通常基于密钥,有对称加密和非对称加密两种主要类型。
-
对称加密: 使用相同的密钥进行加密和解密。常见的对称加密算法包括 AES、DES、3DES。
-
非对称加密: 使用一对密钥,包括公钥和私钥,公钥用于加密,私钥用于解密,或者私钥用于加密,公钥用于解密。常见的非对称加密算法包括 RSA、ECC。
解密是加密的逆过程,即将密文还原为明文。只有持有正确密钥的人或系统能够进行解密操作。解密的目的是还原加密前的原始信息,使其能够被理解和使用。
加密与解密是为了防止内容被泄露,保证内容的机密性。
为什么单独开发一个支付服务呢?
一个项目或一个企业通常有很多系统,这些系统有很多都有支付的需求,如果每个系统都和微信、支付宝等这些第三方平台对接一遍很多工作是重复的,抽取了支付服务就是要把这个工作进行简化,简化开发支付业务,通常你到企业中一般都是和现成的支付系统进行对接。
测试支付接口
下边我们测试支付接口,用户点击支付按钮,前端请求订单服务,订单服务请求支付服务,最后支付服务请求微信的下单接口。
请求参数说明:
- changeChannel:当用户先微信支付,然后又进行支付宝支付表示切换了支付渠道,此时传入true
- tradingChannel:微信支付传入WECHAT_PAY,支付宝支付传入ALI_PAY
- memo: 备注信息
- productOrderNo是业务系统的订单号,本项目就是家政服务的订单号。
- enterpriseId:商户号,进入微信或支付宝商户平台获取。
- tradingAmount:支付金额
响应参数说明:
- tradingChannel:微信或支付宝
- tradingOrderNo:支付服务生成的交易号(微信的订交易号)
- productOrderNo:业务系统的订单号,本项目就是家政服务的订单号。
- qrCode:二维码base64
注意:保证每次请求支付下单传入productOrderNo不一致,productOrderNo表示商品的订单,同一个订单不能重复支付
注意:这里要记住支付服务生成的交易单号tradingOrderNo,稍后查询交易结果使用。
支付结果查询
测试退款接口
退款接口是异步的,退款发出时,可能接受到的响应参数是 退款中 ,但是可能已经收到退款,但是退款状态还是退款中
理解支付服务的设计
-
支付渠道表
支付渠道表存储了第三方支付(微信、支付宝)的支付参数,如:商户号、证书序列号、api私钥等信息
create table `jzo2o-trade`.pay_channel
(
id bigint not null comment '主键'
constraint `PRIMARY`
primary key,
channel_name varchar(32) null comment '通道名称',
channel_label varchar(32) null comment '通道唯一标记',
domain varchar(255) null comment '域名',
app_id varchar(32) collate utf8_bin not null comment '商户appid',
public_key varchar(2000) collate utf8_bin not null comment '支付公钥',
merchant_private_key varchar(2000) collate utf8_bin not null comment '商户私钥',
other_config varchar(1000) null comment '其他配置',
encrypt_key varchar(255) charset utf8mb4 null comment 'AES混淆密钥',
remark varchar(400) null comment '说明',
notify_url varchar(255) null comment '回调地址',
enable_flag varchar(10) null comment '是否有效',
enterprise_id bigint null comment '商户ID【系统内部识别使用】',
create_time datetime default CURRENT_TIMESTAMP null comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间'
)
comment '交易渠道表' collate = utf8mb4_unicode_ci;
-
交易单表
支付服务请求第三方支付下单成功向交易表写入一条记录
家政服务的一个订单可能对应支付服务的多条交易单,比如:用户用微信支付在交易单表生成一条交易单,如果微信支付失败再用支付宝支付时也会在交易单表中生成一条记录。
用户支付成功后支付服务更新交易单表的支付状态。
create table `jzo2o-trade`.trading
(
id bigint not null comment '主键'
constraint `PRIMARY`
primary key,
product_order_no bigint not null comment '业务系统订单号',
trading_order_no bigint not null comment '交易系统订单号【对于三方来说:商户订单】',
transaction_id varchar(50) null comment '第三方支付交易号',
trading_channel varchar(32) charset utf8mb4 not null comment '支付渠道【支付宝、微信、现金、免单挂账】',
trading_type varchar(22) not null comment '交易类型【付款、退款、免单、挂账】',
trading_state int not null comment '交易单状态【2-付款中,3-付款失败,4-已结算,5-取消订单,6-免单,7-挂账】',
payee_name varchar(50) null comment '收款人姓名',
payee_id bigint null comment '收款人账户ID',
payer_name varchar(50) null comment '付款人姓名',
payer_id bigint null comment '付款人Id',
trading_amount decimal(22, 2) not null comment '交易金额,单位:元',
refund decimal(12, 2) null comment '退款金额【付款后,单位:元',
is_refund varchar(32) charset utf8mb4 null comment '是否有退款:YES,NO',
result_code varchar(80) null comment '第三方交易返回编码【最终确认交易结果】',
result_msg varchar(255) null comment '第三方交易返回提示消息【最终确认交易信息】',
result_json varchar(2000) null comment '第三方交易返回信息json【分析交易最终信息】',
place_order_code varchar(80) null comment '统一下单返回编码',
place_order_msg varchar(255) null comment '统一下单返回信息',
place_order_json text null comment '统一下单返回信息json【用于生产二维码、Android ios唤醒支付等】',
enterprise_id bigint not null comment '商户号',
memo varchar(150) null comment '备注【订单门店,桌台信息】',
qr_code text null comment '二维码base64数据',
open_id varchar(36) collate utf8mb4_unicode_ci null comment 'open_id标识',
enable_flag varchar(10) null comment '是否有效',
create_time datetime default CURRENT_TIMESTAMP null comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',
constraint trading_order_no
unique (trading_order_no) comment '支付订单号'
)
comment '交易订单表' charset = utf8;
-
退款记录表
用户申请退款在退款记录表写一条记录。
退款成功后支付服务更新退款状态。
create table `jzo2o-trade`.refund_record
(
id bigint not null comment '主键'
constraint `PRIMARY`
primary key,
trading_order_no bigint not null comment '交易系统订单号【对于三方来说:商户订单】',
product_order_no bigint not null comment '业务系统订单号',
refund_no bigint not null comment '本次退款订单号',
refund_id varchar(50) null comment '第三方支付的退款单号',
enterprise_id bigint not null comment '商户号',
trading_channel varchar(32) charset utf8mb4 not null comment '退款渠道【支付宝、微信、现金】',
refund_status int not null comment '退款状态:0-发起退款,1-退款中,2-成功, 3-失败',
refund_code varchar(80) charset utf8 null comment '返回编码',
refund_msg text charset utf8 null comment '返回信息',
memo varchar(150) charset utf8 null comment '备注【订单门店,桌台信息】',
refund_amount decimal(12, 2) not null comment '本次退款金额',
total decimal(12, 2) not null comment '原订单金额',
create_time datetime default CURRENT_TIMESTAMP null comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',
constraint refund_no
unique (refund_no)
)
comment '退款记录表' collate = utf8mb4_unicode_ci;
数据流
- 支付接口:收到支付请求后请求第三方支付的下单接口,并向交易单表新增记录。
- 查询交易结果接口:请求第三方支付的查询支付结果并更新交易单表的支付状态。
- 接收第三方通过支付结果:更新交易单表的支付状态。
- 退款接口:新增退款记录
- 更新退款状态:请求第三方退款结果查询接口查询退款状态,并更新退款状态。
代码
支付接口
- 首先查询交易单是否生成交易单,如果已生成直接返回
- 如果切换支付渠道且已存在原有支付渠道的交易单,则先关闭原支付渠道的交易单,避免重复支付。
- 使用分布式锁防止同一个交易单重复支付
- 根据支付渠道调用不同的bean请求第三方支付
- 获取第三方返回的支付URL,支付服务生成支付二维码,插入交易单记录。
public Trading createDownLineTrading(boolean changeChannel,Trading tradingEntity) {
//获取付款中的记录
Trading trading = tradingService.queryDuringTrading(tradingEntity.getProductAppId(),tradingEntity.getProductOrderNo(), tradingEntity.getTradingChannel());
//如果切换二维码需要查询其它支付渠道付款中的交易单进行退款操作
if(changeChannel){
changeChannelAndCloseTrading(tradingEntity.getProductAppId(),tradingEntity.getProductOrderNo(),tradingEntity.getTradingChannel());
}
//付款中的记录直接返回无需生成二维码
if (ObjectUtil.isNotNull(trading)){
return trading;
}
//交易前置处理:检测交易单参数
beforePayHandler.checkCreateTrading(tradingEntity);
tradingEntity.setTradingType(TradingConstant.TRADING_TYPE_FK);
tradingEntity.setEnableFlag(Constants.YES);
//对交易订单加锁
Long productOrderNo = tradingEntity.getProductOrderNo();
String key = TradingCacheConstant.CREATE_PAY + productOrderNo;
RLock lock = redissonClient.getFairLock(key);
try {
//获取锁
if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) {
//交易前置处理:幂等性处理
// this.beforePayHandler.idempotentCreateTrading(tradingEntity);
//调用不同的支付渠道进行处理
PayChannelEnum payChannel = PayChannelEnum.valueOf(tradingEntity.getTradingChannel());
NativePayHandler nativePayHandler = HandlerFactory.get(payChannel, NativePayHandler.class);
nativePayHandler.createDownLineTrading(tradingEntity);
//生成统一收款二维码
String placeOrderMsg = tradingEntity.getPlaceOrderMsg();
String qrCode = this.qrCodeService.generate(placeOrderMsg, payChannel);
tradingEntity.setQrCode(qrCode);
//指定交易状态为付款中
tradingEntity.setTradingState(TradingStateEnum.FKZ);
//新增交易数据
boolean flag = this.tradingService.save(tradingEntity);
if (!flag) {
throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.SAVE_OR_UPDATE_FAIL.getValue());
}
return tradingEntity;
}
throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_PAY_FAIL.getValue());
} catch (CommonException e) {
throw e;
} catch (Exception e) {
log.error("统一收单线下交易预创建异常:{}", ExceptionUtil.stacktraceToString(e));
throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_PAY_FAIL.getValue());
} finally {
lock.unlock();
}
}
退款接口
首先查询有没有退款记录,有退款记录(若为退款中),查询微信退款接口更新退款状态
没有退款记录则插入退款记录,请求微信退款接口
家政订单支付功
用户点击支付按钮,前端请求订单服务,订单服务请求支付服务,最后支付服务请求微信的下单接口。
流程如下
要生成二维码需要由订单管理服务请求支付服务的支付接口,支付服务的支付接口如下:
接口路径:POST/trade/inner/native
支付方法实现:
查询订单,如果订单不存在,返回异常。如果支付成功直接返回。
查看订单状态,如果未支付,请求支付,判断支付渠道,如果支付渠道不为空,判断支付渠道是否更换,如更换则让原来的支付渠道失效。发送支付请求。更改支付渠道和交易号。
/**
* 订单支付
*
* @param id 订单id
* @param ordersPayReqDTO 订单支付请求体
* @return 订单支付响应体
*/
@Override
public OrdersPayResDTO pay(Long id, OrdersPayReqDTO ordersPayReqDTO) {
Orders orders = baseMapper.selectById(id);
if (ObjectUtil.isNull(orders)) {
throw new CommonException(TRADE_FAILED, "订单不存在");
}
//订单的支付状态为成功直接返回
if (OrderPayStatusEnum.PAY_SUCCESS.getStatus() == orders.getPayStatus()
&& ObjectUtil.isNotEmpty(orders.getTradingOrderNo())) {
OrdersPayResDTO ordersPayResDTO = new OrdersPayResDTO();
BeanUtil.copyProperties(orders, ordersPayResDTO);
ordersPayResDTO.setProductOrderNo(orders.getId());
return ordersPayResDTO;
} else {
//生成二维码
NativePayResDTO nativePayResDTO = generateQrCode(orders, ordersPayReqDTO.getTradingChannel());
OrdersPayResDTO ordersPayResDTO = BeanUtil.toBean(nativePayResDTO, OrdersPayResDTO.class);
return ordersPayResDTO;
}
}
//生成二维码
private NativePayResDTO generateQrCode(Orders orders, PayChannelEnum tradingChannel) {
//判断支付渠道
Long enterpriseId = ObjectUtil.equal(PayChannelEnum.ALI_PAY, tradingChannel) ?
tradeProperties.getAliEnterpriseId() : tradeProperties.getWechatEnterpriseId();
//构建支付请求参数
NativePayReqDTO nativePayReqDTO = new NativePayReqDTO();
//商户号
nativePayReqDTO.setEnterpriseId(enterpriseId);
//业务系统标识
nativePayReqDTO.setProductAppId("jzo2o.orders");
//家政订单号
nativePayReqDTO.setProductOrderNo(orders.getId());
//支付渠道
nativePayReqDTO.setTradingChannel(tradingChannel);
//支付金额
nativePayReqDTO.setTradingAmount(orders.getRealPayAmount());
//备注信息
nativePayReqDTO.setMemo(orders.getServeItemName());
//判断是否切换支付渠道
if (ObjectUtil.isNotEmpty(orders.getTradingChannel())
&& ObjectUtil.notEqual(orders.getTradingChannel(), tradingChannel.toString())) {
nativePayReqDTO.setChangeChannel(true);
}
//生成支付二维码
NativePayResDTO downLineTrading = nativePayApi.createDownLineTrading(nativePayDTO);
if(ObjectUtils.isNotNull(downLineTrading)){
log.info("订单:{}请求支付,生成二维码:{}",orders.getId(),downLineTrading.toString());
//将二维码更新到交易订单中
boolean update = lambdaUpdate()
.eq(Orders::getId, downLineTrading.getProductOrderNo())
.set(Orders::getTradingOrderNo, downLineTrading.getTradingOrderNo())
.set(Orders::getTradingChannel, downLineTrading.getTradingChannel())
.update();
if(!update){
throw new CommonException("订单:"+orders.getId()+"请求支付更新交易单号失败");
}
}
return downLineTrading;
}
查询支付结果
(这个是用户点击查看支付结果)
在用户支付后用户点击“完成支付”此时前端请求订单服务的查询支付结果接口,如果支付成功则跳转到支付成功界面。
查询订单状态中的支付结果,如果支付状态为待支付,并且存在交易单号,那么请求微服务去查询,并更新订单表,如果订单状态为支付成功,则直接返回相关信息
@Override
public OrdersPayResDTO getPayResultFromTradServer(Long id) {
//查询订单表
Orders orders = baseMapper.selectById(id);
if (ObjectUtil.isNull(orders)) {
throw new CommonException(TRADE_FAILED, "订单不存在");
}
//支付结果
Integer payStatus = orders.getPayStatus();
//未支付且已存在支付服务的交易单号此时远程调用支付服务查询支付结果
if (OrderPayStatusEnum.NO_PAY.getStatus() == payStatus
&& ObjectUtil.isNotEmpty(orders.getTradingOrderNo())) {
//远程调用支付服务查询支付结果
TradingResDTO tradingResDTO = tradingApi.findTradResultByTradingOrderNo(orders.getTradingOrderNo());
//如果支付成功这里更新订单状态
if (ObjectUtil.isNotNull(tradingResDTO)
&& ObjectUtil.equals(tradingResDTO.getTradingState(), TradingStateEnum.YJS)) {
//设置订单的支付状态成功
TradeStatusMsg msg = TradeStatusMsg.builder()
.productOrderNo(orders.getId())
.tradingChannel(tradingResDTO.getTradingChannel())
.statusCode(TradingStateEnum.YJS.getCode())
.tradingOrderNo(tradingResDTO.getTradingOrderNo())
.transactionId(tradingResDTO.getTransactionId())
.build();
owner.paySuccess(msg);
//构造返回数据
OrdersPayResDTO ordersPayResDTO = BeanUtils.toBean(tradeStatusMsg, OrdersPayResDTO.class);
ordersPayResDTO.setPayStatus(OrderPayStatusEnum.PAY_SUCCESS.getStatus());
return ordersPayResDTO;
}
}
OrdersPayResDTO ordersPayResDTO = new OrdersPayResDTO();
ordersPayResDTO.setPayStatus(payStatus);
ordersPayResDTO.setProductOrderNo(orders.getId());
ordersPayResDTO.setTradingOrderNo(orders.getTradingOrderNo());
ordersPayResDTO.setTradingChannel(orders.getTradingChannel());
return ordersPayResDTO;
}
/**
* 支付成功, 其他信息暂且不填
*
* @param tradeStatusMsg 交易状态消息
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void paySuccess(TradeStatusMsg tradeStatusMsg) {
//查询订单
Orders orders = baseMapper.selectById(tradeStatusMsg.getProductOrderNo());
if (ObjectUtil.isNull(orders)) {
throw new CommonException(TRADE_FAILED, "订单不存在");
}
//校验支付状态如果不是待支付状态则不作处理
if (ObjectUtil.notEqual(OrderPayStatusEnum.NO_PAY.getStatus(), orders.getPayStatus())) {
log.info("更新订单支付成功,当前订单:{}支付状态不是待支付状态", orders.getId());
return;
}
//校验订单状态如果不是待支付状态则不作处理
if (ObjectUtils.notEqual(OrderStatusEnum.NO_PAY.getStatus(),orders.getOrdersStatus())) {
log.info("更新订单支付成功,当前订单:{}状态不是待支付状态", orders.getId());
}
//第三方支付单号校验
if (ObjectUtil.isEmpty(tradeStatusMsg.getTransactionId())) {
throw new CommonException("支付成功通知缺少第三方支付单号");
}
//更新订单的支付状态及第三方交易单号等信息
boolean update = lambdaUpdate()
.eq(Orders::getId, orders.getId())
.set(Orders::getPayTime, LocalDateTime.now())//支付时间
.set(Orders::getTradingOrderNo, tradeStatusMsg.getTradingOrderNo())//交易单号
.set(Orders::getTradingChannel, tradeStatusMsg.getTradingChannel())//支付渠道
.set(Orders::getTransactionId, tradeStatusMsg.getTransactionId())//第三方支付交易号
.set(Orders::getPayStatus, OrderPayStatusEnum.PAY_SUCCESS.getStatus())//支付状态
.set(Orders::getOrdersStatus, OrderStatusEnum.DISPATCHING.getStatus())//订单状态更新为派单中
.update();
if(!update){
log.info("更新订单:{}支付成功失败", orders.getId());
throw new CommonException("更新订单"+orders.getId()+"支付成功失败");
}
}
接收支付通知
(系统自动更新)
只处理自己的业务的
使用 JSON.parseArray
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = MqConstants.Queues.ORDERS_TRADE_UPDATE_STATUS),
exchange = @Exchange(name = MqConstants.Exchanges.TRADE, type = ExchangeTypes.TOPIC),
key = MqConstants.RoutingKeys.TRADE_UPDATE_STATUS
))
public void listenTradeUpdatePayStatusMsg(String msg) {
log.info("接收到支付结果状态的消息 ({})-> {}", MqConstants.Queues.ORDERS_TRADE_UPDATE_STATUS, msg);
//将msg转为java对象
List<TradeStatusMsg> tradeStatusMsgs = JSON.parseArray(msg, TradeStatusMsg.class);
// 只处理家政服务的订单且是支付成功的
List<TradeStatusMsg> msgList = tradeStatusMsgList.stream().filter(v -> v.getStatusCode().equals(TradingStateEnum.YJS.getCode()) && "jzo2o.orders".equals(v.getProductAppId())).collect(Collectors.toList());
if (CollUtil.isEmpty(msgList)) {
return;
}
//修改订单状态
msgList.forEach(m -> ordersCreateService.paySuccess(m));
}
}
家政订单退款功能
用户下单成功可以取消订单,在订单的不同状态下去取消订单其执行的逻辑是不同的:
待支付状态下取消订单:
更改订单的状态为已取消。
订单已支付,状态为待派单时取消订单:
更改订单的状态为已关闭。
请求支付服务自动退款。
取消派单中的订单存在如下问题:
远程调用退款接口操作不放在事务方法中,避免影响数据库性能。
如果远程调用退款接口失败了将无法退款,这个怎么处理?
以上问题采用异步退款的方式来解决:
如下图:
取消订单执行如下操作:
1、使用数据库事务控制,保存以下数据
更新订单状态。
保存取消订单记录表,记录取消的原因等信息。
保存退款记录表。
2、事务提交后先启动一个线程请求支付服务的退款接口(为了及时退款)
3、定时任务扫描退款记录表,对未退款的记录请求支付服务进行退款,退款成功更新订单的退款状态,并删除退款记录。
说明:
第2步的作用为了第一时间申请退款,因为定时任务会有一定的延迟。
第3步的作用是由定时任务去更新退款的状态,因为调用了退款接口只是申请退款了,退款结果可能还没有拿到,通过定时任务再次请求支付服务的退款接口,拿到退款结果。
订单取消记录表:
create table `jzo2o-orders`.orders_canceled
(
id bigint not null comment '订单id'
constraint `PRIMARY`
primary key,
canceller_id bigint null comment '取消人',
canceler_name varchar(50) null comment '取消人名称',
canceller_type int null comment '取消人类型,1:普通用户,4:运营人员',
cancel_reason varchar(50) null comment '取消原因',
cancel_time datetime null comment '取消时间',
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间'
)
comment '订单取消表' charset = utf8mb4;
订单退款记录表:存储 了待退款的记录
create table `jzo2o-orders`.orders_refund
(
id bigint not null comment '订单id'
constraint `PRIMARY`
primary key,
trading_order_no bigint null comment '支付服务交易单号',
real_pay_amount decimal(10, 2) null comment '实付金额',
create_time datetime default CURRENT_TIMESTAMP null comment '创建时间'
)
comment '订单退款表' charset = utf8mb4;
取消未支付订单实现
根据需求,取消订单需要实现两部分功能:
针对未支付订单的取消操作:
修改订单的状态为已取消。
保存取消订单的记录。
@Override
public void cancel(OrderCancelDTO orderCancelDTO) {
//查询订单信息
Orders orders = getById(orderCancelDTO.getId());
if (ObjectUtil.isNull(orders)) {
throw new DbRuntimeException("找不到要取消的订单,订单号:{}",orderCancelDTO.getId());
}
//订单状态
Integer ordersStatus = orders.getOrdersStatus();
//根据订单状态执行取消逻辑
if(OrderStatusEnum.NO_PAY.getStatus()==ordersStatus){ //订单状态为待支付
}else if(OrderStatusEnum.DISPATCHING.getStatus()==ordersStatus){ //订单状态为派单中
}else{
throw new CommonException("当前订单状态不支持取消");
}
}
定义取消未支付订单service
@Transactional(rollbackFor = Exception.class)
public void cancelByNoPay(OrderCancelDTO orderCancelDTO) {
//保存取消订单记录
OrdersCanceled ordersCanceled = BeanUtil.toBean(orderCancelDTO, OrdersCanceled.class);
ordersCanceled.setCancellerId(orderCancelDTO.getCurrentUserId());
ordersCanceled.setCancelerName(orderCancelDTO.getCurrentUserName());
ordersCanceled.setCancellerType(orderCancelDTO.getCurrentUserType());
ordersCanceled.setCancelTime(LocalDateTime.now());
ordersCanceledService.save(ordersCanceled);
//更新订单状态为取消订单
OrderUpdateStatusDTO orderUpdateStatusDTO = OrderUpdateStatusDTO.builder()
.id(orderCancelDTO.getId())
.originStatus(OrderStatusEnum.NO_PAY.getStatus())
.targetStatus(OrderStatusEnum.CANCELED.getStatus())
.build();
int result = ordersCommonService.updateStatus(orderUpdateStatusDTO);
if (result <= 0) {
throw new DbRuntimeException("订单取消事件处理失败");
}
}
完善取消订单的service
自动取消未支付订单(1)
取消未支付订单除了人工操作外还可以自动取消,下单超过15分钟未支付则自动取消订单。这个需求在很多业务场景都有用到,比如:购买火车票、购买电影票在一定时间内未支付将自动取消。
如何实现自动取消订单操作呢?
这里最关键有一个定时的要求,从下单开始计算15分钟后取消订单。
如果要分秒不差执行取消订单操作就需要使用定时器每秒去判断是否到达过期时间。
如果订单较多使用多个定时器会耗费CPU。
其实,我们可以不用分秒不差执行取消操作,从用户的角度去思考,通常是用户点击订单查看的时候发现不能支付,因为到达支付过期时间。
所以,我们可以通过定时任务和懒加载方式。
定时任务方式:每分钟将支付过期的订单查询出来进行取消操作。
懒加载方式:当用户查看订单详情时判断如果订单未支付且支付超时,此时触发订单取消操作。
查询未支付的订单
这里要注意这个last 对于 limit 要注意加上一个空格
/**
* 查询超时订单id列表
*
* @param count 数量
* @return 订单id列表
*/
@Override
public List<Orders> queryOverTimePayOrdersListByCount(Integer count) {
//根据订单创建时间查询超过15分钟未支付的订单
List<Orders> list = lambdaQuery()
//查询待支付状态的订单
.eq(Orders::getOrdersStatus, OrderStatusEnum.NO_PAY.getStatus())
//小于当前时间减去15分钟,即待支付状态已过15分钟
.lt(Orders::getCreateTime, LocalDateTime.now().minusMinutes(15))
.last("limit " + count)
.list();
return list;
}
xxl-job 定时任务扫描
@Component
public class OrdersHandler {
/**
* 支付超时取消订单
* 每分钟执行一次
*/
@XxlJob(value = "cancelOverTimePayOrder")
public void cancelOverTimePayOrder() {
//查询支付超时状态订单
List<Orders> ordersList = ordersCreateService.queryOverTimePayOrdersListByCount(100);
if (CollUtil.isEmpty(ordersList)) {
XxlJobHelper.log("查询超时订单列表为空!");
return;
}
for (Orders orders : ordersList) {
//取消订单
OrderCancelDTO orderCancelDTO = BeanUtil.toBean(order, OrderCancelDTO.class);
orderCancelDTO.setCurrentUserType(UserType.SYSTEM);
orderCancelDTO.setCancelReason("订单超时支付,自动取消");
ordersManagerService.cancel(orderCancelDTO);
}
}
自动取消未支付订单(2)
懒加载方式取消订单
就是用户点击订单的时候,查询一下是否超时支付,然后返回
/**
* 根据订单id查询
*
* @param id 订单id
* @return 订单详情
*/
@Override
public OrderResDTO getDetail(Long id) {
//查询订单
Orders orders = queryById(id);
//如果支付过期则取消订单
orders = canalIfPayOvertime(orders);
OrderResDTO orderResDTO = BeanUtil.toBean(orders, OrderResDTO.class);
return orderResDTO;
}
/**
* 如果支付过期则取消订单
* @param orders
*/
private Orders canalIfPayOvertime(Orders orders){
//创建订单未支付15分钟后自动取消
if(orders.getOrdersStatus()==OrderStatusEnum.NO_PAY.getStatus() && orders.getCreateTime().plusMinutes(15).isBefore(LocalDateTime.now())){
//查询支付结果,如果支付最新状态仍是未支付进行取消订单
OrdersPayResDTO ordersPayResDTO = ordersCreateService.getPayResultFromTradServer(orders.getId());
int payResultFromTradServer = ordersPayResDTO.getPayStatus();
if(payResultFromTradServer != OrderPayStatusEnum.PAY_SUCCESS.getStatus()){
//取消订单
OrderCancelDTO orderCancelDTO = BeanUtil.toBean(orders, OrderCancelDTO.class);
orderCancelDTO.setCurrentUserType(UserType.SYSTEM);
orderCancelDTO.setCancelReason("订单超时支付,自动取消");
cancel(orderCancelDTO);
orders = getById(orders.getId());
}
}
return orders;
}
取消派单中订单实现
当订单状态为派单中,取消此类订单需要进行退款操作,根据退款流程,需要作以下操作:
-
添加取消订单记录。
-
更新订单状态为“已关闭”。
-
添加退款记录。
public class OrdersManagerServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements IOrdersManagerService {
@Resource
private IOrdersRefundService ordersRefundService;
//派单中状态取消订单
@Transactional(rollbackFor = Exception.class)
public void cancelByDispatching(OrderCancelDTO orderCancelDTO) {
//保存取消订单记录
OrdersCanceled ordersCanceled = BeanUtil.toBean(orderCancelDTO, OrdersCanceled.class);
ordersCanceled.setCancellerId(orderCancelDTO.getCurrentUserId());
ordersCanceled.setCancelerName(orderCancelDTO.getCurrentUserName());
ordersCanceled.setCancellerType(orderCancelDTO.getCurrentUserType());
ordersCanceled.setCancelTime(LocalDateTime.now());
ordersCanceledService.save(ordersCanceled);
//更新订单状态为关闭订单
OrderUpdateStatusDTO orderUpdateStatusDTO = OrderUpdateStatusDTO.builder().id(orderCancelDTO.getId())
.originStatus(OrderStatusEnum.DISPATCHING.getStatus())
.targetStatus(OrderStatusEnum.CLOSED.getStatus())
.refundStatus(OrderRefundStatusEnum.REFUNDING.getStatus())//退款状态为退款中
.build();
int result = ordersCommonService.updateStatus(orderUpdateStatusDTO);
if (result <= 0) {
throw new DbRuntimeException("待服务订单关闭事件处理失败");
}
//添加退款记录
OrdersRefund ordersRefund = new OrdersRefund();
ordersRefund.setId(orderCancelDTO.getId());
ordersRefund.setTradingOrderNo(orderCancelDTO.getTradingOrderNo());
ordersRefund.setRealPayAmount(orderCancelDTO.getRealPayAmount());
ordersRefundService.save(ordersRefund);
}
...
定时任务请求退款接口
取消派单中的订单进行自动退款,这里通过定时任务请求退款接口:
在OrdersHandler 类中定义定时任务方法。
定时任务根据退款记录去请求第三方支付服务的退款接口,根据退款结果进行处理,如果退款成功将更新订单的退款状态、删除退款记录。
(因为调用退款接口可能抛出异常,我们要把他捕获,防止其影响他处理其他方法。调用退款接口时,因为是异步的,可能已经退完款了,但是上面还没更新,所以定时任务保底更新订单信息)
package com.jzo2o.orders.manager.handler;
@Slf4j
@Component
public class OrdersHandler {
@Resource
private RefundRecordApi refundRecordApi;
//解决同级方法调用,事务失效问题
@Resource
private OrdersHandler ordersHandler;
@Resource
private IOrdersRefundService ordersRefundService;
@Resource
private OrdersMapper ordersMapper;
/**
* 订单退款异步任务
*/
@XxlJob(value = "handleRefundOrders")
public void handleRefundOrders() {
//查询退款中订单
List<OrdersRefund> ordersRefundList = ordersRefundService.queryRefundOrderListByCount(100);
for (OrdersRefund ordersRefund : ordersRefundList) {
//请求退款
requestRefundOrder(ordersRefund);
}
}
/**
* 请求退款
* @param ordersRefund 退款记录
*/
public void requestRefundOrder(OrdersRefund ordersRefund){
//调用第三方进行退款
ExecutionResultResDTO executionResultResDTO = null;
try {
executionResultResDTO = refundRecordApi.refundTrading(ordersRefund.getTradingOrderNo(), ordersRefund.getRealPayAmount());
} catch (Exception e) {
e.printStackTrace();
}
if(executionResultResDTO!=null){
//退款后处理订单相关信息
ordersHandler.refundOrder(ordersRefund, executionResultResDTO);
}
}
/**
* 更新退款状态
* @param ordersRefund
* @param executionResultResDTO
*/
@Transactional(rollbackFor = Exception.class)
public void refundOrder(OrdersRefund ordersRefund, ExecutionResultResDTO executionResultResDTO) {
//根据响应结果更新退款状态
int refundStatus = OrderRefundStatusEnum.REFUNDING.getStatus();//退款中
if (ObjectUtil.equal(RefundStatusEnum.SUCCESS.getCode(), executionResultResDTO.getRefundStatus())) {
//退款成功
refundStatus = OrderRefundStatusEnum.REFUND_SUCCESS.getStatus();
} else if (ObjectUtil.equal(RefundStatusEnum.FAIL.getCode(), executionResultResDTO.getRefundStatus())) {
//退款失败
refundStatus = OrderRefundStatusEnum.REFUND_FAIL.getStatus();
}
//如果是退款中状态,程序结束
if (ObjectUtil.equal(refundStatus, OrderRefundStatusEnum.REFUNDING.getStatus())) {
return;
}
//非退款中状态,更新订单的退款状态
LambdaUpdateWrapper<Orders> updateWrapper = new LambdaUpdateWrapper<Orders>()
.eq(Orders::getId, ordersRefund.getId())
.ne(Orders::getRefundStatus, refundStatus)
.set(Orders::getRefundStatus, refundStatus)
.set(ObjectUtil.isNotEmpty(executionResultResDTO.getRefundId()), Orders::getRefundId, executionResultResDTO.getRefundId())
.set(ObjectUtil.isNotEmpty(executionResultResDTO.getRefundNo()), Orders::getRefundNo, executionResultResDTO.getRefundNo());
ordersMapper.update(null,updateWrapper);
//非退款中状态,删除申请退款记录,删除后定时任务不再扫描
if(update>0){
//非退款中状态,删除申请退款记录,删除后定时任务不再扫描
ordersRefundService.removeById(ordersRefund.getId());
}
}
...
及时退款实现
为了及时进行退款,在订单取消后启动一个新线程请求支付服务的退款接口
@Slf4j
@Component
public class OrdersHandler {
@Resource
private RefundRecordApi refundRecordApi;
//解决同级方法调用,事务失效问题
@Resource
private OrdersHandler orderHandler;
@Resource
private IOrdersRefundService ordersRefundService;
@Resource
private OrdersMapper ordersMapper;
/**
* 新启动一个线程请求退款
* @param ordersRefundId
*/
public void requestRefundNewThread(Long ordersRefundId){
//启动一个线程请求第三方退款接口
new Thread(()->{
//查询退款记录
OrdersRefund ordersRefund = ordersRefundService.getById(ordersRefundId);
if(ObjectUtil.isNotNull(ordersRefund)){
//请求退款
requestRefundOrder(ordersRefund);
}
}).start();
}
}
完善取消订单service