---
title: "帝国 CMS 已完赛的赛程还在首页？MySQL 与 PHP 时区排查记录"
source: https://www.iowill.com/blog/2026/10/empire-cms-schedule-mysql-timezone/
date: 2026-10-20
updated: 2026-10-20
tags: [Docker, MySQL, PHP, 帝国CMS, 故障排查, 时区]
categories: [系统与运维]
---

# 帝国 CMS 已完赛的赛程还在首页？MySQL 与 PHP 时区排查记录

下午看首页，早上 7 点的比赛还在推荐区。卡片上明明写着“已完赛”，却没有隐藏，下方的普通赛程列表也留着多场上午已经结束的比赛。

这次排查最有用的线索，正是“已经显示完赛，却仍然被选中”。状态文字能判断结束，不代表查询条件也会把这条记录排除。

本文根据 2026 年 10 月 20 日的排查记录、保存的首页 HTML 和修正版模板整理。页面样本确认了异常展示，记录确认了 MySQL 当前运行时区的调整；数据库重启后的持久化和重新生成首页后的效果，还需要分别复查。已观察结果与建议验证步骤分别标注。

## 返回 HTML 中的过期赛事

最初怀疑首页没有重新生成，或者浏览器还在使用缓存。按当时记录，首页文件已更新，返回的 HTML 里仍然包含过期赛事。

保存的页面也能看到：推荐卡片中有 06:30、07:00、08:00 的赛事，状态都是“已完赛”。这说明过期卡片已经进入页面输出，继续刷新浏览器无法解释筛选为何失效。

手头三个不同名称的诊断 HTML，SHA-256 完全一致。它们只能算同一份页面证据，不能证明不同访问方式返回了不同内容，也不能作为修复前后对比。

需要核对模板中**赛事筛选条件与完赛状态的判断条件是否一致**。

## 查询保留三小时，状态却只算两小时

原推荐区条件是：

```php
isgood=1 and unix_timestamp(livetime)>=$time-10800
```

`10800` 秒是三小时。这个条件选中未来赛事，以及开赛时间在最近三小时以内的推荐赛事。

显示状态的代码却用了两小时：

```php
$livetime = strtotime($bqr['livetime']);

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

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

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

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

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

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

```bash
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;'
```

当时结果为：

```text
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 当前连接的检查结果如下：

```sql
SELECT
    NOW() AS db_now,
    UTC_TIMESTAMP() AS utc_now,
    @@session.time_zone AS session_timezone,
    @@global.time_zone AS global_timezone;
```

```text
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` 或格式一致的日期字符串：

```text
2026-10-20 07:00:00
```

它没有携带时区。PHP 在上海时区下用 `strtotime()` 解析，得到北京时间早上 7 点；MySQL 的 `UNIX_TIMESTAMP(livetime)` 则按会话时区解释这个日期。在 UTC 会话里，这个值对应 UTC 早上 7 点，换到北京时间就是下午 3 点。这个转换规则可查 [MySQL 日期函数说明](https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_unix-timestamp)，PHP 的解析规则见 [strtotime 文档](https://www.php.net/manual/en/function.strtotime.php)。

![同一个不带时区的 07:00 日期在 PHP 上海时区与 MySQL UTC 会话中的不同解释](https://img.iowill.com/file/%E7%B3%BB%E7%BB%9F%E4%B8%8E%E8%BF%90%E7%BB%B4/%E5%B8%9D%E5%9B%BDCMS%E8%B5%9B%E7%A8%8B%E4%B8%8EMySQL%E6%97%B6%E5%8C%BA%E6%8E%92%E6%9F%A5/body1.png)

以北京时间 17:00 为例，两条判断就分开了：

| 判断位置 | 解释出的北京时间 | 距离当前时间 | 结果 |
| --- | --- | --- | --- |
| SQL 推荐筛选 | 15:00 | 两小时 | 仍满足最近三小时条件 |
| PHP 状态判断 | 07:00 | 十小时 | 已完赛 |
| 卡片输出 | 使用查询结果 | 未跳过已完赛记录 | 继续显示 |

这并不是 `time()` 自身带了错误时区。Unix 时间戳描述的是一个绝对时刻，问题发生在**把不带时区的日期解释成时间戳**时。[PHP time 文档](https://www.php.net/manual/en/function.time.php)也区分了时间戳与本地日期显示。

还差一步不能省：CLI 的 PHP 设置不一定等于网站请求或静态生成进程的设置，命令行 MySQL 会话也不一定等于 CMS 的数据库会话。若 CMS 建连后另行执行过 `SET time_zone`，必须检查实际生成首页的那条连接，才能确认八小时偏差确实发生在网站链路中。

## 先核对字段，再调整数据库会话

在套用修复前，先看 `livetime` 的类型。表前缀按实际环境替换，例如：

```sql
SHOW COLUMNS FROM phome_ecms_zb LIKE 'livetime';
```

再沿着采集或录入流程确认，保存的是北京时间还是 UTC。`DATETIME` 与 `TIMESTAMP` 的行为不同，后者会在存储和取出时涉及会话时区转换，不能直接套用“北京时间字符串”的解释。[MySQL 字段类型说明](https://dev.mysql.com/doc/refman/8.0/en/datetime.html)列出了这种差异。

对于本案例“业务按北京时间保存和处理日期”的前提，可以在有相应权限的数据库连接中设置：

```sql
SET GLOBAL time_zone = '+08:00';
SET SESSION time_zone = '+08:00';
```

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

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

原记录中的后续检查为：

```sql
SELECT VERSION(), NOW(), @@global.time_zone, @@session.time_zone;
```

```text
VERSION():          5.6.51
NOW():              2026-10-20 17:12:36
@@global.time_zone:  +08:00
@@session.time_zone: +08:00
```

这证明当前全局值与当前连接已经调整成功，尚不能代替 CMS 实际连接的检查。全局与会话设置的作用范围见 [MySQL 时区文档](https://dev.mysql.com/doc/refman/8.0/en/time-zone-support.html)。这里引用官方手册核对通用行为，现场版本仍是记录中的 **5.6.51**。

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

```sql
SELECT
    @@session.time_zone,
    UNIX_TIMESTAMP('2026-10-20 07:00:00') AS kickoff_epoch;
```

```php
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]` 段，添加：

```ini
[mysqld]
default-time-zone = '+08:00'
```

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

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

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

## 修正版模板：先算截止时间，再统一筛选

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

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

推荐区的帝国 CMS 标签改成：

```html
[e:loop={'zb',8,18,0,"isgood=1 and livetime>'$scheduleCutoff'", "livetime asc"}]
```

其中模型名、数量和标签参数沿用该模板，不是所有帝国 CMS 站点都通用。关键变化是筛选条件：

```sql
livetime > '由 PHP 计算的北京时间截止值'
```

这样比较的是同一业务时间基准下的日期值，避免在这条筛选中再次把 `livetime` 按 MySQL 会话时区转换成时间戳。它适用于已经确认语义的 `DATETIME`；若是字符串，必须保证全部采用可按时间顺序比较的 `YYYY-MM-DD HH:mm:ss` 格式，没有混用 UTC、其他格式或异常值。`TIMESTAMP` 和 UTC 存储需要按自己的数据约定另行设计。

普通赛程列表也改用同一个 `$scheduleCutoff`。例如原模板昨天、今天分组中的查询，可保留日期分组条件，同时追加统一截止条件：

```php
$sql2 = "select * from {$dbtbpre}ecms_zb "
      . "where to_days('" . $curdate . "')=to_days(livetime) "
      . "and livetime>'" . $scheduleCutoff . "' "
      . "order by livetime limit 50";
```

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

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

```php
$livetime = strtotime($bqr['livetime']);

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

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

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

![统一北京时间截止值后，查询、输出过滤与首页重新生成的关系](https://img.iowill.com/file/%E7%B3%BB%E7%BB%9F%E4%B8%8E%E8%BF%90%E7%BB%B4/%E5%B8%9D%E5%9B%BDCMS%E8%B5%9B%E7%A8%8B%E4%B8%8EMySQL%E6%97%B6%E5%8C%BA%E6%8E%92%E6%9F%A5/body2.png)

按这份规则，边界应当一致：

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

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

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

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

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

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

复查顺序为：

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

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

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