本文目录导读:

在PHP项目中实现AI审核结果的人工复核流转,核心思路是将“AI自动判断”与“人工干预”解耦,并通过状态机或工作流引擎来管理审核状态。
以下是实现该功能的完整方案,分为数据结构设计、状态流转逻辑、关键代码实现和UI交互建议四个部分。
数据库表结构设计(核心)
需要两张核心表:audit_records(审核记录表)和audit_workflow_log(审核日志表)。
表1:audit_records(审核记录表)
CREATE TABLE `audit_records` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`target_type` VARCHAR(50) NOT NULL COMMENT '审核对象类型,如:article, comment, image',
`target_id` INT UNSIGNED NOT NULL COMMENT '审核对象ID',
`content` TEXT COMMENT '原始内容摘要,方便人工查看',
// AI 审核结果
`ai_result` ENUM('pass', 'reject', 'review') DEFAULT 'review' COMMENT 'AI判定结果:通过/拒绝/疑似需人工审核',
`ai_reason` VARCHAR(500) DEFAULT NULL COMMENT 'AI驳回原因或风险标签',
`ai_score` DECIMAL(5,2) DEFAULT NULL COMMENT 'AI置信度分数,用于辅助判断',
// 流转控制字段(关键)
`status` ENUM('pending_ai', 'pending_review', 'passed', 'rejected') DEFAULT 'pending_ai' COMMENT '当前审核状态',
`assigned_to` INT UNSIGNED DEFAULT NULL COMMENT '当前审核人ID(抢单/分配)',
`assigned_at` DATETIME DEFAULT NULL COMMENT '分配时间',
`reviewed_by` INT UNSIGNED DEFAULT NULL COMMENT '最终复核人ID',
`review_result` ENUM('pass', 'reject') DEFAULT NULL COMMENT '人工最终结果',
`review_reason` TEXT COMMENT '人工备注',
// 时间戳
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_target` (`target_type`, `target_id`),
INDEX `idx_status` (`status`),
INDEX `idx_assigned_to` (`assigned_to`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
表2:audit_workflow_log(审核流转日志)
CREATE TABLE `audit_workflow_log` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`record_id` INT UNSIGNED NOT NULL,
`from_status` VARCHAR(50) DEFAULT NULL,
`to_status` VARCHAR(50) NOT NULL,
`action` VARCHAR(100) NOT NULL COMMENT '操作:ai_auto / assign / review_pass / review_reject / recall',
`operator_id` INT UNSIGNED DEFAULT NULL COMMENT '操作人ID(null表示系统)',
`operator_name` VARCHAR(100) DEFAULT NULL,
`remark` TEXT COMMENT '操作备注',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_record` (`record_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
状态流转设计
设计一个有限状态机(FSM),避免乱序操作,推荐使用枚举或常量类管理:
| 当前状态 | 可执行动作 | 下一状态 | 说明 |
|---|---|---|---|
pending_ai |
AI 审核完成 | pending_review(AI 结果为 review) |
AI 认为需要人工复核 |
pending_ai |
AI 审核完成 | passed(AI 结果为 pass) |
AI 直接通过(可选) |
pending_ai |
AI 审核完成 | rejected(AI 结果为 reject) |
AI 直接拒绝(可选) |
pending_review |
人工复核通过 | passed |
核心流转点 |
pending_review |
人工复核拒绝 | rejected |
核心流转点 |
pending_review |
手动分配/抢单 | pending_review (更新assigned_to) |
分配审核人 |
passed |
撤回操作(如果需要) | pending_review |
特殊情况,如误判 |
rejected |
撤回操作(如果需要) | pending_review |
特殊情况 |
PHP 核心逻辑实现
建议封装一个单独的“审核工作流服务类”。
<?php
// app/Services/AuditWorkflowService.php
class AuditWorkflowService
{
// 定义允许的状态转换
private array $transitions = [
'pending_ai' => ['pending_review', 'passed', 'rejected'],
'pending_review' => ['passed', 'rejected'],
'passed' => ['pending_review'], // 撤回
'rejected' => ['pending_review'], // 撤回
];
/**
* 执行状态变更(核心方法)
* @param int $recordId 审核记录ID
* @param string $action 操作动作
* @param int|null $operatorId 操作人ID
* @param string|null $remark 备注
* @param array $extraData 额外数据(如 review_result, review_reason)
* @return bool
* @throws \Exception
*/
public function transition(int $recordId, string $action, ?int $operatorId = null, ?string $remark = null, array $extraData = []): bool
{
DB::beginTransaction();
try {
$record = AuditRecord::lockForUpdate()->find($recordId);
if (!$record) throw new \Exception('审核记录不存在');
$currentStatus = $record->status;
// 根据action 计算出目标状态
$newStatus = $this->getNextStatus($currentStatus, $action);
// 校验转换是否合法 (防止直接修改数据库)
if (!in_array($newStatus, $this->transitions[$currentStatus] ?? [])) {
throw new \Exception("不允许从 {$currentStatus} 转换到 {$newStatus}");
}
// 特殊校验:分配操作
if ($action === 'assign') {
$this->assignHandler($record, $extraData['assigned_to'] ?? 0);
}
// 更新主记录
$updateData = [
'status' => $newStatus,
'updated_at' => now(),
];
// 如果是人工最终审核(通过/拒绝),写入人工结果
if (in_array($newStatus, ['passed', 'rejected'])) {
$updateData['reviewed_by'] = $operatorId;
$updateData['review_result'] = ($newStatus === 'passed') ? 'pass' : 'reject';
$updateData['review_reason'] = $remark;
}
$record->update($updateData);
// 写流转日志
$this->logTransition($recordId, $currentStatus, $newStatus, $action, $operatorId, $remark);
DB::commit();
return true;
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
private function getNextStatus(string $currentStatus, string $action): string
{
// 根据动作映射新状态
$map = [
'pass' => 'passed',
'reject' => 'rejected',
'assign' => 'pending_review', // 分配后依然是待审状态
'recall' => 'pending_review', // 撤回
];
return $map[$action] ?? $currentStatus;
}
private function assignHandler($record, int $assignedTo): void
{
// 这里可以增加并发控制,防止多人同时抢同一单
if ($record->assigned_to && $record->assigned_to != $assignedTo) {
throw new \Exception('该记录已被其他审核员认领');
}
$record->update([
'assigned_to' => $assignedTo,
'assigned_at' => now(),
]);
}
private function logTransition($recordId, $fromStatus, $toStatus, $action, $operatorId, $remark): void
{
AuditWorkflowLog::create([
'record_id' => $recordId,
'from_status' => $fromStatus,
'to_status' => $toStatus,
'action' => $action,
'operator_id' => $operatorId,
'operator_name' => $operatorId ? User::find($operatorId)->name : 'system',
'remark' => $remark,
]);
}
// --- 对外API ---
/**
* 人工审核通过
*/
public function reviewPass(int $recordId, int $operatorId, string $reason = '')
{
return $this->transition($recordId, 'pass', $operatorId, $reason);
}
/**
* 人工审核拒绝
*/
public function reviewReject(int $recordId, int $operatorId, string $reason = '')
{
return $this->transition($recordId, 'reject', $operatorId, $reason);
}
/**
* 认领/分配
*/
public function assign(int $recordId, int $operatorId)
{
return $this->transition($recordId, 'assign', $operatorId, '认领任务', ['assigned_to' => $operatorId]);
}
}
AI 审核回调(触发入口)
在你的AI回调逻辑中,调用工作流服务。
// 假设在AI审核回调中
public function handleAiCallback($targetType, $targetId, $aiResult, $aiReason, $aiScore)
{
// 1. 查找或创建审核记录
$record = AuditRecord::firstOrCreate(
['target_type' => $targetType, 'target_id' => $targetId],
['content' => '...', 'status' => 'pending_ai']
);
// 2. 写入AI结果
$record->update([
'ai_result' => $aiResult,
'ai_reason' => $aiReason,
'ai_score' => $aiScore,
]);
// 3. 根据AI结果决定下一步流转
$workflow = new AuditWorkflowService();
switch ($aiResult) {
case 'pass':
$workflow->transition($record->id, 'auto_pass', null, 'AI自动通过, 分数:'.$aiScore);
break;
case 'reject':
$workflow->transition($record->id, 'auto_reject', null, 'AI自动拒绝:'.$aiReason);
break;
case 'review':
// 这里可以触发消息通知,提醒审核员有新任务
$workflow->transition($record->id, 'need_review', null, 'AI判定需人工复审');
break;
}
}
用户界面交互建议
- 待审列表:根据
status = 'pending_review'过滤,列表显示 AI 评分、AI 判定原因(黄/红色标签提示),方便人工优先级排序。 - 详情页面:
- 顶部:展示
AI Result(绿色/红色徽章)和AI Reason。 - 中间:展示原始内容(文本/图片)。
- 底部:审核操作按钮(通过 / 拒绝),并强制填写理由。
- 顶部:展示
- 抢单机制:可以加一个
assigned_to字段,不为空的记录在其他人列表中不显示,利用数据库事务 + 行锁防止重复分配。 - 批量处理:在列表页提供多选框 + 批量通过/拒绝(注意:批量操作也要走循环调用
transition,因为事务和锁的开销)。 - 撤回功能:仅开放给超级管理员,用于纠正误操作。
注意事项(坑点)
- 并发问题:审核员可能同时点击“通过”,日志里的
lockForUpdate()是必要的,防止状态错乱。 - 消息通知:当 AI 标记为
review时,应通过 WebSocket 或消息队列通知相关负责人。 - 批量操作:不要直接用
update()跳过日志,即使批量通过/拒绝,也应该逐条调用transition记录日志。 - 撤回风险:被撤回的记录,内容中应保留原始 AI 和人工的审核历史,不能丢失。
- 可视化流程图:如果能用 laravel-workflow 或类似库画一个流程图更好,但一般的 PHPer 直接用上面状态机足够了。
方案的核心是:status 字段 + audit_workflow_log 日志 + lockForUpdate 并发控制,通过简单的 switch-case 或 状态数组 来定义流转规则,可维护性高,不易出错。