电商系统实现“完全撤销”和“反撤销”的关键是将操作建模为可存、可调、可倒带的命令对象,并通过ICommand与IUndoableCommand双接口统一约束;命令须自带执行前快照,用undo/redo双栈协同管理历史,不可逆操作需清空栈并设计补偿逻辑。

电商系统里要实现“完全撤销”和“反撤销”,关键不是堆功能,而是让每个操作变成可存、可调、可倒带的对象,并通过接口统一约束行为边界。核心不在命令怎么写,而在接口怎么定义、状态怎么捕获、历史怎么管理。
定义可撤销命令的统一接口
必须抽象出带明确语义的接口,不能只靠约定。推荐两个层级:
-
ICommand:含
Execute()方法,所有操作都得实现它 -
IUndoableCommand:继承自 ICommand,额外声明
Undo()和IsUndoable() bool
这样设计能自然区分“下单”(可撤销)、“发货”(不可撤销)、“支付回调确认”(不可逆)等场景。调用器执行前先判断 IsUndoable(),避免把不可逆操作压入撤销栈。
命令对象必须自带执行前快照
撤销不是靠“猜原来是什么”,而是靠“当时记了什么”。比如“修改商品库存”命令,不能在 Undo() 里去查数据库当前值再减回去——网络延迟、并发写入会让这个值失效。
- 构造时传入原始值:
NewUpdateStockCommand(productID, newQty, oldQty) - 或在
Execute()开头主动捕获:this.oldStock = receiver.GetStock(id) - 快照字段必须精简:只存影响还原的关键数据(如库存数、订单状态码、用户余额),不存整个实体
双栈协同管理历史,且严格清空规则
撤销/重做不是单个列表来回跳,而是两个独立栈协作:
- undoStack:存已成功执行的可撤销命令
- redoStack:存刚被 undo 掉的命令
- 新命令执行后:
undoStack.Push(cmd),同时redoStack.Clear() - 执行
Undo():弹出undoStack顶部命令 → 调用其Undo()→redoStack.Push(cmd) - 执行
Redo():弹出redoStack顶部命令 → 调用其Execute()→undoStack.Push(cmd)
特别注意:任何不可撤销操作(如调用第三方支付网关)一旦执行,必须清空 undoStack,防止用户误点撤销触发异常回滚。
对不可逆操作设计补偿逻辑而非硬撤销
电商里很多动作天然不可逆:短信发送、邮件通知、物流打单、支付扣款。它们不能靠 Undo() 回退,必须提前设计补偿路径:
- “发短信”命令对应“撤回短信”(若通道支持)或“补发更正短信”
- “创建订单”可撤销,“支付成功”不可撤销,但可配套生成“申请退款”补偿命令
- 命令类内显式标记:
IsCompensatable() bool,供调用器决定是否允许入栈或触发补偿流程
补偿命令本身也应实现 IUndoableCommand,形成可追溯、可再撤销的闭环。

















