统一修改ruoyi->bms
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
package com.bms.ar.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.bms.ar.domain.ArBalanceWriteoff;
|
||||
import com.bms.ar.domain.ArBill;
|
||||
|
||||
/**
|
||||
* 应收账单余额核销 Service接口
|
||||
*
|
||||
* <p>核销仅更新账单已核销/未核销金额及状态(2026-09-23 口径变更后不再变动 ctr_customer_account 余额与额度)。</p>
|
||||
*
|
||||
* @author bms
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
public interface IArBalanceWriteoffService
|
||||
{
|
||||
/** 查询客户可核销账单(已确认/部分核销,未核销额大于0) */
|
||||
List<ArBill> selectUnsettledBills(Long customerId);
|
||||
|
||||
/** 查询余额核销记录 */
|
||||
List<ArBalanceWriteoff> selectWriteoffList(ArBalanceWriteoff query);
|
||||
|
||||
/** 用客户账户余额核销账单 */
|
||||
ArBalanceWriteoff writeoff(ArBalanceWriteoff cmd);
|
||||
|
||||
/** 反核销(取消一笔有效余额核销,余额回补、账单金额回退) */
|
||||
void cancelWriteoff(Long writeoffId, String remark);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.bms.ar.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.bms.ar.domain.ArBillLog;
|
||||
import com.bms.ar.enums.ArLogObjectType;
|
||||
import com.bms.ar.enums.ArLogOperationType;
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.bms.ar.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.bms.ar.domain.ArBill;
|
||||
import com.bms.ar.domain.ArBankAccount;
|
||||
import com.bms.ar.domain.ArBillDetail;
|
||||
import com.bms.ar.domain.ArBillDetailFee;
|
||||
import com.bms.ar.domain.ArBillExport;
|
||||
import com.bms.ar.domain.ArBillGenerateTask;
|
||||
import com.bms.ar.domain.ArBillLog;
|
||||
import com.bms.ar.domain.vo.ArBillGenerateRequest;
|
||||
import com.bms.ar.domain.vo.ArBillGenerateQuery;
|
||||
import com.bms.ar.domain.vo.ArBillActionRequest;
|
||||
import com.bms.ar.domain.vo.ArBillQuery;
|
||||
import com.bms.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);
|
||||
|
||||
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);
|
||||
|
||||
ArBill voidBill(Long arBillId, ArBillActionRequest request, String operator, String requestId);
|
||||
|
||||
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;
|
||||
|
||||
List<ArBillLog> selectLogList(ArBillLog query);
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
package com.bms.ar.service.impl;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
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;
|
||||
import org.springframework.util.StringUtils;
|
||||
import com.bms.ar.domain.ArBalanceWriteoff;
|
||||
import com.bms.ar.domain.ArBill;
|
||||
import com.bms.ar.domain.ArBillLog;
|
||||
import com.bms.ar.enums.ArBalanceWriteoffStatus;
|
||||
import com.bms.ar.enums.ArBillStatus;
|
||||
import com.bms.ar.enums.ArLogObjectType;
|
||||
import com.bms.ar.enums.ArLogOperationSource;
|
||||
import com.bms.ar.enums.ArLogOperationType;
|
||||
import com.bms.ar.enums.ArLogResult;
|
||||
import com.bms.ar.mapper.ArBalanceWriteoffMapper;
|
||||
import com.bms.ar.mapper.ArBillMapper;
|
||||
import com.bms.ar.service.IArBalanceWriteoffService;
|
||||
import com.bms.ar.service.IArBillLogService;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
import com.bms.common.utils.SecurityUtils;
|
||||
import com.bms.common.utils.uuid.IdUtils;
|
||||
|
||||
/**
|
||||
* 应收账单余额核销 Service实现
|
||||
*
|
||||
* @author bms
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
@Service
|
||||
public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService {
|
||||
private static final DateTimeFormatter NO_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
|
||||
private static final int MONEY_SCALE = 2;
|
||||
|
||||
@Autowired
|
||||
private ArBillMapper billMapper;
|
||||
|
||||
@Autowired
|
||||
private ArBalanceWriteoffMapper writeoffMapper;
|
||||
|
||||
@Autowired
|
||||
private IArBillLogService logService;
|
||||
|
||||
/**
|
||||
* 按客户查询允许余额核销的未结清账单。
|
||||
*/
|
||||
@Override
|
||||
public List<ArBill> selectUnsettledBills(Long customerId) {
|
||||
if (customerId == null) {
|
||||
return List.of();
|
||||
}
|
||||
return billMapper.selectUnsettledByCustomer(customerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询账单余额核销记录列表。
|
||||
*/
|
||||
@Override
|
||||
public List<ArBalanceWriteoff> selectWriteoffList(ArBalanceWriteoff query) {
|
||||
return writeoffMapper.selectWriteoffList(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行余额核销主流程:锁账单、登记核销单、更新账单金额和状态。
|
||||
* 口径变更(2026-09-23):核销不再变动账户余额与已用额度,资金变动由收款/计费入口产生。
|
||||
* 任一步失败都会回滚当前事务,避免核销单与账单状态不一致。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
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) {
|
||||
throw new ServiceException("核销金额必须大于0");
|
||||
}
|
||||
amount = amount.setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new ServiceException("核销金额保留两位小数后必须大于0");
|
||||
}
|
||||
|
||||
// 1. 行锁锁定账单(加锁顺序:账单 -> 账户,与反核销保持一致)
|
||||
ArBill bill = billMapper.selectArBillByIdForUpdate(cmd.getArBillId());
|
||||
if (bill == null) {
|
||||
throw new ServiceException("应收账单不存在");
|
||||
}
|
||||
ArBillStatus status = ArBillStatus.of(bill.getStatus());
|
||||
if (status == null || !status.canWriteOff()) {
|
||||
throw new ServiceException("当前账单状态不允许核销:" + (status == null ? bill.getStatus() : status.getLabel()));
|
||||
}
|
||||
if (cmd.getCustomerId() != null && !cmd.getCustomerId().equals(bill.getCustomerId())) {
|
||||
throw new ServiceException("账单与所选客户不匹配");
|
||||
}
|
||||
BigDecimal unwritten = nvl(bill.getUnwrittenAmount());
|
||||
if (amount.compareTo(unwritten) > 0) {
|
||||
throw new ServiceException("核销金额超过账单未核销金额:" + unwritten.toPlainString());
|
||||
}
|
||||
|
||||
String operator = SecurityUtils.getUsername();
|
||||
String requestId = IdUtils.fastSimpleUUID();
|
||||
String currency = StringUtils.hasText(bill.getCurrency()) ? bill.getCurrency().trim().toUpperCase() : "CNY";
|
||||
Date writeoffDate = cmd.getWriteoffDate() != null ? cmd.getWriteoffDate() : new Date();
|
||||
String fromStatus = bill.getStatus();
|
||||
|
||||
// 2. 先落核销单(账户与流水引用拿到后回写)
|
||||
ArBalanceWriteoff writeoff = new ArBalanceWriteoff();
|
||||
writeoff.setWriteoffNo(generateNo());
|
||||
writeoff.setArBillId(bill.getArBillId());
|
||||
writeoff.setBillNo(bill.getBillNo());
|
||||
writeoff.setCustomerId(bill.getCustomerId());
|
||||
writeoff.setCurrency(currency);
|
||||
writeoff.setWriteoffAmount(amount);
|
||||
writeoff.setWriteoffDate(writeoffDate);
|
||||
writeoff.setStatus(ArBalanceWriteoffStatus.EFFECTIVE.getCode());
|
||||
writeoff.setRemark(StringUtils.hasText(cmd.getRemark()) ? cmd.getRemark().trim() : null);
|
||||
writeoff.setCreateBy(operator);
|
||||
writeoffMapper.insertArBalanceWriteoff(writeoff);
|
||||
|
||||
// 3. 账单核销口径变更(2026-09-23):核销不再变动账户余额与已用额度,
|
||||
// 仅更新账单已核销/未核销金额与状态,资金变动由收款/计费入口产生。
|
||||
// 4. 更新账单已核销/未核销缓存与状态(乐观条件更新,状态被并发改动则整体回滚)
|
||||
BigDecimal written = nvl(bill.getWrittenOffAmount()).add(amount);
|
||||
BigDecimal newUnwritten = unwritten.subtract(amount);
|
||||
String nextStatus = billStatus(written, nvl(bill.getTotalAmount()), fromStatus);
|
||||
if (billMapper.updateArBillAmounts(bill.getArBillId(), written, newUnwritten,
|
||||
nextStatus, operator, fromStatus) != 1) {
|
||||
throw new ServiceException("账单状态已发生变化,请刷新后重试");
|
||||
}
|
||||
|
||||
recordLog(bill.getArBillId(), writeoff.getWriteoffId(), ArLogOperationType.BALANCE_WRITEOFF,
|
||||
"余额核销账单 " + amount.toPlainString() + " " + currency,
|
||||
fromStatus, nextStatus, operator, requestId);
|
||||
return writeoff;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行反核销主流程:锁定核销单和账单、恢复账单未核销金额,
|
||||
* 最后将原核销记录标记为已取消(口径变更后不再回补账户余额、不再重新占用已用额度)。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void cancelWriteoff(Long writeoffId, String remark) {
|
||||
if (writeoffId == null) {
|
||||
throw new ServiceException("核销记录不存在");
|
||||
}
|
||||
// 1. 锁核销单 -> 锁账单(与核销相同的加锁顺序)
|
||||
ArBalanceWriteoff writeoff = writeoffMapper.selectByIdForUpdate(writeoffId);
|
||||
if (writeoff == null) {
|
||||
throw new ServiceException("核销记录不存在");
|
||||
}
|
||||
if (!ArBalanceWriteoffStatus.EFFECTIVE.getCode().equals(writeoff.getStatus())) {
|
||||
throw new ServiceException("只有有效状态的核销记录可以反核销");
|
||||
}
|
||||
ArBill bill = billMapper.selectArBillByIdForUpdate(writeoff.getArBillId());
|
||||
if (bill == null) {
|
||||
throw new ServiceException("核销关联的账单不存在");
|
||||
}
|
||||
|
||||
String operator = SecurityUtils.getUsername();
|
||||
String requestId = IdUtils.fastSimpleUUID();
|
||||
String cancelRemark = StringUtils.hasText(remark) ? remark.trim() : "取消余额核销";
|
||||
String fromStatus = bill.getStatus();
|
||||
|
||||
// 2. 账单核销金额回退,状态按剩余已核销额重算(归零则回到已确认)
|
||||
// 口径变更(2026-09-23):反核销不再回补账户余额、不再重新占用已用额度。
|
||||
BigDecimal written = nvl(bill.getWrittenOffAmount()).subtract(nvl(writeoff.getWriteoffAmount()));
|
||||
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) {
|
||||
throw new ServiceException("账单状态已发生变化,请刷新后重试");
|
||||
}
|
||||
|
||||
// 3. 核销单置为CANCELLED(口径变更后无回补流水,reverse_flow_id 置空)
|
||||
if (writeoffMapper.updateCancelled(writeoffId, null, operator, cancelRemark,
|
||||
ArBalanceWriteoffStatus.CANCELLED.getCode(), ArBalanceWriteoffStatus.EFFECTIVE.getCode()) != 1) {
|
||||
throw new ServiceException("核销记录状态已变化,请刷新后重试");
|
||||
}
|
||||
recordLog(bill.getArBillId(), writeoffId, ArLogOperationType.CANCEL_BALANCE_WRITEOFF,
|
||||
"反核销取消核销:" + cancelRemark,
|
||||
fromStatus, nextStatus, operator, requestId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 账单状态重算:归零(非草稿)为已确认,足额为已核销,否则为部分核销。
|
||||
*/
|
||||
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();
|
||||
}
|
||||
return written.compareTo(total) >= 0
|
||||
? 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) {
|
||||
ArBillLog log = new ArBillLog();
|
||||
log.setArBillId(billId);
|
||||
log.setObjectType(ArLogObjectType.BALANCE_WRITEOFF.getCode());
|
||||
log.setObjectId(String.valueOf(writeoffId));
|
||||
log.setOperationType(operationType.getCode());
|
||||
log.setOperationDesc(desc);
|
||||
log.setFromStatus(fromStatus);
|
||||
log.setToStatus(toStatus);
|
||||
log.setOperatorId(operator);
|
||||
log.setOperatorName(operator);
|
||||
log.setOperationSource(ArLogOperationSource.WEB.getCode());
|
||||
log.setRequestId(requestId);
|
||||
log.setResult(ArLogResult.SUCCESS.getCode());
|
||||
logService.record(log);
|
||||
}
|
||||
|
||||
private String generateNo() {
|
||||
return "BWO" + LocalDateTime.now().format(NO_FORMATTER)
|
||||
+ ThreadLocalRandom.current().nextInt(100, 1000);
|
||||
}
|
||||
|
||||
private BigDecimal nvl(BigDecimal value) {
|
||||
return value == null ? BigDecimal.ZERO : value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.bms.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;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.bms.ar.domain.ArBillLog;
|
||||
import com.bms.ar.enums.ArLogObjectType;
|
||||
import com.bms.ar.enums.ArLogOperationSource;
|
||||
import com.bms.ar.enums.ArLogOperationType;
|
||||
import com.bms.ar.enums.ArLogResult;
|
||||
import com.bms.ar.mapper.ArBillLogMapper;
|
||||
import com.bms.ar.service.IArBillLogService;
|
||||
import com.bms.common.utils.uuid.IdUtils;
|
||||
|
||||
@Service
|
||||
public class ArBillLogServiceImpl implements IArBillLogService {
|
||||
@Autowired
|
||||
private ArBillLogMapper logMapper;
|
||||
|
||||
@Override
|
||||
public List<ArBillLog> selectLogList(ArBillLog query) {
|
||||
return logMapper.selectList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
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) {
|
||||
ArBillLog log = new ArBillLog();
|
||||
log.setArBillId(arBillId);
|
||||
log.setObjectType(objectType.getCode());
|
||||
log.setObjectId(objectId);
|
||||
log.setOperationType(operationType.getCode());
|
||||
log.setOperationDesc(message);
|
||||
log.setFromStatus(fromStatus);
|
||||
log.setOperatorId(operator);
|
||||
log.setOperatorName(operator);
|
||||
log.setOperationSource(ArLogOperationSource.WEB.getCode());
|
||||
log.setRequestId(requestId == null ? IdUtils.fastSimpleUUID() : requestId);
|
||||
log.setResult(ArLogResult.FAILED.getCode());
|
||||
log.setErrorMessage(message);
|
||||
logMapper.insert(log);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user