Merge branch 'master' of http://git.lionquexpress.com/wuweihua/BMS
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
package com.ruoyi.ar.config;
|
||||
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
/** 应收账单大文件导出专用线程池。 */
|
||||
@Configuration
|
||||
public class ArBillExportTaskConfig
|
||||
{
|
||||
/**
|
||||
* 账单导出会同时占用数据库连接、CPU 和磁盘,最多允许两个任务并发生成。
|
||||
* 队列满时由提交器将任务标记为失败,避免回退到 Web 请求线程执行。
|
||||
*/
|
||||
@Bean(name = "arBillExportTaskExecutor")
|
||||
public ThreadPoolTaskExecutor arBillExportTaskExecutor()
|
||||
{
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(2);
|
||||
executor.setMaxPoolSize(2);
|
||||
executor.setQueueCapacity(2);
|
||||
executor.setKeepAliveSeconds(60);
|
||||
executor.setThreadNamePrefix("ar-bill-export-");
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
|
||||
return executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 账单生成会同时占用数据库连接、CPU 和磁盘,2核4G环境下先限制为单任务串行处理。
|
||||
*/
|
||||
@Bean(name = "arBillGenerateTaskExecutor")
|
||||
public ThreadPoolTaskExecutor arBillGenerateTaskExecutor()
|
||||
{
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(1);
|
||||
executor.setMaxPoolSize(1);
|
||||
executor.setQueueCapacity(5);
|
||||
executor.setKeepAliveSeconds(60);
|
||||
executor.setThreadNamePrefix("ar-bill-generate-");
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,15 @@ package com.ruoyi.ar.domain;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/** 公司收款账户配置。 */
|
||||
/**
|
||||
* 公司收款账户配置。
|
||||
*/
|
||||
@Data
|
||||
public class ArBankAccount implements Serializable
|
||||
{
|
||||
public class ArBankAccount implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long bankAccountId;
|
||||
|
||||
@@ -3,41 +3,72 @@ package com.ruoyi.ar.domain;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/** 正式应收账单。 */
|
||||
/**
|
||||
* 正式应收账单。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ArBill extends BaseEntity
|
||||
{
|
||||
public class ArBill extends BaseEntity {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 账单头主键。
|
||||
*/
|
||||
private Long arBillId;
|
||||
/**
|
||||
* 对外展示的账单编号。
|
||||
*/
|
||||
private String billNo;
|
||||
/**
|
||||
* 财务填写的账单名称。
|
||||
*/
|
||||
private String billName;
|
||||
/**
|
||||
* 账单所属客户主键。
|
||||
*/
|
||||
private Long customerId;
|
||||
private String customerCode;
|
||||
private String customerName;
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
/** 账单所属期开始日期,导出模板展示用。 */
|
||||
private Date billPeriodStart;
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
/** 账单所属期结束日期,同时作为生成订单的截止日期。 */
|
||||
private Date billPeriodEnd;
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date billDate;
|
||||
private String currency;
|
||||
private Integer orderCount;
|
||||
/**
|
||||
* 账单明细应收金额合计。
|
||||
*/
|
||||
private BigDecimal totalAmount;
|
||||
/**
|
||||
* 已通过余额核销的金额。
|
||||
*/
|
||||
private BigDecimal writtenOffAmount;
|
||||
/**
|
||||
* 尚未核销的金额,核销入口按此字段判断是否可操作。
|
||||
*/
|
||||
private BigDecimal unwrittenAmount;
|
||||
/** 状态:DRAFT草稿、CONFIRMED已确认、PARTIAL_WRITTEN_OFF部分核销、WRITTEN_OFF已核销、VOID已作废。 */
|
||||
/**
|
||||
* 状态:DRAFT草稿、CONFIRMED已确认、PARTIAL_WRITTEN_OFF部分核销、WRITTEN_OFF已核销、VOID已作废。
|
||||
*/
|
||||
private String status;
|
||||
/**
|
||||
* 生成状态:GENERATING生成中、READY可用、FAILED生成失败。
|
||||
*/
|
||||
private String generationStatus;
|
||||
private Integer billVersion;
|
||||
private String bankAccountName;
|
||||
private String bankAccountNo;
|
||||
@@ -47,7 +78,16 @@ public class ArBill extends BaseEntity
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date confirmedTime;
|
||||
|
||||
/**
|
||||
* 账单明细;详情接口分页查询时通常不直接填充该集合。
|
||||
*/
|
||||
private List<ArBillDetail> details;
|
||||
/**
|
||||
* 当前账单的导出历史。
|
||||
*/
|
||||
private List<ArBillExport> exports;
|
||||
/**
|
||||
* 当前账单的操作日志。
|
||||
*/
|
||||
private List<ArBillLog> logs;
|
||||
}
|
||||
|
||||
@@ -4,13 +4,15 @@ import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/** 账单订单快照。 */
|
||||
/**
|
||||
* 账单订单快照。
|
||||
*/
|
||||
@Data
|
||||
public class ArBillDetail implements Serializable
|
||||
{
|
||||
public class ArBillDetail implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long detailId;
|
||||
|
||||
@@ -3,13 +3,15 @@ package com.ruoyi.ar.domain;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/** 账单费用快照。 */
|
||||
/**
|
||||
* 账单费用快照。
|
||||
*/
|
||||
@Data
|
||||
public class ArBillDetailFee implements Serializable
|
||||
{
|
||||
public class ArBillDetailFee implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long detailFeeId;
|
||||
|
||||
@@ -2,26 +2,49 @@ package com.ruoyi.ar.domain;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/** 账单导出文件元数据。 */
|
||||
/**
|
||||
* 账单导出文件元数据。
|
||||
*/
|
||||
@Data
|
||||
public class ArBillExport implements Serializable
|
||||
{
|
||||
public class ArBillExport implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long exportId;
|
||||
private Long arBillId;
|
||||
private String fileName;
|
||||
private String fileType;
|
||||
/**
|
||||
* 提交导出任务时绑定的模板版本,避免后续模板变更影响历史任务。
|
||||
*/
|
||||
private Long templateId;
|
||||
private String templateCode;
|
||||
private String templateVersion;
|
||||
private String templateStorageType;
|
||||
private String templateStorageKey;
|
||||
private String templateHash;
|
||||
/**
|
||||
* 生成文件的实际存储键,下载接口据此读取文件。
|
||||
*/
|
||||
private String storageKey;
|
||||
/**
|
||||
* 文件字节数,便于前端展示和运维排查。
|
||||
*/
|
||||
private Long fileSize;
|
||||
/**
|
||||
* 文件摘要,用于校验文件完整性。
|
||||
*/
|
||||
private String fileHash;
|
||||
/**
|
||||
* 导出的明细行数。
|
||||
*/
|
||||
private Integer rowCount;
|
||||
/** 导出状态:PROCESSING生成中、SUCCESS成功可下载、FAILED失败。 */
|
||||
/**
|
||||
* 导出状态:PROCESSING生成中、SUCCESS成功可下载、FAILED失败。
|
||||
*/
|
||||
private String exportStatus;
|
||||
private String exportBy;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.ruoyi.ar.domain;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 应收账单异步生成任务。
|
||||
*/
|
||||
@Data
|
||||
public class ArBillGenerateTask implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 异步任务主键。
|
||||
*/
|
||||
private Long taskId;
|
||||
/**
|
||||
* 面向日志和前端展示的任务编号。
|
||||
*/
|
||||
private String taskNo;
|
||||
/**
|
||||
* 任务对应的账单头主键。
|
||||
*/
|
||||
private Long arBillId;
|
||||
/**
|
||||
* 订单选择模式 code:EXPLICIT=手动选中,ALL_MATCHED=全选查询结果。
|
||||
*/
|
||||
private String selectionMode;
|
||||
/**
|
||||
* 生成请求快照,避免任务执行过程中依赖前端当前条件。
|
||||
*/
|
||||
private String requestPayload;
|
||||
/**
|
||||
* 任务创建时的来源订单创建时间快照。
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date snapshotCreateTime;
|
||||
/**
|
||||
* 按 bill_id 字典序处理的游标。
|
||||
*/
|
||||
private String lastSourceBillId;
|
||||
/**
|
||||
* 待处理总数;全选模式统计完成前使用 -1。
|
||||
*/
|
||||
private Integer totalCount;
|
||||
/**
|
||||
* 已扫描的来源订单数,用于进度展示。
|
||||
*/
|
||||
private Integer processedCount;
|
||||
/**
|
||||
* 成功写入账单明细的订单数。
|
||||
*/
|
||||
private Integer successCount;
|
||||
/**
|
||||
* 已写入账单明细的累计应收金额。
|
||||
*/
|
||||
private BigDecimal totalAmount;
|
||||
/**
|
||||
* 任务状态,取 ArBillGenerateTaskStatus 中定义的值。
|
||||
*/
|
||||
private String status;
|
||||
/**
|
||||
* 失败、取消或补偿清理时返回给用户的说明。
|
||||
*/
|
||||
private String errorMessage;
|
||||
/**
|
||||
* 请求幂等号,防止重复创建账单生成任务。
|
||||
*/
|
||||
private String requestToken;
|
||||
/**
|
||||
* 发起任务的用户。
|
||||
*/
|
||||
private String createBy;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
/** 首次被执行器领取的时间。 */
|
||||
private Date startedAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
/** 最近一次进度更新时间,用于识别执行节点异常。 */
|
||||
private Date heartbeatTime;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
/** 成功、失败或取消完成的时间。 */
|
||||
private Date finishedAt;
|
||||
}
|
||||
@@ -2,23 +2,31 @@ package com.ruoyi.ar.domain;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/** 应收模块追加式审计日志。 */
|
||||
/**
|
||||
* 应收模块追加式审计日志。
|
||||
*/
|
||||
@Data
|
||||
public class ArBillLog implements Serializable
|
||||
{
|
||||
public class ArBillLog implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long logId;
|
||||
private Long arBillId;
|
||||
/** 对象类型:BILL应收账单、EXPORT账单导出、BALANCE_WRITEOFF余额核销。 */
|
||||
/**
|
||||
* 对象类型:BILL应收账单、EXPORT账单导出、BALANCE_WRITEOFF余额核销。
|
||||
*/
|
||||
private String objectType;
|
||||
private String objectId;
|
||||
/** 对象业务标识,例如账单号、收款单号或核销单号。 */
|
||||
/**
|
||||
* 对象业务标识,例如账单号、收款单号或核销单号。
|
||||
*/
|
||||
private String objectDisplay;
|
||||
/** 操作类型,取值由 ArLogOperationType 维护。 */
|
||||
/**
|
||||
* 操作类型,取值由 ArLogOperationType 维护。
|
||||
*/
|
||||
private String operationType;
|
||||
private String operationDesc;
|
||||
private String fromStatus;
|
||||
@@ -27,11 +35,15 @@ public class ArBillLog implements Serializable
|
||||
private String afterData;
|
||||
private String operatorId;
|
||||
private String operatorName;
|
||||
/** 操作来源:WEB网页端、API接口、JOB定时任务、IMPORT数据导入。 */
|
||||
/**
|
||||
* 操作来源:WEB网页端、API接口、JOB定时任务、IMPORT数据导入。
|
||||
*/
|
||||
private String operationSource;
|
||||
private String requestId;
|
||||
private String traceId;
|
||||
/** 操作结果:SUCCESS成功、FAILED失败。 */
|
||||
/**
|
||||
* 操作结果:SUCCESS成功、FAILED失败。
|
||||
*/
|
||||
private String result;
|
||||
private String errorMessage;
|
||||
private String clientIp;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.ruoyi.ar.domain;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 应收账单导出模板元数据。
|
||||
*/
|
||||
@Data
|
||||
public class ArExportTemplate implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long templateId;
|
||||
private String templateCode;
|
||||
private String templateName;
|
||||
private String businessType;
|
||||
private String templateVersion;
|
||||
private String storageType;
|
||||
private String storageKey;
|
||||
private String fileHash;
|
||||
private String status;
|
||||
private String isDefault;
|
||||
private Date effectiveStart;
|
||||
private Date effectiveEnd;
|
||||
private String remark;
|
||||
}
|
||||
@@ -16,4 +16,10 @@ public class ArBillGenerateQuery
|
||||
private Date billPeriodEnd;
|
||||
|
||||
private String currency;
|
||||
|
||||
/** 订单号或运单号关键词,按 OR 条件模糊查询。 */
|
||||
private String orderOrTrackingNo;
|
||||
|
||||
/** 全选模式下,排除弹窗使用的附加订单号或运单号关键词。 */
|
||||
private String excludeOrderOrTrackingNo;
|
||||
}
|
||||
|
||||
+9
-2
@@ -4,8 +4,8 @@ import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import com.ruoyi.ar.enums.ArBillSelectionMode;
|
||||
import lombok.Data;
|
||||
|
||||
/** 从订单计费结果生成正式账单的参数。 */
|
||||
@@ -21,14 +21,21 @@ public class ArBillGenerateRequest
|
||||
@NotNull(message = "账单截止日不能为空")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date billPeriodEnd;
|
||||
@NotEmpty(message = "请至少选择一条订单")
|
||||
/** 手动模式选中的来源订单 ID;全选模式为空。 */
|
||||
private List<String> sourceBillIds;
|
||||
/** 选择模式:EXPLICIT 手动选择,ALL_MATCHED 全选查询结果。 */
|
||||
private String selectionMode = ArBillSelectionMode.EXPLICIT.getCode();
|
||||
/** 全选模式中用户明确排除的来源订单 ID。 */
|
||||
private List<String> excludedSourceBillIds;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date billDate;
|
||||
private String currency = "CNY";
|
||||
/** 订单号或运单号关键词,生成时用于再次校验筛选条件。 */
|
||||
private String orderOrTrackingNo;
|
||||
private String bankAccountName;
|
||||
private String bankAccountNo;
|
||||
private String bankName;
|
||||
private String remark;
|
||||
/** 前端幂等号;重复提交同一请求时复用原任务。 */
|
||||
private String requestId;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.ruoyi.ar.enums;
|
||||
|
||||
/** 应收账单异步生成任务状态。 */
|
||||
public final class ArBillGenerateTaskStatus
|
||||
{
|
||||
/** 待线程池领取的任务。 */
|
||||
public static final String WAITING = "WAITING";
|
||||
/** 已被某个执行节点领取,正在生成明细。 */
|
||||
public static final String RUNNING = "RUNNING";
|
||||
/** 明细全部生成成功。 */
|
||||
public static final String SUCCESS = "SUCCESS";
|
||||
/** 生成失败,账单已进入失败状态。 */
|
||||
public static final String FAILED = "FAILED";
|
||||
/** 用户已请求取消,执行器会在安全边界清理已生成数据。 */
|
||||
public static final String CANCEL_REQUESTED = "CANCEL_REQUESTED";
|
||||
/** 取消清理完成,账单及其生成明细已回收。 */
|
||||
public static final String CANCELED = "CANCELED";
|
||||
|
||||
private ArBillGenerateTaskStatus()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.ruoyi.ar.enums;
|
||||
|
||||
/** 应收账单生成状态,与账单业务状态分离。 */
|
||||
public final class ArBillGenerationStatus
|
||||
{
|
||||
/** 账单头已创建,明细和费用正在异步生成。 */
|
||||
public static final String GENERATING = "GENERATING";
|
||||
/** 明细生成完成,允许确认、核销和导出。 */
|
||||
public static final String READY = "READY";
|
||||
/** 明细生成失败或取消后已完成清理。 */
|
||||
public static final String FAILED = "FAILED";
|
||||
|
||||
private ArBillGenerationStatus()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.ruoyi.ar.enums;
|
||||
|
||||
/**
|
||||
* 应收账单生成时的订单选择模式。
|
||||
* 数据库和接口仍保存英文 code,业务代码通过枚举统一校验和判断。
|
||||
*/
|
||||
public enum ArBillSelectionMode
|
||||
{
|
||||
/** 手动选择当前预览结果中的订单。 */
|
||||
EXPLICIT("EXPLICIT", "手动选择"),
|
||||
/** 选择全部查询结果,仅排除用户明确勾选的订单。 */
|
||||
ALL_MATCHED("ALL_MATCHED", "全选查询结果");
|
||||
|
||||
private final String code;
|
||||
private final String label;
|
||||
|
||||
ArBillSelectionMode(String code, String label)
|
||||
{
|
||||
this.code = code;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public String getCode()
|
||||
{
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getLabel()
|
||||
{
|
||||
return label;
|
||||
}
|
||||
|
||||
/** 空值按历史默认行为视为手动选择,非法非空值返回 null 交由业务层记录失败日志。 */
|
||||
public static ArBillSelectionMode of(String code)
|
||||
{
|
||||
if (code == null || code.trim().isEmpty())
|
||||
{
|
||||
return EXPLICIT;
|
||||
}
|
||||
for (ArBillSelectionMode mode : values())
|
||||
{
|
||||
if (mode.code.equalsIgnoreCase(code.trim()))
|
||||
{
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -28,16 +28,19 @@ public enum ArBillStatus
|
||||
return label;
|
||||
}
|
||||
|
||||
/** 只有草稿账单允许确认。 */
|
||||
public boolean canConfirm()
|
||||
{
|
||||
return this == DRAFT;
|
||||
}
|
||||
|
||||
/** 草稿或已确认账单允许作废;已核销账单不可直接作废。 */
|
||||
public boolean canVoid()
|
||||
{
|
||||
return this == DRAFT || this == CONFIRMED;
|
||||
}
|
||||
|
||||
/** 已确认或部分核销账单允许继续核销。 */
|
||||
public boolean canWriteOff()
|
||||
{
|
||||
return this == CONFIRMED || this == PARTIAL_WRITTEN_OFF;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ruoyi.ar.event;
|
||||
|
||||
/**
|
||||
* 应收账单导出任务已提交事件。
|
||||
*
|
||||
* <p>由事务提交后监听器处理,确保异步线程读取到已落库的导出任务。</p>
|
||||
*/
|
||||
public class ArBillExportTaskEvent
|
||||
{
|
||||
private final Long exportId;
|
||||
|
||||
public ArBillExportTaskEvent(Long exportId)
|
||||
{
|
||||
this.exportId = exportId;
|
||||
}
|
||||
|
||||
public Long getExportId()
|
||||
{
|
||||
return exportId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ruoyi.ar.event;
|
||||
|
||||
/**
|
||||
* 应收账单异步生成任务已提交事件。
|
||||
* 监听器在创建账单事务提交后消费,避免后台线程读取到未提交的任务或账单头。
|
||||
*/
|
||||
public class ArBillGenerateTaskEvent
|
||||
{
|
||||
/** 待投递的生成任务主键。 */
|
||||
private final Long taskId;
|
||||
|
||||
public ArBillGenerateTaskEvent(Long taskId)
|
||||
{
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public Long getTaskId()
|
||||
{
|
||||
return taskId;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package com.ruoyi.ar.mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.ar.domain.ArBankAccount;
|
||||
|
||||
public interface ArBankAccountMapper
|
||||
{
|
||||
public interface ArBankAccountMapper {
|
||||
ArBankAccount selectDefaultByCurrency(@Param("currency") String currency);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
package com.ruoyi.ar.mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.ar.domain.ArBillDetailFee;
|
||||
|
||||
public interface ArBillDetailFeeMapper
|
||||
{
|
||||
public interface ArBillDetailFeeMapper {
|
||||
List<ArBillDetailFee> selectByDetailId(Long detailId);
|
||||
|
||||
List<ArBillDetailFee> selectByBillIdAndDetailId(@Param("arBillId") Long arBillId,
|
||||
@Param("detailId") Long detailId);
|
||||
|
||||
List<ArBillDetailFee> selectByBillId(Long arBillId);
|
||||
|
||||
List<ArBillDetailFee> selectByDetailIds(@Param("detailIds") List<Long> detailIds);
|
||||
|
||||
int insert(ArBillDetailFee fee);
|
||||
|
||||
int insertBatch(List<ArBillDetailFee> fees);
|
||||
|
||||
int deleteByBillId(Long arBillId);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
package com.ruoyi.ar.mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.ar.domain.ArBillDetail;
|
||||
|
||||
public interface ArBillDetailMapper
|
||||
{
|
||||
public interface ArBillDetailMapper {
|
||||
List<ArBillDetail> selectByBillId(Long arBillId);
|
||||
|
||||
/** 查询账单明细及费用明细,避免详情页逐层查询。 */
|
||||
List<ArBillDetail> selectByBillIdAndSourceIds(@Param("arBillId") Long arBillId,
|
||||
@Param("sourceBillIds") List<String> sourceBillIds);
|
||||
|
||||
/**
|
||||
* 查询账单明细及费用明细,避免详情页逐层查询。
|
||||
*/
|
||||
List<ArBillDetail> selectByBillIdWithFees(Long arBillId);
|
||||
|
||||
List<ArBillDetail> selectExportPage(@Param("arBillId") Long arBillId,
|
||||
@Param("lastDetailNo") Integer lastDetailNo, @Param("pageSize") Integer pageSize);
|
||||
|
||||
int insert(ArBillDetail detail);
|
||||
|
||||
int insertBatch(List<ArBillDetail> details);
|
||||
|
||||
int deleteByBillId(Long arBillId);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
package com.ruoyi.ar.mapper;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.ar.domain.ArBillExport;
|
||||
|
||||
public interface ArBillExportMapper
|
||||
{
|
||||
public interface ArBillExportMapper {
|
||||
List<ArBillExport> selectByBillId(Long arBillId);
|
||||
|
||||
ArBillExport selectById(Long exportId);
|
||||
|
||||
ArBillExport selectLatestSuccessByBillId(Long arBillId);
|
||||
|
||||
ArBillExport selectProcessingByBillId(Long arBillId);
|
||||
|
||||
int insert(ArBillExport export);
|
||||
|
||||
int markSuccess(ArBillExport export);
|
||||
|
||||
int markFailed(@Param("exportId") Long exportId, @Param("remark") String remark);
|
||||
|
||||
int markExpiredProcessingAsFailed(@Param("expiredBefore") Date expiredBefore, @Param("remark") String remark);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.ruoyi.ar.mapper;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.ar.domain.ArBillGenerateTask;
|
||||
|
||||
/**
|
||||
* 应收账单异步生成任务 Mapper。
|
||||
*/
|
||||
public interface ArBillGenerateTaskMapper {
|
||||
ArBillGenerateTask selectById(Long taskId);
|
||||
|
||||
ArBillGenerateTask selectByRequestToken(String requestToken);
|
||||
|
||||
List<ArBillGenerateTask> selectActiveByCreateBy(@Param("createBy") String createBy);
|
||||
|
||||
List<ArBillGenerateTask> selectWaiting(@Param("limit") int limit);
|
||||
|
||||
int insert(ArBillGenerateTask task);
|
||||
|
||||
int markRunning(@Param("taskId") Long taskId, @Param("startedAt") Date startedAt);
|
||||
|
||||
int updateTotalCount(@Param("taskId") Long taskId, @Param("totalCount") Integer totalCount);
|
||||
|
||||
int updateProgress(@Param("taskId") Long taskId, @Param("lastSourceBillId") String lastSourceBillId,
|
||||
@Param("processedCount") Integer processedCount, @Param("successCount") Integer successCount,
|
||||
@Param("totalAmount") java.math.BigDecimal totalAmount, @Param("heartbeatTime") Date heartbeatTime);
|
||||
|
||||
int markSuccess(@Param("taskId") Long taskId, @Param("finishedAt") Date finishedAt);
|
||||
|
||||
int markFailed(@Param("taskId") Long taskId, @Param("errorMessage") String errorMessage,
|
||||
@Param("finishedAt") Date finishedAt);
|
||||
|
||||
int markCancelRequested(@Param("taskId") Long taskId);
|
||||
|
||||
int markCanceled(@Param("taskId") Long taskId, @Param("finishedAt") Date finishedAt);
|
||||
|
||||
int resetExpiredRunning(@Param("expiredBefore") Date expiredBefore);
|
||||
|
||||
List<ArBillGenerateTask> selectCancelRequestedBefore(@Param("expiredBefore") Date expiredBefore,
|
||||
@Param("limit") int limit);
|
||||
}
|
||||
@@ -3,35 +3,49 @@ package com.ruoyi.ar.mapper;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.ar.domain.ArBill;
|
||||
import com.ruoyi.ar.domain.vo.ArBillQuery;
|
||||
|
||||
public interface ArBillMapper
|
||||
{
|
||||
public interface ArBillMapper {
|
||||
List<ArBill> selectArBillList(ArBillQuery query);
|
||||
|
||||
ArBill selectArBillById(Long arBillId);
|
||||
|
||||
ArBill selectArBillByBillNo(String billNo);
|
||||
|
||||
ArBill selectGeneratingByCustomerPeriod(@Param("customerId") Long customerId,
|
||||
@Param("currency") String currency, @Param("cutoffDate") Date cutoffDate);
|
||||
|
||||
Integer selectMaxBillVersion(@Param("customerId") Long customerId,
|
||||
@Param("cutoffDate") Date cutoffDate, @Param("currency") String currency);
|
||||
@Param("cutoffDate") Date cutoffDate, @Param("currency") String currency);
|
||||
|
||||
ArBill selectArBillByIdForUpdate(Long arBillId);
|
||||
|
||||
/** 查询客户可核销账单(已确认/部分核销),按账单日期升序,供余额核销选择 */
|
||||
/**
|
||||
* 查询客户可核销账单(已确认/部分核销),按账单日期升序,供余额核销选择
|
||||
*/
|
||||
List<ArBill> selectUnsettledByCustomer(@Param("customerId") Long customerId);
|
||||
|
||||
int insertArBill(ArBill bill);
|
||||
|
||||
int updateArBillStatus(@Param("arBillId") Long arBillId, @Param("status") String status,
|
||||
@Param("remark") String remark, @Param("operator") String operator,
|
||||
@Param("fromStatus") String fromStatus);
|
||||
@Param("remark") String remark, @Param("operator") String operator,
|
||||
@Param("fromStatus") String fromStatus);
|
||||
|
||||
int updateArBillAmounts(@Param("arBillId") Long arBillId,
|
||||
@Param("writtenOffAmount") BigDecimal writtenOffAmount,
|
||||
@Param("unwrittenAmount") BigDecimal unwrittenAmount,
|
||||
@Param("status") String status, @Param("operator") String operator,
|
||||
@Param("fromStatus") String fromStatus);
|
||||
@Param("writtenOffAmount") BigDecimal writtenOffAmount,
|
||||
@Param("unwrittenAmount") BigDecimal unwrittenAmount,
|
||||
@Param("status") String status, @Param("operator") String operator,
|
||||
@Param("fromStatus") String fromStatus);
|
||||
|
||||
int updateArBillProgress(@Param("arBillId") Long arBillId,
|
||||
@Param("orderCount") Integer orderCount, @Param("totalAmount") BigDecimal totalAmount,
|
||||
@Param("unwrittenAmount") BigDecimal unwrittenAmount,
|
||||
@Param("billPeriodStart") Date billPeriodStart, @Param("operator") String operator);
|
||||
|
||||
int updateArBillGenerationStatus(@Param("arBillId") Long arBillId,
|
||||
@Param("generationStatus") String generationStatus, @Param("operator") String operator,
|
||||
@Param("remark") String remark);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ruoyi.ar.mapper;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.ar.domain.ArExportTemplate;
|
||||
|
||||
public interface ArExportTemplateMapper {
|
||||
/**
|
||||
* 查询指定业务类型当前生效的默认模板。
|
||||
*/
|
||||
ArExportTemplate selectDefaultEnabled(@Param("businessType") String businessType,
|
||||
@Param("effectiveDate") Date effectiveDate);
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
package com.ruoyi.ar.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.ruoyi.ar.domain.ArBillLog;
|
||||
import com.ruoyi.ar.enums.ArLogObjectType;
|
||||
import com.ruoyi.ar.enums.ArLogOperationType;
|
||||
|
||||
public interface IArBillLogService
|
||||
{
|
||||
public interface IArBillLogService {
|
||||
List<ArBillLog> selectLogList(ArBillLog query);
|
||||
|
||||
void record(ArBillLog log);
|
||||
|
||||
void recordFailure(Long arBillId, ArLogObjectType objectType, String objectId, ArLogOperationType operationType,
|
||||
String fromStatus, String message, String operator, String requestId);
|
||||
String fromStatus, String message, String operator, String requestId);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
package com.ruoyi.ar.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.ruoyi.ar.domain.ArBill;
|
||||
import com.ruoyi.ar.domain.ArBankAccount;
|
||||
import com.ruoyi.ar.domain.ArBillDetail;
|
||||
import com.ruoyi.ar.domain.ArBillDetailFee;
|
||||
import com.ruoyi.ar.domain.ArBillExport;
|
||||
import com.ruoyi.ar.domain.ArBillGenerateTask;
|
||||
import com.ruoyi.ar.domain.ArBillLog;
|
||||
import com.ruoyi.ar.domain.vo.ArBillGenerateRequest;
|
||||
import com.ruoyi.ar.domain.vo.ArBillGenerateQuery;
|
||||
@@ -11,17 +16,48 @@ import com.ruoyi.ar.domain.vo.ArBillActionRequest;
|
||||
import com.ruoyi.ar.domain.vo.ArBillQuery;
|
||||
import com.ruoyi.price.domain.PrcOrderBill;
|
||||
|
||||
/**
|
||||
* 应收账单业务接口。
|
||||
* 查询接口面向列表/详情页面,生成和导出接口面向异步任务执行器;
|
||||
* Controller 不直接操作 Mapper,统一由这里维护业务状态和事务边界。
|
||||
*/
|
||||
public interface IArBillService
|
||||
{
|
||||
List<ArBill> selectArBillList(ArBillQuery query);
|
||||
|
||||
ArBill selectArBillById(Long arBillId);
|
||||
|
||||
List<ArBillDetail> selectArBillDetails(Long arBillId);
|
||||
|
||||
List<ArBillDetailFee> selectArBillDetailFees(Long arBillId, Long detailId);
|
||||
|
||||
ArBankAccount getDefaultBankAccount(String currency);
|
||||
|
||||
List<PrcOrderBill> previewBill(ArBillGenerateQuery query);
|
||||
|
||||
ArBill generateBill(ArBillGenerateRequest request, String operator, String requestId);
|
||||
BigDecimal sumPreviewBill(ArBillGenerateQuery query);
|
||||
|
||||
ArBillGenerateTask generateBill(ArBillGenerateRequest request, String operator, String requestId);
|
||||
|
||||
ArBillGenerateTask selectGenerateTask(Long taskId);
|
||||
|
||||
List<ArBillGenerateTask> selectActiveGenerateTasks(String operator);
|
||||
|
||||
ArBillGenerateTask cancelGenerateTask(Long taskId, String operator, String requestId);
|
||||
|
||||
ArBillGenerateTask retryGenerateTask(Long taskId, String operator, String requestId);
|
||||
|
||||
void processGenerateTask(Long taskId);
|
||||
|
||||
boolean claimGenerateTask(Long taskId);
|
||||
|
||||
void failGenerateTask(Long taskId, String remark);
|
||||
|
||||
int recoverExpiredGenerateTasks(Date expiredBefore);
|
||||
|
||||
int recoverCancelRequestedGenerateTasks(Date expiredBefore);
|
||||
|
||||
List<ArBillGenerateTask> selectWaitingGenerateTasks(int limit);
|
||||
|
||||
ArBill confirmBill(Long arBillId, ArBillActionRequest request, String operator, String requestId);
|
||||
|
||||
@@ -29,6 +65,14 @@ public interface IArBillService
|
||||
|
||||
ArBillExport exportBill(Long arBillId, String operator, String requestId);
|
||||
|
||||
ArBillExport selectExport(Long exportId);
|
||||
|
||||
void processExportTask(Long exportId);
|
||||
|
||||
void failExportTask(Long exportId, String remark);
|
||||
|
||||
int failExpiredExportTasks(Date expiredBefore);
|
||||
|
||||
List<ArBillExport> selectExports(Long arBillId);
|
||||
|
||||
void downloadExport(Long exportId, jakarta.servlet.http.HttpServletResponse response) throws Exception;
|
||||
|
||||
+51
-59
@@ -7,6 +7,7 @@ import java.time.format.DateTimeFormatter;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -39,8 +40,7 @@ import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow;
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
@Service
|
||||
public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService
|
||||
{
|
||||
public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService {
|
||||
private static final DateTimeFormatter NO_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
|
||||
private static final int MONEY_SCALE = 2;
|
||||
|
||||
@@ -56,55 +56,55 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService
|
||||
@Autowired
|
||||
private ICustomerAccountApi customerAccountApi;
|
||||
|
||||
/**
|
||||
* 按客户查询允许余额核销的未结清账单。
|
||||
*/
|
||||
@Override
|
||||
public List<ArBill> selectUnsettledBills(Long customerId)
|
||||
{
|
||||
if (customerId == null)
|
||||
{
|
||||
public List<ArBill> selectUnsettledBills(Long customerId) {
|
||||
if (customerId == null) {
|
||||
return List.of();
|
||||
}
|
||||
return billMapper.selectUnsettledByCustomer(customerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询账单余额核销记录列表。
|
||||
*/
|
||||
@Override
|
||||
public List<ArBalanceWriteoff> selectWriteoffList(ArBalanceWriteoff query)
|
||||
{
|
||||
public List<ArBalanceWriteoff> selectWriteoffList(ArBalanceWriteoff query) {
|
||||
return writeoffMapper.selectWriteoffList(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行余额核销主流程:锁账单、登记核销单、扣减客户余额、更新账单金额和状态。
|
||||
* 任一步失败都会回滚当前事务,避免核销单、资金流水和账单状态不一致。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ArBalanceWriteoff writeoff(ArBalanceWriteoff cmd)
|
||||
{
|
||||
if (cmd == null || cmd.getArBillId() == null)
|
||||
{
|
||||
public ArBalanceWriteoff writeoff(ArBalanceWriteoff cmd) {
|
||||
if (cmd == null || cmd.getArBillId() == null) {
|
||||
throw new ServiceException("请选择要核销的账单");
|
||||
}
|
||||
BigDecimal amount = cmd.getWriteoffAmount();
|
||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new ServiceException("核销金额必须大于0");
|
||||
}
|
||||
amount = amount.setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
|
||||
// 1. 行锁锁定账单(加锁顺序:账单 -> 账户,与反核销保持一致)
|
||||
ArBill bill = billMapper.selectArBillByIdForUpdate(cmd.getArBillId());
|
||||
if (bill == null)
|
||||
{
|
||||
if (bill == null) {
|
||||
throw new ServiceException("应收账单不存在");
|
||||
}
|
||||
ArBillStatus status = ArBillStatus.of(bill.getStatus());
|
||||
if (status == null || !status.canWriteOff())
|
||||
{
|
||||
if (status == null || !status.canWriteOff()) {
|
||||
throw new ServiceException("当前账单状态不允许核销:" + (status == null ? bill.getStatus() : status.getLabel()));
|
||||
}
|
||||
if (cmd.getCustomerId() != null && !cmd.getCustomerId().equals(bill.getCustomerId()))
|
||||
{
|
||||
if (cmd.getCustomerId() != null && !cmd.getCustomerId().equals(bill.getCustomerId())) {
|
||||
throw new ServiceException("账单与所选客户不匹配");
|
||||
}
|
||||
BigDecimal unwritten = nvl(bill.getUnwrittenAmount());
|
||||
if (amount.compareTo(unwritten) > 0)
|
||||
{
|
||||
if (amount.compareTo(unwritten) > 0) {
|
||||
throw new ServiceException("核销金额超过账单未核销金额:" + unwritten.toPlainString());
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService
|
||||
|
||||
// 3. 扣减账户余额并释放等额已用额度,写 BILL_PAY 扣款流水(余额不足在此处拦截)
|
||||
String flowRemark = "余额核销账单:" + bill.getBillNo()
|
||||
+ (StringUtils.hasText(writeoff.getRemark()) ? "(" + writeoff.getRemark() + ")" : "");
|
||||
+ (StringUtils.hasText(writeoff.getRemark()) ? "(" + writeoff.getRemark() + ")" : "");
|
||||
BalanceChangeCommand payCmd = new BalanceChangeCommand();
|
||||
payCmd.setCustomerId(bill.getCustomerId());
|
||||
payCmd.setCurrency(currency);
|
||||
@@ -147,40 +147,38 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService
|
||||
BigDecimal newUnwritten = unwritten.subtract(amount);
|
||||
String nextStatus = billStatus(written, nvl(bill.getTotalAmount()), fromStatus);
|
||||
if (billMapper.updateArBillAmounts(bill.getArBillId(), written, newUnwritten,
|
||||
nextStatus, operator, fromStatus) != 1)
|
||||
{
|
||||
nextStatus, operator, fromStatus) != 1) {
|
||||
throw new ServiceException("账单状态已发生变化,请刷新后重试");
|
||||
}
|
||||
|
||||
recordLog(bill.getArBillId(), writeoff.getWriteoffId(), ArLogOperationType.BALANCE_WRITEOFF,
|
||||
"账户余额核销账单 " + amount.toPlainString() + " " + currency,
|
||||
fromStatus, nextStatus, operator, requestId);
|
||||
"账户余额核销账单 " + amount.toPlainString() + " " + currency,
|
||||
fromStatus, nextStatus, operator, requestId);
|
||||
writeoff.setAccountId(flow.getAccountId());
|
||||
writeoff.setBalanceFlowId(flow.getId());
|
||||
return writeoff;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行反核销主流程:锁定核销单和账单、回补客户余额、恢复账单未核销金额,
|
||||
* 最后将原核销记录标记为已取消。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void cancelWriteoff(Long writeoffId, String remark)
|
||||
{
|
||||
if (writeoffId == null)
|
||||
{
|
||||
public void cancelWriteoff(Long writeoffId, String remark) {
|
||||
if (writeoffId == null) {
|
||||
throw new ServiceException("核销记录不存在");
|
||||
}
|
||||
// 1. 锁核销单 -> 锁账单(与核销相同的加锁顺序)
|
||||
ArBalanceWriteoff writeoff = writeoffMapper.selectByIdForUpdate(writeoffId);
|
||||
if (writeoff == null)
|
||||
{
|
||||
if (writeoff == null) {
|
||||
throw new ServiceException("核销记录不存在");
|
||||
}
|
||||
if (!ArBalanceWriteoffStatus.EFFECTIVE.getCode().equals(writeoff.getStatus()))
|
||||
{
|
||||
if (!ArBalanceWriteoffStatus.EFFECTIVE.getCode().equals(writeoff.getStatus())) {
|
||||
throw new ServiceException("只有有效状态的核销记录可以反核销");
|
||||
}
|
||||
ArBill bill = billMapper.selectArBillByIdForUpdate(writeoff.getArBillId());
|
||||
if (bill == null)
|
||||
{
|
||||
if (bill == null) {
|
||||
throw new ServiceException("核销关联的账单不存在");
|
||||
}
|
||||
|
||||
@@ -204,44 +202,40 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService
|
||||
|
||||
// 3. 账单核销金额回退,状态按剩余已核销额重算(归零则回到已确认)
|
||||
BigDecimal written = nvl(bill.getWrittenOffAmount()).subtract(nvl(writeoff.getWriteoffAmount()));
|
||||
if (written.compareTo(BigDecimal.ZERO) < 0)
|
||||
{
|
||||
if (written.compareTo(BigDecimal.ZERO) < 0) {
|
||||
written = BigDecimal.ZERO;
|
||||
}
|
||||
BigDecimal newUnwritten = nvl(bill.getTotalAmount()).subtract(written);
|
||||
String nextStatus = billStatus(written, nvl(bill.getTotalAmount()), fromStatus);
|
||||
if (billMapper.updateArBillAmounts(bill.getArBillId(), written, newUnwritten,
|
||||
nextStatus, operator, fromStatus) != 1)
|
||||
{
|
||||
nextStatus, operator, fromStatus) != 1) {
|
||||
throw new ServiceException("账单状态已发生变化,请刷新后重试");
|
||||
}
|
||||
|
||||
// 4. 核销单置为CANCELLED并登记回补流水
|
||||
if (writeoffMapper.updateCancelled(writeoffId, reverseFlow.getId(), operator, cancelRemark,
|
||||
ArBalanceWriteoffStatus.CANCELLED.getCode(), ArBalanceWriteoffStatus.EFFECTIVE.getCode()) != 1)
|
||||
{
|
||||
ArBalanceWriteoffStatus.CANCELLED.getCode(), ArBalanceWriteoffStatus.EFFECTIVE.getCode()) != 1) {
|
||||
throw new ServiceException("核销记录状态已变化,请刷新后重试");
|
||||
}
|
||||
recordLog(bill.getArBillId(), writeoffId, ArLogOperationType.CANCEL_BALANCE_WRITEOFF,
|
||||
"反核销回补账单:" + cancelRemark,
|
||||
fromStatus, nextStatus, operator, requestId);
|
||||
"反核销回补账单:" + cancelRemark,
|
||||
fromStatus, nextStatus, operator, requestId);
|
||||
}
|
||||
|
||||
/** 账单状态重算:已核销归零(非草稿)-> 已确认;足额 -> 已核销;否则部分核销 */
|
||||
private String billStatus(BigDecimal written, BigDecimal total, String current)
|
||||
{
|
||||
if (written.compareTo(BigDecimal.ZERO) == 0)
|
||||
{
|
||||
/**
|
||||
* 账单状态重算:归零(非草稿)为已确认,足额为已核销,否则为部分核销。
|
||||
*/
|
||||
private String billStatus(BigDecimal written, BigDecimal total, String current) {
|
||||
if (written.compareTo(BigDecimal.ZERO) == 0) {
|
||||
return ArBillStatus.DRAFT.getCode().equals(current)
|
||||
? ArBillStatus.DRAFT.getCode() : ArBillStatus.CONFIRMED.getCode();
|
||||
? ArBillStatus.DRAFT.getCode() : ArBillStatus.CONFIRMED.getCode();
|
||||
}
|
||||
return written.compareTo(total) >= 0
|
||||
? ArBillStatus.WRITTEN_OFF.getCode() : ArBillStatus.PARTIAL_WRITTEN_OFF.getCode();
|
||||
? ArBillStatus.WRITTEN_OFF.getCode() : ArBillStatus.PARTIAL_WRITTEN_OFF.getCode();
|
||||
}
|
||||
|
||||
private void recordLog(Long billId, Long writeoffId, ArLogOperationType operationType, String desc,
|
||||
String fromStatus, String toStatus, String operator, String requestId)
|
||||
{
|
||||
String fromStatus, String toStatus, String operator, String requestId) {
|
||||
ArBillLog log = new ArBillLog();
|
||||
log.setArBillId(billId);
|
||||
log.setObjectType(ArLogObjectType.BALANCE_WRITEOFF.getCode());
|
||||
@@ -258,14 +252,12 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService
|
||||
logService.record(log);
|
||||
}
|
||||
|
||||
private String generateNo()
|
||||
{
|
||||
private String generateNo() {
|
||||
return "BWO" + LocalDateTime.now().format(NO_FORMATTER)
|
||||
+ ThreadLocalRandom.current().nextInt(100, 1000);
|
||||
+ ThreadLocalRandom.current().nextInt(100, 1000);
|
||||
}
|
||||
|
||||
private BigDecimal nvl(BigDecimal value)
|
||||
{
|
||||
private BigDecimal nvl(BigDecimal value) {
|
||||
return value == null ? BigDecimal.ZERO : value;
|
||||
}
|
||||
}
|
||||
|
||||
+8
-9
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.ar.service.impl;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
@@ -15,29 +16,27 @@ import com.ruoyi.ar.service.IArBillLogService;
|
||||
import com.ruoyi.common.utils.uuid.IdUtils;
|
||||
|
||||
@Service
|
||||
public class ArBillLogServiceImpl implements IArBillLogService
|
||||
{
|
||||
public class ArBillLogServiceImpl implements IArBillLogService {
|
||||
@Autowired
|
||||
private ArBillLogMapper logMapper;
|
||||
|
||||
@Override
|
||||
public List<ArBillLog> selectLogList(ArBillLog query)
|
||||
{
|
||||
public List<ArBillLog> selectLogList(ArBillLog query) {
|
||||
return logMapper.selectList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void record(ArBillLog log)
|
||||
{
|
||||
public void record(ArBillLog log) {
|
||||
logMapper.insert(log);
|
||||
}
|
||||
|
||||
/** 失败日志必须独立提交,不能随业务异常一起回滚。 */
|
||||
/**
|
||||
* 失败日志必须独立提交,不能随业务异常一起回滚。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public void recordFailure(Long arBillId, ArLogObjectType objectType, String objectId, ArLogOperationType operationType,
|
||||
String fromStatus, String message, String operator, String requestId)
|
||||
{
|
||||
String fromStatus, String message, String operator, String requestId) {
|
||||
ArBillLog log = new ArBillLog();
|
||||
log.setArBillId(arBillId);
|
||||
log.setObjectType(objectType.getCode());
|
||||
|
||||
+1234
-493
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
package com.ruoyi.ar.task;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.task.TaskRejectedException;
|
||||
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.ruoyi.ar.event.ArBillExportTaskEvent;
|
||||
import com.ruoyi.ar.service.IArBillService;
|
||||
|
||||
/**
|
||||
* 应收账单导出异步任务执行器。
|
||||
* 导出文件写入服务器临时目录,Web 请求只负责提交任务,完成后再提供下载。
|
||||
*/
|
||||
@Component
|
||||
public class ArBillExportTaskRunner {
|
||||
private static final Logger log = LoggerFactory.getLogger(ArBillExportTaskRunner.class);
|
||||
|
||||
/**
|
||||
* 导出超过 60 分钟仍未完成时,视为异常任务。
|
||||
*/
|
||||
private static final long PROCESSING_TIMEOUT_MILLIS = 60L * 60L * 1000L;
|
||||
|
||||
@Autowired
|
||||
private IArBillService billService;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("arBillExportTaskExecutor")
|
||||
private ThreadPoolTaskExecutor exportTaskExecutor;
|
||||
|
||||
/**
|
||||
* 仅在提交导出任务的事务成功提交后异步执行,避免读取到未提交数据。
|
||||
*/
|
||||
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
||||
/** 事务提交后投递到导出专用线程池,避免占用请求线程。 */
|
||||
public void run(ArBillExportTaskEvent event) {
|
||||
try {
|
||||
exportTaskExecutor.execute(() -> billService.processExportTask(event.getExportId()));
|
||||
} catch (TaskRejectedException e) {
|
||||
billService.failExportTask(event.getExportId(), "导出任务较多,请稍后重新发起导出");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定期关闭因应用中断等原因长期未完成的导出任务。
|
||||
*/
|
||||
@Scheduled(cron = "0 0/5 * * * ?")
|
||||
public void failExpiredTasks() {
|
||||
Date expiredBefore = new Date(System.currentTimeMillis() - PROCESSING_TIMEOUT_MILLIS);
|
||||
int count = billService.failExpiredExportTasks(expiredBefore);
|
||||
if (count > 0) {
|
||||
log.warn("已关闭 {} 个超时的应收账单导出任务", count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.ruoyi.ar.task;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.task.TaskRejectedException;
|
||||
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.ruoyi.ar.domain.ArBillGenerateTask;
|
||||
import com.ruoyi.ar.event.ArBillGenerateTaskEvent;
|
||||
import com.ruoyi.ar.service.IArBillService;
|
||||
|
||||
/**
|
||||
* 应收账单异步生成任务调度器。
|
||||
*
|
||||
* <p>事务提交后立即尝试投递任务;定时补偿负责恢复节点异常、清理超时取消任务,
|
||||
* 并重新投递仍处于 WAITING 状态的任务。</p>
|
||||
*/
|
||||
@Component
|
||||
public class ArBillGenerateTaskRunner {
|
||||
private static final Logger log = LoggerFactory.getLogger(ArBillGenerateTaskRunner.class);
|
||||
/**
|
||||
* 生成任务超过 15 分钟没有心跳,认为执行节点异常并恢复为待处理。
|
||||
*/
|
||||
private static final long PROCESSING_TIMEOUT_MILLIS = 15L * 60L * 1000L;
|
||||
/**
|
||||
* 取消请求超过 5 分钟没有完成,交由补偿逻辑清理账单和明细。
|
||||
*/
|
||||
private static final long CANCELLATION_TIMEOUT_MILLIS = 5L * 60L * 1000L;
|
||||
|
||||
@Autowired
|
||||
private IArBillService billService;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("arBillGenerateTaskExecutor")
|
||||
private ThreadPoolTaskExecutor generateTaskExecutor;
|
||||
|
||||
/**
|
||||
* 账单生成事务提交后触发,避免事务回滚时提前执行异步任务。
|
||||
*/
|
||||
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
||||
public void run(ArBillGenerateTaskEvent event) {
|
||||
submit(event.getTaskId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 每 30 秒执行一次任务恢复、取消清理和待处理任务投递。
|
||||
*/
|
||||
@Scheduled(cron = "0/30 * * * * ?")
|
||||
public void recoverAndSubmitTasks() {
|
||||
try {
|
||||
Date expiredBefore = new Date(System.currentTimeMillis() - PROCESSING_TIMEOUT_MILLIS);
|
||||
int recovered = billService.recoverExpiredGenerateTasks(expiredBefore);
|
||||
if (recovered > 0) {
|
||||
log.warn("已恢复 {} 个超时的应收账单生成任务", recovered);
|
||||
}
|
||||
|
||||
Date cancellationExpiredBefore = new Date(System.currentTimeMillis() - CANCELLATION_TIMEOUT_MILLIS);
|
||||
int canceled = billService.recoverCancelRequestedGenerateTasks(cancellationExpiredBefore);
|
||||
if (canceled > 0) {
|
||||
log.warn("已自动清理 {} 个超时的应收账单取消任务", canceled);
|
||||
}
|
||||
|
||||
for (ArBillGenerateTask task : billService.selectWaitingGenerateTasks(5)) {
|
||||
submit(task.getTaskId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("应收账单生成任务调度失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 先用数据库条件更新抢占任务,再提交到有界线程池,避免重复执行。
|
||||
*/
|
||||
private void submit(Long taskId) {
|
||||
try {
|
||||
if (!billService.claimGenerateTask(taskId)) {
|
||||
return;
|
||||
}
|
||||
generateTaskExecutor.execute(() -> billService.processGenerateTask(taskId));
|
||||
} catch (TaskRejectedException e) {
|
||||
log.error("应收账单生成任务提交线程池失败,taskId={}", taskId, e);
|
||||
try {
|
||||
billService.failGenerateTask(taskId, "生成任务较多,请稍后重新发起");
|
||||
} catch (Exception cleanupException) {
|
||||
log.error("线程池拒绝后,清理应收账单生成任务失败,taskId={}", taskId, cleanupException);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("应收账单生成任务提交失败,taskId={}", taskId, e);
|
||||
try {
|
||||
billService.failGenerateTask(taskId, "生成任务提交失败,请稍后重新发起");
|
||||
} catch (Exception cleanupException) {
|
||||
log.error("任务提交异常后,清理应收账单生成任务失败,taskId={}", taskId, cleanupException);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,12 +20,36 @@ public interface PrcOrderBillMapper
|
||||
|
||||
/** 应收账单生成读取指定客户截止日期前的未出账订单计费快照。 */
|
||||
public List<PrcOrderBill> selectPrcOrderBillByCustomerPeriod(@Param("customerId") Long customerId,
|
||||
@Param("customerCode") String customerCode, @Param("cutoffDate") Date cutoffDate,
|
||||
@Param("currency") String currency, @Param("arBillStatus") String arBillStatus);
|
||||
@Param("customerCode") String customerCode, @Param("orderOrTrackingNo") String orderOrTrackingNo,
|
||||
@Param("excludeOrderOrTrackingNo") String excludeOrderOrTrackingNo,
|
||||
@Param("cutoffDate") Date cutoffDate, @Param("currency") String currency,
|
||||
@Param("arBillStatus") String arBillStatus, @Param("excludedBillIds") List<String> excludedBillIds);
|
||||
|
||||
/** 统计应收账单预览结果金额,供全选模式展示。 */
|
||||
BigDecimal sumPrcOrderBillIncomeByCustomerPeriod(@Param("customerId") Long customerId,
|
||||
@Param("customerCode") String customerCode, @Param("orderOrTrackingNo") String orderOrTrackingNo,
|
||||
@Param("excludeOrderOrTrackingNo") String excludeOrderOrTrackingNo,
|
||||
@Param("cutoffDate") Date cutoffDate, @Param("currency") String currency,
|
||||
@Param("arBillStatus") String arBillStatus);
|
||||
|
||||
/** 异步生成账单按 bill_id 游标读取一批来源订单。 */
|
||||
List<PrcOrderBill> selectPrcOrderBillByCustomerPeriodCursor(@Param("customerId") Long customerId,
|
||||
@Param("customerCode") String customerCode, @Param("orderOrTrackingNo") String orderOrTrackingNo,
|
||||
@Param("cutoffDate") Date cutoffDate, @Param("currency") String currency,
|
||||
@Param("arBillStatus") String arBillStatus, @Param("snapshotCreateTime") Date snapshotCreateTime,
|
||||
@Param("lastSourceBillId") String lastSourceBillId, @Param("billIds") List<String> billIds,
|
||||
@Param("excludedBillIds") List<String> excludedBillIds, @Param("limit") int limit);
|
||||
|
||||
int countPrcOrderBillByCustomerPeriod(@Param("customerId") Long customerId,
|
||||
@Param("customerCode") String customerCode, @Param("orderOrTrackingNo") String orderOrTrackingNo,
|
||||
@Param("cutoffDate") Date cutoffDate, @Param("currency") String currency,
|
||||
@Param("arBillStatus") String arBillStatus, @Param("snapshotCreateTime") Date snapshotCreateTime,
|
||||
@Param("billIds") List<String> billIds, @Param("excludedBillIds") List<String> excludedBillIds);
|
||||
|
||||
/** 按生成账单时用户勾选的订单读取,并再次校验账单筛选条件。 */
|
||||
public List<PrcOrderBill> selectPrcOrderBillByCustomerCutoffAndIds(@Param("customerId") Long customerId,
|
||||
@Param("customerCode") String customerCode, @Param("cutoffDate") Date cutoffDate,
|
||||
@Param("customerCode") String customerCode, @Param("orderOrTrackingNo") String orderOrTrackingNo,
|
||||
@Param("cutoffDate") Date cutoffDate,
|
||||
@Param("currency") String currency, @Param("billIds") List<String> billIds,
|
||||
@Param("arBillStatus") String arBillStatus);
|
||||
|
||||
@@ -37,6 +61,9 @@ public interface PrcOrderBillMapper
|
||||
public int releaseArBillOrders(@Param("billIds") List<String> billIds, @Param("operator") String operator,
|
||||
@Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus);
|
||||
|
||||
int releaseArBillOrdersByArBillId(@Param("arBillId") Long arBillId, @Param("operator") String operator,
|
||||
@Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus);
|
||||
|
||||
public int insertPrcOrderBill(PrcOrderBill bill);
|
||||
|
||||
public int updatePrcOrderBill(PrcOrderBill bill);
|
||||
|
||||
Reference in New Issue
Block a user