如何在Eloquent模型中实现用户变更的审批与回滚机制?
这个场景在内容管理系统里太常见了,我之前做过类似的功能,核心思路就是不要直接修改主数据,而是用一个变更请求表来隔离待审批的操作,这样驳回的时候根本不用“恢复”——因为原数据本来就没动过!下面给你详细拆解实现思路:
核心方案:变更请求表 + 主数据隔离
我们需要两张核心表:一张存正式的games数据,另一张存用户发起的变更请求game_change_requests。通过这种方式,所有待审批的操作都先落在请求表里,只有管理员批准后才会同步到主表。
1. 表结构设计
主表 games
保留你原本的字段,再加个软删除字段(可选但推荐):
id(主键)title(游戏标题)desc(游戏描述)is_deleted(布尔值,标记软删除,默认false)- 其他你需要的字段(比如创建时间、更新时间)
变更请求表 game_change_requests
用来记录所有待审批的编辑/删除操作,关键是要存变更前的原数据快照:
id(主键)game_id(外键,关联games表)requester_id(发起请求的用户ID,比如用户A)change_type(枚举:edit/delete,标记是编辑还是删除请求)payload(JSON格式,存变更后的内容,比如编辑请求的新title、新desc)original_payload(JSON格式,存变更前的原数据快照,比如编辑前的title、desc——这是兜底恢复的关键)status(枚举:pending/approved/rejected,请求状态)admin_id(处理请求的管理员ID,审批后填充)processed_at(审批时间,审批后填充)
2. 完整流程拆解
2.1 用户发起编辑/删除请求
当用户A想要修改Game的title或者删除它时:
- 编辑操作:先读取当前
Game的原数据,在game_change_requests里创建一条记录,payload填新的title(和其他要修改的字段),original_payload填当前的原title和desc,状态设为pending。注意:此时绝对不能修改games表的任何数据! - 删除操作:同样在
game_change_requests里创建一条记录,change_type设为delete,original_payload填当前Game的完整快照(方便后续如果误删可以恢复),状态设为pending。
2.2 管理员处理请求
- 批准编辑:从
game_change_requests里取出payload,用它更新games表对应的记录,然后把请求状态改成approved。 - 驳回编辑:什么都不用改
games表!因为原数据根本没被触动,直接把请求状态改成rejected就行——完美解决了“恢复原title”的问题,我们从一开始就没碰过原数据。 - 批准删除:把
games表对应记录的is_deleted设为true(软删除),或者直接硬删(但软删更安全),然后把请求状态改成approved。 - 驳回删除:直接把请求状态改成
rejected,games表数据保持原样。
3. 代码示例(以Laravel为例)
模型定义
// Game 主模型 class Game extends Model { use SoftDeletes; // 启用软删除 protected $fillable = ['title', 'desc']; protected $dates = ['deleted_at']; } // 变更请求模型 class GameChangeRequest extends Model { protected $fillable = [ 'game_id', 'requester_id', 'change_type', 'payload', 'original_payload', 'status', 'admin_id' ]; // 把JSON字段自动转成数组 protected $casts = [ 'payload' => 'array', 'original_payload' => 'array', ]; }
用户发起编辑请求
// 用户A编辑Game title的逻辑 $game = Game::findOrFail($gameId); $newTitle = "我的新游戏标题"; GameChangeRequest::create([ 'game_id' => $game->id, 'requester_id' => auth()->id(), // 当前登录用户(用户A)的ID 'change_type' => 'edit', 'payload' => [ 'title' => $newTitle, 'desc' => $game->desc, // 如果只改title,desc沿用原数据 ], 'original_payload' => [ 'title' => $game->title, 'desc' => $game->desc, ], 'status' => 'pending', ]);
管理员批准编辑
// 管理员处理编辑请求的逻辑 $request = GameChangeRequest::findOrFail($requestId); $adminId = auth()->id(); // 当前登录管理员的ID if ($request->change_type === 'edit' && $request->status === 'pending') { // 更新主表数据 $game = Game::findOrFail($request->game_id); $game->update($request->payload); // 更新请求状态 $request->update([ 'status' => 'approved', 'admin_id' => $adminId, 'processed_at' => now(), ]); }
管理员驳回编辑
// 管理员驳回编辑请求的逻辑 $request = GameChangeRequest::findOrFail($requestId); $adminId = auth()->id(); if ($request->status === 'pending') { // 直接标记为驳回,不需要动主表 $request->update([ 'status' => 'rejected', 'admin_id' => $adminId, 'processed_at' => now(), ]); }
4. 额外优化点
- 乐观锁防并发修改:如果担心在请求审批期间,其他用户(或者用户A自己)又发起了新的变更,可以给
games表加个version字段。创建变更请求时记录当前version,管理员批准前检查games表的version是否和请求里的一致,不一致就提示管理员“数据已被修改,请重新审核”。 - 前端显示优化:用户A自己查看游戏时,可以优先显示他发起的待审批变更内容;其他用户(包括管理员)看到的是主表的正式数据。
- 删除恢复支持:如果管理员误批准了删除请求,因为我们存了
original_payload,可以直接用快照恢复games表的数据。
内容的提问来源于stack exchange,提问作者rook
相关产品推荐
相关产品推荐

