本文目录导读:

我来为你设计一个完整的PHP客服系统工单流程方案。
整体架构设计
数据库表结构
-- 工单表 CREATE TABLE `tickets` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `ticket_no` varchar(30) NOT NULL COMMENT '工单编号', `user_id` int(11) NOT NULL COMMENT '提交用户ID', `category_id` int(11) DEFAULT NULL COMMENT '分类ID', `priority` tinyint(1) DEFAULT '2' COMMENT '优先级 1紧急 2高 3普通 4低', `subject` varchar(200) NOT NULL COMMENT '标题', `status` tinyint(1) DEFAULT '1' COMMENT '状态 1待处理 2处理中 3待确认 4已解决 5已关闭', `assignee_id` int(11) DEFAULT NULL COMMENT '处理人ID', `channel` varchar(20) DEFAULT 'web' COMMENT '渠道 web/app/wechat/email', `created_at` datetime DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `closed_at` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_ticket_no` (`ticket_no`), KEY `idx_user_id` (`user_id`), KEY `idx_assignee` (`assignee_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 工单回复表 CREATE TABLE `ticket_replies` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `ticket_id` bigint(20) NOT NULL, `user_id` int(11) NOT NULL COMMENT '回复人ID', `content` text NOT NULL COMMENT '回复内容', `is_customer` tinyint(1) DEFAULT '0' COMMENT '是否客户回复', `attachments` text COMMENT '附件JSON', `created_at` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_ticket_id` (`ticket_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 工单分类表 CREATE TABLE `ticket_categories` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `parent_id` int(11) DEFAULT '0', `sort_order` int(11) DEFAULT '0', `status` tinyint(1) DEFAULT '1', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 工单操作日志 CREATE TABLE `ticket_logs` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `ticket_id` bigint(20) NOT NULL, `user_id` int(11) DEFAULT NULL, `action` varchar(50) NOT NULL COMMENT '操作类型', `old_value` text, `new_value` text, `created_at` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_ticket_id` (`ticket_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
核心类设计
工单管理类
<?php
class TicketManager {
private $db;
private $cache;
public function __construct($db, $cache) {
$this->db = $db;
$this->cache = $cache;
}
/**
* 创建工单
*/
public function createTicket($userId, $data) {
// 生成工单编号
$ticketNo = $this->generateTicketNo();
// 数据验证
if (empty($data['subject'])) {
throw new Exception('工单主题不能为空');
}
// 创建工单
$ticketData = [
'ticket_no' => $ticketNo,
'user_id' => $userId,
'category_id' => $data['category_id'] ?? 0,
'priority' => $data['priority'] ?? 2,
'subject' => $data['subject'],
'content' => $data['content'] ?? '',
'channel' => $data['channel'] ?? 'web'
];
$ticketId = $this->db->insert('tickets', $ticketData);
// 记录日志
$this->logAction($ticketId, $userId, 'create');
// 发送通知
$this->notifyAssignedAgent($ticketId);
return $ticketId;
}
/**
* 分配工单
*/
public function assignTicket($ticketId, $agentId, $operatorId) {
$ticket = $this->getTicketById($ticketId);
if (!$ticket) {
throw new Exception('工单不存在');
}
$this->db->update('tickets', [
'assignee_id' => $agentId,
'status' => 2 // 处理中
], ['id' => $ticketId]);
// 记录日志
$this->logAction($ticketId, $operatorId, 'assign',
$ticket['assignee_id'], $agentId);
// 通知被分配人
$this->notifyAgent($agentId, '您有新工单需要处理');
return true;
}
/**
* 回复工单
*/
public function replyTicket($ticketId, $userId, $content, $isCustomer = false) {
// 检查工单状态
$ticket = $this->getTicketById($ticketId);
if (!$ticket || $ticket['status'] == 5) {
throw new Exception('工单不存在或已关闭');
}
// 添加回复
$this->db->insert('ticket_replies', [
'ticket_id' => $ticketId,
'user_id' => $userId,
'content' => $content,
'is_customer' => $isCustomer
]);
// 更新工单状态
$newStatus = $isCustomer ? 1 : 3; // 客户回复=>待处理,客服回复=>待确认
$this->updateStatus($ticketId, $newStatus);
// 通知相关方
$this->notifyRelevantParties($ticketId, $content, $isCustomer);
return true;
}
/**
* 关闭工单
*/
public function closeTicket($ticketId, $userId, $resolution) {
$this->db->update('tickets', [
'status' => 5,
'closed_at' => date('Y-m-d H:i:s')
], ['id' => $ticketId]);
$this->logAction($ticketId, $userId, 'close', null, $resolution);
// 发送满意度调查
$this->sendSatisfactionSurvey($ticketId);
return true;
}
/**
* 生成工单编号
*/
private function generateTicketNo() {
$prefix = 'TK';
$date = date('Ymd');
$random = strtoupper(substr(uniqid(), -4));
return $prefix . $date . $random;
}
/**
* 查询工单列表
*/
public function getTicketsByUser($userId, $filters = []) {
$where = ['user_id' => $userId];
if (!empty($filters['status'])) {
$where['status'] = $filters['status'];
}
return $this->db->select(
'SELECT * FROM tickets WHERE user_id = ? ORDER BY created_at DESC LIMIT 20',
[$userId]
);
}
}
控制器设计
<?php
class TicketController extends BaseController {
private $ticketManager;
private $auth;
public function __construct() {
$this->ticketManager = new TicketManager($this->db, $this->cache);
$this->auth = new AuthService();
}
/**
* 创建工单页面
*/
public function create() {
$this->requireLogin();
$categories = $this->getCategories();
$this->render('ticket/create', ['categories' => $categories]);
}
/**
* 提交工单
*/
public function store() {
$this->requireLogin();
$userId = $this->auth->getUserId();
try {
$data = $this->request->all();
$ticketId = $this->ticketManager->createTicket($userId, $data);
// 上传附件
if ($this->request->hasFile('attachments')) {
$this->uploadAttachments($ticketId);
}
$this->redirect('/tickets/' . $ticketId);
} catch (Exception $e) {
$this->setFlash('error', $e->getMessage());
$this->redirect('/tickets/create');
}
}
/**
* 工单详情
*/
public function show($ticketId) {
$this->requireLogin();
$ticket = $this->ticketManager->getTicketById($ticketId);
// 权限检查
if (!$this->canViewTicket($ticket)) {
$this->abort(403);
}
// 获取回复列表
$replies = $this->ticketManager->getTicketReplies($ticketId);
$this->render('ticket/show', [
'ticket' => $ticket,
'replies' => $replies
]);
}
/**
* 回复工单
*/
public function reply($ticketId) {
$this->requireLogin();
$userId = $this->auth->getUserId();
$content = $this->request->input('content');
$isCustomer = $this->auth->isCustomer();
try {
$this->ticketManager->replyTicket(
$ticketId,
$userId,
$content,
$isCustomer
);
$this->redirect('/tickets/' . $ticketId);
} catch (Exception $e) {
$this->setFlash('error', $e->getMessage());
$this->redirect('/tickets/' . $ticketId);
}
}
/**
* 工单列表(后台)
*/
public function adminIndex() {
$this->requireAdmin();
$filters = $this->request->all();
$tickets = $this->ticketManager->getTicketsByFilters($filters);
$this->render('ticket/admin/index', [
'tickets' => $tickets,
'filters' => $filters
]);
}
/**
* 分配工单(后台)
*/
public function assign($ticketId) {
$this->requireAgent();
$agentId = $this->request->input('agent_id');
$operatorId = $this->auth->getUserId();
try {
$this->ticketManager->assignTicket($ticketId, $agentId, $operatorId);
$this->json(['success' => true]);
} catch (Exception $e) {
$this->json(['success' => false, 'error' => $e->getMessage()]);
}
}
/**
* 关闭工单
*/
public function close($ticketId) {
$this->requireLogin();
$userId = $this->auth->getUserId();
$resolution = $this->request->input('resolution');
try {
$this->ticketManager->closeTicket($ticketId, $userId, $resolution);
$this->json(['success' => true]);
} catch (Exception $e) {
$this->json(['success' => false, 'error' => $e->getMessage()]);
}
}
/**
* 导出工单
*/
public function export() {
$this->requireAdmin();
$filters = $this->request->all();
$data = $this->ticketManager->exportTickets($filters);
// 输出CSV
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename="tickets.csv"');
$output = fopen('php://output', 'w');
foreach ($data as $row) {
fputcsv($output, $row);
}
fclose($output);
}
}
视图模板示例
工单创建页面
<!-- ticket/create.php -->
<div class="container mt-4">
<h2>提交工单</h2>
<?php if ($this->hasFlash('error')): ?>
<div class="alert alert-danger">
<?= $this->getFlash('error') ?>
</div>
<?php endif; ?>
<form action="/tickets" method="POST" enctype="multipart/form-data">
<div class="form-group">
<label>工单分类</label>
<select name="category_id" class="form-control" required>
<option value="">请选择分类</option>
<?php foreach ($categories as $category): ?>
<option value="<?= $category['id'] ?>">
<?= $category['name'] ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label>优先级</label>
<select name="priority" class="form-control">
<option value="2">高</option>
<option value="3" selected>普通</option>
<option value="4">低</option>
</select>
</div>
<div class="form-group">
<label>标题</label>
<input type="text" name="subject" class="form-control"
required maxlength="200" placeholder="请输入标题">
</div>
<div class="form-group">
<label>描述</label>
<textarea name="content" class="form-control" rows="6"
required placeholder="请详细描述您的问题"></textarea>
</div>
<div class="form-group">
<label>附件</label>
<input type="file" name="attachments[]" class="form-control-file" multiple>
</div>
<button type="submit" class="btn btn-primary">提交工单</button>
</form>
</div>
工单详情页
<!-- ticket/show.php -->
<div class="container mt-4">
<h2>工单详情</h2>
<div class="card mb-4">
<div class="card-header">
<div class="d-flex justify-content-between">
<span>工单号: <?= $ticket['ticket_no'] ?></span>
<span class="badge badge-<?= $this->getStatusBadge($ticket['status']) ?>">
<?= $this->getStatusText($ticket['status']) ?>
</span>
</div>
</div>
<div class="card-body">
<h5><?= $ticket['subject'] ?></h5>
<p><?= $ticket['content'] ?></p>
<div class="mt-3">
<small>创建时间: <?= $ticket['created_at'] ?></small><br>
<small>优先级: <?= $this->getPriorityText($ticket['priority']) ?></small>
</div>
</div>
</div>
<!-- 回复列表 -->
<div class="ticket-replies mb-4">
<h4>回复记录</h4>
<?php foreach ($replies as $reply): ?>
<div class="card mb-2">
<div class="card-body">
<div class="d-flex justify-content-between">
<strong>
<?= $reply['is_customer'] ? '客户' : '客服' ?>
</strong>
<small><?= $reply['created_at'] ?></small>
</div>
<p class="mt-2"><?= $reply['content'] ?></p>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- 回复表单 -->
<?php if ($ticket['status'] != 5): ?>
<form action="/tickets/<?= $ticket['id'] ?>/reply" method="POST">
<div class="form-group">
<textarea name="content" class="form-control" rows="4"
required placeholder="输入回复内容"></textarea>
</div>
<button type="submit" class="btn btn-primary">回复</button>
</form>
<?php endif; ?>
</div>
业务规则
状态流转
创建工单 → 待处理 → 分配/认领 → 处理中 → 回复客户 → 待确认
↓ ↓
客户满意 客户不满意
↓ ↓
已解决 重新处理
↓
关闭
权限控制
用户角色:
- 普通用户 - 提交工单,回复自己的工单
- 客服人员 - 查看所有工单,回复,修改状态
- 管理员 - 分配任务,查看统计,导出数据
权限规则:
- 用户只能查看自己的工单
- 客服人员可查看被分配及所有工单
- 只有管理员可分配任务
- 关闭的工单只能查看
自动化工单流程
<?php
class TicketAutomation {
private $ticketManager;
private $mailer;
/**
* 检测超时工单
*/
public function checkOverdueTickets() {
// 获取超过24小时未处理的工单
$overdueTickets = $this->db->query(
"SELECT * FROM tickets
WHERE status = 1 AND created_at < DATE_SUB(NOW(), INTERVAL 24 HOUR)"
);
foreach ($overdueTickets as $ticket) {
// 升级优先级
$this->ticketManager->updatePriority($ticket['id'], 1);
// 通知管理员
$this->notifyAdmin($ticket);
}
}
/**
* 自动分配工单
*/
public function autoAssign($ticketId) {
// 获取当前负载最少的客服
$agent = $this->db->queryOne(
"SELECT a.id, COUNT(t.id) as ticket_count
FROM agents a
LEFT JOIN tickets t ON t.assignee_id = a.id AND t.status != 5
WHERE a.status = 1
GROUP BY a.id
ORDER BY ticket_count ASC
LIMIT 1"
);
if ($agent) {
$this->ticketManager->assignTicket($ticketId, $agent['id']);
}
}
/**
* 定期发送工单更新提醒
*/
public function sendUpdates() {
// 获取所有进行中的工单
$activeTickets = $this->db->query(
"SELECT * FROM tickets WHERE status IN (1, 2, 3)"
);
foreach ($activeTickets as $ticket) {
$this->mailer->sendEmail(
getUserEmail($ticket['user_id']),
'工单更新提醒',
"您的工单 {$ticket['ticket_no']} 有新的进展"
);
}
}
}
性能优化
缓存策略
<?php
class TicketCache {
private $redis;
/**
* 获取工单缓存,减少数据库查询
*/
public function getTicketWithCache($ticketId) {
$cacheKey = "ticket:{$ticketId}";
if ($data = $this->redis->get($cacheKey)) {
return unserialize($data);
}
$ticket = $this->db->queryOne(
"SELECT * FROM tickets WHERE id = ?",
[$ticketId]
);
// 缓存10分钟
$this->redis->setex($cacheKey, 600, serialize($ticket));
return $ticket;
}
/**
* 清除缓存
*/
public function clearTicketCache($ticketId) {
$this->redis->delete("ticket:{$ticketId}");
$this->redis->delete("ticket:replies:{$ticketId}");
$this->redis->delete("ticket:logs:{$ticketId}");
}
/**
* 缓存工单统计数据
*/
public function getTicketStats() {
$cacheKey = 'ticket:stats';
if ($data = $this->redis->get($cacheKey)) {
return unserialize($data);
}
$stats = $this->db->query(
"SELECT
COUNT(*) as total,
SUM(status = 1) as pending,
SUM(status = 2) as processing,
SUM(status = 3) as waiting,
AVG(TIMESTAMPDIFF(HOUR, created_at,
COALESCE(closed_at, NOW()))) as avg_resolution
FROM tickets"
);
$this->redis->setex($cacheKey, 300, serialize($stats));
return $stats;
}
}
异步处理
<?php
// 使用队列处理耗时操作
class TicketTaskHandler {
private $queue;
public function handleCreate($ticketId) {
// 发送邮件通知
$this->queue->push('send_email', [
'ticket_id' => $ticketId,
'type' => 'create'
]);
// 生成PDF工单记录(如果不需要)
// $this->queue->push('generate_pdf', $ticketId);
}
public function handleReply($ticketId, $replyId) {
// 发送实时通知
$this->queue->push('push_notification', [
'ticket_id' => $ticketId,
'reply_id' => $replyId
]);
// 更新搜索索引
$this->queue->push('update_search_index', $ticketId);
}
}
安全考虑
CSRF保护
<form action="/ticket/reply/<?= $ticketId ?>" method="POST">
<input type="hidden" name="_token" value="<?= csrf_token() ?>">
<!-- 表单内容 -->
</form>
XSS防护
// 输出时使用 <?= htmlspecialchars($content, ENT_QUOTES, 'UTF-8') ?>
文件上传安全
public function validateUpload($file) {
$allowedTypes = ['jpg', 'png', 'pdf', 'doc', 'docx'];
$maxFileSize = 5 * 1024 * 1024; // 5MB
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($extension, $allowedTypes)) {
throw new Exception('不允许的文件类型');
}
if ($file['size'] > $maxFileSize) {
throw new Exception('文件大小超出限制');
}
return true;
}
敏感信息过滤
public function maskSensitiveInfo($content) {
// 过滤手机号
$content = preg_replace('/1[3-9]\d{9}/', '138****0000', $content);
// 过滤身份证号
$content = preg_replace('/\d{17}[\dXx]/', '***************', $content);
// 过滤邮箱
$content = preg_replace('/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/',
'***@***.com', $content);
return $content;
}
监控与统计
工单统计报表
<form action="/tickets/report" method="GET">
<div class="row">
<div class="col-md-3">
<input type="date" name="start_date" class="form-control">
</div>
<div class="col-md-3">
<input type="date" name="end_date" class="form-control">
</div>
<div class="col-md-3">
<select name="status" class="form-control">
<option value="">全部状态</option>
<option value="1">待处理</option>
<option value="2">处理中</option>
<option value="3">待确认</option>
<option value="4">已解决</option>
<option value="5">已关闭</option>
</select>
</div>
<button type="submit" class="btn btn-primary">生成报表</button>
</div>
</form>
实时监控面板
public function dashboard() {
$stats = $this->ticketManager->getDashboardData();
$dashboard = [
'today_tickets' => $stats['today_tickets'],
'pending' => $stats['pending'],
'processing' => $stats['processing'],
'avg_response_time' => $stats['avg_response_time'],
'avg_resolution_time' => $stats['avg_resolution_time'],
'satisfaction_rate' => $stats['satisfaction_rate'],
'trend_chart' => $this->generateChartData(),
'hot_categories' => $stats['hot_categories']
];
return $this->json($dashboard);
}
部署建议
邮件配置
config/mail.php
[
'driver' => 'smtp',
'host' => 'smtp.example.com',
'port' => 587,
'encryption' => 'tls',
'username' => 'support@example.com',
'password' => 'your_pass',
'from_address' => 'support@example.com',
'from_name' => '客服中心'
]
队列配置
config/queue.php
[
'default' => 'redis',
'connections' => [
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => 'ticket_events'
]
],
'workers' => 4
]
定时任务
# 每分钟检测超时工单 * * * * * php /path/to/server/scripts/check_overdue_tickets.php # 每小时自动分配积压工单 0 * * * * php /path/to/server/scripts/auto_assign.php # 每天凌晨发送统计报表 0 0 * * * php /path/to/server/scripts/send_statistics.php
性能优化建议
-
数据库优化
- 创建必要的索引
- 分区大表
- 读写分离
-
缓存策略
- 工单列表缓存10分钟
- 统计数据缓存5分钟
- 会话数据使用Redis
-
消息队列
- 发送邮件走队列
- 生成报表走队列
- 实时通知走WebSocket
-
CDN加速
- 静态资源使用CDN
- 图片自动压缩
这个PHP客服系统工单流程完整实现了从创建、分配到解决的整个流程,你可以根据实际需求调整优先级、状态流转和通知方式,有任何具体功能需要详细说明的,请告诉我。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播