hyperf定时任务在utc 9点(即北京时间17点)执行,本质是时区未对齐:默认依赖time.local,容器中常fallback至utc;须在crontab.php中显式配置'timezone' => 'asia/shanghai',预装tzdata,并验证getnexttime()输出t为cst。

Hyperf 定时任务出现“明明配了 0 9 * * *,却在 UTC 时间 9 点(即北京时间 17 点)执行”,本质不是 cron 不准,而是时区没对齐——Hyperf 默认用 time.Local,而容器或服务器里它大概率 fallback 到 UTC。校准关键不在改代码逻辑,而在显式绑定可信时区并验证生效。
一、强制指定可信时区,别信 time.Local
不要依赖 time.Now().Location() 或系统默认时区。Docker 镜像通常没装 tzdata,time.LoadLocation("Asia/Shanghai") 失败会静默回退到 UTC,且不报错。
- 在
config/autoload/crontab.php中明确设置:'timezone' => 'Asia/Shanghai', - 确保该配置被 crontab 组件实际读取——启动后检查日志是否打印
[INFO] Crontab manager started with X tasks,并确认timezone已生效 - 若使用注解方式(
@Crontab),时区仍由全局配置控制,无需每个类重复声明
二、验证 cron 解析结果是否真按本地时区算
光设配置不够,得看框架内部是否真正按你期望的时区解析表达式。最直接的方式是查调度器维护的下次触发时间:
- 启动服务后,在任意地方(如命令行任务或 HTTP 接口)调用:
$crontabManager = $container->get(\Hyperf\Crontab\Manager::class);<br>foreach ($crontabManager->getEntries() as $entry) {<br> echo $entry->getName() . ': next run at ' . $entry->getNextTime()->format('Y-m-d H:i:s T') . PHP_EOL;<br>} - 观察输出中的
T(时区缩写),应为CST(中国标准时间),而非UTC或GMT - 若显示 UTC,说明
timezone配置未生效,或time.LoadLocation加载失败(可加日志验证:var_dump(time_load_location("Asia/Shanghai"));)
三、容器环境必须预装 tzdata
Docker 内 time.LoadLocation("Asia/Shanghai") 失败是高频原因。Alpine 镜像默认无时区数据,Ubuntu/Debian 镜像也常精简掉。
- 在
Dockerfile中加入:RUN apk add --no-cache tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime(Alpine)
或RUN apt-get update && apt-get install -y tzdata && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime(Debian/Ubuntu) - 构建镜像后,进入容器执行:
date—— 应显示 CST 时间ls /usr/share/zoneinfo/Asia/ | grep Shanghai—— 应存在 - 避免用
ENV TZ=Asia/Shanghai替代,它只影响部分命令,不解决 Go runtime 的time.LoadLocation问题
四、避免规则字段语义冲突导致“假漂移”
看起来是时区问题,有时其实是 cron 表达式本身无法触发,让人误以为“延迟”。Hyperf 使用的 cron 解析器遵循 Quartz 标准,日(Day-of-month)和周(Day-of-week)字段互斥:
- 错误写法:
0 0 9 1 * 1 *(每月 1 号且周一 9 点)→ 实际永不触发 - 正确写法:
只按日期:0 0 9 1 * ? *
只按星期:0 0 9 ? * 1 * - 前端填写时,若 UI 未做语义校验,这类表达式能保存但不会执行——先用 crontab.guru 校验语义再填入











