✨ 支持应收账单异步作废
This commit is contained in:
+11
-1
@@ -35,6 +35,7 @@ import com.bms.receivable.domain.vo.ArBillGenerateRequest;
|
|||||||
import com.bms.receivable.domain.vo.ArBillListExportRow;
|
import com.bms.receivable.domain.vo.ArBillListExportRow;
|
||||||
import com.bms.receivable.domain.vo.ArBillQuery;
|
import com.bms.receivable.domain.vo.ArBillQuery;
|
||||||
import com.bms.receivable.service.IArBillService;
|
import com.bms.receivable.service.IArBillService;
|
||||||
|
import com.bms.receivable.service.ArBillVoidTaskService;
|
||||||
import com.bms.receivable.security.ArBillScopeResolver;
|
import com.bms.receivable.security.ArBillScopeResolver;
|
||||||
import com.bms.receivable.task.ArBillGeneratePreviewExportRunner;
|
import com.bms.receivable.task.ArBillGeneratePreviewExportRunner;
|
||||||
import com.bms.common.annotation.Log;
|
import com.bms.common.annotation.Log;
|
||||||
@@ -61,6 +62,9 @@ public class ArBillController extends BaseController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private IArBillService billService;
|
private IArBillService billService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ArBillVoidTaskService voidTaskService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ArBillScopeResolver billScope;
|
private ArBillScopeResolver billScope;
|
||||||
|
|
||||||
@@ -297,7 +301,13 @@ public class ArBillController extends BaseController {
|
|||||||
public AjaxResult voidBill(@PathVariable Long arBillId, @RequestBody ArBillActionRequest request,
|
public AjaxResult voidBill(@PathVariable Long arBillId, @RequestBody ArBillActionRequest request,
|
||||||
@RequestHeader(value = "X-Request-Id", required = false) String headerRequestId) {
|
@RequestHeader(value = "X-Request-Id", required = false) String headerRequestId) {
|
||||||
String requestId = requestId(request == null ? null : request.getRequestId(), 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+11
@@ -9,6 +9,17 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
|||||||
@Configuration
|
@Configuration
|
||||||
public class ArBillExportTaskConfig
|
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 和磁盘,最多允许两个任务并发生成。
|
* 账单导出会同时占用数据库连接、CPU 和磁盘,最多允许两个任务并发生成。
|
||||||
* 队列满时由提交器将任务标记为失败,避免回退到 Web 请求线程执行。
|
* 队列满时由提交器将任务标记为失败,避免回退到 Web 请求线程执行。
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ public class ArBill extends BaseEntity {
|
|||||||
* 状态:DRAFT草稿、CONFIRMED已确认、PARTIAL_WRITTEN_OFF部分核销、WRITTEN_OFF已核销、VOID已作废。
|
* 状态:DRAFT草稿、CONFIRMED已确认、PARTIAL_WRITTEN_OFF部分核销、WRITTEN_OFF已核销、VOID已作废。
|
||||||
*/
|
*/
|
||||||
private String status;
|
private String status;
|
||||||
|
/** 非空表示账单有正在后台执行的作废任务。 */
|
||||||
|
private Long voidTaskId;
|
||||||
|
/** 最近一次作废失败的原因。 */
|
||||||
|
private String voidTaskError;
|
||||||
/**
|
/**
|
||||||
* 生成状态:GENERATING生成中、READY可用、FAILED生成失败。
|
* 生成状态:GENERATING生成中、READY可用、FAILED生成失败。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.bms.receivable.event;
|
||||||
|
|
||||||
|
public record ArBillVoidTaskEvent(Long taskId) { }
|
||||||
@@ -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<ArBillVoidTask> selectWaiting(@Param("limit") int limit);
|
||||||
|
int markSuccess(@Param("taskId") Long taskId);
|
||||||
|
int markFailed(@Param("taskId") Long taskId, @Param("errorMessage") String errorMessage);
|
||||||
|
}
|
||||||
+126
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,8 +72,6 @@ public interface IArBillService
|
|||||||
|
|
||||||
ArBill confirmBill(Long arBillId, ArBillActionRequest request, String operator, String requestId);
|
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 exportBill(Long arBillId, String operator, String requestId);
|
||||||
|
|
||||||
ArBillExport selectExport(Long exportId);
|
ArBillExport selectExport(Long exportId);
|
||||||
|
|||||||
+6
-50
@@ -84,6 +84,7 @@ import com.bms.receivable.mapper.ArBankAccountMapper;
|
|||||||
import com.bms.receivable.mapper.ArExportTemplateMapper;
|
import com.bms.receivable.mapper.ArExportTemplateMapper;
|
||||||
import com.bms.receivable.service.IArBillLogService;
|
import com.bms.receivable.service.IArBillLogService;
|
||||||
import com.bms.receivable.service.IArBillService;
|
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.ArBillActionRequest;
|
||||||
import com.bms.receivable.domain.vo.ArBillGenerateQuery;
|
import com.bms.receivable.domain.vo.ArBillGenerateQuery;
|
||||||
import com.bms.receivable.domain.vo.ArBillGenerateRequest;
|
import com.bms.receivable.domain.vo.ArBillGenerateRequest;
|
||||||
@@ -204,6 +205,8 @@ public class ArBillServiceImpl implements IArBillService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ArBillMapper billMapper;
|
private ArBillMapper billMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
private ArBillVoidTaskService voidTaskService;
|
||||||
|
@Autowired
|
||||||
private BmsCustomerMapper customerMapper;
|
private BmsCustomerMapper customerMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
private SysUserMapper sysUserMapper;
|
private SysUserMapper sysUserMapper;
|
||||||
@@ -1024,51 +1027,6 @@ public class ArBillServiceImpl implements IArBillService {
|
|||||||
return result;
|
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 startedAt = System.nanoTime();
|
||||||
Long exportUserId = SecurityUtils.getUserId();
|
Long exportUserId = SecurityUtils.getUserId();
|
||||||
billScope.requireBillAccessible(arBillId);
|
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(),
|
ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, ArLogObjectType.EXPORT.getCode(),
|
||||||
ArLogResult.SUCCESS.getCode());
|
ArLogResult.SUCCESS.getCode());
|
||||||
if (previous != null && isNumber(previous.getObjectId())) {
|
if (previous != null && isNumber(previous.getObjectId())) {
|
||||||
@@ -1090,11 +1051,6 @@ public class ArBillServiceImpl implements IArBillService {
|
|||||||
return previousExport;
|
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())) {
|
if (!ArBillGenerationStatus.READY.equals(bill.getGenerationStatus())) {
|
||||||
throw failure(arBillId, ArLogObjectType.EXPORT, String.valueOf(arBillId), ArLogOperationType.EXPORT,
|
throw failure(arBillId, ArLogObjectType.EXPORT, String.valueOf(arBillId), ArLogOperationType.EXPORT,
|
||||||
bill.getStatus(), "账单尚未生成完成,暂不能导出", operator, requestId);
|
bill.getStatus(), "账单尚未生成完成,暂不能导出", operator, requestId);
|
||||||
|
|||||||
@@ -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<Long> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,8 @@
|
|||||||
<result property="writtenOffAmount" column="written_off_amount"/>
|
<result property="writtenOffAmount" column="written_off_amount"/>
|
||||||
<result property="unwrittenAmount" column="unwritten_amount"/>
|
<result property="unwrittenAmount" column="unwritten_amount"/>
|
||||||
<result property="status" column="status"/>
|
<result property="status" column="status"/>
|
||||||
|
<result property="voidTaskId" column="void_task_id"/>
|
||||||
|
<result property="voidTaskError" column="void_task_error"/>
|
||||||
<result property="generationStatus" column="generation_status"/>
|
<result property="generationStatus" column="generation_status"/>
|
||||||
<result property="billVersion" column="bill_version"/>
|
<result property="billVersion" column="bill_version"/>
|
||||||
<result property="bankAccountName" column="bank_account_name"/>
|
<result property="bankAccountName" column="bank_account_name"/>
|
||||||
@@ -111,7 +113,11 @@
|
|||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
<select id="selectArBillList" resultMap="ArBillResult">
|
<select id="selectArBillList" resultMap="ArBillResult">
|
||||||
select b.* from ar_bill b
|
select b.*, (select vt.task_id from ar_bill_void_task vt
|
||||||
|
where vt.ar_bill_id = b.ar_bill_id and vt.status = 'WAITING' limit 1) as void_task_id,
|
||||||
|
(select case when vt.status = 'FAILED' then vt.error_message else null end
|
||||||
|
from ar_bill_void_task vt where vt.ar_bill_id = b.ar_bill_id order by vt.task_id desc limit 1) as void_task_error
|
||||||
|
from ar_bill b
|
||||||
<include refid="arBillListFilter"/>
|
<include refid="arBillListFilter"/>
|
||||||
order by create_time desc, ar_bill_id desc
|
order by create_time desc, ar_bill_id desc
|
||||||
</select>
|
</select>
|
||||||
@@ -246,6 +252,9 @@
|
|||||||
confirmed_time = case when #{status} = 'CONFIRMED' then sysdate() else confirmed_time end
|
confirmed_time = case when #{status} = 'CONFIRMED' then sysdate() else confirmed_time end
|
||||||
where ar_bill_id = #{arBillId}
|
where ar_bill_id = #{arBillId}
|
||||||
and status = #{fromStatus}
|
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'
|
||||||
|
))
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
<update id="updateArBillAmounts">
|
<update id="updateArBillAmounts">
|
||||||
@@ -257,6 +266,9 @@
|
|||||||
update_time = sysdate()
|
update_time = sysdate()
|
||||||
where ar_bill_id = #{arBillId}
|
where ar_bill_id = #{arBillId}
|
||||||
and status = #{fromStatus}
|
and status = #{fromStatus}
|
||||||
|
and not exists (
|
||||||
|
select 1 from ar_bill_void_task vt where vt.ar_bill_id = #{arBillId} and vt.status = 'WAITING'
|
||||||
|
)
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
<update id="updateArBillProgress">
|
<update id="updateArBillProgress">
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.bms.receivable.mapper.ArBillVoidTaskMapper">
|
||||||
|
<resultMap id="Task" type="com.bms.receivable.domain.ArBillVoidTask">
|
||||||
|
<id property="taskId" column="task_id"/>
|
||||||
|
<result property="arBillId" column="ar_bill_id"/>
|
||||||
|
<result property="fromStatus" column="from_status"/>
|
||||||
|
<result property="reason" column="reason"/>
|
||||||
|
<result property="status" column="status"/>
|
||||||
|
<result property="errorMessage" column="error_message"/>
|
||||||
|
<result property="requestId" column="request_id"/>
|
||||||
|
<result property="createBy" column="create_by"/>
|
||||||
|
<result property="createUserId" column="create_user_id"/>
|
||||||
|
<result property="createTime" column="create_time"/>
|
||||||
|
<result property="finishedAt" column="finished_at"/>
|
||||||
|
</resultMap>
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="taskId">
|
||||||
|
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())
|
||||||
|
</insert>
|
||||||
|
<select id="selectById" resultMap="Task">select * from ar_bill_void_task where task_id = #{taskId}</select>
|
||||||
|
<select id="selectByIdForUpdate" resultMap="Task">select * from ar_bill_void_task where task_id = #{taskId} for update</select>
|
||||||
|
<select id="selectActive" resultMap="Task">
|
||||||
|
select * from ar_bill_void_task where ar_bill_id = #{arBillId} and status = 'WAITING' limit 1
|
||||||
|
</select>
|
||||||
|
<select id="selectActiveForUpdate" resultMap="Task">
|
||||||
|
select * from ar_bill_void_task where ar_bill_id = #{arBillId} and status = 'WAITING' limit 1 for update
|
||||||
|
</select>
|
||||||
|
<select id="selectWaiting" resultMap="Task">
|
||||||
|
select * from ar_bill_void_task where status = 'WAITING' order by task_id limit #{limit}
|
||||||
|
</select>
|
||||||
|
<update id="markSuccess">update ar_bill_void_task set status = 'SUCCESS', finished_at = sysdate() where task_id = #{taskId} and status = 'WAITING'</update>
|
||||||
|
<update id="markFailed">update ar_bill_void_task set status = 'FAILED', error_message = #{errorMessage}, finished_at = sysdate() where task_id = #{taskId} and status = 'WAITING'</update>
|
||||||
|
</mapper>
|
||||||
@@ -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='应收账单异步作废任务';
|
||||||
@@ -102,6 +102,25 @@ CREATE TABLE `ar_bill` (
|
|||||||
KEY `idx_ar_bill_cutoff_date` (`bill_period_end`,`ar_bill_id`)
|
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='正式应收账单主表';
|
) 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`
|
-- 表结构:`ar_bill_detail`
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
|
|||||||
@@ -84,6 +84,10 @@ export function voidArBill(arBillId, data) {
|
|||||||
return request({ url: '/ar/bill/' + arBillId + '/void', method: 'put', 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) {
|
export function exportArBill(arBillId) {
|
||||||
return request({ url: '/ar/bill/' + arBillId + '/export', method: 'post' })
|
return request({ url: '/ar/bill/' + arBillId + '/export', method: 'post' })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,12 +44,18 @@
|
|||||||
<template #col-unwrittenAmount="{ row }"><span class="unwritten">{{ money(row.unwrittenAmount) }}</span></template>
|
<template #col-unwrittenAmount="{ row }"><span class="unwritten">{{ money(row.unwrittenAmount) }}</span></template>
|
||||||
<template #col-action="{ row }">
|
<template #col-action="{ row }">
|
||||||
<el-button link type="primary" icon="View" @click="viewBill(row)">详情</el-button>
|
<el-button link type="primary" icon="View" @click="viewBill(row)">详情</el-button>
|
||||||
|
<span v-if="isVoiding(row)" class="voiding-status"><el-icon class="is-loading"><Loading /></el-icon> 作废中</span>
|
||||||
|
<template v-else>
|
||||||
|
<el-tooltip v-if="row.voidTaskError" :content="row.voidTaskError" placement="top">
|
||||||
|
<span class="void-failed-status">作废失败</span>
|
||||||
|
</el-tooltip>
|
||||||
<el-button v-if="row.status === AR_BILL_STATUS.DRAFT" link type="success" v-hasPermi="['ar:bill:confirm']" @click="confirmBill(row)">确认</el-button>
|
<el-button v-if="row.status === AR_BILL_STATUS.DRAFT" link type="success" v-hasPermi="['ar:bill:confirm']" @click="confirmBill(row)">确认</el-button>
|
||||||
<el-button v-if="row.status === AR_BILL_STATUS.DRAFT || row.status === AR_BILL_STATUS.CONFIRMED" link type="warning" v-hasPermi="['ar:bill:void']" @click="voidBill(row)">作废</el-button>
|
<el-button v-if="row.status === AR_BILL_STATUS.DRAFT || row.status === AR_BILL_STATUS.CONFIRMED" link type="warning" v-hasPermi="['ar:bill:void']" @click="voidBill(row)">作废</el-button>
|
||||||
<el-button v-if="row.status !== AR_BILL_STATUS.DRAFT && row.status !== AR_BILL_STATUS.VOID" link type="primary" v-hasPermi="['ar:bill:export']" @click="exportBill(row)">导出明细</el-button>
|
<el-button v-if="row.status !== AR_BILL_STATUS.DRAFT && row.status !== AR_BILL_STATUS.VOID" link type="primary" v-hasPermi="['ar:bill:export']" @click="exportBill(row)">导出明细</el-button>
|
||||||
<el-button v-if="canGoWriteoff(row)" link type="primary" v-hasPermi="['customer:bill:writeoff']" @click="goWriteoff(row)">核销</el-button>
|
<el-button v-if="canGoWriteoff(row)" link type="primary" v-hasPermi="['customer:bill:writeoff']" @click="goWriteoff(row)">核销</el-button>
|
||||||
<el-button v-if="canGoReverseWriteoff(row)" link type="warning" v-hasPermi="['customer:bill:writeoff:cancel']" @click="goReverseWriteoff(row)">反核销</el-button>
|
<el-button v-if="canGoReverseWriteoff(row)" link type="warning" v-hasPermi="['customer:bill:writeoff:cancel']" @click="goReverseWriteoff(row)">反核销</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
</template>
|
||||||
</basic-table>
|
</basic-table>
|
||||||
|
|
||||||
<bill-detail v-model="detailOpen" :detail="detail" :loading="detailLoading" @download="downloadExport" @export="exportBill(detail)" />
|
<bill-detail v-model="detailOpen" :detail="detail" :loading="detailLoading" @download="downloadExport" @export="exportBill(detail)" />
|
||||||
@@ -69,7 +75,7 @@
|
|||||||
|
|
||||||
<script setup name="ArBill">
|
<script setup name="ArBill">
|
||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
import { listArBillCustomerOptions, listArBill, getArBill, confirmArBill, voidArBill, exportArBill, getArBillExport, downloadArBillExport, getArBillExportDownloadInfo, getArBillGenerateTask, listActiveArBillGenerateTasks, cancelArBillGenerateTask, retryArBillGenerateTask } from '@/api/ar/bill'
|
import { listArBillCustomerOptions, listArBill, getArBill, confirmArBill, voidArBill, getArBillVoidTask, exportArBill, getArBillExport, downloadArBillExport, getArBillExportDownloadInfo, getArBillGenerateTask, listActiveArBillGenerateTasks, cancelArBillGenerateTask, retryArBillGenerateTask } from '@/api/ar/bill'
|
||||||
import BillDetail from './components/BillDetail.vue'
|
import BillDetail from './components/BillDetail.vue'
|
||||||
import GenerateBillDialog from './components/GenerateBillDialog.vue'
|
import GenerateBillDialog from './components/GenerateBillDialog.vue'
|
||||||
import { AR_BILL_STATUS, AR_BILL_STATUS_OPTIONS, AR_BILL_STATUS_TYPES } from '@/constants/ar/bill'
|
import { AR_BILL_STATUS, AR_BILL_STATUS_OPTIONS, AR_BILL_STATUS_TYPES } from '@/constants/ar/bill'
|
||||||
@@ -91,6 +97,9 @@ const defaultSearchParams = computed(() => ({
|
|||||||
const detailOpen = ref(false)
|
const detailOpen = ref(false)
|
||||||
const detailLoading = ref(false)
|
const detailLoading = ref(false)
|
||||||
const detail = ref(null)
|
const detail = ref(null)
|
||||||
|
const voidTasks = reactive(new Map())
|
||||||
|
const voidPollTimers = new Map()
|
||||||
|
let voidPageClosed = false
|
||||||
const openedBillId = ref()
|
const openedBillId = ref()
|
||||||
const generateOpen = ref(false)
|
const generateOpen = ref(false)
|
||||||
// 任务可能在首次轮询前就完成,至少展示一段时间让用户感知到“正在生成”。
|
// 任务可能在首次轮询前就完成,至少展示一段时间让用户感知到“正在生成”。
|
||||||
@@ -177,7 +186,17 @@ function normalizeBillQuery(params) {
|
|||||||
delete query.billDateRange
|
delete query.billDateRange
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
function queryList(params) { return listArBill(normalizeBillQuery(params)) }
|
function queryList(params) {
|
||||||
|
return listArBill(normalizeBillQuery(params)).then(res => {
|
||||||
|
;(res.rows || []).forEach(row => {
|
||||||
|
if (row.voidTaskId && !voidTasks.has(row.arBillId)) {
|
||||||
|
voidTasks.set(row.arBillId, row.voidTaskId)
|
||||||
|
pollVoidTask(row.arBillId, row.voidTaskId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
})
|
||||||
|
}
|
||||||
function handleExport() {
|
function handleExport() {
|
||||||
const query = normalizeBillQuery(tableRef.value?.getQueryParams() || defaultSearchParams.value)
|
const query = normalizeBillQuery(tableRef.value?.getQueryParams() || defaultSearchParams.value)
|
||||||
delete query.pageNum
|
delete query.pageNum
|
||||||
@@ -322,7 +341,40 @@ function viewBill(row) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
function confirmBill(row) { proxy.$modal.confirm(`确认账单「${row.billNo}」?确认后将进入正式账单状态,请确认订单及费用金额无误后继续。`).then(() => confirmArBill(row.arBillId)).then(() => { proxy.$modal.msgSuccess('账单已确认'); tableRef.value.refresh(); viewBill(row) }).catch(() => {}) }
|
function confirmBill(row) { proxy.$modal.confirm(`确认账单「${row.billNo}」?确认后将进入正式账单状态,请确认订单及费用金额无误后继续。`).then(() => confirmArBill(row.arBillId)).then(() => { proxy.$modal.msgSuccess('账单已确认'); tableRef.value.refresh(); viewBill(row) }).catch(() => {}) }
|
||||||
function voidBill(row) { proxy.$prompt(`请输入账单「${row.billNo}」的作废原因`, '作废账单', { inputPattern: /\S+/, inputErrorMessage: '作废原因不能为空' }).then(({ value }) => voidArBill(row.arBillId, { remark: value })).then(() => { proxy.$modal.msgSuccess('账单已作废'); tableRef.value.refresh(); viewBill(row) }).catch(() => {}) }
|
function isVoiding(row) { return voidTasks.has(row.arBillId) || !!row.voidTaskId }
|
||||||
|
function voidBill(row) {
|
||||||
|
proxy.$prompt(`请输入账单「${row.billNo}」的作废原因`, '作废账单', { inputPattern: /\S+/, inputErrorMessage: '作废原因不能为空' })
|
||||||
|
.then(({ value }) => voidArBill(row.arBillId, { remark: value }))
|
||||||
|
.then(res => {
|
||||||
|
const task = res.data
|
||||||
|
if (!task?.taskId) return
|
||||||
|
voidTasks.set(row.arBillId, task.taskId)
|
||||||
|
pollVoidTask(row.arBillId, task.taskId)
|
||||||
|
proxy.$modal.msgSuccess('作废任务已提交')
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
function pollVoidTask(billId, taskId) {
|
||||||
|
if (voidPollTimers.has(taskId) || voidPageClosed) return
|
||||||
|
const poll = async () => {
|
||||||
|
voidPollTimers.delete(taskId)
|
||||||
|
if (voidPageClosed) return
|
||||||
|
try {
|
||||||
|
const res = await getArBillVoidTask(taskId)
|
||||||
|
if (voidPageClosed) return
|
||||||
|
const task = res.data
|
||||||
|
if (task && ['SUCCESS', 'FAILED'].includes(task.status)) {
|
||||||
|
voidTasks.delete(billId)
|
||||||
|
tableRef.value?.refresh()
|
||||||
|
if (detailOpen.value && openedBillId.value === billId) viewBill({ arBillId: billId })
|
||||||
|
if (task.status === 'SUCCESS') proxy.$modal.msgSuccess('账单已作废')
|
||||||
|
else proxy.$modal.msgError(`账单作废失败:${task.errorMessage || '请稍后重试'}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (_) { /* 临时网络错误时继续轮询 */ }
|
||||||
|
if (!voidPageClosed) voidPollTimers.set(taskId, window.setTimeout(poll, 2000))
|
||||||
|
}
|
||||||
|
voidPollTimers.set(taskId, window.setTimeout(poll, 1000))
|
||||||
|
}
|
||||||
function exportBill(row) {
|
function exportBill(row) {
|
||||||
if (!row?.arBillId) return
|
if (!row?.arBillId) return
|
||||||
exportSubmittingCount.value += 1
|
exportSubmittingCount.value += 1
|
||||||
@@ -415,6 +467,9 @@ watch(() => route.query.customerId, (newId, oldId) => {
|
|||||||
|
|
||||||
// 页面销毁时清理所有轮询定时器,避免切换菜单后仍访问旧页面状态。
|
// 页面销毁时清理所有轮询定时器,避免切换菜单后仍访问旧页面状态。
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
voidPageClosed = true
|
||||||
|
voidPollTimers.forEach(timer => window.clearTimeout(timer))
|
||||||
|
voidPollTimers.clear()
|
||||||
generatePollTimers.forEach(timer => window.clearTimeout(timer))
|
generatePollTimers.forEach(timer => window.clearTimeout(timer))
|
||||||
generatePollTimers.clear()
|
generatePollTimers.clear()
|
||||||
generateFinishTimers.forEach(timer => window.clearTimeout(timer))
|
generateFinishTimers.forEach(timer => window.clearTimeout(timer))
|
||||||
@@ -462,6 +517,8 @@ function statusType(value) { return AR_BILL_STATUS_TYPES[value] || 'info' }
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.voiding-status { color: var(--el-color-warning); margin-left: 8px; white-space: nowrap; }
|
||||||
|
.void-failed-status { color: var(--el-color-danger); margin-left: 8px; white-space: nowrap; }
|
||||||
.ar-bill-page :deep(.basic-table > .basic-form) {
|
.ar-bill-page :deep(.basic-table > .basic-form) {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
|||||||
Reference in New Issue
Block a user