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变为completedpending变为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。
备注:内容仅供参考。
