订单状态字段必须用 ENUM
或 TINYINT
,别用 VARCHAR
用字符串存状态(比如
'pending'、
'shipped')看着直观,但查起来慢、改起来难、还容易拼错。MySQL 里最稳妥的是
TINYINT:0=待支付,1=已支付,2=已发货,3=已完成,-1=已取消。或者用
ENUM('pending','paid','shipped','done','canceled')——它底层也转成整数,还能防非法值。
如果已有表用了
VARCHAR,别直接
ALTER TABLE ... MODIFY status VARCHAR(20)然后手动 UPDATE,得先加约束再批量更新:
ALTER TABLE orders ADD COLUMN status_new TINYINT DEFAULT 0; UPDATE orders SET status_new = CASE status WHEN 'pending' THEN 0 WHEN 'paid' THEN 1 WHEN 'shipped' THEN 2 ELSE 0 END; ALTER TABLE orders DROP COLUMN status, CHANGE status_new status TINYINT NOT NULL;
发货操作必须用事务 + 行级锁,不能只靠应用层判断
常见错误是先
SELECT status FROM orders WHERE id = 123,发现是
paid就执行
UPDATE ... SET status = 2。这在并发下会超发:两个发货员同时查到同一单是
paid,都去更新,结果发了两次货。
正确做法是在一条语句里完成校验和更新,并加
FOR UPDATE锁住这行:
BEGIN; SELECT * FROM orders WHERE id = 123 AND status = 1 FOR UPDATE; -- 如果查不到,说明状态不对,直接 ROLLBACK UPDATE orders SET status = 2, shipped_at = NOW(), tracking_no = 'SF123456789' WHERE id = 123 AND status = 1; -- 检查 ROW_COUNT() 是否为 1,不是就说明被别人抢先改了 COMMIT;
发货时要同步更新库存,且库存扣减必须原子化
订单发货 ≠ 单纯改订单状态。真实场景中,发货前得确认对应商品的库存是否足够(尤其是预售或多仓场景),发货后要扣减可售库存。别在应用里先查库存再扣减——中间可能被其他订单抢走。
推荐在发货事务里一起处理,用子查询或
JOIN确保一致性:
UPDATE products p JOIN order_items oi ON p.id = oi.product_id SET p.stock = p.stock - oi.quantity WHERE oi.order_id = 123 AND p.stock >= oi.quantity;
如果这条语句影响行为 0,说明某商品库存不足,整个发货事务该回滚。
库存字段建议用INT(无符号),避免负数 高并发时考虑把库存拆到缓存(如 Redis)做预占,MySQL 只做最终落库 不要用
UPDATE ... SET stock = stock - 1而不校验,这是典型的“超卖”温床
发货记录要独立建表,别堆在 orders
里加一堆 shipped_at
、carrier
、tracking_no
一张订单可能多次发货(比如分批发货、补发、换货),如果所有字段都塞进
orders表,历史轨迹就没了,查询也变重。应该拆出
shipments表:
CREATE TABLE shipments ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, status TINYINT DEFAULT 1, -- 1=created, 2=packed, 3=shipped, 4=delivered shipped_at DATETIME NULL, carrier VARCHAR(32), tracking_no VARCHAR(64), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_order_id (order_id), FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE );
这样每次发货写一行,订单状态字段只反映「当前整体进展」,而
shipments表承载完整操作日志。导出物流数据、对接快递平台、做发货时效分析,都靠这张表撑着。
真正麻烦的是状态流转逻辑——比如「已发货」不等于「物流已揽收」,系统得能区分业务状态和物流节点。这个边界一旦模糊,售后查件就会卡在「到底发没发」的扯皮里。
