123123123
This commit is contained in:
1 parent
20e36d6d86
commit
9eea0ab3c5
6 files changed
+267
-41
No files matched your search
@@ -0,0 +1,30 @@
|
||||
-- 2026-09-14 自动退款任务僵尸循环止血脚本
|
||||
--
|
||||
-- 背景:商户关闭自动退款开关的任务被置为 status='30'(开关关闭暂停)且 next_retry_time=NULL,
|
||||
-- 扫描器对 next_retry_time IS NULL 的暂停任务每 30 秒无条件重扫,配合 wallet 配置服务不可达,
|
||||
-- 形成 select for update + Feign 超时的死循环,拖垮订单接口(当晚 page-join 504)。
|
||||
-- 代码修复(暂停任务加 10 分钟退避)上线后新数据自动退避,本脚本只处理存量 NULL 行。
|
||||
--
|
||||
-- 执行方式:手工执行(无自动迁移),先于代码部署。
|
||||
-- 注意:不置终态——商户重新打开开关后任务仍可正常退款,仅把复查时间推迟一天降温。
|
||||
|
||||
-- 1. 执行前确认存量僵尸任务数量(预期为线上扫描风暴的规模)
|
||||
SELECT COUNT(*) AS zombie_task_count
|
||||
FROM hard_auto_refund_task
|
||||
WHERE del_flag = '0'
|
||||
AND status = '30'
|
||||
AND next_retry_time IS NULL;
|
||||
|
||||
-- 2. 将存量僵尸任务的复查时间推至一天后,立即脱离 30 秒扫描循环
|
||||
UPDATE hard_auto_refund_task
|
||||
SET next_retry_time = DATE_ADD(NOW(), INTERVAL 1 DAY)
|
||||
WHERE del_flag = '0'
|
||||
AND status = '30'
|
||||
AND next_retry_time IS NULL;
|
||||
|
||||
-- 3. 执行后确认已无 NULL 复查时间的暂停任务(预期为 0)
|
||||
SELECT COUNT(*) AS remaining_zombie_task_count
|
||||
FROM hard_auto_refund_task
|
||||
WHERE del_flag = '0'
|
||||
AND status = '30'
|
||||
AND next_retry_time IS NULL;
|
||||
+19
@@ -142,6 +142,25 @@ public interface HardOrderMapper extends PjcloudBaseMapper<HardOrderEntity> {
|
||||
*/
|
||||
Page<HardOrderVo> scopePageList(Page page, @Param("dto") HardOrderDto dto, DataScope dataScope);
|
||||
|
||||
/**
|
||||
* 管理端先按全部筛选条件和数据范围分页读取订单主键,展示表关联与最新售后/发票子查询不参与全量排序。
|
||||
* 查询列保留 merchant_dept_id 供数据权限拦截器包装部门过滤;总数由分页插件基于数据权限改写后的 SQL 自动统计。
|
||||
*
|
||||
* @param page 主键分页参数,排序规则与 {@link #scopePageList} 完全一致
|
||||
* @param dto 订单、商户、状态、时间、设备类型及售后单号筛选条件
|
||||
* @param dataScope 后台商户部门数据范围
|
||||
* @return 当前页订单主键(附商户部门列)及符合条件的总数
|
||||
*/
|
||||
Page<HardOrderVo> scopePageIds(Page page, @Param("dto") HardOrderDto dto, DataScope dataScope);
|
||||
|
||||
/**
|
||||
* 按已分页的订单主键批量读取管理端展示详情,设备、最新售后单、付款人、发票和兑换券关联仅对当前页执行。
|
||||
*
|
||||
* @param orderIds 当前页订单主键集合,调用方保证非空且已完成数据范围过滤
|
||||
* @return 当前租户内存在的订单详情;SQL 不承诺返回顺序,由服务层按主键页顺序重排
|
||||
*/
|
||||
List<HardOrderVo> listPageDetails(@Param("orderIds") List<Long> orderIds);
|
||||
|
||||
/**
|
||||
* 先按 App 可见范围和全部筛选条件分页读取订单主键,避免展示表关联参与全量排序。
|
||||
*
|
||||
|
||||
+47
-13
@@ -99,8 +99,16 @@ public class HardAutoRefundTaskServiceImpl extends ServiceImpl<HardAutoRefundTas
|
||||
|
||||
/**
|
||||
* 配置服务异常后再次复查的等待时间,单位秒,不消耗自动尝试额度。
|
||||
* 取 5 分钟而非更短间隔:Wallet 整体不可用时,避免扫描器每轮对上百个暂停任务串行等待 Feign 超时;
|
||||
* 代价是 Wallet 恢复后任务最多延迟 5 分钟继续执行,对异步补偿任务可接受。
|
||||
*/
|
||||
private static final long CONFIG_RECHECK_DELAY_SECONDS = 30L;
|
||||
private static final long CONFIG_RECHECK_DELAY_SECONDS = 300L;
|
||||
|
||||
/**
|
||||
* 开关关闭暂停后的固定复查间隔,单位分钟。暂停任务按该间隔复查开关状态,
|
||||
* 不再以扫描频率(30 秒)反复占用任务行锁、配置调用和数据库连接。
|
||||
*/
|
||||
private static final long SWITCH_DISABLED_RECHECK_MINUTES = 10L;
|
||||
|
||||
/**
|
||||
* 任务和尝试日志错误字段允许保存的最大字符数。
|
||||
@@ -350,9 +358,13 @@ public class HardAutoRefundTaskServiceImpl extends ServiceImpl<HardAutoRefundTas
|
||||
}
|
||||
try {
|
||||
try {
|
||||
// 配置查询移到数据库行锁事务之外:Wallet 抖动或宕机时不再持有任务 FOR UPDATE 行锁等待 Feign 超时,
|
||||
// 同时把一次尝试的两次配置调用收敛为一次;人工直执路径不消费配置,传 null 即可。
|
||||
MchAutoRefundConfigVo currentConfig = needsCurrentConfig(triggerType)
|
||||
? loadCurrentConfig(current.getMchId(), SUPPORTED_ORDER_TYPE) : null;
|
||||
if (Objects.equals(current.getTaskType(), HardAutoRefundTaskTypeEnum.EXPIRED_RESERVE.getType())
|
||||
&& current.getAfterSaleId() == null) {
|
||||
if (!getSelf().canPrepareExpiredReserveTask(taskId, triggerType, operatorUserId)) {
|
||||
if (!getSelf().canPrepareExpiredReserveTask(taskId, triggerType, operatorUserId, currentConfig)) {
|
||||
return;
|
||||
}
|
||||
getSelf().prepareExpiredReserveTask(taskId);
|
||||
@@ -361,7 +373,7 @@ public class HardAutoRefundTaskServiceImpl extends ServiceImpl<HardAutoRefundTas
|
||||
return;
|
||||
}
|
||||
}
|
||||
getSelf().executeAttemptInTransaction(taskId, triggerType, operatorUserId);
|
||||
getSelf().executeAttemptInTransaction(taskId, triggerType, operatorUserId, currentConfig);
|
||||
} catch (CurrentConfigLookupException exception) {
|
||||
if (!tryRecoverPersistedWalletRefund(taskId, triggerType, operatorUserId)) {
|
||||
getSelf().pauseConfigUnavailableInTransaction(taskId, exception.getMessage());
|
||||
@@ -376,15 +388,17 @@ public class HardAutoRefundTaskServiceImpl extends ServiceImpl<HardAutoRefundTas
|
||||
|
||||
/**
|
||||
* 在创建系统售后前复核到期时间、任务状态和当前配置,避免开关已关闭时提前占用订单售后。
|
||||
* 当前配置由调用方在事务外查询后传入,事务内不再发起远程调用。
|
||||
*
|
||||
* @param taskId 过期预约任务主键
|
||||
* @param triggerType 触发来源,人工业务退款不受当前开关控制
|
||||
* @param operatorUserId 人工操作用户 ID,自动触发为空
|
||||
* @param current 事务外预加载的当前配置;人工触发路径不使用
|
||||
* @return 当前允许准备系统售后时返回 {@code true}
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean canPrepareExpiredReserveTask(Long taskId, HardAutoRefundTriggerTypeEnum triggerType,
|
||||
Long operatorUserId) {
|
||||
Long operatorUserId, MchAutoRefundConfigVo current) {
|
||||
HardAutoRefundTaskEntity task = baseMapper.selectByIdForUpdate(taskId);
|
||||
if (!canExecute(task, triggerType)
|
||||
|| !Objects.equals(task.getTaskType(), HardAutoRefundTaskTypeEnum.EXPIRED_RESERVE.getType())) {
|
||||
@@ -393,7 +407,6 @@ public class HardAutoRefundTaskServiceImpl extends ServiceImpl<HardAutoRefundTas
|
||||
if (triggerType == HardAutoRefundTriggerTypeEnum.EXPIRED_MANUAL) {
|
||||
return true;
|
||||
}
|
||||
MchAutoRefundConfigVo current = loadCurrentConfig(task.getMchId(), SUPPORTED_ORDER_TYPE);
|
||||
if (!matchesExpiredSnapshot(task, current)) {
|
||||
pauseConfigUnavailable(task, "自动退款配置与订单快照不一致");
|
||||
return false;
|
||||
@@ -426,15 +439,18 @@ public class HardAutoRefundTaskServiceImpl extends ServiceImpl<HardAutoRefundTas
|
||||
}
|
||||
|
||||
/**
|
||||
* 在单个 Hardware 事务中锁任务、复核配置、调用 Wallet 并原子完成售后和任务状态。
|
||||
* 在单个 Hardware 事务中锁任务、复核调用方预加载的配置、调用 Wallet 并原子完成售后和任务状态。
|
||||
* 配置查询在事务外完成,事务内不因 Feign 超时长时间持有任务行锁;
|
||||
* 资金调用异常会使本事务全部回滚,外层随后用独立事务写失败日志。
|
||||
*
|
||||
* @param taskId 任务主键
|
||||
* @param triggerType 触发来源
|
||||
* @param operatorUserId 人工重试用户 ID
|
||||
* @param current 事务外预加载的当前配置;人工直执路径不使用
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void executeAttemptInTransaction(Long taskId, HardAutoRefundTriggerTypeEnum triggerType, Long operatorUserId) {
|
||||
public void executeAttemptInTransaction(Long taskId, HardAutoRefundTriggerTypeEnum triggerType,
|
||||
Long operatorUserId, MchAutoRefundConfigVo current) {
|
||||
HardAutoRefundTaskEntity task = baseMapper.selectByIdForUpdate(taskId);
|
||||
if (!canExecute(task, triggerType)) {
|
||||
return;
|
||||
@@ -450,7 +466,6 @@ public class HardAutoRefundTaskServiceImpl extends ServiceImpl<HardAutoRefundTas
|
||||
completeAttempt(task, triggerType, operatorUserId, false);
|
||||
return;
|
||||
}
|
||||
MchAutoRefundConfigVo current = loadCurrentConfig(task.getMchId(), SUPPORTED_ORDER_TYPE);
|
||||
if (Objects.equals(task.getTaskType(), HardAutoRefundTaskTypeEnum.EXPIRED_RESERVE.getType())) {
|
||||
if (!matchesExpiredSnapshot(task, current)) {
|
||||
pauseConfigUnavailable(task, "自动退款配置与订单快照不一致");
|
||||
@@ -819,7 +834,10 @@ public class HardAutoRefundTaskServiceImpl extends ServiceImpl<HardAutoRefundTas
|
||||
}
|
||||
|
||||
/**
|
||||
* 首次遇到关闭开关时记录一次跳过审计,持续暂停扫描不会重复增加日志。
|
||||
* 遇到关闭开关时记录一次跳过审计并进入暂停,不增加自动尝试次数。
|
||||
* 暂停任务写入固定复查间隔而不是空值:空值会被扫描器当作立即可重扫,
|
||||
* 在 Wallet 配置服务抖动时任务在开关关闭与配置不可用之间振荡,反复占用行锁并刷跳过日志。
|
||||
* 暂停态之间的流转(配置不可用转开关关闭)不重复记审计日志。
|
||||
*
|
||||
* @param task 已锁定任务
|
||||
* @param triggerType 触发来源
|
||||
@@ -827,7 +845,9 @@ public class HardAutoRefundTaskServiceImpl extends ServiceImpl<HardAutoRefundTas
|
||||
*/
|
||||
private void pauseForDisabledSwitch(HardAutoRefundTaskEntity task, HardAutoRefundTriggerTypeEnum triggerType,
|
||||
Long operatorUserId) {
|
||||
if (!Objects.equals(task.getStatus(), HardAutoRefundTaskStatusEnum.SWITCH_DISABLED.getStatus())) {
|
||||
boolean alreadyPaused = Objects.equals(task.getStatus(), HardAutoRefundTaskStatusEnum.SWITCH_DISABLED.getStatus())
|
||||
|| Objects.equals(task.getStatus(), HardAutoRefundTaskStatusEnum.CONFIG_UNAVAILABLE.getStatus());
|
||||
if (!alreadyPaused) {
|
||||
int attemptNo = safeCount(task.getTotalAttemptCount()) + 1;
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
HardAutoRefundAttemptLogEntity attempt = HardAutoRefundAttemptLogEntity.builder()
|
||||
@@ -846,7 +866,7 @@ public class HardAutoRefundTaskServiceImpl extends ServiceImpl<HardAutoRefundTas
|
||||
task.setLastAttemptTime(now);
|
||||
}
|
||||
task.setStatus(HardAutoRefundTaskStatusEnum.SWITCH_DISABLED.getStatus());
|
||||
task.setNextRetryTime(null);
|
||||
task.setNextRetryTime(LocalDateTime.now().plusMinutes(SWITCH_DISABLED_RECHECK_MINUTES));
|
||||
task.setLastError(null);
|
||||
baseMapper.updateById(task);
|
||||
}
|
||||
@@ -898,6 +918,17 @@ public class HardAutoRefundTaskServiceImpl extends ServiceImpl<HardAutoRefundTas
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断触发来源是否需要预加载当前配置;两种后台人工直执路径在执行阶段明确跳过配置与开关复核。
|
||||
*
|
||||
* @param triggerType 触发来源
|
||||
* @return 需要在进入事务前查询配置时返回 {@code true}
|
||||
*/
|
||||
private boolean needsCurrentConfig(HardAutoRefundTriggerTypeEnum triggerType) {
|
||||
return triggerType != HardAutoRefundTriggerTypeEnum.EXPIRED_MANUAL
|
||||
&& triggerType != HardAutoRefundTriggerTypeEnum.AUTO_VERIFY_MANUAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前配置并统一校验 Feign 业务响应,异常由调用方区分受理降级或任务暂停。
|
||||
*
|
||||
@@ -1390,7 +1421,9 @@ public class HardAutoRefundTaskServiceImpl extends ServiceImpl<HardAutoRefundTas
|
||||
existing.setRestoreInventory(restoreInventory);
|
||||
existing.setStatus(enabled ? HardAutoRefundTaskStatusEnum.PENDING.getStatus()
|
||||
: HardAutoRefundTaskStatusEnum.SWITCH_DISABLED.getStatus());
|
||||
existing.setNextRetryTime(enabled ? triggerTime : null);
|
||||
// 开关关闭同样写入固定复查间隔,空值会被扫描器当作立即可重扫形成僵尸循环。
|
||||
existing.setNextRetryTime(enabled ? triggerTime
|
||||
: LocalDateTime.now().plusMinutes(SWITCH_DISABLED_RECHECK_MINUTES));
|
||||
existing.setLastError(null);
|
||||
if (baseMapper.updateById(existing) != 1) {
|
||||
throw new RuntimeException("刷新过期预约退款任务失败");
|
||||
@@ -1405,7 +1438,8 @@ public class HardAutoRefundTaskServiceImpl extends ServiceImpl<HardAutoRefundTas
|
||||
.status(enabled ? HardAutoRefundTaskStatusEnum.PENDING.getStatus()
|
||||
: HardAutoRefundTaskStatusEnum.SWITCH_DISABLED.getStatus())
|
||||
.autoAttemptCount(0).totalAttemptCount(0)
|
||||
.nextRetryTime(enabled ? triggerTime : null)
|
||||
.nextRetryTime(enabled ? triggerTime
|
||||
: LocalDateTime.now().plusMinutes(SWITCH_DISABLED_RECHECK_MINUTES))
|
||||
.build();
|
||||
if (baseMapper.insert(task) != 1) {
|
||||
throw new RuntimeException("创建过期预约退款任务失败");
|
||||
|
||||
+33
-2
@@ -346,11 +346,42 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
disposeList(list.getRecords(), dto);
|
||||
return list;
|
||||
}
|
||||
Page<HardOrderVo> list = baseMapper.scopePageList(page, dto, dataScope);
|
||||
// 管理端与 App 相同:先分页主键再回填详情,最新售后/发票相关子查询只对当前页执行,不参与全量排序与计数。
|
||||
Page<HardOrderVo> list = idPageThenDetails(page, dto, dataScope);
|
||||
disposeList(list.getRecords(), dto);
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理端先在订单主表上分页主键,再按主键回填当前页详情,避免售后、发票等展示关联参与全量排序与计数。
|
||||
*
|
||||
* @param page 原始订单分页参数
|
||||
* @param dto 订单筛选条件
|
||||
* @param dataScope 后台商户部门数据范围
|
||||
* @return 记录顺序与主键分页查询完全一致的订单页
|
||||
*/
|
||||
private Page<HardOrderVo> idPageThenDetails(Page page, HardOrderDto dto, DataScope dataScope) {
|
||||
Page<HardOrderVo> idPage = new Page<>(page.getCurrent(), page.getSize(), page.searchCount());
|
||||
idPage.setMaxLimit(page.maxLimit());
|
||||
idPage.setOptimizeCountSql(page.optimizeCountSql());
|
||||
idPage.setOptimizeJoinOfCountSql(page.optimizeJoinOfCountSql());
|
||||
idPage.setOrders(page.orders());
|
||||
idPage = baseMapper.scopePageIds(idPage, dto, dataScope);
|
||||
|
||||
List<Long> orderIds = idPage.getRecords().stream().map(HardOrderVo::getId).toList();
|
||||
List<HardOrderVo> orderedRecords = List.of();
|
||||
if (ObjectUtil.isNotEmpty(orderIds)) {
|
||||
Map<Long, HardOrderVo> detailMap = CollectionUtils.convertMap(baseMapper.listPageDetails(orderIds), HardOrderVo::getId);
|
||||
orderedRecords = orderIds.stream().map(detailMap::get).filter(Objects::nonNull).toList();
|
||||
}
|
||||
|
||||
Page<HardOrderVo> result = new Page<>(idPage.getCurrent(), idPage.getSize(), idPage.getTotal(), idPage.searchCount());
|
||||
result.setRecords(orderedRecords);
|
||||
result.setOrders(page.orders());
|
||||
result.setMaxLimit(idPage.maxLimit());
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* App 订单先分页主键、再关联当前页详情,避免售后和父购物车关联参与全部历史订单的排序与分页。
|
||||
*
|
||||
@@ -390,7 +421,7 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
applyIdentityQuery(dto);
|
||||
dto.setOrderPayTypeCodes(List.of(HardPayTypeEnum.HARD_DOOR_FREE_PAY.getType()));
|
||||
dto.setOrderStatus(HardOrderStatusEnum.CREATE.getType());
|
||||
Page<HardOrderVo> list = baseMapper.scopePageList(page, dto, dataScope);
|
||||
Page<HardOrderVo> list = idPageThenDetails(page, dto, dataScope);
|
||||
disposeList(list.getRecords(), dto);
|
||||
return list;
|
||||
}
|
||||
|
||||
+107
-15
@@ -360,6 +360,25 @@
|
||||
</if>
|
||||
</sql>
|
||||
|
||||
<!-- 管理端分页统一排序:ID 预查询与 pageList 必须保持完全一致的顺序,保证主键回填顺序与导出口径一致。 -->
|
||||
<sql id="pageListOrder">
|
||||
<choose>
|
||||
<when test="dto.verificationTimeList != null and dto.verificationTimeList.length == 2">
|
||||
order by a.verification_time desc, a.id desc
|
||||
</when>
|
||||
<when test="dto.apptUseDateTimeList != null and dto.apptUseDateTimeList.length == 2">
|
||||
order by a.appt_use_date desc, a.hour_type asc, a.id desc
|
||||
</when>
|
||||
<when test="dto.updateTimeList != null and dto.updateTimeList.length == 2">
|
||||
order by a.update_time desc, a.id desc
|
||||
</when>
|
||||
<otherwise>
|
||||
<!-- id 作为唯一兜底排序,保证异步导出跨页读取时不会因相同时间和用户出现重复或遗漏。 -->
|
||||
order by a.create_time desc, a.user_id desc, a.id desc
|
||||
</otherwise>
|
||||
</choose>
|
||||
</sql>
|
||||
|
||||
<sql id="pageList">
|
||||
<include refid="selectA"/>
|
||||
m.name as hardName,
|
||||
@@ -416,27 +435,100 @@
|
||||
and s.no = #{dto.afterSaleNo}
|
||||
</if>
|
||||
</where>
|
||||
<choose>
|
||||
<when test="dto.verificationTimeList != null and dto.verificationTimeList.length == 2">
|
||||
order by a.verification_time desc, a.id desc
|
||||
</when>
|
||||
<when test="dto.apptUseDateTimeList != null and dto.apptUseDateTimeList.length == 2">
|
||||
order by a.appt_use_date desc, a.hour_type asc, a.id desc
|
||||
</when>
|
||||
<when test="dto.updateTimeList != null and dto.updateTimeList.length == 2">
|
||||
order by a.update_time desc, a.id desc
|
||||
</when>
|
||||
<otherwise>
|
||||
<!-- id 作为唯一兜底排序,保证异步导出跨页读取时不会因相同时间和用户出现重复或遗漏。 -->
|
||||
order by a.create_time desc, a.user_id desc, a.id desc
|
||||
</otherwise>
|
||||
</choose>
|
||||
<include refid="pageListOrder"/>
|
||||
</sql>
|
||||
|
||||
<select id="scopePageList" resultType="com.bsd.pjcloud.hard.api.vo.order.HardOrderVo">
|
||||
<include refid="pageList"/>
|
||||
</select>
|
||||
|
||||
<!--
|
||||
管理端先在订单主表上分页主键,最新售后/发票相关子查询只对当前页执行(与 App 端 appVisibilityPageIds 同款优化)。
|
||||
merchant_dept_id 必须保留在查询列中:数据权限拦截器对部门权限用户包一层
|
||||
SELECT * FROM (本查询) temp_data_scope WHERE temp_data_scope.merchant_dept_id IN (...),
|
||||
缺少该列会直接 SQL 报错;总数统计走分页插件自动 count,基于数据权限改写后的 SQL 生成,同样带权限。
|
||||
-->
|
||||
<select id="scopePageIds" resultType="com.bsd.pjcloud.hard.api.vo.order.HardOrderVo">
|
||||
select a.id,
|
||||
a.merchant_dept_id
|
||||
from hard_order a
|
||||
<if test="(dto.hardTypeCode != null and dto.hardTypeCode != '') or (dto.hardTypeCodes != null and dto.hardTypeCodes.size() != 0)">
|
||||
left join hard_manager m on m.id = a.hard_id
|
||||
</if>
|
||||
<if test="dto.afterSaleNo != null and dto.afterSaleNo != ''">
|
||||
left join hard_after_sale s on s.id = (
|
||||
select max(latest_sale.id)
|
||||
from hard_after_sale latest_sale
|
||||
where latest_sale.order_id = a.id
|
||||
and latest_sale.del_flag = '0'
|
||||
)
|
||||
</if>
|
||||
<where>
|
||||
<include refid="whereA"/>
|
||||
<include refid="appVisibilityWhere"/>
|
||||
<if test="dto.hardTypeCode != null and dto.hardTypeCode != '' ">
|
||||
and m.type = #{dto.hardTypeCode}
|
||||
</if>
|
||||
<if test="dto.hardTypeCodes != null and dto.hardTypeCodes.size() != 0">
|
||||
and m.type in
|
||||
<foreach collection="dto.hardTypeCodes" item="v" open="(" close=")" separator=",">
|
||||
#{v}
|
||||
</foreach>
|
||||
</if>
|
||||
<if test="dto.afterSaleNo != null and dto.afterSaleNo != ''">
|
||||
and s.no = #{dto.afterSaleNo}
|
||||
</if>
|
||||
</where>
|
||||
<include refid="pageListOrder"/>
|
||||
</select>
|
||||
|
||||
<!-- 管理端当前页详情:按主键集合回填,展示列关联与最新售后/发票子查询只对本页订单执行。 -->
|
||||
<select id="listPageDetails" resultType="com.bsd.pjcloud.hard.api.vo.order.HardOrderVo">
|
||||
<include refid="selectA"/>
|
||||
m.name as hardName,
|
||||
m.type as hardType,
|
||||
m.dept_id as hardTypeDeptId,
|
||||
s.id as afterSaleId,
|
||||
s.no as afterSaleNo,
|
||||
p.user_id as payerUserId,
|
||||
latest_invoice.id as latestInvoiceTaskId,
|
||||
latest_invoice.status as latestInvoiceStatus,
|
||||
latest_invoice.print_no as latestInvoicePrintNo,
|
||||
latest_invoice.hard_id as latestInvoiceSourceHardId,
|
||||
latest_voucher.id as latestVoucherTaskId,
|
||||
latest_voucher.status as latestVoucherStatus,
|
||||
latest_voucher.print_no as latestVoucherPrintNo,
|
||||
latest_voucher.hard_id as latestVoucherSourceHardId
|
||||
from hard_order a
|
||||
left join hard_manager m on m.id = a.hard_id
|
||||
left join hard_after_sale s on s.id = (
|
||||
select max(latest_sale.id)
|
||||
from hard_after_sale latest_sale
|
||||
where latest_sale.order_id = a.id
|
||||
and latest_sale.del_flag = '0'
|
||||
)
|
||||
left join hard_parent_car p on p.id = a.parent_car_id and p.del_flag = '0'
|
||||
left join hard_invoice latest_invoice on latest_invoice.id = (
|
||||
select max(invoice_task.id)
|
||||
from hard_invoice invoice_task
|
||||
where invoice_task.order_id = a.id
|
||||
and (invoice_task.print_type = '1' or invoice_task.print_type is null)
|
||||
and invoice_task.del_flag = '0'
|
||||
)
|
||||
left join hard_invoice latest_voucher on latest_voucher.id = (
|
||||
select max(voucher_task.id)
|
||||
from hard_invoice voucher_task
|
||||
where voucher_task.order_id = a.id
|
||||
and voucher_task.print_type = '2'
|
||||
and voucher_task.del_flag = '0'
|
||||
)
|
||||
where a.id in
|
||||
<foreach collection="orderIds" item="orderId" open="(" close=")" separator=",">
|
||||
#{orderId}
|
||||
</foreach>
|
||||
and a.del_flag = '0'
|
||||
</select>
|
||||
|
||||
<!-- App 先在订单主表上完成分页,展示字段关联不会再参与全量排序。 -->
|
||||
<select id="appVisibilityPageIds" resultType="java.lang.Long">
|
||||
select a.id
|
||||
|
||||
+31
-11
@@ -182,12 +182,11 @@ class HardAutoRefundTaskServiceImplTest {
|
||||
persistedRefund.setPayRefundOrderId(2L);
|
||||
HardAfterSaleService afterSaleService = org.mockito.Mockito.mock(HardAfterSaleService.class);
|
||||
when(taskMapper.selectByIdForUpdate(1L)).thenReturn(task);
|
||||
when(configService.getExact(4L, "2")).thenReturn(R.ok(config));
|
||||
when(walletRechargeService.getRefundByAfterSaleId(2L)).thenReturn(R.ok(persistedRefund));
|
||||
when(afterSaleServiceProvider.getObject()).thenReturn(afterSaleService);
|
||||
when(attemptLogMapper.updateById(any(HardAutoRefundAttemptLogEntity.class))).thenReturn(1);
|
||||
|
||||
service.executeAttemptInTransaction(1L, HardAutoRefundTriggerTypeEnum.SCHEDULED, null);
|
||||
service.executeAttemptInTransaction(1L, HardAutoRefundTriggerTypeEnum.SCHEDULED, null, config);
|
||||
|
||||
verify(afterSaleService).completeAutoRefund(2L, 5L);
|
||||
assertEquals(1, task.getAutoAttemptCount());
|
||||
@@ -272,6 +271,7 @@ class HardAutoRefundTaskServiceImplTest {
|
||||
when(taskMapper.selectOne(any())).thenReturn(null);
|
||||
when(taskMapper.insert(any(HardAutoRefundTaskEntity.class))).thenReturn(1);
|
||||
ArgumentCaptor<HardAutoRefundTaskEntity> taskCaptor = ArgumentCaptor.forClass(HardAutoRefundTaskEntity.class);
|
||||
LocalDateTime before = LocalDateTime.now();
|
||||
|
||||
int result = service.syncExpiredRescanTask(order, config, triggerTime, YesNoEnum.NO.getCode(), false);
|
||||
|
||||
@@ -279,7 +279,9 @@ class HardAutoRefundTaskServiceImplTest {
|
||||
verify(taskMapper).insert(taskCaptor.capture());
|
||||
assertEquals(HardAutoRefundTaskStatusEnum.SWITCH_DISABLED.getStatus(), taskCaptor.getValue().getStatus());
|
||||
assertEquals(triggerTime, taskCaptor.getValue().getTriggerTime());
|
||||
assertNull(taskCaptor.getValue().getNextRetryTime());
|
||||
// 开关关闭创建的暂停任务必须带固定复查间隔,空值会被扫描器当作立即可重扫形成僵尸循环。
|
||||
assertTrue(!taskCaptor.getValue().getNextRetryTime().isBefore(before.plusMinutes(10))
|
||||
&& !taskCaptor.getValue().getNextRetryTime().isAfter(LocalDateTime.now().plusMinutes(10)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -300,11 +302,10 @@ class HardAutoRefundTaskServiceImplTest {
|
||||
void autoVerifyTaskShouldSkipWhenOrderTerminal() {
|
||||
HardAutoRefundTaskEntity task = verifyTask(HardAutoRefundTaskStatusEnum.PENDING.getStatus());
|
||||
when(taskMapper.selectByIdForUpdate(1L)).thenReturn(task);
|
||||
when(configService.getExact(4L, "2")).thenReturn(R.ok(verifyConfig(true)));
|
||||
when(orderMapper.selectByIdForUpdate(3L)).thenReturn(HardOrderEntity.builder()
|
||||
.id(3L).orderStatus(HardOrderStatusEnum.CANCEL.getType()).build());
|
||||
|
||||
service.executeAttemptInTransaction(1L, HardAutoRefundTriggerTypeEnum.SCHEDULED, null);
|
||||
service.executeAttemptInTransaction(1L, HardAutoRefundTriggerTypeEnum.SCHEDULED, null, verifyConfig(true));
|
||||
|
||||
assertEquals(HardAutoRefundTaskStatusEnum.SKIPPED.getStatus(), task.getStatus());
|
||||
assertNull(task.getNextRetryTime());
|
||||
@@ -319,7 +320,6 @@ class HardAutoRefundTaskServiceImplTest {
|
||||
void autoVerifyTaskShouldDeferWhenRefundPending() {
|
||||
HardAutoRefundTaskEntity task = verifyTask(HardAutoRefundTaskStatusEnum.PENDING.getStatus());
|
||||
when(taskMapper.selectByIdForUpdate(1L)).thenReturn(task);
|
||||
when(configService.getExact(4L, "2")).thenReturn(R.ok(verifyConfig(true)));
|
||||
when(orderMapper.selectByIdForUpdate(3L)).thenReturn(HardOrderEntity.builder()
|
||||
.id(3L)
|
||||
.orderStatus(HardOrderStatusEnum.PAY.getType())
|
||||
@@ -327,7 +327,7 @@ class HardAutoRefundTaskServiceImplTest {
|
||||
.build());
|
||||
LocalDateTime before = LocalDateTime.now();
|
||||
|
||||
service.executeAttemptInTransaction(1L, HardAutoRefundTriggerTypeEnum.SCHEDULED, null);
|
||||
service.executeAttemptInTransaction(1L, HardAutoRefundTriggerTypeEnum.SCHEDULED, null, verifyConfig(true));
|
||||
|
||||
assertEquals(HardAutoRefundTaskStatusEnum.WAITING_RETRY.getStatus(), task.getStatus());
|
||||
assertEquals(0, task.getAutoAttemptCount());
|
||||
@@ -336,17 +336,37 @@ class HardAutoRefundTaskServiceImplTest {
|
||||
verify(autoVerifyServiceProvider, org.mockito.Mockito.never()).getObject();
|
||||
}
|
||||
|
||||
/** 自动核销开关关闭时应沿用退款任务的暂停语义,任务保持开关关闭暂停等待重新开启。 */
|
||||
/** 自动核销开关关闭时应暂停并写入固定复查间隔;事务内不再发起配置远程调用。 */
|
||||
@Test
|
||||
void autoVerifyTaskShouldPauseWhenSwitchDisabled() {
|
||||
HardAutoRefundTaskEntity task = verifyTask(HardAutoRefundTaskStatusEnum.PENDING.getStatus());
|
||||
when(taskMapper.selectByIdForUpdate(1L)).thenReturn(task);
|
||||
when(configService.getExact(4L, "2")).thenReturn(R.ok(verifyConfig(false)));
|
||||
LocalDateTime before = LocalDateTime.now();
|
||||
|
||||
service.executeAttemptInTransaction(1L, HardAutoRefundTriggerTypeEnum.SCHEDULED, null);
|
||||
service.executeAttemptInTransaction(1L, HardAutoRefundTriggerTypeEnum.SCHEDULED, null, verifyConfig(false));
|
||||
|
||||
assertEquals(HardAutoRefundTaskStatusEnum.SWITCH_DISABLED.getStatus(), task.getStatus());
|
||||
// 暂停任务带 10 分钟复查间隔,避免被扫描器每 30 秒重扫形成僵尸循环。
|
||||
assertTrue(!task.getNextRetryTime().isBefore(before.plusMinutes(10))
|
||||
&& !task.getNextRetryTime().isAfter(LocalDateTime.now().plusMinutes(10)));
|
||||
verify(orderMapper, org.mockito.Mockito.never()).selectByIdForUpdate(any());
|
||||
verify(configService, org.mockito.Mockito.never()).getExact(any(), any());
|
||||
}
|
||||
|
||||
/** 配置不可用暂停转开关关闭暂停属于暂停态之间流转,不应重复写跳过日志或增加尝试次数。 */
|
||||
@Test
|
||||
void pauseTransitionBetweenPausedStatesShouldNotDuplicateAuditLog() {
|
||||
HardAutoRefundTaskEntity task = verifyTask(HardAutoRefundTaskStatusEnum.CONFIG_UNAVAILABLE.getStatus());
|
||||
when(taskMapper.selectByIdForUpdate(1L)).thenReturn(task);
|
||||
LocalDateTime before = LocalDateTime.now();
|
||||
|
||||
service.executeAttemptInTransaction(1L, HardAutoRefundTriggerTypeEnum.SCHEDULED, null, verifyConfig(false));
|
||||
|
||||
assertEquals(HardAutoRefundTaskStatusEnum.SWITCH_DISABLED.getStatus(), task.getStatus());
|
||||
assertEquals(0, task.getTotalAttemptCount());
|
||||
verify(attemptLogMapper, org.mockito.Mockito.never()).insert(any(HardAutoRefundAttemptLogEntity.class));
|
||||
assertTrue(!task.getNextRetryTime().isBefore(before.plusMinutes(10))
|
||||
&& !task.getNextRetryTime().isAfter(LocalDateTime.now().plusMinutes(10)));
|
||||
}
|
||||
|
||||
/** 触发时间未到或任务已跳过终态时,canExecute 都不允许自动核销任务进入执行。 */
|
||||
@@ -394,7 +414,7 @@ class HardAutoRefundTaskServiceImplTest {
|
||||
task.setTriggerTime(LocalDateTime.now().plusDays(2));
|
||||
when(taskMapper.selectByIdForUpdate(1L)).thenReturn(task);
|
||||
|
||||
service.executeAttemptInTransaction(1L, HardAutoRefundTriggerTypeEnum.AUTO_VERIFY_MANUAL, 99L);
|
||||
service.executeAttemptInTransaction(1L, HardAutoRefundTriggerTypeEnum.AUTO_VERIFY_MANUAL, 99L, null);
|
||||
|
||||
assertEquals(HardAutoRefundTaskStatusEnum.SUCCESS.getStatus(), task.getStatus());
|
||||
assertEquals(0, task.getAutoAttemptCount());
|
||||
|
||||
Reference in new issue
Block a user