feat(edu): 缴费计划 Service 实现-生成/查询/改/收款联动/作废
This commit is contained in:
parent
b52ab2eb40
commit
abfb425b4f
|
|
@ -0,0 +1,212 @@
|
|||
package org.dromara.edu.application.service.contract.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.edu.application.dto.EduPaymentPlanBo;
|
||||
import org.dromara.edu.application.dto.PaymentReceiveBo;
|
||||
import org.dromara.edu.application.service.contract.IEduPaymentPlanService;
|
||||
import org.dromara.edu.common.constant.EduConstants;
|
||||
import org.dromara.edu.common.exception.EduBusinessException;
|
||||
import org.dromara.edu.domain.model.contract.EduContract;
|
||||
import org.dromara.edu.domain.model.contract.EduContractLog;
|
||||
import org.dromara.edu.domain.model.contract.EduContractPaymentPlan;
|
||||
import org.dromara.edu.infrastructure.persistence.mapper.EduContractLogMapper;
|
||||
import org.dromara.edu.infrastructure.persistence.mapper.EduContractMapper;
|
||||
import org.dromara.edu.infrastructure.persistence.mapper.EduContractPaymentPlanMapper;
|
||||
import org.dromara.edu.interfaces.vo.EduContractPaymentPlanVo;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 合同缴费计划 业务实现
|
||||
* <p>分期合同的应收节点生成 + 收款联动合同账本 + 退费/作废联动作废</p>
|
||||
*
|
||||
* @author FIGS
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class EduPaymentPlanServiceImpl implements IEduPaymentPlanService {
|
||||
|
||||
private final EduContractPaymentPlanMapper paymentPlanMapper;
|
||||
private final EduContractMapper contractMapper;
|
||||
private final EduContractLogMapper contractLogMapper;
|
||||
|
||||
/**
|
||||
* 生成缴费计划(幂等: 先删后建)
|
||||
* <p>金额: 首期吸收尾差, 保证各期之和精确等于 totalAmount;
|
||||
* 日期: 首期=签约日, 第N期=签约日+(N-1)*30天</p>
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void generatePaymentPlan(Long contractId, int installmentCount) {
|
||||
EduContract contract = contractMapper.selectById(contractId);
|
||||
if (contract == null) {
|
||||
throw new EduBusinessException("CONTRACT_NOT_FOUND", "合同不存在");
|
||||
}
|
||||
if (installmentCount != 2 && installmentCount != 3) {
|
||||
throw new EduBusinessException("INSTALLMENT_COUNT_INVALID", "分期数必须为 2 或 3");
|
||||
}
|
||||
// 幂等: 先删后建
|
||||
paymentPlanMapper.delete(new LambdaQueryWrapper<EduContractPaymentPlan>()
|
||||
.eq(EduContractPaymentPlan::getContractId, contractId));
|
||||
|
||||
BigDecimal totalAmount = nz(contract.getTotalAmount());
|
||||
// base = totalAmount / N, 第2~N期取 base, 第1期吸收尾差
|
||||
BigDecimal base = totalAmount.divide(BigDecimal.valueOf(installmentCount), 2, RoundingMode.HALF_UP);
|
||||
BigDecimal firstAmount = totalAmount.subtract(base.multiply(BigDecimal.valueOf(installmentCount - 1)));
|
||||
|
||||
LocalDate signLocal = toLocalDate(contract.getSignDate());
|
||||
for (int i = 1; i <= installmentCount; i++) {
|
||||
EduContractPaymentPlan plan = new EduContractPaymentPlan();
|
||||
plan.setContractId(contractId);
|
||||
plan.setInstallmentNo(i);
|
||||
plan.setPlanAmount(i == 1 ? firstAmount : base);
|
||||
plan.setPlanDate(toDate(signLocal.plus(30L * (i - 1), ChronoUnit.DAYS)));
|
||||
plan.setPaidAmount(BigDecimal.ZERO);
|
||||
plan.setStatus(EduConstants.PaymentPlanStatus.PENDING);
|
||||
paymentPlanMapper.insert(plan);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询合同的计划列表(按期次升序)
|
||||
*/
|
||||
@Override
|
||||
public List<EduContractPaymentPlanVo> selectPlanList(Long contractId) {
|
||||
return paymentPlanMapper.selectVoList(new LambdaQueryWrapper<EduContractPaymentPlan>()
|
||||
.eq(EduContractPaymentPlan::getContractId, contractId)
|
||||
.orderByAsc(EduContractPaymentPlan::getInstallmentNo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改单期计划(仅待收态可改应收金额/应付日期)
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int updatePlan(EduPaymentPlanBo bo) {
|
||||
EduContractPaymentPlan exist = paymentPlanMapper.selectById(bo.getId());
|
||||
if (exist == null) {
|
||||
throw new EduBusinessException("PLAN_NOT_FOUND", "缴费计划不存在");
|
||||
}
|
||||
if (!EduConstants.PaymentPlanStatus.PENDING.equals(exist.getStatus())) {
|
||||
throw new EduBusinessException("PLAN_STATUS_NOT_EDITABLE", "仅待收态计划可修改");
|
||||
}
|
||||
EduContractPaymentPlan update = new EduContractPaymentPlan();
|
||||
update.setId(bo.getId());
|
||||
if (bo.getPlanAmount() != null) {
|
||||
update.setPlanAmount(bo.getPlanAmount());
|
||||
}
|
||||
if (bo.getPlanDate() != null) {
|
||||
update.setPlanDate(bo.getPlanDate());
|
||||
}
|
||||
update.setRemark(bo.getRemark());
|
||||
return paymentPlanMapper.updateById(update);
|
||||
}
|
||||
|
||||
/**
|
||||
* 收款确认: 待收 → 已收, 联动合同账本
|
||||
* <p>1. 置该期已收, 记 paidAmount/paidDate/paymentMethod;
|
||||
* 2. 重算合同 paidAmount = SUM(已收期 paidAmount);
|
||||
* 3. 重算 paymentStatus: 全额=已支付(2), 否则部分支付(1);
|
||||
* 4. 写合同流水(action=RECEIVE_PAYMENT).</p>
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int receivePayment(PaymentReceiveBo bo) {
|
||||
EduContractPaymentPlan plan = paymentPlanMapper.selectById(bo.getId());
|
||||
if (plan == null) {
|
||||
throw new EduBusinessException("PLAN_NOT_FOUND", "缴费计划不存在");
|
||||
}
|
||||
if (!EduConstants.PaymentPlanStatus.PENDING.equals(plan.getStatus())) {
|
||||
throw new EduBusinessException("PLAN_STATUS_NOT_RECEIVABLE", "仅待收态计划可收款");
|
||||
}
|
||||
// 1. 更新该期
|
||||
EduContractPaymentPlan update = new EduContractPaymentPlan();
|
||||
update.setId(plan.getId());
|
||||
update.setStatus(EduConstants.PaymentPlanStatus.RECEIVED);
|
||||
update.setPaidAmount(bo.getPaidAmount());
|
||||
update.setPaymentMethod(bo.getPaymentMethod());
|
||||
update.setPaidDate(bo.getPaidDate() != null ? bo.getPaidDate() : new Date());
|
||||
update.setRemark(bo.getRemark());
|
||||
int rows = paymentPlanMapper.updateById(update);
|
||||
|
||||
// 2. 重算合同实收: SUM(已收期 paidAmount), 本期刚置为已收, 库里已为最新值
|
||||
List<EduContractPaymentPlan> all = paymentPlanMapper.selectList(
|
||||
new LambdaQueryWrapper<EduContractPaymentPlan>()
|
||||
.eq(EduContractPaymentPlan::getContractId, plan.getContractId()));
|
||||
BigDecimal receivedSum = all.stream()
|
||||
.filter(p -> EduConstants.PaymentPlanStatus.RECEIVED.equals(p.getStatus()))
|
||||
.map(p -> nz(p.getPaidAmount()))
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
// 3. 重算 paymentStatus 并回写合同
|
||||
EduContract contract = contractMapper.selectById(plan.getContractId());
|
||||
BigDecimal total = nz(contract.getTotalAmount());
|
||||
String paymentStatus = receivedSum.compareTo(total) >= 0 ? "2" : "1";
|
||||
EduContract contractUpd = new EduContract();
|
||||
contractUpd.setId(contract.getId());
|
||||
contractUpd.setPaidAmount(receivedSum);
|
||||
contractUpd.setPaymentStatus(paymentStatus);
|
||||
contractMapper.updateById(contractUpd);
|
||||
|
||||
// 4. 写合同流水
|
||||
EduContractLog log = new EduContractLog();
|
||||
log.setContractId(plan.getContractId());
|
||||
log.setAction(EduConstants.ContractAction.RECEIVE_PAYMENT);
|
||||
log.setAmount(bo.getPaidAmount());
|
||||
log.setOperatorId(LoginHelper.getUserId());
|
||||
log.setOperatorName(LoginHelper.getUsername());
|
||||
log.setReason("第" + plan.getInstallmentNo() + "期收款, 金额 " + bo.getPaidAmount()
|
||||
+ (bo.getPaymentMethod() != null ? ", 方式 " + bo.getPaymentMethod() : ""));
|
||||
log.setLogTime(new Date());
|
||||
contractLogMapper.insert(log);
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量作废未收期次(合同退费/作废联动)
|
||||
* <p>仅把"待收"期置"已作废", "已收"期保留作为历史收款记录</p>
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int voidPendingPlans(Long contractId) {
|
||||
List<EduContractPaymentPlan> pending = paymentPlanMapper.selectList(
|
||||
new LambdaQueryWrapper<EduContractPaymentPlan>()
|
||||
.eq(EduContractPaymentPlan::getContractId, contractId)
|
||||
.eq(EduContractPaymentPlan::getStatus, EduConstants.PaymentPlanStatus.PENDING));
|
||||
int rows = 0;
|
||||
for (EduContractPaymentPlan p : pending) {
|
||||
EduContractPaymentPlan upd = new EduContractPaymentPlan();
|
||||
upd.setId(p.getId());
|
||||
upd.setStatus(EduConstants.PaymentPlanStatus.VOIDED);
|
||||
rows += paymentPlanMapper.updateById(upd);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 私有辅助
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private BigDecimal nz(BigDecimal v) {
|
||||
return v == null ? BigDecimal.ZERO : v;
|
||||
}
|
||||
|
||||
private LocalDate toLocalDate(Date date) {
|
||||
return date == null ? LocalDate.now() : date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
|
||||
}
|
||||
|
||||
private Date toDate(LocalDate localDate) {
|
||||
return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user