BEFORE UPDATE/INSERT 触发器必须用 SIGNAL SQLSTATE '45000' 强制报错中断事务,否则判断逻辑无效;需避开 WEEKDAY 和 HOUR 边界陷阱,精确控制时间范围。

BEFORE UPDATE/INSERT 触发器必须用 SIGNAL 中断
MySQL 触发器不会自动拒绝操作,不抛异常就等于放行。你不能靠 SELECT 或 IF 判断后啥也不做——那只是“看了眼时间就放过去了”。唯一可靠方式是显式调用 SIGNAL SQLSTATE '45000' 强制报错中断事务。
常见错误写法:IF HOUR(NOW()) NOT BETWEEN 9 AND 17 THEN SELECT 'blocked'; END IF; —— 这条 SELECT 在触发器里直接报错(1415 - Not allowed to return a result set from a trigger),而且即使改成赋值变量,没 SIGNAL 仍会静默通过。
- 必须用
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Off-hours edit denied'; - 确保 MySQL 版本 ≥ 5.5 且
sql_mode包含STRICT_TRANS_TABLES,否则SIGNAL可能被忽略 - 不要在触发器里写
ROLLBACK:MySQL 不允许在触发器中执行事务控制语句
时间判断要避开 WEEKDAY 和 HOUR 的边界陷阱
WEEKDAY(NOW()) 返回 0(周一)到 6(周日),而 HOUR(NOW()) 是整点小时数(0–23),两者组合时容易漏掉关键边界。
比如想拦住周五 17:00 之后、周一 9:00 之前的所有修改,不能只写 WEEKDAY(NOW()) IN (5,6) OR HOUR(NOW()) NOT BETWEEN 9 AND 17——这会让周五 17:00:01 到 17:59:59 之间的时间段漏掉,因为 HOUR() 对 17:30 仍返回 17。
- 正确做法是拆开比较:
WEEKDAY(NOW()) IN (5,6) OR HOUR(NOW()) 17 - 若需精确到分钟(如只允许 9:00–17:59),补上
OR (HOUR(NOW()) = 17 AND MINUTE(NOW()) > 59)(虽然逻辑上不可能,但显式写更清晰) - 避免用
BETWEEN 9 AND 17,它包含 17:00:00,但业务通常要求“17:59 前有效”,所以用>= 9 AND 更贴近真实语义
触发器定义必须覆盖 INSERT/UPDATE/DELETE 三类操作
只建一个 BEFORE UPDATE 触发器,拦不住凌晨手动 INSERT 新订单或 DELETE 错误记录。非工作时间的写入风险来自所有 DML 路径,不是只有更新。
你得为每种操作单独建触发器,或者统一用一个通用审计表 + 应用层拦截,但触发器防线必须全量覆盖。
- 对关键业务表(如
orders、payments),至少创建三个触发器:trg_block_offhours_insert、trg_block_offhours_update、trg_block_offhours_delete - 每个触发器都用相同的时间判断逻辑,避免某类操作漏检
- 注意:MySQL 不允许在
BEFORE DELETE中访问NEW,只能用OLD;同理BEFORE INSERT只有NEW,别写OLD.id这类不存在的引用
服务器本地时间 ≠ 业务所在地时间,时区错配会直接让规则失效
当前时间是 2026年9月28日,星期一22时6分——如果你的数据库服务器设的是 UTC,而业务要求按北京时间(UTC+8)判断工作时间,那么 NOW() 返回的是 2026-09-28 14:06:00,直接拿它比 9–18 就全错了。
MySQL 没有内置函数自动转时区后取小时,CONVERT_TZ(NOW(), '+00:00', '+08:00') 是唯一办法,但它在高并发下有性能损耗,且必须确保时区表已加载(mysql_tzinfo_to_sql 导入过)。
- 最稳妥的做法:把数据库服务器时区设为业务所在地时区(如
systemctl set-timezone Asia/Shanghai+ 重启 MySQL) - 如果无法改服务器时区,必须用
CONVERT_TZ(NOW(), '+00:00', '+08:00')替代所有NOW(),并在触发器开头加注释说明 - 测试时一定要连到服务器本地终端执行
date和SELECT NOW();对比,确认两者一致


















