公司动态

SpringBoot3+XXL-Job+Redis 合同到期提醒:窗口扫描、SETNX 幂等与收付款逾期打标

📅 2026/8/28 5:07:06
SpringBoot3+XXL-Job+Redis 合同到期提醒:窗口扫描、SETNX 幂等与收付款逾期打标
SpringBoot3XXL-JobRedis 合同到期提醒窗口扫描、SETNX 幂等与收付款逾期打标文档地址https://ruoyioffice.com源码1·GitHubhttps://github.com/yuqing2026/ruoyi-office源码2·GitCodehttps://gitcode.com/zhouzhongyan/ruoyi-office源码3·Giteehttps://gitee.com/yqzy1688/ruoyi-office微信17156169080备注「RuoYi Office」一句话到期提醒要验收的不是「台账上有结束日期」而是窗口怎么围、谁有资格进窗口、同一天会不会发两封信、计划和合同到期是不是同一条 Job。RuoYi Office 把提醒和逾期拆开前者扫合同endDate发站内信后者扫收付款planDate改状态。▲ 上条是到期闭区间已过期、窗口内、窗口外、草稿/终止四种命运不同。左下 Redis 键带租户和日历日发送失败要删键右下逾期 Job 只改待收付→已逾期不发信引言结束日期写在卡片上不等于有人会被提醒合同全生命周期——起草、审批、签署、履约、变更——已有专文。现场真正漏掉的往往是到期之后那一跳现场原话只存 endDate 的后果「提前一个月提醒负责人续签」没有窗口要么不提醒要么每天全表扫描到明年「任务重跑了同一个合同弹了两次」没有按天幂等站内信变骚扰「提醒关了还在发」开关没接到 Job 入口「计划日过了还显示待收付」到期提醒和逾期打标被做成同一件事本文只写提醒窗口 Redis 去重 逾期打标。不讲套打正文、不讲签署流程。技术栈锚点2026Spring Boot 3.5 Java 17调度 XXL-Job去重 RedisSETNX站内信模板编码contract-expire-notify。一、配置先决定「要不要跑、窗口多宽」结论Job 入口第一行就是读配置。开关关闭整租户直接 return连窗口都不要算。合同模块「基础配置」分成三块卡片编号规则、到期提醒、签署环节。演示环境到期提醒开着提前天数 30编号前缀HT当日示例HT20260826001。▲ 合同 → 合同配置 → 基础配置。到期提醒开关为「开启」提前提醒天数 30。Job 用这两个字段围窗口编号规则给台账用提醒逻辑不读前缀对应字段配置Job 怎么用notifyEnabledfalse 则整段跳过notifyDays空或 ≤0 视为未配置打 warn 后 return窗口右端today.plusDays(notifyDays)签署开关、编号规则与提醒无关。不要把「能不能签署」和「会不会提醒」绑在一个布尔上。二、窗口是闭区间状态是白名单结论进窗口要同时满足日期和状态缺一不可。LocalDatetodayLocalDate.now();LocalDatedeadlinetoday.plusDays(notifyDays);ListContractLedgerDOexpiringcontractLedgerMapper.selectList(newLambdaQueryWrapperXContractLedgerDO().isNotNull(ContractLedgerDO::getEndDate).ge(ContractLedgerDO::getEndDate,today).le(ContractLedgerDO::getEndDate,deadline).in(ContractLedgerDO::getStatus,STATUS_APPROVED,STATUS_EFFECTIVE,STATUS_PERFORMING));日期谓词是today ≤ endDate ≤ todayN。今天已经过了结束日的合同不会再进提醒队列——过期是履约状态该推进的事不是每天再吓负责人一次。状态只放三种已通过审批完尚未切到生效、已生效、履约中。草稿、已终止、已归档不提醒。演示台账HT20260301001宇擎 ERP 系统销售合同是「已生效」结束日 2027-03-14以 2026-08-26、提前 30 天计算右端是 2026-09-25它还在窗口外今天不会发信。这是对的。对照人事调动那条 Job调动用eq today漏跑不补合同提醒用闭区间窗口内每一天都会再扫到同一份合同。所以必须按天去重否则 30 天窗口会发 30 封。三、Redis SETNX同一天只占一次坑结论幂等键按租户 日历日 合同 ID 拼TTL 两天发送失败必须删键。StringdedupKeyNOTIFY_DEDUP_KEY_PREFIX(tenantIdnull?default:tenantId):today:contract.getId();if(!Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(dedupKey,1,Duration.ofDays(2)))){returnfalse;// 今天已经提醒过}try{notifyMessageSendApi.sendSingleMessageToAdmin(reqDTO).checkError();returntrue;}catch(Exceptione){stringRedisTemplate.delete(dedupKey);returnfalse;}几个边界没有负责人直接跳过不占键。否则空合同会把当天坑占死补了负责人也发不出去。SETNX 失败视为已提醒。多实例、XXL-Job 重试、手工再点一次都不会双发。发送失败删键模板缺失、用户停用、消息中心超时下一轮还能再试。TTL 两天是兜底别把失败锁成「这两天都安静」。键里带租户TenantJob按租户跑键不带租户会在共享 Redis 里串单。模板编码写死为contract-expire-notify。文案、占位符{contractCode}{contractName}{endDate}改站内信模板不改 Java。占位符还有today、deadline方便写「请在窗口结束前处理」。四、逾期是另一条 Job改状态不发信结论合同到期看台账结束日收付款逾期看计划日。两件事不要合成一个定时任务。XxlJob(contractPaymentOverdueJob)TenantJobpublicStringexecute(){LocalDatetodayLocalDate.now();intupdateCountcontractPaymentPlanMapper.updateStatusByPlanDateBefore(PENDING.getValue(),OVERDUE.getValue(),today);returnString.format(收付款逾期检查完成更新 %d 条计划日期 %s,updateCount,today);}条件是planDate today且当前是待收付。批量改成已逾期。没有站内信、没有 Redis 键。演示合同HT20260301001的收付款计划正好能对上这套口径▲ HT20260301001 宇擎 ERP 系统销售合同已生效。收付款计划三期2026-03-20 首付 174000 状态已逾期实际金额 174000 / 2026-03-18 已填2026-06-30 中期款 232000 仍待收付2026-09-30 尾款 174000 待收付。到期提醒看的是合同结束日 2027-03-14不会因为这张计划表发信这张图有两处要对账的细节第一期已经填了实收状态仍是已逾期。逾期 Job 只认「待收付 计划日已过」不会根据actualAmount自动改成已收付也不会把已逾期改回去。真正收款时必须把计划状态写成已收付值为 1否则台账会一直红着。第二期计划日 2026-06-30今天已是 8 月仍显示待收付。说明逾期 Job 和提醒 Job 一样都要调度真正跑起来。窗口配置开着不等于计划日过了就会变逾期。提醒窗口里的合同计划日完全可以在窗口之外反过来计划已经逾期的合同结束日也可能在两年后。拆开是为了让财务催款和法务续签各看各的时钟。五、设计决策与边界决策点方案理由开关在配置不在代码notifyEnabled租户可静音闭区间窗口ge todayle todayN每天扫一遍靠 Redis 去重状态白名单通过 / 生效 / 履约草稿和终止不骚扰过期不提醒endDate ≥ today过期走履约/终止不是每天恐吓去重粒度租户日合同窗口内每天最多一封失败释放键catch 里 DELETE模板挂了不能锁两天无负责人不占键先判ownerUserId补负责人当天还能发逾期独立 Job只 update 状态催款列表靠状态筛选不必重复发信逾期不看实收只认待收付收款动作自己改已收付多租户TenantJob 键含 tenantId不串提醒边界结束日当天仍在窗口内ge today。想「到期日当天改发升级提醒」可以再加一条 Job不要改现有闭区间语义。Redis 宕机则 SETNX 失败可能漏提醒或若异常处理不当重复。生产要把 Redis 当成提醒链路的一部分来监控。模板编码写死换编码要改 Java。运营只应改模板内容。六、数据结构要点合同配置notify_enabled、notify_days。和编号前缀、签署开关同表不同职责。合同台账end_dateLocalDate、status、owner_user_id、contract_code、contract_name。提醒只读这五列。收付款计划plan_date、status0 待收付 / 1 已收付 / 2 已逾期、plan_amount、actual_amount、actual_date。逾期 Job 只碰前两列。Redis 键contract:expire-notify:{tenantId}:{yyyy-MM-dd}:{contractId}值1TTL 2 天。七、技术亮点总结设计要点实现方式价值配置开关入口 return租户可静音闭区间窗口todaytodayN提前量可配状态白名单in(通过,生效,履约)草稿不提醒按天去重Redis SETNX窗口内不刷屏失败释放删键可重试无负责人跳过不占键补人当天能发模板解耦固定 code改文案不发版逾期分轨另一 Job续签和催款分开逾期批量一条 UPDATE不逐行发信多租户键tenantId 前缀不串单八、快速体验在线演示https://ruoyioffice.com/web/账号 admin / admin123打开合同 → 合同配置 → 基础配置确认到期提醒开启、提前天数 30。打开台账HT20260301001看结束日是否落在「今天今天30」不在窗口内则今天不应产生到期提醒。切到「收付款计划」对照三期的计划日和状态已逾期、待收付、待收付。系统管理 → 消息中心 → 站内信模板按编码contract-expire-notify找到「合同到期提醒」看占位符。本地可在 XXL-Job 控制台分别触发contractExpireNotifyJob、contractPaymentOverdueJob对比返回的「已发送」和「更新条数」。源码仓库GitHubhttps://github.com/yuqing2026/ruoyi-office GitCodehttps://gitcode.com/zhouzhongyan/ruoyi-office Giteehttps://gitee.com/yqzy1688/ruoyi-office常见问题FAQ到期提醒会不会把已经过期的合同每天都发一遍不会。查询带endDate ≥ today。过了结束日就离开窗口。窗口内尚未到期的合同会每天扫到靠 Redis 当天只发一封。为什么要用 Redis不用「最后提醒日」字段字段也能做但多实例并发时要靠行锁。SETNX 把「今天是否已提醒」放到调度层失败删键比回滚一列日期更直接。TTL 两天防止键泄漏。收了钱为什么还显示已逾期逾期 Job 不会根据实际金额把状态改成已收付也不会把已逾期改回待收付。收款动作必须把计划status写成已收付。演示第一期已经填了实收 174000状态仍是已逾期就是这两条线没在收款时汇合。计划日过了还是待收付是不是提醒没开不是。提醒开关只管合同结束日的站内信。计划日过了仍待收付说明contractPaymentOverdueJob没跑或条件不是「待收付且计划日早于今天」。草稿合同快到期会提醒吗不会。状态白名单只有已通过、已生效、履约中。草稿先走审批。结语合同到期治理的核心是承认「合同结束」和「计划应收」是两口钟。结束日用闭区间窗口 状态白名单 按天 SETNX解决续签提醒不漏、不刷屏计划日用另一条 Job 把待收付打成逾期解决催款列表。开关在配置里文案在模板里Java 只负责窗口和键。起草审批解决合同从哪来提醒逾期解决合同临近结束时谁该被叫醒。两件事单据可以是同一张台账时钟必须分开。你们现在的合同到期是 Excel 标红还是已经能说出窗口右端和 Redis 键长什么样欢迎在评论里对一下提前天数。想要体验 RuoYi Office 的强大功能在线演示https://ruoyioffice.com/web/账号 admin / admin123源码仓库GitHubhttps://github.com/yuqing2026/ruoyi-office GitCodehttps://gitcode.com/zhouzhongyan/ruoyi-office Giteehttps://gitee.com/yqzy1688/ruoyi-office技术咨询添加微信17156169080备注「RuoYi Office」⭐如果觉得不错请给个 Star 支持一下