¥
立即购买

数据库触发器专业编写助手

7 浏览
1 试用
0 购买
Dec 3, 2025更新

本提示词专为数据库管理员和开发人员设计,能够根据具体的数据库类型和业务需求,生成专业、准确的触发器代码。通过系统化的需求分析和代码生成流程,确保触发器逻辑严谨、性能优化,并提供详细的代码注释和使用说明,帮助用户快速实现数据库层面的业务规则自动化,提升数据一致性和系统可靠性。

包含3个提示词变量:
变量 描述 示例
数据库类型 目标数据库管理系统类型 mysql
触发动作 触发器的触发时机和操作类型 update
业务场景 触发器的具体业务应用场景描述 用户注册后自动初始化账户信息
查看全部

触发器设计概述

  • 业务场景说明
    • 当订单状态由 PAID 更新为 SHIPPED 时,需要自动扣减对应商品库存(products.stock),并记录一条库存台账明细到 inventory_ledger(sku_id, delta, order_id, ts)。
    • 扣减数量按 order_items 中的同一订单维度汇总到 sku 级别,禁止库存变为负数。
    • 以 (order_id, sku_id) 为幂等键:同一个订单对同一个 SKU 的扣减只能发生一次;重复执行不得重复扣减。
  • 触发条件分析
    • 仅在 orders.status 从 PAID 变更为 SHIPPED 时执行。
    • 若订单再次被错误地回退并重新改回 SHIPPED,需做到不重复扣减(幂等)。
  • 预期效果描述
    • 在同一事务内(更新订单状态的事务),对相关 products 行做行级锁以避免并发造成超卖。
    • 扣减前检查:若存在未处理 SKU 的库存不足或 SKU 不存在则整体失败并回滚订单状态更新。
    • 成功扣减后一次性插入台账,且通过左连接过滤和唯一索引建议,实现幂等。

触发器代码

-- 假设表结构:
-- orders(id PK, status)
-- order_items(order_id, sku_id, quantity)
-- products(sku_id PK, stock)
-- inventory_ledger(sku_id, delta, order_id, ts)

DELIMITER //

DROP TRIGGER IF EXISTS trg_orders_after_update_shipped//
CREATE TRIGGER trg_orders_after_update_shipped
AFTER UPDATE ON orders
FOR EACH ROW
BEGIN
  -- 仅当订单状态由 PAID -> SHIPPED 时执行
  IF (OLD.status = 'PAID' AND NEW.status = 'SHIPPED') THEN

    DECLARE v_missing INT DEFAULT 0;      -- 未处理 SKU 中,有无在 products 不存在的
    DECLARE v_insufficient INT DEFAULT 0; -- 未处理 SKU 中,有无库存不足的

    -- 步骤1:锁定本次需处理的 products 行,降低并发超卖风险(按主键顺序加锁可减小死锁概率)
    SELECT p.sku_id
    FROM products p
    JOIN (
      SELECT sku_id, SUM(quantity) AS qty
      FROM order_items
      WHERE order_id = NEW.id
      GROUP BY sku_id
      HAVING SUM(quantity) > 0
    ) oi ON oi.sku_id = p.sku_id
    LEFT JOIN inventory_ledger l
      ON l.order_id = NEW.id AND l.sku_id = oi.sku_id
    WHERE l.order_id IS NULL
    ORDER BY p.sku_id
    FOR UPDATE;

    -- 步骤2:检查:未处理的 SKU 是否在 products 中缺失
    SELECT COUNT(*) INTO v_missing
    FROM (
      SELECT oi.sku_id
      FROM (
        SELECT sku_id, SUM(quantity) AS qty
        FROM order_items
        WHERE order_id = NEW.id
        GROUP BY sku_id
        HAVING SUM(quantity) > 0
      ) oi
      LEFT JOIN inventory_ledger l
        ON l.order_id = NEW.id AND l.sku_id = oi.sku_id
      LEFT JOIN products p
        ON p.sku_id = oi.sku_id
      WHERE l.order_id IS NULL
        AND p.sku_id IS NULL
    ) AS x;

    IF v_missing > 0 THEN
      SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'Stock deduction failed: one or more SKUs in the order do not exist (unprocessed).';
    END IF;

    -- 步骤3:检查:未处理的 SKU 是否库存不足
    SELECT COUNT(*) INTO v_insufficient
    FROM (
      SELECT oi.sku_id
      FROM (
        SELECT sku_id, SUM(quantity) AS qty
        FROM order_items
        WHERE order_id = NEW.id
        GROUP BY sku_id
        HAVING SUM(quantity) > 0
      ) oi
      JOIN products p
        ON p.sku_id = oi.sku_id
      LEFT JOIN inventory_ledger l
        ON l.order_id = NEW.id AND l.sku_id = oi.sku_id
      WHERE l.order_id IS NULL
        AND p.stock < oi.qty
    ) AS y;

    IF v_insufficient > 0 THEN
      SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'Stock deduction failed: insufficient stock for one or more SKUs (unprocessed).';
    END IF;

    -- 步骤4:扣减库存(仅对尚未入台账的 SKU 执行,保证幂等)
    UPDATE products p
    JOIN (
      SELECT sku_id, SUM(quantity) AS qty
      FROM order_items
      WHERE order_id = NEW.id
      GROUP BY sku_id
      HAVING SUM(quantity) > 0
    ) oi
      ON p.sku_id = oi.sku_id
    LEFT JOIN inventory_ledger l
      ON l.order_id = NEW.id AND l.sku_id = oi.sku_id
    SET p.stock = p.stock - oi.qty
    WHERE l.order_id IS NULL;

    -- 步骤5:写入库存台账(幂等:仅对尚未入台账的 SKU 写入;建议配合唯一索引)
    INSERT INTO inventory_ledger (sku_id, delta, order_id, ts)
    SELECT oi.sku_id, -oi.qty, NEW.id, NOW()
    FROM (
      SELECT sku_id, SUM(quantity) AS qty
      FROM order_items
      WHERE order_id = NEW.id
      GROUP BY sku_id
      HAVING SUM(quantity) > 0
    ) oi
    LEFT JOIN inventory_ledger l
      ON l.order_id = NEW.id AND l.sku_id = oi.sku_id
    WHERE l.order_id IS NULL;

  END IF;
END//
DELIMITER ;

代码说明

  • 关键语法解释

    • AFTER UPDATE 行级触发器:对被更新的每一行订单执行一次。
    • SELECT ... FOR UPDATE:在 InnoDB 下对匹配的 products 行加排他锁,防并发超卖。
    • SIGNAL SQLSTATE '45000':在检查失败时抛出业务错误,回滚本次订单状态更新以及触发器内操作。
    • LEFT JOIN inventory_ledger 并过滤 l.order_id IS NULL:仅处理尚未入台账的 SKU,保证幂等。
    • 聚合 SUM(quantity) 并 GROUP BY sku_id:将 order_items 汇总到 SKU 粒度进行一次性扣减和台账写入。
  • 业务逻辑说明

    1. 仅当状态从 PAID 改为 SHIPPED 才触发,避免非目标状态的冗余执行。
    2. 对尚未入台账的 SKU 加锁并做两类检查:
      • SKU 在 products 中不存在(配置/数据问题)
      • 库存不足(防止负库存)
    3. 检查通过后,先批量扣减库存,再批量写入台账,均仅针对尚未处理的 SKU,以确保重复执行无副作用。
    4. 一旦任何检查失败,触发器抛错,整笔事务回滚,库存不变、台账不写。
  • 注意事项提醒

    • 假设 products.sku_id 为主键;若使用其他主键,请相应调整连接字段。
    • 假设 order_items.quantity 为正整数;如果允许退货或负数,需在 HAVING 和逻辑中相应调整。
    • 触发器运行在更新 orders 的同一事务中,请保持 orders、order_items、products、inventory_ledger 都使用 InnoDB。
    • 请勿在业务中大批量 UPDATE 订单为 SHIPPED(例如一次性更新大量订单),以免触发器高负载;应逐单处理或分批。

性能建议

  • 索引优化建议
    • products(sku_id) 设为主键或唯一索引(必需)。
    • order_items 建议建立复合索引 (order_id, sku_id) 覆盖本触发器查询。
    • inventory_ledger 建议建立唯一索引 uniq_ledger_order_sku(order_id, sku_id),保障幂等与并发写入安全;并增加普通索引 (sku_id)、(order_id) 便于审计查询。
    • orders(id) 为主键,status 上无需额外索引(触发器不依赖)。
  • 并发与行级锁控制
    • SELECT ... FOR UPDATE 基于 products.sku_id 加锁,只锁定当前订单涉及的 SKU 行,配合上述索引可最大限度缩小锁粒度。
    • 通过 ORDER BY p.sku_id 一致化加锁顺序,降低并发订单之间的死锁概率。
  • 监控指标说明
    • 监控 inventory_ledger 插入量与失败率(SQLSTATE 45000)。
    • 监控 InnoDB 行锁等待与死锁日志(SHOW ENGINE INNODB STATUS / performance_schema)。
    • 关注 products.stock 的最小值分布,预警低库存 SKU。
  • 常见问题预防
    • 防止数据漂移:强烈建议对 inventory_ledger(order_id, sku_id) 建唯一索引,避免外部程序重复写入。
    • 避免大事务:配送批量处理时分批 commit,缩短锁持有时间。
    • 保证数据一致性:对 order_items.sku_id 建外键指向 products.sku_id(可选,视业务决定)。

测试用例

  • 基本功能测试场景
    1. 正常扣减
      • 初始:products(sku_id=1001, stock=10);order_items 有两行 (order_id=1, sku_id=1001, quantity=3) 和 (order_id=1, sku_id=1001, quantity=2);orders(id=1, status='PAID')
      • 操作:UPDATE orders SET status='SHIPPED' WHERE id=1;
      • 期望:products.stock -> 5;inventory_ledger 插入一行 (1001, -5, 1, ts)。
    2. 幂等
      • 对同一订单再次执行:UPDATE orders SET status='SHIPPED' WHERE id=1;
      • 期望:无库存变化,无新增台账记录。
  • 边界条件测试建议
    1. 库存不足
      • 初始:products.stock=4;order_items 汇总需扣减为5
      • 操作:将订单状态改为 SHIPPED
      • 期望:触发器报错 SQLSTATE 45000,订单状态更新失败,库存不变,台账不写。
    2. SKU 缺失
      • order_items 中包含在 products 不存在的 sku_id,且该 (order_id, sku_id) 尚未入台账
      • 期望:触发器报错 SQLSTATE 45000,回滚。
    3. 并发同 SKU 扣减
      • 两个不同订单同时将相同 SKU 发货,合并扣减量超过库存
      • 期望:基于行锁与条件检查,只有一个事务成功,另一个因库存不足回滚。
    4. 部分已处理
      • 手工插入一条 inventory_ledger(order_id, sku_id) 使其被视为已处理
      • 再次发货
      • 期望:仅对未入台账的 SKU 扣减并写入台账,已入台账的 SKU 不重复处理。

如需,我可以提供配套索引及约束的 DDL 建议(强烈推荐):

  • CREATE UNIQUE INDEX uniq_ledger_order_sku ON inventory_ledger(order_id, sku_id);
  • CREATE INDEX idx_order_items_order_sku ON order_items(order_id, sku_id);
  • 确保 products.sku_id 为 PRIMARY KEY 或 UNIQUE。

触发器设计概述

  • 业务场景说明
    • 财务总账入账前(before insert)进行严谨校验与自动补全:
      • 校验:单行借贷规范(仅一边为正),会计期是否开启,避免重复入账(签名去重),并在提交时保证同一 trace_id 的分录借贷平衡。
      • 自动补全:缺省补全 trace_id、posted_at;按汇率 fx_rate 折算 base_amount,并“锁定”所用汇率版本。
      • 审计:生成稳定的 sha256 签名,作为幂等去重凭据,保障可追溯性与一致性。
  • 触发条件分析
    • BEFORE INSERT 行级触发器:
      • 保证单行借贷合法性
      • 检查会计期开启
      • 自动补全 trace_id、posted_at
      • 计算与锁定汇率(查表并 FOR SHARE 锁定版本),计算 base_amount
      • 基于关键字段生成 sha256 签名
    • DEFERRABLE 约束触发器(AFTER,INITIALLY DEFERRED):
      • 在事务提交时,保证同一 trace_id 下的分录合计借贷平衡
  • 预期效果描述
    • 插入 gl_entry 时,若字段缺失将被自动补全;若不符合会计期或汇率不可用、或借贷不合法、或重复入账,将立即阻断。
    • 提交事务时,如同一 trace_id 的分录借贷不平衡,将阻断提交,确保总账一致性。

触发器代码

-- 要求:PostgreSQL 12+;需要扩展 pgcrypto 用于哈希与 UUID
CREATE EXTENSION IF NOT EXISTS pgcrypto;

-- 可选应用级配置项(可通过 ALTER DATABASE/ROLE SET 设置持久化)
-- app.base_currency:基础币种(默认 USD)
-- app.fx_table:汇率表名(默认 fx_rates)
-- app.period_table:会计期间表名(默认 accounting_periods)

-- 示例期望表结构(用于文档说明,不会被执行)
-- gl_entry(
--   id bigserial pk,
--   account_id bigint not null,
--   currency_code text not null,
--   debit_amount numeric not null default 0,
--   credit_amount numeric not null default 0,
--   base_amount numeric not null,
--   fx_rate numeric,
--   fx_rate_version integer,
--   base_currency_code text default current_setting('app.base_currency', true),
--   posted_at timestamptz,
--   period_id integer,
--   trace_id uuid,
--   description text,
--   source_system text,
--   signature text,        -- 建议为 text 存放十六进制哈希
--   created_at timestamptz default now()
-- );

-- 辅助函数:获取开启的会计期间ID(动态SQL避免编译期对象依赖)
CREATE OR REPLACE FUNCTION fn_get_open_period_id(p_ts timestamptz)
RETURNS integer
LANGUAGE plpgsql
STABLE
SET search_path = pg_catalog, public
AS $$
DECLARE
  v_tbl text := COALESCE(current_setting('app.period_table', true), 'accounting_periods');
  v_sql text;
  v_id integer;
BEGIN
  IF to_regclass(v_tbl) IS NULL THEN
    RAISE EXCEPTION '会计期间表 "%" 不存在,请通过 app.period_table 指定正确表名', v_tbl;
  END IF;

  v_sql := format(
    'SELECT id FROM %I
      WHERE start_at <= $1 AND $1 < end_at
        AND is_open = true
      ORDER BY id DESC
      LIMIT 1
      FOR SHARE',
    v_tbl
  );

  EXECUTE v_sql INTO v_id USING p_ts;

  IF v_id IS NULL THEN
    RAISE EXCEPTION '未找到开启的会计期间覆盖时间点 %', p_ts;
  END IF;

  RETURN v_id;
END;
$$;

-- 辅助函数:查询并锁定汇率与版本
-- 期望 fx_rates 表具备字段:from_currency, to_currency, valid_from, valid_to, rate, version
CREATE OR REPLACE FUNCTION fn_get_fx(p_from text, p_to text, p_at timestamptz)
RETURNS TABLE(rate numeric, version integer)
LANGUAGE plpgsql
STABLE
SET search_path = pg_catalog, public
AS $$
DECLARE
  v_tbl text := COALESCE(current_setting('app.fx_table', true), 'fx_rates');
  v_sql text;
BEGIN
  IF p_from = p_to THEN
    rate := 1;
    version := 0;
    RETURN NEXT;
    RETURN;
  END IF;

  IF to_regclass(v_tbl) IS NULL THEN
    RAISE EXCEPTION '汇率表 "%" 不存在,请通过 app.fx_table 指定正确表名', v_tbl;
  END IF;

  v_sql := format($f$
    SELECT r.rate, r.version
      FROM %I r
     WHERE r.from_currency = $1
       AND r.to_currency   = $2
       AND r.valid_from   <= $3
       AND (r.valid_to IS NULL OR $3 < r.valid_to)
     ORDER BY r.valid_from DESC, r.version DESC
     LIMIT 1
     FOR SHARE
  $f$, v_tbl);

  EXECUTE v_sql INTO rate, version USING p_from, p_to, p_at;

  IF rate IS NULL THEN
    RAISE EXCEPTION '未找到汇率:% -> % 在 %', p_from, p_to, p_at;
  END IF;

  RETURN NEXT;
END;
$$;

-- BEFORE INSERT 触发器函数:单行校验、自动补全、汇率折算、签名生成
CREATE OR REPLACE FUNCTION trg_bi_gl_entry()
RETURNS trigger
LANGUAGE plpgsql
VOLATILE
SET search_path = pg_catalog, public
AS $$
DECLARE
  v_base_ccy text := COALESCE(current_setting('app.base_currency', true), 'USD');
  v_rate numeric;
  v_ver integer;
  v_amt numeric;
BEGIN
  -- 1) 借贷规范:仅一边为正,且不可同时为零或同时为正
  IF COALESCE(NEW.debit_amount, 0) > 0 AND COALESCE(NEW.credit_amount, 0) > 0 THEN
    RAISE EXCEPTION '借贷同时为正:debit=%, credit=%', NEW.debit_amount, NEW.credit_amount;
  END IF;
  IF COALESCE(NEW.debit_amount, 0) = 0 AND COALESCE(NEW.credit_amount, 0) = 0 THEN
    RAISE EXCEPTION '借贷至少一边为正(另一边必须为0)';
  END IF;
  IF NEW.debit_amount < 0 OR NEW.credit_amount < 0 THEN
    RAISE EXCEPTION '借贷金额不可为负';
  END IF;

  -- 2) 自动补全 posted_at / trace_id
  IF NEW.posted_at IS NULL THEN
    NEW.posted_at := clock_timestamp();
  END IF;
  IF NEW.trace_id IS NULL THEN
    NEW.trace_id := gen_random_uuid();
  END IF;

  -- 3) 会计期开启校验(并回填 period_id,如存在该列)
  --    若表结构中不存在 period_id,可忽略赋值,但校验仍会执行
  PERFORM 1;
  BEGIN
    NEW.period_id := fn_get_open_period_id(NEW.posted_at);
  EXCEPTION
    WHEN undefined_column THEN
      -- period_id 列不存在,仅做校验不回填
      PERFORM fn_get_open_period_id(NEW.posted_at);
  END;

  -- 4) 汇率折算与版本锁定
  IF NEW.currency_code IS NULL THEN
    RAISE EXCEPTION 'currency_code 不能为空';
  END IF;

  IF NEW.currency_code = v_base_ccy THEN
    -- 基础币种:汇率视为1,版本可置0
    NEW.fx_rate := 1;
    IF NEW.fx_rate_version IS NULL THEN
      NEW.fx_rate_version := 0;
    END IF;
  ELSE
    -- 若外部已经传入 fx_rate 与版本,可复用并不重复查询;否则查询并锁定
    IF NEW.fx_rate IS NULL OR NEW.fx_rate <= 0 OR NEW.fx_rate_version IS NULL THEN
      SELECT rate, version INTO v_rate, v_ver
        FROM fn_get_fx(NEW.currency_code, v_base_ccy, NEW.posted_at);
      IF NEW.fx_rate IS NULL OR NEW.fx_rate <= 0 THEN
        NEW.fx_rate := v_rate;
      END IF;
      IF NEW.fx_rate_version IS NULL THEN
        NEW.fx_rate_version := v_ver;
      END IF;
    END IF;

    IF NEW.fx_rate IS NULL OR NEW.fx_rate <= 0 THEN
      RAISE EXCEPTION '无效汇率:% -> % at %', NEW.currency_code, v_base_ccy, NEW.posted_at;
    END IF;
  END IF;

  -- 5) 计算 base_amount(以正额计量,方向由借/贷体现)
  v_amt := CASE
             WHEN COALESCE(NEW.debit_amount, 0) > 0 THEN NEW.debit_amount
             ELSE NEW.credit_amount
           END;
  NEW.base_amount := v_amt * NEW.fx_rate;

  -- 6) 生成幂等签名(sha256,十六进制),作为去重依据
  --    签名构成应涵盖:trace_id、账户、币种、借贷金额、base_amount、基础币种、汇率/版本、posted_at(秒级)
  --    注意:签名字段 signature 建议为 text
  NEW.signature := encode(
    digest(
      concat_ws('|',
        COALESCE(NEW.trace_id::text, ''),
        COALESCE(NEW.account_id::text, ''),
        COALESCE(NEW.currency_code, ''),
        to_char(COALESCE(NEW.debit_amount, 0), 'FM9999999999990.999999'),
        to_char(COALESCE(NEW.credit_amount, 0), 'FM9999999999990.999999'),
        to_char(COALESCE(NEW.base_amount, 0), 'FM9999999999990.999999'),
        v_base_ccy,
        to_char(COALESCE(NEW.fx_rate, 0), 'FM9999999999990.999999'),
        COALESCE(NEW.fx_rate_version::text, ''),
        to_char(date_trunc('second', NEW.posted_at), 'YYYY-MM-DD"T"HH24:MI:SSOF')
      )::bytea,
      'sha256'
    ),
    'hex'
  );

  RETURN NEW;
END;
$$;

-- BEFORE INSERT 触发器
DROP TRIGGER IF EXISTS bi_gl_entry ON gl_entry;
CREATE TRIGGER bi_gl_entry
BEFORE INSERT ON gl_entry
FOR EACH ROW
EXECUTE FUNCTION trg_bi_gl_entry();

-- 事务提交时的借贷平衡校验(按 trace_id 分组),使用约束触发器,初始化为延迟到提交时检查
CREATE OR REPLACE FUNCTION trg_balance_gl_entry()
RETURNS trigger
LANGUAGE plpgsql
VOLATILE
SET search_path = pg_catalog, public
AS $$
DECLARE
  v_trace uuid;
  v_debit numeric;
  v_credit numeric;
BEGIN
  v_trace := CASE
               WHEN TG_OP IN ('INSERT', 'UPDATE') THEN NEW.trace_id
               ELSE OLD.trace_id
             END;

  -- trace_id 为空则不校验(允许独立单条分录场景)
  IF v_trace IS NULL THEN
    RETURN NULL;
  END IF;

  SELECT COALESCE(SUM(debit_amount), 0), COALESCE(SUM(credit_amount), 0)
    INTO v_debit, v_credit
    FROM gl_entry
   WHERE trace_id = v_trace;

  IF v_debit <> v_credit THEN
    RAISE EXCEPTION '分录不平衡(trace_id=%):debit=%,credit=%', v_trace, v_debit, v_credit;
  END IF;

  RETURN NULL;
END;
$$;

DROP TRIGGER IF EXISTS cs_gl_entry_balance ON gl_entry;
CREATE CONSTRAINT TRIGGER cs_gl_entry_balance
AFTER INSERT OR UPDATE OR DELETE ON gl_entry
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE FUNCTION trg_balance_gl_entry();

-- 去重建议:对签名建立唯一索引(需确保 signature 为 text 且非 NULL)
-- 注意:CONCURRENTLY 不能在事务中执行,如在迁移脚本中请拆到单独事务
-- CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS ux_gl_entry_signature ON gl_entry(signature);

-- 性能建议:支撑触发器校验与查询的必要索引(如不存在)
-- 建议的非唯一索引(根据实际表结构调整)
-- CREATE INDEX IF NOT EXISTS ix_gl_entry_trace_id ON gl_entry(trace_id);

代码说明

  • 关键语法解释

    • 使用 CREATE EXTENSION pgcrypto 提供 digest 与 gen_random_uuid。
    • 使用 SET search_path = pg_catalog, public 固定搜索路径,规避 search_path 污染风险。
    • fn_get_open_period_id / fn_get_fx 使用动态 SQL,依赖 app.period_table / app.fx_table 配置或默认表名;通过 to_regclass 动态检测表是否存在,避免函数创建期失败。
    • BEFORE INSERT 触发器:
      • 校验借贷:仅允许一侧为正且非负;两侧同时为0或同时为正都会抛错。
      • 自动补全:posted_at 缺省为当前时间;trace_id 缺省为 gen_random_uuid()。
      • 会计期开启:调用 fn_get_open_period_id 进行校验,并尝试赋值 NEW.period_id(若该列存在)。
      • 汇率获取与锁定:若非基础币种,优先使用传入的 fx_rate/fx_rate_version,否则从 fx 表选取有效期内最新记录,同时 FOR SHARE 以锁定使用版本。
      • base_amount 计算:以正额计量(方向由借/贷体现),避免产生符号歧义。
      • 签名:将关键字段按字符串拼接,digest 后以十六进制存储到 signature(text)。
    • 约束触发器(DEFERRABLE INITIALLY DEFERRED):在事务提交时,按 trace_id 汇总借贷金额,若不相等则阻断提交,保证成组分录平衡。
  • 业务逻辑说明

    • 单行规范保证:每行分录必须要么借要么贷,不得两者兼有或皆无。
    • 会计期校验:确保入账时间点在开启会计期间内,防止跨期/关账期间入账。
    • 汇率与版本:确保计算 base_amount 的汇率有明确版本来源,且在插入时“锁定”所用版本,便于审计追溯。
    • 签名去重:签名覆盖 trace_id、账户、币种、金额、汇率、版本、posted_at 等关键信息,防止重复入账。
    • 成组平衡校验:对同一 trace_id 的整组分录在事务提交时必须借贷相等,满足会计平衡。
  • 注意事项提醒

    • 请确保 gl_entry.signature 字段类型为 text;如为 bytea,请将签名赋值改为直接 digest(...),或 encode(...)->text 改为 bytea。
    • 请配置 app.base_currency(如 USD/CNY 等)并保证 gl_entry.currency_code 存在有效值。
    • 请准备汇率表与会计期间表并通过 app.fx_table、app.period_table 指向正确对象,且列名与示例一致或相应调整动态 SQL。
    • 唯一索引 ux_gl_entry_signature 未在上述脚本中强制创建为 CONCURRENTLY 以兼顾部署安全性,请在变更窗口单独执行。

性能建议

  • 索引优化建议
    • gl_entry(trace_id):支撑提交时的分组汇总校验。
    • gl_entry(signature) 唯一索引:强力去重,建议 NOT NULL 并保持短列(十六进制64字符)。
    • 会计期间表:accounting_periods(start_at, end_at, is_open) 或基于有效期的覆盖索引。
    • 汇率表:fx_rates(from_currency, to_currency, valid_from DESC) 覆盖有效期查询;可加部分索引包含 valid_to。
  • 监控指标说明
    • 插入失败率与异常类型分布:用于监控会计期关闭、汇率缺失、重复入账等异常。
    • 事务提交时的触发器延迟:大批量同一 trace_id 的提交时,注意提交阶段的聚合开销。
    • 索引命中率与膨胀:trace_id、signature 索引的大小与碎片,定期 REINDEX/维护。
  • 常见问题预防
    • 大批量批处理:建议批次内复用同一 trace_id,且在单事务内提交;避免跨事务导致组内不平衡。
    • 汇率切换:确保 fx_rates 版本与有效期维护规范,避免边界时间点无匹配;必要时业务侧显式传入 fx_rate_version。
    • 时区一致性:posted_at 建议使用 timestamptz,统一以 UTC 存储,避免时区导致的期间匹配偏差。

测试用例

  • 基本功能测试场景

    1. 自动补全
      • INSERT 仅给出 debit_amount/currency_code/account_id,缺省 posted_at/trace_id,期望自动补全且 period_id 正确。
    2. 借贷规范
      • 同时赋正的 debit_amount 与 credit_amount,期望报错。
      • 二者皆为0,期望报错。
    3. 会计期校验
      • posted_at 落在关闭期间,期望报错。
      • posted_at 落在开启期间,成功。
    4. 汇率与 base_amount
      • 基础币种(等于 app.base_currency),期望 fx_rate=1、version=0、base_amount=原币金额。
      • 外币且不传 fx_rate,存在有效期内汇率,期望自动填充 fx_rate/fx_rate_version 且 base_amount=金额*汇率。
      • 外币且无有效汇率,期望报错。
    5. 签名去重
      • 两次插入完全相同数据(含 trace_id),第二次因唯一索引(创建后)报重复键错误。
    6. 组平衡校验
      • 同一 trace_id 下插入一借一贷金额相等的两条,事务提交成功。
      • 只插入借方不插入贷方,提交时触发器报不平衡错误。
  • 边界条件测试建议

    • 极小/极大金额与多小数位,确认签名格式化无歧义,base_amount 精度与四舍五入符合财务规则。
    • 在有效期边界(valid_from/valid_to)时刻插入,验证汇率选择正确。
    • 高并发插入同一 trace_id 的事务,验证提交阶段平衡校验与索引支撑性能。
    • 基础币种与外币相同/不同切换,验证 fx_rate 与 version 逻辑。
    • signature 列空值与唯一索引行为(建议 signature 设为 NOT NULL 并默认由触发器生成)。

说明:以上代码与建议基于 PostgreSQL 12+ 并假定 gl_entry 具有文中提及字段类型。若实际表结构存在差异,请相应调整触发器函数中的列名与签名构成字段,并确保 app.fx_table、app.period_table 指向的对象及列名与查询一致。为避免运行时语义变化,建议在测试环境回归后再上线生产。

触发器设计概述

  • 业务场景说明
    • 当从 dbo.customer_address 删除客户地址记录时:
      • 将已删除行的关键信息归档到 dbo.address_archive(至少包含原主键 address_id、删除时间 ts、删除人 deleted_by=SESSION_CONTEXT('user'))。
      • 写入审计日志 dbo.audit_log(table, pk, op, ts) 记录删除动作。
      • 写入出站事件表 dbo.outbox(topic='address.deleted', payload, ts),供下游 ERP 清理流程消费。
  • 触发条件分析
    • 触发时机:AFTER DELETE(支持多行删除,使用 deleted 伪表进行集合化处理)。
    • 原子性:触发器内所有写入与外层 DELETE 属于同一事务,任一失败将回滚,保证审计/归档/出站与删除一致。
  • 预期效果描述
    • 每条被删除的地址都会生成一条归档记录(含原主键、删除时间、删除人)。
    • 审计日志记录被删除表名、主键、操作类型、时间。
    • Outbox 生成主题为 address.deleted 的消息,payload 为包含主键信息的 JSON,供异步下游消费。

触发器代码

-- 先确保会话设置(对象创建时所需)
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO

-- 触发器:客户地址删除后审计与归档,并写入 outbox
-- 兼容 SQL Server 2016+(使用 FOR JSON 生成 JSON)
CREATE OR ALTER TRIGGER dbo.trg_customer_address_after_delete
ON dbo.customer_address
AFTER DELETE
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;

    -- 无行删除时快速返回(包含安全防御)
    IF NOT EXISTS (SELECT 1 FROM deleted) RETURN;

    BEGIN TRY
        DECLARE @now datetime2(3) = SYSUTCDATETIME();
        DECLARE @user nvarchar(128) = TRY_CONVERT(nvarchar(128), SESSION_CONTEXT(N'user'));

        -- 若应用层未设置 SESSION_CONTEXT('user'),回退为原始登录名
        IF @user IS NULL SET @user = ORIGINAL_LOGIN();

        -------------------------------------------------------------------------
        -- 1) 归档:插入 address_archive(最小需求:原主键、时间戳、删除人)
        --    注意:假定 address_archive 至少包含以下列:
        --    (address_id <与主表相同类型>, ts datetime2(3), deleted_by nvarchar(128))
        -------------------------------------------------------------------------
        INSERT INTO dbo.address_archive (address_id, ts, deleted_by)
        SELECT d.address_id, @now, @user
        FROM deleted AS d;

        -------------------------------------------------------------------------
        -- 2) 审计日志:插入 audit_log(table, pk, op, ts)
        --    注意:[table] 为保留字,需加方括号
        --    假定 audit_log 列类型如下:
        --    [table] sysname, [pk] nvarchar(256), [op] varchar(10), [ts] datetime2(3)
        -------------------------------------------------------------------------
        INSERT INTO dbo.audit_log ([table], [pk], [op], [ts])
        SELECT
            N'dbo.customer_address' AS [table],
            CAST(d.address_id AS nvarchar(256)) AS [pk],
            N'DELETE' AS [op],
            @now AS [ts]
        FROM deleted AS d;

        -------------------------------------------------------------------------
        -- 3) Outbox:写入主题为 address.deleted 的消息
        --    假定 outbox 至少包含:
        --    topic sysname, payload nvarchar(max), ts datetime2(3)
        --    payload 为每行一个 JSON(含最小必要字段)
        --    若需携带更多上下文,可在子查询中追加字段
        -------------------------------------------------------------------------
        INSERT INTO dbo.outbox (topic, payload, ts)
        SELECT
            N'address.deleted' AS topic,
            (
                SELECT
                    d.address_id,
                    d.customer_id
                FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
            ) AS payload,
            @now AS ts
        FROM deleted AS d;

    END TRY
    BEGIN CATCH
        -- 将错误冒泡给外层事务,确保原子性
        DECLARE @errnum int = ERROR_NUMBER(),
                @errmsg nvarchar(4000) = ERROR_MESSAGE(),
                @errstate int = ERROR_STATE();
        THROW @errnum, @errmsg, @errstate;
    END CATCH
END
GO

代码说明

  • 关键语法解释
    • AFTER DELETE:确保主删除已通过约束检查后执行触发器,且与外层事务一致。
    • deleted 伪表:包含被删除的所有行,使用集合方式一次性处理多行删除,避免逐行循环导致性能问题。
    • SYSUTCDATETIME():使用 UTC 时间戳,便于跨时区一致审计。
    • SESSION_CONTEXT('user'):应用层可通过 sp_set_session_context 设置当前操作人;未设置则回退 ORIGINAL_LOGIN()。
    • SELECT ... FOR JSON PATH, WITHOUT_ARRAY_WRAPPER:为每条删除行构造一条扁平 JSON 作为 outbox payload(SQL Server 2016+)。
  • 业务逻辑说明
    • 归档最小化:将 address_id + 删除时间 + 删除人写入归档,满足“含原主键、ts、deleted_by”的硬性要求。若需完整快照,可扩展 address_archive 结构并在 INSERT 中追加列。
    • 审计日志保存变更元数据:表名、主键、操作类型、时间,支持统一审计查询。
    • Outbox 事件:主题固定为 address.deleted;payload 携带最少必要的主键信息(address_id, customer_id),便于下游去读模型或触发清理。可按需扩展字段。
  • 注意事项提醒
    • 请确保以下目标表与列存在且类型匹配:
      • dbo.address_archive (address_id, ts, deleted_by)
      • dbo.audit_log ([table], [pk], [op], [ts])
      • dbo.outbox (topic, payload, ts)
    • 若 address_id 不是主键或为复合键,请调整 INSERT 列与 payload 构造逻辑。
    • 建议数据库保持 RECURSIVE_TRIGGERS OFF(默认),避免意外触发链导致复杂度上升。
    • 触发器内不做外部服务调用或长耗时操作,避免阻塞删除事务。

性能建议

  • 索引优化建议
    • dbo.audit_log:建议建立组合索引 (table, ts DESC) 或 (table, pk, ts) 以便常见审计查询与清理归档。
    • dbo.outbox:建议索引 (topic, ts) 并为消费轮询建立覆盖索引,例如 (processed_at IS NULL, ts) 或添加状态列 processed_at/attempts。
    • dbo.address_archive:根据查询模式建立 (address_id, ts DESC) 索引;若 address_id 不唯一,考虑将 (address_id, ts) 设为聚簇或唯一约束以避免重复。
  • 监控指标说明
    • 触发器执行时间与阻塞:监控等待类型(如 LCK_*)与平均执行时间,避免 outbox/audit 表成为热点。
    • outbox 积压:监控 outbox 未处理消息数量与最大滞留时间,评估下游消费是否及时。
    • 错误与重试:监控触发器执行失败率(可在 CATCH 前后添加轻量日志,但避免在触发器内再写复杂逻辑)。
  • 常见问题预防
    • 大批量删除:建议分批次删除(批量大小可控制在数千行),降低日志压力与锁竞争。
    • JSON 支持:SQL Server 2016+ 才支持 FOR JSON。若版本更低,需要改为手工拼接 JSON(不推荐)或使用 XML/纯文本。
    • 会话用户未设置:应用层在执行删除前调用 EXEC sp_set_session_context @key=N'user', @value=@user_name;触发器已提供回退 ORIGINAL_LOGIN(),但建议统一由应用设置。

测试用例

  • 基本功能测试场景
    1. 设置会话用户
      • EXEC sys.sp_set_session_context @key = N'user', @value = N'alice';
    2. 删除单行
      • DELETE FROM dbo.customer_address WHERE address_id = 1001;
      • 期望:
        • address_archive 出现 (1001, ts≈当前UTC, deleted_by='alice')
        • audit_log 出现一行 table='dbo.customer_address', pk='1001', op='DELETE'
        • outbox 出现 topic='address.deleted',payload 含 {"address_id":1001,...}
    3. 删除多行
      • DELETE FROM dbo.customer_address WHERE customer_id = 200;
      • 期望上述三张表分别批量插入对应行数,且 ts 基本一致,deleted_by='alice'。
  • 边界条件测试建议
    • 未设置会话用户:不调用 sp_set_session_context,删除一行;期望 deleted_by 回退为 ORIGINAL_LOGIN()。
    • 大批量删除:构造数万行数据,分批删除(例如每批 5k-10k),观察执行时间、锁等待与事务日志增长。
    • 事务回滚:BEGIN TRAN 后删除一行,确认三张表新增记录;随后 ROLLBACK TRAN,确认三张表新增记录回滚消失,验证原子性。
    • JSON 校验:验证 outbox.payload 为合法 JSON,字段包含 address_id(及需要的 customer_id)。

备注与扩展

  • 如需归档完整行快照:在 address_archive 中增加与主表相同的数据列(如 customer_id、地址字段等),并在 INSERT 语句中按显式列清单补齐 SELECT 对应列,保持列对齐、避免 SELECT *。
  • 如需幂等或防重复:可在 address_archive 以 (address_id, ts) 建唯一约束;outbox 可增加去重键(如 (topic, hash(payload)))以防重复写入。

示例详情

解决的问题

用一条提示词,把“写触发器”这件高风险、费时间的工作,变成标准化、可复用、可审计的交付流程。

  • 基于业务描述,快速产出可直接上线的触发器方案与代码,覆盖 MySQL/PostgreSQL/Oracle/SQL Server。
  • 内置三重护栏:逻辑严谨、性能稳健、合规安全;默认阻断高风险与低效写法。
  • 全链路产出:需求拆解→语法适配→逻辑设计→代码生成→优化建议→测试用例,一次搞定。
  • 交付即文档:代码自带清晰注释、使用说明与注意事项,降低沟通与评审成本。
  • 可复用模板服务电商库存、金融审计、ERP 同步、日志追踪等高频场景,缩短交付周期,减少回滚与线上告警。
  • 让新人也能做出“专家级”触发器,团队标准得以沉淀,维护成本持续降低。

适用用户

数据库管理员(DBA)

快速制定统一的触发器规范,批量生成与校验,多库版本适配与迁移更省心;附带上线清单和监控建议,显著降低生产事故与告警噪声。

后端开发工程师

把业务需求一键转成触发逻辑,自动处理库存更新、余额变更、日志记录;自带注释与测试场景,减少后端代码复杂度与返工。

金融与风控工程师

构建交易审计、异常留痕、额度管控等规则,自动生成边界测试与验证步骤,确保关键数据可追溯、可核验。

特征总结

按数据库类型自动匹配语法,一键生成可直接执行的触发器代码,减少手工改动
从业务场景自然转化为触发逻辑,自动梳理触发条件与动作,避免遗漏与冲突
自动添加清晰注释与使用说明,新成员接手也能看懂,降低沟通与维护成本
内置性能优化建议与索引提示,帮助触发器稳定运行,减少锁等待与慢查询
提供可直接套用的测试场景与数据,快速验证边界条件,保障上线质量与安全
多行业场景模板一键调用,电商库存、交易审计、日志追踪等需求即刻落地
内建风险守则与最佳实践,自动规避递归触发、死锁等隐患,先发制人减少事故
支持参数化定制与版本适配,轻松复用方案,在不同库与环境间快速迁移
生成上线清单与回滚指引,配套监控指标建议,确保发布过程可控可追踪

如何使用购买的提示词模板

1. 直接在外部 Chat 应用中使用

将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。

2. 发布为 API 接口调用

把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。

3. 在 MCP Client 中配置使用

在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。

AI 提示词价格
¥20.00元
先用后买,用好了再付款,超安全!

您购买后可以获得什么

获得完整提示词模板
- 共 613 tokens
- 3 个可调节参数
{ 数据库类型 } { 触发动作 } { 业务场景 }
获得社区贡献内容的使用权
- 精选社区优质案例,助您快速上手提示词
使用提示词兑换券,低至 ¥ 9.9
了解兑换券 →
限时半价

不要错过!

半价获取高级提示词-优惠即将到期

17
:
23
小时
:
59
分钟
:
59