第8章:定时任务与密钥 —— CronJob 与 Secrets
约 4 分钟 · 更新于 2026-09-02
两个"小而关键"的原语:CronJob 让定时任务成为一行声明(不用管调度器基础设施),Secrets 让密钥彻底离开代码库。两者都有几条必须记住的硬约束。
一、CronJob:声明式定时任务
typescript
import { CronJob } from "encore.dev/cron";
import { api } from "encore.dev/api";
// 被调度的端点:无参、无响应
export const reconcileOrders = api({}, async () => {
// 对账逻辑
});
// 声明定时任务
const _ = new CronJob("reconcile-orders", {
title: "对账任务",
every: "1h",
endpoint: reconcileOrders,
});
不需要部署任何调度器:Encore(自托管镜像内置调度,Encore Cloud 平台托管)负责按时调用端点。
两种调度表达
typescript
// every:周期式,从 UTC 午夜起算
every: "10m" // 每 10 分钟:00:00, 00:10, 00:20 ...
every: "6h" // 每 6 小时:00:00, 06:00, 12:00, 18:00
// schedule:标准 cron 表达式,复杂调度用
schedule: "0 4 15 * *" // 每月 15 日 UTC 04:00
every 必须整除 24 小时
10m、6h 合法;7h 非法(24 无法整除)——这是为了保证每天的执行时刻可预期。不能整除的需求用 schedule cron 表达式表达。注意两种表达都以 UTC 计时,换算北京时间自己减 8 小时。
四条硬约束
- 本地和 Preview 环境不执行——encore run 下 Cron 不会跑。本地调试就手动 curl 那个端点(所以端点常同时声明 expose 或留私有再用仪表盘 API Explorer 调);
- 端点必须无参——调度器不会构造参数;
- 端点必须幂等——调度系统故障恢复时可能补跑或重跑;
- 端点可以是私有(不写 expose)——Cron 调度属于内部调用,无需暴露公网。
适用边界
CronJob 适合运维型周期任务:对账、清理过期数据、缓存预热、报表生成、监控巡检(第 14 章实战)。它不是业务流程编排器——"下单 30 分钟未支付自动取消"这类单实例定时需求,用"Cron 每分钟扫表"模式实现,而不是幻想给每个订单挂一个定时器。
typescript
// 典型模式:Cron 扫表处理到期业务
export const cancelExpired = api({}, async () => {
await db.exec`
UPDATE orders SET status = 'CANCELLED'
WHERE status = 'CREATED' AND created_at < NOW() - INTERVAL '30 minutes'
`;
});
const _ = new CronJob("cancel-expired", {
title: "取消超时未支付订单",
every: "1m",
endpoint: cancelExpired,
});
二、Secrets:密钥管理
声明与使用
typescript
import { secret } from "encore.dev/config";
// 声明:返回的是一个函数
const paymentApiKey = secret("PaymentApiKey");
// 使用:调用函数取值(运行时按当前环境解析)
async function callPaymentGateway() {
const resp = await fetch("https://gateway.example.com/pay", {
headers: { Authorization: `Bearer ${paymentApiKey()}` },
});
}
要点:
- secret("Name") 也是静态分析识别的原语——应用模型知道你的应用需要哪些密钥,缺值会在部署前报出;
- 密钥名全应用唯一(跨服务共享同名密钥即同一个值);
- 代码库里永远没有明文。
三种设置方式
bash
# 1. CLI(推荐):按环境类型设置
encore secret set --type dev,local,pr PaymentApiKey
encore secret set --type prod PaymentApiKey
# 2. 本地覆盖文件(gitignore 掉):.secrets.local.cue
# PaymentApiKey: "test-key-xxxx"
# 3. Encore Cloud 控制台:Settings → Secrets
环境类型四种:production(prod) / development(dev) / preview(pr) / local。解析优先级:具体环境的专属值 > 环境类型值;本地开发时 .secrets.local.cue 覆盖一切。
管理命令:
bash
encore secret list # 列出全部密钥
encore secret archive <id> # 归档旧值
encore secret unarchive <id> # 恢复
本机 B2B 项目的密钥实践
上游分销网关的 AK/SK、支付渠道私钥全部走 secret() + .secrets.local.cue(gitignored),仓库里只提交 .secrets.local.cue.example 模板。可选配置类 secret(如网关域名覆盖 DistGatewayUrl)约定"空值=默认行为,配错报错不回落"——把配置错误暴露在启动期而不是线上请求期。
自托管场景
不用 Encore Cloud 时,密钥通过 Docker 运行时的环境变量/挂载注入(第 16 章 infra-config.json 里 {"$env": "DB_PASSWORD"} 的形式),secret() 的代码写法不变。
三、两原语的共同哲学
CronJob 和 Secret 看似不相关,但体现同一设计:把"应用需要什么"声明在代码里,把"具体值/具体调度"留给环境。于是:
- 静态分析能画出完整依赖图(这个应用需要 3 个密钥、2 个定时任务);
- 换环境不改代码;
- 评审代码就能看见全部基础设施需求——不存在"部署时才发现少配了个密钥"。
四、常见误区
- every: "7h"——不能整除 24h,编译报错。
- 在本地等 Cron 触发——本地不执行,手动调端点。
- Cron 端点带参数或不幂等——前者不满足约束,后者在补跑时出重复副作用。
- 忘了 UTC——schedule: "0 4 * * *" 是北京时间中午 12 点。
- 把密钥写进 encore.app 或普通配置文件——密钥只走 secret()。
- .secrets.local.cue 提交进 git——必须 gitignore,仓库只放 .example 模板。
- 在 handler 里 secret("Name") 现声明——原语包级声明;handler 里只调用取值函数。
- 用 Cron 实现"每订单一个定时器"——用扫表模式。
五、实践练习
- 给订单应用加"每分钟取消超时未支付订单"的 Cron(扫表模式),本地手动 curl 验证逻辑,观察仪表盘里 Cron 的注册信息。
- 把练习 1 的间隔改成 90s,看编译器的反应;再改用 schedule 表达"每天北京时间 09:00"(提示:UTC 01:00)。
- 声明 SmsApiKey 密钥,用 .secrets.local.cue 提供本地值,在通知服务里读取;确认明文不出现在任何提交文件里。
- 用 encore secret set --type dev,local,pr 与 --type prod 给同一密钥设不同值,说出每个环境各取到哪个。
- 结合第 7 章:实现 outbox 补发——事件表 + 每分钟 Cron 扫描未发布事件重新 publish。
六、总结
- CronJob = 一行声明的定时任务:every 须整除 24h、schedule 走 cron 表达式、全部 UTC。
- 四条硬约束:本地不跑、端点无参、必须幂等、可以私有。
- Cron 的正确用法是运维型周期任务与"扫表处理到期业务",不是业务编排器。
- secret() 声明密钥需求,值按环境类型解析:CLI 设置 / .secrets.local.cue 本地覆盖 / 云控制台。
- 两者共同哲学:需求进代码、取值留环境,依赖在编译期全部可见。
请继续阅读:第9章:对象存储与缓存。
原始资料引用