本文目录导读:

我来帮您设计一个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);
}
}