本文目录导读:

我来帮您设计一个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);
}
}
版权声明: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直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播