PHP项目客服系统工单流程

wen PHP项目 4

本文目录导读:

PHP项目客服系统工单流程

  1. 整体架构设计
  2. 核心类设计
  3. 视图模板示例
  4. 业务规则
  5. 性能优化
  6. 安全考虑
  7. 监控与统计
  8. 部署建议
  9. 性能优化建议

我来为你设计一个完整的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

性能优化建议

  1. 数据库优化

    • 创建必要的索引
    • 分区大表
    • 读写分离
  2. 缓存策略

    • 工单列表缓存10分钟
    • 统计数据缓存5分钟
    • 会话数据使用Redis
  3. 消息队列

    • 发送邮件走队列
    • 生成报表走队列
    • 实时通知走WebSocket
  4. CDN加速

    • 静态资源使用CDN
    • 图片自动压缩

这个PHP客服系统工单流程完整实现了从创建、分配到解决的整个流程,你可以根据实际需求调整优先级、状态流转和通知方式,有任何具体功能需要详细说明的,请告诉我。

抱歉,评论功能暂时关闭!