本文目录导读:

在 PHP 项目中,实现反馈工单的流转与跟进状态,通常需要结合数据库设计、后端状态机逻辑以及前端交互,下面是一个标准的实现方案,涵盖了从提交到关闭的完整生命周期。
核心:工单状态机设计
需要定义清晰的状态流转规则,常见的工单状态如下:
| 状态 | 含义 | 下一步可能状态 |
|---|---|---|
| 待处理 | 用户提交,管理员未查看 | 处理中、已关闭 |
| 处理中 | 管理员已接单,正在处理 | 待回复、已解决、已关闭 |
| 待回复 | 管理员回复或询问,等待用户反馈 | 处理中、已关闭 |
| 已解决 | 管理员认为已解决,等待用户确认 | 待回复、已关闭 |
| 已关闭 | 最终状态,不可再流转 | - |
状态流转图
[用户提交] --> 待处理
|
v
处理中 <---> 待回复
|
v
已解决 <---> 待回复 (用户不满意)
|
v
已关闭
数据库表设计
建议使用 MySQL 或 PostgreSQL。
-- 工单主表
CREATE TABLE tickets (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL, -- 提交用户IDVARCHAR(255) NOT NULL, -- 工单标题
description TEXT, -- 问题描述
status ENUM('待处理', '处理中', '待回复', '已解决', '已关闭') DEFAULT '待处理',
priority ENUM('低', '中', '高', '紧急') DEFAULT '中',
assignee_id INT DEFAULT NULL, -- 当前处理人(管理员ID)
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
closed_at TIMESTAMP NULL, -- 关闭时间
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (assignee_id) REFERENCES admins(id)
);
-- 跟进记录表(记录所有操作日志)
CREATE TABLE ticket_logs (
id INT PRIMARY KEY AUTO_INCREMENT,
ticket_id INT NOT NULL,
operator_type ENUM('user', 'admin') NOT NULL, -- 操作者类型
operator_id INT NOT NULL, -- 操作者ID
action ENUM('created', 'assigned', 'status_changed', 'replied', 'closed'),
old_status VARCHAR(20), -- 变更前状态
new_status VARCHAR(20), -- 变更后状态
content TEXT, -- 备注或回复内容
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (ticket_id) REFERENCES tickets(id)
);
PHP 后端逻辑实现(状态机)
在 PHP 中,建议使用 状态模式 或 简单的条件判断 来控制流转,下面是一个基于 switch 的示例:
<?php
class TicketService
{
/**
* 更新工单状态(核心流转逻辑)
* @param int $ticketId
* @param string $newStatus 目标状态
* @param int $operatorId 操作人ID
* @param string $operatorType 操作人类型 (user/admin)
* @return bool|string 成功返回true,失败返回错误信息
*/
public function changeStatus(int $ticketId, string $newStatus, int $operatorId, string $operatorType): bool|string
{
// 1. 获取当前工单
$ticket = Ticket::find($ticketId);
if (!$ticket) {
return '工单不存在';
}
$currentStatus = $ticket->status;
$allowedTransitions = $this->getAllowedTransitions($currentStatus, $operatorType);
// 2. 验证状态是否允许
if (!in_array($newStatus, $allowedTransitions)) {
return "不允许从 [$currentStatus] 流转到 [$newStatus]";
}
// 3. 权限检查(用户不能直接改为“已解决”)
if (!$this->checkPermission($operatorType, $newStatus, $ticket)) {
return '无权限执行此操作';
}
// 4. 执行状态变更(数据库事务)
DB::beginTransaction();
try {
// 更新主表
$ticket->status = $newStatus;
$ticket->updated_at = now();
if ($newStatus == '已关闭') {
$ticket->closed_at = now();
}
$ticket->save();
// 记录日志
$log = new TicketLog();
$log->ticket_id = $ticketId;
$log->operator_type = $operatorType;
$log->operator_id = $operatorId;
$log->action = 'status_changed';
$log->old_status = $currentStatus;
$log->new_status = $newStatus;
$log->content = "状态由 [$currentStatus] 变更为 [$newStatus]";
$log->save();
// 可选:发送通知
$this->sendStatusChangeNotification($ticket, $newStatus);
DB::commit();
return true;
} catch (\Exception $e) {
DB::rollBack();
return '状态更新失败:' . $e->getMessage();
}
}
/**
* 定义允许的状态转换矩阵
*/
private function getAllowedTransitions(string $currentStatus, string $operatorType): array
{
// 基础转换规则
$transitions = [
'待处理' => ['处理中', '已关闭'],
'处理中' => ['待回复', '已解决', '已关闭'],
'待回复' => ['处理中', '已关闭'], // 用户回复后管理员可再次进入处理中
'已解决' => ['待回复', '已关闭'], // 用户不满意可回到待回复
'已关闭' => [], // 最终态,不可变更
];
// 管理员可以执行所有允许的转换
if ($operatorType === 'admin') {
return $transitions[$currentStatus] ?? [];
}
// 用户只能执行部分转换(提交时,回复时)
$userTransitions = [
'待处理' => [], // 用户提交后不能再改
'处理中' => ['待回复'], // 用户可回复,但不改变状态
'待回复' => ['处理中'], // 用户回复后,状态变回处理中(等待管理员)
'已解决' => ['待回复'], // 用户不满意,重新开启
'已关闭' => [], // 用户不能重开,需要管理员处理
];
// 特殊处理:用户只能将待回复 -> 处理中, 已解决 -> 待回复
// 用户回复 不等于 状态变更,需要单独处理。
// 这里简化:用户主动点击 "重新开启" 才改变状态。
return $userTransitions[$currentStatus] ?? [];
}
/**
* 简单权限检查
*/
private function checkPermission(string $operatorType, string $newStatus, $ticket): bool
{
// 禁止用户关闭自己的工单(应由管理员关闭)
if ($operatorType === 'user' && in_array($newStatus, ['已关闭', '已解决'])) {
return false;
}
// 管理员不能将状态回退到“待处理”(用户自己提交的才算待处理)
if ($operatorType === 'admin' && $newStatus === '待处理') {
return false;
}
return true;
}
/**
* 发送通知(邮件/钉钉/内部消息)
*/
private function sendStatusChangeNotification($ticket, string $newStatus): void
{
// 可以发送给提交者或处理人
// $ticket->user->notify(new TicketStatusUpdated($ticket, $newStatus));
}
}
前端交互设计
前端需要根据当前用户角色(用户/管理员)显示不同的操作按钮。
用户视角:
- 待处理:只能查看,不能操作
- 处理中:可以追加回复(此时状态不变,但管理员会收到通知)
- 待回复:可以(回复后,状态变为“处理中”)
- 已解决:可以确认关闭(或重新开启,状态变为“待回复”)
管理员视角:
- 待处理:可接单(状态变为处理中)
- 处理中:可回复用户(回复后自动变为“待回复”或保持“处理中”),或标记为已解决
- 待回复:查看用户回复后,可继续处理(状态变为处理中),或关闭工单
- 已解决:可最终关闭,或重新开启(如果用户不满意但未操作)
界面按钮示例:
<!-- 管理员操作区 --> <button class="btn btn-primary" data-action="接单" data-status="处理中">接单</button> <button class="btn btn-success" data-action="解决" data-status="已解决">标记已解决</button> <button class="btn btn-danger" data-action="关闭" data-status="已关闭">关闭工单</button> <!-- 用户操作区 --> <button class="btn btn-info" data-action="回复" data-status="处理中">回复</button> <button class="btn btn-warning" data-action="重新开启" data-status="待回复">重新开启</button>
JavaScript 示例:
// 使用 Ajax 调用后端 API
$('.status-change-btn').click(function() {
const ticketId = $(this).data('ticket-id');
const newStatus = $(this).data('status');
const action = $(this).data('action');
if (!confirm(`确定要${action}吗?`)) return;
$.ajax({
url: '/api/ticket/change-status',
method: 'POST',
data: {
ticket_id: ticketId,
new_status: newStatus,
_token: csrfToken
},
success: function(response) {
if (response.success) {
alert('操作成功');
location.reload();
} else {
alert('操作失败:' + response.message);
}
},
error: function() {
alert('网络错误');
}
});
});
进阶优化建议
-
使用状态机库: 推荐使用
sebastian/environment或更强大的Finite(yohang/Finite)库来管理复杂的状态流转,避免硬编码。 -
引入工作流引擎: 对于非常复杂的工单(如跨部门审批、SLA 监控),可以考虑集成 Symfony Workflow 或 Petri网 概念。
-
通知与提醒:
- 状态变更时,通过邮件、短信、WebSocket 或钉钉/企业微信机器人通知相关方。
- SLA 超时自动升级(如:待处理超过 24 小时自动升级为紧急)。
-
权限与角色:
- 普通用户:只能查看自己的工单,不能关闭。
- 普通管理员:可以处理所有工单,但不能删除。
- 超级管理员:可以关闭、删除、重新分配。
-
搜索与筛选: 提供按状态、优先级、处理人、时间范围进行高级搜索。
-
前端框架: 对于复杂工单,建议使用 Vue.js 或 React 构建单页应用 (SPA) 来实时显示状态变化,配合 WebSocket 实现即时通讯。
在 PHP 项目中实现工单流转,核心是定义好状态机,记录操作日志,规范权限,前端根据当前状态和用户角色动态显示操作按钮,通过这样一套设计,可以确保工单在不同角色之间有序流转,同时为后续的统计分析和问题追溯提供完整的数据支持。