触发器应建在所有实际写入状态的源头表上,如packages和package_events表,并为分库分表逐个创建;需同时定义insert和update触发器且逻辑分离;须标准化状态值并校验枚举;避免同步调用外部服务,高并发下可改用异步日志队列。

触发器该挂在哪张表上才不会漏掉状态变更
物流系统里包裹状态通常存在 packages 表的 status 字段,但直接监听这张表容易遗漏——比如批量更新时用 UPDATE packages SET status = 'delivered' WHERE order_id IN (...),触发器能捕获,但若业务层绕过主表、直接往 package_events 插记录,或通过消息队列异步改状态,那触发器就完全失效。
真正可靠的做法是:把触发器建在所有**实际写入状态的源头表**上。常见组合包括:
-
packages表(主状态字段status或current_status) -
package_events表(每次事件都应生成轨迹,哪怕只是“状态未变但扫描了”) - 如果用了分库分表或影子表(如
packages_2024),触发器必须逐个创建,MySQL 不支持跨库触发器
INSERT 和 UPDATE 触发器都要写,且逻辑不能一样
只写 UPDATE 触发器会漏掉首次插入时的状态(比如新建运单就已是 packed);只写 INSERT 又抓不到后续变更。更关键的是:两者处理逻辑必须区分。
示例(PostgreSQL 风格,MySQL 类似):
CREATE OR REPLACE FUNCTION log_package_status_change()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO package_status_log (package_id, status, created_at, event_type)
VALUES (NEW.id, NEW.status, NOW(), 'initial');
ELSIF TG_OP = 'UPDATE' AND OLD.status != NEW.status THEN
INSERT INTO package_status_log (package_id, status, created_at, event_type, prev_status)
VALUES (NEW.id, NEW.status, NOW(), 'status_change', OLD.status);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
- INSERT 分支不判断
OLD.status(不存在),直接记初始状态 - UPDATE 分支必须显式比较
OLD.status != NEW.status,否则每次更新(哪怕没改状态)都多一条日志 - MySQL 中注意:不能在触发器里对同一张表做 DML(比如在
packages的 UPDATE 触发器里再 UPDATEpackages),会报错ERROR 1442
状态字段值不规范会导致日志不可查
很多系统用字符串存状态:'shipped'、'Shipped'、'SHIPPED '(带空格)、'in_transit'、'in transit'……触发器照单全收,但下游查轨迹时得写一堆 OR status LIKE '%ship%',性能差还易出错。
- 强制在触发器里做标准化:用
TRIM(UPPER(NEW.status))统一格式 - 更稳妥的是在触发器开头校验枚举值:
IF NEW.status NOT IN ('created', 'packed', 'shipped', 'delivered', 'returned') THEN RAISE EXCEPTION 'invalid status'; END IF; - 别依赖应用层传来的
updated_by字段——触发器拿不到 HTTP header 或用户 session,得靠数据库级上下文(如 PostgreSQL 的current_user,MySQL 的USER()),或提前在事务里写入操作人到临时字段
高并发下触发器可能拖慢主流程
每笔状态更新都同步写日志表,遇到双十一大促,packages 表 QPS 上万时,package_status_log 的写入会成为瓶颈,甚至引发锁等待或超时。
- 日志表必须有合理索引:至少
(package_id, created_at)联合索引,避免SELECT * FROM package_status_log WHERE package_id = ? ORDER BY created_at DESC LIMIT 10全表扫 - 不要在触发器里调用外部 API 或发消息——这是反模式,失败会导致主事务回滚
- 如果实时性要求不高(比如轨迹只需 5 秒内可见),可考虑用异步方案替代:触发器只往一张极轻量的
log_queue表插 ID,另起一个定时任务批量消费并写入正式日志表
最常被忽略的一点:触发器不会自动继承到新分区表或新租户 schema,上线后新增分片或 SaaS 多租户扩展时,必须手动补上对应触发器,否则新数据就没了轨迹。











