query('SELECT * FROM ' . tn('ticket_departments') . ' ORDER BY sort, id')->fetchAll();
$deptMap = [];
foreach ($depts as $d) { $deptMap[$d['id']] = $d['name']; }
$validStatus = ['open', 'pending', 'resolved', 'closed'];
// 重新开启已关闭工单(快捷按钮)
if (isset($_POST['reopen'])) {
verifyCsrf();
$id = (int) ($_POST['id'] ?? 0);
$stmt = $db->prepare('SELECT id FROM ' . tn('tickets') . ' WHERE id = ?');
$stmt->execute([$id]);
if ($stmt->fetch()) {
$db->prepare('UPDATE ' . tn('tickets') . ' SET status=?, updated_at=NOW(), closed_at=NULL WHERE id=?')
->execute(['open', $id]);
$msg = '工单已重新开启。';
} else {
$err = '工单不存在';
}
}
// 分配负责人(仅管理员可操作;客服仅能看自己负责的工单)
if (isset($_POST['assign']) && $isAdmin) {
verifyCsrf();
$id = (int) ($_POST['id'] ?? 0);
$toUid = (int) ($_POST['assignee_id'] ?? 0);
if ($id <= 0) {
$err = '工单不存在';
} else {
$staffCheck = $db->prepare('SELECT id FROM ' . tn('users') . ' WHERE id = ? AND role IN (\'admin\',\'cs\') AND status = 1');
$staffCheck->execute([$toUid]);
$realUid = $toUid > 0 && $staffCheck->fetch() ? $toUid : null;
$db->prepare('UPDATE ' . tn('tickets') . ' SET assignee_id = ? WHERE id = ?')
->execute([$realUid, $id]);
$msg = $realUid ? '已分配负责人。' : '已取消分配(工单转为未分配,仅管理员可见)。';
}
}
// 处理提交(追加回复 / 状态变更)
if (isset($_POST['reply'])) {
verifyCsrf();
$id = (int) ($_POST['id'] ?? 0);
$status = trim($_POST['status'] ?? 'open');
$reply = trim($_POST['admin_reply'] ?? '');
$notify = !empty($_POST['notify']);
// 客服仅能处理分配给自己的工单
if (!$isAdmin) {
$chk = $db->prepare('SELECT assignee_id FROM ' . tn('tickets') . ' WHERE id = ?');
$chk->execute([$id]);
$ar = $chk->fetch();
if (!$ar || (int) $ar['assignee_id'] !== (int) $me['id']) {
$err = '该工单未分配给你,无权操作。';
$id = 0;
}
}
if (!in_array($status, $validStatus, true)) {
$err = '状态不合法';
} else {
$stmt = $db->prepare('SELECT t.*, u.email AS user_email, u.username AS user_name FROM ' . tn('tickets') . ' t LEFT JOIN ' . tn('users') . ' u ON u.id = t.user_id WHERE t.id = ?');
$stmt->execute([$id]);
$t = $stmt->fetch();
if (!$t) {
$err = '工单不存在';
} else {
$adminId = $me['id'];
$closedAt = $status === 'closed' ? date('Y-m-d H:i:s') : null;
if ($reply !== '') {
$db->prepare('INSERT INTO ' . tn('ticket_replies') . ' (ticket_id, user_id, is_admin, message, created_at) VALUES (?,?,?,?,NOW())')
->execute([$id, $adminId, 1, $reply]);
}
$db->prepare('UPDATE ' . tn('tickets') . ' SET status=?, admin_id=?, updated_at=NOW(), closed_at=? WHERE id=?')
->execute([$status, $adminId, $closedAt, $id]);
if ($notify && $t['user_email'] !== '') {
$base = siteBaseUrl();
$url = rtrim($base, '/') . '/mytickets.php?view=' . $id;
$subject = '[' . SITE_NAME . '] 您的工单 #' . $id . ' 有新回复';
$body = '
'
. '
工单处理通知 '
. '
您好 ' . h($t['user_name']) . ',您提交的工单已更新:
'
. '
工单标题: ' . h($t['subject']) . '
'
. '
当前状态: ' . ticketStatusLabel($status) . '
'
. ($reply !== '' ? '
' . nl2br(h($reply)) . '
' : '')
. '
查看工单详情
'
. '
';
$r = fnwSendMail($t['user_email'], $subject, $body);
if (!$r['ok']) {
$msg = '工单已保存,但邮件通知发送失败:' . $r['error'];
} else {
$msg = '工单已保存,并已邮件通知用户。';
}
} else {
$msg = '工单已保存。';
}
}
}
}
// 批准续期(续期工单专用)
if (isset($_POST['approve_renew'])) {
verifyCsrf();
if (!$isAdmin) {
$err = '仅管理员可批准续期。';
} else {
$id = (int) ($_POST['id'] ?? 0);
$reply = trim($_POST['admin_reply'] ?? '');
$stmt = $db->prepare('SELECT t.*, o.expires_at, o.period_days, u.email AS user_email, u.username AS user_name FROM ' . tn('tickets') . ' t LEFT JOIN ' . tn('orders') . ' o ON o.id = t.order_id LEFT JOIN ' . tn('users') . ' u ON u.id = t.user_id WHERE t.id = ?');
$stmt->execute([$id]);
$t = $stmt->fetch();
if (!$t || $t['type'] !== 'renewal' || !$t['order_id']) {
$err = '该工单不是有效的续期申请';
} elseif (empty($t['period_days']) || $t['period_days'] <= 0) {
$err = '关联订单无有效周期,无法续期';
} else {
// 从当前到期(未过期则顺延)或现在起,延长一个周期
$base = ($t['expires_at'] && !isExpired($t['expires_at'])) ? $t['expires_at'] : date('Y-m-d H:i:s');
$newExpiry = date('Y-m-d H:i:s', strtotime($base . ' +' . (int)$t['period_days'] . ' days'));
$db->prepare('UPDATE ' . tn('orders') . ' SET expires_at = ? WHERE id = ?')->execute([$newExpiry, $t['order_id']]);
$reply = $reply !== '' ? $reply : ('已为你续期,新到期时间:' . $newExpiry);
$adminId = $me['id'];
$db->prepare('INSERT INTO ' . tn('ticket_replies') . ' (ticket_id, user_id, is_admin, message, created_at) VALUES (?,?,?,?,NOW())')
->execute([$id, $adminId, 1, $reply]);
$db->prepare('UPDATE ' . tn('tickets') . ' SET admin_reply=?, status=?, admin_id=?, updated_at=NOW(), closed_at=NULL WHERE id=?')
->execute([$reply, 'resolved', $adminId, $id]);
$msg = '已批准续期,关联订单 #' . $t['order_id'] . ' 到期时间已延长至 ' . $newExpiry;
if (!empty($t['user_email'])) {
$baseUrl = siteBaseUrl();
$url = rtrim($baseUrl, '/') . '/mytickets.php?view=' . $id;
$subject = '[' . SITE_NAME . '] 你的续期申请已通过(订单 #' . $t['order_id'] . ')';
$body = ''
. '
续期成功 '
. '
你好 ' . h($t['user_name']) . ',你的续期申请已通过:
'
. '
关联订单: #' . (int)$t['order_id'] . '
'
. '
新到期时间: ' . h($newExpiry) . '
'
. '
' . nl2br(h($reply)) . '
'
. '
查看工单
'
. '
可在「我的订单 → 详情」中查看完整凭据。
';
fnwSendMail($t['user_email'], $subject, $body);
}
}
}
}
// 列表筛选
$filterStatus = trim($_GET['status'] ?? '');
$filterDept = isset($_GET['dept']) ? (int) $_GET['dept'] : 0;
$where = [];
$params = [];
if ($filterStatus !== '') { $where[] = 't.status = ?'; $params[] = $filterStatus; }
if ($filterDept > 0) { $where[] = 't.dept_id = ?'; $params[] = $filterDept; }
// 客服仅能看到分配给自己的工单
if (!$isAdmin) {
$where[] = 't.assignee_id = ?';
$params[] = $me['id'];
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$totalStmt = $db->prepare('SELECT COUNT(*) FROM ' . tn('tickets') . ' t ' . $whereSql);
$totalStmt->execute($params);
$total = $totalStmt->fetchColumn();
$stmt = $db->prepare(
'SELECT t.*, u.username AS user_name, u.email AS user_email, d.name AS dept_name,
a.username AS assignee_name
FROM ' . tn('tickets') . ' t
LEFT JOIN ' . tn('users') . ' u ON u.id = t.user_id
LEFT JOIN ' . tn('ticket_departments') . ' d ON d.id = t.dept_id
LEFT JOIN ' . tn('users') . ' a ON a.id = t.assignee_id
' . $whereSql . '
ORDER BY t.updated_at DESC'
);
$stmt->execute($params);
$list = $stmt->fetchAll();
// 查看单条
$view = isset($_GET['view']) ? (int) $_GET['view'] : 0;
$ticket = null;
$replies = [];
if ($view > 0) {
$stmt = $db->prepare(
'SELECT t.*, u.username AS user_name, u.email AS user_email, d.name AS dept_name,
a.username AS assignee_name
FROM ' . tn('tickets') . ' t
LEFT JOIN ' . tn('users') . ' u ON u.id = t.user_id
LEFT JOIN ' . tn('ticket_departments') . ' d ON d.id = t.dept_id
LEFT JOIN ' . tn('users') . ' a ON a.id = t.assignee_id
WHERE t.id = ?'
);
$stmt->execute([$view]);
$ticket = $stmt->fetch();
// 客服仅能查看分配给自己的工单
if ($ticket && !$isAdmin && (int) $ticket['assignee_id'] !== (int) $me['id']) {
$ticket = null;
$err = '该工单未分配给你,无权查看。';
}
if ($ticket) {
$rstmt = $db->prepare(
'SELECT r.*, u.username FROM ' . tn('ticket_replies') . ' r
LEFT JOIN ' . tn('users') . ' u ON u.id = r.user_id
WHERE r.ticket_id = ? ORDER BY r.created_at ASC'
);
$rstmt->execute([$view]);
$replies = $rstmt->fetchAll();
}
}
// 负责人候选(管理员/客服),仅管理员可在表单中分配
$assignees = [];
if ($isAdmin) {
$assignees = $db->query("SELECT id, username, role FROM " . tn('users') . " WHERE role IN ('admin','cs') AND status = 1 ORDER BY role DESC, username")->fetchAll();
}
adminHeader('工单管理', 'tickets');
?>
工单管理(共 = (int) $total ?>)
= h($msg) ?>
= h($err) ?>
= ticketStatusLabel($ticket['status']) ?>
类型:= ticketTypeLabel($ticket['type']) ?>
提交人:= h($ticket['user_name'] ?? '游客') ?>(= h($ticket['user_email'] ?? '未填邮箱') ?>)
部门:= h($ticket['dept_name'] ?? '未指定') ?>
优先级:= ticketPriorityLabel($ticket['priority']) ?>
负责人:= !empty($ticket['assignee_name']) ? h($ticket['assignee_name']) : '未分配 ' ?>
提交:= date('Y-m-d H:i', strtotime($ticket['created_at'])) ?>
prepare('SELECT order_no, period_days, expires_at, status FROM ' . tn('orders') . ' WHERE id = ?'); $ord->execute([$ticket['order_id']]); $ord = $ord->fetch(); ?>
续期申请 · 关联订单
#= (int)$ticket['order_id'] ?>(= $ord ? h($ord['order_no']) : '已删除' ?>)
· 周期 = periodLabel($ord['period_days']) ?> · 当前到期 = expiryText($ord['expires_at']) ?>
= h($ticket['user_name'] ?? '用户') ?> · = date('Y-m-d H:i', strtotime($ticket['created_at'])) ?>
= h($ticket['subject']) ?>
= nl2br(h($ticket['message'])) ?>
处理人员(= h($r['username'] ?? '管理员') ?>) · = date('Y-m-d H:i', strtotime($r['created_at'])) ?>
= nl2br(h($r['message'])) ?>
= h($ticket['user_name'] ?? '用户') ?> · = date('Y-m-d H:i', strtotime($r['created_at'])) ?>
= nl2br(h($r['message'])) ?>
全部状态
>待处理
>处理中
>已解决
>已关闭
全部部门
>= h($d['name']) ?>
重置
暂无工单。
编号 标题 提交人 部门 类型 优先级 状态 负责人 更新
#= (int) $t['id'] ?>
= h($t['subject']) ?>
= h($t['user_name'] ?? '游客') ?>
= h($t['dept_name'] ?? '未指定') ?>
= ticketTypeLabel($t['type']) ?>
= ticketPriorityLabel($t['priority']) ?>
= ticketStatusLabel($t['status']) ?>
= !empty($t['assignee_name']) ? h($t['assignee_name']) : '未分配 ' ?>
= date('m-d H:i', strtotime($t['updated_at'])) ?>
处理