下午看首页,早上 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
2
3
4
5
6
7
8
9
$livetime = strtotime($bqr['livetime']);

if ($time < $livetime) {
$liver = '未开始';
} elseif ($time > $livetime && $time < $livetime + 7200) {
$liver = '直播中';
} else {
$liver = '已完赛';
}

这里至少有三个边界要处理:

  • 开赛满两小时、未满三小时,查询仍会选中,状态已经显示完赛。
  • 进入“已完赛”分支后,代码仍继续拼接卡片,没有跳过输出。
  • 当前时间恰好等于开赛时间时,两个严格比较都不成立,也会落进“已完赛”。

不过,这些只能解释局部规则冲突。早上 7 点的比赛到下午 5 点还出现,差距远超过一小时,需要继续检查日期转换。

同一个 07:00,被解释成了两个时刻

排查记录先检查了应用容器与 PHP:

1
2
docker exec cms date
docker exec cms php -r 'echo date_default_timezone_get(), PHP_EOL, date("Y-m-d H:i:s P"), PHP_EOL;'

当时结果为:

1
2
3
4
Tue Oct 20 09:10:41 UTC 2026

Asia/Shanghai
2026-10-20 17:10:41 +08:00

cms 是示例中的容器名,执行时换成实际名称。这里的 UTC 09:10 和北京时间 17:10 是同一个时刻,不能仅凭显示差八小时,就认定容器的系统时钟出错。

真正值得比较的是应用解析日期时使用的时区。PHP CLI 显示 Asia/Shanghai;MySQL 当前连接的检查结果如下:

1
2
3
4
5
SELECT
NOW() AS db_now,
UTC_TIMESTAMP() AS utc_now,
@@session.time_zone AS session_timezone,
@@global.time_zone AS global_timezone;
1
2
3
4
db_now:           2026-10-20 09:10:54
utc_now: 2026-10-20 09:10:54
session_timezone: SYSTEM
global_timezone: SYSTEM

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 文档。

同一个不带时区的 07:00 日期在 PHP 上海时区与 MySQL UTC 会话中的不同解释

以北京时间 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
2
SET GLOBAL time_zone = '+08:00';
SET SESSION time_zone = '+08:00';

这两条语句作用不同:全局值决定后续新连接的默认值;会话值只改变当前连接。已经建立的其他连接,不会随全局设置自动切换。使用持久连接的 CMS 需要重建对应连接,或在建连初始化时明确设置业务所需时区。

更改全局默认值前,还要确认同一数据库服务上的其他应用是否依赖原配置。若只有这个 CMS 需要北京时间,可以优先在它的连接初始化中设置会话值。

原记录中的后续检查为:

1
SELECT VERSION(), NOW(), @@global.time_zone, @@session.time_zone;
1
2
3
4
VERSION():          5.6.51
NOW(): 2026-10-20 17:12:36
@@global.time_zone: +08:00
@@session.time_zone: +08:00

这证明当前全局值与当前连接已经调整成功,尚不能代替 CMS 实际连接的检查。全局与会话设置的作用范围见 MySQL 时区文档。这里引用官方手册核对通用行为,现场版本仍是记录中的 5.6.51。

为了核对解析结果,还可以在 CMS 实际连接中执行下面的只读诊断,并与 PHP 的结果比较:

1
2
3
SELECT
@@session.time_zone,
UNIX_TIMESTAMP('2026-10-20 07:00:00') AS kickoff_epoch;
1
2
date_default_timezone_set('Asia/Shanghai');
echo strtotime('2026-10-20 07:00:00'), PHP_EOL;

两个值一致,才说明这一日期在两端被解释为同一时刻。这里是建议复查的步骤,不是已有服务器执行记录。

MySQL 5.6 的设置还要写进持久配置

运行时设置不会自动变成重启后的配置。现场是 MySQL 5.6.51,不能直接照搬 MySQL 8.0 的 SET PERSIST 用法。

在数据库实际加载的配置文件中,找到 [mysqld] 段,添加:

1
2
[mysqld]
default-time-zone = '+08:00'

已有 [mysqld] 段就只加配置行。使用固定 +08:00 偏移,不依赖 MySQL 已加载命名时区表;不要把 PHP 能识别 Asia/Shanghai 等同于 MySQL 一定能识别同名设置。

Docker 部署要确认文件来自宿主机挂载或其他持久化配置,同时检查启动参数和 !includedir 引入的文件是否有冲突设置。只改容器可写层,重建后可能丢失。

重启或重建后,再用一个新连接复查 NOW()、全局时区和会话时区。本次材料没有提供这一步的结果,因此这里只记录持久化方法,不把它写成已验证完成。

修正版模板:先算截止时间,再统一筛选

数据库调整解决时间解释问题,模板本身的两小时、三小时冲突仍需消除。提供的修正版没有继续对字段调用 UNIX_TIMESTAMP(),而是在 PHP 中统一计算北京时间截止值:

1
2
3
4
5
6
7
<?php
// 前提:livetime 按北京时间保存。
date_default_timezone_set('Asia/Shanghai');
$scheduleNow = time();
$scheduleWindow = 7200;
$scheduleCutoff = date('Y-m-d H:i:s', $scheduleNow - $scheduleWindow);
?>

推荐区的帝国 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
2
3
4
$sql2 = "select * from {$dbtbpre}ecms_zb "
. "where to_days('" . $curdate . "')=to_days(livetime) "
. "and livetime>'" . $scheduleCutoff . "' "
. "order by livetime limit 50";

示例中的日期来自程序计算,不要把外部请求参数直接拼进 SQL。该查询只展示原模板的修复位置,未对索引或执行计划作性能验证。

输出前再用相同的时间和窗口判断,推荐区使用 $bqr,普通列表使用 $tr:

1
2
3
4
5
6
7
$livetime = strtotime($bqr['livetime']);

if ($livetime === false || $scheduleNow >= $livetime + $scheduleWindow) {
continue;
}

$liver = $scheduleNow < $livetime ? '未开始' : '直播中';

这里 continue 必须位于循环内部,而且要在拼接卡片、递增有效条数之前执行。普通列表将字段替换为 $tr['livetime']。只改状态文案,记录仍然会输出。

统一北京时间截止值后,查询、输出过滤与首页重新生成的关系

按这份规则,边界应当一致:

当前时刻相对开赛时间 是否输出 状态
尚未开赛 是 未开始
恰好开赛 是 直播中
已开赛,未满两小时 是 直播中
恰好两小时或更久 否 不再生成卡片
解析失败 否 跳过并排查源数据

两小时仍然只是模板的估算窗口,并非真实完赛状态。加时、延迟开赛或中断都可能超出它;数据源若有可信的赛事状态和实际结束时间,应优先据此设计展示规则。

修正版顶部另有独立的“热门赛程比分/录像”模块,它使用另一组查询,并不属于这次推荐区与普通列表的两小时过滤。若要求所有模块都隐藏完赛赛事,需要先确定录像入口是否也适用这项业务规则,不能把局部修复写成全首页模块均已处理。

重新生成首页,再做一次原故障复测

帝国 CMS 生成的是静态首页时,修改数据库或模板不会让已有 HTML 自动重新执行 PHP。保存模板后,需要重新生成首页,再检查返回的 HTML 中是否还有那张早上 7 点的卡片。

模板尾部虽然包含定时生成扩展入口,也不能据此认定任务一直正常运行。需要核对触发方式、执行日志和实际生成时间;没有可靠的生成机制,状态仍会停留在上一次生成时刻。即使任务正常,静态页面的更新也受生成周期影响。

可以按下面顺序复查:

  1. 在实际生成首页的 PHP 进程与 MySQL 连接中,确认日期解析一致。
  2. 用未来、恰好开赛、刚满两小时、已过期和无效日期记录核对过滤边界。
  3. 保存模板并重新生成首页,直接检查响应 HTML 中的目标记录。
  4. 如果源站输出已更新而访问端仍是旧页面,再检查 CDN、反向代理或浏览器缓存。
  5. 等待下一次计划生成,确认赛事会随时间退出;数据库重启后另行复查持久配置。

本次能确认的是:异常页面里确实保留了已完赛卡片,原模板的窗口规则有冲突,排查记录中的数据库运行时区已调整。修正版提供了统一截止值与输出过滤,但持久配置、CMS 实际会话和重新生成后的线上结果仍需要上述复测。

已有记录若本来就按北京时间保存,直接批量加减八小时会改变正确的数据。应先确认字段语义和解析链路,再决定是否需要修数据。