From 1ba431540d9f915d96c9a3a6dbe881ca75f50582 Mon Sep 17 00:00:00 2001 From: caojia <1301869380@qq.com> Date: Thu, 12 Nov 2026 17:33:21 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E6=94=AF=E6=8C=81=E5=BA=94?= =?UTF-8?q?=E6=94=B6=E8=B4=A6=E5=8D=95=E5=BC=82=E6=AD=A5=E4=BD=9C=E5=BA=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../receivable/ArBillController.java | 12 +- .../config/ArBillExportTaskConfig.java | 11 ++ .../com/bms/receivable/domain/ArBill.java | 4 + .../bms/receivable/domain/ArBillVoidTask.java | 20 +++ .../receivable/event/ArBillVoidTaskEvent.java | 3 + .../mapper/ArBillVoidTaskMapper.java | 16 +++ .../service/ArBillVoidTaskService.java | 126 ++++++++++++++++++ .../receivable/service/IArBillService.java | 2 - .../service/impl/ArBillServiceImpl.java | 56 +------- .../receivable/task/ArBillVoidTaskRunner.java | 50 +++++++ .../mapper/receivable/ArBillMapper.xml | 14 +- .../receivable/ArBillVoidTaskMapper.xml | 34 +++++ bms-back/sql/20261112_ar_bill_void_task.sql | 18 +++ bms-back/sql/bms_init.sql | 19 +++ bms-front/src/api/ar/bill.js | 4 + bms-front/src/views/ar/bill/index.vue | 73 ++++++++-- 16 files changed, 400 insertions(+), 62 deletions(-) create mode 100644 bms-back/bms-system/src/main/java/com/bms/receivable/domain/ArBillVoidTask.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/receivable/event/ArBillVoidTaskEvent.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/receivable/mapper/ArBillVoidTaskMapper.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/receivable/service/ArBillVoidTaskService.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/receivable/task/ArBillVoidTaskRunner.java create mode 100644 bms-back/bms-system/src/main/resources/mapper/receivable/ArBillVoidTaskMapper.xml create mode 100644 bms-back/sql/20261112_ar_bill_void_task.sql diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/receivable/ArBillController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/receivable/ArBillController.java index 885c230b..3b1ef384 100644 --- a/bms-back/bms-admin/src/main/java/com/bms/web/controller/receivable/ArBillController.java +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/receivable/ArBillController.java @@ -35,6 +35,7 @@ import com.bms.receivable.domain.vo.ArBillGenerateRequest; import com.bms.receivable.domain.vo.ArBillListExportRow; import com.bms.receivable.domain.vo.ArBillQuery; import com.bms.receivable.service.IArBillService; +import com.bms.receivable.service.ArBillVoidTaskService; import com.bms.receivable.security.ArBillScopeResolver; import com.bms.receivable.task.ArBillGeneratePreviewExportRunner; import com.bms.common.annotation.Log; @@ -61,6 +62,9 @@ public class ArBillController extends BaseController { @Autowired private IArBillService billService; + @Autowired + private ArBillVoidTaskService voidTaskService; + @Autowired private ArBillScopeResolver billScope; @@ -297,7 +301,13 @@ public class ArBillController extends BaseController { public AjaxResult voidBill(@PathVariable Long arBillId, @RequestBody ArBillActionRequest request, @RequestHeader(value = "X-Request-Id", required = false) String headerRequestId) { String requestId = requestId(request == null ? null : request.getRequestId(), headerRequestId); - return success(billService.voidBill(arBillId, request, getUsername(), requestId)).put("request_id", requestId); + return success(voidTaskService.submit(arBillId, request == null ? null : request.getRemark(), getUsername(), requestId)).put("request_id", requestId); + } + + @PreAuthorize("@ss.hasPermi('ar:bill:list')") + @GetMapping("/void/task/{taskId}") + public AjaxResult voidTask(@PathVariable Long taskId) { + return success(voidTaskService.get(taskId)); } /** diff --git a/bms-back/bms-system/src/main/java/com/bms/receivable/config/ArBillExportTaskConfig.java b/bms-back/bms-system/src/main/java/com/bms/receivable/config/ArBillExportTaskConfig.java index 1848c1c0..fbe6aa14 100644 --- a/bms-back/bms-system/src/main/java/com/bms/receivable/config/ArBillExportTaskConfig.java +++ b/bms-back/bms-system/src/main/java/com/bms/receivable/config/ArBillExportTaskConfig.java @@ -9,6 +9,17 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; @Configuration public class ArBillExportTaskConfig { + @Bean(name = "arBillVoidTaskExecutor") + public ThreadPoolTaskExecutor arBillVoidTaskExecutor() + { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(1); + executor.setMaxPoolSize(1); + executor.setQueueCapacity(5); + executor.setThreadNamePrefix("ar-bill-void-"); + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); + return executor; + } /** * 账单导出会同时占用数据库连接、CPU 和磁盘,最多允许两个任务并发生成。 * 队列满时由提交器将任务标记为失败,避免回退到 Web 请求线程执行。 diff --git a/bms-back/bms-system/src/main/java/com/bms/receivable/domain/ArBill.java b/bms-back/bms-system/src/main/java/com/bms/receivable/domain/ArBill.java index d62508b2..f1f10011 100644 --- a/bms-back/bms-system/src/main/java/com/bms/receivable/domain/ArBill.java +++ b/bms-back/bms-system/src/main/java/com/bms/receivable/domain/ArBill.java @@ -70,6 +70,10 @@ public class ArBill extends BaseEntity { * 状态:DRAFT草稿、CONFIRMED已确认、PARTIAL_WRITTEN_OFF部分核销、WRITTEN_OFF已核销、VOID已作废。 */ private String status; + /** 非空表示账单有正在后台执行的作废任务。 */ + private Long voidTaskId; + /** 最近一次作废失败的原因。 */ + private String voidTaskError; /** * 生成状态:GENERATING生成中、READY可用、FAILED生成失败。 */ diff --git a/bms-back/bms-system/src/main/java/com/bms/receivable/domain/ArBillVoidTask.java b/bms-back/bms-system/src/main/java/com/bms/receivable/domain/ArBillVoidTask.java new file mode 100644 index 00000000..6c6ae11f --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/receivable/domain/ArBillVoidTask.java @@ -0,0 +1,20 @@ +package com.bms.receivable.domain; + +import java.util.Date; +import lombok.Data; + +/** 作废任务在数据库中持久化,后台事务完成前保持 WAITING。 */ +@Data +public class ArBillVoidTask { + private Long taskId; + private Long arBillId; + private String fromStatus; + private String reason; + private String status; + private String errorMessage; + private String requestId; + private String createBy; + private Long createUserId; + private Date createTime; + private Date finishedAt; +} diff --git a/bms-back/bms-system/src/main/java/com/bms/receivable/event/ArBillVoidTaskEvent.java b/bms-back/bms-system/src/main/java/com/bms/receivable/event/ArBillVoidTaskEvent.java new file mode 100644 index 00000000..3b77c4b1 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/receivable/event/ArBillVoidTaskEvent.java @@ -0,0 +1,3 @@ +package com.bms.receivable.event; + +public record ArBillVoidTaskEvent(Long taskId) { } diff --git a/bms-back/bms-system/src/main/java/com/bms/receivable/mapper/ArBillVoidTaskMapper.java b/bms-back/bms-system/src/main/java/com/bms/receivable/mapper/ArBillVoidTaskMapper.java new file mode 100644 index 00000000..7207b1ff --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/receivable/mapper/ArBillVoidTaskMapper.java @@ -0,0 +1,16 @@ +package com.bms.receivable.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.bms.receivable.domain.ArBillVoidTask; + +public interface ArBillVoidTaskMapper { + int insert(ArBillVoidTask task); + ArBillVoidTask selectById(@Param("taskId") Long taskId); + ArBillVoidTask selectByIdForUpdate(@Param("taskId") Long taskId); + ArBillVoidTask selectActive(@Param("arBillId") Long arBillId); + ArBillVoidTask selectActiveForUpdate(@Param("arBillId") Long arBillId); + List selectWaiting(@Param("limit") int limit); + int markSuccess(@Param("taskId") Long taskId); + int markFailed(@Param("taskId") Long taskId, @Param("errorMessage") String errorMessage); +} diff --git a/bms-back/bms-system/src/main/java/com/bms/receivable/service/ArBillVoidTaskService.java b/bms-back/bms-system/src/main/java/com/bms/receivable/service/ArBillVoidTaskService.java new file mode 100644 index 00000000..493a4be4 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/receivable/service/ArBillVoidTaskService.java @@ -0,0 +1,126 @@ +package com.bms.receivable.service; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.transaction.PlatformTransactionManager; +import com.bms.common.exception.ServiceException; +import com.bms.common.utils.SecurityUtils; +import com.bms.receivable.domain.ArBill; +import com.bms.receivable.domain.ArBillLog; +import com.bms.receivable.domain.ArBillVoidTask; +import com.bms.receivable.enums.ArBillGenerationStatus; +import com.bms.receivable.enums.ArBillStatus; +import com.bms.receivable.event.ArBillVoidTaskEvent; +import com.bms.receivable.mapper.ArBillMapper; +import com.bms.receivable.mapper.ArBillVoidTaskMapper; +import com.bms.receivable.security.ArBillScopeResolver; +import com.bms.price.mapper.PrcIncomeBillFeeMapper; +import com.bms.price.mapper.PrcOrderBillMapper; + +/** 任务状态持久化;账单行锁保证提交、核销与执行串行。 */ +@Service +public class ArBillVoidTaskService { + private final ArBillMapper bills; + private final ArBillVoidTaskMapper tasks; + private final ArBillScopeResolver scope; + private final PrcIncomeBillFeeMapper fees; + private final PrcOrderBillMapper orders; + private final IArBillLogService logs; + private final ApplicationEventPublisher events; + private final TransactionTemplate transaction; + + public ArBillVoidTaskService(ArBillMapper bills, ArBillVoidTaskMapper tasks, ArBillScopeResolver scope, + PrcIncomeBillFeeMapper fees, PrcOrderBillMapper orders, IArBillLogService logs, + ApplicationEventPublisher events, PlatformTransactionManager manager) { + this.bills = bills; this.tasks = tasks; this.scope = scope; this.fees = fees; + this.orders = orders; this.logs = logs; this.events = events; this.transaction = new TransactionTemplate(manager); + } + + @Transactional(rollbackFor = Exception.class) + public ArBillVoidTask submit(Long billId, String reason, String operator, String requestId) { + if (reason == null || reason.trim().isEmpty()) throw new ServiceException("作废必须填写原因"); + if (reason.length() > 500) throw new ServiceException("作废原因不能超过500字"); + scope.requireBillAccessible(billId); + ArBillVoidTask active = tasks.selectActive(billId); + if (active != null) return active; + ArBill bill = bills.selectArBillByIdForUpdate(billId); + if (bill == null) throw new ServiceException("应收账单不存在"); + active = tasks.selectActiveForUpdate(billId); + if (active != null) return active; + if (!ArBillGenerationStatus.READY.equals(bill.getGenerationStatus())) throw new ServiceException("账单尚未生成完成,暂不能作废"); + ArBillStatus status = ArBillStatus.of(bill.getStatus()); + if (status == null || !status.canVoid()) throw new ServiceException("当前状态不允许作废"); + ArBillVoidTask task = new ArBillVoidTask(); + task.setArBillId(billId); task.setFromStatus(bill.getStatus()); task.setReason(reason); + task.setRequestId(requestId); task.setCreateBy(operator); task.setCreateUserId(SecurityUtils.getUserId()); + task.setStatus("WAITING"); + tasks.insert(task); + events.publishEvent(new ArBillVoidTaskEvent(task.getTaskId())); + return task; + } + + public ArBillVoidTask get(Long taskId) { + ArBillVoidTask task = tasks.selectById(taskId); + if (task == null) throw new ServiceException("作废任务不存在"); + scope.requireBillAccessible(task.getArBillId()); + return task; + } + + /** 调用方必须先锁定账单行,保持与任务执行相同的加锁顺序。 */ + public void requireNotVoiding(Long billId) { + if (tasks.selectActiveForUpdate(billId) != null) throw new ServiceException("账单正在作废中,请稍后再试"); + } + + public void execute(Long taskId) { + ArBillVoidTask initial = tasks.selectById(taskId); + if (initial == null || !"WAITING".equals(initial.getStatus())) return; + try { + transaction.executeWithoutResult(s -> { + ArBill bill = bills.selectArBillByIdForUpdate(initial.getArBillId()); + ArBillVoidTask task = tasks.selectByIdForUpdate(taskId); + if (task == null || !"WAITING".equals(task.getStatus())) return; + if (bill == null || !task.getFromStatus().equals(bill.getStatus()) + || !ArBillGenerationStatus.READY.equals(bill.getGenerationStatus())) { + throw new ServiceException("账单状态已发生变化,无法作废"); + } + if (bills.updateArBillStatus(bill.getArBillId(), "VOID", task.getReason(), task.getCreateBy(), task.getFromStatus()) != 1) + throw new ServiceException("账单状态已发生变化,无法作废"); + fees.restoreUnbilledIfNoActiveArBillByArBillId(bill.getArBillId()); + orders.refreshArBillStatusByArBillId(bill.getArBillId()); + ArBillLog log = new ArBillLog(); + log.setArBillId(bill.getArBillId()); log.setObjectType("BILL"); log.setObjectId(String.valueOf(bill.getArBillId())); + log.setOperationType("VOID"); + String description = "作废应收账单:" + task.getReason(); + log.setOperationDesc(description.substring(0, Math.min(500, description.length()))); + log.setFromStatus(task.getFromStatus()); log.setToStatus("VOID"); log.setOperatorId(task.getCreateUserId()); + log.setBeforeData("{\"status\":\"" + task.getFromStatus() + "\"}"); + log.setAfterData("{\"status\":\"VOID\"}"); + log.setOperatorName(task.getCreateBy()); log.setOperationSource("WEB"); log.setRequestId(task.getRequestId()); log.setResult("SUCCESS"); + logs.record(log); + tasks.markSuccess(taskId); + }); + } catch (Exception e) { + // 事务回滚后重新锁定账单和任务,避免覆盖其他节点已完成的结果。 + transaction.executeWithoutResult(s -> { + bills.selectArBillByIdForUpdate(initial.getArBillId()); + ArBillVoidTask task = tasks.selectByIdForUpdate(taskId); + if (task != null && "WAITING".equals(task.getStatus())) { + String message = e instanceof ServiceException ? e.getMessage() : "后台处理失败,请稍后重试"; + message = message == null ? "后台处理失败,请稍后重试" : message.substring(0, Math.min(500, message.length())); + if (tasks.markFailed(taskId, message) == 1) { + ArBillLog failure = new ArBillLog(); + failure.setArBillId(task.getArBillId()); failure.setObjectType("BILL"); + failure.setObjectId(String.valueOf(task.getArBillId())); failure.setOperationType("VOID"); + failure.setOperationDesc("应收账单作废失败"); failure.setFromStatus(task.getFromStatus()); + failure.setOperatorId(task.getCreateUserId()); failure.setOperatorName(task.getCreateBy()); + failure.setOperationSource("WEB"); failure.setRequestId(task.getRequestId()); + failure.setResult("FAILED"); failure.setErrorMessage(message); logs.record(failure); + } + } + }); + throw new IllegalStateException("应收账单作废任务执行失败", e); + } + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/receivable/service/IArBillService.java b/bms-back/bms-system/src/main/java/com/bms/receivable/service/IArBillService.java index a0384626..a36c91e6 100644 --- a/bms-back/bms-system/src/main/java/com/bms/receivable/service/IArBillService.java +++ b/bms-back/bms-system/src/main/java/com/bms/receivable/service/IArBillService.java @@ -72,8 +72,6 @@ public interface IArBillService ArBill confirmBill(Long arBillId, ArBillActionRequest request, String operator, String requestId); - ArBill voidBill(Long arBillId, ArBillActionRequest request, String operator, String requestId); - ArBillExport exportBill(Long arBillId, String operator, String requestId); ArBillExport selectExport(Long exportId); diff --git a/bms-back/bms-system/src/main/java/com/bms/receivable/service/impl/ArBillServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/receivable/service/impl/ArBillServiceImpl.java index 808b8933..f9fbf150 100644 --- a/bms-back/bms-system/src/main/java/com/bms/receivable/service/impl/ArBillServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/receivable/service/impl/ArBillServiceImpl.java @@ -84,6 +84,7 @@ import com.bms.receivable.mapper.ArBankAccountMapper; import com.bms.receivable.mapper.ArExportTemplateMapper; import com.bms.receivable.service.IArBillLogService; import com.bms.receivable.service.IArBillService; +import com.bms.receivable.service.ArBillVoidTaskService; import com.bms.receivable.domain.vo.ArBillActionRequest; import com.bms.receivable.domain.vo.ArBillGenerateQuery; import com.bms.receivable.domain.vo.ArBillGenerateRequest; @@ -204,6 +205,8 @@ public class ArBillServiceImpl implements IArBillService { @Autowired private ArBillMapper billMapper; @Autowired + private ArBillVoidTaskService voidTaskService; + @Autowired private BmsCustomerMapper customerMapper; @Autowired private SysUserMapper sysUserMapper; @@ -1024,51 +1027,6 @@ public class ArBillServiceImpl implements IArBillService { return result; } - /** - * 作废账单并释放来源订单;明细不删除,用于保留历史快照和审计依据。 - */ - @Override - @Transactional(rollbackFor = Exception.class) - public ArBill voidBill(Long arBillId, ArBillActionRequest request, String operator, String requestId) { - long startedAt = System.nanoTime(); - String remark = request == null ? null : request.getRemark(); - if (StringUtils.isEmpty(remark)) { - throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.VOID, - null, "作废必须填写原因", operator, requestId); - } - ArBill bill = billMapper.selectArBillByIdForUpdate(arBillId); - billScope.requireBillAccessible(arBillId); - if (bill == null) { - throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.VOID, - null, "应收账单不存在", operator, requestId); - } - if (!ArBillGenerationStatus.READY.equals(bill.getGenerationStatus())) { - throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.VOID, - bill.getStatus(), "账单尚未生成完成,暂不能作废", operator, requestId); - } - ArBillStatus currentStatus = ArBillStatus.of(bill.getStatus()); - if (currentStatus == null || !currentStatus.canVoid()) { - throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.VOID, - bill.getStatus(), - "当前状态不允许作废", operator, requestId); - } - if (billMapper.updateArBillStatus(arBillId, ArBillStatus.VOID.getCode(), remark, operator, currentStatus.getCode()) != 1) { - throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.VOID, - bill.getStatus(), - "账单状态已发生变化,请刷新后重试", operator, requestId); - } - // 作废后保留历史明细,只释放本账单关联的费用;若费用仍被其他有效账单引用,则继续保持 BILLED。 - sourceFeeMapper.restoreUnbilledIfNoActiveArBillByArBillId(arBillId); - sourceBillMapper.refreshArBillStatusByArBillId(arBillId); - recordSuccess(bill, ArLogObjectType.BILL, ArLogOperationType.VOID, - "作废应收账单:" + remark, bill.getStatus(), ArBillStatus.VOID.getCode(), requestId, - SecurityUtils.getUserId(), operator); - ArBill result = selectArBillById(arBillId); - log.info("应收账单作废处理完成,operator={}, requestId={}, arBillId={}, status={}, costMs={}", - safeLogValue(operator), safeLogValue(requestId), arBillId, result.getStatus(), elapsedMillis(startedAt)); - return result; - } - /** * 创建异步导出任务;模板版本在提交时绑定,保证后续模板变更不影响历史导出。 */ @@ -1078,6 +1036,9 @@ public class ArBillServiceImpl implements IArBillService { long startedAt = System.nanoTime(); Long exportUserId = SecurityUtils.getUserId(); billScope.requireBillAccessible(arBillId); + ArBill bill = billMapper.selectArBillByIdForUpdate(arBillId); + if (bill == null) throw new ServiceException("应收账单不存在"); + voidTaskService.requireNotVoiding(arBillId); ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, ArLogObjectType.EXPORT.getCode(), ArLogResult.SUCCESS.getCode()); if (previous != null && isNumber(previous.getObjectId())) { @@ -1090,11 +1051,6 @@ public class ArBillServiceImpl implements IArBillService { return previousExport; } } - ArBill bill = billMapper.selectArBillById(arBillId); - if (bill == null) { - throw failure(arBillId, ArLogObjectType.EXPORT, String.valueOf(arBillId), ArLogOperationType.EXPORT, - null, "应收账单不存在", operator, requestId); - } if (!ArBillGenerationStatus.READY.equals(bill.getGenerationStatus())) { throw failure(arBillId, ArLogObjectType.EXPORT, String.valueOf(arBillId), ArLogOperationType.EXPORT, bill.getStatus(), "账单尚未生成完成,暂不能导出", operator, requestId); diff --git a/bms-back/bms-system/src/main/java/com/bms/receivable/task/ArBillVoidTaskRunner.java b/bms-back/bms-system/src/main/java/com/bms/receivable/task/ArBillVoidTaskRunner.java new file mode 100644 index 00000000..978a6a6c --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/receivable/task/ArBillVoidTaskRunner.java @@ -0,0 +1,50 @@ +package com.bms.receivable.task; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; +import com.bms.receivable.event.ArBillVoidTaskEvent; +import com.bms.receivable.mapper.ArBillVoidTaskMapper; +import com.bms.receivable.service.ArBillVoidTaskService; + +/** WAITING 任务的业务事务回滚时仍可由定时调度恢复。 */ +@Component +public class ArBillVoidTaskRunner { + private static final Logger log = LoggerFactory.getLogger(ArBillVoidTaskRunner.class); + private final Set scheduled = ConcurrentHashMap.newKeySet(); + private final ArBillVoidTaskMapper tasks; + private final ArBillVoidTaskService service; + private final ThreadPoolTaskExecutor executor; + + public ArBillVoidTaskRunner(ArBillVoidTaskMapper tasks, ArBillVoidTaskService service, + @Qualifier("arBillVoidTaskExecutor") ThreadPoolTaskExecutor executor) { + this.tasks = tasks; this.service = service; this.executor = executor; + } + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onTask(ArBillVoidTaskEvent event) { submit(event.taskId()); } + @Scheduled(cron = "0/30 * * * * ?") + public void recover() { + try { tasks.selectWaiting(20).forEach(task -> submit(task.getTaskId())); } + catch (Exception e) { log.error("调度应收账单作废任务失败", e); } + } + private void submit(Long taskId) { + if (!scheduled.add(taskId)) return; + try { + executor.execute(() -> { + try { service.execute(taskId); } + catch (Exception e) { log.error("应收账单作废任务执行失败,taskId={}", taskId, e); } + finally { scheduled.remove(taskId); } + }); + } catch (Exception e) { + scheduled.remove(taskId); + log.warn("作废任务暂未投递,将由调度器重试,taskId={}", taskId, e); + } + } +} diff --git a/bms-back/bms-system/src/main/resources/mapper/receivable/ArBillMapper.xml b/bms-back/bms-system/src/main/resources/mapper/receivable/ArBillMapper.xml index 4d917000..a2e15ef9 100644 --- a/bms-back/bms-system/src/main/resources/mapper/receivable/ArBillMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/receivable/ArBillMapper.xml @@ -24,6 +24,8 @@ + + @@ -111,7 +113,11 @@ @@ -246,6 +252,9 @@ confirmed_time = case when #{status} = 'CONFIRMED' then sysdate() else confirmed_time end where ar_bill_id = #{arBillId} and status = #{fromStatus} + and (#{status} = 'VOID' or not exists ( + select 1 from ar_bill_void_task vt where vt.ar_bill_id = #{arBillId} and vt.status = 'WAITING' + )) @@ -257,6 +266,9 @@ update_time = sysdate() where ar_bill_id = #{arBillId} and status = #{fromStatus} + and not exists ( + select 1 from ar_bill_void_task vt where vt.ar_bill_id = #{arBillId} and vt.status = 'WAITING' + ) diff --git a/bms-back/bms-system/src/main/resources/mapper/receivable/ArBillVoidTaskMapper.xml b/bms-back/bms-system/src/main/resources/mapper/receivable/ArBillVoidTaskMapper.xml new file mode 100644 index 00000000..18221cef --- /dev/null +++ b/bms-back/bms-system/src/main/resources/mapper/receivable/ArBillVoidTaskMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + insert into ar_bill_void_task (ar_bill_id, from_status, reason, status, request_id, create_by, create_user_id, create_time) + values (#{arBillId}, #{fromStatus}, #{reason}, 'WAITING', #{requestId}, #{createBy}, #{createUserId}, sysdate()) + + + + + + + update ar_bill_void_task set status = 'SUCCESS', finished_at = sysdate() where task_id = #{taskId} and status = 'WAITING' + update ar_bill_void_task set status = 'FAILED', error_message = #{errorMessage}, finished_at = sysdate() where task_id = #{taskId} and status = 'WAITING' + diff --git a/bms-back/sql/20261112_ar_bill_void_task.sql b/bms-back/sql/20261112_ar_bill_void_task.sql new file mode 100644 index 00000000..7ec7e071 --- /dev/null +++ b/bms-back/sql/20261112_ar_bill_void_task.sql @@ -0,0 +1,18 @@ +-- 应收账单异步作废任务。部署应用前执行;已存在同名表时请先核对结构,勿覆盖历史任务。 +CREATE TABLE ar_bill_void_task ( + task_id bigint NOT NULL AUTO_INCREMENT, + ar_bill_id bigint NOT NULL, + from_status varchar(32) NOT NULL, + reason varchar(500) NOT NULL, + status varchar(16) NOT NULL COMMENT 'WAITING、SUCCESS、FAILED', + error_message varchar(500) DEFAULT NULL, + request_id varchar(64) NOT NULL, + create_by varchar(64) NOT NULL, + create_user_id bigint NOT NULL, + create_time datetime NOT NULL, + finished_at datetime DEFAULT NULL, + PRIMARY KEY (task_id), + KEY idx_void_task_bill_status (ar_bill_id, status, task_id), + KEY idx_void_task_bill_latest (ar_bill_id, task_id), + KEY idx_void_task_status (status, task_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应收账单异步作废任务'; diff --git a/bms-back/sql/bms_init.sql b/bms-back/sql/bms_init.sql index 590a9cda..f527d4cb 100644 --- a/bms-back/sql/bms_init.sql +++ b/bms-back/sql/bms_init.sql @@ -102,6 +102,25 @@ CREATE TABLE `ar_bill` ( KEY `idx_ar_bill_cutoff_date` (`bill_period_end`,`ar_bill_id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='正式应收账单主表'; +-- 应收账单异步作废任务(新库初始化;存量库使用 20261112_ar_bill_void_task.sql) +CREATE TABLE `ar_bill_void_task` ( + `task_id` bigint NOT NULL AUTO_INCREMENT, + `ar_bill_id` bigint NOT NULL, + `from_status` varchar(32) NOT NULL, + `reason` varchar(500) NOT NULL, + `status` varchar(16) NOT NULL COMMENT 'WAITING、SUCCESS、FAILED', + `error_message` varchar(500) DEFAULT NULL, + `request_id` varchar(64) NOT NULL, + `create_by` varchar(64) NOT NULL, + `create_user_id` bigint NOT NULL, + `create_time` datetime NOT NULL, + `finished_at` datetime DEFAULT NULL, + PRIMARY KEY (`task_id`), + KEY `idx_void_task_bill_status` (`ar_bill_id`, `status`, `task_id`), + KEY `idx_void_task_bill_latest` (`ar_bill_id`, `task_id`), + KEY `idx_void_task_status` (`status`, `task_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应收账单异步作废任务'; + -- ---------------------------- -- 表结构:`ar_bill_detail` -- ---------------------------- diff --git a/bms-front/src/api/ar/bill.js b/bms-front/src/api/ar/bill.js index c0576f9a..6f887c4b 100644 --- a/bms-front/src/api/ar/bill.js +++ b/bms-front/src/api/ar/bill.js @@ -84,6 +84,10 @@ export function voidArBill(arBillId, data) { return request({ url: '/ar/bill/' + arBillId + '/void', method: 'put', data }) } +export function getArBillVoidTask(taskId) { + return request({ url: '/ar/bill/void/task/' + taskId, method: 'get' }) +} + export function exportArBill(arBillId) { return request({ url: '/ar/bill/' + arBillId + '/export', method: 'post' }) } diff --git a/bms-front/src/views/ar/bill/index.vue b/bms-front/src/views/ar/bill/index.vue index 84c2e87b..3d168ecf 100644 --- a/bms-front/src/views/ar/bill/index.vue +++ b/bms-front/src/views/ar/bill/index.vue @@ -44,11 +44,17 @@ @@ -69,7 +75,7 @@