Compare commits

...

5 Commits

14 changed files with 715 additions and 5 deletions

View File

@ -122,6 +122,11 @@ public class EduContractBo extends BaseEntity {
*/
private BigDecimal installmentThird;
/**
* 分期数(2 3), isInstallment=Y 时必填, 用于生成缴费计划
*/
private Integer installmentCount;
/**
* 实际总收费
*/
@ -133,10 +138,15 @@ public class EduContractBo extends BaseEntity {
private Date settlementDate;
/**
* 签约负责人
* 签约负责人(姓名, 旧字段保留兼容)
*/
private String signPerson;
/**
* 签约负责人ID(edu_staff.id)
*/
private Long signUserId;
/**
* 合同是否已收回 Y/N
*/

View File

@ -0,0 +1,44 @@
package org.dromara.edu.application.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 缴费计划修改业务对象
* <p>仅待收态可改应收金额/应付日期</p>
*
* @author FIGS
*/
@Data
public class EduPaymentPlanBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 计划ID
*/
@NotNull(message = "计划ID不能为空")
private Long id;
/**
* 本期应收金额
*/
private BigDecimal planAmount;
/**
* 本期应付日期
*/
private Date planDate;
/**
* 备注
*/
private String remark;
}

View File

@ -0,0 +1,50 @@
package org.dromara.edu.application.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 缴费计划收款确认业务对象
* <p>把一期计划从"待收"推进到"已收", 联动合同账本</p>
*
* @author FIGS
*/
@Data
public class PaymentReceiveBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 计划ID
*/
@NotNull(message = "计划ID不能为空")
private Long id;
/**
* 实收金额
*/
@NotNull(message = "实收金额不能为空")
private BigDecimal paidAmount;
/**
* 支付方式 CASH/WECHAT/ALIPAY/BANK/CARD
*/
private String paymentMethod;
/**
* 收款日期(留空取当前日期)
*/
private Date paidDate;
/**
* 备注
*/
private String remark;
}

View File

@ -0,0 +1,45 @@
package org.dromara.edu.application.service.contract;
import org.dromara.edu.application.dto.EduPaymentPlanBo;
import org.dromara.edu.application.dto.PaymentReceiveBo;
import org.dromara.edu.interfaces.vo.EduContractPaymentPlanVo;
import java.util.List;
/**
* 合同缴费计划 业务层
*
* @author FIGS
*/
public interface IEduPaymentPlanService {
/**
* 生成缴费计划(合同创建/修改时调用, 幂等: 先删后建)
*
* @param contractId 合同ID
* @param installmentCount 分期数 2 3
*/
void generatePaymentPlan(Long contractId, int installmentCount);
/**
* 查询合同的计划列表( installment_no 升序)
*/
List<EduContractPaymentPlanVo> selectPlanList(Long contractId);
/**
* 修改单期计划(仅待收态可改应收金额/应付日期)
*/
int updatePlan(EduPaymentPlanBo bo);
/**
* 收款确认: 联动合同账本(累加 paidAmount, 重算 paymentStatus, PAY 流水)
*/
int receivePayment(PaymentReceiveBo bo);
/**
* 批量作废未收期次(合同退费/作废时联动调用)
* <p>仅把"待收"期置"已作废", "已收"期保留不动</p>
*/
int voidPendingPlans(Long contractId);
}

View File

@ -34,6 +34,7 @@ import org.dromara.edu.infrastructure.persistence.mapper.EduContractMapper;
import org.dromara.edu.infrastructure.persistence.mapper.EduContractPackageMapper;
import org.dromara.edu.infrastructure.persistence.mapper.EduHoursLogMapper;
import org.dromara.edu.interfaces.vo.EduContractVo;
import org.dromara.edu.interfaces.vo.EduStaffVo;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -58,6 +59,8 @@ public class EduContractServiceImpl implements IEduContractService {
private final EduHoursLogMapper hoursLogMapper;
private final org.dromara.edu.application.service.service.IEduMilestoneService milestoneService;
private final org.dromara.edu.infrastructure.persistence.mapper.EduContractMilestoneMapper contractMilestoneMapper;
private final org.dromara.edu.application.service.contract.IEduPaymentPlanService paymentPlanService;
private final org.dromara.edu.infrastructure.persistence.mapper.EduStaffMapper staffMapper;
/**
* 分页查询合同列表
@ -65,6 +68,8 @@ public class EduContractServiceImpl implements IEduContractService {
@Override
public TableDataInfo<EduContractVo> selectPageContractList(EduContractBo contract, PageQuery pageQuery) {
Page<EduContractVo> page = contractMapper.selectPageContractList(pageQuery.build(), this.buildQueryWrapper(contract));
// 翻译顾问/管家/签约负责人姓名
fillStaffNames(page.getRecords());
return TableDataInfo.build(page);
}
@ -86,6 +91,10 @@ public class EduContractServiceImpl implements IEduContractService {
.orderByDesc(EduContractLog::getLogTime)));
// 回填里程碑(升学服务合同)
vo.setMilestones(milestoneService.selectContractMilestoneList(id));
// 回填缴费计划(分期合同)
vo.setPaymentPlans(paymentPlanService.selectPlanList(id));
// 翻译顾问/管家/签约负责人姓名
fillStaffNames(java.util.Collections.singletonList(vo));
return vo;
}
@ -142,6 +151,10 @@ public class EduContractServiceImpl implements IEduContractService {
// 记流水
recordLog(entity.getId(), EduConstants.ContractAction.CREATE,
totalAmount, BigDecimal.ZERO, null, "创建合同");
// 分期合同: 生成缴费计划(分期数取自入参 BO, 实体无此字段)
if ("Y".equals(contract.getIsInstallment()) && contract.getInstallmentCount() != null) {
paymentPlanService.generatePaymentPlan(entity.getId(), contract.getInstallmentCount());
}
return rows;
}
@ -183,6 +196,10 @@ public class EduContractServiceImpl implements IEduContractService {
entity.setTotalHours(totalHours);
entity.setRemainHours(totalHours.subtract(nz(exist.getUsedHours())));
entity.setGiftHours(giftHours);
// 分期合同: 重建缴费计划(幂等先删后建, 分期数取自入参 BO)
if ("Y".equals(contract.getIsInstallment()) && contract.getInstallmentCount() != null) {
paymentPlanService.generatePaymentPlan(contract.getId(), contract.getInstallmentCount());
}
return contractMapper.updateById(entity);
}
@ -204,6 +221,8 @@ public class EduContractServiceImpl implements IEduContractService {
update.setStatus(EduConstants.ContractStatus.VOIDED);
recordLog(id, EduConstants.ContractAction.VOID,
BigDecimal.ZERO, BigDecimal.ZERO, null, "作废合同");
// 联动作废未收期缴费计划(已收期保留为历史)
paymentPlanService.voidPendingPlans(id);
return contractMapper.updateById(update);
}
@ -343,6 +362,8 @@ public class EduContractServiceImpl implements IEduContractService {
bo.getRefundAmount().negate(), paidRemainHours.negate(), null,
String.format("合同退费%s元, 剩余付费课时%s退清, 赠送课时%s不退",
bo.getRefundAmount(), paidRemainHours, giftHours));
// 联动作废未收期缴费计划(已收期保留为历史收款记录)
paymentPlanService.voidPendingPlans(bo.getId());
return rows;
}
@ -582,6 +603,32 @@ public class EduContractServiceImpl implements IEduContractService {
return v == null ? nz(def) : v;
}
/**
* 批量翻译合同上的人员ID为姓名(顾问/管家/签约负责人)
* <p>收集所有非空 staff id, 一次 selectBatchIds 查出, 回填到 VO *Name 字段</p>
*/
private void fillStaffNames(java.util.List<EduContractVo> voList) {
if (voList == null || voList.isEmpty()) {
return;
}
java.util.Set<Long> staffIds = new java.util.HashSet<>();
for (EduContractVo vo : voList) {
if (vo.getSalesId() != null) staffIds.add(vo.getSalesId());
if (vo.getHousekeeperId() != null) staffIds.add(vo.getHousekeeperId());
if (vo.getSignUserId() != null) staffIds.add(vo.getSignUserId());
}
if (staffIds.isEmpty()) {
return;
}
java.util.Map<Long, String> id2name = staffMapper.selectVoByIds(staffIds).stream()
.collect(java.util.stream.Collectors.toMap(EduStaffVo::getId, EduStaffVo::getName, (a, b) -> a));
for (EduContractVo vo : voList) {
vo.setSalesName(id2name.get(vo.getSalesId()));
vo.setHousekeeperName(id2name.get(vo.getHousekeeperId()));
vo.setSignUserName(id2name.get(vo.getSignUserId()));
}
}
/**
* 构造查询条件
*/

View File

@ -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());
}
}

View File

@ -74,6 +74,17 @@ public final class EduConstants {
private MilestoneStatus() {}
}
/** 缴费计划状态 */
public static final class PaymentPlanStatus {
/** 待收 */
public static final String PENDING = "0";
/** 已收 */
public static final String RECEIVED = "1";
/** 已作废 */
public static final String VOIDED = "4";
private PaymentPlanStatus() {}
}
/** 合同状态 */
public static final class ContractStatus {
public static final String DRAFT = "0";
@ -97,6 +108,8 @@ public final class EduConstants {
public static final String ADDON = "ADDON";
public static final String EXPIRE = "EXPIRE";
public static final String VOID = "VOID";
/** 缴费计划收款确认 */
public static final String RECEIVE_PAYMENT = "RECEIVE_PAYMENT";
private ContractAction() {}
}

View File

@ -158,10 +158,15 @@ public class EduContract extends TenantEntity {
private Date settlementDate;
/**
* 签约负责人
* 签约负责人(姓名, 旧字段保留兼容)
*/
private String signPerson;
/**
* 签约负责人ID(edu_staff.id)
*/
private Long signUserId;
/**
* 合同是否已收回 Y/N
*/

View File

@ -0,0 +1,81 @@
package org.dromara.edu.domain.model.contract;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.tenant.core.TenantEntity;
import java.math.BigDecimal;
import java.util.Date;
/**
* 合同缴费计划 edu_contract_payment_plan
* <p>分期合同生成的应收节点: 每期应收金额/应付日期/收款状态, 收款确认联动合同账本</p>
*
* @author FIGS
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("edu_contract_payment_plan")
public class EduContractPaymentPlan extends TenantEntity {
/**
* 主键
*/
@TableId(value = "id")
private Long id;
/**
* 合同ID
*/
private Long contractId;
/**
* 期次号 1/2/3
*/
private Integer installmentNo;
/**
* 本期应收金额
*/
private BigDecimal planAmount;
/**
* 本期应付日期
*/
private Date planDate;
/**
* 本期实收金额
*/
private BigDecimal paidAmount;
/**
* 实际收款日期
*/
private Date paidDate;
/**
* 本期支付方式 CASH/WECHAT/ALIPAY/BANK/CARD
*/
private String paymentMethod;
/**
* 状态 0=待收 1=已收 4=已作废
*/
private String status;
/**
* 删除标志 0=存在 1=删除
*/
@TableLogic
private String delFlag;
/**
* 备注
*/
private String remark;
}

View File

@ -0,0 +1,14 @@
package org.dromara.edu.infrastructure.persistence.mapper;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.edu.domain.model.contract.EduContractPaymentPlan;
import org.dromara.edu.interfaces.vo.EduContractPaymentPlanVo;
/**
* 合同缴费计划 数据层
*
* @author FIGS
*/
public interface EduContractPaymentPlanMapper extends BaseMapperPlus<EduContractPaymentPlan, EduContractPaymentPlanVo> {
}

View File

@ -0,0 +1,68 @@
package org.dromara.edu.interfaces.controller.contract;
import cn.dev33.satoken.annotation.SaCheckPermission;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.web.core.BaseController;
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.interfaces.vo.EduContractPaymentPlanVo;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 合同缴费计划管理
*
* @author FIGS
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/edu/contract/payment-plan")
public class EduPaymentPlanController extends BaseController {
private final IEduPaymentPlanService paymentPlanService;
/**
* 查询某合同的缴费计划列表
*/
@SaCheckPermission("edu:contract:query")
@GetMapping("/list/{contractId}")
public R<List<EduContractPaymentPlanVo>> list(@PathVariable Long contractId) {
return R.ok(paymentPlanService.selectPlanList(contractId));
}
/**
* 修改单期计划(仅待收态可改应收金额/应付日期)
*/
@SaCheckPermission("edu:contract:edit")
@Log(title = "缴费计划", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping
public R<Void> edit(@Validated @RequestBody EduPaymentPlanBo bo) {
return toAjax(paymentPlanService.updatePlan(bo));
}
/**
* 收款确认(联动合同账本)
*/
@SaCheckPermission("edu:contract:activate")
@Log(title = "缴费计划", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping("/receive")
public R<Void> receive(@Validated @RequestBody PaymentReceiveBo bo) {
return toAjax(paymentPlanService.receivePayment(bo));
}
}

View File

@ -0,0 +1,79 @@
package org.dromara.edu.interfaces.vo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.dromara.edu.domain.model.contract.EduContractPaymentPlan;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 合同缴费计划视图对象 edu_contract_payment_plan
*
* @author FIGS
*/
@Data
@AutoMapper(target = EduContractPaymentPlan.class)
public class EduContractPaymentPlanVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
private Long id;
/**
* 合同ID
*/
private Long contractId;
/**
* 期次号 1/2/3
*/
private Integer installmentNo;
/**
* 本期应收金额
*/
private BigDecimal planAmount;
/**
* 本期应付日期
*/
private Date planDate;
/**
* 本期实收金额
*/
private BigDecimal paidAmount;
/**
* 实际收款日期
*/
private Date paidDate;
/**
* 本期支付方式 CASH/WECHAT/ALIPAY/BANK/CARD
*/
private String paymentMethod;
/**
* 状态 0=待收 1=已收 4=已作废
*/
private String status;
/**
* 备注
*/
private String remark;
/**
* 创建时间
*/
private Date createTime;
}

View File

@ -51,6 +51,8 @@ public class EduContractVo implements Serializable {
private BigDecimal actualTotalReceived;
private Date settlementDate;
private String signPerson;
/** 签约负责人ID(edu_staff.id) */
private Long signUserId;
private String contractRecovered;
private String remittanceName;
private String needInvoice;
@ -58,6 +60,12 @@ public class EduContractVo implements Serializable {
private String status;
private Long salesId;
private Long housekeeperId;
/** 课程顾问姓名(查询时翻译填充) */
private String salesName;
/** 管家姓名(查询时翻译填充) */
private String housekeeperName;
/** 签约负责人姓名(查询时翻译填充) */
private String signUserName;
private String signLocation;
private String signImageUrl;
private String description;
@ -101,4 +109,9 @@ public class EduContractVo implements Serializable {
*/
private List<EduContractMilestoneVo> milestones;
/**
* 缴费计划列表(分期合同详情)
*/
private List<EduContractPaymentPlanVo> paymentPlans;
}

View File

@ -296,8 +296,9 @@ CREATE TABLE `edu_contract` (
`payment_status` char(1) DEFAULT '0' COMMENT '支付状态 0=未支付 1=部分支付 2=已支付 3=已退费',
`status` char(1) DEFAULT '0' COMMENT '合同状态 0=草稿 1=待生效 2=执行中 3=已结清 4=已退费 5=已转课 9=作废',
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志 0=存在 1=删除',
`sales_id` bigint DEFAULT NULL COMMENT '课程顾问ID',
`housekeeper_id` bigint DEFAULT NULL COMMENT '管家ID',
`sales_id` bigint DEFAULT NULL COMMENT '课程顾问ID(edu_staff.id)',
`housekeeper_id` bigint DEFAULT NULL COMMENT '管家ID(edu_staff.id)',
`sign_user_id` bigint DEFAULT NULL COMMENT '签约负责人ID(edu_staff.id)',
`sign_location` varchar(255) DEFAULT NULL COMMENT '签约地点',
`sign_image_url` varchar(1000) DEFAULT NULL COMMENT '合同扫描件URL',
`description` varchar(2000) DEFAULT NULL COMMENT '合同说明',
@ -401,6 +402,31 @@ CREATE TABLE `edu_contract_log` (
KEY `idx_tenant_action` (`tenant_id`, `action`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合同操作流水';
-- 5.5 合同缴费计划(分期付款时生成, 对标 edu_contract_milestone)
DROP TABLE IF EXISTS `edu_contract_payment_plan`;
CREATE TABLE `edu_contract_payment_plan` (
`id` bigint NOT NULL COMMENT '主键',
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户ID',
`contract_id` bigint NOT NULL COMMENT '合同ID',
`installment_no` int NOT NULL COMMENT '期次号 1/2/3',
`plan_amount` decimal(14,2) NOT NULL COMMENT '本期应收金额',
`plan_date` date NOT NULL COMMENT '本期应付日期',
`paid_amount` decimal(14,2) NOT NULL DEFAULT 0.00 COMMENT '本期实收金额',
`paid_date` date DEFAULT NULL COMMENT '实际收款日期',
`payment_method` varchar(32) DEFAULT NULL COMMENT '本期支付方式 CASH/WECHAT/ALIPAY/BANK/CARD',
`status` char(1) DEFAULT '0' COMMENT '状态 0=待收 1=已收 4=已作废',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志 0=存在 1=删除',
`create_dept` bigint DEFAULT NULL COMMENT '创建部门',
`create_by` bigint DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_tenant_contract_no` (`tenant_id`, `contract_id`, `installment_no`) USING BTREE,
KEY `idx_tenant_contract` (`tenant_id`, `contract_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合同缴费计划(分期)';
-- =====================================================================
-- 六、排课消课上下文 schedule
-- =====================================================================
@ -604,7 +630,10 @@ INSERT INTO `edu_dict`(`id`,`tenant_id`,`dict_type`,`dict_label`,`dict_value`,`s
(104,'000000','hours_biz_type','转入','TRANSFER_IN',5,'N','0'),
(105,'000000','hours_biz_type','赠送','GIFT',6,'N','0'),
(106,'000000','hours_biz_type','过期','EXPIRE',7,'N','0'),
(107,'000000','hours_biz_type','手动调整','ADJUST',8,'N','0');
(107,'000000','hours_biz_type','手动调整','ADJUST',8,'N','0'),
(110,'000000','payment_plan_status','待收','0',1,'Y','0'),
(111,'000000','payment_plan_status','已收','1',2,'N','0'),
(112,'000000','payment_plan_status','已作废','4',3,'N','0');
-- =====================================================================
-- 九、菜单与权限(注入 ruoyi 的 sys_menu