第四节 实战:订单状态变更自动记录日志

目标:设计并实现一个健壮的触发器,用于自动、详细地记录 orders 表中状态字段(status)的每一次变更,为业务审计和问题追溯提供可靠依据。

在许多业务系统(如电商、物流、工单处理)中,核心对象的状态流转是关键业务逻辑。记录每一次状态变更不仅是合规性要求,也是排查问题、分析业务流程的重要手段。本节将通过一个完整的实战案例,展示如何利用触发器实现这一功能。


场景分析

我们需要一个 order_status_audit 表,专门用来存放 orders 表的状态变更历史。当一个订单的状态从 pending 变为 paid,再到 shipped 时,每一条变更都应该被记录下来。

审计日志应包含以下关键信息:

  • 关联的订单ID (order_id)
  • 旧状态 (old_status)
  • 新状态 (new_status)
  • 变更发生的时间 (changed_at)
  • 执行变更的操作员 (changed_by)

🛠️ 第一步:创建数据表结构

首先,我们需要 orders 表和用于审计的 order_status_audit 表。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
-- 订单表
CREATE TABLE orders (
    order_id SERIAL PRIMARY KEY,
    customer_name TEXT NOT NULL,
    total_amount NUMERIC(10, 2) NOT NULL,
    status TEXT NOT NULL DEFAULT 'pending', -- 订单状态
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- 订单状态审计日志表
CREATE TABLE order_status_audit (
    audit_id SERIAL PRIMARY KEY,
    order_id INT NOT NULL,
    old_status TEXT,
    new_status TEXT NOT NULL,
    changed_by NAME DEFAULT CURRENT_USER, -- 记录执行操作的数据库用户
    changed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT fk_order
        FOREIGN KEY(order_id) 
        REFERENCES orders(order_id)
        ON DELETE CASCADE -- 如果订单被删除,关联的日志也一并删除
);

-- 为审计表的 order_id 创建索引,加速查询
CREATE INDEX idx_order_status_audit_order_id ON order_status_audit(order_id);

✍️ 第二步:编写触发器函数

触发器函数是核心逻辑所在。它需要在 orders 表发生 INSERTUPDATE 时被调用,并判断 status 字段是否发生变化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
CREATE OR REPLACE FUNCTION log_order_status_change()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
    -- 场景1: 新插入订单 (TG_OP = 'INSERT')
    -- 直接记录初始状态
    IF (TG_OP = 'INSERT') THEN
        INSERT INTO order_status_audit (order_id, old_status, new_status)
        VALUES (NEW.order_id, NULL, NEW.status);

    -- 场景2: 更新订单 (TG_OP = 'UPDATE')
    -- 仅在 status 字段实际发生变化时记录
    ELSIF (TG_OP = 'UPDATE' AND NEW.status IS DISTINCT FROM OLD.status) THEN
        INSERT INTO order_status_audit (order_id, old_status, new_status)
        VALUES (NEW.order_id, OLD.status, NEW.status);
    END IF;

    -- 更新主表的 updated_at 时间戳
    NEW.updated_at := NOW();

    -- 返回 NEW 以便 DML 操作继续执行
    RETURN NEW;
END;
$$;

代码解析

  • TG_OP 是一个特殊变量,值为 INSERT, UPDATE, DELETE 等,表示触发事件的类型。
  • NEW.status IS DISTINCT FROM OLD.status 是一种严谨的比较方式,可以正确处理 NULL 值。
  • 我们顺便在触发器中更新了 updated_at 字段,这是触发器的常见用法之一。

🔗 第三步:创建并绑定触发器

现在,我们将这个函数绑定到 orders 表。我们希望在每次插入或更新行之前(BEFORE)执行此逻辑。

1
2
3
4
CREATE TRIGGER trigger_orders_status_change
BEFORE INSERT OR UPDATE ON orders
FOR EACH ROW
EXECUTE FUNCTION log_order_status_change();

为什么用 BEFORE

  • 可以在数据写入磁盘前,统一修改 NEW 记录,如更新 updated_at 字段。
  • 如果触发器逻辑中发现错误,可以通过 RAISE EXCEPTION 来阻止不合法的 INSERTUPDATE 操作。

✅ 第四步:验证触发器功能

让我们模拟一次完整的订单生命周期,来验证触发器是否按预期工作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
-- 1. 创建一个新订单
INSERT INTO orders (customer_name, total_amount, status)
VALUES ('张三', 199.99, 'pending');
-- 此时应在 audit 表插入一条 (NULL -> 'pending') 的记录

-- 2. 模拟支付,更新状态
UPDATE orders SET status = 'paid' WHERE customer_name = '张三';
-- 此时应在 audit 表插入一条 ('pending' -> 'paid') 的记录

-- 3. 更新订单的其他信息,但不改变状态
UPDATE orders SET total_amount = 205.99 WHERE customer_name = '张三';
-- 此时 audit 表不应有新记录,但 orders.updated_at 应被更新

-- 4. 模拟发货,更新状态
UPDATE orders SET status = 'shipped' WHERE customer_name = '张三';
-- 此时应在 audit 表插入一条 ('paid' -> 'shipped') 的记录

查询验证结果:

1
2
3
4
5
6
7
8
-- 查看订单最终状态
SELECT order_id, status, updated_at FROM orders WHERE customer_name = '张三';

-- 查看完整的状态变更历史
SELECT order_id, old_status, new_status, changed_at
FROM order_status_audit
WHERE order_id = (SELECT order_id FROM orders WHERE customer_name = '张三')
ORDER BY changed_at;

预期输出: order_status_audit 表中应有三条按时间排序的记录,清晰地展示了订单从创建到支付再到发货的完整状态流转路径。


📌 小结

通过本实战,我们成功构建了一个自动化的订单状态审计系统。这个模式不仅限于订单,可以轻松扩展到任何需要追踪字段变更历史的场景,例如:

  • 记录用户权限的变更。
  • 审计产品价格的调整。
  • 追踪敏感配置的修改。

触发器是实现此类自动化、强制性数据策略的强大工具,但务必注意保持其逻辑的简洁和高效,以避免对数据库性能造成不必要的影响。