ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

cron 表达式为什么总跑错?10 个场景实测,下次执行时间一眼看懂

cron 表达式为什么总跑错?10 个场景实测,下次执行时间一眼看懂

5 段式 cron 只有 5 个字段,写错基本错在三处:*/n从哪个值起步、日和周是并集还是交集、2 月 29 日不是每年都有。

我把这三处做成了一张 30 天日历,然后一个个场景点过去。

同样是「工作日跑一次」,0 3 * * 1-5未来 30 天跑 20 次,0 9-18 * * 1-5就要跑 200 次。

0 0 1 * *30 天只跑 1 次;0 0 29 2 *从 2027 年 2 月 27 日往后数,下一次要等 366 天,之后每次间隔 1461 天。

这些数字不是背下来的,是我在页面上换基准时间一格一格点出来的。

在线案例与源码:https://atomgit.com/deli007/demo_park/tree/main/codearts-cron-expression-lab
成品只有一个 index.html,双击就能用,不装任何依赖,页面自己会跑 12 条自检。

想自己看一遍:git clone仓库后双击 index.html,或者用python -m http.server 8000起个静态服务。

一、为什么值得做:这题人人会写,但很少有人把 30 天跑一遍

搜「cron 表达式」,出来的大多是一张字段范围表加一个在线计算器。

范围表告诉你分钟是 0–59,但不会告诉你*/15展开成 0,15,30,45 还是 15,30,45,60。

也不会告诉你0 0 1 * 1到底是「每月 1 号并且是周一」还是「每月 1 号或者周一」。

我把它做成了一个能点、能换基准时间、还会自己验算的页面。

二、准备环境:打开码道 Web,把需求写成一段话

浏览器打开码道 Web 版:https://devcloud.cn-north-4.huaweicloud.com/chat?source=dmzntgwsf&sourcead=dmzntgwwbwz ,登录后在对话框里写需求,就能拿到一个能跑的单文件应用。

码道有三种使用方式:WebUI(浏览器对话)、TUI(终端命令行)和桌面 IDE(IDE 插件)。本文用 WebUI 版演示。

需求我写了 7 条,事后看最值钱的是第 4 条和第 6 条。

第 4 条要求画 30 天日历并统计执行次数,第 6 条要求页面自带自检脚本并把结果显示出来。

有了这两条,页面就不再是一个「下次执行时间」查询框,而是一台能自己验算的日历。

三、需求原文(可以照着复制)

做一个单文件网页应用 index.html(原生 HTML+CSS+JS,无外链、无 CDN、无构建、可离线打开),主题是 cron 表达式可视化实验室。 1. 顶部一个输入框输入 5 段式 cron 表达式(分 时 日 月 周),旁边给出 8 个常用预设按钮:`0 3 * * *`、`0 3 * * 1-5`、`*/15 * * * *`、`0 9-18 * * 1-5`、`0 0 1 * *`、`30 6 * * 0`、`0 */2 * * *`、`0 0 29 2 *`。 2. 逐字段解析:把输入按 5 段切开,每段单独一列,显示字段名、取值范围的合法区间、这段展开后的具体取值集合(例如 `*/15` 展开成 0,15,30,45;`1-5` 展开成 1~5;`0` 就是 0),非法的一段标红并说明错在哪。 3. 下一次执行:给出从基准时间起接下来 10 次的执行时间(精确到分钟,显示年月日 星期 时分),第一行还要显示「距现在多久」精确到分钟。 4. 未来 30 天日历视图:按天铺一张月历,命中的日期高亮,每格标出当天会跑几次;旁边统计面板显示「未来 30 天共执行 N 次」「平均每天 M 次」「最密集的一天是哪天、跑几次」「平均间隔」。 5. 基准时间可调:提供一个 datetime-local 输入,改它之后所有结果按新基准重算(这样能把 `0 0 29 2 *` 这种闰年表达式验出来)。再加一个「星期字段 0 和 7 都表示周日」的开关说明。 6. 页面上必须有自检脚本并把每条断言结果显示出来(例如 `0 3 * * 1-5` 未来 30 天执行次数、`*/15 * * * *` 平均间隔=15 分钟、`0 0 29 2 *` 在 2028-02 有 1 次而 2027-02 有 0 次、星期 0 与 7 等价),改动参数后重跑自检。 7. 输入非法参数(段数不是 5、写了 99 或者 25 这种越界数字、`*/0`、`1-` 这种残缺范围)要给明确中文提示,保留上一次有效值,界面不清空。 完成后打开预览,让我直接看到运行效果。

四、页面长什么样:一张月历,加十条下次执行

最上面是表达式输入框、8 个常用预设、基准时间输入,还有一个「星期字段 0 和 7 都表示周日」的开关。

中间一行是五个字段的逐段解析,每个字段给出合法区间和展开后的取值集合。

最下面是月历和统计面板,命中的日期高亮,角标 ×N 是当天跑几次。

五、实测:换个表达式,数字全变

基准时间统一取 2026-10-17 23:16,把 7 个表达式挨个跑了一遍。

表达式未来 30 天执行平均每天平均间隔第一条下次执行
0 3 * * 1-520 次0.7 次≈1895 分钟,1 天 7 小时 35 分2026-10-19 周一 03:00
0 9-18 * * 1-5200 次6.7 次≈184 分钟,3 小时 4 分2026-10-19 周一 09:00
*/15 * * * *2880 次96 次≈15 分钟2026-10-17 周六 23:30
*/5 * * * *8640 次288 次≈5 分钟2026-10-17 周六 23:20
0 */2 * * *360 次12 次≈120 分钟2026-10-18 周日 00:00
0 0 1 * *1 次0 次仅 1 次,无法计算2026-11-01 周日 00:00
0 0 * * 6,010 次0.3 次≈4640 分钟,3 天 5 小时 20 分2026-10-18 周日 00:00

*/15 * * * *那行最能说明问题:30 天 2880 次,平均每天 96 次,正好是 24 乘 4。

0 0 * * 6,0是「周六或周日零点」,30 天命中 10 次。

它的字段解析显示取值是 0 和 6,页面把周六统一排到了周日后面。

六、日和周同时受限,是并集不是交集

这一条我用0 0 1 * 1单独验了一遍,语义是「每月 1 号或者每周一」。

30 天里它命中 5 次,分别是 10-19 周一、10-26 周一、11-01 周日、11-02 周一、11-09 周一。

11-01 是 1 号但不是周一,11-02 是周一但不是 1 号,两天都执行了,确实是并集。

如果你要的是「每月 1 号并且是周一」,5 段式 cron 写不出来,只能在脚本里再判断一次日期。

七、闰年:换个基准日,间隔从 1 天变成 1461 天

基准拉到 2028-02-28 10:00,0 0 29 2 *的下一条是 2028 年 2 月 29 日,距基准 14 小时。

基准换回 2027-02-27,同一个表达式的下一条变成 2028-02-29,距基准 366 天,未来 30 天执行 0 次。

再往外是0 0 31 2 *,2 月没有 31 号,页面直接给了一句「在可预见的未来(约 2 年窗口)没有命中任何时间点,请调整表达式或基准时间。」

八、核心那段代码:字段展开只用一个正则

每个字段都走同一个正则,星号、单值、范围、步进、逗号列表都在里面。

constm=String(p).match(/^(\*|\d+)(?:-(\d+))?(?:\/(\d+))?$/);if(!m)returnfail(`片段 "${p}" 无法解析(支持:*、a、a-b、*/n、a-b/n、逗号组合)`);if(step<1)returnfail(`片段 "${p}" 中的步进值${step}必须 ≥ 1`);

日和周的关系写在dayHit里,两个字段只要有一个受限,就按并集判断。

functiondayHit(d,E){if(!E.months.has(d.getMonth()+1))returnfalse;constdom=d.getDate(),dow=d.getDay();if(E.domFull&&E.dowFull)returntrue;if(E.domFull)returnE.dows.has(dow);if(E.dowFull)returnE.doms.has(dom);returnE.doms.has(dom)||E.dows.has(dow);}

collectNext按天往后扫,命中就展开当天的时分组合,凑够 10 条为止,外面还挂了一条 40 万天的保险丝。

九、验证与踩坑

页面加载后跑 12 条断言,内置参数下 12/12 通过。

前 7 条盯字段展开、平均间隔、闰年和周日等价,后 5 条专门验非法输入。

这里有一条我自己踩到的坑。

自检里「拒绝非法输入 · 1-」那条是绿的,但它绿得不对。

1-只有一段,被段数校验先拦下了,报的是「表达式只有 1 段,需要恰好 5 段」。

真正的残缺范围提示要到字段内部才看得到,我补测了一次0 1- * * *,页面才给出「片段 “1-” 无法解析(支持:、a、a-b、/n、a-b/n、逗号组合)」。

断言绿了不等于断言测到了你想测的东西,这一条我记下来了。

失败路径我也点了一遍,把分钟字段写成*/0。

页面给的原话是「片段 “*/0” 中的步进值 0 必须 ≥ 1」。

结果区不白屏,橙色提示写着「当前表达式无效,以下保留最近一次有效结果;修正表达式后会自动更新。」

另外三种非法输入的原文我也抄下来了。

  • 99 * * * *给出的是「片段 “99” 起始值 99 超出合法区间 0–59」
  • 0 3 * *给出的是「表达式只有 4 段,需要恰好 5 段(分 时 日 月 周)。例:0 3 * * 1-5」
  • * * * * * *给出的是「表达式有 6 段,需要恰好 5 段(分 时 日 月 周),请删除多余段」

十、实测数据汇总

项目实测值
页面自检12 / 12 通过
单文件体积26858 个字符,621 行
外部依赖1 个 script 标签,0 个 link 标签
资源请求0 条,性能接口里资源列表是空的
*/15 * * * *平均间隔≈15 分钟
0 0 29 2 *相邻两次间隔1461 天
0 0 */5 * *日字段展开1,6,11,16,21,26,31

「资源请求 0 条」是用浏览器性能接口复核的,打开页面到把十来个表达式点完,资源列表一直是空数组。

十一、本地复现

双击 index.html 就能用,不需要装任何依赖。

想走静态服务:python -m http.server 8000,然后访问 http://localhost:8000 。

想看它有没有偷偷联网,按 F12 打开网络面板再切几个表达式,列表里不会多出请求。

十二、说点不足

只支持 5 段式,没有秒字段和年份字段,Quartz 的L、W、#这些扩展一个都没做。

所以「每月最后一个工作日」这类写法,这个页面写不出来。

时区全按浏览器本地时间算,没有夏令时处理,也没有单独的时区选择。

页面在北京打开就算北京时间,放到服务器上看差 8 小时。

*/n一律从字段最小值起步,日字段写*/5展开成 1,6,11,16,21,26,31,不是 5,10,15。

窗口是写死的 30 天,最近 10 次也是写死的,没有做成可调。

页面只做预测,不管执行。

任务跑失败会不会重试、机器关机期间错过的任务要不要补,这些真正运维时会遇到的问题它一概不回答。

直接下载试玩

这个 Demo 已经完整开源,源代码、本文档和全部截图都在公开仓库里,可以直接下载来玩:

  • Demo Park 仓库:https://atomgit.com/deli007/demo_park
  • 本案例目录:https://atomgit.com/deli007/demo_park/tree/main/codearts-cron-expression-lab

打开上面的链接,进入 codearts-cron-expression-lab 文件夹,把 index.html 下载到本地,用浏览器打开即可运行。

十三、用码道做这个页面的体会

第一条是需求里把期望值写死。

我把「*/15展开 = 0,15,30,45」「周日 0 和 7 等价」直接写进需求,交回来的页面就自带这些断言。

第二条是断言尽量写关系式。

0 3 * * 1-5那次我给的是 18~24 次的区间,因为结果随基准日是周几浮动,写死一个常量以后一改参数就会假红。

第三条是让页面把统计也做出来。

只给一个「下次执行时间」查询框,文章里就没有可对比的数字;加上日历和次数统计之后,才有上面那张表。

第四条是失败路径单独交代。

我在需求里写了非法输入要给中文提示并保留上一次有效值,交回来的就是标红加橙色提示,结果区不清空。

第五条是边界要自己验。

2 月 29 日那段我拿了 2028 和 2027 两个基准日各跑一遍,才敢把 366 天和 1461 天写进来。

第六条是自检全绿不等于页面全对。

前面那条「1-」就是例子,函数的断言绿了,真正拦下它的却是段数校验。

接下来我打算把时区和秒级字段补上,再看看能不能把「每月最后一个工作日」这类 Quartz 写法也加进去。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进