This commit is contained in:
dzc committed 2026-08-28 21:18:54 +08:00
1 parent fc98470e09
commit 4d84dc41b1
13 files changed
+258 -45

No files matched your search

@@ -17,7 +17,7 @@ public enum ThirdPaymentTaskTypeEnum {
REFUND_CREATE("REFUND_CREATE"),
/** 商户微信或支付宝默认平台分佣关系绑定。 */
FZ_RELATION_BIND("FZ_RELATION_BIND"),
/** 微信或支付宝分账创建/阶段推进。 */
/** 微信分账创建,或支付宝直付通确认结算/可选抽佣阶段记录创建。 */
FZ_CREATE("FZ_CREATE"),
/** 支付宝直付通确认结算;只推进确认结算阶段。 */
FZ_SETTLE_CONFIRM("FZ_SETTLE_CONFIRM"),
@@ -35,7 +35,7 @@ public class ThirdPayOrderVo extends ThirdPayOrderEntity {
private String fzStatus;
/**
* 支付宝确认结算阶段状态;微信为 NOT_APPLICABLE,未启用分账为 NOT_CONFIGURED。
* 支付宝确认结算阶段状态;支付宝无论是否开启平台抽佣都必须执行,其他未配置场景为 NOT_CONFIGURED。
*/
private String settleConfirmStatus;
@@ -218,7 +218,7 @@ public class ThirdAppPlusPayJob {
}
/**
* 创建支付成功后缺失的分账订单,并轮询微信分账、微信分账退款及支付宝直付通两阶段状态。
* 创建支付成功后缺失的直付通阶段记录,并轮询微信分账、微信分账退款及支付宝确认结算/可选抽佣状态。
*/
public void handleFzOrders() {
if (!pollingCoordinator.tryStartFzPolling()) {
@@ -231,6 +231,11 @@ public class ThirdAppPlusPayJob {
List<ThirdFzOrderEntity> fzOrders = thirdFzOrderService.list(new LambdaQueryWrapper<ThirdFzOrderEntity>()
.in(ThirdFzOrderEntity::getStatus, ThirdFzOrderStatusEnum.FZ_UNPAID.getType(),
ThirdFzOrderStatusEnum.FZ_REFUNDING.getType())
// 关闭平台抽佣的支付宝订单确认结算成功后已经完成全部资金处理,不能继续占用旧轮询批次。
.notExists("SELECT 1 FROM third_pay_order p WHERE p.tenant_id = third_fz_order.tenant_id"
+ " AND p.pay_no = third_fz_order.pay_no AND p.del_flag = '0'"
+ " AND p.pay_platform_type = '20' AND (p.enable_fz IS NULL OR p.enable_fz <> '1')"
+ " AND JSON_UNQUOTE(JSON_EXTRACT(COALESCE(third_fz_order.create_res, '{}'), '$.settleConfirm.status')) = 'SUCCESS'")
.orderByAsc(ThirdFzOrderEntity::getUpdateTime)
.last("limit " + PAYMENT_COMPENSATION_BATCH_SIZE));
hasPendingFz = ObjectUtil.isNotEmpty(fzOrders);
@@ -251,14 +256,14 @@ public class ThirdAppPlusPayJob {
}
/**
* 为支付成功满 30 秒且尚无分账记录的订单创建稳定分账请求。
* 为支付成功满 30 秒且尚无阶段记录的支付宝直付通订单或已开启分账的微信订单创建稳定请求。
*/
private void createMissingFzOrders() {
List<ThirdPayOrderEntity> orders = thirdPayOrderService.list(new LambdaQueryWrapper<ThirdPayOrderEntity>()
.in(ThirdPayOrderEntity::getPayPlatformType,
AppPlusPlatformTypeEnum.ALI_PAY.getType(), AppPlusPlatformTypeEnum.WECHAT.getType())
.eq(ThirdPayOrderEntity::getStatus, ThirdPayOrderStatusEnum.PAY_SUCCESS.getType())
.eq(ThirdPayOrderEntity::getEnableFz, YesNoEnum.YES.getCode())
.and(wrapper -> wrapper.eq(ThirdPayOrderEntity::getPayPlatformType, AppPlusPlatformTypeEnum.ALI_PAY.getType())
.or(wechat -> wechat.eq(ThirdPayOrderEntity::getPayPlatformType, AppPlusPlatformTypeEnum.WECHAT.getType())
.eq(ThirdPayOrderEntity::getEnableFz, YesNoEnum.YES.getCode())))
.le(ThirdPayOrderEntity::getPayTime, LocalDateTime.now().minusSeconds(FZ_MIN_DELAY_SECONDS))
.notExists("SELECT 1 FROM third_fz_order f WHERE f.pay_no = third_pay_order.pay_no AND f.del_flag = '0'")
.orderByAsc(ThirdPayOrderEntity::getUpdateTime)
@@ -277,7 +282,7 @@ public class ThirdAppPlusPayJob {
if (thirdFzOrderService.getByPayNo(latest.getPayNo()) == null) {
touchPayOrderAwaitingFzCreation(latest.getId());
}
}, "创建分账订单");
}, "创建直付通阶段记录");
}
}
@@ -392,7 +397,7 @@ public class ThirdAppPlusPayJob {
}
/**
* 分账记录因历史配置异常尚未创建时轮转支付订单,防止它持续占据缺失分账记录扫描首批。
* 直付通阶段记录因历史配置异常尚未创建时轮转支付订单,防止它持续占据缺失记录扫描首批。
*
* @param orderId 支付订单主键
*/
@@ -402,7 +407,8 @@ public class ThirdAppPlusPayJob {
thirdPayOrderService.update(update, new LambdaUpdateWrapper<ThirdPayOrderEntity>()
.eq(ThirdPayOrderEntity::getId, orderId)
.eq(ThirdPayOrderEntity::getStatus, ThirdPayOrderStatusEnum.PAY_SUCCESS.getType())
.eq(ThirdPayOrderEntity::getEnableFz, YesNoEnum.YES.getCode()));
.and(wrapper -> wrapper.eq(ThirdPayOrderEntity::getEnableFz, YesNoEnum.YES.getCode())
.or().eq(ThirdPayOrderEntity::getPayPlatformType, AppPlusPlatformTypeEnum.ALI_PAY.getType())));
}
/**
@@ -63,7 +63,7 @@ public interface ThirdAppPlusAliPayService {
AlipayTradeFastpayRefundQueryResponse queryRefund(ThirdPayOrderEntity payOrder);
/**
* 在支付成功达到直付通最早结算时间后,确认结算并执行平台抽佣;请求具有稳定幂等号。
* 在支付成功达到直付通最早结算时间后执行确认结算,并按订单开关决定是否继续平台抽佣;请求具有稳定幂等号。
*
* @param thirdPayOrder 已确认支付成功的支付订单
*/
@@ -71,7 +71,7 @@ public interface ThirdAppPlusAliPayService {
/**
* 只执行支付宝直付通确认结算阶段,任务成功不代表平台抽佣成功。
* @param fzOrder 分账订单
* @param fzOrder 直付通确认结算与可选抽佣阶段记录
* @param payOrder 已支付成功的支付订单
* @return SUCCESS、PROCESSING 或 FAILED 阶段状态
*/
@@ -79,7 +79,7 @@ public interface ThirdAppPlusAliPayService {
/**
* 只提交支付宝直付通平台抽佣,成功受理后返回 SUBMITTED,禁止提前写入最终分账成功。
* @param fzOrder 分账订单
* @param fzOrder 直付通确认结算与可选抽佣阶段记录
* @param payOrder 已支付成功的支付订单
* @return SUBMITTED、PROCESSING 或 FAILED 阶段状态
*/
@@ -87,23 +87,23 @@ public interface ThirdAppPlusAliPayService {
/**
* 查询支付宝平台抽佣最终状态;只有支付宝返回最终成功才推进 third_fz_order。
* @param fzOrder 分账订单
* @param fzOrder 直付通确认结算与可选抽佣阶段记录
* @param payOrder 已支付成功的支付订单
* @return SUCCESS、PROCESSING 或 FAILED 阶段状态
*/
String queryDirectPayRoyaltyStage(ThirdFzOrderEntity fzOrder, ThirdPayOrderEntity payOrder);
/**
* 只预留支付宝直付通分账记录,不发起任何支付宝结算或抽佣请求。
* 只预留支付宝直付通确认结算与可选抽佣阶段记录,不发起任何支付宝请求。
* @param payOrder 已确认支付成功的支付宝订单
* @return 已存在或新建的分账订单
* @return 已存在或新建的直付通阶段记录
*/
ThirdFzOrderEntity ensureDirectPayFzOrder(ThirdPayOrderEntity payOrder);
/**
* 查询或补偿支付宝直付通分账及分账退款状态。
* 查询或补偿支付宝直付通确认结算、可选平台抽佣及退分账状态。
*
* @param fzOrder 已持久化的分账订单
* @param fzOrder 已持久化的直付通阶段记录
* @param payOrder 对应支付订单
*/
void pollDirectPayLifecycle(ThirdFzOrderEntity fzOrder, ThirdPayOrderEntity payOrder);
@@ -118,9 +118,9 @@ public class ThirdPaymentTaskEnqueuer {
*
* @param tenantId 支付订单所属租户ID;为空时读取当前租户上下文
* @param payOrderId 原支付订单主键
* @param fzOrderId 直付通分账阶段记录主键
* @param fzOrderId 直付通确认结算与可选抽佣阶段记录主键
* @param payNo 原支付订单稳定支付单号
* @param fzNo 直付通稳定分账业务号
* @param fzNo 直付通稳定阶段业务号
* @param nextRunTime 最早确认结算时间;为空时立即可领取,仅供显式人工恢复使用
*/
public void enqueueFzSettleConfirm(Long tenantId, Long payOrderId, Long fzOrderId, String payNo, String fzNo,
@@ -752,7 +752,7 @@ public class ThirdAppPlusAliPayServiceImpl implements ThirdAppPlusAliPayService
}
/**
* 执行支付宝直付通的两阶段结算与平台抽佣。支付成功后不足 30 秒时只保留待处理状态,
* 执行支付宝直付通确认结算,并在订单开启平台抽佣时继续完成抽佣阶段。支付成功后不足 30 秒时只保留待处理状态,
* 由数据库轮询在服务重启后继续执行;所有请求号均按支付订单稳定生成。
*
* @param thirdPayOrder 已确认支付成功的支付订单
@@ -762,7 +762,6 @@ public class ThirdAppPlusAliPayServiceImpl implements ThirdAppPlusAliPayService
if (thirdFzOrderService == null || thirdPayOrder == null
|| ObjectUtil.notEqual(thirdPayOrder.getPayPlatformType(), AppPlusPlatformTypeEnum.ALI_PAY.getType())
|| ObjectUtil.notEqual(thirdPayOrder.getStatus(), ThirdPayOrderStatusEnum.PAY_SUCCESS.getType())
|| ObjectUtil.notEqual(thirdPayOrder.getEnableFz(), com.bsd.pjcloud.common.core.constant.enums.YesNoEnum.YES.getCode())
|| thirdPayOrder.getPayTime() == null || thirdPayOrder.getPayTime().plusSeconds(30).isAfter(LocalDateTime.now())) {
return;
}
@@ -773,20 +772,19 @@ public class ThirdAppPlusAliPayServiceImpl implements ThirdAppPlusAliPayService
}
/**
* 预留支付宝直付通分账事实;外部请求必须由后续独立任务执行。
* 预留支付宝直付通确认结算与可选平台抽佣事实;外部请求必须由后续独立任务执行。
*
* @param payOrder 已确认支付成功的支付宝订单
* @return 已存在或新建的分账订单;订单尚未满足分账条件时为空
* @return 已存在或新建的直付通阶段记录;订单尚未支付成功或不是支付宝订单时为空
*/
@Override
public ThirdFzOrderEntity ensureDirectPayFzOrder(ThirdPayOrderEntity payOrder) {
if (thirdFzOrderService == null || payOrder == null
|| ObjectUtil.notEqual(payOrder.getPayPlatformType(), AppPlusPlatformTypeEnum.ALI_PAY.getType())
|| ObjectUtil.notEqual(payOrder.getStatus(), ThirdPayOrderStatusEnum.PAY_SUCCESS.getType())
|| ObjectUtil.notEqual(payOrder.getEnableFz(), com.bsd.pjcloud.common.core.constant.enums.YesNoEnum.YES.getCode())) {
|| ObjectUtil.notEqual(payOrder.getStatus(), ThirdPayOrderStatusEnum.PAY_SUCCESS.getType())) {
return null;
}
// 历史成功订单可能因旧实现没有写 pay_time;这里只物化本地分账事实,不访问支付宝。
// 历史成功订单可能因旧实现没有写 pay_time;这里只物化本地直付通阶段事实,不访问支付宝。
// 最早确认结算时间由任务 Worker 以“发现时间 + 30 秒”兜底,不能在这里永久阻断恢复。
ThirdFzOrderEntity existing = thirdFzOrderService.getByPayNo(payOrder.getPayNo());
if (existing != null) {
@@ -796,15 +794,21 @@ public class ThirdAppPlusAliPayServiceImpl implements ThirdAppPlusAliPayService
if (paySetting == null) {
throw new IllegalStateException("支付宝商户支付配置快照不存在");
}
MchPaySettingEntity.AliPaySetting aliPaySetting = requireAliPaySetting(paySetting);
ThirdFzOrderEntity fzOrder = new ThirdFzOrderEntity();
fzOrder.setId(payOrder.getId());
fzOrder.setTenantId(payOrder.getTenantId());
fzOrder.setPayNo(payOrder.getPayNo());
fzOrder.setFzNo("fz_" + payOrder.getPayNo());
ThirdMchFzRelationEntity relation = mchFzRelationService == null ? null
: mchFzRelationService.getById(payOrder.getFzRelationId());
requireDirectPayRelation(relation, payOrder, requireAliPaySetting(paySetting));
fzOrder.setPrice(calculateFzAmount(payOrder.getPrice(), relation.getFzRatio()));
if (isProfitSharingEnabled(payOrder)) {
ThirdMchFzRelationEntity relation = mchFzRelationService == null ? null
: mchFzRelationService.getById(payOrder.getFzRelationId());
requireDirectPayRelation(relation, payOrder, aliPaySetting);
fzOrder.setPrice(calculateFzAmount(payOrder.getPrice(), relation.getFzRatio()));
} else {
// third_fz_order 复用为确认结算阶段载体;关闭平台抽佣时金额固定为 0,不能形成抽佣或统计金额。
fzOrder.setPrice(0);
}
fzOrder.setStatus(ThirdFzOrderStatusEnum.FZ_UNPAID.getType());
fzOrder.setFzTime(LocalDateTime.now());
fzOrder.setCreateRes(JSONUtil.createObj().set("provider", "ALIPAY").toString());
@@ -818,10 +822,10 @@ public class ThirdAppPlusAliPayServiceImpl implements ThirdAppPlusAliPayService
}
/**
* 补偿支付宝直付通结算、平台抽佣及已分账退款。支付宝此链路没有可替代本地幂等号的轮询结果,
* 补偿支付宝直付通确认结算、可选平台抽佣及已分账退款。支付宝此链路没有可替代本地幂等号的轮询结果,
* 因此对超时或系统异常重复提交相同请求,显式业务失败才进入失败状态。
*
* @param fzOrder 已持久化的分账订单
* @param fzOrder 已持久化的直付通阶段记录
* @param payOrder 对应的支付订单
*/
@Override
@@ -842,6 +846,10 @@ public class ThirdAppPlusAliPayServiceImpl implements ThirdAppPlusAliPayService
if (!"SUCCESS".equals(settleStatus)) {
return;
}
// 确认结算决定款项进入二级商户,所有直付通订单都必须执行;fzRelationId 只控制后续平台抽佣。
if (!isProfitSharingEnabled(payOrder)) {
return;
}
String submitStatus = submitDirectPayRoyaltyStage(fzOrder, payOrder);
if (!List.of("SUBMITTED", "PROCESSING", "SUCCESS").contains(submitStatus)) {
return;
@@ -1220,11 +1228,15 @@ public class ThirdAppPlusAliPayServiceImpl implements ThirdAppPlusAliPayService
settleInfo.setSettleDetailInfos(List.of(detail));
model.setSettleInfo(settleInfo);
SettleConfirmExtendParams extendParams = new SettleConfirmExtendParams();
extendParams.setRoyaltyFreeze("true");
// 仅平台抽佣订单冻结后续可分佣资金;关闭抽佣时必须释放全额结算,不能留下无后继任务的冻结款。
extendParams.setRoyaltyFreeze(isProfitSharingEnabled(payOrder) ? "true" : "false");
model.setExtendParams(extendParams);
AlipayTradeSettleConfirmRequest request = new AlipayTradeSettleConfirmRequest();
request.setBizModel(model);
putAppAuthToken(request::putOtherTextParam, aliPaySetting);
// 关闭平台抽佣时沿用直付通支付创建协议,只使用 smid 结算,不附带平台抽佣授权令牌。
if (isProfitSharingEnabled(payOrder)) {
putAppAuthToken(request::putOtherTextParam, aliPaySetting);
}
stage.set("outRequestNo", outRequestNo);
stage.set("request", JSONUtil.parseObj(JSONUtil.toJsonStr(model)));
try {
@@ -1334,6 +1346,17 @@ public class ThirdAppPlusAliPayServiceImpl implements ThirdAppPlusAliPayService
}
}
/**
* 判断支付订单是否开启平台抽佣;该标记只控制确认结算后的平台抽佣阶段,不控制直付通确认结算。
*
* @param payOrder 支付创建时保存的平台抽佣开关快照
* @return 开启平台抽佣时返回 {@code true}
*/
private boolean isProfitSharingEnabled(ThirdPayOrderEntity payOrder) {
return payOrder != null && ObjectUtil.equals(payOrder.getEnableFz(),
com.bsd.pjcloud.common.core.constant.enums.YesNoEnum.YES.getCode());
}
/**
* 校验支付宝支付订单固定的直付通关系;历史关系允许继续退分账,但禁止回退当前可变配置。
*
@@ -305,16 +305,16 @@ public class ThirdPayOrderServiceImpl extends ServiceImpl<ThirdPayOrderMapper, T
*/
private void fillProfitSharingStage(ThirdPayOrderVo order, ThirdFzOrderEntity fzOrder) {
boolean enabled = ObjectUtil.equals(order.getEnableFz(), YesNoEnum.YES.getCode());
if (!enabled) {
boolean alipay = ObjectUtil.equals(order.getPayPlatformType(), AppPlusPlatformTypeEnum.ALI_PAY.getType());
if (!enabled && !alipay) {
order.setSettleConfirmStatus("NOT_CONFIGURED");
order.setRoyaltyStatus("NOT_CONFIGURED");
order.setFzRefundStatus("NOT_CONFIGURED");
return;
}
boolean alipay = ObjectUtil.equals(order.getPayPlatformType(), AppPlusPlatformTypeEnum.ALI_PAY.getType());
order.setSettleConfirmStatus(alipay ? "PENDING" : "NOT_APPLICABLE");
order.setRoyaltyStatus("PENDING");
order.setFzRefundStatus("NOT_STARTED");
order.setRoyaltyStatus(enabled ? "PENDING" : "NOT_CONFIGURED");
order.setFzRefundStatus(enabled ? "NOT_STARTED" : "NOT_CONFIGURED");
if (fzOrder == null) {
return;
}
@@ -323,6 +323,10 @@ public class ThirdPayOrderServiceImpl extends ServiceImpl<ThirdPayOrderMapper, T
if (alipay) {
copySettleConfirmStage(order, createSnapshot == null ? null : createSnapshot.getJSONObject("settleConfirm"));
}
// 关闭平台抽佣的支付宝订单只展示必做的确认结算阶段,不能把阶段载体误投影成平台抽佣。
if (!enabled) {
return;
}
copyRoyaltyStage(order, createSnapshot == null ? null : createSnapshot.getJSONObject("royalty"), fzOrder);
copyRefundStage(order, fzOrder);
}
@@ -4,6 +4,7 @@ import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.common.core.constant.enums.YesNoEnum;
import com.bsd.pjcloud.common.data.tenant.TenantBroker;
import com.bsd.pjcloud.thirdparty.constant.fz.ThirdFzOrderStatusEnum;
import com.bsd.pjcloud.thirdparty.constant.miniplatform.AppPlusPlatformTypeEnum;
@@ -301,7 +302,7 @@ public class ThirdPaymentTaskServiceImpl extends ServiceImpl<ThirdPaymentTaskMap
} else {
ThirdFzOrderEntity createdAli = aliPayService.ensureDirectPayFzOrder(payOrder);
if (createdAli == null) {
return TaskResult.retry("PROCESSING", "FZ_NOT_CREATED", "支付宝分账记录尚未生成");
return TaskResult.retry("PROCESSING", "FZ_NOT_CREATED", "支付宝直付通阶段记录尚未生成");
}
// 历史成功订单可能没有 payTime;此时以当前处理时间作为安全下限,仍保证确认结算不会早于本次发现。
LocalDateTime settleReadyTime = payOrder.getPayTime() == null
@@ -317,7 +318,8 @@ public class ThirdPaymentTaskServiceImpl extends ServiceImpl<ThirdPaymentTaskMap
return TaskResult.success("MISSING");
}
String settleStatus = aliPayService.confirmDirectPaySettlementStage(settleOrder, payOrder);
if ("SUCCESS".equals(settleStatus)) {
// 支付宝确认结算对全部直付通订单必做;只有支付创建时开启平台抽佣才排后续抽佣任务。
if ("SUCCESS".equals(settleStatus) && isProfitSharingEnabled(payOrder)) {
paymentTaskEnqueuer.enqueueFzRoyalty(payOrder.getTenantId(), payOrder.getId(), settleOrder.getId(),
settleOrder.getPayNo(), settleOrder.getFzNo());
}
@@ -328,6 +330,10 @@ public class ThirdPaymentTaskServiceImpl extends ServiceImpl<ThirdPaymentTaskMap
if (royaltyOrder == null || payOrder == null) {
return TaskResult.success("MISSING");
}
// 兼容修复前已错误物化的任务,关闭平台抽佣后禁止访问支付宝抽佣接口。
if (!isProfitSharingEnabled(payOrder)) {
return TaskResult.success("NOT_CONFIGURED");
}
String royaltyStatus = aliPayService.submitDirectPayRoyaltyStage(royaltyOrder, payOrder);
if ("SUBMITTED".equals(royaltyStatus) || "PROCESSING".equals(royaltyStatus)) {
paymentTaskEnqueuer.enqueueFzRoyaltyQuery(payOrder.getTenantId(), payOrder.getId(), royaltyOrder.getId(),
@@ -340,6 +346,9 @@ public class ThirdPaymentTaskServiceImpl extends ServiceImpl<ThirdPaymentTaskMap
if (royaltyQueryOrder == null || payOrder == null) {
return TaskResult.success("MISSING");
}
if (!isProfitSharingEnabled(payOrder)) {
return TaskResult.success("NOT_CONFIGURED");
}
return settleStageResult(aliPayService.queryDirectPayRoyaltyStage(royaltyQueryOrder, payOrder),
"ALIPAY_ROYALTY_QUERY", "支付宝平台抽佣查询");
case FZ_QUERY:
@@ -479,6 +488,16 @@ public class ThirdPaymentTaskServiceImpl extends ServiceImpl<ThirdPaymentTaskMap
: TaskResult.success(order.getStatus());
}
/**
* 判断支付创建时是否开启平台抽佣;支付宝确认结算不受此开关限制。
*
* @param order 支付订单快照
* @return 需要继续平台抽佣时返回 {@code true}
*/
private boolean isProfitSharingEnabled(ThirdPayOrderEntity order) {
return order != null && YesNoEnum.YES.getCode().equals(order.getEnableFz());
}
/**
* 按分账状态判定当前阶段是否完成。
*
@@ -88,8 +88,8 @@
p.id AS pay_order_id, p.pay_no, 90 AS priority,
DATE_ADD(COALESCE(p.pay_time, p.update_time, #{now}), INTERVAL 30 SECOND) AS next_run_time
FROM third_pay_order p
WHERE p.del_flag = '0' AND p.status = '20' AND p.enable_fz = '1'
AND p.pay_no IS NOT NULL
WHERE p.del_flag = '0' AND p.status = '20' AND p.pay_no IS NOT NULL
AND (p.pay_platform_type = '20' OR (p.pay_platform_type = '10' AND p.enable_fz = '1'))
AND NOT EXISTS (
SELECT 1 FROM third_fz_order f
WHERE f.tenant_id = p.tenant_id AND f.pay_no = p.pay_no AND f.del_flag = '0')
@@ -133,7 +133,7 @@
p.id AS pay_order_id, f.id AS fz_order_id, f.pay_no, f.fz_no, 100 AS priority, #{now} AS next_run_time
FROM third_fz_order f
JOIN third_pay_order p ON p.tenant_id = f.tenant_id AND p.pay_no = f.pay_no AND p.del_flag = '0'
WHERE f.del_flag = '0' AND f.status = '10' AND p.pay_platform_type = '20'
WHERE f.del_flag = '0' AND f.status = '10' AND p.pay_platform_type = '20' AND p.enable_fz = '1'
AND JSON_UNQUOTE(JSON_EXTRACT(COALESCE(f.create_res, '{}'), '$.settleConfirm.status')) = 'SUCCESS'
AND (JSON_UNQUOTE(JSON_EXTRACT(COALESCE(f.create_res, '{}'), '$.royalty.status')) IS NULL
OR JSON_UNQUOTE(JSON_EXTRACT(COALESCE(f.create_res, '{}'), '$.royalty.status')) NOT IN ('SUBMITTED', 'SUCCESS'))
@@ -146,7 +146,7 @@
p.id AS pay_order_id, f.id AS fz_order_id, f.pay_no, f.fz_no, 100 AS priority, #{now} AS next_run_time
FROM third_fz_order f
JOIN third_pay_order p ON p.tenant_id = f.tenant_id AND p.pay_no = f.pay_no AND p.del_flag = '0'
WHERE f.del_flag = '0' AND f.status = '10' AND p.pay_platform_type = '20'
WHERE f.del_flag = '0' AND f.status = '10' AND p.pay_platform_type = '20' AND p.enable_fz = '1'
AND JSON_UNQUOTE(JSON_EXTRACT(COALESCE(f.create_res, '{}'), '$.royalty.status')) IN ('SUBMITTED', 'PROCESSING')
AND NOT EXISTS (SELECT 1 FROM third_payment_task t WHERE t.tenant_id = f.tenant_id
AND t.task_key = CONCAT('FZ_ROYALTY_QUERY:', f.pay_no) AND t.del_flag = '0')
@@ -119,6 +119,25 @@ class ThirdPaymentTaskMapperSqlContractTest {
() -> Assertions.assertTrue(migrationSql.contains("`idx_third_aggregate_close_scan` (`status`, `del_flag`, `create_time`, `id`)")));
}
/**
* 支付宝直付通必须无条件物化确认结算阶段,而平台抽佣及查询候选仍只允许开启分账的订单。
*/
@Test
void alipaySettlementMaterializationShouldBeIndependentFromProfitSharing() {
String sql = mapperXml;
int createStart = sql.indexOf("<when test=\"taskType == 'FZ_CREATE'\">");
int settleStart = sql.indexOf("<when test=\"taskType == 'FZ_SETTLE_CONFIRM'\">");
int royaltyStart = sql.indexOf("<when test=\"taskType == 'FZ_ROYALTY'\">");
int royaltyQueryStart = sql.indexOf("<when test=\"taskType == 'FZ_ROYALTY_QUERY'\">");
int refundQueryStart = sql.indexOf("<when test=\"taskType == 'FZ_REFUND_QUERY'\">");
Assertions.assertAll(
() -> Assertions.assertTrue(sql.substring(createStart, settleStart).contains(
"p.pay_platform_type = '20' OR (p.pay_platform_type = '10' AND p.enable_fz = '1')")),
() -> Assertions.assertFalse(sql.substring(settleStart, royaltyStart).contains("p.enable_fz = '1'")),
() -> Assertions.assertTrue(sql.substring(royaltyStart, royaltyQueryStart).contains("p.enable_fz = '1'")),
() -> Assertions.assertTrue(sql.substring(royaltyQueryStart, refundQueryStart).contains("p.enable_fz = '1'")));
}
/**
* 提取指定 MyBatis 语句片段。
*
@@ -378,6 +378,53 @@ class ThirdAppPlusAliPayServiceImplTest {
verify(thirdFzOrderService).save(result);
}
/**
* 关闭平台抽佣的支付宝直付通订单仍必须创建确认结算阶段记录,但不得读取分佣关系或计算抽佣金额。
*/
@Test
void ensureDirectPayFzOrderShouldCreateSettlementStageWhenProfitSharingDisabled() {
ThirdPayOrderEntity payOrder = paidDirectPayOrder();
payOrder.setEnableFz(YesNoEnum.NO.getCode());
payOrder.setFzRelationId(null);
ThirdFzOrderEntity result = service.ensureDirectPayFzOrder(payOrder);
Assertions.assertNotNull(result);
Assertions.assertEquals(0, result.getPrice());
Assertions.assertEquals(ThirdFzOrderStatusEnum.FZ_UNPAID.getType(), result.getStatus());
verify(thirdFzOrderService).save(result);
verify(mchFzRelationService, never()).getById(any());
}
/**
* 关闭平台抽佣后仍调用支付宝确认结算,并释放全部可结算资金;确认成功后不得调用平台抽佣接口。
*
* @throws Exception 支付宝 SDK Mock 声明的受检异常
*/
@Test
void directPayLifecycleShouldConfirmSettlementWithoutRoyaltyWhenProfitSharingDisabled() throws Exception {
ThirdPayOrderEntity payOrder = paidDirectPayOrder();
payOrder.setEnableFz(YesNoEnum.NO.getCode());
payOrder.setFzRelationId(null);
ThirdFzOrderEntity fzOrder = pendingFzOrder(payOrder);
fzOrder.setPrice(0);
when(alipayClient.execute(any(AlipayTradeSettleConfirmRequest.class)))
.thenReturn(successSettleConfirmResponse());
when(thirdFzOrderService.update(any(ThirdFzOrderEntity.class), any(Wrapper.class))).thenReturn(true);
service.pollDirectPayLifecycle(fzOrder, payOrder);
ArgumentCaptor<AlipayTradeSettleConfirmRequest> requestCaptor =
ArgumentCaptor.forClass(AlipayTradeSettleConfirmRequest.class);
verify(alipayClient).execute(requestCaptor.capture());
AlipayTradeSettleConfirmModel model = (AlipayTradeSettleConfirmModel) requestCaptor.getValue().getBizModel();
Assertions.assertEquals("smid-1001", model.getSettleInfo().getSettleDetailInfos().get(0).getSettleEntityId());
Assertions.assertEquals("false", model.getExtendParams().getRoyaltyFreeze());
Assertions.assertFalse(requestCaptor.getValue().getTextParams().containsKey("app_auth_token"));
verify(alipayClient, never()).execute(any(AlipayTradeOrderSettleRequest.class));
verify(mchFzRelationService, never()).getById(any());
}
/** 低比例向下取整为零时,支付宝分账事实必须按渠道下限保存为一分钱。 */
@Test
void ensureDirectPayFzOrderShouldUseOneFenMinimumAmount() {
@@ -132,6 +132,24 @@ class ThirdPayOrderServiceImplTest {
Assertions.assertNull(order.getRoyaltyReq());
}
/**
* 关闭平台抽佣的支付宝订单仍展示确认结算结果,平台抽佣和退分账明确显示为未配置。
*/
@Test
void disabledProfitSharingShouldStillProjectAlipaySettlementStage() {
ThirdPayOrderVo order = alipayOrder();
order.setEnableFz(YesNoEnum.NO.getCode());
ThirdFzOrderEntity fzOrder = fzOrder(order.getPayNo());
ReflectionTestUtils.invokeMethod(service, "fillProfitSharingStage", order, fzOrder);
Assertions.assertEquals("SUCCESS", order.getSettleConfirmStatus());
Assertions.assertEquals("NOT_CONFIGURED", order.getRoyaltyStatus());
Assertions.assertEquals("NOT_CONFIGURED", order.getFzRefundStatus());
Assertions.assertNull(order.getRoyaltyReq());
Assertions.assertNull(order.getRoyaltyRes());
}
/**
* 验证同一支付单的告警任务优先于较新的成功任务展示,人工重试不会误选已完成任务。
*/
@@ -1,10 +1,12 @@
package com.bsd.pjcloud.thirdparty.service.impl.pay;
import com.bsd.pjcloud.common.core.constant.enums.YesNoEnum;
import com.bsd.pjcloud.thirdparty.constant.pay.ThirdAggregatePaySessionStatusEnum;
import com.bsd.pjcloud.thirdparty.constant.pay.ThirdPayOrderStatusEnum;
import com.bsd.pjcloud.thirdparty.constant.pay.ThirdPaymentTaskTypeEnum;
import com.bsd.pjcloud.thirdparty.dto.pay.ThirdPaymentTaskDispatchDto;
import com.bsd.pjcloud.thirdparty.entity.pay.ThirdAggregatePaySessionEntity;
import com.bsd.pjcloud.thirdparty.entity.pay.ThirdFzOrderEntity;
import com.bsd.pjcloud.thirdparty.entity.pay.ThirdPayOrderEntity;
import com.bsd.pjcloud.thirdparty.entity.pay.ThirdPaymentTaskEntity;
import com.bsd.pjcloud.thirdparty.mapper.pay.ThirdPaymentTaskAttemptMapper;
@@ -200,6 +202,52 @@ class ThirdPaymentTaskServiceImplTest {
Assertions.assertTrue(nowCaptor.getValue().isAfter(dto.getTriggerTime()));
}
/**
* 关闭平台抽佣的支付宝订单确认结算成功后任务直接完成,不得继续排平台抽佣。
*
* @throws Exception 反射调用私有任务分派方法失败时抛出
*/
@Test
void settleConfirmShouldFinishWithoutRoyaltyWhenProfitSharingDisabled() throws Exception {
ThirdPayOrderEntity payOrder = disabledProfitSharingOrder();
ThirdFzOrderEntity fzOrder = settlementStage(payOrder);
when(fzOrderService.getById(fzOrder.getId())).thenReturn(fzOrder);
when(payOrderService.getByPayNo(payOrder.getPayNo())).thenReturn(payOrder);
when(aliPayService.confirmDirectPaySettlementStage(fzOrder, payOrder)).thenReturn("SUCCESS");
ThirdPaymentTaskEntity task = new ThirdPaymentTaskEntity();
task.setTaskType(ThirdPaymentTaskTypeEnum.FZ_SETTLE_CONFIRM.getCode());
task.setFzOrderId(fzOrder.getId());
Object result = runTask(task);
Assertions.assertTrue(resultSuccess(result));
Assertions.assertEquals("SUCCESS", resultExternalStatus(result));
verify(aliPayService).confirmDirectPaySettlementStage(fzOrder, payOrder);
verify(paymentTaskEnqueuer, never()).enqueueFzRoyalty(any(), any(), any(), any(), any());
}
/**
* 修复前已生成的抽佣任务在订单关闭抽佣后必须本地收敛,不能再访问支付宝抽佣接口。
*
* @throws Exception 反射调用私有任务分派方法失败时抛出
*/
@Test
void staleRoyaltyTaskShouldNotCallAlipayWhenProfitSharingDisabled() throws Exception {
ThirdPayOrderEntity payOrder = disabledProfitSharingOrder();
ThirdFzOrderEntity fzOrder = settlementStage(payOrder);
when(fzOrderService.getById(fzOrder.getId())).thenReturn(fzOrder);
when(payOrderService.getByPayNo(payOrder.getPayNo())).thenReturn(payOrder);
ThirdPaymentTaskEntity task = new ThirdPaymentTaskEntity();
task.setTaskType(ThirdPaymentTaskTypeEnum.FZ_ROYALTY.getCode());
task.setFzOrderId(fzOrder.getId());
Object result = runTask(task);
Assertions.assertTrue(resultSuccess(result));
Assertions.assertEquals("NOT_CONFIGURED", resultExternalStatus(result));
verifyNoInteractions(aliPayService);
}
/**
* 构造历史聚合会话,状态保持 EXPIRED 以模拟服务重启后的恢复记录。
*
@@ -216,6 +264,35 @@ class ThirdPaymentTaskServiceImplTest {
return session;
}
/**
* 构造关闭平台抽佣但仍需确认结算的支付宝支付成功订单。
*
* @return 关闭平台抽佣的支付订单
*/
private ThirdPayOrderEntity disabledProfitSharingOrder() {
ThirdPayOrderEntity order = new ThirdPayOrderEntity();
order.setId(401L);
order.setTenantId(9L);
order.setPayNo("pay-settle-1001");
order.setStatus(ThirdPayOrderStatusEnum.PAY_SUCCESS.getType());
order.setEnableFz(YesNoEnum.NO.getCode());
return order;
}
/**
* 构造承载支付宝确认结算状态的本地阶段记录。
*
* @param payOrder 对应支付订单
* @return 待确认结算阶段记录
*/
private ThirdFzOrderEntity settlementStage(ThirdPayOrderEntity payOrder) {
ThirdFzOrderEntity fzOrder = new ThirdFzOrderEntity();
fzOrder.setId(payOrder.getId());
fzOrder.setPayNo(payOrder.getPayNo());
fzOrder.setFzNo("fz_" + payOrder.getPayNo());
return fzOrder;
}
/**
* 构造关联指定聚合会话的任务事实。
*