欢迎光临
我们一直在努力

SpringBoot+Vue3+Flowable 实现合同管理全生命周期:起草·审批·签署·履约·回款台账闭环设计

SpringBoot+Vue3+Flowable 实现合同管理全生命周期:起草·审批·签署·履约·回款台账闭环设计

📦 源码1:ruoyi-office-vben |📦 源码2:ruoyi-office |📦 源码3:ruoyi-office

合同管理在企业里地位特殊——它既是销售/采购的成果落地,又是法务/财务/履约/审计的台账依据。起草态和资产态是两种完全不同的对象:起草态是"在编辑、可改、可拒绝"的草稿;资产态是"已成立、要履行、要存档"的法律凭证。绝大多数合同系统把两者混在一张表里,结果"草稿被人当作生效合同用、生效合同被人当作草稿改",事故频发。RuoYi Office 用 contract_info(起草单)+ contract_ledger(台账)双表分离 + 9 状态生命周期 + 三条 BPM 流程,把合同从草稿到归档全程串成一条不可逾越的闭环。本文完整拆解其设计思路与核心源码。 contract-lifecycle-panorama.png

▲ 合同管理全景:9 状态生命周期、起草单 vs 台账双表分离、三条 BPM 流程经 FlowBillService 回调驱动、收付款计划 + 履约登记 + 到期提醒闭环

引言:合同管理的 5 个老大难

不管是销售合同、采购合同、租赁合同还是服务合同,本质都要回答这五个问题:

问题一:起草和生效混为一谈。常见做法是一张 contract 表加一个 status 字段,草稿、审批中、生效、终止全塞里面。结果导出"现有合同台账"时还要 WHERE status >= 2 过滤——草稿和台账逻辑搅在一起,谁查都难。

问题二:审批通过后改不了又必须改。合同审批通过 = 法律生效 = 不能修改。但现实是经常出现"忘填一个字段、对方公司名拼错、金额漏一位"——必须重新走"合同变更"流程,但又不能让审批中的变更影响主合同台账。

问题三:签署环节有没有?。有些公司纯电子合同(如 SaaS 订阅),审批通过就生效;有些公司必须线下盖章签字回传扫描件才生效。这两种业务模式必须能同套系统兼容。

问题四:履约不只是收款。一份合同的履约可能包括:交付货物、开发票、收款、付款、验收、提供服务……每个动作都要登记台账,最后汇总成"已履约金额 / 履约进度"。

问题五:到期了没人盯。合同 12 月 31 日到期,到时候没人续签——客户跑了、租赁合同自动到期续约……需要到期前 N 天自动提醒相关人。

image.png

RuoYi Office 用一个独立的 yudao-module-contract 模块解决这五个问题。本文拆解。


一、生命周期:9 个状态、3 条流程

1.1 状态机一图

草稿0 ──提交审批──▶ 审批中1 ──通过──▶ 已起草2
│ │
└──拒绝──┐ ├─signEnabled=true──▶ 签署中3 ──签署通过──▶ 已生效4
▼ └─signEnabled=false─────────────────────▶ 已生效4
(回草稿0)

首次履约

履约中5

标记完成

已完成6

归档

已归档8

任意阶段 ──终止──▶ 已终止7

1.2 状态枚举

public enum ContractStatusEnum {
DRAFT(0, "草稿"),
APPROVING(1, "审批中"),
APPROVED(2, "已起草"),
SIGNING(3, "签署中"),
EFFECTIVE(4, "已生效"),
PERFORMING(5, "履约中"),
COMPLETED(6, "已完成"),
TERMINATED(7, "已终止"),
ARCHIVED(8, "已归档");

private final Integer value;
private final String name;
}

注意 status 的设计哲学:前 2 个状态(0、1)是"起草态",2 之后都是"资产态"——这条边界正是双表分离的依据。

1.3 三条 BPM 流程

public enum ContractBillTypeEnum implements BillTypeEnum {
CONTRACT_APPROVE("301", "合同审批", "contract_approve"),
CONTRACT_CHANGE("302", "合同变更审批", "contract_change"),
CONTRACT_SIGN("303", "合同签署确认", "contract_sign");
}

流程触发时机通过后效果
contract_approve 起草单提交 生成台账(status=2 已起草)
contract_change 已生效合同需要修改 把变更回写到主合同
contract_sign 已起草合同需要签署 台账状态推进到"已生效"

二、双表分离:起草单 vs 台账

2.1 设计思想

┌─────────────────────────┐ ┌─────────────────────────┐
│ contract_info(起草单)│ │ contract_ledger(台账) │
│ │ │ │
│ state: 0草稿 / 1审批中 │ 通过后 │ state: 2 已起草 ─▶ 8归档│
│ / 2已起草 │ 镜像复制│ │
│ │ ───────▶│ ledger.id │
│ 承载:草稿编辑、提交 │ 同 ID │ = draft.id │
│ BPM、变更 │ │ = draftContractId │
└─────────────────────────┘ └─────────────────────────┘

起草单只关心"能不能签" 台账只关心"已经签了,怎么管"

关键规则:

  • 起草单 contract_info 的 ID 和台账 contract_ledger 的 ID 完全相同(不重新生成)
  • 审批通过后通过 BeanUtils.toBean 把起草单镜像复制到台账
  • 起草单的 status 最多到 2(已起草),后续状态推进只发生在台账上
  • 草稿期改起草单不影响台账;变更流程提交"变更单",审批通过后回写主合同(起草单)

2.2 起草单 DO

@TableName("contract_info")
public class ContractInfoDO extends BaseDO {
@TableId
private Long id;
private String billCode; // 单据编号(Redis 严格序号)
private String contractCode; // 合同编号(HT+日期+序号)
private String contractName;
private Long partyAId; // 甲方
private Long partyBId; // 乙方
private BigDecimal totalAmount; // 合同总额
private LocalDate startDate;
private LocalDate endDate;
private Integer status; // 0草稿 / 1审批中 / 2已起草
private String processInstanceId;
private Integer processStatus;
private String contractFileUrls; // 起草稿
private String signedFileUrl; // 签署后文件
private LocalDate effectiveDate;
// …
private BigDecimal performanceAmount; // 累计履约金额
private Integer performanceStatus; // 履约进度状态
}

2.3 台账 DO

@TableName("contract_ledger")
public class ContractLedgerDO extends BaseDO {
@TableId
private Long id; // = draft.id
private Long draftContractId; // = draft.id
private String contractCode;
private String contractName;
// … 与起草单镜像的业务字段 …
private BigDecimal totalAmount;
private BigDecimal performanceAmount;
private Integer performanceStatus;
/** 状态:2 已起草 / 3 签署中 / 4 已生效 / 5 履约中 / 6 已完成 / 7 已终止 / 8 已归档 */
private Integer status;
}

2.4 审批通过 → 生成台账

@Transactional(rollbackFor = Exception.class)
public ContractLedgerDO upsertFromDraft(ContractInfoDO draft, Integer status) {
ContractLedgerDO ledger = BeanUtils.toBean(draft, ContractLedgerDO.class);
ledger.setId(draft.getId());
ledger.setDraftContractId(draft.getId());
ledger.setStatus(status);
ContractLedgerDO existing = contractLedgerMapper.selectById(ledger.getId());
if (existing == null) {
contractLedgerMapper.insert(ledger);
} else {
contractLedgerMapper.updateById(ledger);
}
return ledger;
}

双表分离的价值:

维度单表方案双表分离
查"现有合同" WHERE status >= 2(容易写错) SELECT * FROM contract_ledger(语义清晰)
草稿大量但生效少 全部查 contract 表,影响性能 台账表数据少且稳定
起草修改频繁 与生效查询互相影响 互相隔离
法律合规 起草/资产难分离 台账即资产,可独立做审计

三、起草审批 → 生成台账

3.1 提交审批

public void submitContract(Long id) {
ContractInfoDO contractInfo = validateContractInfoExists(id);
if (!ContractStatusEnum.DRAFT.getValue().equals(contractInfo.getStatus())) {
throw exception(CONTRACT_SUBMIT_FAIL_NOT_DRAFT);
}
Map<String, Object> processInstanceVariables = ...; // 流程变量(金额/期限分支用)
String processInstanceId = processInstanceApi.submitProcessInstance(
Long.valueOf(contractInfo.getCreator()),
new BpmProcessInstanceCreateReqDTO()
.setProcessDefinitionKey(ContractBillTypeEnum.CONTRACT_APPROVE.getProcessDefinitionKey())
.setVariables(processInstanceVariables)
.setStartUserSelectAssignees(saveReqVO.getStartUserSelectAssignees())
.setBusinessKey(String.valueOf(id))
).getCheckedData();
// 更新状态为 审批中
contractInfoMapper.updateById(new ContractInfoDO().setId(id)
.setStatus(ContractStatusEnum.APPROVING.getValue())
.setProcessInstanceId(processInstanceId));
}

3.2 onProcessApproved 自动建台账

审批通过后,Flowable 引擎触发回调 → ContractInfoServiceImpl.onProcessApproved:

public void onProcessApproved(String businessKey) {
Long id = Long.parseLong(businessKey);
// 1. 起草单状态 → 已起草(2)
contractInfoMapper.updateById(new ContractInfoDO().setId(id)
.setStatus(ContractStatusEnum.APPROVED.getValue()));

// 2. 镜像生成台账,状态=已起草(2)
ContractInfoDO contractInfo = contractInfoMapper.selectById(id);
contractLedgerService.upsertFromDraft(contractInfo, ContractStatusEnum.APPROVED.getValue());

// 3. 根据签署配置决定下一步
ContractConfigDO config = contractConfigService.getConfig();
boolean signEnabled = config != null && Boolean.TRUE.equals(config.getSignEnabled());
if (signEnabled) {
// 自动生成签署记录草稿(用户后续手动提交签署流程)
ContractSignSaveReqVO signReq = ...;
contractSignService.createContractSign(signReq);
} else {
// 未启用签署 → 直接进入"已生效"
contractInfoMapper.updateById(new ContractInfoDO().setId(id)
.setStatus(ContractStatusEnum.EFFECTIVE.getValue())
.setEffectiveDate(LocalDate.now()));
contractLedgerService.markEffective(id, LocalDate.now(), null);
}
}

signEnabled 是一个全局开关——一句配置决定整个企业的合同是否走线下签署环节。


四、FlowBillService 工厂回调机制

4.1 统一的 BPM 回调框架

合同模块的三条流程、CRM 的两条流程、OA/HR 的所有审批,都走 RuoYi Office 的 FlowBillService 工厂回调机制。核心是一个抽象监听器 AbstractFlowLocalNotificationListener,监听 BPM 引擎发出的"流程状态变更事件",按 processDefinitionKey 路由到对应业务模块的 FlowBillService 实现:

private void invokeLifecycleHooks(BpmProcessInstanceStatusEvent message) {
String processDefinitionKey = message.getProcessDefinitionKey();
Integer status = message.getStatus();
String businessKey = message.getBusinessKey();
FlowBillService<T> flowBillService = getFlowBillServiceFactory().getServiceByProcessKey(processDefinitionKey);

Runnable hookAction;
if (BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(status)) {
hookAction = () -> flowBillService.onProcessApproved(businessKey);
} else if (BpmProcessInstanceStatusEnum.REJECT.getStatus().equals(status)) {
hookAction = () -> flowBillService.onProcessRejected(businessKey);
} else if (BpmProcessInstanceStatusEnum.CANCEL.getStatus().equals(status)) {
hookAction = () -> flowBillService.onProcessCancelled(businessKey);
}
hookAction.run();
}

4.2 合同模块的工厂

@Component
public class ContractFlowBillServiceFactory extends FlowBillServiceFactory<ContractBillTypeEnum> {
@Override
protected ContractBillTypeEnum[] getBillTypeValues() {
return ContractBillTypeEnum.values();
}
}

工厂启动时收集所有 FlowBillService 实现,按 getSupportedBillType() 注册到 Map。运行时按 processDefinitionKey 找到对应实现。

4.3 三个 Service 各自实现 onProcessApproved

流程实现类onProcessApproved 行为
合同审批 ContractInfoServiceImpl 生成台账 + 按 signEnabled 决定下一步
合同变更 ContractChangeServiceImpl 把变更内容回写主合同(含台账)
合同签署 ContractSignServiceImpl 台账状态 → 已生效

签署通过的关键代码:

public void onProcessApproved(String businessKey) {
Long id = Long.parseLong(businessKey);
ContractSignDO signDO = validateContractSignExists(id);
// … 校验主合同 …
contractUpdate.setStatus(ContractStatusEnum.EFFECTIVE.getValue());
contractUpdate.setEffectiveDate(LocalDate.now());
contractInfoMapper.updateById(contractUpdate);
contractLedgerService.markEffective(signDO.getContractId(), LocalDate.now(), signedFileUrl);
}

单体/微服务双模式支持:本地用 ContractLocalNotificationListener 监听 Spring 事件;微服务用 ContractMqNotificationConsumer 消费 MQ 消息。Service 实现不变,只是触发方式不同。这是 RuoYi Office 单体/微服务无缝切换的关键设计。


五、收付款计划 + 履约登记

5.1 收付款计划

@TableName("contract_payment_plan")
public class ContractPaymentPlanDO extends BaseDO {
private Long id;
private Long contractId;
private Integer period; // 期次
private BigDecimal planAmount; // 计划金额
private LocalDate planDate; // 计划日期
private BigDecimal actualAmount; // 实际金额(履约登记回写)
private LocalDate actualDate;
private Integer status; // 0待收付 / 1已收付 / 2已逾期
private String remark;
}

public enum ContractPaymentPlanStatusEnum {
PENDING(0, "待收付"),
PAID(1, "已收付"),
OVERDUE(2, "已逾期");
}

5.2 逾期 XXL-Job

@Component
public class ContractPaymentOverdueJob {
@XxlJob("contractPaymentOverdueJob")
@TenantJob
public String execute() {
LocalDate today = LocalDate.now();
int updateCount = contractPaymentPlanMapper.updateStatusByPlanDateBefore(
ContractPaymentPlanStatusEnum.PENDING.getValue(),
ContractPaymentPlanStatusEnum.OVERDUE.getValue(),
today);
return String.format("收付款逾期检查完成,更新 %d 条(计划日期 < %s)", updateCount, today);
}
}

Mapper 一条 UPDATE 批量改状态:

default int updateStatusByPlanDateBefore(Integer fromStatus, Integer toStatus, LocalDate beforeDate) {
return update(null, new LambdaUpdateWrapper<ContractPaymentPlanDO>()
.set(ContractPaymentPlanDO::getStatus, toStatus)
.eq(ContractPaymentPlanDO::getStatus, fromStatus)
.isNotNull(ContractPaymentPlanDO::getPlanDate)
.lt(ContractPaymentPlanDO::getPlanDate, beforeDate));
}

@TenantJob 注解保证多租户下每个租户独立扫表,不会串数据。

5.3 履约登记 → 回写计划 + 汇总台账

履约登记是"实际发生了什么"的台账,可对应交付、验收、收款、付款等多种类型:

@TableName("contract_performance")
public class ContractPerformanceDO extends BaseDO {
private Long id;
private Long contractId;
private Long paymentPlanId; // 关联哪个计划(可选)
/** 1交付 2验收 3开票 4收款 5付款 6服务交付 99其他 */
private Integer performanceType;
private BigDecimal performanceAmount;
private LocalDateTime performanceDate;
private String performanceContent;
}

创建履约的核心逻辑:

public Long createPerformance(ContractPerformanceSaveReqVO createReqVO) {
ContractLedgerDO contract = validateContractExists(createReqVO.getContractId());
assertContractCanPerform(contract); // 必须是已生效(4) / 履约中(5)
// …
contractPerformanceMapper.insert(row);

writebackPaymentPlan(createReqVO); // 回写计划
syncContractPerformanceStats(createReqVO.getContractId()); // 汇总台账
return row.getId();
}

回写计划:

private void writebackPaymentPlan(ContractPerformanceSaveReqVO reqVO) {
if (reqVO.getPaymentPlanId() == null) return;
ContractPaymentPlanDO update = new ContractPaymentPlanDO();
update.setId(reqVO.getPaymentPlanId());
update.setActualAmount(reqVO.getPerformanceAmount());
update.setActualDate(toLocalDate(reqVO.getPerformanceDate()));
update.setStatus(ContractPaymentPlanStatusEnum.PAID.getValue());
contractPaymentPlanMapper.updateById(update);
}

汇总台账履约额 + 推进状态:

private void syncContractPerformanceStats(Long contractId) {
BigDecimal sum = contractPerformanceMapper.selectSumAmountByContractId(contractId);
ContractLedgerDO contract = contractLedgerMapper.selectById(contractId);
update.setPerformanceAmount(sum);
update.setPerformanceStatus(status);
// 已生效(4) + 有履约 → 推进到 履约中(5)
if (sum != null && sum.compareTo(BigDecimal.ZERO) > 0
&& ContractStatusEnum.EFFECTIVE.getValue().equals(contract.getStatus())) {
update.setStatus(ContractStatusEnum.PERFORMING.getValue());
}
contractLedgerMapper.updateById(update);
}

亮点:状态推进是"事件驱动"的——首次履约自动从 EFFECTIVE 推进到 PERFORMING,不用人工切换。


六、到期提醒:ContractExpireNotifyJob

@Component
public class ContractExpireNotifyJob {
@XxlJob("contractExpireNotifyJob")
@TenantJob
public String execute() {
ContractConfigDO config = contractConfigService.getConfig();
if (config == null || !Boolean.TRUE.equals(config.getNotifyEnabled())) {
return "到期提醒未启用,跳过";
}
Integer notifyDays = config.getNotifyDays(); // 提前 N 天
LocalDate today = LocalDate.now();
LocalDate deadline = today.plusDays(notifyDays);

// 扫描台账:endDate 在 [today, today+N] 窗口内、状态为已起草/已生效/履约中
List<ContractLedgerDO> expiring = contractLedgerMapper.selectList(
new LambdaQueryWrapperX<ContractLedgerDO>()
.isNotNull(ContractLedgerDO::getEndDate)
.ge(ContractLedgerDO::getEndDate, today)
.le(ContractLedgerDO::getEndDate, deadline)
.in(ContractLedgerDO::getStatus, STATUS_APPROVED, STATUS_EFFECTIVE, STATUS_PERFORMING));
// … 用 NotifyMessageSendApi 发站内信,Redis 幂等去重(同一合同同一天只发一次)
return String.format("到期提醒完成,共 %d 份合同", expiring.size());
}
}

幂等设计:用 Redis SETNX contract:expire:notify:{contractId}:{date} 做去重,避免 Job 重试或多实例并发时重复发消息。


七、编号生成:billCode + contractCode

合同有两个编号字段:

字段用途生成方式
billCode 单据编号(系统内严格唯一) Redis INCR 严格递增
contractCode 合同编号(业务可读) HT + 日期 + 4 位序号

7.1 billCode:Redis 严格序号

public class BillCodeUtils {
public String generate(String sysCode, String billTypeCode) {
String dateStr = DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATE_PATTERN);
String redisKey = REDIS_KEY_PREFIX + ":" + sysCode + ":" + billTypeCode + ":" + dateStr;
Long sequenceNo = stringRedisTemplate.opsForValue().increment(redisKey);
stringRedisTemplate.expire(redisKey, Duration.ofDays(2L));
return sysCode + billTypeCode + SEPARATOR + dateStr + String.format("%0" + SEQUENCE_LENGTH + "d", sequenceNo);
}
}

合同模块系统码 CT,类型码 301/302/303,示例:CT301-2026060300001、CT303-2026060300002。Redis INCR 严格递增,保证流水不重复。

7.2 contractCode:业务可读

public String generateContractCode() {
String dateStr = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String prefix = "HT" + dateStr;
int seq = (int) (Math.random() * 9000) + 1000;
return prefix + String.format("%04d", seq);
}

示例:HT202606031234。ContractConfigDO 已预留 codePrefix / codeDateFormat / codeSeqLength 字段,便于后续从配置表读取规则做严格序号化升级。


八、关键表与索引

表关键字段重要索引
contract_info id, status, processInstanceId (status, end_date), (creator)
contract_ledger id, status, endDate, performanceAmount (status, end_date), (party_a_id)
contract_payment_plan contractId, period, planDate, status (contract_id, period), (plan_date, status)
contract_performance contractId, paymentPlanId, performanceType (contract_id, performance_date), (payment_plan_id)
contract_sign contractId, status, processInstanceId (contract_id)
contract_change contractId, status, processInstanceId (contract_id)
contract_config signEnabled, notifyEnabled, notifyDays 单租户单条配置

九、技术亮点总结

设计要点实现方式价值
双表分离 contract_info + contract_ledger 同 ID 镜像 起草态与资产态完全隔离
9 状态生命周期 ContractStatusEnum 顺序状态机 草稿→归档全程可追溯
三条 BPM 流程 contract_approve / change / sign 节点条件可视化配置
统一回调框架 FlowBillService 工厂 + 抽象监听器 单体/微服务无缝切换
签署可选 signEnabled 全局配置 电子/线下两种业务模式兼容
履约事件驱动 首次履约自动推进到 PERFORMING 状态不用人工切
履约回写 计划状态 + 台账履约额双向同步 数据一致性强
逾期定时扫描 XXL-Job + 批量 UPDATE 性能好,多租户隔离
到期提醒 XXL-Job + Redis 幂等 不漏发也不重复
编号双轨 billCode(Redis)+ contractCode(HT+日期) 系统唯一 + 业务可读

十、快速体验

  • 在线演示:http://ruoyioffice.com/web/(账号 admin / admin123)
  • 操作路径:登录后台 → 顶部菜单 合同管理
  • 推荐体验流程:
  • 合同配置 打开"启用签署环节"、设置到期提醒天数(如 30 天)
  • 合同起草 新建一份合同,填总额、起止日期、收付款计划(分 3 期)
  • 提交审批 走 BPM 流程,审批通过后观察 合同台账 自动出现一条 “已起草” 记录
  • 进 合同签署 提交签署流程,通过后台账状态变 “已生效”
  • 履约登记 录入一笔收款,观察对应回款计划自动标 “已收付”、台账推进到 “履约中”
  • 故意把某条计划的 planDate 改到昨天,等待或手动触发 contractPaymentOverdueJob,看状态变 “已逾期”
  • 故意把合同 endDate 改到一周内,触发 contractExpireNotifyJob 看站内信
  • 进 工作流 → 流程模型 查看 contract_approve / contract_change / contract_sign 三条流程的 BPMN 设计

源码仓库:

仓库地址
后端(GitCode) https://gitcode.com/zhouzhongyan/ruoyi-office.git
前端(GitCode) https://gitcode.com/zhouzhongyan/ruoyi-office-vben.git
后端(GitHub) https://github.com/yuqing2026/ruoyi-office.git

结语

合同管理本质是把一份"法律凭证"在企业内的全生命周期管起来。起草态和资产态分离是这套设计的灵魂——前者是"在编辑"的草稿,后者是"已成立"的资产,两者不能混。三条 BPM 流程 + FlowBillService 统一回调让审批/变更/签署三个环节有统一的扩展模型;履约登记 + 收付款计划构成闭环的"实际 vs 计划"对账机制;定时 Job + 幂等去重保证逾期与到期不漏发也不滥发。

这套架构同样适用于采购合同、租赁合同、服务合同、框架协议等任何"长生命周期 + 履约阶段 + 到期管理"的业务——只要替换字段含义和履约类型枚举即可。

你们公司的合同管理还在用 Excel/钉钉文档?欢迎评论区聊聊你的痛点和改造思路。

常见问题(FAQ)

合同模块在 RuoYi Office 的哪个目录?

独立模块 yudao-module-contract,路径 ruoyi-office/yudao-module-contract/,拆为 *-api(枚举/API)和 *-server(实现)两个子模块。注意 yudao-module-crm 里另有 CrmContractDO 是销售场景的简化合同,与本文的"合同管理模块"是两套独立实现。

为什么要做起草单 + 台账双表分离,不能用一张表加 status 字段吗?

可以但不推荐。单表方案在草稿大量、生效合同少的场景查询性能差;起草修改频繁时与生效查询互相影响;起草态和资产态在法律语义上是两类对象,分表更清晰、更安全、更易做审计。

signEnabled 配置可以按合同类型差异化吗?

当前是全局配置。如果业务真的需要差异化(如销售合同要签署、采购合同不要签署),可以在 ContractConfigDO 上扩 signEnabledByType JSON 字段,或者新增 contract_type_config 表按合同类型存配置,框架开放。

合同变更流程通过后会覆盖所有字段吗?

不会。ContractChangeDO 里只保留"变更项"(金额/期限/条款的旧值新值),审批通过时由 applyChangeToContract(change) 按变更项更新主合同对应字段,其他字段不动。历史变更记录保留在变更表里供审计。

合同到期提醒会不会重复发?

不会。ContractExpireNotifyJob 用 Redis SETNX contract:expire:notify:{contractId}:{date} 幂等去重,同一合同同一天只发一次。Job 失败重试或多租户/多实例并发都不会重复。

XXL-Job 没装能不能用?

逾期判断和到期提醒依赖 XXL-Job。如果暂未引入 XXL-Job,可用 Spring 的 @Scheduled 定时调用相同的 Mapper 方法替代,但失去任务分片、告警、人工触发等能力。生产环境推荐配置 XXL-Job。


💡 想要体验 RuoYi Office 的强大功能?

🌐 在线演示:http://ruoyioffice.com/web/(账号 admin / admin123)

📦 源码仓库:GitCode | GitHub

💬 技术咨询:添加微信 17156169080,备注「RuoYi Office」

⭐ 如果觉得不错,请给个 Star 支持一下!

赞(0)
未经允许不得转载:171主机测评 » SpringBoot+Vue3+Flowable 实现合同管理全生命周期:起草·审批·签署·履约·回款台账闭环设计
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址