PAYATHON 2026

payment status 为 complete 时向 order 表插入行的 trigger

支付阿杰

结论

应创建一个 AFTER UPDATE 触发器,检查 dj_status 是否由非 completed 状态变为 completed。创建订单时,应使用 NEW.dj_id、NEW.dj_user_id 等更新后的数据。

当前代码存在以下问题:

  • MySQL 的相等运算符是 =,不是 ==。
  • SELECT ... INTO 的语法不正确。
  • 更新后的字段应从 NEW 读取,而不是 OLD。
  • 仅判断 NEW.dj_status = 'completed' 可能导致重复创建订单。
  • users 和 user_addresses 中的字段应带上表别名,以免产生歧义。
  • 如果用户有多个地址,查询可能返回多行,导致触发器执行失败。

推荐实现

假设交易表名为 deals,主键为 dj_id,用户字段为 dj_user_id,可以直接使用 INSERT ... SELECT,不需要声明中间变量:

DELIMITER $$

CREATE TRIGGER trg_deals_create_order
AFTER UPDATE ON deals
FOR EACH ROW
BEGIN
    IF NEW.dj_status = 'completed'
       AND NOT (OLD.dj_status <=> 'completed') THEN

        INSERT INTO `order` (
            `or_dj_id`,
            `or_track_number`,
            `or_recipient_name`,
            `or_recipient_number`,
            `or_address`
        )
        SELECT
            NEW.dj_id,
            '',
            u.user_name,
            u.user_contact_number,
            ua.user_address_google
        FROM users AS u
        INNER JOIN user_addresses AS ua
            ON ua.user_address_user_id = u.user_id
        WHERE u.user_id = NEW.dj_user_id
        LIMIT 1;

    END IF;
END$$

DELIMITER ;

这里使用了 MySQL 的 NULL 安全比较运算符 <=>。以下状态变化都会触发插入:

  • NULL 变为 completed
  • pending 变为 completed
  • 其他状态变为 completed

如果原状态已经是 completed,之后只修改交易的其他字段,不会再次插入订单。

如果必须使用局部变量

正确的 SELECT ... INTO 语法是先列出全部查询字段,再统一列出对应的目标变量:

DELIMITER $$

CREATE TRIGGER trg_deals_create_order
AFTER UPDATE ON deals
FOR EACH ROW
BEGIN
    DECLARE recipient_name VARCHAR(225);
    DECLARE recipient_number VARCHAR(225);
    DECLARE recipient_address VARCHAR(225);

    IF NEW.dj_status = 'completed'
       AND NOT (OLD.dj_status <=> 'completed') THEN

        SELECT
            u.user_name,
            u.user_contact_number,
            ua.user_address_google
        INTO
            recipient_name,
            recipient_number,
            recipient_address
        FROM users AS u
        INNER JOIN user_addresses AS ua
            ON ua.user_address_user_id = u.user_id
        WHERE u.user_id = NEW.dj_user_id
        LIMIT 1;

        INSERT INTO `order` (
            `or_dj_id`,
            `or_track_number`,
            `or_recipient_name`,
            `or_recipient_number`,
            `or_address`
        )
        VALUES (
            NEW.dj_id,
            '',
            recipient_name,
            recipient_number,
            recipient_address
        );

    END IF;
END$$

DELIMITER ;

通常,直接使用 INSERT ... SELECT 更简洁,也可以避免局部变量未正确赋值的问题。

用户没有地址时如何处理

使用 INNER JOIN 时,如果用户不存在或没有地址,SELECT 不会返回记录,订单也不会创建。

如果业务允许在没有地址的情况下创建订单,可以从 users 表查询,并改用 LEFT JOIN:

INSERT INTO `order` (
    `or_dj_id`,
    `or_track_number`,
    `or_recipient_name`,
    `or_recipient_number`,
    `or_address`
)
SELECT
    NEW.dj_id,
    '',
    u.user_name,
    u.user_contact_number,
    ua.user_address_google
FROM users AS u
LEFT JOIN user_addresses AS ua
    ON ua.user_address_user_id = u.user_id
WHERE u.user_id = NEW.dj_user_id
LIMIT 1;

这样一来,没有地址的订单会把 or_address 写为 NULL,前提是该列允许存储 NULL。

多地址和重复订单问题

如果一个用户可能有多个地址,不建议只使用 LIMIT 1。没有指定排序规则时,数据库不保证会选中哪条记录。更合适的做法是在地址表中保存默认地址标志,例如:

AND ua.is_default = 1

也可以按照明确的规则选择最新地址:

ORDER BY ua.updated_at DESC
LIMIT 1

还可以在订单表中为交易编号添加唯一约束,防止并发操作或其他程序逻辑重复创建订单:

ALTER TABLE `order`
ADD UNIQUE KEY `uk_order_deal` (`or_dj_id`);

能否添加这项约束,取决于一个 deal 是否只能对应一条订单。如果一个 deal 可以拆分为多条订单,就不能设置该约束。

关于“由 admin 设置状态”

触发器只能判断数据发生了什么变化,通常无法仅根据 dj_status 判断操作者是否为管理员。如果必须限定为管理员操作,需要满足以下条件之一:

  • 应用程序同时写入 updated_by 或 completed_by 字段,由触发器检查对应用户;
  • 管理员更新由专用数据库账号执行,触发器再结合数据库账号进行判断;
  • 在应用层验证管理员权限,数据库触发器只根据状态变化创建订单。

更常见且更容易维护的方式,是由应用层确认管理员权限后更新状态,触发器只检查状态是否首次变为 completed。

备注:内容仅供参考。