PHP 工单系统集成

wen PHP项目 2

本文目录导读:

PHP 工单系统集成

  1. 数据库设计
  2. 核心类文件
  3. API接口文件
  4. 前端示例
  5. 邮件通知集成

我来帮您设计一个PHP工单系统的集成方案,这是一个完整的企业级工单系统解决方案。

数据库设计

-- 工单主表
CREATE TABLE `tickets` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `ticket_no` varchar(30) NOT NULL COMMENT '工单编号',
  `user_id` bigint(20) NOT NULL COMMENT '创建人ID',
  `department_id` bigint(20) DEFAULT NULL COMMENT '部门ID',
  `category_id` bigint(20) DEFAULT NULL COMMENT '分类ID',
  `priority` tinyint(4) NOT NULL DEFAULT '2' COMMENT '优先级 1紧急 2高 3中 4低',
  `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态 1待处理 2处理中 3已解决 4已关闭 5已驳回',
  `subject` varchar(200) NOT NULL COMMENT '标题',
  `content` text COMMENT '内容',
  `attachment` text COMMENT '附件路径',
  `assignee_id` bigint(20) DEFAULT NULL COMMENT '处理人ID',
  `due_time` datetime DEFAULT NULL COMMENT '截止时间',
  `closed_time` datetime DEFAULT NULL COMMENT '关闭时间',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ticket_no` (`ticket_no`),
  KEY `user_id` (`user_id`),
  KEY `assignee_id` (`assignee_id`),
  KEY `status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 工单分类表
CREATE TABLE `ticket_categories` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL COMMENT '分类名称',
  `parent_id` bigint(20) DEFAULT '0' COMMENT '父级ID',
  `sort_order` int(11) DEFAULT '0' COMMENT '排序',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 工单回复表
CREATE TABLE `ticket_replies` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `ticket_id` bigint(20) NOT NULL,
  `user_id` bigint(20) NOT NULL COMMENT '回复人',
  `content` text NOT NULL,
  `is_internal` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否内部备注',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `ticket_id` (`ticket_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` bigint(20) NOT NULL,
  `action` varchar(100) NOT NULL COMMENT '操作',
  `detail` text COMMENT '详情',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `ticket_id` (`ticket_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 已读状态表
CREATE TABLE `ticket_reads` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `ticket_id` bigint(20) NOT NULL,
  `user_id` bigint(20) NOT NULL,
  `last_read_time` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_ticket_user` (`ticket_id`,`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

核心类文件

TicketService.php

<?php
class TicketService {
    private $db;
    private $cache;
    public function __construct($db, $cache = null) {
        $this->db = $db;
        $this->cache = $cache;
    }
    /**
     * 创建工单
     */
    public function createTicket($userId, $data) {
        // 验证数据
        $this->validateTicketData($data);
        // 生成工单编号
        $ticketNo = $this->generateTicketNo();
        // 处理附件
        $attachments = $this->processAttachments($_FILES['attachments'] ?? []);
        // 入库
        $sql = "INSERT INTO tickets (ticket_no, user_id, department_id, category_id, 
                priority, subject, content, attachment, due_time) 
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
        $this->db->prepare($sql)->execute([
            $ticketNo,
            $userId,
            $data['department_id'] ?? null,
            $data['category_id'] ?? null,
            $data['priority'] ?? 2,
            $data['subject'],
            $data['content'],
            json_encode($attachments),
            $data['due_time'] ?? null
        ]);
        $ticketId = $this->db->lastInsertId();
        // 记录日志
        $this->addLog($ticketId, $userId, 'create', '创建工单');
        // 发送通知
        $this->notifyNewTicket($ticketId);
        // 清除缓存
        $this->clearCache($userId);
        return $ticketId;
    }
    /**
     * 更新工单
     */
    public function updateTicket($ticketId, $userId, $data) {
        // 检查权限
        $ticket = $this->getTicket($ticketId);
        if (!$ticket || $ticket['user_id'] != $userId) {
            throw new Exception('没有权限操作此工单');
        }
        // 更新数据
        $updateFields = [];
        $updateParams = [];
        if (isset($data['subject'])) {
            $updateFields[] = "subject = ?";
            $updateParams[] = $data['subject'];
        }
        if (isset($data['content'])) {
            $updateFields[] = "content = ?";
            $updateParams[] = $data['content'];
        }
        if (isset($data['priority'])) {
            $updateFields[] = "priority = ?";
            $updateParams[] = $data['priority'];
        }
        if (empty($updateFields)) {
            return false;
        }
        $updateParams[] = $ticketId;
        $sql = "UPDATE tickets SET " . implode(', ', $updateFields) . " WHERE id = ?";
        $this->db->prepare($sql)->execute($updateParams);
        // 记录日志
        $this->addLog($ticketId, $userId, 'update', '更新工单');
        // 清除缓存
        $this->clearCache($userId);
        return true;
    }
    /**
     * 分配工单
     */
    public function assignTicket($ticketId, $operatorId, $assigneeId) {
        // 检查权限
        if (!$this->isOperator($operatorId)) {
            throw new Exception('无权操作');
        }
        $sql = "UPDATE tickets SET assignee_id = ?, status = 2, updated_at = NOW() WHERE id = ?";
        $this->db->prepare($sql)->execute([$assigneeId, $ticketId]);
        // 记录日志
        $this->addLog($ticketId, $operatorId, 'assign', "分配给用户ID: $assigneeId");
        // 通知处理人
        $this->notifyAssignment($ticketId, $assigneeId);
        return true;
    }
    /**
     * 添加回复
     */
    public function addReply($ticketId, $userId, $content, $isInternal = false) {
        $sql = "INSERT INTO ticket_replies (ticket_id, user_id, content, is_internal) 
                VALUES (?, ?, ?, ?)";
        $this->db->prepare($sql)->execute([$ticketId, $userId, $content, $isInternal]);
        // 更新工单状态为处理中
        if (!$isInternal) {
            $sql = "UPDATE tickets SET status = 2, updated_at = NOW() WHERE id = ?";
            $this->db->prepare($sql)->execute([$ticketId]);
        }
        // 记录日志
        $this->addLog($ticketId, $userId, 'reply', $isInternal ? '添加备注' : '回复工单');
        // 发送通知
        $this->notifyNewReply($ticketId, $userId);
        return $this->db->lastInsertId();
    }
    /**
     * 获取工单列表
     */
    public function getTickets($filters = [], $page = 1, $limit = 20) {
        $where = [];
        $params = [];
        if (isset($filters['user_id'])) {
            $where[] = "user_id = ?";
            $params[] = $filters['user_id'];
        }
        if (isset($filters['status'])) {
            $where[] = "status = ?";
            $params[] = $filters['status'];
        }
        if (isset($filters['assignee_id'])) {
            $where[] = "assignee_id = ?";
            $params[] = $filters['assignee_id'];
        }
        if (isset($filters['priority'])) {
            $where[] = "priority = ?";
            $params[] = $filters['priority'];
        }
        if (isset($filters['keyword'])) {
            $where[] = "(subject LIKE ? OR content LIKE ? OR ticket_no LIKE ?)";
            $keyword = "%{$filters['keyword']}%";
            $params[] = $keyword;
            $params[] = $keyword;
            $params[] = $keyword;
        }
        $whereSql = count($where) > 0 ? "WHERE " . implode(" AND ", $where) : "";
        $offset = ($page - 1) * $limit;
        $sql = "SELECT t.*, u.name as user_name, d.name as department_name, 
                c.name as category_name, ua.name as assignee_name
                FROM tickets t
                LEFT JOIN users u ON t.user_id = u.id
                LEFT JOIN departments d ON t.department_id = d.id
                LEFT JOIN ticket_categories c ON t.category_id = c.id
                LEFT JOIN users ua ON t.assignee_id = ua.id
                $whereSql
                ORDER BY t.created_at DESC
                LIMIT $offset, $limit";
        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    /**
     * 获取工单详情
     */
    public function getTicket($ticketId, $userId = null) {
        // 尝试从缓存获取
        if ($this->cache) {
            $cached = $this->cache->get("ticket:{$ticketId}");
            if ($cached) {
                return json_decode($cached, true);
            }
        }
        $sql = "SELECT t.*, u.name as user_name, d.name as department_name,
                c.name as category_name, ua.name as assignee_name
                FROM tickets t
                LEFT JOIN users u ON t.user_id = u.id
                LEFT JOIN departments d ON t.department_id = d.id
                LEFT JOIN ticket_categories c ON t.category_id = c.id
                LEFT JOIN users ua ON t.assignee_id = ua.id
                WHERE t.id = ?";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$ticketId]);
        $ticket = $stmt->fetch(PDO::FETCH_ASSOC);
        if ($ticket) {
            // 获取回复列表
            $ticket['replies'] = $this->getTicketReplies($ticketId, $userId);
            // 获取日志
            $ticket['logs'] = $this->getTicketLogs($ticketId);
            // 标记已读
            if ($userId) {
                $this->markAsRead($ticketId, $userId);
            }
            // 存入缓存
            if ($this->cache) {
                $this->cache->set("ticket:{$ticketId}", json_encode($ticket), 3600);
            }
        }
        return $ticket;
    }
    /**
     * 更改工单状态
     */
    public function changeStatus($ticketId, $userId, $newStatus, $comment = '') {
        // 检查权限
        $ticket = $this->getTicket($ticketId);
        if (!$ticket || ($ticket['user_id'] != $userId && $ticket['assignee_id'] != $userId 
            && !$this->isOperator($userId))) {
            throw new Exception('没有权限操作此工单');
        }
        $validTransitions = [
            1 => [2, 3, 4],  // 待处理 -> 处理中/已解决/已关闭
            2 => [3, 4],     // 处理中 -> 已解决/已关闭
            3 => [2, 4],     // 已解决 -> 处理中/已关闭
        ];
        if (!isset($validTransitions[$ticket['status']]) || 
            !in_array($newStatus, $validTransitions[$ticket['status']])) {
            throw new Exception('无效的状态转换');
        }
        $closedTime = $newStatus == 3 ? date('Y-m-d H:i:s') : null;
        $sql = "UPDATE tickets SET status = ?, closed_time = ?, updated_at = NOW() WHERE id = ?";
        $this->db->prepare($sql)->execute([$newStatus, $closedTime, $ticketId]);
        // 更新工单状态时添加备注
        if (!empty($comment)) {
            $this->addReply($ticketId, $userId, $comment, true);
        }
        // 记录日志
        $statusNames = [1=>'待处理', 2=>'处理中', 3=>'已解决', 4=>'已关闭', 5=>'已驳回'];
        $this->addLog($ticketId, $userId, 'status_change', 
            "状态从 '{$statusNames[$ticket['status']]}' 变为 '{$statusNames[$newStatus]}'");
        // 清除缓存
        $this->clearCache($ticket['user_id'], $ticketId);
        return true;
    }
    /**
     * 生成工单编号
     */
    private function generateTicketNo() {
        $date = date('Ymd');
        $sql = "SELECT COUNT(*) FROM tickets WHERE ticket_no LIKE '%$date%'";
        $count = $this->db->query($sql)->fetchColumn();
        $seq = str_pad($count + 1, 4, '0', STR_PAD_LEFT);
        return "TK{$date}{$seq}";
    }
    /**
     * 处理上传的附件
     */
    private function processAttachments($files) {
        $attachments = [];
        if (empty($files)) return $attachments;
        foreach ($files['tmp_name'] as $key => $tmpName) {
            if ($files['error'][$key] === UPLOAD_ERR_OK) {
                $filename = time() . '_' . uniqid() . '_' . $files['name'][$key];
                $year_month = date('Ym');
                $uploadDir = UPLOAD_PATH . '/tickets/' . $year_month . '/';
                if (!file_exists($uploadDir)) {
                    mkdir($uploadDir, 0755, true);
                }
                if (move_uploaded_file($tmpName, $uploadDir . $filename)) {
                    $attachments[] = [
                        'name' => $files['name'][$key],
                        'path' => "/uploads/tickets/{$year_month}/{$filename}",
                        'size' => $files['size'][$key],
                        'type' => $files['type'][$key]
                    ];
                }
            }
        }
        return $attachments;
    }
    /**
     * 获取工单回复
     */
    private function getTicketReplies($ticketId, $currentUserId = null) {
        $sql = "SELECT r.*, u.name as user_name, u.avatar 
                FROM ticket_replies r
                JOIN users u ON r.user_id = u.id
                WHERE r.ticket_id = ? 
                AND (r.is_internal = 0 OR r.user_id = ? OR ? IN 
                    (SELECT id FROM users WHERE role = 'admin') OR
                    ? IN (SELECT assignee_id FROM tickets WHERE id = ?))
                ORDER BY r.created_at DESC";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$ticketId, $currentUserId, $currentUserId, $currentUserId, $ticketId]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    /**
     * 添加操作日志
     */
    private function addLog($ticketId, $userId, $action, $detail = '') {
        $sql = "INSERT INTO ticket_logs (ticket_id, user_id, action, detail) 
                VALUES (?, ?, ?, ?)";
        $this->db->prepare($sql)->execute([$ticketId, $userId, $action, $detail]);
    }
    /**
     * 获取工单日志
     */
    private function getTicketLogs($ticketId) {
        $sql = "SELECT l.*, u.name as user_name 
                FROM ticket_logs l
                LEFT JOIN users u ON l.user_id = u.id
                WHERE l.ticket_id = ?
                ORDER BY l.created_at DESC";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$ticketId]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    /**
     * 标记已读
     */
    private function markAsRead($ticketId, $userId) {
        $sql = "INSERT INTO ticket_reads (ticket_id, user_id, last_read_time) 
                VALUES (?, ?, NOW())
                ON DUPLICATE KEY UPDATE last_read_time = NOW()";
        $this->db->prepare($sql)->execute([$ticketId, $userId]);
    }
    /**
     * 发送新工单通知
     */
    private function notifyNewTicket($ticketId) {
        $ticket = $this->getTicket($ticketId);
        // 通知管理员
        $adminIds = $this->getAdminUserIds();
        $this->sendNotification($adminIds, '新工单', "您有新工单 #{$ticket['ticket_no']}");
        // 如果分配了处理人,通知处理人
        if ($ticket['assignee_id']) {
            $this->sendNotification([$ticket['assignee_id']], '新工单分配', 
                "您被分配了新工单 #{$ticket['ticket_no']}");
        }
    }
    /**
     * 清除缓存
     */
    private function clearCache($userId, $ticketId = null) {
        if (!$this->cache) return;
        // 清除用户相关缓存
        $this->cache->delete("user:tickets:{$userId}");
        // 清除工单缓存
        if ($ticketId) {
            $this->cache->delete("ticket:{$ticketId}");
        }
    }
    /**
     * 校验工单数据
     */
    private function validateTicketData($data) {
        if (empty($data['subject'])) {
            throw new InvalidArgumentException('工单标题不能为空');
        }
        if (empty($data['content'])) {
            throw new InvalidArgumentException('工单内容不能为空');
        }
        if (!in_array($data['priority'] ?? 2, [1, 2, 3, 4])) {
            throw new InvalidArgumentException('无效的优先级');
        }
    }
}

API接口文件

api/tickets.php

<?php
require_once '../includes/init.php';
require_once '../lib/TicketService.php';
header('Content-Type: application/json');
$auth = new Auth();
$auth->checkAuth();
$method = $_SERVER['REQUEST_METHOD'];
$service = new TicketService($db, $cache);
try {
    switch ($method) {
        case 'GET':
            if (isset($_GET['id'])) {
                // 获取单个工单
                $result = $service->getTicket($_GET['id'], $_SESSION['user_id']);
            } else {
                // 获取工单列表
                $filters = [
                    'user_id' => $_GET['user_id'] ?? null,
                    'status' => $_GET['status'] ?? null,
                    'assignee_id' => $_GET['assignee_id'] ?? null,
                    'priority' => $_GET['priority'] ?? null,
                    'keyword' => $_GET['keyword'] ?? null
                ];
                $page = $_GET['page'] ?? 1;
                $limit = $_GET['limit'] ?? 20;
                $result = $service->getTickets($filters, $page, $limit);
            }
            break;
        case 'POST':
            // 创建工单
            $result = $service->createTicket($_SESSION['user_id'], $_POST);
            break;
        case 'PUT':
            // 更新工单
            $data = json_decode(file_get_contents('php://input'), true);
            $result = $service->updateTicket($data['id'], $_SESSION['user_id'], $data);
            break;
        case 'DELETE':
            // 关闭工单
            $data = json_decode(file_get_contents('php://input'), true);
            $result = $service->changeStatus($data['id'], $_SESSION['user_id'], 4);
            break;
        default:
            http_response_code(405);
            $result = ['error' => 'Method not allowed'];
    }
    echo json_encode([
        'success' => true,
        'data' => $result
    ]);
} catch (Exception $e) {
    http_response_code(400);
    echo json_encode([
        'success' => false,
        'error' => $e->getMessage()
    ]);
}

前端示例

tickets.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">工单系统</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container-fluid py-4">
    <div class="row">
        <div class="col-12 mb-4">
            <h2>工单管理系统</h2>
        </div>
        <!-- 筛选栏 -->
        <div class="col-12 mb-3">
            <form class="row g-2" id="filterForm">
                <div class="col-auto">
                    <select class="form-select" name="status" id="statusFilter">
                        <option value="">全部状态</option>
                        <option value="1">待处理</option>
                        <option value="2">处理中</option>
                        <option value="3">已解决</option>
                        <option value="4">已关闭</option>
                    </select>
                </div>
                <div class="col-auto">
                    <select class="form-select" name="priority" id="priorityFilter">
                        <option value="">全部优先级</option>
                        <option value="1">紧急</option>
                        <option value="2">高</option>
                        <option value="3">中</option>
                        <option value="4">低</option>
                    </select>
                </div>
                <div class="col-auto">
                    <input type="text" class="form-control" name="keyword" id="keyword" 
                           placeholder="搜索关键词">
                </div>
                <div class="col-auto">
                    <button type="submit" class="btn btn-primary">筛选</button>
                    <button type="button" class="btn btn-success" onclick="createTicketModal()">
                        新建工单
                    </button>
                </div>
            </form>
        </div>
        <!-- 工单列表 -->
        <div class="col-12">
            <div class="card">
                <div class="card-body">
                    <table class="table table-hover">
                        <thead>
                            <tr>
                                <th>工单编号</th>
                                <th>标题</th>
                                <th>优先级</th>
                                <th>状态</th>
                                <th>负责人</th>
                                <th>创建时间</th>
                                <th>操作</th>
                            </tr>
                        </thead>
                        <tbody id="ticketList">
                        </tbody>
                    </table>
                    <!-- 分页 -->
                    <nav>
                        <ul class="pagination" id="pagination">
                        </ul>
                    </nav>
                </div>
            </div>
        </div>
    </div>
</div>
<!-- 新建工单模态框 -->
<div class="modal fade" id="createTicketModal" tabindex="-1">
    <div class="modal-dialog modal-lg">
        <div class="modal-content">
            <form id="ticketForm">
                <div class="modal-header">
                    <h5 class="modal-title">创建新工单</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
                </div>
                <div class="modal-body">
                    <div class="mb-3">
                        <label class="form-label">标题 *</label>
                        <input type="text" class="form-control" name="subject" required>
                    </div>
                    <div class="mb-3">
                        <label class="form-label">优先级</label>
                        <select class="form-select" name="priority">
                            <option value="2">高</option>
                            <option value="3" selected>中</option>
                            <option value="4">低</option>
                        </select>
                    </div>
                    <div class="mb-3">
                        <label class="form-label">分类</label>
                        <select class="form-select" name="category_id">
                            <!-- 动态加载分类 -->
                        </select>
                    </div>
                    <div class="mb-3">
                        <label class="form-label">内容描述 *</label>
                        <textarea class="form-control" name="content" rows="5" required></textarea>
                    </div>
                    <div class="mb-3">
                        <label class="form-label">附件</label>
                        <input type="file" class="form-control" name="attachments[]" multiple>
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="submit" class="btn btn-primary">提交工单</button>
                </div>
            </form>
        </div>
    </div>
</div>
<!-- JavaScript -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
$(document).ready(function() {
    loadTickets();
    // 表单筛选
    $('#filterForm').on('submit', function(e) {
        e.preventDefault();
        loadTickets(1);
    });
    // 创建工单表单提交
    $('#ticketForm').on('submit', function(e) {
        e.preventDefault();
        submitTicket();
    });
});
// 加载工单列表
function loadTickets(page = 1) {
    const filters = {
        status: $('#statusFilter').val(),
        priority: $('#priorityFilter').val(),
        keyword: $('#keyword').val(),
        page: page
    };
    $.ajax({
        url: '/api/tickets.php',
        type: 'GET',
        data: filters,
        success: function(response) {
            renderTickets(response.data);
            renderPagination(response.total, response.page, response.pages);
        },
        error: function(xhr) {
            showError(xhr.responseJSON?.error || '加载失败');
        }
    });
}
// 渲染工单列表
function renderTickets(tickets) {
    const tbody = $('#ticketList');
    tbody.empty();
    if (tickets.length === 0) {
        tbody.html('<tr><td colspan="7" class="text-center">暂无工单</td></tr>');
        return;
    }
    tickets.forEach(ticket => {
        const priorityClass = getPriorityClass(ticket.priority);
        const statusClass = getStatusClass(ticket.status);
        tbody.append(`
            <tr>
                <td><a href="#" onclick="viewTicket(${ticket.id});return false;">${ticket.ticket_no}</a></td>
                <td>${escapeHtml(ticket.subject)}</td>
                <td><span class="badge ${priorityClass}">${getPriorityText(ticket.priority)}</span></td>
                <td><span class="badge ${statusClass}">${getStatusText(ticket.status)}</span></td>
                <td>${escapeHtml(ticket.assignee_name || '未分配')}</td>
                <td>${new Date(ticket.created_at).toLocaleString()}</td>
                <td>
                    <button class="btn btn-sm btn-primary" 
                            onclick="viewTicket(${ticket.id})">查看</button>
                </td>
            </tr>
        `);
    });
}
// 查看工单详情
function viewTicket(id) {
    window.location.href = `/ticket_detail.php?id=${id}`;
}
// 创建工单模态框
function createTicketModal() {
    loadCategories();
    $('#createTicketModal').modal('show');
}
// 提交工单
function submitTicket() {
    const formData = new FormData($('#ticketForm')[0]);
    $.ajax({
        url: '/api/tickets.php',
        type: 'POST',
        data: formData,
        processData: false,
        contentType: false,
        success: function(response) {
            $('#createTicketModal').modal('hide');
            $('#ticketForm')[0].reset();
            showSuccess('工单创建成功');
            loadTickets();
        },
        error: function(xhr) {
            const error = xhr.responseJSON?.error || '创建工单失败';
            showError(error);
        }
    });
}
// 加载分类
function loadCategories() {
    $.get('/api/categories.php', function(response) {
        const select = $('select[name="category_id"]');
        select.empty();
        select.append('<option value="">请选择分类</option>');
        response.data.forEach(category => {
            select.append(`<option value="${category.id}">${category.name}</option>`);
        });
    });
}
// 工具函数
function getPriorityClass(priority) {
    const classMap = {1: 'bg-danger', 2: 'bg-warning', 3: 'bg-info', 4: 'bg-secondary'};
    return classMap[priority] || 'bg-secondary';
}
function getStatusClass(status) {
    const classMap = {1: 'bg-warning', 2: 'bg-info', 3: 'bg-success', 4: 'bg-secondary'};
    return classMap[status] || 'bg-secondary';
}
function getPriorityText(priority) {
    const textMap = {1: '紧急', 2: '高', 3: '中', 4: '低'};
    return textMap[priority] || '未知';
}
function getStatusText(status) {
    const textMap = {1: '待处理', 2: '处理中', 3: '已解决', 4: '已关闭'};
    return textMap[status] || '未知';
}
function escapeHtml(text) {
    if (!text) return '';
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}
function showSuccess(message) {
    // 实现通知提示
    alert(message);
}
function showError(message) {
    // 实现错误提示
    alert(message);
}
// 分页渲染
function renderPagination(total, current, pages) {
    const pagination = $('#pagination');
    pagination.empty();
    if (pages <= 1) return;
    // 上一页
    pagination.append(`
        <li class="page-item ${current <= 1 ? 'disabled' : ''}">
            <a class="page-link" href="#" onclick="loadTickets(${current - 1});return false;">上一页</a>
        </li>
    `);
    // 页码
    for (let i = 1; i <= pages; i++) {
        pagination.append(`
            <li class="page-item ${i == current ? 'active' : ''}">
                <a class="page-link" href="#" onclick="loadTickets(${i});return false;">${i}</a>
            </li>
        `);
    }
    // 下一页
    pagination.append(`
        <li class="page-item ${current >= pages ? 'disabled' : ''}">
            <a class="page-link" href="#" onclick="loadTickets(${current + 1});return false;">下一页</a>
        </li>
    `);
}
</script>
</body>
</html>

邮件通知集成

EmailNotifier.php

<?php
class EmailNotifier {
    private $mailer;
    public function __construct($config) {
        // 初始化邮件发送器 (使用 PHPMailer 或 SwiftMailer)
        $this->mailer = new PHPMailer\PHPMailer\PHPMailer();
        $this->mailer->isSMTP();
        $this->mailer->Host = $config['host'];
        $this->mailer->SMTPAuth = true;
        $this->mailer->Username = $config['username'];
        $this->mailer->Password = $config['password'];
        $this->mailer->SMTPSecure = 'tls';
        $this->mailer->Port = $config['port'];
        $this->mailer->setFrom($config['from_email'], $config['from_name']);
    }
    /**
     * 发送新工单通知
     */
    public function sendNewTicketNotification($ticket, $assignee) {
        $subject = "新工单通知: {$ticket['ticket_no']}";
        $htmlBody = "
            <h2>新工单 #{ $ticket['ticket_no'] }</h2>
            <p><strong>标题:</strong> {$ticket['subject']}</p>
            <p><strong>优先级:</strong> {$this->getPriorityText($ticket['priority'])}</p>
            <p><strong>内容:</strong></p>
            <div>{$ticket['content']}</div>
            <a href='https://yoursite.com/tickets/{$ticket['id']}'>查看工单</a>
        ";
        $this->sendEmail($assignee['email'], $assignee['name'], $subject, $htmlBody);
    }
    /**
     * 发送回复通知
     */
    public function sendReplyNotification($ticket, $reply, $recipients) {
        $subject = "工单回复: {$ticket['ticket_no']}";
        $htmlBody = "
            <h2>工单 #{$ticket['ticket_no']} 有新回复</h2>
            <p><strong>回复人:</strong> {$reply['user_name']}</p>
            <p><strong>时间:</strong> {$reply['created_at']}</p>
            <div>{$reply['content']}</div>
            <a href='https://yoursite.com/tickets/{$ticket['id']}'>查看详情</a>
        ";
        foreach ($recipients as $recipient) {
            $this->sendEmail($recipient['email'], $recipient['name'], $subject, $htmlBody);
        }
    }

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