应收账单模块,调整枚举代码优化

This commit is contained in:
caojia
2026-09-17 11:00:59 +08:00
parent 5207c00574
commit a17f09683b
26 changed files with 397 additions and 140 deletions
@@ -22,11 +22,6 @@ public class ArBalanceWriteoff extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 有效 */
public static final String STATUS_EFFECTIVE = "EFFECTIVE";
/** 已取消(反核销) */
public static final String STATUS_CANCELLED = "CANCELLED";
/** 余额核销ID */
private Long writeoffId;
@@ -61,7 +56,7 @@ public class ArBalanceWriteoff extends BaseEntity
@JsonFormat(pattern = "yyyy-MM-dd")
private Date writeoffDate;
/** 状态:EFFECTIVE/CANCELLED */
/** 状态:EFFECTIVE(有效)/CANCELLED(已反核销),取值由 ArBalanceWriteoffStatus 维护。 */
private String status;
/** 核销备注 */
@@ -36,6 +36,7 @@ public class ArBill extends BaseEntity
private BigDecimal totalAmount;
private BigDecimal writtenOffAmount;
private BigDecimal unwrittenAmount;
/** 状态:DRAFT草稿、CONFIRMED已确认、PARTIAL_WRITTEN_OFF部分核销、WRITTEN_OFF已核销、VOID已作废。 */
private String status;
private Integer billVersion;
private String bankAccountName;
@@ -21,6 +21,7 @@ public class ArBillExport implements Serializable
private Long fileSize;
private String fileHash;
private Integer rowCount;
/** 导出状态:PROCESSING生成中、SUCCESS成功可下载、FAILED失败。 */
private String exportStatus;
private String exportBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@@ -13,10 +13,12 @@ public class ArBillLog implements Serializable
private Long logId;
private Long arBillId;
/** 对象类型:BILL应收账单、EXPORT账单导出、BALANCE_WRITEOFF余额核销。 */
private String objectType;
private String objectId;
/** 对象业务标识,例如账单号、收款单号或核销单号。 */
private String objectDisplay;
/** 操作类型,取值由 ArLogOperationType 维护。 */
private String operationType;
private String operationDesc;
private String fromStatus;
@@ -25,9 +27,11 @@ public class ArBillLog implements Serializable
private String afterData;
private String operatorId;
private String operatorName;
/** 操作来源:WEB网页端、API接口、JOB定时任务、IMPORT数据导入。 */
private String operationSource;
private String requestId;
private String traceId;
/** 操作结果:SUCCESS成功、FAILED失败。 */
private String result;
private String errorMessage;
private String clientIp;
@@ -0,0 +1,50 @@
package com.ruoyi.ar.enums;
/**
* 应收账单余额核销单状态。
*
* <p>数据库保存英文编码,页面通过前端枚举展示中文名称。</p>
*/
public enum ArBalanceWriteoffStatus
{
/** 核销已生效,余额已扣减。 */
EFFECTIVE("EFFECTIVE", "有效"),
/** 核销已取消,余额已回补。 */
CANCELLED("CANCELLED", "已反核销");
private final String code;
private final String label;
ArBalanceWriteoffStatus(String code, String label)
{
this.code = code;
this.label = label;
}
public String getCode()
{
return code;
}
public String getLabel()
{
return label;
}
public boolean isEffective()
{
return this == EFFECTIVE;
}
public static ArBalanceWriteoffStatus of(String code)
{
for (ArBalanceWriteoffStatus status : values())
{
if (status.code.equalsIgnoreCase(code))
{
return status;
}
}
return null;
}
}
@@ -1,21 +1,52 @@
package com.ruoyi.ar.enums;
/** 应收账单导出任务状态。 */
/**
* 应收账单导出任务状态。
*
* <p>PROCESSING 表示处理中,SUCCESS 表示生成成功,FAILED 表示生成失败。</p>
*/
public enum ArExportStatus
{
PROCESSING("PROCESSING"),
SUCCESS("SUCCESS"),
FAILED("FAILED");
/** 文件正在生成。 */
PROCESSING("PROCESSING", "生成中"),
/** 文件已生成,可以下载。 */
SUCCESS("SUCCESS", "成功"),
/** 文件生成失败。 */
FAILED("FAILED", "失败");
private final String code;
private final String label;
ArExportStatus(String code)
ArExportStatus(String code, String label)
{
this.code = code;
this.label = label;
}
public String getCode()
{
return code;
}
public String getLabel()
{
return label;
}
public boolean isSuccess()
{
return this == SUCCESS;
}
public static ArExportStatus of(String code)
{
for (ArExportStatus status : values())
{
if (status.code.equalsIgnoreCase(code))
{
return status;
}
}
return null;
}
}
@@ -0,0 +1,31 @@
package com.ruoyi.ar.enums;
/** 应收日志关联对象类型。 */
public enum ArLogObjectType
{
/** 应收账单主对象。 */
BILL("BILL", "应收账单"),
/** 账单导出文件对象。 */
EXPORT("EXPORT", "账单导出"),
/** 客户余额核销单对象。 */
BALANCE_WRITEOFF("BALANCE_WRITEOFF", "余额核销");
private final String code;
private final String label;
ArLogObjectType(String code, String label)
{
this.code = code;
this.label = label;
}
public String getCode()
{
return code;
}
public String getLabel()
{
return label;
}
}
@@ -0,0 +1,33 @@
package com.ruoyi.ar.enums;
/** 应收日志操作来源。 */
public enum ArLogOperationSource
{
/** 浏览器页面操作。 */
WEB("WEB", "网页端"),
/** 开放接口或系统间接口调用。 */
API("API", "接口"),
/** 定时任务或后台作业。 */
JOB("JOB", "定时任务"),
/** 数据导入操作。 */
IMPORT("IMPORT", "数据导入");
private final String code;
private final String label;
ArLogOperationSource(String code, String label)
{
this.code = code;
this.label = label;
}
public String getCode()
{
return code;
}
public String getLabel()
{
return label;
}
}
@@ -0,0 +1,39 @@
package com.ruoyi.ar.enums;
/** 应收日志操作类型。 */
public enum ArLogOperationType
{
/** 生成应收账单成功。 */
CREATE("CREATE", "生成账单"),
/** 生成账单前的校验或生成失败记录。 */
GENERATE("GENERATE", "生成账单校验"),
/** 确认草稿账单。 */
CONFIRM("CONFIRM", "确认账单"),
/** 作废账单并释放来源订单。 */
VOID("VOID", "作废账单"),
/** 生成账单导出文件。 */
EXPORT("EXPORT", "导出账单"),
/** 使用客户余额核销账单。 */
BALANCE_WRITEOFF("BALANCE_WRITEOFF", "余额核销"),
/** 取消余额核销并回补客户余额。 */
CANCEL_BALANCE_WRITEOFF("CANCEL_BALANCE_WRITEOFF", "取消余额核销");
private final String code;
private final String label;
ArLogOperationType(String code, String label)
{
this.code = code;
this.label = label;
}
public String getCode()
{
return code;
}
public String getLabel()
{
return label;
}
}
@@ -3,18 +3,39 @@ package com.ruoyi.ar.enums;
/** 应收操作日志结果。 */
public enum ArLogResult
{
SUCCESS("SUCCESS"),
FAILED("FAILED");
/** 操作执行成功。 */
SUCCESS("SUCCESS", "成功"),
/** 操作执行失败。 */
FAILED("FAILED", "失败");
private final String code;
private final String label;
ArLogResult(String code)
ArLogResult(String code, String label)
{
this.code = code;
this.label = label;
}
public String getCode()
{
return code;
}
public String getLabel()
{
return label;
}
public static ArLogResult of(String code)
{
for (ArLogResult result : values())
{
if (result.code.equalsIgnoreCase(code))
{
return result;
}
}
return null;
}
}
@@ -27,7 +27,9 @@ public interface ArBalanceWriteoffMapper
int updateCancelled(@Param("writeoffId") Long writeoffId,
@Param("reverseFlowId") Long reverseFlowId,
@Param("cancelBy") String cancelBy,
@Param("cancelRemark") String cancelRemark);
@Param("cancelRemark") String cancelRemark,
@Param("status") String status,
@Param("effectiveStatus") String effectiveStatus);
/** 查询核销记录列表(关联账单名称/客户名称) */
List<ArBalanceWriteoff> selectWriteoffList(ArBalanceWriteoff query);
@@ -9,7 +9,8 @@ public interface ArBillLogMapper
List<ArBillLog> selectList(ArBillLog query);
ArBillLog selectSuccessByRequestId(@Param("requestId") String requestId,
@Param("objectType") String objectType);
@Param("objectType") String objectType,
@Param("result") String result);
int insert(ArBillLog log);
}
@@ -2,6 +2,8 @@ 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
{
@@ -9,6 +11,6 @@ public interface IArBillLogService
void record(ArBillLog log);
void recordFailure(Long arBillId, String objectType, String objectId, String operationType,
void recordFailure(Long arBillId, ArLogObjectType objectType, String objectId, ArLogOperationType operationType,
String fromStatus, String message, String operator, String requestId);
}
@@ -14,7 +14,11 @@ import org.springframework.util.StringUtils;
import com.ruoyi.ar.domain.ArBalanceWriteoff;
import com.ruoyi.ar.domain.ArBill;
import com.ruoyi.ar.domain.ArBillLog;
import com.ruoyi.ar.enums.ArBalanceWriteoffStatus;
import com.ruoyi.ar.enums.ArBillStatus;
import com.ruoyi.ar.enums.ArLogObjectType;
import com.ruoyi.ar.enums.ArLogOperationSource;
import com.ruoyi.ar.enums.ArLogOperationType;
import com.ruoyi.ar.enums.ArLogResult;
import com.ruoyi.ar.mapper.ArBalanceWriteoffMapper;
import com.ruoyi.ar.mapper.ArBillMapper;
@@ -119,7 +123,7 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService
writeoff.setCurrency(currency);
writeoff.setWriteoffAmount(amount);
writeoff.setWriteoffDate(writeoffDate);
writeoff.setStatus(ArBalanceWriteoff.STATUS_EFFECTIVE);
writeoff.setStatus(ArBalanceWriteoffStatus.EFFECTIVE.getCode());
writeoff.setRemark(StringUtils.hasText(cmd.getRemark()) ? cmd.getRemark().trim() : null);
writeoff.setCreateBy(operator);
writeoffMapper.insertArBalanceWriteoff(writeoff);
@@ -148,7 +152,7 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService
throw new ServiceException("账单状态已发生变化,请刷新后重试");
}
recordLog(bill.getArBillId(), writeoff.getWriteoffId(), "BALANCE_WRITEOFF",
recordLog(bill.getArBillId(), writeoff.getWriteoffId(), ArLogOperationType.BALANCE_WRITEOFF,
"账户余额核销账单 " + amount.toPlainString() + " " + currency,
fromStatus, nextStatus, operator, requestId);
writeoff.setAccountId(flow.getAccountId());
@@ -170,7 +174,7 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService
{
throw new ServiceException("核销记录不存在");
}
if (!ArBalanceWriteoff.STATUS_EFFECTIVE.equals(writeoff.getStatus()))
if (!ArBalanceWriteoffStatus.EFFECTIVE.getCode().equals(writeoff.getStatus()))
{
throw new ServiceException("只有有效状态的核销记录可以反核销");
}
@@ -213,11 +217,12 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService
}
// 4. 核销单置为CANCELLED并登记回补流水
if (writeoffMapper.updateCancelled(writeoffId, reverseFlow.getId(), operator, cancelRemark) != 1)
if (writeoffMapper.updateCancelled(writeoffId, reverseFlow.getId(), operator, cancelRemark,
ArBalanceWriteoffStatus.CANCELLED.getCode(), ArBalanceWriteoffStatus.EFFECTIVE.getCode()) != 1)
{
throw new ServiceException("核销记录状态已变化,请刷新后重试");
}
recordLog(bill.getArBillId(), writeoffId, "CANCEL_BALANCE_WRITEOFF",
recordLog(bill.getArBillId(), writeoffId, ArLogOperationType.CANCEL_BALANCE_WRITEOFF,
"反核销回补账单:" + cancelRemark,
fromStatus, nextStatus, operator, requestId);
}
@@ -234,20 +239,20 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService
? ArBillStatus.WRITTEN_OFF.getCode() : ArBillStatus.PARTIAL_WRITTEN_OFF.getCode();
}
private void recordLog(Long billId, Long writeoffId, String operationType, String desc,
private void recordLog(Long billId, Long writeoffId, ArLogOperationType operationType, String desc,
String fromStatus, String toStatus, String operator, String requestId)
{
ArBillLog log = new ArBillLog();
log.setArBillId(billId);
log.setObjectType("BALANCE_WRITEOFF");
log.setObjectType(ArLogObjectType.BALANCE_WRITEOFF.getCode());
log.setObjectId(String.valueOf(writeoffId));
log.setOperationType(operationType);
log.setOperationType(operationType.getCode());
log.setOperationDesc(desc);
log.setFromStatus(fromStatus);
log.setToStatus(toStatus);
log.setOperatorId(operator);
log.setOperatorName(operator);
log.setOperationSource("WEB");
log.setOperationSource(ArLogOperationSource.WEB.getCode());
log.setRequestId(requestId);
log.setResult(ArLogResult.SUCCESS.getCode());
logService.record(log);
@@ -6,6 +6,9 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.ar.domain.ArBillLog;
import com.ruoyi.ar.enums.ArLogObjectType;
import com.ruoyi.ar.enums.ArLogOperationSource;
import com.ruoyi.ar.enums.ArLogOperationType;
import com.ruoyi.ar.enums.ArLogResult;
import com.ruoyi.ar.mapper.ArBillLogMapper;
import com.ruoyi.ar.service.IArBillLogService;
@@ -32,19 +35,19 @@ public class ArBillLogServiceImpl implements IArBillLogService
/** 失败日志必须独立提交,不能随业务异常一起回滚。 */
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void recordFailure(Long arBillId, String objectType, String objectId, String operationType,
public void recordFailure(Long arBillId, ArLogObjectType objectType, String objectId, ArLogOperationType operationType,
String fromStatus, String message, String operator, String requestId)
{
ArBillLog log = new ArBillLog();
log.setArBillId(arBillId);
log.setObjectType(objectType);
log.setObjectType(objectType.getCode());
log.setObjectId(objectId);
log.setOperationType(operationType);
log.setOperationType(operationType.getCode());
log.setOperationDesc(message);
log.setFromStatus(fromStatus);
log.setOperatorId(operator);
log.setOperatorName(operator);
log.setOperationSource("WEB");
log.setOperationSource(ArLogOperationSource.WEB.getCode());
log.setRequestId(requestId == null ? IdUtils.fastSimpleUUID() : requestId);
log.setResult(ArLogResult.FAILED.getCode());
log.setErrorMessage(message);
@@ -37,6 +37,9 @@ import com.ruoyi.ar.domain.ArBillExport;
import com.ruoyi.ar.domain.ArBillLog;
import com.ruoyi.ar.enums.ArBillStatus;
import com.ruoyi.ar.enums.ArExportStatus;
import com.ruoyi.ar.enums.ArLogObjectType;
import com.ruoyi.ar.enums.ArLogOperationSource;
import com.ruoyi.ar.enums.ArLogOperationType;
import com.ruoyi.ar.enums.ArLogResult;
import com.ruoyi.ar.enums.ArOrderBillStatus;
import com.ruoyi.ar.mapper.ArBillDetailFeeMapper;
@@ -135,7 +138,8 @@ public class ArBillServiceImpl implements IArBillService
public ArBill generateBill(ArBillGenerateRequest request, String operator, String requestId)
{
validatePeriod(request);
ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, "BILL");
ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, ArLogObjectType.BILL.getCode(),
ArLogResult.SUCCESS.getCode());
if (previous != null && isNumber(previous.getObjectId()))
{
return selectArBillById(Long.valueOf(previous.getObjectId()));
@@ -144,7 +148,8 @@ public class ArBillServiceImpl implements IArBillService
List<String> requestedSourceBillIds = normalizeSourceBillIds(request.getSourceBillIds());
if (requestedSourceBillIds.isEmpty())
{
throw failure(null, "BILL", "", "GENERATE", null, "请至少选择一条有效订单", operator, requestId);
throw failure(null, ArLogObjectType.BILL, "", ArLogOperationType.GENERATE,
null, "请至少选择一条有效订单", operator, requestId);
}
String requestedCurrency = StringUtils.isEmpty(request.getCurrency()) ? "CNY" : request.getCurrency();
List<PrcOrderBill> sources = sourceBillMapper.selectPrcOrderBillByCustomerCutoffAndIds(
@@ -152,7 +157,7 @@ public class ArBillServiceImpl implements IArBillService
requestedSourceBillIds, ArOrderBillStatus.UNBILLED.getCode());
if (sources == null || sources.isEmpty() || sources.size() != requestedSourceBillIds.size())
{
throw failure(null, "BILL", "", "GENERATE", null,
throw failure(null, ArLogObjectType.BILL, "", ArLogOperationType.GENERATE, null,
"部分所选订单已出账、已变更或不符合当前筛选条件,请重新查询后生成", operator, requestId);
}
@@ -162,7 +167,8 @@ public class ArBillServiceImpl implements IArBillService
{
if (source.getCurrency() != null && !currency.equalsIgnoreCase(source.getCurrency()))
{
throw failure(null, "BILL", "", "GENERATE", null, "截止日期内存在多个币种,不能合并为一张账单", operator, requestId);
throw failure(null, ArLogObjectType.BILL, "", ArLogOperationType.GENERATE,
null, "截止日期内存在多个币种,不能合并为一张账单", operator, requestId);
}
total = total.add(nvl(source.getIncomeTotal()));
}
@@ -170,7 +176,7 @@ public class ArBillServiceImpl implements IArBillService
ArBankAccount bankAccount = bankAccountMapper.selectDefaultByCurrency(currency);
if (bankAccount == null)
{
throw failure(null, "BILL", "", "GENERATE", null,
throw failure(null, ArLogObjectType.BILL, "", ArLogOperationType.GENERATE, null,
"未配置 " + currency + " 币种的公司默认收款账户,请先完成收款账户配置", operator, requestId);
}
@@ -206,7 +212,7 @@ public class ArBillServiceImpl implements IArBillService
ArOrderBillStatus.UNBILLED.getCode(), ArOrderBillStatus.BILLED.getCode());
if (claimedCount != sourceBillIds.size())
{
throw failure(null, "BILL", "", "GENERATE", null,
throw failure(null, ArLogObjectType.BILL, "", ArLogOperationType.GENERATE, null,
"部分计费订单已被其他账单占用,请重新查询后生成", operator, requestId);
}
@@ -249,7 +255,8 @@ public class ArBillServiceImpl implements IArBillService
{
detailFeeMapper.insertBatch(detailFees);
}
recordSuccess(bill, "CREATE", "生成正式账单", null, ArBillStatus.DRAFT.getCode(), requestId, operator);
recordSuccess(bill, ArLogObjectType.BILL, ArLogOperationType.CREATE,
"生成正式账单", null, ArBillStatus.DRAFT.getCode(), requestId, operator);
bill.setBankAccountNo(maskAccount(bill.getBankAccountNo()));
return bill;
}
@@ -261,26 +268,35 @@ public class ArBillServiceImpl implements IArBillService
ArBill bill = billMapper.selectArBillByIdForUpdate(arBillId);
if (bill == null)
{
throw failure(arBillId, "BILL", String.valueOf(arBillId), "CONFIRM", null, "应收账单不存在", operator, requestId);
throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.CONFIRM,
null, "应收账单不存在", operator, requestId);
}
ArBillStatus currentStatus = ArBillStatus.of(bill.getStatus());
if (currentStatus == null || !currentStatus.canConfirm())
{
throw failure(arBillId, "BILL", String.valueOf(arBillId), "CONFIRM", bill.getStatus(),
throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.CONFIRM,
bill.getStatus(),
"只有草稿账单可以确认,当前状态为 " + bill.getStatus(), operator, requestId);
}
if (!validateDetails(arBillId, bill.getTotalAmount()))
{
throw failure(arBillId, "BILL", String.valueOf(arBillId), "CONFIRM", bill.getStatus(),
throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.CONFIRM,
bill.getStatus(),
"账单费用明细汇总与订单或账单总额不一致", operator, requestId);
}
if (billMapper.updateArBillStatus(arBillId, ArBillStatus.CONFIRMED.getCode(),
StringUtils.isEmpty(request == null ? null : request.getRemark()) ? bill.getRemark() : request.getRemark(),
operator, ArBillStatus.DRAFT.getCode()) != 1)
{
throw failure(arBillId, "BILL", String.valueOf(arBillId), "CONFIRM", bill.getStatus(),
throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.CONFIRM,
bill.getStatus(),
"账单状态已发生变化,请刷新后重试", operator, requestId);
}
// 账单确认:应收形成,按账单金额占用客户授信额度(账户不存在时仅CNY会自动初始化)
customerMoneyService.occupyCredit(bill.getCustomerId(), bill.getCurrency(),
nvl(bill.getTotalAmount()));
recordSuccess(bill, ArLogObjectType.BILL, ArLogOperationType.CONFIRM,
"确认应收账单", ArBillStatus.DRAFT.getCode(), ArBillStatus.CONFIRMED.getCode(), requestId, operator);
// 账单确认:应收形成,按账单金额占用客户授信额度(账户不存在时仅CNY会自动初始化)。
// 不校验可用额度,兼容历史 used_credit 可高于总额度、外币账户静默跳过的口径
customerAccountApi.occupyCredit(buildCreditCommand(bill));
@@ -295,17 +311,20 @@ public class ArBillServiceImpl implements IArBillService
String remark = request == null ? null : request.getRemark();
if (StringUtils.isEmpty(remark))
{
throw failure(arBillId, "BILL", String.valueOf(arBillId), "VOID", null, "作废必须填写原因", operator, requestId);
throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.VOID,
null, "作废必须填写原因", operator, requestId);
}
ArBill bill = billMapper.selectArBillByIdForUpdate(arBillId);
if (bill == null)
{
throw failure(arBillId, "BILL", String.valueOf(arBillId), "VOID", null, "应收账单不存在", operator, requestId);
throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.VOID,
null, "应收账单不存在", operator, requestId);
}
ArBillStatus currentStatus = ArBillStatus.of(bill.getStatus());
if (currentStatus == null || !currentStatus.canVoid())
{
throw failure(arBillId, "BILL", String.valueOf(arBillId), "VOID", bill.getStatus(),
throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.VOID,
bill.getStatus(),
"当前状态不允许作废", operator, requestId);
}
List<String> sourceBillIds = new ArrayList<>();
@@ -318,13 +337,15 @@ public class ArBillServiceImpl implements IArBillService
}
if (billMapper.updateArBillStatus(arBillId, ArBillStatus.VOID.getCode(), remark, operator, currentStatus.getCode()) != 1)
{
throw failure(arBillId, "BILL", String.valueOf(arBillId), "VOID", bill.getStatus(),
throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.VOID,
bill.getStatus(),
"账单状态已发生变化,请刷新后重试", operator, requestId);
}
// 账单作废:已确认账单占用的授信额度全额释放(可作废的CONFIRMED账单必然零核销)
if (currentStatus == ArBillStatus.CONFIRMED)
{
customerAccountApi.releaseCredit(buildCreditCommand(bill));
customerMoneyService.releaseCredit(bill.getCustomerId(), bill.getCurrency(),
nvl(bill.getTotalAmount()));
}
if (!sourceBillIds.isEmpty())
{
@@ -332,18 +353,21 @@ public class ArBillServiceImpl implements IArBillService
ArOrderBillStatus.BILLED.getCode(), ArOrderBillStatus.UNBILLED.getCode());
if (releasedCount != sourceBillIds.size())
{
throw failure(arBillId, "BILL", String.valueOf(arBillId), "VOID", bill.getStatus(),
throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.VOID,
bill.getStatus(),
"部分来源订单状态异常,账单未作废,请刷新后重试", operator, requestId);
}
}
recordSuccess(bill, "VOID", "作废应收账单:" + remark, bill.getStatus(), ArBillStatus.VOID.getCode(), requestId, operator);
recordSuccess(bill, ArLogObjectType.BILL, ArLogOperationType.VOID,
"作废应收账单:" + remark, bill.getStatus(), ArBillStatus.VOID.getCode(), requestId, operator);
return selectArBillById(arBillId);
}
@Override
public ArBillExport exportBill(Long arBillId, String operator, String requestId)
{
ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, "EXPORT");
ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, ArLogObjectType.EXPORT.getCode(),
ArLogResult.SUCCESS.getCode());
if (previous != null && isNumber(previous.getObjectId()))
{
ArBillExport previousExport = exportMapper.selectById(Long.valueOf(previous.getObjectId()));
@@ -355,11 +379,13 @@ public class ArBillServiceImpl implements IArBillService
ArBill bill = billMapper.selectArBillById(arBillId);
if (bill == null)
{
throw failure(arBillId, "EXPORT", String.valueOf(arBillId), "EXPORT", null, "应收账单不存在", operator, requestId);
throw failure(arBillId, ArLogObjectType.EXPORT, String.valueOf(arBillId), ArLogOperationType.EXPORT,
null, "应收账单不存在", operator, requestId);
}
if (ArBillStatus.is(bill.getStatus(), ArBillStatus.DRAFT) || ArBillStatus.is(bill.getStatus(), ArBillStatus.VOID))
{
throw failure(arBillId, "EXPORT", String.valueOf(arBillId), "EXPORT", bill.getStatus(),
throw failure(arBillId, ArLogObjectType.EXPORT, String.valueOf(arBillId), ArLogOperationType.EXPORT,
bill.getStatus(),
"草稿或作废账单不可导出", operator, requestId);
}
fillDetails(bill);
@@ -385,7 +411,8 @@ public class ArBillServiceImpl implements IArBillService
export.setFileHash(sha256(data));
export.setExportStatus(ArExportStatus.SUCCESS.getCode());
exportMapper.insert(export);
recordSuccess(bill, "EXPORT", "导出账单 XLSX", bill.getStatus(), bill.getStatus(), requestId, operator, export.getExportId());
recordSuccess(bill, ArLogObjectType.EXPORT, ArLogOperationType.EXPORT,
"导出账单 XLSX", bill.getStatus(), bill.getStatus(), requestId, operator, export.getExportId());
return export;
}
catch (Exception e)
@@ -394,7 +421,8 @@ public class ArBillServiceImpl implements IArBillService
export.setExportStatus(ArExportStatus.FAILED.getCode());
export.setRemark(e.getMessage());
exportMapper.insert(export);
logService.recordFailure(arBillId, "EXPORT", String.valueOf(arBillId), "EXPORT", bill.getStatus(),
logService.recordFailure(arBillId, ArLogObjectType.EXPORT, String.valueOf(arBillId), ArLogOperationType.EXPORT,
bill.getStatus(),
e.getMessage(), operator, requestId);
throw new ServiceException("账单导出失败:" + e.getMessage());
}
@@ -513,20 +541,20 @@ public class ArBillServiceImpl implements IArBillService
return fee;
}
private void recordSuccess(ArBill bill, String operationType, String desc, String fromStatus,
private void recordSuccess(ArBill bill, ArLogObjectType objectType, ArLogOperationType operationType, String desc, String fromStatus,
String toStatus, String requestId, String operator)
{
recordSuccess(bill, operationType, desc, fromStatus, toStatus, requestId, operator, null);
recordSuccess(bill, objectType, operationType, desc, fromStatus, toStatus, requestId, operator, null);
}
private void recordSuccess(ArBill bill, String operationType, String desc, String fromStatus,
private void recordSuccess(ArBill bill, ArLogObjectType objectType, ArLogOperationType operationType, String desc, String fromStatus,
String toStatus, String requestId, String operator, Long objectId)
{
ArBillLog log = new ArBillLog();
log.setArBillId(bill.getArBillId());
log.setObjectType("BILL".equals(operationType) ? "BILL" : operationType.equals("EXPORT") ? "EXPORT" : "BILL");
log.setObjectType(objectType.getCode());
log.setObjectId(objectId == null ? String.valueOf(bill.getArBillId()) : String.valueOf(objectId));
log.setOperationType(operationType);
log.setOperationType(operationType.getCode());
log.setOperationDesc(desc);
log.setFromStatus(fromStatus);
log.setToStatus(toStatus);
@@ -534,13 +562,13 @@ public class ArBillServiceImpl implements IArBillService
log.setAfterData(safeStatusJson(toStatus));
log.setOperatorId(operator);
log.setOperatorName(operator);
log.setOperationSource("WEB");
log.setOperationSource(ArLogOperationSource.WEB.getCode());
log.setRequestId(requestId);
log.setResult(ArLogResult.SUCCESS.getCode());
logService.record(log);
}
private ServiceException failure(Long arBillId, String objectType, String objectId, String operationType,
private ServiceException failure(Long arBillId, ArLogObjectType objectType, String objectId, ArLogOperationType operationType,
String fromStatus, String message, String operator, String requestId)
{
logService.recordFailure(arBillId, objectType, objectId, operationType, fromStatus, message, operator, requestId);
@@ -861,21 +889,6 @@ public class ArBillServiceImpl implements IArBillService
private BigDecimal decimal(BigDecimal value, int scale) { return value == null ? null : value.setScale(scale, java.math.RoundingMode.HALF_UP); }
private BigDecimal nvl(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; }
/** 构造账单授信占用/释放命令(确认占用、作废释放共用,关联账单单据便于流水追溯) */
private CreditUsageCommand buildCreditCommand(ArBill bill)
{
CreditUsageCommand command = new CreditUsageCommand();
command.setCustomerId(bill.getCustomerId());
command.setCurrency(bill.getCurrency());
command.setAmount(nvl(bill.getTotalAmount()));
command.setBizType(CustomerFundBizType.AR_BILL);
command.setBizId(bill.getArBillId());
command.setBizNo(bill.getBillNo());
// 兼容历史口径:不校验可用额度(允许 used_credit 高于总额度、外币账户静默跳过)
command.setCheckAvailable(Boolean.FALSE);
return command;
}
private String safeFilePart(String value) { return StringUtils.isEmpty(value) ? "客户" : value.replaceAll("[^a-zA-Z0-9\\u4e00-\\u9fa5_-]", "_"); }
private String maskAccount(String value) { if (StringUtils.isEmpty(value)) return ""; return value.length() <= 4 ? "****" : "****" + value.substring(value.length() - 4); }
private String sha256(byte[] data) throws NoSuchAlgorithmException { byte[] digest = MessageDigest.getInstance("SHA-256").digest(data); StringBuilder sb = new StringBuilder(); for (byte b : digest) sb.append(String.format("%02x", b)); return sb.toString(); }