Merge branch 'master' of http://git.lionquexpress.com/wuweihua/BMS
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
package com.ruoyi.ar.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 应收账单余额核销 ar_balance_writeoff
|
||||
*
|
||||
* <p>用客户账户余额核销应收账单,与 ar_receipt_writeoff(收款核销)并列,
|
||||
* 两者共享 ar_bill.written_off_amount / unwritten_amount 缓存。</p>
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
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;
|
||||
|
||||
/** 核销单号(BWO开头) */
|
||||
private String writeoffNo;
|
||||
|
||||
/** 应收账单ID */
|
||||
private Long arBillId;
|
||||
|
||||
/** 账单号快照 */
|
||||
private String billNo;
|
||||
|
||||
/** 客户ID */
|
||||
private Long customerId;
|
||||
|
||||
/** 客户账户ID */
|
||||
private Long accountId;
|
||||
|
||||
/** 币种 */
|
||||
private String currency;
|
||||
|
||||
/** 本次核销金额 */
|
||||
private BigDecimal writeoffAmount;
|
||||
|
||||
/** 核销扣款余额流水ID */
|
||||
private Long balanceFlowId;
|
||||
|
||||
/** 反核销回补余额流水ID */
|
||||
private Long reverseFlowId;
|
||||
|
||||
/** 核销日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date writeoffDate;
|
||||
|
||||
/** 状态:EFFECTIVE/CANCELLED */
|
||||
private String status;
|
||||
|
||||
/** 核销备注 */
|
||||
private String remark;
|
||||
|
||||
/** 反核销人 */
|
||||
private String cancelBy;
|
||||
|
||||
/** 反核销时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date cancelTime;
|
||||
|
||||
/** 反核销原因 */
|
||||
private String cancelRemark;
|
||||
|
||||
/* ===== 列表关联展示字段(来自 ar_bill) ===== */
|
||||
|
||||
/** 账单名称 */
|
||||
private String billName;
|
||||
|
||||
/** 客户名称快照 */
|
||||
private String customerName;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ 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;
|
||||
@@ -21,10 +22,13 @@ public class ArBill extends BaseEntity
|
||||
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;
|
||||
|
||||
@@ -15,7 +15,7 @@ public class ArBillDetail implements Serializable
|
||||
|
||||
private Long detailId;
|
||||
private Long arBillId;
|
||||
private String prcBillId;
|
||||
private String prcOrderBillId;
|
||||
private Integer detailNo;
|
||||
private String orderNo;
|
||||
private String trackingNo;
|
||||
|
||||
@@ -14,7 +14,7 @@ public class ArBillDetailFee implements Serializable
|
||||
|
||||
private Long detailFeeId;
|
||||
private Long detailId;
|
||||
private String prcFeeId;
|
||||
private String prcIncomeBillFeeId;
|
||||
private Integer feeNo;
|
||||
private String feeType;
|
||||
private String feeName;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.ruoyi.ar.domain.vo;
|
||||
|
||||
import java.util.Date;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import com.ruoyi.ar.domain.ArBill;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/** 应收账单列表查询参数。 */
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ArBillQuery extends ArBill
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 账单日期查询起始日。 */
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private Date billDateStart;
|
||||
|
||||
/** 账单日期查询结束日。 */
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private Date billDateEnd;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.ruoyi.ar.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.ar.domain.ArBalanceWriteoff;
|
||||
|
||||
/**
|
||||
* 应收账单余额核销 Mapper
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
public interface ArBalanceWriteoffMapper
|
||||
{
|
||||
/** 新增余额核销单 */
|
||||
int insertArBalanceWriteoff(ArBalanceWriteoff writeoff);
|
||||
|
||||
/** 回写扣款账户与余额流水ID */
|
||||
int updateFlowRefs(@Param("writeoffId") Long writeoffId,
|
||||
@Param("accountId") Long accountId,
|
||||
@Param("balanceFlowId") Long balanceFlowId);
|
||||
|
||||
/** 按ID查询(行锁) */
|
||||
ArBalanceWriteoff selectByIdForUpdate(@Param("writeoffId") Long writeoffId);
|
||||
|
||||
/** 反核销:置为CANCELLED并登记回补流水与原因 */
|
||||
int updateCancelled(@Param("writeoffId") Long writeoffId,
|
||||
@Param("reverseFlowId") Long reverseFlowId,
|
||||
@Param("cancelBy") String cancelBy,
|
||||
@Param("cancelRemark") String cancelRemark);
|
||||
|
||||
/** 查询核销记录列表(关联账单名称/客户名称) */
|
||||
List<ArBalanceWriteoff> selectWriteoffList(ArBalanceWriteoff query);
|
||||
}
|
||||
@@ -5,10 +5,11 @@ 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
|
||||
{
|
||||
List<ArBill> selectArBillList(ArBill query);
|
||||
List<ArBill> selectArBillList(ArBillQuery query);
|
||||
|
||||
ArBill selectArBillById(Long arBillId);
|
||||
|
||||
@@ -19,6 +20,9 @@ public interface ArBillMapper
|
||||
|
||||
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,
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.ruoyi.ar.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.ar.domain.ArBalanceWriteoff;
|
||||
import com.ruoyi.ar.domain.ArBill;
|
||||
|
||||
/**
|
||||
* 应收账单余额核销 Service接口
|
||||
*
|
||||
* <p>资金来源为客户账户余额(customer_account.balance),与收款核销
|
||||
* (ar_receipt_writeoff)并列;核销时三方联动:账户余额扣减、已用额度释放、
|
||||
* 账单已核销/未核销金额及状态更新。</p>
|
||||
*
|
||||
* @author ruoyi
|
||||
* @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);
|
||||
}
|
||||
@@ -8,11 +8,12 @@ import com.ruoyi.ar.domain.ArBillLog;
|
||||
import com.ruoyi.ar.domain.vo.ArBillGenerateRequest;
|
||||
import com.ruoyi.ar.domain.vo.ArBillGenerateQuery;
|
||||
import com.ruoyi.ar.domain.vo.ArBillActionRequest;
|
||||
import com.ruoyi.ar.domain.vo.ArBillQuery;
|
||||
import com.ruoyi.price.domain.PrcOrderBill;
|
||||
|
||||
public interface IArBillService
|
||||
{
|
||||
List<ArBill> selectArBillList(ArBill query);
|
||||
List<ArBill> selectArBillList(ArBillQuery query);
|
||||
|
||||
ArBill selectArBillById(Long arBillId);
|
||||
|
||||
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
package com.ruoyi.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.ruoyi.ar.domain.ArBalanceWriteoff;
|
||||
import com.ruoyi.ar.domain.ArBill;
|
||||
import com.ruoyi.ar.domain.ArBillLog;
|
||||
import com.ruoyi.ar.enums.ArBillStatus;
|
||||
import com.ruoyi.ar.enums.ArLogResult;
|
||||
import com.ruoyi.ar.mapper.ArBalanceWriteoffMapper;
|
||||
import com.ruoyi.ar.mapper.ArBillMapper;
|
||||
import com.ruoyi.ar.service.IArBalanceWriteoffService;
|
||||
import com.ruoyi.ar.service.IArBillLogService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.uuid.IdUtils;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow;
|
||||
import com.ruoyi.customer.customermoney.service.ICustomerMoneyService;
|
||||
|
||||
/**
|
||||
* 应收账单余额核销 Service实现
|
||||
*
|
||||
* @author ruoyi
|
||||
* @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;
|
||||
|
||||
@Autowired
|
||||
private ICustomerMoneyService customerMoneyService;
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
// 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(ArBalanceWriteoff.STATUS_EFFECTIVE);
|
||||
writeoff.setRemark(StringUtils.hasText(cmd.getRemark()) ? cmd.getRemark().trim() : null);
|
||||
writeoff.setCreateBy(operator);
|
||||
writeoffMapper.insertArBalanceWriteoff(writeoff);
|
||||
|
||||
// 3. 扣减账户余额并释放等额已用额度,写 BILL_PAY 扣款流水(余额不足在此处拦截)
|
||||
String flowRemark = "余额核销账单:" + bill.getBillNo()
|
||||
+ (StringUtils.hasText(writeoff.getRemark()) ? "(" + writeoff.getRemark() + ")" : "");
|
||||
CustomerBalanceFlow flow = customerMoneyService.payBill(bill.getCustomerId(), currency,
|
||||
amount, writeoff.getWriteoffId(), bill.getBillNo(), flowRemark);
|
||||
writeoffMapper.updateFlowRefs(writeoff.getWriteoffId(), flow.getAccountId(), flow.getId());
|
||||
|
||||
// 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(), "BALANCE_WRITEOFF",
|
||||
"账户余额核销账单 " + 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)
|
||||
{
|
||||
throw new ServiceException("核销记录不存在");
|
||||
}
|
||||
// 1. 锁核销单 -> 锁账单(与核销相同的加锁顺序)
|
||||
ArBalanceWriteoff writeoff = writeoffMapper.selectByIdForUpdate(writeoffId);
|
||||
if (writeoff == null)
|
||||
{
|
||||
throw new ServiceException("核销记录不存在");
|
||||
}
|
||||
if (!ArBalanceWriteoff.STATUS_EFFECTIVE.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. 余额回补、已用额度重新占用,写反向 BILL_PAY 流水
|
||||
String flowRemark = "反核销回补账单:" + bill.getBillNo() + "(" + cancelRemark + ")";
|
||||
CustomerBalanceFlow reverseFlow = customerMoneyService.reverseBillPay(writeoff.getCustomerId(),
|
||||
writeoff.getCurrency(), nvl(writeoff.getWriteoffAmount()), writeoffId, bill.getBillNo(), flowRemark);
|
||||
|
||||
// 3. 账单核销金额回退,状态按剩余已核销额重算(归零则回到已确认)
|
||||
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("账单状态已发生变化,请刷新后重试");
|
||||
}
|
||||
|
||||
// 4. 核销单置为CANCELLED并登记回补流水
|
||||
if (writeoffMapper.updateCancelled(writeoffId, reverseFlow.getId(), operator, cancelRemark) != 1)
|
||||
{
|
||||
throw new ServiceException("核销记录状态已变化,请刷新后重试");
|
||||
}
|
||||
recordLog(bill.getArBillId(), writeoffId, "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, String operationType, String desc,
|
||||
String fromStatus, String toStatus, String operator, String requestId)
|
||||
{
|
||||
ArBillLog log = new ArBillLog();
|
||||
log.setArBillId(billId);
|
||||
log.setObjectType("BALANCE_WRITEOFF");
|
||||
log.setObjectId(String.valueOf(writeoffId));
|
||||
log.setOperationType(operationType);
|
||||
log.setOperationDesc(desc);
|
||||
log.setFromStatus(fromStatus);
|
||||
log.setToStatus(toStatus);
|
||||
log.setOperatorId(operator);
|
||||
log.setOperatorName(operator);
|
||||
log.setOperationSource("WEB");
|
||||
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;
|
||||
}
|
||||
}
|
||||
+18
-6
@@ -50,6 +50,7 @@ import com.ruoyi.ar.service.IArBillService;
|
||||
import com.ruoyi.ar.domain.vo.ArBillActionRequest;
|
||||
import com.ruoyi.ar.domain.vo.ArBillGenerateQuery;
|
||||
import com.ruoyi.ar.domain.vo.ArBillGenerateRequest;
|
||||
import com.ruoyi.ar.domain.vo.ArBillQuery;
|
||||
import com.ruoyi.common.config.RuoYiConfig;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
@@ -57,6 +58,7 @@ import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.file.FileUtils;
|
||||
import com.ruoyi.common.utils.file.FileUploadUtils;
|
||||
import com.ruoyi.common.utils.uuid.IdUtils;
|
||||
import com.ruoyi.customer.customermoney.service.ICustomerMoneyService;
|
||||
import com.ruoyi.price.domain.PrcIncomeBillFee;
|
||||
import com.ruoyi.price.domain.PrcOrderBill;
|
||||
import com.ruoyi.price.mapper.PrcIncomeBillFeeMapper;
|
||||
@@ -79,9 +81,10 @@ public class ArBillServiceImpl implements IArBillService
|
||||
@Autowired private IArBillLogService logService;
|
||||
@Autowired private PrcOrderBillMapper sourceBillMapper;
|
||||
@Autowired private PrcIncomeBillFeeMapper sourceFeeMapper;
|
||||
@Autowired private ICustomerMoneyService customerMoneyService;
|
||||
|
||||
@Override
|
||||
public List<ArBill> selectArBillList(ArBill query)
|
||||
public List<ArBill> selectArBillList(ArBillQuery query)
|
||||
{
|
||||
List<ArBill> bills = billMapper.selectArBillList(query);
|
||||
for (ArBill bill : bills)
|
||||
@@ -224,7 +227,7 @@ public class ArBillServiceImpl implements IArBillService
|
||||
Map<String, ArBillDetail> detailsBySourceBill = new HashMap<>();
|
||||
for (ArBillDetail detail : detailMapper.selectByBillId(bill.getArBillId()))
|
||||
{
|
||||
detailsBySourceBill.put(detail.getPrcBillId(), detail);
|
||||
detailsBySourceBill.put(detail.getPrcOrderBillId(), detail);
|
||||
}
|
||||
List<ArBillDetailFee> detailFees = new ArrayList<>();
|
||||
List<PrcIncomeBillFee> sourceFees = sourceBillIds.isEmpty()
|
||||
@@ -276,6 +279,9 @@ public class ArBillServiceImpl implements IArBillService
|
||||
throw failure(arBillId, "BILL", String.valueOf(arBillId), "CONFIRM", bill.getStatus(),
|
||||
"账单状态已发生变化,请刷新后重试", operator, requestId);
|
||||
}
|
||||
// 账单确认:应收形成,按账单金额占用客户授信额度(账户不存在时仅CNY会自动初始化)
|
||||
customerMoneyService.occupyCredit(bill.getCustomerId(), bill.getCurrency(),
|
||||
nvl(bill.getTotalAmount()));
|
||||
recordSuccess(bill, "CONFIRM", "确认应收账单", ArBillStatus.DRAFT.getCode(), ArBillStatus.CONFIRMED.getCode(), requestId, operator);
|
||||
return selectArBillById(arBillId);
|
||||
}
|
||||
@@ -303,9 +309,9 @@ public class ArBillServiceImpl implements IArBillService
|
||||
List<String> sourceBillIds = new ArrayList<>();
|
||||
for (ArBillDetail detail : detailMapper.selectByBillId(arBillId))
|
||||
{
|
||||
if (!StringUtils.isEmpty(detail.getPrcBillId()))
|
||||
if (!StringUtils.isEmpty(detail.getPrcOrderBillId()))
|
||||
{
|
||||
sourceBillIds.add(detail.getPrcBillId());
|
||||
sourceBillIds.add(detail.getPrcOrderBillId());
|
||||
}
|
||||
}
|
||||
if (billMapper.updateArBillStatus(arBillId, ArBillStatus.VOID.getCode(), remark, operator, currentStatus.getCode()) != 1)
|
||||
@@ -313,6 +319,12 @@ public class ArBillServiceImpl implements IArBillService
|
||||
throw failure(arBillId, "BILL", String.valueOf(arBillId), "VOID", bill.getStatus(),
|
||||
"账单状态已发生变化,请刷新后重试", operator, requestId);
|
||||
}
|
||||
// 账单作废:已确认账单占用的授信额度全额释放(可作废的CONFIRMED账单必然零核销)
|
||||
if (currentStatus == ArBillStatus.CONFIRMED)
|
||||
{
|
||||
customerMoneyService.releaseCredit(bill.getCustomerId(), bill.getCurrency(),
|
||||
nvl(bill.getTotalAmount()));
|
||||
}
|
||||
if (!sourceBillIds.isEmpty())
|
||||
{
|
||||
int releasedCount = sourceBillMapper.releaseArBillOrders(sourceBillIds, operator,
|
||||
@@ -459,7 +471,7 @@ public class ArBillServiceImpl implements IArBillService
|
||||
{
|
||||
ArBillDetail detail = new ArBillDetail();
|
||||
detail.setArBillId(bill.getArBillId());
|
||||
detail.setPrcBillId(source.getBillId());
|
||||
detail.setPrcOrderBillId(source.getBillId());
|
||||
detail.setDetailNo(detailNo);
|
||||
detail.setOrderNo(source.getOrderNo());
|
||||
detail.setTrackingNo(source.getTrackingNo());
|
||||
@@ -487,7 +499,7 @@ public class ArBillServiceImpl implements IArBillService
|
||||
{
|
||||
ArBillDetailFee fee = new ArBillDetailFee();
|
||||
fee.setDetailId(detail.getDetailId());
|
||||
fee.setPrcFeeId(source.getFeeId());
|
||||
fee.setPrcIncomeBillFeeId(source.getFeeId());
|
||||
fee.setFeeNo(feeNo);
|
||||
fee.setFeeType(source.getFeeType());
|
||||
fee.setFeeName(source.getFeeName());
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package com.ruoyi.customer.customerinfo.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 客户套餐绑定对象 prc_customer_price_set
|
||||
* 客户管理-客户编辑-绑定套餐 使用;关联 prc_price_set 的字段仅用于页面展示
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class BmsCustomerPriceSet extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 客户价格套餐ID */
|
||||
private Long customerPricesetId;
|
||||
|
||||
/** 价格套餐ID */
|
||||
private Long priceSetId;
|
||||
|
||||
/** 客户ID */
|
||||
private Long customerId;
|
||||
|
||||
/** 客户代码 */
|
||||
private String customerCode;
|
||||
|
||||
/** 客户名称 */
|
||||
private String customerName;
|
||||
|
||||
/** 绑定优先级,数字越小越优先 */
|
||||
private Integer bindPriority;
|
||||
|
||||
/** 价格开始生效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date prBegindate;
|
||||
|
||||
/** 价格失效日期(空=长期) */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date prEnddate;
|
||||
|
||||
/** 绑定备注 */
|
||||
private String priceSetNote;
|
||||
|
||||
/* -------- 关联 prc_price_set 展示字段 -------- */
|
||||
|
||||
/** 套餐编码 */
|
||||
private String priceSetCode;
|
||||
|
||||
/** 套餐名称 */
|
||||
private String priceSetName;
|
||||
|
||||
/** 计价类型 FIXED=固定价 DISCOUNT=折扣 */
|
||||
private String setType;
|
||||
|
||||
/** 套餐状态 D草稿 A审核中 E生效 R驳回 S停用 */
|
||||
private String priceSetStatus;
|
||||
|
||||
/** 套餐生效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date setEffectiveDate;
|
||||
|
||||
/** 套餐失效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date setExpireDate;
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.ruoyi.customer.customerinfo.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 价格套餐(prc_price_set)简要对象
|
||||
* 仅用于客户编辑-绑定套餐的套餐下拉选择,不参与套餐维护
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
@Data
|
||||
public class BmsPriceSetOption
|
||||
{
|
||||
/** 价格套餐ID */
|
||||
private Long priceSetId;
|
||||
|
||||
/** 套餐编码 */
|
||||
private String priceSetCode;
|
||||
|
||||
/** 套餐名称 */
|
||||
private String priceSetName;
|
||||
|
||||
/** 计价类型 FIXED=固定价 DISCOUNT=折扣 */
|
||||
private String setType;
|
||||
|
||||
/** 套餐状态 D草稿 A审核中 E生效 R驳回 S停用 */
|
||||
private String status;
|
||||
|
||||
/** 生效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date effectiveDate;
|
||||
|
||||
/** 失效日期(空=长期) */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date expireDate;
|
||||
|
||||
/** 关键字(编码/名称模糊) */
|
||||
private String keyword;
|
||||
}
|
||||
+23
@@ -23,6 +23,15 @@ public interface BmsCustomerMapper
|
||||
*/
|
||||
public BmsCustomer selectBmsCustomerByCustomerId(Long customerId);
|
||||
|
||||
/**
|
||||
* 只取客户档案物理额度列(credit_ed 在通用查询里是账户可用额度的子查询别名,
|
||||
* 账户首次初始化时必须读取档案原始值)
|
||||
*
|
||||
* @param customerId 客户ID
|
||||
* @return 仅含 creditEd/tempEd 的客户对象
|
||||
*/
|
||||
public BmsCustomer selectRawCreditByCustomerId(Long customerId);
|
||||
|
||||
/**
|
||||
* 查询客户主列表
|
||||
*
|
||||
@@ -103,6 +112,20 @@ public interface BmsCustomerMapper
|
||||
*/
|
||||
public int deleteBmsCustomerProfileAttachmentByCustomerId(Long customerId);
|
||||
|
||||
/**
|
||||
* 同步客户档案额度字段(额度以客户余额账户为准,调整后回写档案)
|
||||
*
|
||||
* @param customerId 客户ID
|
||||
* @param creditEd 信用额度
|
||||
* @param tempEd 临时额度
|
||||
* @param updateBy 更新人
|
||||
* @return 结果
|
||||
*/
|
||||
public int updateCustomerCredit(@org.apache.ibatis.annotations.Param("customerId") Long customerId,
|
||||
@org.apache.ibatis.annotations.Param("creditEd") java.math.BigDecimal creditEd,
|
||||
@org.apache.ibatis.annotations.Param("tempEd") java.math.BigDecimal tempEd,
|
||||
@org.apache.ibatis.annotations.Param("updateBy") String updateBy);
|
||||
|
||||
/**
|
||||
* 批量删除客户附件
|
||||
*
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.ruoyi.customer.customerinfo.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.customer.customerinfo.domain.BmsCustomerPriceSet;
|
||||
import com.ruoyi.customer.customerinfo.domain.BmsPriceSetOption;
|
||||
|
||||
/**
|
||||
* 客户套餐绑定 数据层(客户管理侧独立实现,操作 prc_customer_price_set)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
public interface BmsCustomerPriceSetMapper
|
||||
{
|
||||
/** 按客户查绑定列表(join prc_price_set 取套餐展示字段) */
|
||||
public List<BmsCustomerPriceSet> selectBindListByCustomerId(@Param("customerId") Long customerId);
|
||||
|
||||
/** 按主键查绑定 */
|
||||
public BmsCustomerPriceSet selectBindById(Long customerPricesetId);
|
||||
|
||||
/** 按客户ID查绑定(uk_customer_one_set 保证至多一条) */
|
||||
public BmsCustomerPriceSet selectBindByCustomerId(@Param("customerId") Long customerId);
|
||||
|
||||
/** 新增绑定 */
|
||||
public int insertBind(BmsCustomerPriceSet bind);
|
||||
|
||||
/** 更新绑定(允许更换套餐 price_set_id 与生效区间/优先级/备注) */
|
||||
public int updateBind(BmsCustomerPriceSet bind);
|
||||
|
||||
/** 按主键批量解绑 */
|
||||
public int deleteBindByIds(Long[] customerPricesetIds);
|
||||
|
||||
/** 套餐下拉选项(只列生效中套餐,可按关键字/类型过滤) */
|
||||
public List<BmsPriceSetOption> selectPriceSetOptionList(BmsPriceSetOption query);
|
||||
|
||||
/** 按主键查套餐简要信息 */
|
||||
public BmsPriceSetOption selectPriceSetOptionById(@Param("priceSetId") Long priceSetId);
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.ruoyi.customer.customerinfo.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.customer.customerinfo.domain.BmsCustomerPriceSet;
|
||||
import com.ruoyi.customer.customerinfo.domain.BmsPriceSetOption;
|
||||
|
||||
/**
|
||||
* 客户套餐绑定 服务层(客户管理侧)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
public interface IBmsCustomerPriceSetService
|
||||
{
|
||||
/** 查询某客户的套餐绑定列表 */
|
||||
public List<BmsCustomerPriceSet> selectBindList(Long customerId);
|
||||
|
||||
/** 查询可绑定的生效套餐选项 */
|
||||
public List<BmsPriceSetOption> selectOptionList(BmsPriceSetOption query);
|
||||
|
||||
/**
|
||||
* 保存客户套餐绑定(一个客户一条记录):已绑定则更新(支持更换套餐),未绑定则新增
|
||||
*/
|
||||
public int saveBind(BmsCustomerPriceSet bind);
|
||||
|
||||
/** 按绑定主键解绑 */
|
||||
public int deleteBindByIds(Long[] customerPricesetIds);
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package com.ruoyi.customer.customerinfo.service.impl;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.customer.customerinfo.domain.BmsCustomer;
|
||||
import com.ruoyi.customer.customerinfo.domain.BmsCustomerPriceSet;
|
||||
import com.ruoyi.customer.customerinfo.domain.BmsPriceSetOption;
|
||||
import com.ruoyi.customer.customerinfo.mapper.BmsCustomerPriceSetMapper;
|
||||
import com.ruoyi.customer.customerinfo.service.IBmsCustomerPriceSetService;
|
||||
import com.ruoyi.customer.customerinfo.service.IBmsCustomerService;
|
||||
|
||||
/**
|
||||
* 客户套餐绑定 服务实现(客户管理侧,独立于价格套餐维护模块)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
@Service
|
||||
public class BmsCustomerPriceSetServiceImpl implements IBmsCustomerPriceSetService
|
||||
{
|
||||
@Autowired
|
||||
private BmsCustomerPriceSetMapper bindMapper;
|
||||
|
||||
@Autowired
|
||||
private IBmsCustomerService customerService;
|
||||
|
||||
@Override
|
||||
public List<BmsCustomerPriceSet> selectBindList(Long customerId)
|
||||
{
|
||||
if (customerId == null)
|
||||
{
|
||||
return List.of();
|
||||
}
|
||||
return bindMapper.selectBindListByCustomerId(customerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BmsPriceSetOption> selectOptionList(BmsPriceSetOption query)
|
||||
{
|
||||
return bindMapper.selectPriceSetOptionList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int saveBind(BmsCustomerPriceSet bind)
|
||||
{
|
||||
if (bind.getCustomerId() == null)
|
||||
{
|
||||
throw new ServiceException("客户ID不能为空");
|
||||
}
|
||||
if (bind.getPriceSetId() == null)
|
||||
{
|
||||
throw new ServiceException("请选择要绑定的套餐");
|
||||
}
|
||||
BmsCustomer customer = customerService.selectBmsCustomerByCustomerId(bind.getCustomerId());
|
||||
if (customer == null)
|
||||
{
|
||||
throw new ServiceException("客户不存在或已被删除");
|
||||
}
|
||||
BmsPriceSetOption set = bindMapper.selectPriceSetOptionById(bind.getPriceSetId());
|
||||
if (set == null)
|
||||
{
|
||||
throw new ServiceException("所选套餐不存在");
|
||||
}
|
||||
if (!"E".equals(set.getStatus()))
|
||||
{
|
||||
throw new ServiceException("仅生效中的套餐允许绑定,当前套餐状态不可用");
|
||||
}
|
||||
if (bind.getPrBegindate() == null)
|
||||
{
|
||||
bind.setPrBegindate(set.getEffectiveDate());
|
||||
}
|
||||
if (bind.getPrBegindate() == null)
|
||||
{
|
||||
throw new ServiceException("绑定生效日期不能为空");
|
||||
}
|
||||
if (bind.getPrEnddate() != null && bind.getPrBegindate().after(bind.getPrEnddate()))
|
||||
{
|
||||
throw new ServiceException("失效日期不能早于生效日期");
|
||||
}
|
||||
if (bind.getBindPriority() == null || bind.getBindPriority() < 1)
|
||||
{
|
||||
bind.setBindPriority(1);
|
||||
}
|
||||
|
||||
bind.setCustomerCode(customer.getCustomerCode());
|
||||
bind.setCustomerName(customer.getCustomerName());
|
||||
|
||||
BmsCustomerPriceSet existing = bindMapper.selectBindByCustomerId(bind.getCustomerId());
|
||||
try
|
||||
{
|
||||
if (existing != null)
|
||||
{
|
||||
bind.setCustomerPricesetId(existing.getCustomerPricesetId());
|
||||
return bindMapper.updateBind(bind);
|
||||
}
|
||||
return bindMapper.insertBind(bind);
|
||||
}
|
||||
catch (DuplicateKeyException e)
|
||||
{
|
||||
throw new ServiceException("该客户已存在套餐绑定,请勿重复绑定");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteBindByIds(Long[] customerPricesetIds)
|
||||
{
|
||||
if (customerPricesetIds == null || customerPricesetIds.length == 0)
|
||||
{
|
||||
throw new ServiceException("请选择要解绑的记录");
|
||||
}
|
||||
return bindMapper.deleteBindByIds(customerPricesetIds);
|
||||
}
|
||||
}
|
||||
+6
@@ -14,6 +14,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.customer.customerinfo.mapper.BmsCustomerMapper;
|
||||
import com.ruoyi.customer.customerinfo.domain.BmsCustomer;
|
||||
import com.ruoyi.customer.customerinfo.service.IBmsCustomerService;
|
||||
import com.ruoyi.customer.customermoney.service.ICustomerMoneyService;
|
||||
|
||||
/**
|
||||
* 客户主Service业务层处理
|
||||
@@ -27,6 +28,9 @@ public class BmsCustomerServiceImpl implements IBmsCustomerService
|
||||
@Autowired
|
||||
private BmsCustomerMapper bmsCustomerMapper;
|
||||
|
||||
@Autowired
|
||||
private ICustomerMoneyService customerMoneyService;
|
||||
|
||||
/**
|
||||
* 查询客户主
|
||||
*
|
||||
@@ -226,6 +230,8 @@ public class BmsCustomerServiceImpl implements IBmsCustomerService
|
||||
saveBmsCustomerAttachments(bmsCustomer.getCustomerId(), bmsCustomer.getAttachmentList());
|
||||
saveBmsCustomerCompanies(bmsCustomer.getCustomerId(), bmsCustomer.getCompanyList());
|
||||
saveBmsCustomerContacts(bmsCustomer.getCustomerId(), bmsCustomer.getContactList());
|
||||
// 新建客户即初始化默认CNY账户:余额0、总额度1000 CNY,写INIT流水
|
||||
customerMoneyService.initDefaultCnyAccount(bmsCustomer.getCustomerId());
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
+11
-1
@@ -1,6 +1,8 @@
|
||||
package com.ruoyi.customer.customermoney.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
@@ -35,9 +37,17 @@ public class CustomerAccount extends BaseEntity
|
||||
/** 总额度 */
|
||||
private BigDecimal totalCredit;
|
||||
|
||||
/** 额外额度 */
|
||||
/** 额外额度(临时额度) */
|
||||
private BigDecimal extraCredit;
|
||||
|
||||
/** 临时额度生效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date extraEffectiveDate;
|
||||
|
||||
/** 临时额度失效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date extraExpireDate;
|
||||
|
||||
/** 已用额度 */
|
||||
private BigDecimal usedCredit;
|
||||
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package com.ruoyi.customer.customermoney.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 客户额度流水 customer_credit_flow
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class CustomerCreditFlow extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 额度类型:信用额度(长期) */
|
||||
public static final String CREDIT_TYPE_CREDIT = "CREDIT";
|
||||
/** 额度类型:临时额度 */
|
||||
public static final String CREDIT_TYPE_TEMP = "TEMP";
|
||||
|
||||
/** 变动类型:初始化 */
|
||||
public static final String CHANGE_INIT = "INIT";
|
||||
/** 变动类型:手工调整 */
|
||||
public static final String CHANGE_ADJUST = "ADJUST";
|
||||
/** 变动类型:到期自动失效 */
|
||||
public static final String CHANGE_EXPIRE = "EXPIRE";
|
||||
|
||||
/** 调整方式:设置为 */
|
||||
public static final String MODE_SET = "SET";
|
||||
/** 调整方式:增加 */
|
||||
public static final String MODE_ADD = "ADD";
|
||||
/** 调整方式:减少 */
|
||||
public static final String MODE_SUB = "SUB";
|
||||
|
||||
/** 主键 */
|
||||
private Long id;
|
||||
|
||||
/** 流水号 */
|
||||
private String flowNo;
|
||||
|
||||
/** 客户ID */
|
||||
private Long customerId;
|
||||
|
||||
/** 客户账户ID */
|
||||
private Long accountId;
|
||||
|
||||
/** 额度类型:CREDIT信用额度 TEMP临时额度 */
|
||||
private String creditType;
|
||||
|
||||
/** 变动类型:INIT初始化 ADJUST调整 EXPIRE到期失效 */
|
||||
private String changeType;
|
||||
|
||||
/** 变动方向:1增加 -1减少 0持平 */
|
||||
private Integer direction;
|
||||
|
||||
/** 变动前额度 */
|
||||
private BigDecimal beforeAmount;
|
||||
|
||||
/** 变动额度(恒为非负数,方向看 direction) */
|
||||
private BigDecimal changeAmount;
|
||||
|
||||
/** 变动后额度 */
|
||||
private BigDecimal afterAmount;
|
||||
|
||||
/** 变动时已用额度快照 */
|
||||
private BigDecimal usedCredit;
|
||||
|
||||
/** 生效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date effectiveDate;
|
||||
|
||||
/** 失效日期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date expireDate;
|
||||
|
||||
/** 调整原因 */
|
||||
private String reason;
|
||||
|
||||
/** 调整方式(仅ADJUST流水有值):SET/ADD/SUB */
|
||||
private String adjustMode;
|
||||
|
||||
/** 操作人ID */
|
||||
private Long operatorId;
|
||||
|
||||
/** 删除标志(0存在 1删除) */
|
||||
private String delFlag;
|
||||
}
|
||||
+12
@@ -4,6 +4,7 @@ import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerAccount;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerCreditFlow;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerReceipt;
|
||||
|
||||
/**
|
||||
@@ -33,6 +34,17 @@ public interface CustomerMoneyMapper
|
||||
/** 乐观锁更新余额 */
|
||||
int updateAccountBalance(CustomerAccount account);
|
||||
|
||||
/** 乐观锁更新额度(信用额度/临时额度/有效期) */
|
||||
int updateAccountCredit(CustomerAccount account);
|
||||
|
||||
/* ================= 额度流水 ================= */
|
||||
|
||||
/** 查询额度流水 */
|
||||
List<CustomerCreditFlow> selectCreditFlowList(CustomerCreditFlow query);
|
||||
|
||||
/** 新增额度流水 */
|
||||
int insertCreditFlow(CustomerCreditFlow flow);
|
||||
|
||||
/* ================= 客户收款 ================= */
|
||||
|
||||
/** 查询收款单列表 */
|
||||
|
||||
+53
@@ -1,8 +1,10 @@
|
||||
package com.ruoyi.customer.customermoney.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerAccount;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerCreditFlow;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerReceipt;
|
||||
|
||||
/**
|
||||
@@ -16,6 +18,15 @@ public interface ICustomerMoneyService
|
||||
/** 查询客户各币种账户 */
|
||||
List<CustomerAccount> selectAccountList(Long customerId);
|
||||
|
||||
/**
|
||||
* 新建客户时初始化默认CNY账户(余额0,默认总额度1000 CNY),并写 INIT 额度流水。
|
||||
* 已存在CNY账户时不重复创建。
|
||||
*
|
||||
* @param customerId 客户ID
|
||||
* @return 1=已初始化或已存在
|
||||
*/
|
||||
int initDefaultCnyAccount(Long customerId);
|
||||
|
||||
/** 查询收款单列表 */
|
||||
List<CustomerReceipt> selectReceiptList(CustomerReceipt query);
|
||||
|
||||
@@ -36,4 +47,46 @@ public interface ICustomerMoneyService
|
||||
|
||||
/** 查询余额流水列表 */
|
||||
List<CustomerBalanceFlow> selectFlowList(CustomerBalanceFlow query);
|
||||
|
||||
/** 查询额度流水列表 */
|
||||
List<CustomerCreditFlow> selectCreditFlowList(CustomerCreditFlow query);
|
||||
|
||||
/**
|
||||
* 调整客户额度(信用额度/临时额度)
|
||||
* 事务内:锁定CNY账户(不存在则初始化)-> 校验(不能为负、不能低于已用额度)
|
||||
* -> 乐观锁更新额度 -> 回写客户档案额度字段 -> 写额度流水
|
||||
*/
|
||||
int adjustCredit(CustomerCreditFlow flow);
|
||||
|
||||
/**
|
||||
* 账单确认时占用授信额度(used_credit += amount)。该币种账户不存在时跳过。
|
||||
* 调用方必须已持有账单行锁,本方法内部再锁账户(加锁顺序:账单 -> 账户)。
|
||||
*/
|
||||
void occupyCredit(Long customerId, String currency, BigDecimal amount);
|
||||
|
||||
/**
|
||||
* 账单作废时释放授信额度(used_credit -= amount,不小于0)。该币种账户不存在时跳过。
|
||||
* 调用方必须已持有账单行锁。
|
||||
*/
|
||||
void releaseCredit(Long customerId, String currency, BigDecimal amount);
|
||||
|
||||
/**
|
||||
* 用账户余额核销账单:校验余额充足 -> balance -= amount、used_credit -= amount
|
||||
* -> 写一条 BILL_PAY 扣款流水(direction=-1)。返回已落库的流水(含ID)。
|
||||
*
|
||||
* @param bizId 余额核销单ID
|
||||
* @param bizNo 账单号
|
||||
*/
|
||||
CustomerBalanceFlow payBill(Long customerId, String currency, BigDecimal amount,
|
||||
Long bizId, String bizNo, String remark);
|
||||
|
||||
/**
|
||||
* 反核销回补:balance += amount、used_credit += amount(重新占用授信)
|
||||
* -> 写一条 BILL_PAY 回补流水(direction=1)。返回已落库的流水(含ID)。
|
||||
*
|
||||
* @param bizId 余额核销单ID
|
||||
* @param bizNo 账单号
|
||||
*/
|
||||
CustomerBalanceFlow reverseBillPay(Long customerId, String currency, BigDecimal amount,
|
||||
Long bizId, String bizNo, String remark);
|
||||
}
|
||||
|
||||
+522
-23
@@ -2,6 +2,7 @@ package com.ruoyi.customer.customermoney.service.impl;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Date;
|
||||
@@ -14,10 +15,12 @@ import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.customer.customerinfo.domain.BmsCustomer;
|
||||
import com.ruoyi.customer.customerinfo.domain.BmsCustomerAttachment;
|
||||
import com.ruoyi.customer.customerinfo.mapper.BmsCustomerMapper;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerAccount;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerCreditFlow;
|
||||
import com.ruoyi.customer.customermoney.domain.CustomerReceipt;
|
||||
import com.ruoyi.customer.customermoney.mapper.CustomerMoneyMapper;
|
||||
import com.ruoyi.customer.customermoney.service.ICustomerMoneyService;
|
||||
@@ -37,6 +40,8 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
|
||||
/** 本位币币种 */
|
||||
private static final String BASE_CURRENCY = BmsExchangeRate.BASE_CURRENCY;
|
||||
/** 新客户默认总额度(CNY) */
|
||||
private static final BigDecimal DEFAULT_CREDIT = new BigDecimal("1000");
|
||||
/** 金额保留位数 */
|
||||
private static final int MONEY_SCALE = 2;
|
||||
|
||||
@@ -50,9 +55,25 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
private IBmsExchangeRateService exchangeRateService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<CustomerAccount> selectAccountList(Long customerId)
|
||||
{
|
||||
return customerMoneyMapper.selectAccountListByCustomerId(customerId);
|
||||
List<CustomerAccount> list = customerMoneyMapper.selectAccountListByCustomerId(customerId);
|
||||
// 查询时顺带处理临时额度到期自动失效(仅CNY账户),保证卡片展示为最新值
|
||||
for (int i = 0; i < list.size(); i++)
|
||||
{
|
||||
CustomerAccount account = list.get(i);
|
||||
if (BASE_CURRENCY.equals(account.getCurrency()) && isTempCreditExpired(account))
|
||||
{
|
||||
CustomerAccount locked = customerMoneyMapper.selectAccountForUpdate(customerId, BASE_CURRENCY);
|
||||
expireTempCreditIfNeeded(locked);
|
||||
if (locked != null)
|
||||
{
|
||||
list.set(i, locked);
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -167,23 +188,8 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
? receipt.getBaseAmount()
|
||||
: originalAmount.multiply(rate).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
|
||||
// 1. 行锁锁定客户本位币(CNY)账户,不存在则自动初始化。无论收什么币种,一律折算 CNY 入账
|
||||
CustomerAccount account = customerMoneyMapper.selectAccountForUpdate(receipt.getCustomerId(), BASE_CURRENCY);
|
||||
if (account == null)
|
||||
{
|
||||
account = new CustomerAccount();
|
||||
account.setCustomerId(receipt.getCustomerId());
|
||||
account.setCurrency(BASE_CURRENCY);
|
||||
account.setBalance(BigDecimal.ZERO);
|
||||
account.setFrozenBalance(BigDecimal.ZERO);
|
||||
account.setTotalCredit(BigDecimal.ZERO);
|
||||
account.setExtraCredit(BigDecimal.ZERO);
|
||||
account.setUsedCredit(BigDecimal.ZERO);
|
||||
account.setTax(BigDecimal.ZERO);
|
||||
account.setVersion(0);
|
||||
account.setCreateBy(username);
|
||||
customerMoneyMapper.insertAccount(account);
|
||||
}
|
||||
// 1. 行锁锁定客户本位币(CNY)账户,不存在则按客户档案额度自动初始化。无论收什么币种,一律折算 CNY 入账
|
||||
CustomerAccount account = getOrInitCnyAccount(receipt.getCustomerId(), username);
|
||||
|
||||
// 2. 本位币余额变动(预充值/混合增加余额;还款核销释放已用额度)
|
||||
BigDecimal beforeBalance = nz(account.getBalance());
|
||||
@@ -191,12 +197,8 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
BigDecimal frozenBalance = nz(account.getFrozenBalance());
|
||||
|
||||
account.setBalance(afterBalance);
|
||||
if (CustomerReceipt.TYPE_REPAY.equals(receipt.getReceiptType()))
|
||||
{
|
||||
account.setUsedCredit(nz(account.getUsedCredit()).subtract(baseAmount).max(BigDecimal.ZERO));
|
||||
}
|
||||
account.setUpdateBy(username);
|
||||
|
||||
// 额度占用统一以账单为准:收款审核只增加余额,核销账单时才释放已用额度
|
||||
int accountRows = customerMoneyMapper.updateAccountBalance(account);
|
||||
if (accountRows == 0)
|
||||
{
|
||||
@@ -260,6 +262,503 @@ public class CustomerMoneyServiceImpl implements ICustomerMoneyService
|
||||
return customerMoneyMapper.selectFlowList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CustomerCreditFlow> selectCreditFlowList(CustomerCreditFlow query)
|
||||
{
|
||||
return customerMoneyMapper.selectCreditFlowList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int adjustCredit(CustomerCreditFlow cmd)
|
||||
{
|
||||
if (cmd == null || cmd.getCustomerId() == null)
|
||||
{
|
||||
throw new ServiceException("客户ID不能为空");
|
||||
}
|
||||
Long customerId = cmd.getCustomerId();
|
||||
String creditType = cmd.getCreditType();
|
||||
if (!CustomerCreditFlow.CREDIT_TYPE_CREDIT.equals(creditType)
|
||||
&& !CustomerCreditFlow.CREDIT_TYPE_TEMP.equals(creditType))
|
||||
{
|
||||
throw new ServiceException("额度类型不正确");
|
||||
}
|
||||
String adjustMode = cmd.getAdjustMode();
|
||||
if (!CustomerCreditFlow.MODE_SET.equals(adjustMode)
|
||||
&& !CustomerCreditFlow.MODE_ADD.equals(adjustMode)
|
||||
&& !CustomerCreditFlow.MODE_SUB.equals(adjustMode))
|
||||
{
|
||||
throw new ServiceException("调整方式不正确");
|
||||
}
|
||||
BigDecimal input = nz(cmd.getChangeAmount());
|
||||
if (input.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
throw new ServiceException("调整金额必须大于0");
|
||||
}
|
||||
if (!StringUtils.hasText(cmd.getReason()))
|
||||
{
|
||||
throw new ServiceException("请填写调整原因");
|
||||
}
|
||||
String reason = cmd.getReason().trim();
|
||||
if (reason.length() > 255)
|
||||
{
|
||||
throw new ServiceException("调整原因不能超过255个字");
|
||||
}
|
||||
String username = SecurityUtils.getUsername();
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
boolean isTemp = CustomerCreditFlow.CREDIT_TYPE_TEMP.equals(creditType);
|
||||
|
||||
// 1. 行锁锁定CNY账户(不存在则按客户档案额度初始化创建);先处理临时额度过期
|
||||
CustomerAccount account = getOrInitCnyAccount(customerId, username);
|
||||
expireTempCreditIfNeeded(account);
|
||||
|
||||
// 2. 计算调整结果
|
||||
BigDecimal before = isTemp ? nz(account.getExtraCredit()) : nz(account.getTotalCredit());
|
||||
BigDecimal otherCredit = isTemp ? nz(account.getTotalCredit()) : nz(account.getExtraCredit());
|
||||
BigDecimal after;
|
||||
switch (adjustMode)
|
||||
{
|
||||
case CustomerCreditFlow.MODE_SET:
|
||||
after = input;
|
||||
break;
|
||||
case CustomerCreditFlow.MODE_ADD:
|
||||
after = before.add(input);
|
||||
break;
|
||||
default:
|
||||
after = before.subtract(input);
|
||||
break;
|
||||
}
|
||||
after = after.setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
if (after.compareTo(BigDecimal.ZERO) < 0)
|
||||
{
|
||||
throw new ServiceException("调整后额度不能为负数");
|
||||
}
|
||||
if (otherCredit.add(after).compareTo(nz(account.getUsedCredit())) < 0)
|
||||
{
|
||||
throw new ServiceException("调整后授信总额度不能小于已用额度:"
|
||||
+ nz(account.getUsedCredit()).toPlainString());
|
||||
}
|
||||
if (after.compareTo(before) == 0)
|
||||
{
|
||||
throw new ServiceException("额度未发生变化,无需调整");
|
||||
}
|
||||
|
||||
// 3. 临时额度有效期校验(不允许预约未来生效,失效日必须晚于生效日)
|
||||
Date effectiveDate = null;
|
||||
Date expireDate = null;
|
||||
if (isTemp)
|
||||
{
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDate eff = cmd.getEffectiveDate() != null ? toLocalDate(cmd.getEffectiveDate()) : today;
|
||||
if (eff.isAfter(today))
|
||||
{
|
||||
throw new ServiceException("生效日期不能晚于今天,需要未来生效请在生效当天再设置");
|
||||
}
|
||||
effectiveDate = toSqlDate(eff);
|
||||
if (cmd.getExpireDate() != null)
|
||||
{
|
||||
LocalDate exp = toLocalDate(cmd.getExpireDate());
|
||||
if (!exp.isAfter(eff))
|
||||
{
|
||||
throw new ServiceException("失效日期必须晚于生效日期");
|
||||
}
|
||||
expireDate = toSqlDate(exp);
|
||||
}
|
||||
account.setExtraCredit(after);
|
||||
account.setExtraEffectiveDate(effectiveDate);
|
||||
account.setExtraExpireDate(expireDate);
|
||||
}
|
||||
else
|
||||
{
|
||||
account.setTotalCredit(after);
|
||||
}
|
||||
account.setUpdateBy(username);
|
||||
|
||||
// 4. 乐观锁更新额度
|
||||
int rows = customerMoneyMapper.updateAccountCredit(account);
|
||||
if (rows == 0)
|
||||
{
|
||||
throw new ServiceException("账户额度已被其他操作变动,请刷新后重试");
|
||||
}
|
||||
account.setVersion(account.getVersion() + 1);
|
||||
|
||||
// 5. 回写客户档案额度字段(额度唯一数据源为账户,档案仅做同步展示)
|
||||
bmsCustomerMapper.updateCustomerCredit(customerId,
|
||||
nz(account.getTotalCredit()), nz(account.getExtraCredit()), username);
|
||||
|
||||
// 6. 写额度流水
|
||||
CustomerCreditFlow flow = new CustomerCreditFlow();
|
||||
flow.setFlowNo(generateNo("EDLS"));
|
||||
flow.setCustomerId(customerId);
|
||||
flow.setAccountId(account.getId());
|
||||
flow.setCreditType(creditType);
|
||||
flow.setChangeType(CustomerCreditFlow.CHANGE_ADJUST);
|
||||
int direction = after.compareTo(before) > 0 ? 1 : -1;
|
||||
flow.setDirection(direction);
|
||||
flow.setBeforeAmount(before);
|
||||
flow.setChangeAmount(after.subtract(before).abs());
|
||||
flow.setAfterAmount(after);
|
||||
flow.setUsedCredit(nz(account.getUsedCredit()));
|
||||
flow.setEffectiveDate(effectiveDate);
|
||||
flow.setExpireDate(expireDate);
|
||||
flow.setReason(reason);
|
||||
flow.setAdjustMode(adjustMode);
|
||||
flow.setOperatorId(userId);
|
||||
flow.setCreateBy(username);
|
||||
flow.setRemark(cmd.getRemark());
|
||||
flow.setDelFlag("0");
|
||||
customerMoneyMapper.insertCreditFlow(flow);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ================= 账单联动:额度占用 / 余额核销 ================= */
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void occupyCredit(Long customerId, String currency, BigDecimal amount)
|
||||
{
|
||||
if (customerId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
String ccny = StringUtils.hasText(currency) ? currency.trim().toUpperCase() : BASE_CURRENCY;
|
||||
CustomerAccount account = lockAccountForBill(customerId, ccny, SecurityUtils.getUsername());
|
||||
if (account == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
account.setUsedCredit(nz(account.getUsedCredit()).add(amount).setScale(MONEY_SCALE, RoundingMode.HALF_UP));
|
||||
account.setUpdateBy(SecurityUtils.getUsername());
|
||||
if (customerMoneyMapper.updateAccountBalance(account) == 0)
|
||||
{
|
||||
throw new ServiceException("账户已被其他操作变动,请刷新后重试");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void releaseCredit(Long customerId, String currency, BigDecimal amount)
|
||||
{
|
||||
if (customerId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
String ccny = StringUtils.hasText(currency) ? currency.trim().toUpperCase() : BASE_CURRENCY;
|
||||
CustomerAccount account = lockAccountForBill(customerId, ccny, SecurityUtils.getUsername());
|
||||
if (account == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
account.setUsedCredit(nz(account.getUsedCredit()).subtract(amount).max(BigDecimal.ZERO)
|
||||
.setScale(MONEY_SCALE, RoundingMode.HALF_UP));
|
||||
account.setUpdateBy(SecurityUtils.getUsername());
|
||||
if (customerMoneyMapper.updateAccountBalance(account) == 0)
|
||||
{
|
||||
throw new ServiceException("账户已被其他操作变动,请刷新后重试");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public CustomerBalanceFlow payBill(Long customerId, String currency, BigDecimal amount,
|
||||
Long bizId, String bizNo, String remark)
|
||||
{
|
||||
if (customerId == null)
|
||||
{
|
||||
throw new ServiceException("客户ID不能为空");
|
||||
}
|
||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
throw new ServiceException("核销金额必须大于0");
|
||||
}
|
||||
String ccny = StringUtils.hasText(currency) ? currency.trim().toUpperCase() : BASE_CURRENCY;
|
||||
String username = SecurityUtils.getUsername();
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
CustomerAccount account = lockAccountForBill(customerId, ccny, username);
|
||||
if (account == null)
|
||||
{
|
||||
throw new ServiceException("客户没有" + ccny + "账户,无法用余额核销");
|
||||
}
|
||||
|
||||
BigDecimal beforeBalance = nz(account.getBalance());
|
||||
if (beforeBalance.compareTo(amount) < 0)
|
||||
{
|
||||
throw new ServiceException("账户余额不足,当前" + ccny + "余额:" + beforeBalance.toPlainString());
|
||||
}
|
||||
BigDecimal afterBalance = beforeBalance.subtract(amount);
|
||||
BigDecimal frozenBalance = nz(account.getFrozenBalance());
|
||||
// 核销账单 = 应收被清偿,同步释放等额授信占用
|
||||
account.setBalance(afterBalance);
|
||||
account.setUsedCredit(nz(account.getUsedCredit()).subtract(amount).max(BigDecimal.ZERO)
|
||||
.setScale(MONEY_SCALE, RoundingMode.HALF_UP));
|
||||
account.setUpdateBy(username);
|
||||
if (customerMoneyMapper.updateAccountBalance(account) == 0)
|
||||
{
|
||||
throw new ServiceException("账户余额已被其他操作变动,请刷新后重试");
|
||||
}
|
||||
|
||||
CustomerBalanceFlow flow = newBalanceFlow(account, ccny, amount, -1,
|
||||
beforeBalance, afterBalance, frozenBalance, frozenBalance,
|
||||
bizId, bizNo, username, userId, remark);
|
||||
customerMoneyMapper.insertFlow(flow);
|
||||
return flow;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public CustomerBalanceFlow reverseBillPay(Long customerId, String currency, BigDecimal amount,
|
||||
Long bizId, String bizNo, String remark)
|
||||
{
|
||||
if (customerId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
throw new ServiceException("反核销金额不正确");
|
||||
}
|
||||
String ccny = StringUtils.hasText(currency) ? currency.trim().toUpperCase() : BASE_CURRENCY;
|
||||
String username = SecurityUtils.getUsername();
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
CustomerAccount account = lockAccountForBill(customerId, ccny, username);
|
||||
if (account == null)
|
||||
{
|
||||
throw new ServiceException("客户" + ccny + "账户不存在,无法反核销");
|
||||
}
|
||||
|
||||
BigDecimal beforeBalance = nz(account.getBalance());
|
||||
BigDecimal afterBalance = beforeBalance.add(amount);
|
||||
BigDecimal frozenBalance = nz(account.getFrozenBalance());
|
||||
// 账单恢复为未核销,授信占用同步加回(允许与存量数据一样出现超出总额度的占用状态)
|
||||
account.setBalance(afterBalance);
|
||||
account.setUsedCredit(nz(account.getUsedCredit()).add(amount).setScale(MONEY_SCALE, RoundingMode.HALF_UP));
|
||||
account.setUpdateBy(username);
|
||||
if (customerMoneyMapper.updateAccountBalance(account) == 0)
|
||||
{
|
||||
throw new ServiceException("账户余额已被其他操作变动,请刷新后重试");
|
||||
}
|
||||
|
||||
CustomerBalanceFlow flow = newBalanceFlow(account, ccny, amount, 1,
|
||||
beforeBalance, afterBalance, frozenBalance, frozenBalance,
|
||||
bizId, bizNo, username, userId, remark);
|
||||
customerMoneyMapper.insertFlow(flow);
|
||||
return flow;
|
||||
}
|
||||
|
||||
/** 账单联动场景锁定账户:CNY不存在则初始化(先处理临时额度过期),外币账户不存在返回null */
|
||||
private CustomerAccount lockAccountForBill(Long customerId, String currency, String username)
|
||||
{
|
||||
if (BASE_CURRENCY.equals(currency))
|
||||
{
|
||||
CustomerAccount account = getOrInitCnyAccount(customerId, username);
|
||||
expireTempCreditIfNeeded(account);
|
||||
return account;
|
||||
}
|
||||
return customerMoneyMapper.selectAccountForUpdate(customerId, currency);
|
||||
}
|
||||
|
||||
private CustomerBalanceFlow newBalanceFlow(CustomerAccount account, String currency, BigDecimal amount,
|
||||
int direction, BigDecimal beforeBalance, BigDecimal afterBalance,
|
||||
BigDecimal beforeFrozen, BigDecimal afterFrozen,
|
||||
Long bizId, String bizNo, String username, Long userId, String remark)
|
||||
{
|
||||
CustomerBalanceFlow flow = new CustomerBalanceFlow();
|
||||
flow.setFlowNo(generateNo("LS"));
|
||||
flow.setCustomerId(account.getCustomerId());
|
||||
flow.setAccountId(account.getId());
|
||||
flow.setCurrency(currency);
|
||||
flow.setExchangeRate(BigDecimal.ONE);
|
||||
flow.setBaseAmount(amount);
|
||||
flow.setBizType(CustomerBalanceFlow.BIZ_BILL_PAY);
|
||||
flow.setBizId(bizId);
|
||||
flow.setBizNo(bizNo);
|
||||
flow.setDirection(direction);
|
||||
flow.setAmount(amount);
|
||||
flow.setBeforeBalance(beforeBalance);
|
||||
flow.setAfterBalance(afterBalance);
|
||||
flow.setBeforeFrozenBalance(beforeFrozen);
|
||||
flow.setAfterFrozenBalance(afterFrozen);
|
||||
flow.setOperatorId(userId);
|
||||
flow.setCreateBy(username);
|
||||
flow.setRemark(remark);
|
||||
return flow;
|
||||
}
|
||||
|
||||
/* ================= 额度私有方法 ================= */
|
||||
|
||||
/**
|
||||
* 新建客户时默认初始化CNY账户:余额0,总额度默认1000 CNY
|
||||
* (客户档案已填信用额度且大于0时按档案值),并写 INIT 流水;已存在则跳过。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int initDefaultCnyAccount(Long customerId)
|
||||
{
|
||||
if (customerId == null)
|
||||
{
|
||||
throw new ServiceException("客户ID不能为空");
|
||||
}
|
||||
String username = SecurityUtils.getUsername();
|
||||
CustomerAccount account = customerMoneyMapper.selectAccountForUpdate(customerId, BASE_CURRENCY);
|
||||
if (account != null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
BmsCustomer customer = bmsCustomerMapper.selectRawCreditByCustomerId(customerId);
|
||||
if (customer == null)
|
||||
{
|
||||
throw new ServiceException("客户不存在");
|
||||
}
|
||||
BigDecimal initCredit = nz(customer.getCreditEd());
|
||||
if (initCredit.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
initCredit = DEFAULT_CREDIT;
|
||||
}
|
||||
BigDecimal initTemp = nz(customer.getTempEd());
|
||||
account = createCnyAccount(customerId, initCredit, initTemp, username);
|
||||
// 档案额度与实际初始额度保持一致
|
||||
bmsCustomerMapper.updateCustomerCredit(customerId, initCredit, initTemp, username);
|
||||
writeInitFlow(account, CustomerCreditFlow.CREDIT_TYPE_CREDIT,
|
||||
initCredit, null, null, username, "新客户默认初始化额度1000 CNY");
|
||||
if (initTemp.compareTo(BigDecimal.ZERO) > 0)
|
||||
{
|
||||
writeInitFlow(account, CustomerCreditFlow.CREDIT_TYPE_TEMP,
|
||||
initTemp, null, null, username, "新客户初始化临时额度");
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/** 行锁获取客户CNY账户,不存在则按客户档案当前额度初始化创建 */
|
||||
private CustomerAccount getOrInitCnyAccount(Long customerId, String username)
|
||||
{
|
||||
CustomerAccount account = customerMoneyMapper.selectAccountForUpdate(customerId, BASE_CURRENCY);
|
||||
if (account == null)
|
||||
{
|
||||
BmsCustomer customer = bmsCustomerMapper.selectRawCreditByCustomerId(customerId);
|
||||
if (customer == null)
|
||||
{
|
||||
throw new ServiceException("客户不存在");
|
||||
}
|
||||
BigDecimal initCredit = nz(customer.getCreditEd());
|
||||
BigDecimal initTemp = nz(customer.getTempEd());
|
||||
account = createCnyAccount(customerId, initCredit, initTemp, username);
|
||||
writeInitFlow(account, CustomerCreditFlow.CREDIT_TYPE_CREDIT,
|
||||
initCredit, null, null, username, "首次创建CNY账户,从客户档案同步初始额度");
|
||||
if (initTemp.compareTo(BigDecimal.ZERO) > 0)
|
||||
{
|
||||
writeInitFlow(account, CustomerCreditFlow.CREDIT_TYPE_TEMP,
|
||||
initTemp, null, null, username, "首次创建CNY账户,从客户档案同步临时额度");
|
||||
}
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
/** 创建CNY账户(余额0,已用0) */
|
||||
private CustomerAccount createCnyAccount(Long customerId, BigDecimal totalCredit,
|
||||
BigDecimal extraCredit, String username)
|
||||
{
|
||||
CustomerAccount account = new CustomerAccount();
|
||||
account.setCustomerId(customerId);
|
||||
account.setCurrency(BASE_CURRENCY);
|
||||
account.setBalance(BigDecimal.ZERO);
|
||||
account.setFrozenBalance(BigDecimal.ZERO);
|
||||
account.setTotalCredit(totalCredit);
|
||||
account.setExtraCredit(extraCredit);
|
||||
account.setUsedCredit(BigDecimal.ZERO);
|
||||
account.setTax(BigDecimal.ZERO);
|
||||
account.setVersion(0);
|
||||
account.setCreateBy(username);
|
||||
customerMoneyMapper.insertAccount(account);
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* 账户首次初始化时写额度 INIT 流水(从客户档案额度同步)
|
||||
*/
|
||||
private void writeInitFlow(CustomerAccount account, String creditType,
|
||||
BigDecimal initAmount, Date effectiveDate, Date expireDate,
|
||||
String username, String remark)
|
||||
{
|
||||
CustomerCreditFlow flow = new CustomerCreditFlow();
|
||||
flow.setFlowNo(generateNo("EDLS"));
|
||||
flow.setCustomerId(account.getCustomerId());
|
||||
flow.setAccountId(account.getId());
|
||||
flow.setCreditType(creditType);
|
||||
flow.setChangeType(CustomerCreditFlow.CHANGE_INIT);
|
||||
flow.setDirection(initAmount.compareTo(BigDecimal.ZERO) > 0 ? 1 : 0);
|
||||
flow.setBeforeAmount(BigDecimal.ZERO);
|
||||
flow.setChangeAmount(initAmount);
|
||||
flow.setAfterAmount(initAmount);
|
||||
flow.setUsedCredit(nz(account.getUsedCredit()));
|
||||
flow.setEffectiveDate(effectiveDate);
|
||||
flow.setExpireDate(expireDate);
|
||||
flow.setReason("账户初始化");
|
||||
flow.setOperatorId(SecurityUtils.getUserId());
|
||||
flow.setCreateBy(username);
|
||||
flow.setRemark(remark);
|
||||
flow.setDelFlag("0");
|
||||
customerMoneyMapper.insertCreditFlow(flow);
|
||||
}
|
||||
|
||||
/** 临时额度是否已过期:有失效日期且今天已晚于失效日(失效日当天仍有效) */
|
||||
private boolean isTempCreditExpired(CustomerAccount account)
|
||||
{
|
||||
return account != null
|
||||
&& nz(account.getExtraCredit()).compareTo(BigDecimal.ZERO) > 0
|
||||
&& account.getExtraExpireDate() != null
|
||||
&& LocalDate.now().isAfter(toLocalDate(account.getExtraExpireDate()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 临时额度到期自动失效:额度清零 -> 回写客户档案 -> 写EXPIRE流水。
|
||||
* 入参必须是事务内行锁查出的账户。
|
||||
*/
|
||||
private void expireTempCreditIfNeeded(CustomerAccount account)
|
||||
{
|
||||
if (account == null || !isTempCreditExpired(account))
|
||||
{
|
||||
return;
|
||||
}
|
||||
String username = SecurityUtils.getUsername();
|
||||
BigDecimal before = nz(account.getExtraCredit());
|
||||
account.setExtraCredit(BigDecimal.ZERO);
|
||||
account.setUpdateBy(username);
|
||||
int rows = customerMoneyMapper.updateAccountCredit(account);
|
||||
if (rows == 0)
|
||||
{
|
||||
throw new ServiceException("账户额度已被其他操作变动,请刷新后重试");
|
||||
}
|
||||
account.setVersion(account.getVersion() + 1);
|
||||
bmsCustomerMapper.updateCustomerCredit(account.getCustomerId(),
|
||||
nz(account.getTotalCredit()), BigDecimal.ZERO, username);
|
||||
|
||||
CustomerCreditFlow flow = new CustomerCreditFlow();
|
||||
flow.setFlowNo(generateNo("EDLS"));
|
||||
flow.setCustomerId(account.getCustomerId());
|
||||
flow.setAccountId(account.getId());
|
||||
flow.setCreditType(CustomerCreditFlow.CREDIT_TYPE_TEMP);
|
||||
flow.setChangeType(CustomerCreditFlow.CHANGE_EXPIRE);
|
||||
flow.setDirection(-1);
|
||||
flow.setBeforeAmount(before);
|
||||
flow.setChangeAmount(before);
|
||||
flow.setAfterAmount(BigDecimal.ZERO);
|
||||
flow.setUsedCredit(nz(account.getUsedCredit()));
|
||||
flow.setEffectiveDate(account.getExtraEffectiveDate());
|
||||
flow.setExpireDate(account.getExtraExpireDate());
|
||||
flow.setReason("临时额度到期自动失效");
|
||||
flow.setOperatorId(SecurityUtils.getUserId());
|
||||
flow.setCreateBy(username);
|
||||
flow.setRemark("系统在访问账户时检测到临时额度已过失效日期,自动置零");
|
||||
flow.setDelFlag("0");
|
||||
customerMoneyMapper.insertCreditFlow(flow);
|
||||
}
|
||||
|
||||
private LocalDate toLocalDate(Date date)
|
||||
{
|
||||
return date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate();
|
||||
}
|
||||
|
||||
private Date toSqlDate(LocalDate localDate)
|
||||
{
|
||||
return java.sql.Date.valueOf(localDate);
|
||||
}
|
||||
|
||||
/* ================= 私有方法 ================= */
|
||||
|
||||
/** 保存收款附件到客户附件表 */
|
||||
|
||||
Reference in New Issue
Block a user