帝国 CMS 已完赛的赛程还在首页?MySQL 与 PHP 时区排查记录
下午看首页,早上 7 点的比赛还在推荐区。卡片上明明写着“已完赛”,却没有隐藏,下方的普通赛程列表也留着多场上午已经结束的比赛。
这次排查最有用的线索,正是“已经显示完赛,却仍然被选中”。状态文字能判断结束,不代表查询条件也会把这条记录排除。
本文根据 2026 年 10 月 20 日的排查记录、保存的首页 HTML 和修正版模板整理。页面样本确认了异常展示,记录确认了 MySQL 当前运行时区的调整;数据库重启后的持久化和重新生成首页后的效果,还需要分别复查。下面会把已经观察到的结果与建议验证步骤分开说明。
先看返回的 HTML,别急着改数据
最初怀疑首页没有重新生成,或者浏览器还在使用缓存。按当时记录,首页文件已更新,返回的 HTML 里仍然包含过期赛事。
保存的页面也能看到:推荐卡片中有 06:30、07:00、08:00 的赛事,状态都是“已完赛”。这说明过期卡片已经进入页面输出,继续刷新浏览器无法解释筛选为何失效。
手头三个不同名称的诊断 HTML,SHA-256 完全一致。它们只能算同一份页面证据,不能证明不同访问方式返回了不同内容,也不能作为修复前后对比。
接下来要找的是:模板在哪一步选中了赛事,又在哪一步判断它已经结束?
查询保留三小时,状态却只算两小时
原推荐区条件是:
1 | isgood=1 and unix_timestamp(livetime)>=$time-10800 |
10800 秒是三小时。这个条件选中未来赛事,以及开赛时间在最近三小时以内的推荐赛事。
显示状态的代码却用了两小时:
1 | $livetime = strtotime($bqr['livetime']); |
这里至少有三个边界要处理:
- 开赛满两小时、未满三小时,查询仍会选中,状态已经显示完赛。
- 进入“已完赛”分支后,代码仍继续拼接卡片,没有跳过输出。
- 当前时间恰好等于开赛时间时,两个严格比较都不成立,也会落进“已完赛”。
不过,这些只能解释局部规则冲突。早上 7 点的比赛到下午 5 点还出现,差距远超过一小时,需要继续检查日期转换。
同一个 07:00,被解释成了两个时刻
排查记录先检查了应用容器与 PHP:
1 | docker exec cms date |
当时结果为:
1 | Tue Oct 20 09:10:41 UTC 2026 |
cms 是示例中的容器名,执行时换成实际名称。这里的 UTC 09:10 和北京时间 17:10 是同一个时刻,不能仅凭显示差八小时,就认定容器的系统时钟出错。
真正值得比较的是应用解析日期时使用的时区。PHP CLI 显示 Asia/Shanghai;MySQL 当前连接的检查结果如下:
1 | SELECT |
1 | db_now: 2026-10-20 09:10:54 |
SYSTEM 的意思是跟随数据库服务所用的系统时区,不是固定指 UTC。这次 NOW() 与 UTC_TIMESTAMP() 相同,说明被检查的连接当时表现为 UTC。
假设 livetime 保存的是北京时间,并且字段为 DATETIME 或格式一致的日期字符串:
1 | 2026-10-20 07:00:00 |
它没有携带时区。PHP 在上海时区下用 strtotime() 解析,得到北京时间早上 7 点;MySQL 的 UNIX_TIMESTAMP(livetime) 则按会话时区解释这个日期。在 UTC 会话里,这个值对应 UTC 早上 7 点,换到北京时间就是下午 3 点。这个转换规则可查 MySQL 日期函数说明,PHP 的解析规则见 strtotime 文档。

以北京时间 17:00 为例,两条判断就分开了:
| 判断位置 | 解释出的北京时间 | 距离当前时间 | 结果 |
|---|---|---|---|
| SQL 推荐筛选 | 15:00 | 两小时 | 仍满足最近三小时条件 |
| PHP 状态判断 | 07:00 | 十小时 | 已完赛 |
| 卡片输出 | 使用查询结果 | 未跳过已完赛记录 | 继续显示 |
这并不是 time() 自身带了错误时区。Unix 时间戳描述的是一个绝对时刻,问题发生在把不带时区的日期解释成时间戳时。PHP time 文档也区分了时间戳与本地日期显示。
还差一步不能省:CLI 的 PHP 设置不一定等于网站请求或静态生成进程的设置,命令行 MySQL 会话也不一定等于 CMS 的数据库会话。若 CMS 建连后另行执行过 SET time_zone,必须检查实际生成首页的那条连接,才能确认八小时偏差确实发生在网站链路中。
先核对字段,再调整数据库会话
在套用修复前,先看 livetime 的类型。表前缀按实际环境替换,例如:
1 | SHOW COLUMNS FROM phome_ecms_zb LIKE 'livetime'; |
再沿着采集或录入流程确认,保存的是北京时间还是 UTC。DATETIME 与 TIMESTAMP 的行为不同,后者会在存储和取出时涉及会话时区转换,不能直接套用“北京时间字符串”的解释。MySQL 字段类型说明列出了这种差异。
对于本案例“业务按北京时间保存和处理日期”的前提,可以在有相应权限的数据库连接中设置:
1 | SET GLOBAL time_zone = '+08:00'; |
这两条语句作用不同:全局值决定后续新连接的默认值;会话值只改变当前连接。已经建立的其他连接,不会随全局设置自动切换。使用持久连接的 CMS 需要重建对应连接,或在建连初始化时明确设置业务所需时区。
更改全局默认值前,还要确认同一数据库服务上的其他应用是否依赖原配置。若只有这个 CMS 需要北京时间,可以优先在它的连接初始化中设置会话值。
原记录中的后续检查为:
1 | SELECT VERSION(), NOW(), @@global.time_zone, @@session.time_zone; |
1 | VERSION(): 5.6.51 |
这证明当前全局值与当前连接已经调整成功,尚不能代替 CMS 实际连接的检查。全局与会话设置的作用范围见 MySQL 时区文档。这里引用官方手册核对通用行为,现场版本仍是记录中的 5.6.51。
为了核对解析结果,还可以在 CMS 实际连接中执行下面的只读诊断,并与 PHP 的结果比较:
1 | SELECT |
1 | date_default_timezone_set('Asia/Shanghai'); |
两个值一致,才说明这一日期在两端被解释为同一时刻。这里是建议复查的步骤,不是已有服务器执行记录。
MySQL 5.6 的设置还要写进持久配置
运行时设置不会自动变成重启后的配置。现场是 MySQL 5.6.51,不能直接照搬 MySQL 8.0 的 SET PERSIST 用法。
在数据库实际加载的配置文件中,找到 [mysqld] 段,添加:
1 | [mysqld] |
已有 [mysqld] 段就只加配置行。使用固定 +08:00 偏移,不依赖 MySQL 已加载命名时区表;不要把 PHP 能识别 Asia/Shanghai 等同于 MySQL 一定能识别同名设置。
Docker 部署要确认文件来自宿主机挂载或其他持久化配置,同时检查启动参数和 !includedir 引入的文件是否有冲突设置。只改容器可写层,重建后可能丢失。
重启或重建后,再用一个新连接复查 NOW()、全局时区和会话时区。本次材料没有提供这一步的结果,因此这里只记录持久化方法,不把它写成已验证完成。
修正版模板:先算截止时间,再统一筛选
数据库调整解决时间解释问题,模板本身的两小时、三小时冲突仍需消除。提供的修正版没有继续对字段调用 UNIX_TIMESTAMP(),而是在 PHP 中统一计算北京时间截止值:
1 |
|
推荐区的帝国 CMS 标签改成:
1 | [e:loop={'zb',8,18,0,"isgood=1 and livetime>'$scheduleCutoff'", "livetime asc"}] |
其中模型名、数量和标签参数沿用该模板,不是所有帝国 CMS 站点都通用。关键变化是筛选条件:
1 | livetime > '由 PHP 计算的北京时间截止值' |
这样比较的是同一业务时间基准下的日期值,避免在这条筛选中再次把 livetime 按 MySQL 会话时区转换成时间戳。它适用于已经确认语义的 DATETIME;若是字符串,必须保证全部采用可按时间顺序比较的 YYYY-MM-DD HH:mm:ss 格式,没有混用 UTC、其他格式或异常值。TIMESTAMP 和 UTC 存储需要按自己的数据约定另行设计。
普通赛程列表也改用同一个 $scheduleCutoff。例如原模板昨天、今天分组中的查询,可保留日期分组条件,同时追加统一截止条件:
1 | $sql2 = "select * from {$dbtbpre}ecms_zb " |
示例中的日期来自程序计算,不要把外部请求参数直接拼进 SQL。该查询只展示原模板的修复位置,未对索引或执行计划作性能验证。
输出前再用相同的时间和窗口判断,推荐区使用 $bqr,普通列表使用 $tr:
1 | $livetime = strtotime($bqr['livetime']); |
这里 continue 必须位于循环内部,而且要在拼接卡片、递增有效条数之前执行。普通列表将字段替换为 $tr['livetime']。只改状态文案,记录仍然会输出。

按这份规则,边界应当一致:
| 当前时刻相对开赛时间 | 是否输出 | 状态 |
|---|---|---|
| 尚未开赛 | 是 | 未开始 |
| 恰好开赛 | 是 | 直播中 |
| 已开赛,未满两小时 | 是 | 直播中 |
| 恰好两小时或更久 | 否 | 不再生成卡片 |
| 解析失败 | 否 | 跳过并排查源数据 |
两小时仍然只是模板的估算窗口,并非真实完赛状态。加时、延迟开赛或中断都可能超出它;数据源若有可信的赛事状态和实际结束时间,应优先据此设计展示规则。
修正版顶部另有独立的“热门赛程比分/录像”模块,它使用另一组查询,并不属于这次推荐区与普通列表的两小时过滤。若要求所有模块都隐藏完赛赛事,需要先确定录像入口是否也适用这项业务规则,不能把局部修复写成全首页模块均已处理。
重新生成首页,再做一次原故障复测
帝国 CMS 生成的是静态首页时,修改数据库或模板不会让已有 HTML 自动重新执行 PHP。保存模板后,需要重新生成首页,再检查返回的 HTML 中是否还有那张早上 7 点的卡片。
模板尾部虽然包含定时生成扩展入口,也不能据此认定任务一直正常运行。需要核对触发方式、执行日志和实际生成时间;没有可靠的生成机制,状态仍会停留在上一次生成时刻。即使任务正常,静态页面的更新也受生成周期影响。
可以按下面顺序复查:
- 在实际生成首页的 PHP 进程与 MySQL 连接中,确认日期解析一致。
- 用未来、恰好开赛、刚满两小时、已过期和无效日期记录核对过滤边界。
- 保存模板并重新生成首页,直接检查响应 HTML 中的目标记录。
- 如果源站输出已更新而访问端仍是旧页面,再检查 CDN、反向代理或浏览器缓存。
- 等待下一次计划生成,确认赛事会随时间退出;数据库重启后另行复查持久配置。
本次能确认的是:异常页面里确实保留了已完赛卡片,原模板的窗口规则有冲突,排查记录中的数据库运行时区已调整。修正版提供了统一截止值与输出过滤,但持久配置、CMS 实际会话和重新生成后的线上结果仍需要上述复测。
已有记录若本来就按北京时间保存,直接批量加减八小时会改变正确的数据。应先确认字段语义和解析链路,再决定是否需要修数据。