背景
我们公司是做海外视频App服务的,所以会员是我们业务线最重要的收入之一了。 最近接入新的h5三方支付,特来总结一下,总体来说三方支付接入不难。我们之前已经接入了原生的 Google pay、Apple Pay、h5三方支付接入了Paypal、payermax。
那既然有了原生的 Google Pay和Apple pay,为啥还需要h5支付呢?
其实是为了包下架之后,原生支付不能使用了,h5支付作为备用支付方式工作。
paypal 我们没有接入订阅功能,所以我简单说一下 payermax和新接入的pingpong支付的区别,其实最大的区别就在于订阅周期的维护。
payermax 在首期签约之后,后续扣款周期以及流程都是由payermax严格控制的,每期扣款结果将以回调商户接口的方式通知商户扣款情况,商户根据扣款结果做对应处理。
pingpong 在首期签约成功之后,是不维护扣款周期的,也不会主动发起扣款,整个续订过程都由商户控制(简单来说就是商户想什么时候扣款就什么时候扣款,当然前提是用户首期签约扣款成功)。
需求分析
-
采用Hosted托管付款页面模式,即直接跳转pingpong信用卡支付页面(跳转pingpong收银台,收银台pingpong支持简单定制);
-
用户首次订阅,订阅号中,需要重点维护用户ID,首月优惠价格,次月价格,并固化价格(当修改价格后,之前订阅的用户不受影响),订阅周期(首次为0,下一期记为1),订阅状态;
特殊情况 :
用户第一次订阅后,订阅状态为"订阅成功",1期续订扣款失败,订阅状态为"订阅成功",备注"1轮扣款失败" 在这种情况下,用户手动再次订阅,原订阅状态变更为"订阅取消(重订阅)",然后创建新的订阅;
-
用户到期时间前24小时发起扣款, 当扣款成功以后,生成对应订单,订阅期数+1;
-
若扣款失败,则在失败后3,6,12小时再次发起扣款申请,直到成功;
-
若在到期前扣款一直没有成功,则不再尝试扣款,订阅号状态标记为"本期扣款失败"(但是订阅仍成功),下一个月继续扣款,若连续3期扣款失败,则后续不再发起扣款申请(订阅中止);
-
若扣款失败后,用户自己发起同一个订阅,则创建新的订阅号,之前的订阅号状态修改为"重订阅取消";
-
允许用户存在多个订阅计划的行为,但不允许同一个订阅计划重复订阅,即已经订阅了连续包月,则不允许再次订阅连续包月,若已经订阅了连续包月,点击支付时,toast提示"您已订阅此计划,请勿重复订阅";
pingpong 支付订阅流程
- Pingpong checkout 交互流程
一次性支付流程很简单:
-
用户发起购买 -> 商户平台创建订单 -> 提交pingpong后台支付申请 -> 获取pingpong收银台地址 -> 商户h5打开pingpong收银台,后续操作都在pingpong系统上了;
-
商户接受pingpong服务器通知,根据通知扣款结果做后续处理;
所以,这里我们只需要实现两个接口即可,一个是下单接口,二是接收回调接口;
-
订阅流程 从流程图中可以看出,订阅的首期签约扣款和一次性购买流程基本一致,只是在向pingpong提交订单申请时增加点字段而已;
计划扣款是重点,不过也是向pingpong提交订单申请,值得注意的是,pingpong的签约号(一个订阅周期的唯一标识)也是由商户这边自己维护的,签约号是首期扣款商户提交的订单号(merchantTransactionId),后续的每一起扣款需要把这个订单号带上,字段是 primaryMerchantTransactionId。
pingpong订阅流程
我们这边的订阅设计方案是简单的定时任务触发,整个设计方案中会用到延迟队列,延迟队列我在之前博文中介绍过: 基于 Redisson 和 Kafka 的延迟队列设计方案、 为了方便开发,我打算实现一个Redis 工具集。
而支付订单相关的表设计,在之前介绍接入 Google pay 时给出过: 海外支付(GooglePay)服务端设计方案、 Google 支付订阅商品服务端设计方案、 Google 支付订阅补坑之路。
表设计
- 订单表
mysql
CREATE TABLE `vip_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_sn` varchar(100) NOT NULL COMMENT '订单编号',
`third_order_sn` varchar(100) DEFAULT NULL COMMENT '第三方订单编号',
`origin_order_sn` varchar(100) DEFAULT NULL COMMENT '源订单号,发起退款时关联的订单',
`type` tinyint(1) DEFAULT NULL COMMENT '订单类型 0 一次性购买 1 周期扣款订单',
`user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
`goods_id` bigint(20) DEFAULT NULL COMMENT '商品id',
`price` bigint(20) DEFAULT '0' COMMENT '订单金额',
`distribution_channel` varchar(20) DEFAULT NULL COMMENT '分销渠道',
`payment_method` varchar(50) DEFAULT NULL COMMENT '付款方式:0 Google pay ',
`status` tinyint(1) DEFAULT NULL COMMENT '订单状态:0:初始化 1:已完成 2 已经取消 3 已退款',
`deliver_status` tinyint(1) DEFAULT NULL COMMENT '发货状态:0:发货失败 1:发货成功',
`complete_time` datetime DEFAULT NULL COMMENT '订单完成时间',
`refund_time` datetime DEFAULT NULL COMMENT '退款时间',
`pay_time` datetime DEFAULT NULL COMMENT '付款时间',
`device_id` varchar(255) DEFAULT NULL COMMENT '设备ID',
`ip` varchar(100) DEFAULT NULL COMMENT 'IP地址',
`country` varchar(20) DEFAULT NULL COMMENT '国家',
`origin_info` text COMMENT '第三方支付信息',
`remark` varchar(200) DEFAULT NULL COMMENT '备注',
`support_refund` tinyint(1) DEFAULT NULL COMMENT '是否支持退款',
`refund_reason` varchar(200) DEFAULT NULL COMMENT '退款原因',
`goods_info` text COMMENT '商品信息',
`package_name` varchar(500) DEFAULT NULL COMMENT '包名',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`abnormal_reason` varchar(500) DEFAULT NULL COMMENT '订单异常类型原因',
`abnormal_status` tinyint(1) DEFAULT NULL COMMENT '异常处理状态 0 未处理 1 处理中 2 已处理',
`abnormal` varchar(50) DEFAULT '0' COMMENT '订单异常类型,0 正常订; 1 价格异常; 2. 多笔订单; 3. 已支付未发货; 4. 发放天数异常; 5. 扣款周期异常',
`local_currency` varchar(25) DEFAULT NULL COMMENT '本地价格单位',
`local_price` varchar(255) DEFAULT NULL COMMENT '本地价格',
PRIMARY KEY (`id`),
KEY `index_third_order_sn` (`third_order_sn`) USING BTREE,
KEY `index_user_id` (`user_id`) USING BTREE,
KEY `index_order_sn` (`order_sn`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='会员订单表';
- 订阅表
mysql
CREATE TABLE `vip_renewal_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_sn` varchar(100) NOT NULL COMMENT '订单编号',
`latest_order_sn` varchar(100) NOT NULL COMMENT '上次订单号',
`subscription_index` int(11) DEFAULT NULL COMMENT '扣款期数',
`sign_no` varchar(255) DEFAULT NULL COMMENT '订阅号',
`user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
`goods_id` bigint(20) DEFAULT NULL COMMENT '商品id',
`goods_info` varchar(1000) DEFAULT NULL COMMENT '固化商品信息',
`first_period_price` int(11) DEFAULT NULL COMMENT '首期价格',
`price` int(11) DEFAULT '0' COMMENT '订单金额',
`sign_channel` varchar(20) DEFAULT NULL COMMENT '签约渠道(支付渠道):google pay 、 paypal',
`sign_status` tinyint(1) DEFAULT NULL COMMENT '订阅状态,0-订阅处理中 1-订阅成功 2-订阅失败 3-订阅取消 4-订阅失效',
`sign_reason` varchar(1000) DEFAULT NULL COMMENT '状态原因',
`deduct_status` tinyint(1) DEFAULT NULL COMMENT '本期扣款状态',
`renewal_content` text COMMENT '第三方续订内容',
`start_order_time` datetime DEFAULT NULL COMMENT '订单第一次订阅时间',
`latest_order_time` datetime DEFAULT NULL COMMENT '上一次订单自动续约时间',
`next_order_time` datetime DEFAULT NULL COMMENT '理论上下次扣款时间',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`linked_purchase_token` varchar(1000) DEFAULT NULL COMMENT '解决google重复订阅的关联token',
`purchase_token` varchar(1000) DEFAULT NULL COMMENT '本次token',
`user_info` varchar(500) DEFAULT NULL COMMENT '固化用户信息',
PRIMARY KEY (`id`),
KEY `index_order_sn` (`order_sn`) USING BTREE,
KEY `index_latest_order_sn` (`latest_order_sn`) USING BTREE,
KEY `index_user_id` (`user_id`) USING BTREE,
KEY `index_purchase_token` (`purchase_token`(191)) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='会员自动续订表';
- pingpong订阅日志表
mysql
CREATE TABLE `pingpong_vip_renewal_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`batch_no` varchar(100) NOT NULL COMMENT '批处理ID',
`subscription_index` int(11) DEFAULT NULL COMMENT '扣款期数',
`sign_no` varchar(255) NOT NULL COMMENT '订阅号',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`goods_id` bigint(20) NOT NULL COMMENT '商品id',
`price` int(11) DEFAULT '0' COMMENT '订单金额',
`status_reason` varchar(500) DEFAULT NULL COMMENT '状态原因',
`deduct_status` tinyint(1) DEFAULT NULL COMMENT '本期扣款状态',
`retry_nums` int(11) DEFAULT NULL COMMENT '本期扣款重试第几次',
`request_content` text COMMENT '请求内容',
`renewal_content` text COMMENT '第三方续订内容',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`order_sn` varchar(255) NOT NULL DEFAULT '' COMMENT '订单号',
PRIMARY KEY (`id`) USING BTREE,
KEY `index_user_id` (`user_id`) USING BTREE,
KEY `index_sign_no` (`sign_no`(191)) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='pingpong会员订阅记录';
重点代码
创建订单
java
@ApiOperation(value = "创建 pingpong vip 订单")
@PostMapping("/pingpong/prePay")
@WriteLog
public ResultResponse<PingPongVipOrderVO> createVipOrderByPingPong(@RequestBody @Valid VipOrderRequest request) throws UserClientException {
if(!PayMethodEnum.PING_PONG_CARD_H5.getCode().equalsIgnoreCase(request.getPayMethod())){
throw new UserClientException("pay method not support");
}
Long userId = httpServletRequestHelper.getCurrentUserId();
String lockKey = String.format(Constant.PING_PONG_CREATE_ORDER_LOCK, userId);
RLock lock = redissonClient.getLock(lockKey);
try {
if(lock.isLocked() || !lock.tryLock()){
log.info("用户:{} 创建 pingpong 订单,未获取到锁,可能存在重复提交行为", userId);
return ResultResponse.success();
}
return ResultResponse.success(pingPongVipService.createVipOrder(request));
} finally {
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
java
/**
* 创建会员订单
* @param request
* @return
* @throws UserClientException
*/
@Override
public PingPongVipOrderVO createVipOrder(VipOrderRequest request) throws UserClientException {
String countryIsoCode = httpServletRequestHelper.getCountryIsoCode();
String language = httpServletRequestHelper.getLanguage();
Long currentUserId = httpServletRequestHelper.getCurrentUserId();
String deviceId = httpServletRequestHelper.getDeviceId();
vipOrderService.auditModeCheckSupportBuy(countryIsoCode, currentUserId, deviceId);
H5MarketVipGoodsVo marketVipGoodsVo = commodityFeign.findMarketGoodsH5(request.getMarketId()).getData();
if(marketVipGoodsVo == null){
log.error("上架市场:{},国家码:{} 未能找到上架商品", request.getMarketId(), countryIsoCode);
throw new UserClientException(Constant.VipOrderExceptionConstant.GOODS_NOT_FOUND);
}
log.info("通过marketId:{}, countryIsoCode:{} 获取商品:{}", request.getMarketId(), countryIsoCode, JSON.toJSONString(marketVipGoodsVo));
H5VipGoodsPriceDTO goodsPrice = vipOrderService.checkPriceAndGet(request, currentUserId, deviceId, marketVipGoodsVo);
PingPongVipOrderVO pingPongVipOrderVO;
if(marketVipGoodsVo.isSubscribed()){
// 订阅商品逻辑
pingPongVipOrderVO = preCreateSubscribedOrder(request, countryIsoCode, currentUserId, marketVipGoodsVo, goodsPrice);
} else {
// 一次性购买商品逻辑
// 创建本地订单
pingPongVipOrderVO = preCreateOneTimePurchaseOrder(request, countryIsoCode, language, marketVipGoodsVo, goodsPrice);
}
return pingPongVipOrderVO;
}
java
/**
* 预创建订阅订单
* @param request
* @param countryIsoCode
* @param currentUserId
* @param marketVipGoodsVo
* @param goodsPrice
* @return
* @throws UserClientException
*/
private PingPongVipOrderVO preCreateSubscribedOrder(VipOrderRequest request, String countryIsoCode, Long currentUserId,
H5MarketVipGoodsVo marketVipGoodsVo, H5VipGoodsPriceDTO goodsPrice) throws UserClientException {
// 检查用户是否重复订阅
checkUserRepeatRenewal(request, currentUserId);
// 预创建订单
String orderSn = orderSnGenerateHelper.generateVipOrderSn(PayMethodEnum.PING_PONG_CARD_H5);
String language = httpServletRequestHelper.getLanguage();
// pingpong 发起订阅请求
BigDecimal decimal = new BigDecimal(String.valueOf(goodsPrice.getLocalPrice())).divide(new BigDecimal("100"), 2, RoundingMode.DOWN);
PingPongPreOrderDto orderDto = PingPongPreOrderDto.builder()
.sku(String.valueOf(marketVipGoodsVo.getGoodsId()))
.currency(goodsPrice.getCurrency())
.amount(decimal.toPlainString())
.goodsName(marketVipGoodsVo.getGoodsNameDefaultEn(language))
.merchantTransactionId(orderSn)
.shopperIP(IPUtil.getRemoteIp())
.tradeCountry(countryIsoCode)
.language(language)
.subscribed(marketVipGoodsVo.isSubscribed())
.build();
PingPongCommonRequest prePayRequest = PingPongCommonRequest.buildPay(pingPongProperties, orderDto);
PingPongResultVo pingPongResultVo = pingPongServiceFeign.prePay(prePayRequest);
log.info("发起pingpong支付申请, param:{}, result:{}", JSON.toJSONString(prePayRequest), JSON.toJSONString(pingPongResultVo));
String renewalContent = JSON.toJSONString(pingPongResultVo);
if(!pingPongResultVo.resultIsSuccess()){
// 下单失败
log.info("pingpong 发起订阅请求失败, orderSn:{}", orderSn);
throw new ThirdPartyServiceCallException("pingpong 发起订阅请求失败");
}
VipOrder vipOrder = vipOrderService.createFirstVipOrderByPingPong(request, marketVipGoodsVo, VipOrderTypeEnum.SUBSCRIPTION,
orderSn, goodsPrice, pingPongResultVo.parserPayBizContent().getTransactionId(), renewalContent);
String subscriptionNo = pingPongResultVo.parserPayBizContent().getMerchantTransactionId();
// 是否有初始化的订阅 如果有就复用订阅
VipRenewalOrder waitingRenewalOrder = renewalOrderRepository.findByUserAndGoodsAndSignChannelAndSignStatus(currentUserId, request.getGoodsId(), RenewalSignChannelEnum.PING_PONG.getCode(), RenewalSignStatusEnum.WAITING.getCode());
if(waitingRenewalOrder != null){
// 复用订阅
vipOrderService.reuseWaitingRenewalOrder(waitingRenewalOrder, vipOrder, renewalContent, marketVipGoodsVo.getRenewalPrice(), subscriptionNo);
} else {
// 创建订阅
waitingRenewalOrder = vipOrderService.createNewSubscribeOrder(vipOrder, subscriptionNo,
orderSnGenerateHelper.generateVipRenewalOrderSn(PayMethodEnum.PING_PONG_CARD_H5), RenewalSignStatusEnum.WAITING,
RenewalSignChannelEnum.PING_PONG, renewalContent, marketVipGoodsVo.getRenewalPrice(), DeductStatusEnum.WAIT_DEDUCTED);
}
// 第一次订阅Log
PingpongVipRenewalLog renewalLog = PingpongVipRenewalLog.create(Constant.PingPongPay.generateBatchId(new Date()), waitingRenewalOrder);
renewalLog.setRequestContent(JSON.toJSONString(prePayRequest));
pingPongVipRenewalLogService.saveNewTransaction(renewalLog);
PingPongVipOrderVO pingPongVipOrderVO = new PingPongVipOrderVO(orderSn, pingPongResultVo.parserPayBizContent().getPaymentUrl(),
pingPongResultVo.parserPayBizContent().getTransactionId());
return pingPongVipOrderVO;
}
java
/**
* 检查用户是否重复订阅
* @param request
* @param currentUserId
* @throws UserClientException
*/
private void checkUserRepeatRenewal(VipOrderRequest request, Long currentUserId) throws UserClientException {
// 检查用户是否正常订阅了该商品
VipRenewalOrder successRenewalOrder = renewalOrderRepository.findByUserAndGoodsAndSignChannelAndSignStatus(currentUserId, request.getGoodsId(), RenewalSignChannelEnum.PING_PONG.getCode(), RenewalSignStatusEnum.SUCCESS.getCode());
if(successRenewalOrder != null){
// 如果本期扣款 成功 则提示 有成功的订阅 如果本期扣款失败,则取消原订阅,生成新订阅
if(RenewalSignStatusEnum.SUCCESS.getCode().equals(successRenewalOrder.getSignStatus())
&& DeductStatusEnum.repeatSubscription(successRenewalOrder.getDeductStatus())){
log.error("出现重复订阅,根据用户:{} 和商品:{} 找到签约成功的续订单", currentUserId, request.getGoodsId());
throw new UserClientException(Constant.VipOrderExceptionConstant.VIP_SUBSCRIBE_FAIL_REPEAT_SUBSCRIPTION);
}
// 取消原来订单
successRenewalOrder.setSignStatus(RenewalSignStatusEnum.CANCEL.getCode());
successRenewalOrder.setStatusReason("用户重订阅取消");
renewalOrderRepository.save(successRenewalOrder);
}
}
接收支付回调用
java
/**
* 交易 通知
* @param pingPongResultVo
* @param callback
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void pingPongPayCallback(PingPongResultVo pingPongResultVo, PingPongBizContentCallback callback) {
// 检查签名
checkSign(pingPongResultVo);
// 检查订单
VipOrder vipOrder = vipOrderService.checkPayOrder(callback.getMerchantTransactionId());
String renewalContent = JSON.toJSONString(pingPongResultVo);
if(StringUtils.isEmpty(vipOrder.getThirdOrderSn())){
// 补偿三方交易流水
log.info("订单:{} 三方交易流水null, 补偿三方交易流水:{}", vipOrder.getOrderSn(), callback.getTransactionId());
vipOrder.setThirdOrderSn(callback.getTransactionId());
vipOrderService.updateOrderData(vipOrder);
}
if(!callback.paySuccess()){
log.info("订单:{} 支付失败,三方回执:{}", callback.getMerchantTransactionId(), renewalContent);
//订单处理失败
vipOrderService.updateOrderFail(vipOrder.getId(), renewalContent);
// 如果是续订,不是第一期扣款的话 扣款失败需要重试
if(vipOrder.subscriptionOrder()){
vipRenewalOrderService.updatePingPongRenewalFail(callback.getMerchantTransactionId(), renewalContent);
}
return;
}
// 更新订单为 已支付
vipOrderService.updateOrderPaid(vipOrder.getId(), renewalContent);
if(vipOrder.subscriptionOrder()){
// 更新订阅成功数据
vipRenewalOrderService.updatePingPongRenewalSuccess(callback.getMerchantTransactionId(), vipOrder, renewalContent);
}
// 通知发放会员
boolean grantUserVipSuccess = vipOrderService.grantUserVip(vipOrder);
if(grantUserVipSuccess){
vipOrderService.updateOrderDeliverSuccess(vipOrder);
}
}
java
// 订阅失败处理
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void updatePingPongRenewalFail(String orderSn, String renewalContent) {
// 订阅订单
VipRenewalOrder vipRenewalOrder = renewalOrderRepository.findByLatestOrderSn(orderSn);
if(vipRenewalOrder == null){
return;
}
// 不是第一期 发送延迟消息 重试
if(vipRenewalOrder.getSubscriptionIndex() != 0){
// 发送延迟消息
// 更新订阅
vipRenewalOrder.setDeductStatus(DeductStatusEnum.DEDUCTION_RETRY.getCode());
vipRenewalOrder.setStatusReason("本期扣款失败,正在扣款重试中...");
// 更新订阅log
PingpongVipRenewalLog vipRenewalLog = pingpongVipRenewalLogRepository.findByOrderSnAndSignNo(orderSn, vipRenewalOrder.getSignNo());
if(vipRenewalLog != null){
vipRenewalLog.setDeductStatus(DeductStatusEnum.DEDUCTED_FAIL.getCode());
vipRenewalLog.setStatusReason("本期扣款失败");
vipRenewalLog.setRenewalContent(renewalContent);
pingpongVipRenewalLogRepository.save(vipRenewalLog);
}
// 发送延迟消息 重试
Integer retryNums = Optional.ofNullable(vipRenewalLog.getRetryNums()).orElse(0) + 1;
if(retryNums > pingPongProperties.getMaxRetryNums()){
log.info("ping pong 订阅:{} 达到最大重试次数, 更新订阅失败", vipRenewalOrder.getSignNo());
// 检查最近几期都失败
checkPingPongRecentlyFailed(vipRenewalOrder, vipRenewalLog.getSubscriptionIndex());
} else {
TimeUnit timeUnit = pingPongProperties.isOpenTest() ? TimeUnit.SECONDS : TimeUnit.HOURS;
delayQueueMessageProducer.sendMessage(MessageEvent.PING_PONG_RENEWAL_FAIL_RETRY_TOPIC, vipRenewalOrder.getSignNo(),
retryNums * pingPongProperties.getRetryIntervalRatio(), timeUnit);
}
} else {
// 首期就失败
vipRenewalOrder.setDeductStatus(DeductStatusEnum.DEDUCTED_FAIL.getCode());
vipRenewalOrder.setSignStatus(RenewalSignStatusEnum.FAIL.getCode());
vipRenewalOrder.setStatusReason("首期扣款失败,签约失败");
}
vipRenewalOrder.setRenewalContent(renewalContent);
renewalOrderRepository.save(vipRenewalOrder);
}
计划扣款
java
/** job 触发
*
* 订阅周期扣款 创建扣款日志 ,发送 扣款任务到 kafka
* @param date
*/
@Override
public void subscriptionDeduction(Date date) {
String batchId = Constant.PingPongPay.generateBatchId(date);
// 获取 到期时间前24小时 订阅状态是订阅中的 订阅
int page = 0;
Page<VipRenewalOrder> renewalOrderPage;
do {
PageRequest pageRequest = PageRequest.of(page, 1000, new Sort(Sort.Direction.DESC, "id"));
renewalOrderPage = renewalOrderRepository.findByTimePage(pingPongProperties.getBeforeExpirationMinutes(), pageRequest);
page ++;
// 写入续订日志表
List<VipRenewalOrder> content = renewalOrderPage.getContent();
if(!CollectionUtils.isEmpty(content)){
// 过滤状态不是 扣款处理中和扣款重试的订阅
List<PingpongVipRenewalLog> renewalLogs = content.stream()
.filter(item -> !DeductStatusEnum.DEDUCTION_RETRY.getCode().equals(item.getDeductStatus()) &&
!DeductStatusEnum.DEDUCTED_ING.getCode().equals(item.getDeductStatus()))
.map(item -> PingpongVipRenewalLog.create(batchId, item)).collect(Collectors.toList());
pingPongVipRenewalLogService.saveAllNewTransaction(renewalLogs);
renewalLogs.stream().forEach(item -> messageProducer.sendSimpleMessage(MessageEvent.PING_PONG_RENEWAL_LOG_TOPIC , item.getId()));
}
} while (renewalOrderPage.hasNext());
}
kafka消费需要扣款的续订任务
java
@KafkaListener(topics = { MessageEvent.PING_PONG_RENEWAL_LOG_TOPIC }, groupId = KAFKA_GROUP)
public void listenerPingPongPayPlanDeduction(String logId) throws UserClientException {
log.info("topic : {}, 监听pingpong 订阅任务 , logId : {} 开始处理订阅", MessageEvent.PING_PONG_RENEWAL_LOG_TOPIC, logId);
PingpongVipRenewalLog renewalLog = logRepository.findById(Long.valueOf(logId)).orElse(null);
if(renewalLog == null){
log.info("PingpongVipRenewalLog logId: {} 不存在", logId);
return;
}
RLock lock = null;
try {
// 锁住签约号,多个任务对同于一个签约处理,需要排队
lock = redisUtils.lock(String.format(Constant.PING_PONG_VIP_PAY_PLAN_DEDUCTION_LOCK, renewalLog.getSignNo()));
// 开始处理 计划扣款
pingPongVipService.prePlanDeduction(renewalLog);
} finally {
if (lock != null && lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
java
/**
* 开始处理计划扣款 后续扣款
* @param renewalLog
*/
@Override
public void prePlanDeduction(PingpongVipRenewalLog renewalLog) {
// 检查订阅日志是否已经处理
if(renewalLog.processed()){
log.info("PingpongVipRenewalLog:{} processed", renewalLog.getId());
return;
}
// 获取 订阅
VipRenewalOrder renewalOrder = renewalOrderRepository.findBySignNoAndSignChannel(renewalLog.getSignNo(), RenewalSignChannelEnum.PING_PONG.getCode());
// 检查订阅是否需要处理
if(!renewalOrder.needProcess()){
log.info("VipRenewalOrder :{} not need process", JSON.toJSONString(renewalOrder));
return;
}
log.info("----开始处理扣款---:{}", renewalLog.getId());
// 开始处理 扣款
this.doPlanDeduction(renewalLog, renewalOrder, false);
}
java
/**
* 处理扣款
* @param renewalLog
* @param renewalOrder
*/
private void doPlanDeduction(PingpongVipRenewalLog renewalLog, VipRenewalOrder renewalOrder, boolean retry) {
// 固化商品
H5MarketVipGoodsVo marketVipGoodsVo = JSON.parseObject(renewalOrder.getGoodsInfo(), H5MarketVipGoodsVo.class);
// 固化的用户
VipOrderUserInfoDTO userInfoDTO = JSON.parseObject(renewalOrder.getUserInfo(), VipOrderUserInfoDTO.class);
userInfoDTO.setUserId(renewalOrder.getUserId());
// 创建订单号
String orderSn = orderSnGenerateHelper.generateVipOrderSn(PayMethodEnum.PING_PONG_CARD_H5);
// 创建请求参数
BigDecimal decimal = new BigDecimal(String.valueOf(marketVipGoodsVo.getLocalRenewPrice())).divide(new BigDecimal("100"), 2, RoundingMode.DOWN);
PingPongPreOrderDto orderDto = PingPongPreOrderDto.builder()
.sku(String.valueOf(marketVipGoodsVo.getGoodsId()))
.currency(marketVipGoodsVo.getLocalUnit())
.amount(decimal.toString())
.goodsName(marketVipGoodsVo.getGoodsNameDefaultEn(userInfoDTO.getLanguage()))
.merchantTransactionId(orderSn)
.tradeCountry(userInfoDTO.getCountryIsoCode())
.language(userInfoDTO.getLanguage())
.primaryMerchantTransactionId(renewalOrder.getSignNo())
.subscribed(marketVipGoodsVo.isSubscribed())
.build();
PingPongCommonRequest prePayRequest = PingPongCommonRequest.buildUnifiedPay(pingPongProperties, orderDto);
// 修改订阅 , 订单状态本期处理中
int subscriptionIndex = Optional.ofNullable(renewalOrder.getSubscriptionIndex()).orElse(0);
if(!retry){
subscriptionIndex = subscriptionIndex + 1;
}
// 创建订单
VipOrder vipOrder = vipOrderService.createVipOrderAndUpdateOrderSnByPingPongRenew(userInfoDTO, marketVipGoodsVo, orderSn, renewalLog, renewalOrder, JSON.toJSONString(prePayRequest), subscriptionIndex);
PingPongResultVo pingPongResultVo = pingPongServiceFeign.unifiedPay(prePayRequest);
log.info("ping pong 发起计划扣款,请求:{},响应:{}", JSON.toJSONString(prePayRequest), JSON.toJSONString(pingPongResultVo));
// 交易号,返回值
vipOrder.setThirdOrderSn(pingPongResultVo.findPingPongTransactionId());
vipOrderService.updateOrderData(vipOrder);
}
kafka监听失败重试
java
@KafkaListener(topics = { MessageEvent.PING_PONG_RENEWAL_FAIL_RETRY_TOPIC }, groupId = KAFKA_GROUP)
public void listenerPingPongPayFailRetry(String renewalSignNo) throws UserClientException {
log.info("topic : {}, 监听pingpong 扣款失败 , renewalSignNo : {} 准备重试", MessageEvent.PING_PONG_RENEWAL_FAIL_RETRY_TOPIC, renewalSignNo);
RLock lock = null;
try {
// 锁住签约号,多个任务对同于一个签约处理,需要排队
lock = redisUtils.lock(String.format(Constant.PING_PONG_VIP_PAY_PLAN_DEDUCTION_LOCK, renewalSignNo));
// 开始处理 计划扣款重试
pingPongVipService.payFailRetry(renewalSignNo);
} finally {
if (lock != null && lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
scss
/**
* 扣款失败重试
* @param renewalSignNo
*/
@Override
public void payFailRetry(String renewalSignNo) {
VipRenewalOrder vipRenewalOrder = renewalOrderRepository.findBySignNoAndSignChannel(renewalSignNo, RenewalSignChannelEnum.PING_PONG.getCode());
// 检查订阅是否需要处理
if(!vipRenewalOrder.needProcess()){
log.info("VipRenewalOrder :{} not need process", JSON.toJSONString(vipRenewalOrder));
return;
}
// 获取上一次重试日志
PingpongVipRenewalLog prevRenewalLog = vipRenewalLogRepository.findByOrderSnAndSignNo(vipRenewalOrder.getLatestOrderSn(), vipRenewalOrder.getSignNo());
// 检查是否达到最大重试次数
Integer retryNums = Optional.ofNullable(prevRenewalLog.getRetryNums()).orElse(0) + 1;
if(retryNums > pingPongProperties.getMaxRetryNums()){
log.info("ping pong 订阅:{} 达到最大重试次数, 更新订阅失败", vipRenewalOrder.getSignNo());
// 检查最近几期都失败
vipRenewalOrderService.checkPingPongRecentlyFailed(vipRenewalOrder, prevRenewalLog.getSubscriptionIndex());
return;
}
// 开始重试
// 创建重试日志
PingpongVipRenewalLog renewalLog = PingpongVipRenewalLog.create(Constant.PingPongPay.generateBatchId(new Date()), vipRenewalOrder);
renewalLog.setRetryNums(retryNums);
pingPongVipRenewalLogService.saveNewTransaction(renewalLog);
// 开始处理重试
this.doPlanDeduction(renewalLog, vipRenewalOrder, true);
}