2.1 版本文件
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
if (!defined('IN_APP')) {
|
||||
http_response_code(403);
|
||||
exit('Forbidden');
|
||||
}
|
||||
|
||||
function adminHeader($title = '', $active = '') {
|
||||
$staff = currentStaff();
|
||||
if (!$staff) {
|
||||
// 未登录(理论上 requireStaff 已拦截,这里兜底)
|
||||
redirect('login.php');
|
||||
}
|
||||
$isAdmin = (int) $staff['is_admin'] === 1;
|
||||
$roleLabel = $isAdmin ? '管理员' : '客服';
|
||||
if ($title) {
|
||||
$pageTitle = $title . ' - ' . getSiteName();
|
||||
} else {
|
||||
$pageTitle = getSiteName() . ' - 后台';
|
||||
}
|
||||
|
||||
$cssVer = file_exists(__DIR__ . '/../assets/style.css') ? filemtime(__DIR__ . '/../assets/style.css') : time();
|
||||
// 后台导航定义
|
||||
$navAll = [
|
||||
'index' => ['url' => './', 'label' => '<i class="fas fa-dashboard"></i> 仪表盘'],
|
||||
'products' => ['url' => 'products', 'label' => '<i class="fas fa-box"></i> 商品管理'],
|
||||
'groups' => ['url' => 'groups', 'label' => '<i class="fas fa-tags"></i> 商品分组'],
|
||||
'orders' => ['url' => 'orders', 'label' => '<i class="fas fa-receipt"></i> 订单管理'],
|
||||
'tickets' => ['url' => 'tickets', 'label' => '<i class="fas fa-ticket-alt"></i> 工单管理'],
|
||||
'departments' => ['url' => 'departments','label' => '<i class="fas fa-sitemap"></i> 工单部门'],
|
||||
'announcements' => ['url' => 'announcements', 'label' => '<i class="fas fa-bullhorn"></i> 公告中心'],
|
||||
'users' => ['url' => 'users', 'label' => '<i class="fas fa-users"></i> 用户管理'],
|
||||
'admins' => ['url' => 'admins', 'label' => '<i class="fas fa-user-shield"></i> 管理员'],
|
||||
'settings' => ['url' => 'settings', 'label' => '<i class="fas fa-cog"></i> 设置'],
|
||||
'logs' => ['url' => 'logs', 'label' => '<i class="fas fa-scroll"></i> 系统日志'],
|
||||
];
|
||||
// 管理员与客服均可见全部导航(客服为只读/受限操作,在各页面内单独控制)
|
||||
$nav = $navAll;
|
||||
$theme = currentTheme() === 'dark' ? 'dark' : 'light';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="<?= $theme ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= h($pageTitle) ?></title>
|
||||
<?php
|
||||
$favicon = getSetting('favicon', '');
|
||||
if (!empty($favicon) && file_exists(__DIR__ . '/../' . $favicon)) {
|
||||
echo '<link rel="icon" href="../' . h($favicon) . '?v=' . filemtime(__DIR__ . '/../' . $favicon) . '">' . "\n ";
|
||||
}
|
||||
?>
|
||||
<link rel="stylesheet" href="../assets/vendor/fontawesome/all.min.css">
|
||||
<link rel="stylesheet" href="../assets/style.css?v=<?= $cssVer ?>">
|
||||
</head>
|
||||
<body>
|
||||
<header class="admin-bar">
|
||||
<div class="container nav">
|
||||
<a href="/" class="btn btn-ghost"><i class="fas fa-arrow-left"></i></a>
|
||||
<a href="./" class="brand"><i class="fas fa-store"></i></a>
|
||||
<button class="nav-toggle" onclick="toggleAdminNav()">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<nav class="nav-links" id="adminNavLinks">
|
||||
<?php foreach ($nav as $k => $n): ?>
|
||||
<a href="<?= $n['url'] ?>" class="<?= $k === $active ? 'active' : '' ?>"><?= $n['label'] ?></a>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
<div class="nav-actions">
|
||||
<span class="admin-user" title="当前登录身份"><i class="fas fa-<?= $isAdmin ? 'user-shield' : 'headset' ?>"></i> <?= h($staff['username']) ?> · <?= $roleLabel ?></span>
|
||||
<a href="logout" class="btn btn-ghost"><i class="fas fa-sign-out-alt"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main class="container admin-main">
|
||||
<?php
|
||||
}
|
||||
|
||||
function adminFooter()
|
||||
{
|
||||
?>
|
||||
</main>
|
||||
<footer class="admin-footer">
|
||||
<div class="container">
|
||||
<p>© <?= date('Y') ?> <?= h(getSiteName()) ?> · 后台管理</p>
|
||||
</div>
|
||||
</footer>
|
||||
<script>
|
||||
function toggleAdminNav() {
|
||||
var nav = document.getElementById('adminNavLinks');
|
||||
nav.classList.toggle('open');
|
||||
}
|
||||
document.addEventListener('click', function(e) {
|
||||
var nav = document.getElementById('adminNavLinks');
|
||||
var toggle = document.querySelector('.admin-bar .nav-toggle');
|
||||
if (nav && toggle) {
|
||||
if (!nav.contains(e.target) && !toggle.contains(e.target)) {
|
||||
nav.classList.remove('open');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
// 后台:管理员 / 客服账户管理(增删改角色 + 终极管理员保护)
|
||||
require __DIR__ . '/../includes/init.php';
|
||||
requireStaff();
|
||||
require '_common.php';
|
||||
$isAdmin = isAdminStaff();
|
||||
$me = currentStaff();
|
||||
$db = db();
|
||||
$msg = '';
|
||||
$err = '';
|
||||
|
||||
// 客服只读
|
||||
if (!$isAdmin && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$err = '客服账号仅可查看,如需操作请联系管理员。';
|
||||
}
|
||||
|
||||
// ─── 编辑角色(admin ↔ cs)────
|
||||
if (isset($_POST['edit_role']) && $isAdmin) {
|
||||
verifyCsrf();
|
||||
$id = (int) ($_POST['edit_role'] ?? 0);
|
||||
$role = trim($_POST['new_role'] ?? '');
|
||||
$role = in_array($role, ['admin', 'cs'], true) ? $role : '';
|
||||
|
||||
if ($id <= 0 || $role === '') {
|
||||
$err = '参数不合法。';
|
||||
} elseif ($id === (int) $me['id']) {
|
||||
$err = '不能修改当前登录账号的角色。';
|
||||
} elseif ($id === 1) {
|
||||
$err = '不能修改终极管理员(ID=1)的角色。';
|
||||
} else {
|
||||
$row = $db->prepare('SELECT id, role, is_admin FROM ' . tn('users') . ' WHERE id = ? AND role IN (\'admin\',\'cs\')')->execute([$id])->fetch();
|
||||
if (!$row) {
|
||||
$err = '账号不存在。';
|
||||
} else {
|
||||
$newIsAdmin = ($role === 'admin') ? 1 : 0;
|
||||
$db->prepare('UPDATE ' . tn('users') . ' SET role = ?, is_admin = ? WHERE id = ?')
|
||||
->execute([$role, $newIsAdmin, $id]);
|
||||
$msg = '已将账号 #' . $id . ' 角色改为「' . ($role === 'admin' ? '管理员' : '客服') . '」。';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 删除 ──
|
||||
if (isset($_POST['delete']) && $isAdmin) {
|
||||
verifyCsrf();
|
||||
$id = (int) ($_POST['delete'] ?? 0);
|
||||
if ($id === (int) $me['id']) {
|
||||
$err = '不能删除当前登录的账号。';
|
||||
} elseif ($id === 1) {
|
||||
$err = '不能删除终极管理员(ID=1),该账号受系统保护。';
|
||||
} else {
|
||||
$row = $db->prepare('SELECT id, role, is_admin FROM ' . tn('users') . ' WHERE id = ? AND role IN (\'admin\',\'cs\')')->execute([$id])->fetch();
|
||||
if (!$row) {
|
||||
$err = '账号不存在。';
|
||||
} elseif ((int) $row['is_admin'] === 1) {
|
||||
// 管理员:确保至少保留一名管理员(含终极管理员)
|
||||
$cnt = $db->query('SELECT COUNT(*) FROM ' . tn('users') . ' WHERE role = \'admin\' AND status = 1')->fetchColumn();
|
||||
if ((int) $cnt <= 1) {
|
||||
$err = '至少要保留一名管理员,不能继续删除。';
|
||||
} else {
|
||||
$db->prepare('DELETE FROM ' . tn('users') . ' WHERE id = ?')->execute([$id]);
|
||||
$msg = '管理员已删除。';
|
||||
}
|
||||
} else {
|
||||
$db->prepare('DELETE FROM ' . tn('users') . ' WHERE id = ?')->execute([$id]);
|
||||
$msg = '客服已删除。';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 添加 ──
|
||||
if (isset($_POST['add']) && $isAdmin) {
|
||||
verifyCsrf();
|
||||
$u = trim($_POST['username'] ?? '');
|
||||
$em = trim($_POST['email'] ?? '');
|
||||
$pw = $_POST['password'] ?? '';
|
||||
$role = trim($_POST['role'] ?? 'admin');
|
||||
$role = in_array($role, ['admin', 'cs'], true) ? $role : 'admin';
|
||||
if (strlen($u) < 3) $err = '账号至少 3 个字符。';
|
||||
elseif (!filter_var($em, FILTER_VALIDATE_EMAIL)) $err = '请填写有效的邮箱。';
|
||||
elseif (strlen($pw) < 6) $err = '密码至少 6 位。';
|
||||
else {
|
||||
$chk = $db->prepare('SELECT id FROM ' . tn('users') . ' WHERE username = ?');
|
||||
$chk->execute([$u]);
|
||||
if ($chk->fetch()) {
|
||||
$err = '该用户名已存在。';
|
||||
} else {
|
||||
$hash = password_hash($pw, PASSWORD_DEFAULT);
|
||||
$code = genInviteCode();
|
||||
$isAdminFlag = $role === 'admin' ? 1 : 0;
|
||||
$db->prepare('INSERT INTO ' . tn('users') . ' (username,password,email,invite_code,is_admin,role,created_at) VALUES (?,?,?,?,?,?,NOW())')
|
||||
->execute([$u, $hash, $em, $code, $isAdminFlag, $role]);
|
||||
$msg = ($role === 'admin' ? '管理员' : '客服') . '已添加。';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$list = $db->query('SELECT id, username, email, status, role, created_at FROM ' . tn('users') . ' WHERE role IN (\'admin\',\'cs\') ORDER BY role DESC, id ASC')->fetchAll();
|
||||
|
||||
adminHeader('管理员', 'admins');
|
||||
?>
|
||||
<h1 class="page-title"><i class="fas fa-user-shield"></i> 管理员与客服</h1>
|
||||
|
||||
<?php if ($msg): ?><p class="banner-ok"><i class="fas fa-check-circle"></i> <?= h($msg) ?></p><?php endif; ?>
|
||||
<?php if ($err): ?><p class="form-err"><?= h($err) ?></p><?php endif; ?>
|
||||
|
||||
<div class="panel">
|
||||
<h3>添加账号(管理员 / 客服)</h3>
|
||||
<?php if ($isAdmin): ?>
|
||||
<p class="muted">管理员拥有全部后台权限;客服可查看所有页面但仅能处理工单相关操作。</p>
|
||||
<form method="post" action="" class="grid-form" style="max-width:680px;">
|
||||
<?= csrfField() ?>
|
||||
<label>账号 *<input type="text" name="username" placeholder="至少 3 个字符" required></label>
|
||||
<label>邮箱 *<input type="email" name="email" required></label>
|
||||
<label>角色
|
||||
<select name="role">
|
||||
<option value="admin">管理员(全部权限)</option>
|
||||
<option value="cs">客服(查看 + 工单操作)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="span2">密码 *<input type="password" name="password" placeholder="至少 6 位密码" required></label>
|
||||
<div class="form-actions span2"><button type="submit" name="add" class="btn btn-primary"><i class="fas fa-user-plus"></i> 添加账号</button></div>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<p class="muted">客服账号无法添加新管理员,如需操作请联系管理员。</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>账号列表(共 <?= count($list) ?> 名)</h3>
|
||||
<?php if (empty($list)): ?>
|
||||
<p class="empty">暂无账号。</p>
|
||||
<?php else: ?>
|
||||
<table class="data-table">
|
||||
<thead><tr><th>ID</th><th>账号</th><th>邮箱</th><th>角色</th><th>状态</th><th>注册时间</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($list as $a): ?>
|
||||
<?php
|
||||
$isMe = (int)$a['id'] === (int)$me['id'];
|
||||
$isSuper = (int)$a['id'] === 1;
|
||||
$isAdm = ($a['role'] === 'admin');
|
||||
?>
|
||||
<tr <?= $isSuper ? 'style="background:color-mix(in srgb,var(--primary) 6%,transparent)"' : '' ?>>
|
||||
<td><?= (int) $a['id'] ?><?= $isSuper ? ' <sup style="color:var(--primary);font-size:.72rem">超级</sup>' : '' ?></td>
|
||||
<td><?= h($a['username']) ?><?= $isMe ? ' <span class="badge-on">我</span>' : '' ?></td>
|
||||
<td><?= h($a['email']) ?></td>
|
||||
<td>
|
||||
<?php if ($isAdmin && !$isMe && !$isSuper): ?>
|
||||
<form method="post" action="" style="display:inline" onsubmit="return confirmRoleChange(<?= (int) $a['id'] ?>, '<?= h($a['username']) ?>');">
|
||||
<?= csrfField() ?>
|
||||
<input type="hidden" name="edit_role" value="<?= (int) $a['id'] ?>">
|
||||
<select name="new_role" onchange="this.form.submit()" style="padding:2px 6px;border:1px solid var(--outline);border-radius:6px;font-size:.82rem;cursor:pointer;background:transparent;color:inherit">
|
||||
<option value="admin" <?php if ($isAdm): ?>selected<?php endif; ?>>管理员</option>
|
||||
<option value="cs" <?php if (!$isAdm): ?>selected<?php endif; ?>>客服</option>
|
||||
</select>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<?php if ($isAdm): ?>
|
||||
<span class="badge-on">管理员</span>
|
||||
<?php else: ?>
|
||||
<span class="badge-on" style="background:var(--primary-container);color:var(--on-primary-container)">客服</span>
|
||||
<?php endif; ?>
|
||||
<?php if ($isSuper): ?><span class="muted" style="font-size:.75rem;margin-left:4px">(受保护)</span><?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= (int) $a['status'] === 1 ? '<span class="badge-on">正常</span>' : '<span class="badge-off">封禁</span>' ?></td>
|
||||
<td><?= date('Y-m-d', strtotime($a['created_at'])) ?></td>
|
||||
<td class="row-actions">
|
||||
<?php if ($isMe): ?>
|
||||
<span class="muted">当前账号</span>
|
||||
<?php elseif ($isAdmin): ?>
|
||||
<?php if ($isSuper): ?>
|
||||
<span class="muted" title="终极管理员受系统保护,不可删除"><i class="fas fa-shield-halved" style="color:var(--primary)"></i> 受保护</span>
|
||||
<?php else: ?>
|
||||
<form method="post" action="" style="display:inline" onsubmit="return confirm('确定删除该账号?此操作不可恢复。');">
|
||||
<?= csrfField() ?>
|
||||
<button type="submit" name="delete" value="<?= (int) $a['id'] ?>" class="mini-btn danger"><i class="fas fa-trash"></i></button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<span class="muted">—</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php if ($isAdmin): ?>
|
||||
<p class="muted" style="margin-top:10px;font-size:.82rem">
|
||||
<i class="fas fa-info-circle"></i> 终极管理员(ID=1)受系统保护,不可被删除或降级。其他管理员可由同级管理员删除或改为客服。
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<script>
|
||||
function confirmRoleChange(id, name) {
|
||||
var sel = event.target.form.elements.new_role;
|
||||
var roleLabel = sel.options[sel.selectedIndex].text;
|
||||
return confirm('确定将 ' + name + ' 的角色改为「' + roleLabel + '」?');
|
||||
}
|
||||
</script>
|
||||
<?php adminFooter(); ?>
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
require __DIR__ . '/../includes/init.php';
|
||||
requireStaff();
|
||||
require '_common.php';
|
||||
$isAdmin = isAdminStaff();
|
||||
|
||||
$db = db();
|
||||
$msg = '';
|
||||
$err = '';
|
||||
// 客服账号仅可查看,禁止写操作
|
||||
if (!$isAdmin && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$err = '客服账号仅可查看,如需操作请联系管理员。';
|
||||
}
|
||||
if (isset($_POST['delete'])) {
|
||||
verifyCsrf();
|
||||
$id = (int) ($_POST['delete'] ?? 0);
|
||||
$db->prepare('DELETE FROM ' . tn('announcements') . ' WHERE id = ?')->execute([$id]);
|
||||
$msg = '已删除这条公告。';
|
||||
}
|
||||
|
||||
if (isset($_POST['save'])) {
|
||||
verifyCsrf();
|
||||
$id = (int) ($_POST['id'] ?? 0);
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$content = trim($_POST['content'] ?? '');
|
||||
$pinned = isset($_POST['pinned']) ? 1 : 0;
|
||||
$status = isset($_POST['status']) ? 1 : 0;
|
||||
|
||||
if ($title === '') $err = '请填写公告标题。';
|
||||
elseif ($content === '') $err = '请填写公告内容。';
|
||||
|
||||
if ($err === '') {
|
||||
if ($id > 0) {
|
||||
$db->prepare('UPDATE ' . tn('announcements') . ' SET title=?, content=?, pinned=?, status=? WHERE id=?')
|
||||
->execute([$title, $content, $pinned, $status, $id]);
|
||||
$msg = '公告已更新!';
|
||||
} else {
|
||||
$db->prepare('INSERT INTO ' . tn('announcements') . ' (title, content, pinned, status, created_at) VALUES (?,?,?,?,NOW())')
|
||||
->execute([$title, $content, $pinned, $status]);
|
||||
$msg = '公告已发布!';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$edit = null;
|
||||
if (isset($_GET['edit'])) {
|
||||
$stmt = $db->prepare('SELECT * FROM ' . tn('announcements') . ' WHERE id = ?');
|
||||
$stmt->execute([(int) $_GET['edit']]);
|
||||
$edit = $stmt->fetch();
|
||||
}
|
||||
$showForm = ($edit !== null) || isset($_GET['act']) && $_GET['act'] === 'add';
|
||||
|
||||
$list = $db->query('SELECT * FROM ' . tn('announcements') . ' ORDER BY pinned DESC, created_at DESC')->fetchAll();
|
||||
|
||||
adminHeader('公告中心', 'announcements');
|
||||
?>
|
||||
|
||||
<h1 class="page-title"><i class="fas fa-bullhorn"></i> 公告中心</h1>
|
||||
|
||||
<?php if ($msg): ?><p class="banner-ok"><i class="fas fa-check-circle"></i> <?= h($msg) ?></p><?php endif; ?>
|
||||
<?php if ($err): ?><p class="form-err"><?= h($err) ?></p><?php endif; ?>
|
||||
|
||||
<?php if ($showForm): ?>
|
||||
<div class="panel">
|
||||
<h3><?= $edit ? '编辑公告 #' . (int)$edit['id'] : '发布新公告' ?></h3>
|
||||
<form method="post" action="" class="grid-form">
|
||||
<?= csrfField() ?>
|
||||
<input type="hidden" name="id" value="<?= $edit ? (int)$edit['id'] : 0 ?>">
|
||||
<label class="span2">
|
||||
<span>公告标题 <b style="color: red;">*</b></span>
|
||||
<input type="text" name="title" value="<?= $edit ? h($edit['title']) : '' ?>" required>
|
||||
</label>
|
||||
<label class="span2">
|
||||
<span>公告内容 <b style="color: red;">*</b>(支持 <a href="https://markdown.com.cn/cheat-sheet.html">Markdown 语法</a>)</span>
|
||||
<textarea id="annContent" name="content" rows="8" required><?= $edit ? h($edit['content']) : '' ?></textarea>
|
||||
</label>
|
||||
<div class="span2 ann-preview-wrap">
|
||||
<div class="ann-preview-bar">
|
||||
<strong>实时预览</strong>
|
||||
<button type="button" class="btn btn-ghost btn-sm" id="annPreviewToggle">显示 / 隐藏预览</button>
|
||||
</div>
|
||||
<div class="ann-detail-body ann-preview" id="annPreview"></div>
|
||||
</div>
|
||||
<label class="checkbox"><input type="checkbox" name="pinned" <?= $edit && !empty($edit['pinned']) ? 'checked' : '' ?>> 置顶</label>
|
||||
<label class="checkbox"><input type="checkbox" name="status" <?= !$edit || !empty($edit['status']) ? 'checked' : '' ?>> 前台显示</label>
|
||||
<div class="form-actions span2">
|
||||
<button type="submit" name="save" class="btn btn-primary"><i class="fas fa-save"></i> 保存</button>
|
||||
<a href="announcements.php" class="btn btn-ghost">取消</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<h3>公告列表(共 <?= count($list) ?> 条)</h3>
|
||||
<?php if (!$showForm): ?><a href="announcements.php?act=add" class="btn btn-primary"><i class="fas fa-plus"></i> 发布公告</a><?php endif; ?>
|
||||
</div>
|
||||
<?php if (empty($list)): ?>
|
||||
<p class="empty">还没有公告,点上方「发布公告」。</p>
|
||||
<?php else: ?>
|
||||
<table class="data-table">
|
||||
<thead><tr><th>ID</th><th>标题</th><th>置顶</th><th>显示</th><th>时间</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($list as $a): ?>
|
||||
<tr>
|
||||
<td><?= (int)$a['id'] ?></td>
|
||||
<td><?= h($a['title']) ?></td>
|
||||
<td><?= !empty($a['pinned']) ? '<span class="badge-on">置顶</span>' : '<span class="badge-off">否</span>' ?></td>
|
||||
<td><?= !empty($a['status']) ? '<span class="badge-on">显示</span>' : '<span class="badge-off">隐藏</span>' ?></td>
|
||||
<td><?= date('m-d H:i', strtotime($a['created_at'])) ?></td>
|
||||
<td class="row-actions">
|
||||
<a href="../announcement.php?id=<?= (int)$a['id'] ?>" class="mini-btn" target="_blank" title="前台预览"><i class="fas fa-eye"></i></a>
|
||||
<a href="announcements.php?edit=<?= (int)$a['id'] ?>" class="mini-btn"><i class="fas fa-edit"></i></a>
|
||||
<form method="post" action="" style="display:inline" onsubmit="return confirm('确定删除该公告?');">
|
||||
<?= csrfField() ?>
|
||||
<button type="submit" name="delete" value="<?= (int)$a['id'] ?>" class="mini-btn danger"><i class="fas fa-trash"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
$mv = file_exists(__DIR__ . '/../assets/marked.js') ? filemtime(__DIR__ . '/../assets/marked.js') : time();
|
||||
?>
|
||||
<script src="../assets/marked.js?v=<?= $mv ?>"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var ta = document.getElementById('annContent');
|
||||
var pv = document.getElementById('annPreview');
|
||||
if (!ta || !pv || !window.marked) return;
|
||||
function render() {
|
||||
try { pv.innerHTML = marked.parse(ta.value || ''); }
|
||||
catch (e) { pv.textContent = ta.value; }
|
||||
}
|
||||
ta.addEventListener('input', render);
|
||||
render();
|
||||
var btn = document.getElementById('annPreviewToggle');
|
||||
if (btn) btn.addEventListener('click', function () { pv.hidden = !pv.hidden; });
|
||||
})();
|
||||
</script>
|
||||
<?php adminFooter(); ?>
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
// 后台:工单部门管理(创建 / 删除)
|
||||
require __DIR__ . '/../includes/init.php';
|
||||
requireStaff();
|
||||
require '_common.php';
|
||||
$isAdmin = isAdminStaff();
|
||||
|
||||
$db = db();
|
||||
$msg = '';
|
||||
$err = '';
|
||||
// 客服账号仅可查看,禁止写操作
|
||||
if (!$isAdmin && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$err = '客服账号仅可查看,如需操作请联系管理员。';
|
||||
}
|
||||
// 删除
|
||||
if (isset($_POST['delete'])) {
|
||||
verifyCsrf();
|
||||
$id = (int) ($_POST['delete'] ?? 0);
|
||||
// 该部门下的工单改挂「未指定」(dept_id=0)
|
||||
$db->prepare('UPDATE ' . tn('tickets') . ' SET dept_id = 0 WHERE dept_id = ?')->execute([$id]);
|
||||
$db->prepare('DELETE FROM ' . tn('ticket_departments') . ' WHERE id = ?')->execute([$id]);
|
||||
$msg = '部门已删除,其工单已移至「未指定」。';
|
||||
}
|
||||
|
||||
// 新增
|
||||
if (isset($_POST['add'])) {
|
||||
verifyCsrf();
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$desc = trim($_POST['description'] ?? '');
|
||||
$sort = (int) ($_POST['sort'] ?? 0);
|
||||
if ($name === '') {
|
||||
$err = '请填写部门名称';
|
||||
} else {
|
||||
$db->prepare('INSERT INTO ' . tn('ticket_departments') . ' (name, description, sort, created_at) VALUES (?,?,?,NOW())')
|
||||
->execute([$name, $desc, $sort]);
|
||||
$msg = '部门已添加';
|
||||
}
|
||||
}
|
||||
|
||||
$list = $db->query('SELECT * FROM ' . tn('ticket_departments') . ' ORDER BY sort, id')->fetchAll();
|
||||
$counts = $db->query('SELECT dept_id, COUNT(*) c FROM ' . tn('tickets') . ' GROUP BY dept_id')->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
adminHeader('工单部门', 'departments');
|
||||
?>
|
||||
<h1 class="page-title"><i class="fas fa-sitemap"></i> 工单部门</h1>
|
||||
|
||||
<?php if ($msg): ?><p class="banner-ok"><i class="fas fa-check-circle"></i> <?= h($msg) ?></p><?php endif; ?>
|
||||
<?php if ($err): ?><p class="form-err"><?= h($err) ?></p><?php endif; ?>
|
||||
|
||||
<div class="panel">
|
||||
<h3>新增部门</h3>
|
||||
<form method="post" action="" class="grid-form" style="max-width:640px;">
|
||||
<?= csrfField() ?>
|
||||
<label>部门名称 *<input type="text" name="name" required></label>
|
||||
<label>排序<input type="number" name="sort" value="0"></label>
|
||||
<label class="span2">说明<input type="text" name="description" placeholder="选填,如负责范围"></label>
|
||||
<div class="form-actions span2"><button type="submit" name="add" class="btn btn-primary"><i class="fas fa-plus"></i> 添加部门</button></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>部门列表(共 <?= count($list) ?> 个)</h3>
|
||||
<?php if (empty($list)): ?>
|
||||
<p class="empty">还没有部门。</p>
|
||||
<?php else: ?>
|
||||
<table class="data-table">
|
||||
<thead><tr><th>ID</th><th>名称</th><th>说明</th><th>工单数</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($list as $d): ?>
|
||||
<tr>
|
||||
<td><?= (int) $d['id'] ?></td>
|
||||
<td><?= h($d['name']) ?></td>
|
||||
<td><?= h($d['description']) ?></td>
|
||||
<td><?= (int) ($counts[$d['id']] ?? 0) ?></td>
|
||||
<td class="row-actions">
|
||||
<form method="post" action="" style="display:inline" onsubmit="return confirm('删除该部门?其下工单将移至「未指定」。');">
|
||||
<?= csrfField() ?>
|
||||
<button type="submit" name="delete" value="<?= (int) $d['id'] ?>" class="mini-btn danger"><i class="fas fa-trash"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php adminFooter(); ?>
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
// 后台商品分组管理:列表 + 新增 / 删除(删除前将相关商品归为「其他」避免孤儿)
|
||||
require __DIR__ . '/../includes/init.php';
|
||||
requireStaff();
|
||||
require '_common.php';
|
||||
$isAdmin = isAdminStaff();
|
||||
|
||||
$db = db();
|
||||
$msg = '';
|
||||
$err = '';
|
||||
// 客服账号仅可查看,禁止写操作
|
||||
if (!$isAdmin && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$err = '客服账号仅可查看,如需操作请联系管理员。';
|
||||
}
|
||||
// ---------- 新增 / 编辑保存 ----------
|
||||
if (isset($_POST['save'])) {
|
||||
verifyCsrf();
|
||||
$id = (int) ($_POST['id'] ?? 0);
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$icon = trim($_POST['icon'] ?? 'fa-tags');
|
||||
$sort = max(0, (int) ($_POST['sort_order'] ?? 0));
|
||||
|
||||
if ($name === '') {
|
||||
$err = '请填写分组名称';
|
||||
} elseif (mb_strlen($name) > 40) {
|
||||
$err = '分组名称过长(最多 40 字)';
|
||||
} else {
|
||||
// 重名检查(排除自身)
|
||||
$dup = $db->prepare('SELECT COUNT(*) FROM ' . tn('product_groups') . ' WHERE name = ? AND id <> ?');
|
||||
$dup->execute([$name, $id]);
|
||||
if ((int) $dup->fetchColumn() > 0) {
|
||||
$err = '已存在同名分组,请换一个名称';
|
||||
}
|
||||
}
|
||||
|
||||
if ($err === '') {
|
||||
if ($id > 0) {
|
||||
$db->prepare('UPDATE ' . tn('product_groups') . ' SET name=?, icon=?, sort_order=? WHERE id=?')
|
||||
->execute([$name, $icon, $sort, $id]);
|
||||
$msg = '分组已更新';
|
||||
} else {
|
||||
$db->prepare('INSERT INTO ' . tn('product_groups') . ' (name, icon, sort_order, created_at) VALUES (?,?,?,NOW())')
|
||||
->execute([$name, $icon, $sort]);
|
||||
$msg = '分组已添加';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 删除 ----------
|
||||
if (isset($_POST['delete'])) {
|
||||
verifyCsrf();
|
||||
$id = (int) $_POST['delete'];
|
||||
$row = $db->prepare('SELECT name FROM ' . tn('product_groups') . ' WHERE id = ?');
|
||||
$row->execute([$id]);
|
||||
$g = $row->fetch();
|
||||
if ($g) {
|
||||
// 将该分组下的商品归为「其他」,避免孤儿数据
|
||||
$db->prepare('UPDATE ' . tn('products') . ' SET category = ? WHERE category = ?')
|
||||
->execute(['其他', $g['name']]);
|
||||
$db->prepare('DELETE FROM ' . tn('product_groups') . ' WHERE id = ?')->execute([$id]);
|
||||
$msg = '分组「' . $g['name'] . '」已删除,相关商品已归为「其他」';
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 编辑回填 ----------
|
||||
$edit = null;
|
||||
if (isset($_GET['edit'])) {
|
||||
$stmt = $db->prepare('SELECT * FROM ' . tn('product_groups') . ' WHERE id = ?');
|
||||
$stmt->execute([(int) $_GET['edit']]);
|
||||
$edit = $stmt->fetch();
|
||||
}
|
||||
$showForm = ($edit !== null) || (isset($_GET['act']) && $_GET['act'] === 'add');
|
||||
|
||||
// ---------- 列表(含商品计数) ----------
|
||||
$list = $db->query(
|
||||
'SELECT g.*, (SELECT COUNT(*) FROM ' . tn('products') . ' p WHERE p.category = g.name) AS cnt
|
||||
FROM ' . tn('product_groups') . ' g ORDER BY g.sort_order ASC, g.id ASC'
|
||||
)->fetchAll();
|
||||
|
||||
adminHeader('商品分组', 'groups');
|
||||
?>
|
||||
<h1 class="page-title"><i class="fas fa-tags"></i> 商品分组</h1>
|
||||
<p class="muted" style="margin-bottom:16px;">分组用于首页分类筛选与商品归类。删除分组会将其下商品自动归为「其他」,不会丢失商品数据。</p>
|
||||
|
||||
<?php if ($msg): ?><p class="banner-ok"><i class="fas fa-check-circle"></i> <?= h($msg) ?></p><?php endif; ?>
|
||||
<?php if ($err): ?><p class="form-err"><?= h($err) ?></p><?php endif; ?>
|
||||
|
||||
<?php if ($showForm): ?>
|
||||
<div class="panel">
|
||||
<h3><?= $edit ? '编辑分组 #' . (int) $edit['id'] : '新增分组' ?></h3>
|
||||
<form method="post" action="" class="grid-form">
|
||||
<?= csrfField() ?>
|
||||
<input type="hidden" name="id" value="<?= $edit ? (int) $edit['id'] : 0 ?>">
|
||||
<label>分组名称 *<input type="text" name="name" value="<?= $edit ? h($edit['name']) : '' ?>" required maxlength="40" placeholder="如 周边 / 数码"></label>
|
||||
<label>排序(数字小靠前)<input type="number" name="sort_order" value="<?= $edit ? (int) $edit['sort_order'] : 0 ?>"></label>
|
||||
<label class="span2">图标类名<input type="text" name="icon" value="<?= $edit ? h($edit['icon']) : 'fa-tags' ?>" placeholder="如 fa-tags(FontAwesome 类名)"></label>
|
||||
<div class="form-actions span2">
|
||||
<button type="submit" name="save" class="btn btn-primary"><i class="fas fa-save"></i> 保存</button>
|
||||
<a href="groups.php" class="btn btn-ghost">取消</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<h3>分组列表(共 <?= count($list) ?> 个)</h3>
|
||||
<?php if (!$showForm): ?><a href="groups.php?act=add" class="btn btn-primary"><i class="fas fa-plus"></i> 新增分组</a><?php endif; ?>
|
||||
</div>
|
||||
<?php if (empty($list)): ?>
|
||||
<p class="empty">还没有分组,点上方「新增分组」添加吧。</p>
|
||||
<?php else: ?>
|
||||
<table class="data-table">
|
||||
<thead><tr><th>ID</th><th>图标</th><th>名称</th><th>排序</th><th>商品数</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($list as $g): ?>
|
||||
<tr>
|
||||
<td><?= (int) $g['id'] ?></td>
|
||||
<td><i class="fas <?= h($g['icon']) ?>"></i></td>
|
||||
<td><?= h($g['name']) ?></td>
|
||||
<td><?= (int) $g['sort_order'] ?></td>
|
||||
<td><?= (int) $g['cnt'] ?></td>
|
||||
<td class="row-actions">
|
||||
<a href="groups.php?edit=<?= (int) $g['id'] ?>" class="mini-btn"><i class="fas fa-edit"></i></a>
|
||||
<form method="post" action="" style="display:inline" onsubmit="return confirm('确定删除该分组?其下商品将归为「其他」。');">
|
||||
<?= csrfField() ?>
|
||||
<button type="submit" name="delete" value="<?= (int) $g['id'] ?>" class="mini-btn danger"><i class="fas fa-trash"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php adminFooter(); ?>
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
// 后台仪表盘
|
||||
require __DIR__ . '/../includes/init.php';
|
||||
requireStaff();
|
||||
require '_common.php';
|
||||
|
||||
$db = db();
|
||||
$isAdmin = isAdminStaff();
|
||||
$me = currentStaff();
|
||||
|
||||
$openTickets = $db->query("SELECT COUNT(*) FROM " . tn('tickets') . " WHERE status IN ('open','pending')")->fetchColumn();
|
||||
$myOpenTickets = 0;
|
||||
if (!$isAdmin) {
|
||||
// 客服:仅统计分配给自己的待处理工单
|
||||
$stmt = $db->prepare("SELECT COUNT(*) FROM " . tn('tickets') . " WHERE status IN ('open','pending') AND assignee_id = ?");
|
||||
$stmt->execute([$me['id']]);
|
||||
$myOpenTickets = (int) $stmt->fetchColumn();
|
||||
}
|
||||
|
||||
$recentTickets = $db->query(
|
||||
'SELECT t.*, u.username FROM ' . tn('tickets') . ' t
|
||||
LEFT JOIN ' . tn('users') . ' u ON u.id = t.user_id
|
||||
' . ($isAdmin ? '' : ' WHERE t.assignee_id = ' . (int)$me['id']) . '
|
||||
ORDER BY t.updated_at DESC LIMIT 5'
|
||||
)->fetchAll();
|
||||
|
||||
$statCards = [];
|
||||
if ($isAdmin) {
|
||||
$onSale = $db->query('SELECT COUNT(*) FROM ' . tn('products') . ' WHERE status = 1')->fetchColumn();
|
||||
$orderCnt = $db->query('SELECT COUNT(*) FROM ' . tn('orders'))->fetchColumn();
|
||||
$sales = $db->query("SELECT COALESCE(SUM(total),0) FROM " . tn('orders') . " WHERE status IN ('paid','shipped','completed')")->fetchColumn();
|
||||
$pending = $db->query("SELECT COUNT(*) FROM " . tn('orders') . " WHERE status = 'paid'")->fetchColumn();
|
||||
$annCnt = $db->query('SELECT COUNT(*) FROM ' . tn('announcements') . ' WHERE status = 1')->fetchColumn();
|
||||
$groupCnt = $db->query('SELECT COUNT(*) FROM ' . tn('product_groups'))->fetchColumn();
|
||||
|
||||
$statCards[] = ['num' => (int)$onSale, 'label' => '在售商品'];
|
||||
$statCards[] = ['num' => (int)$orderCnt, 'label' => '累计订单'];
|
||||
$statCards[] = ['num' => money($sales), 'label' => '销售总额'];
|
||||
$statCards[] = ['num' => (int)$pending, 'label' => '等待发货', 'warn' => true];
|
||||
$statCards[] = ['num' => (int)$annCnt, 'label' => '发布公告'];
|
||||
$statCards[] = ['num' => (int)$groupCnt, 'label' => '商品分组'];
|
||||
}
|
||||
$statCards[] = ['num' => ($isAdmin ? (int)$openTickets : $myOpenTickets), 'label' => ($isAdmin ? '待处理工单' : '我待处理工单'), 'warn' => true];
|
||||
|
||||
adminHeader('仪表盘', 'index');
|
||||
?>
|
||||
|
||||
<h1 class="page-title"><i class="fas fa-tachometer-alt"></i> 仪表盘</h1>
|
||||
|
||||
<div class="stat-grid">
|
||||
<?php foreach ($statCards as $c): ?>
|
||||
<div class="stat-card<?= !empty($c['warn']) ? ' warn' : '' ?>"><div class="stat-num"><?= $c['num'] ?></div><div class="stat-label"><?= h($c['label']) ?></div></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($isAdmin): ?>
|
||||
<h2 class="sec-title">最新订单</h2>
|
||||
<?php
|
||||
$recent = $db->query(
|
||||
'SELECT o.*, u.username FROM ' . tn('orders') . ' o
|
||||
LEFT JOIN ' . tn('users') . ' u ON u.id = o.user_id
|
||||
ORDER BY o.created_at DESC LIMIT 5'
|
||||
)->fetchAll();
|
||||
?>
|
||||
<?php if (empty($recent)): ?>
|
||||
<p class="empty">暂无订单。</p>
|
||||
<?php else: ?>
|
||||
<table class="data-table">
|
||||
<thead><tr><th>订单号</th><th>用户</th><th>金额</th><th>状态</th><th>时间</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($recent as $o): ?>
|
||||
<tr>
|
||||
<td><?= h($o['order_no']) ?></td>
|
||||
<td><?= h($o['username'] ?? '游客') ?></td>
|
||||
<td><?= money($o['total']) ?></td>
|
||||
<td><span class="badge-status status-<?= h($o['status']) ?>"><?= orderStatusLabel($o['status']) ?></span></td>
|
||||
<td><?= date('m-d H:i', strtotime($o['created_at'])) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<a href="orders.php" class="btn btn-ghost">查看全部订单 →</a>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<h2 class="sec-title"><?= $isAdmin ? '最新工单' : '我负责的工单' ?></h2>
|
||||
<?php if (empty($recentTickets)): ?>
|
||||
<p class="empty">暂无工单。</p>
|
||||
<?php else: ?>
|
||||
<table class="data-table">
|
||||
<thead><tr><th>编号</th><th>标题</th><th>提交人</th><th>状态</th><th>更新</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($recentTickets as $t): ?>
|
||||
<tr>
|
||||
<td>#<?= (int)$t['id'] ?></td>
|
||||
<td><?= h($t['subject']) ?></td>
|
||||
<td><?= h($t['username'] ?? '游客') ?></td>
|
||||
<td><span class="badge-status status-<?= h($t['status']) ?>"><?= ticketStatusLabel($t['status']) ?></span></td>
|
||||
<td><?= date('m-d H:i', strtotime($t['updated_at'])) ?></td>
|
||||
<td><a href="tickets.php?view=<?= (int)$t['id'] ?>" class="mini-btn">处理</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<a href="tickets.php" class="btn btn-ghost">查看全部工单 →</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($isAdmin): ?>
|
||||
<h2 class="sec-title">最新公告</h2>
|
||||
<?php
|
||||
$recentAnn = $db->query('SELECT * FROM ' . tn('announcements') . ' ORDER BY pinned DESC, created_at DESC LIMIT 5')->fetchAll();
|
||||
?>
|
||||
<?php if (empty($recentAnn)): ?>
|
||||
<p class="empty">暂无公告。</p>
|
||||
<?php else: ?>
|
||||
<table class="data-table">
|
||||
<thead><tr><th>标题</th><th>置顶</th><th>时间</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($recentAnn as $a): ?>
|
||||
<tr>
|
||||
<td><?= h($a['title']) ?><?= !empty($a['pinned']) ? ' <i class="fas fa-thumbtack" style="color:var(--primary)"></i>' : '' ?></td>
|
||||
<td><?= !empty($a['pinned']) ? '是' : '否' ?></td>
|
||||
<td><?= date('m-d H:i', strtotime($a['created_at'])) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<a href="announcements.php" class="btn btn-ghost">管理公告 →</a>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php adminFooter(); ?>
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
// 后台登录(仅管理员 is_admin=1 可进入)
|
||||
session_start();
|
||||
|
||||
require_once __DIR__ . '/../includes/_install_redirect.php';
|
||||
define('IN_APP', true);
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once __DIR__ . '/../includes/db.php';
|
||||
require_once __DIR__ . '/../includes/functions.php';
|
||||
|
||||
if (!empty($_SESSION['admin_id'])) {
|
||||
header("HTTP/2 404 Not Found");
|
||||
exit;
|
||||
}
|
||||
|
||||
$err = '';
|
||||
$cssVer = file_exists(__DIR__ . '/../assets/style.css') ? filemtime(__DIR__ . '/../assets/style.css') : time();
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
verifyCsrf();
|
||||
$u = trim($_POST['username'] ?? '');
|
||||
$pw = $_POST['password'] ?? '';
|
||||
$stmt = db()->prepare('SELECT * FROM ' . tn('users') . ' WHERE username = ? AND role IN (\'admin\', \'cs\')');
|
||||
$stmt->execute([$u]);
|
||||
$row = $stmt->fetch();
|
||||
if ($row && password_verify($pw, $row['password'])) {
|
||||
$_SESSION['admin_id'] = $row['id'];
|
||||
$_SESSION['admin_user'] = $row['username'];
|
||||
unset($_SESSION['csrf']);
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
$err = '管理员账号或密码错误';
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="<?= currentTheme() === 'dark' ? 'dark' : 'light' ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>后台登录 - <?= h(SITE_NAME) ?></title>
|
||||
<link rel="stylesheet" href="../assets/vendor/fontawesome/all.min.css">
|
||||
<link rel="stylesheet" href="../assets/style.css?v=<?= $cssVer ?>">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-login">
|
||||
<div class="auth-card">
|
||||
<h2 class="al-title"><i class="fas fa-user-shield"></i> 后台登录</h2>
|
||||
<p class="al-sub"><?= h(SITE_NAME) ?> 管理后台</p>
|
||||
<?php if ($err): ?><p class="form-err"><?= h($err) ?></p><?php endif; ?>
|
||||
<form method="post" action="" class="auth-form">
|
||||
<?= csrfField() ?>
|
||||
<label>管理员账号<input type="text" name="username" required></label>
|
||||
<label>密码<input type="password" name="password" required></label>
|
||||
<button type="submit" class="btn btn-primary" style="width:100%">登 录</button>
|
||||
</form>
|
||||
<a href="../index.php" class="al-back"><i class="fas fa-arrow-left"></i> 返回商城前台</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
// 后台退出
|
||||
session_start();
|
||||
unset($_SESSION['admin_id'], $_SESSION['admin_user']);
|
||||
session_destroy();
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
// 后台:系统日志查看器
|
||||
require __DIR__ . '/../includes/init.php';
|
||||
requireStaff();
|
||||
require '_common.php';
|
||||
$isAdmin = isAdminStaff();
|
||||
|
||||
$logDir = realpath(__DIR__ . '/../storage/logs') ?: (__DIR__ . '/../storage/logs');
|
||||
$msg = '';
|
||||
$err = '';
|
||||
|
||||
// 确保日志目录存在
|
||||
if (!is_dir($logDir)) {
|
||||
@mkdir($logDir, 0755, true);
|
||||
}
|
||||
|
||||
// 获取请求的日期,默认今天
|
||||
$date = trim($_GET['date'] ?? '');
|
||||
if ($date === '') {
|
||||
$date = date('Y-m-d');
|
||||
}
|
||||
// 安全校验:只允许 YYYY-MM-DD 格式
|
||||
if (!preg_match('#^\d{4}-\d{2}-\d{2}$#', $date)) {
|
||||
$date = date('Y-m-d');
|
||||
}
|
||||
|
||||
$logFile = $logDir . DIRECTORY_SEPARATOR . $date . '.log';
|
||||
$lines = [];
|
||||
$totalLines = 0;
|
||||
$pageSize = 100;
|
||||
$page = max(1, (int) ($_GET['page'] ?? 1));
|
||||
|
||||
// 读取日志文件
|
||||
if (file_exists($logFile) && is_readable($logFile)) {
|
||||
$content = file_get_contents($logFile);
|
||||
$allLines = explode("\n", rtrim($content));
|
||||
$totalLines = count($allLines);
|
||||
|
||||
// 分页(最新的在前面,所以倒序)
|
||||
$allLines = array_reverse($allLines);
|
||||
$offset = ($page - 1) * $pageSize;
|
||||
$lines = array_slice($allLines, $offset, $pageSize);
|
||||
} elseif (file_exists($logFile)) {
|
||||
$err = '日志文件不可读,请检查目录权限。';
|
||||
}
|
||||
|
||||
// 获取所有可用的日志日期
|
||||
$logFiles = glob($logDir . DIRECTORY_SEPARATOR . '*.log');
|
||||
$dates = [];
|
||||
foreach ($logFiles as $f) {
|
||||
$n = basename($f);
|
||||
// 排除轮转文件 (.log.1)
|
||||
if (preg_match('#^(\d{4}-\d{2}-\d{2})\.log$#', $n, $m)) {
|
||||
$dates[] = $m[1];
|
||||
}
|
||||
}
|
||||
rsort($dates);
|
||||
$totalPages = max(1, ceil($totalLines / $pageSize));
|
||||
|
||||
adminHeader('系统日志', 'logs');
|
||||
?>
|
||||
<h1 class="page-title"><i class="fas fa-scroll"></i> 系统日志</h1>
|
||||
|
||||
<?php if ($msg): ?><p class="banner-ok"><i class="fas fa-check-circle"></i> <?= h($msg) ?></p><?php endif; ?>
|
||||
<?php if ($err): ?><p class="form-err"><?= h($err) ?></p><?php endif; ?>
|
||||
|
||||
<!-- 日期选择 -->
|
||||
<div class="panel" style="margin-bottom:18px;">
|
||||
<form method="get" action="" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
|
||||
<label style="font-weight:600;margin:0">选择日期:
|
||||
<input type="date" name="date" value="<?= h($date) ?>" style="padding:6px 10px;border:1px solid var(--outline);border-radius:8px;font-size:.9rem">
|
||||
</label>
|
||||
<button type="submit" class="btn btn-ghost btn-sm"><i class="fas fa-search"></i> 查看</button>
|
||||
<?php if (!empty($dates)): ?>
|
||||
<span class="muted" style="font-size:.82rem">可用日期:
|
||||
<?php foreach (array_slice($dates, 0, 14) as $d): ?>
|
||||
<a href="?date=<?= h($d) ?>" class="<?= $d === $date ? 'btn btn-sm btn-primary' : 'btn btn-ghost btn-sm' ?>" style="padding:2px 8px;font-size:.78rem"><?= h($d) ?></a>
|
||||
<?php endforeach; ?>
|
||||
<?php if (count($dates) > 14): ?><span class="muted">... 等 <?= count($dates) ?> 天</span><?php endif; ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>
|
||||
<i class="fas fa-file-lines"></i> <?= h($date) ?>.log
|
||||
<?php if ($totalLines > 0): ?>
|
||||
<span class="muted" style="font-weight:400;font-size:.85rem">(共 <?= number_format($totalLines) ?> 条)</span>
|
||||
<?php endif; ?>
|
||||
</h3>
|
||||
|
||||
<?php if (empty($lines) && empty($err)): ?>
|
||||
<p class="empty">当天暂无日志记录。</p>
|
||||
<p class="muted" style="font-size:.85rem">
|
||||
日志级别:DEBUG / INFO / WARN / ERROR。当前模式:<strong><?= defined('DEBUG') && DEBUG ? 'DEBUG(记录全部)' : 'INFO(及以上)' ?></strong><br>
|
||||
可在 config.php 中修改 <code>define('DEBUG', true)</code> 以开启 DEBUG 级别记录。
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<!-- 分页 -->
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;font-size:.88rem">
|
||||
<?php if ($page > 1): ?>
|
||||
<a href="?date=<?= h($date) ?>&page=<?= $page - 1 ?>" class="btn btn-ghost btn-sm">« 上一页</a>
|
||||
<?php endif; ?>
|
||||
<span class="muted">第 <strong><?= $page ?></strong> / <?= $totalPages ?> 页</span>
|
||||
<?php if ($page < $totalPages): ?>
|
||||
<a href="?date=<?= h($date) ?>&page=<?= $page + 1 ?>" class="btn btn-ghost btn-sm">下一页 »</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="log-viewer">
|
||||
<table class="data-table">
|
||||
<thead><tr><th style="width:160px">时间</th><th style="width:70px">级别</th><th style="width:140px">来源</th><th>内容</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($lines as $line):
|
||||
$line = trim($line);
|
||||
if ($line === '') continue;
|
||||
// 解析格式: [2026-08-13 10:30:00] [WARN] [file.php:42] message [ctx]
|
||||
preg_match('#^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]\s*\[(\w+)\](?:\s*\[([^\]]+)\])?\s*(.+)$#sU', $line, $m);
|
||||
$time = $m[1] ?? '';
|
||||
$level = $m[2] ?? '';
|
||||
$source = $m[3] ?? '';
|
||||
$body = $m[4] ?? $line;
|
||||
|
||||
$levelClass = match($level) {
|
||||
'DEBUG' => 'log-debug',
|
||||
'INFO' => 'log-info',
|
||||
'WARN' => 'log-warn',
|
||||
'ERROR' => 'log-error',
|
||||
default => ''
|
||||
};
|
||||
?>
|
||||
<tr class="<?= $levelClass ?>">
|
||||
<td><code style="font-size:.8rem;white-space:nowrap"><?= h($time) ?: '—' ?></code></td>
|
||||
<td><span class="badge-status status-<?= strtolower($level) ?>"><?= h($level) ?></span></td>
|
||||
<td><code style="font-size:.78rem;color:var(--muted)"><?= h($source) ?: '' ?></code></td>
|
||||
<td style="word-break:break-all;font-family:monospace;font-size:.84rem"><?= h($body) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 底部分页 -->
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-top:12px;font-size:.88rem">
|
||||
<?php if ($page > 1): ?>
|
||||
<a href="?date=<?= h($date) ?>&page=<?= $page - 1 ?>" class="btn btn-ghost btn-sm">« 上一页</a>
|
||||
<?php endif; ?>
|
||||
<span class="muted">第 <strong><?= $page ?></strong> / <?= $totalPages ?> 页</span>
|
||||
<?php if ($page < $totalPages): ?>
|
||||
<a href="?date=<?= h($date) ?>&page=<?= $page + 1 ?>" class="btn btn-ghost btn-sm">下一页 »</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.log-viewer { overflow-x: auto; }
|
||||
.log-viewer table { font-size:.85rem; }
|
||||
.log-debug { opacity: 0.55; }
|
||||
.log-info { /* default */ }
|
||||
.log-warn { background: color-mix(in srgb, var(--warn) 8%, transparent); }
|
||||
.log-error { background: color-mix(in srgb, var(--danger) 8%, transparent); }
|
||||
.status-debug { background:#909399;color:#fff }
|
||||
.status-info { background:var(--primary);color:#fff }
|
||||
.status-warn { background:var(--warn);color:#000 }
|
||||
.status-error { background:var(--danger);color:#fff }
|
||||
</style>
|
||||
<?php adminFooter(); ?>
|
||||
@@ -0,0 +1,306 @@
|
||||
<?php
|
||||
// 后台订单管理:列表 + 状态流转 + 查看详情 + 发货信息填写
|
||||
require __DIR__ . '/../includes/init.php';
|
||||
requireStaff();
|
||||
require '_common.php';
|
||||
$isAdmin = isAdminStaff();
|
||||
|
||||
$db = db();
|
||||
$msg = '';
|
||||
$err = '';
|
||||
// 客服账号仅可查看,禁止写操作
|
||||
if (!$isAdmin && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$err = '客服账号仅可查看,如需操作请联系管理员。';
|
||||
}
|
||||
$allowed = ['pending', 'paid', 'shipped', 'completed', 'cancelled'];
|
||||
|
||||
// 行内状态变更
|
||||
if (isset($_POST['update_status'])) {
|
||||
verifyCsrf();
|
||||
$id = (int) ($_POST['id'] ?? 0);
|
||||
$status = $_POST['status'] ?? '';
|
||||
if (!in_array($status, $allowed, true)) {
|
||||
$err = '非法的订单状态';
|
||||
} else {
|
||||
$db->prepare('UPDATE ' . tn('orders') . ' SET status = ? WHERE id = ?')->execute([$status, $id]);
|
||||
$msg = '订单 #' . $id . ' 状态已更新为「' . orderStatusLabel($status) . '」';
|
||||
}
|
||||
}
|
||||
|
||||
// 发货:填写连接/登录信息
|
||||
if (isset($_POST['ship'])) {
|
||||
verifyCsrf();
|
||||
$id = (int) ($_POST['id'] ?? 0);
|
||||
$serverIp = trim($_POST['server_ip'] ?? '');
|
||||
$connAddr = trim($_POST['conn_addr'] ?? '');
|
||||
$loginUser = trim($_POST['login_user'] ?? '');
|
||||
$loginPass = trim($_POST['login_pass'] ?? '');
|
||||
$remark = trim($_POST['remark'] ?? '');
|
||||
$activated = isset($_POST['activated']) ? 1 : 0;
|
||||
$productInfo = trim($_POST['product_info'] ?? '');
|
||||
|
||||
$chk = $db->prepare('SELECT id, status FROM ' . tn('orders') . ' WHERE id = ?');
|
||||
$chk->execute([$id]);
|
||||
$ord = $chk->fetch();
|
||||
if (!$ord) {
|
||||
$err = '订单不存在';
|
||||
} else {
|
||||
// 已存在发货记录则更新,否则插入
|
||||
$ex = $db->prepare('SELECT id FROM ' . tn('order_deliveries') . ' WHERE order_id = ? ORDER BY id DESC LIMIT 1');
|
||||
$ex->execute([$id]);
|
||||
$exId = $ex->fetchColumn();
|
||||
if ($exId) {
|
||||
$db->prepare('UPDATE ' . tn('order_deliveries') . ' SET server_ip=?, conn_addr=?, login_user=?, login_pass=?, remark=?, activated=?, product_info=? WHERE id=?')
|
||||
->execute([$serverIp, $connAddr, $loginUser, $loginPass, $remark, $activated, $productInfo, $exId]);
|
||||
} else {
|
||||
$db->prepare('INSERT INTO ' . tn('order_deliveries') . ' (order_id, server_ip, conn_addr, login_user, login_pass, remark, activated, product_info, created_at) VALUES (?,?,?,?,?,?,?,?,NOW())')
|
||||
->execute([$id, $serverIp, $connAddr, $loginUser, $loginPass, $remark, $activated, $productInfo]);
|
||||
}
|
||||
// 由「已付款」发货后自动转为「已发货」
|
||||
if ($ord['status'] === 'paid') {
|
||||
$db->prepare('UPDATE ' . tn('orders') . ' SET status = ? WHERE id = ?')->execute(['shipped', $id]);
|
||||
}
|
||||
$msg = '发货信息已保存(订单 #' . $id . ')。';
|
||||
// 通过 SMTP 通知客户(失败不影响保存)
|
||||
$u = $db->prepare('SELECT u.email, u.username FROM ' . tn('orders') . ' o LEFT JOIN ' . tn('users') . ' u ON u.id = o.user_id WHERE o.id = ?');
|
||||
$u->execute([$id]);
|
||||
$urow = $u->fetch();
|
||||
if ($urow && !empty($urow['email'])) {
|
||||
$subject = '[' . SITE_NAME . '] 你的订单 #' . $id . ' 已发货';
|
||||
$body = '<div style="font-family:sans-serif;max-width:560px;margin:auto">'
|
||||
. '<h2 style="color:#1a73e8">订单已发货</h2>'
|
||||
. '<p>你好 ' . h($urow['username']) . ',你购买的以下服务/商品已开通:</p>'
|
||||
. '<div style="background:#f5f7fb;border-left:4px solid #1a73e8;padding:12px 16px;margin:10px 0;">'
|
||||
. '<p style="margin:4px 0;">连接地址:' . h($connAddr) . '</p>'
|
||||
. '<p style="margin:4px 0;">登录账户:' . h($loginUser) . '</p>'
|
||||
. '<p style="margin:4px 0;">可在「我的订单 → 详情」中查看完整登录凭据。</p>'
|
||||
. '</div>'
|
||||
. '<p class="muted">如有问题请到「我的工单」联系客服。</p></div>';
|
||||
fnwSendMail($urow['email'], $subject, $body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 修改到期时间(后台)
|
||||
if (isset($_POST['save_expire'])) {
|
||||
verifyCsrf();
|
||||
$id = (int) ($_POST['id'] ?? 0);
|
||||
$clear = isset($_POST['clear_expire']);
|
||||
$expRaw = trim($_POST['expires_at'] ?? '');
|
||||
$newExp = null;
|
||||
if (!$clear) {
|
||||
if ($expRaw === '') {
|
||||
$err = '请填写新的到期时间,或勾选「清除到期时间」';
|
||||
} else {
|
||||
$t = strtotime(str_replace('T', ' ', $expRaw));
|
||||
if ($t === false) {
|
||||
$err = '到期时间格式不正确';
|
||||
} else {
|
||||
$newExp = date('Y-m-d H:i:s', $t);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($err === '') {
|
||||
$db->prepare('UPDATE ' . tn('orders') . ' SET expires_at = ?, expire_notify_sent = 0 WHERE id = ?')
|
||||
->execute([$newExp, $id]);
|
||||
$msg = '订单 #' . $id . ' 的到期时间已更新' . ($newExp ? '为 ' . $newExp : '(已清除)') . ',到期提醒将重新计算。';
|
||||
}
|
||||
}
|
||||
|
||||
// 删除订单(含明细与发货信息)
|
||||
if (isset($_POST['delete'])) {
|
||||
verifyCsrf();
|
||||
$id = (int) ($_POST['delete'] ?? 0);
|
||||
$db->prepare('DELETE FROM ' . tn('order_deliveries') . ' WHERE order_id = ?')->execute([$id]);
|
||||
$db->prepare('DELETE FROM ' . tn('order_items') . ' WHERE order_id = ?')->execute([$id]);
|
||||
$db->prepare('DELETE FROM ' . tn('orders') . ' WHERE id = ?')->execute([$id]);
|
||||
$msg = '订单 #' . $id . ' 已删除';
|
||||
}
|
||||
|
||||
$filter = trim($_GET['status'] ?? '');
|
||||
$where = $filter !== '' ? 'WHERE o.status = ?' : '';
|
||||
$params = $filter !== '' ? [$filter] : [];
|
||||
$stmt = $db->prepare(
|
||||
'SELECT o.*, u.username, u.email AS u_email FROM ' . tn('orders') . ' o
|
||||
LEFT JOIN ' . tn('users') . ' u ON u.id = o.user_id
|
||||
' . $where . ' ORDER BY o.created_at DESC'
|
||||
);
|
||||
$stmt->execute($params);
|
||||
$list = $stmt->fetchAll();
|
||||
|
||||
// 预取商品明细
|
||||
$itemsOf = [];
|
||||
foreach ($list as $o) {
|
||||
$it = $db->prepare('SELECT name, qty, period_days FROM ' . tn('order_items') . ' WHERE order_id = ?');
|
||||
$it->execute([$o['id']]);
|
||||
$itemsOf[$o['id']] = $it->fetchAll();
|
||||
}
|
||||
|
||||
// 查看单条 + 发货
|
||||
$view = isset($_GET['view']) ? (int) $_GET['view'] : 0;
|
||||
$order = $delivery = null;
|
||||
if ($view > 0) {
|
||||
$ostmt = $db->prepare('SELECT o.*, u.username, u.email AS u_email FROM ' . tn('orders') . ' o LEFT JOIN ' . tn('users') . ' u ON u.id = o.user_id WHERE o.id = ?');
|
||||
$ostmt->execute([$view]);
|
||||
$order = $ostmt->fetch();
|
||||
if ($order) {
|
||||
$istmt = $db->prepare('SELECT * FROM ' . tn('order_items') . ' WHERE order_id = ?');
|
||||
$istmt->execute([$order['id']]);
|
||||
$order['items'] = $istmt->fetchAll();
|
||||
$dstmt = $db->prepare('SELECT * FROM ' . tn('order_deliveries') . ' WHERE order_id = ? ORDER BY id DESC LIMIT 1');
|
||||
$dstmt->execute([$order['id']]);
|
||||
$delivery = $dstmt->fetch();
|
||||
}
|
||||
}
|
||||
|
||||
adminHeader('订单管理', 'orders');
|
||||
?>
|
||||
|
||||
<h1 class="page-title"><i class="fas fa-receipt"></i> 订单管理</h1>
|
||||
|
||||
<?php if ($msg): ?><p class="banner-ok"><i class="fas fa-check-circle"></i> <?= h($msg) ?></p><?php endif; ?>
|
||||
<?php if ($err): ?><p class="form-err"><?= h($err) ?></p><?php endif; ?>
|
||||
|
||||
<div class="cat-chips">
|
||||
<a href="orders.php" class="chip <?= $filter===''?'active':'' ?>">全部</a>
|
||||
<?php foreach ($allowed as $s): ?>
|
||||
<a href="orders.php?status=<?= $s ?>" class="chip <?= $filter===$s?'active':'' ?>"><?= orderStatusLabel($s) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($order): ?>
|
||||
<div class="panel">
|
||||
<div class="od-head">
|
||||
<div>
|
||||
<div class="od-no">订单号:<?= h($order['order_no']) ?></div>
|
||||
<div class="od-time"><?= date('Y-m-d H:i', strtotime($order['created_at'])) ?></div>
|
||||
</div>
|
||||
<span class="badge-status status-<?= h($order['status']) ?>"><?= orderStatusLabel($order['status']) ?></span>
|
||||
</div>
|
||||
|
||||
<div class="od-info">
|
||||
<div class="od-block">
|
||||
<div class="od-label">订单发起人</div>
|
||||
<div class="od-line"><?= h($order['username'] ?? '未知') ?></div>
|
||||
<div class="od-line">账号邮箱:<?= h($order['u_email'] ?? '未填写') ?></div>
|
||||
</div>
|
||||
<div class="od-block">
|
||||
<div class="od-label">联系信息</div>
|
||||
<div class="od-line">联系人:<?= $order['contact'] !== '' ? h($order['contact']) : '—' ?></div>
|
||||
<div class="od-line">联系邮箱:<?= $order['contact_email'] !== '' ? h($order['contact_email']) : '—' ?></div>
|
||||
<div class="od-line">收货地址:<?= $order['address'] !== '' ? h($order['address']) : '(未填写)' ?></div>
|
||||
</div>
|
||||
<div class="od-block">
|
||||
<div class="od-label">周期 / 到期</div>
|
||||
<div class="od-line">周期:<?= periodLabel($order['period_days']) ?></div>
|
||||
<div class="od-line">到期时间:<?= expiryBadge($order['expires_at']) ?></div>
|
||||
<?php if (!empty($order['expire_notify_sent'])): ?><div class="od-line muted">已发送「到期前提醒」邮件</div><?php endif; ?>
|
||||
<form method="post" action="" class="expire-edit">
|
||||
<?= csrfField() ?>
|
||||
<input type="hidden" name="id" value="<?= (int) $order['id'] ?>">
|
||||
<label class="expire-field">修改到期时间
|
||||
<input type="datetime-local" name="expires_at" value="<?= $order['expires_at'] ? date('Y-m-d\TH:i', strtotime($order['expires_at'])) : '' ?>">
|
||||
</label>
|
||||
<label class="expire-clear"><input type="checkbox" name="clear_expire" value="1"> 清除到期时间</label>
|
||||
<button type="submit" name="save_expire" class="btn btn-primary btn-sm"><i class="fas fa-save"></i> 保存到期时间</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="od-block">
|
||||
<div class="od-label">备注</div>
|
||||
<div class="od-line"><?= $order['note'] !== '' ? nl2br(h($order['note'])) : '(无)' ?></div>
|
||||
</div>
|
||||
<div class="od-block">
|
||||
<div class="od-label">支付方式</div>
|
||||
<div class="od-line"><?= payTypeLabel($order['pay_type']) ?><?= $order['pay_type']==='points' ? '(消耗 ' . (int)$order['points_used'] . ' 积分)' : '' ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>商品明细</h3>
|
||||
<table class="data-table">
|
||||
<thead><tr><th>商品</th><th>单价</th><th>数量</th><th>小计</th><th>周期</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach (($order['items'] ?? []) as $it): ?>
|
||||
<tr>
|
||||
<td><?= h($it['name']) ?></td>
|
||||
<td><?= money($it['price']) ?></td>
|
||||
<td><?= (int) $it['qty'] ?></td>
|
||||
<td><?= money($it['subtotal']) ?></td>
|
||||
<td><?= periodLabel($it['period_days']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3><i class="fas fa-truck"></i> 发货信息(连接地址 / 登录凭据 / 商品信息)</h3>
|
||||
<?php if ($delivery): ?>
|
||||
<div class="ship-existing">
|
||||
<p class="muted">已填写,当前内容如下(可重新提交覆盖):</p>
|
||||
<div class="od-line">激活状态:<span class="badge-<?= !empty($delivery['activated']) ? 'on' : 'off' ?>"><?= !empty($delivery['activated']) ? '已激活' : '未激活' ?></span></div>
|
||||
<div class="od-line">服务器 IP:<?= h($delivery['server_ip']) ?: '—' ?></div>
|
||||
<div class="od-line">连接地址:<?= h($delivery['conn_addr']) ?: '—' ?></div>
|
||||
<div class="od-line">登录账户:<?= h($delivery['login_user']) ?: '—' ?></div>
|
||||
<div class="od-line">登录密码:<?= $delivery['login_pass'] !== '' ? h($delivery['login_pass']) : '—' ?></div>
|
||||
<?php if ($delivery['product_info'] !== ''): ?><div class="od-line">商品信息:<?= nl2br(h($delivery['product_info'])) ?></div><?php endif; ?>
|
||||
<?php if ($delivery['remark'] !== ''): ?><div class="od-line">备注:<?= nl2br(h($delivery['remark'])) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="" class="grid-form ship-form">
|
||||
<?= csrfField() ?>
|
||||
<input type="hidden" name="id" value="<?= (int) $order['id'] ?>">
|
||||
<label>服务器 IP<input type="text" name="server_ip" value="<?= $delivery ? h($delivery['server_ip']) : '' ?>" placeholder="如 1.2.3.4"></label>
|
||||
<label>连接地址 / 域名<input type="text" name="conn_addr" value="<?= $delivery ? h($delivery['conn_addr']) : '' ?>" placeholder="如 mc.example.com:25565"></label>
|
||||
<label>登录账户<input type="text" name="login_user" value="<?= $delivery ? h($delivery['login_user']) : '' ?>" placeholder="如 player01"></label>
|
||||
<label>登录密码<input type="text" name="login_pass" value="<?= $delivery ? h($delivery['login_pass']) : '' ?>" placeholder="如 ********"></label>
|
||||
<label>激活状态
|
||||
<select name="activated">
|
||||
<option value="0" <?= $delivery && empty($delivery['activated']) ? 'selected' : '' ?>>未激活</option>
|
||||
<option value="1" <?= $delivery && !empty($delivery['activated']) ? 'selected' : '' ?>>已激活</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="span2">商品信息(展示给用户)<textarea name="product_info" rows="3" placeholder="选填,将展示给该用户,如开通说明、使用须知"><?= $delivery ? h($delivery['product_info']) : '' ?></textarea></label>
|
||||
<label class="span2">备注<textarea name="remark" rows="2" placeholder="选填,仅内部可见"><?= $delivery ? h($delivery['remark']) : '' ?></textarea></label>
|
||||
<div class="form-actions span2">
|
||||
<button type="submit" name="ship" class="btn btn-primary"><i class="fas fa-paper-plane"></i> 保存发货信息<?= $delivery ? '(覆盖)' : '' ?></button>
|
||||
<a href="orders.php" class="btn btn-ghost">返回列表</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="panel">
|
||||
<?php if (empty($list)): ?>
|
||||
<p class="empty">没有符合条件的订单。</p>
|
||||
<?php else: ?>
|
||||
<table class="data-table">
|
||||
<thead><tr><th>订单号</th><th>用户</th><th>商品</th><th>金额</th><th>周期</th><th>支付</th><th>状态</th><th>时间</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($list as $o): ?>
|
||||
<tr>
|
||||
<td><?= h($o['order_no']) ?></td>
|
||||
<td><?= h($o['username'] ?? '游客') ?></td>
|
||||
<td class="cell-items">
|
||||
<?php foreach (($itemsOf[$o['id']] ?? []) as $it): ?>
|
||||
<div><?= h($it['name']) ?> ×<?= (int)$it['qty'] ?></div>
|
||||
<?php endforeach; ?>
|
||||
</td>
|
||||
<td><?= money($o['total']) ?></td>
|
||||
<td><?= periodLabel($o['period_days']) ?></td>
|
||||
<td><span class="pay-<?= h($o['pay_type']) ?>"><?= payTypeLabel($o['pay_type']) ?><?= $o['pay_type']==='points' ? ' · ' . (int)$o['points_used'] . '分' : '' ?></span></td>
|
||||
<td><span class="badge-status status-<?= h($o['status']) ?>"><?= orderStatusLabel($o['status']) ?></span></td>
|
||||
<td><?= date('m-d H:i', strtotime($o['created_at'])) ?></td>
|
||||
<td class="row-actions">
|
||||
<a href="orders.php?view=<?= (int)$o['id'] ?>" class="mini-btn" title="查看 / 发货"><i class="fas fa-eye"></i></a>
|
||||
<form method="post" action="" style="display:inline" onsubmit="return confirm('确定删除该订单?');">
|
||||
<?= csrfField() ?>
|
||||
<button type="submit" name="delete" value="<?= (int)$o['id'] ?>" class="mini-btn danger" title="删除订单"><i class="fas fa-trash"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php adminFooter(); ?>
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
// 后台商品管理:列表 + 新增 / 编辑 / 删除
|
||||
require __DIR__ . '/../includes/init.php';
|
||||
requireStaff();
|
||||
require '_common.php';
|
||||
$isAdmin = isAdminStaff();
|
||||
|
||||
$db = db();
|
||||
$msg = '';
|
||||
$err = '';
|
||||
// 客服账号仅可查看,禁止写操作
|
||||
if (!$isAdmin && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$err = '客服账号仅可查看,如需操作请联系管理员。';
|
||||
}
|
||||
// 分类选项来自「商品分组」表,并兜底保留「其他」
|
||||
$groups = $db->query('SELECT name FROM ' . tn('product_groups') . ' ORDER BY sort_order ASC, name ASC')->fetchAll(PDO::FETCH_COLUMN);
|
||||
$catOptions = $groups;
|
||||
if (!in_array('其他', $catOptions, true)) $catOptions[] = '其他';
|
||||
|
||||
// 删除
|
||||
if (isset($_POST['delete'])) {
|
||||
verifyCsrf();
|
||||
$id = (int) $_POST['delete'];
|
||||
$cnt = $db->prepare('SELECT COUNT(*) FROM ' . tn('order_items') . ' WHERE product_id = ?');
|
||||
$cnt->execute([$id]);
|
||||
if ((int) $cnt->fetchColumn() > 0) {
|
||||
$err = '该商品已有订单记录,无法删除,可将其下架。';
|
||||
} else {
|
||||
$db->prepare('DELETE FROM ' . tn('products') . ' WHERE id = ?')->execute([$id]);
|
||||
$msg = '商品已删除';
|
||||
}
|
||||
}
|
||||
|
||||
// 保存(新增 / 编辑)
|
||||
if (isset($_POST['save'])) {
|
||||
verifyCsrf();
|
||||
$id = (int) ($_POST['id'] ?? 0);
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$stock = max(0, (int) ($_POST['stock'] ?? 0));
|
||||
$periodDays = max(0, (int) ($_POST['period_days'] ?? 0));
|
||||
$pointsPrice= max(0, (int) ($_POST['points_price'] ?? 0));
|
||||
$category = trim($_POST['category'] ?? '其他');
|
||||
$icon = trim($_POST['icon'] ?? 'fa-box');
|
||||
$image = trim($_POST['image'] ?? '');
|
||||
$description= trim($_POST['description'] ?? '');
|
||||
$status = isset($_POST['status']) ? 1 : 0;
|
||||
|
||||
if ($name === '') $err = '请填写商品名称';
|
||||
elseif (!in_array($category, $catOptions, true)) $err = '分类不合法';
|
||||
|
||||
if ($err === '') {
|
||||
if ($id > 0) {
|
||||
$db->prepare('UPDATE ' . tn('products') . ' SET name=?,stock=?,period_days=?,points_price=?,category=?,icon=?,image=?,description=?,status=? WHERE id=?')
|
||||
->execute([$name, $stock, $periodDays, $pointsPrice, $category, $icon, $image, $description, $status, $id]);
|
||||
$msg = '商品已更新';
|
||||
} else {
|
||||
$db->prepare('INSERT INTO ' . tn('products') . ' (name,stock,period_days,points_price,category,icon,image,description,status,created_at) VALUES (?,?,?,?,?,?,?,?,?,NOW())')
|
||||
->execute([$name, $stock, $periodDays, $pointsPrice, $category, $icon, $image, $description, $status]);
|
||||
$msg = '商品已添加';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑回填
|
||||
$edit = null;
|
||||
if (isset($_GET['edit'])) {
|
||||
$stmt = $db->prepare('SELECT * FROM ' . tn('products') . ' WHERE id = ?');
|
||||
$stmt->execute([(int) $_GET['edit']]);
|
||||
$edit = $stmt->fetch();
|
||||
}
|
||||
$showForm = ($edit !== null) || isset($_GET['act']) && $_GET['act'] === 'add';
|
||||
|
||||
// 列表
|
||||
$list = $db->query('SELECT * FROM ' . tn('products') . ' ORDER BY created_at DESC')->fetchAll();
|
||||
|
||||
adminHeader('商品管理', 'products');
|
||||
?>
|
||||
|
||||
<h1 class="page-title"><i class="fas fa-box"></i> 商品管理</h1>
|
||||
|
||||
<?php if ($msg): ?><p class="banner-ok"><i class="fas fa-check-circle"></i> <?= h($msg) ?></p><?php endif; ?>
|
||||
<?php if ($err): ?><p class="form-err"><?= h($err) ?></p><?php endif; ?>
|
||||
|
||||
<?php if ($showForm): ?>
|
||||
<div class="panel">
|
||||
<h3><?= $edit ? '编辑商品 #' . (int)$edit['id'] : '新增商品' ?></h3>
|
||||
<form method="post" action="" class="grid-form">
|
||||
<?= csrfField() ?>
|
||||
<input type="hidden" name="id" value="<?= $edit ? (int)$edit['id'] : 0 ?>">
|
||||
<label>商品名称 *<input type="text" name="name" value="<?= $edit ? h($edit['name']) : '' ?>" required></label>
|
||||
<label>库存 *<input type="number" min="0" name="stock" value="<?= $edit ? (int)$edit['stock'] : 0 ?>" required></label>
|
||||
<label>周期(天)<input type="number" min="0" name="period_days" value="<?= $edit ? (int)$edit['period_days'] : 0 ?>" placeholder="0=一次性/实物"></label>
|
||||
<label>积分价(分)<input type="number" min="0" name="points_price" value="<?= $edit ? (int)$edit['points_price'] : 0 ?>" placeholder="0=直接下单,不扣积分"></label>
|
||||
<label>分类
|
||||
<select name="category">
|
||||
<?php if ($edit && !in_array($edit['category'], $catOptions, true)): ?>
|
||||
<option value="<?= h($edit['category']) ?>" selected><?= h($edit['category']) ?>(已停用分组)</option>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($catOptions as $c): ?>
|
||||
<option value="<?= h($c) ?>" <?= $edit && $edit['category']===$c ? 'selected' : '' ?>><?= h($c) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label>图标类名<input type="text" name="icon" value="<?= $edit ? h($edit['icon']) : 'fa-box' ?>" placeholder="如 fa-box"></label>
|
||||
<label>图片链接<input type="text" name="image" value="<?= $edit ? h($edit['image']) : '' ?>" placeholder="留空则显示图标"></label>
|
||||
<label class="span2">商品描述<textarea name="description" rows="3"><?= $edit ? h($edit['description']) : '' ?></textarea></label>
|
||||
<label class="checkbox"><input type="checkbox" name="status" <?= !$edit || $edit['status']==1 ? 'checked' : '' ?>> 上架销售</label>
|
||||
<div class="form-actions span2">
|
||||
<button type="submit" name="save" class="btn btn-primary"><i class="fas fa-save"></i> 保存</button>
|
||||
<a href="products.php" class="btn btn-ghost">取消</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<h3>商品列表(共 <?= count($list) ?> 件)</h3>
|
||||
<?php if (!$showForm): ?><a href="products.php?act=add" class="btn btn-primary"><i class="fas fa-plus"></i> 新增商品</a><?php endif; ?>
|
||||
</div>
|
||||
<?php if (empty($list)): ?>
|
||||
<p class="empty">还没有商品,点上方「新增商品」添加吧。</p>
|
||||
<?php else: ?>
|
||||
<table class="data-table">
|
||||
<thead><tr><th>ID</th><th>名称</th><th>分类</th><th>积分价</th><th>库存</th><th>周期</th><th>状态</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($list as $p): ?>
|
||||
<tr>
|
||||
<td><?= (int)$p['id'] ?></td>
|
||||
<td><?= h($p['name']) ?></td>
|
||||
<td><?= h($p['category']) ?></td>
|
||||
<td><?= (int)$p['points_price'] > 0 ? (int)$p['points_price'] . ' 分' : '直接下单' ?></td>
|
||||
<td><?= (int)$p['stock'] ?></td>
|
||||
<td><?= periodLabel($p['period_days']) ?></td>
|
||||
<td><?= $p['status']==1 ? '<span class="badge-on">上架</span>' : '<span class="badge-off">下架</span>' ?></td>
|
||||
<td class="row-actions">
|
||||
<a href="products.php?edit=<?= (int)$p['id'] ?>" class="mini-btn"><i class="fas fa-edit"></i></a>
|
||||
<form method="post" action="" style="display:inline" onsubmit="return confirm('确定删除该商品?');">
|
||||
<?= csrfField() ?>
|
||||
<button type="submit" name="delete" value="<?= (int)$p['id'] ?>" class="mini-btn danger"><i class="fas fa-trash"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php adminFooter(); ?>
|
||||
@@ -0,0 +1,299 @@
|
||||
<?php
|
||||
require __DIR__ . '/../includes/init.php';
|
||||
requireStaff();
|
||||
require '_common.php';
|
||||
$isAdmin = isAdminStaff();
|
||||
$admin = currentAdmin();
|
||||
$msg = '';
|
||||
$err = '';
|
||||
// 客服账号仅可查看,禁止任何写操作(直接丢弃写请求键)
|
||||
if (!$isAdmin && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$err = '客服账号仅可查看,如需操作请联系管理员。';
|
||||
foreach (['savesite','changepw','savesmtp','saveabout','savepoints','savepay_ali','savepay_wx','savefavicon','delfavicon'] as $k) {
|
||||
unset($_POST[$k]);
|
||||
}
|
||||
}
|
||||
if (isset($_POST['savesite'])) {
|
||||
verifyCsrf();
|
||||
$siteName = trim($_POST['site_name'] ?? '');
|
||||
if ($siteName === '') {
|
||||
$err = '站点名称不能为空。';
|
||||
} else {
|
||||
saveSetting('site_name', $siteName);
|
||||
$GLOBALS['_site_name'] = null; // 清空全局缓存
|
||||
$msg = '站点名称已修改。';
|
||||
}
|
||||
}
|
||||
if (isset($_POST['changepw'])) {
|
||||
verifyCsrf();
|
||||
$cur = $_POST['cur'] ?? '';
|
||||
$new = $_POST['new'] ?? '';
|
||||
$new2 = $_POST['new2'] ?? '';
|
||||
$stmt = db()->prepare('SELECT password FROM ' . tn('users') . ' WHERE id = ?');
|
||||
$stmt->execute([$admin['id']]);
|
||||
$hash = $stmt->fetchColumn();
|
||||
if (!password_verify($cur, $hash)) {
|
||||
$err = '当前密码错误。';
|
||||
} elseif (strlen($new) < 6) {
|
||||
$err = '新密码至少 6 位。';
|
||||
} elseif ($new !== $new2) {
|
||||
$err = '两次输入的新密码不一致。';
|
||||
} else {
|
||||
$newHash = password_hash($new, PASSWORD_DEFAULT);
|
||||
db()->prepare('UPDATE ' . tn('users') . ' SET password = ? WHERE id = ?')
|
||||
->execute([$newHash, $admin['id']]);
|
||||
$msg = '管理员密码已修改。';
|
||||
}
|
||||
}
|
||||
if (isset($_POST['savesmtp'])) {
|
||||
verifyCsrf();
|
||||
foreach (['smtp_host', 'smtp_port', 'smtp_enc', 'smtp_user', 'smtp_pass', 'smtp_from', 'smtp_fromname', 'notify_emails'] as $k) {
|
||||
saveSetting($k, trim((string) ($_POST[$k] ?? '')));
|
||||
}
|
||||
$msg = 'SMTP 配置已保存!';
|
||||
}
|
||||
if (isset($_POST['saveabout'])) {
|
||||
verifyCsrf();
|
||||
saveSetting('mall_about', trim((string) ($_POST['mall_about'] ?? '')));
|
||||
$msg = '商城介绍已保存!';
|
||||
}
|
||||
if (isset($_POST['savepoints'])) {
|
||||
verifyCsrf();
|
||||
saveSetting('points_enabled', isset($_POST['points_enabled']) ? '1' : '0');
|
||||
saveSetting('points_sign', max(0, (int) ($_POST['points_sign'] ?? 0)));
|
||||
saveSetting('points_invite', max(0, (int) ($_POST['points_invite'] ?? 0)));
|
||||
$msg = '积分设置已保存';
|
||||
}
|
||||
if (isset($_POST['savepay_ali'])) {
|
||||
verifyCsrf();
|
||||
saveSetting('pay_alipay_enabled', isset($_POST['pay_alipay_enabled']) ? '1' : '0');
|
||||
saveSetting('pay_alipay_appid', trim((string) ($_POST['pay_alipay_appid'] ?? '')));
|
||||
saveSetting('pay_alipay_private_key', trim((string) ($_POST['pay_alipay_private_key'] ?? '')));
|
||||
saveSetting('pay_alipay_public_key', trim((string) ($_POST['pay_alipay_public_key'] ?? '')));
|
||||
saveSetting('pay_alipay_sandbox', isset($_POST['pay_alipay_sandbox']) ? '1' : '0');
|
||||
$msg = '支付宝配置已保存!';
|
||||
}
|
||||
if (isset($_POST['savepay_wx'])) {
|
||||
verifyCsrf();
|
||||
saveSetting('pay_wechat_enabled', isset($_POST['pay_wechat_enabled']) ? '1' : '0');
|
||||
saveSetting('pay_wechat_mch_id', trim((string) ($_POST['pay_wechat_mch_id'] ?? '')));
|
||||
saveSetting('pay_wechat_api_key', trim((string) ($_POST['pay_wechat_api_key'] ?? '')));
|
||||
saveSetting('pay_wechat_appid', trim((string) ($_POST['pay_wechat_appid'] ?? '')));
|
||||
$msg = '微信支付配置已保存!';
|
||||
}
|
||||
if (isset($_POST['savefavicon']) || isset($_POST['delfavicon'])) {
|
||||
verifyCsrf();
|
||||
if (!$isAdmin) {
|
||||
$err = '客服账号仅可查看,如需操作请联系管理员。';
|
||||
} elseif (isset($_POST['delfavicon'])) {
|
||||
$old = getSetting('favicon', '');
|
||||
if ($old && file_exists(__DIR__ . '/../' . $old)) { @unlink(__DIR__ . '/../' . $old); }
|
||||
saveSetting('favicon', '');
|
||||
$msg = '已恢复默认图标。';
|
||||
} else {
|
||||
if (empty($_FILES['favicon_file']) || $_FILES['favicon_file']['error'] !== UPLOAD_ERR_OK) {
|
||||
$err = '请选择要上传的 .ico 文件。';
|
||||
} else {
|
||||
$f = $_FILES['favicon_file'];
|
||||
$ext = strtolower(pathinfo($f['name'], PATHINFO_EXTENSION));
|
||||
if ($ext !== 'ico') {
|
||||
$err = '仅支持 .ico 格式的图标文件。';
|
||||
} elseif ($f['size'] > 256 * 1024) {
|
||||
$err = '文件过大,请控制在 256KB 以内。';
|
||||
} else {
|
||||
// 校验 ICO 文件头:00 00 01 00
|
||||
$head = file_get_contents($f['tmp_name'], false, null, 0, 4);
|
||||
if ($head !== "\x00\x00\x01\x00") {
|
||||
$err = '文件内容不是有效的 ICO 图标(文件头校验失败)。';
|
||||
} else {
|
||||
$dest = __DIR__ . '/../assets/favicon.ico';
|
||||
if (!move_uploaded_file($f['tmp_name'], $dest) && !copy($f['tmp_name'], $dest)) {
|
||||
$err = '保存失败,请检查 assets/ 目录写入权限。';
|
||||
} else {
|
||||
saveSetting('favicon', 'assets/favicon.ico');
|
||||
$msg = '网站图标已更新,刷新浏览器即可看到(若未变请强制刷新清缓存)。';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$testResult = null;
|
||||
if (isset($_POST['testmail'])) {
|
||||
verifyCsrf();
|
||||
$to = trim($_POST['test_to'] ?? '');
|
||||
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
||||
$err = '请填写有效的测试收件邮箱。';
|
||||
} else {
|
||||
$cfg = smtpConfig();
|
||||
$body = '<div style="font-family:sans-serif;max-width:520px;margin:auto">'
|
||||
. '<h2 style="color:#2563eb">[' . h(SITE_NAME) . '] SMTP 测试邮件</h2>'
|
||||
. '<p>这是一封由商城后台发送的测试邮件,若你收到了它,说明 SMTP 配置正确。</p>'
|
||||
. '<p class="muted">发送时间:' . date('Y-m-d H:i:s') . '</p></div>';
|
||||
$testResult = fnwSendMail($to, '[' . SITE_NAME . '] SMTP 测试邮件', $body);
|
||||
if ($testResult['ok']) {
|
||||
$msg = '测试邮件已发送,请查收(含 SMTP 会话日志见下方)。';
|
||||
} else {
|
||||
$err = '发送失败:' . ($testResult['error'] ?? '未知错误');
|
||||
}
|
||||
}
|
||||
}
|
||||
$cfg = smtpConfig();
|
||||
adminHeader('设置', 'settings');
|
||||
?>
|
||||
<h1 class="page-title"><i class="fas fa-cog"></i> 设置</h1>
|
||||
<?php if ($msg): ?><p class="banner-ok"><i class="fas fa-check-circle"></i> <?= h($msg) ?></p><?php endif; ?>
|
||||
<?php if ($err): ?><p class="form-err"><?= h($err) ?></p><?php endif; ?>
|
||||
<div class="panel" style="max-width:560px;">
|
||||
<h3>修改管理员密码</h3>
|
||||
<p class="muted">当前管理员:<strong><?= h($admin['username']) ?></strong></p>
|
||||
<form method="post" action="" class="auth-form">
|
||||
<?= csrfField() ?>
|
||||
<label>当前密码<input type="password" name="cur" required></label>
|
||||
<label>新密码<input type="password" name="new" placeholder="至少 6 位。" required></label>
|
||||
<label>确认新密码<input type="password" name="new2" required></label>
|
||||
<button type="submit" name="changepw" class="btn btn-primary">保存修改</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel" style="max-width:680px;">
|
||||
<h3>商城介绍(前台首页展示)</h3>
|
||||
<p class="muted">这段文字会显示在商城首页「关于商城」区块,向访客介绍你的商城。留空则不显示该区块。</p>
|
||||
<form method="post" action="" class="grid-form">
|
||||
<?= csrfField() ?>
|
||||
<label class="span2">商城介绍<textarea name="mall_about" rows="4" placeholder="如:本商城是 XX 团队旗下的……"><?= h(getSetting('mall_about', '')) ?></textarea></label>
|
||||
<div class="form-actions span2">
|
||||
<button type="submit" name="saveabout" class="btn btn-primary"><i class="fas fa-save"></i> 保存商城介绍</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="panel" style="max-width:560px;">
|
||||
<h3><i class="fas fa-store"></i> 站点名称</h3>
|
||||
<p class="muted">该名称会显示在浏览器标题、页面顶部、邮件通知等位置。</p>
|
||||
<form method="post" action="" class="auth-form">
|
||||
<?= csrfField() ?>
|
||||
<label>站点名称
|
||||
<input type="text" name="site_name" value="<?= h(getSetting('site_name', SITE_NAME)) ?>" required>
|
||||
</label>
|
||||
<button type="submit" name="savesite" class="btn btn-primary"><i class="fas fa-save"></i> 保存站点名称</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="panel" style="max-width:560px;">
|
||||
<h3><i class="fas fa-image"></i> 网站图标(Favicon)</h3>
|
||||
<p class="muted">上传 .ico 格式图标(建议 16×16 或 32×32,最大 256KB),将显示在浏览器标签页。留空则使用默认图标。</p>
|
||||
<p style="margin:12px 0;">
|
||||
<?php
|
||||
$favCur = getSetting('favicon', '');
|
||||
if ($favCur && file_exists(__DIR__ . '/../' . $favCur)):
|
||||
?>
|
||||
<img src="../<?= h($favCur) ?>?v=<?= filemtime(__DIR__ . '/../' . $favCur) ?>" alt="当前图标" style="width:32px;height:32px;border:1px solid var(--border);border-radius:6px;vertical-align:middle;">
|
||||
<span class="muted"> 当前图标(<?= h($favCur) ?>)</span>
|
||||
<?php else: ?>
|
||||
<span class="muted">当前:默认图标</span>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<form method="post" action="" enctype="multipart/form-data" class="auth-form">
|
||||
<?= csrfField() ?>
|
||||
<label>选择 .ico 文件
|
||||
<input type="file" name="favicon_file" accept=".ico,image/vnd.microsoft.icon">
|
||||
</label>
|
||||
<div class="form-actions">
|
||||
<button type="submit" name="savefavicon" class="btn btn-primary"><i class="fas fa-upload"></i> 上传并启用</button>
|
||||
</div>
|
||||
</form>
|
||||
<?php if ($favCur): ?>
|
||||
<form method="post" action="" class="auth-form" style="margin-top:8px;">
|
||||
<?= csrfField() ?>
|
||||
<button type="submit" name="delfavicon" class="btn btn-ghost" onclick="return confirm('确定恢复为默认图标?');"><i class="fas fa-undo"></i> 恢复默认</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="panel" style="max-width:680px;">
|
||||
<h3>积分系统设置</h3>
|
||||
<p class="muted">开启后,用户可在前台「每日签到」获取积分、通过邀请链接邀请好友获得积分;商品可设置「积分价」,结算时选择「积分支付」。</p>
|
||||
<form method="post" action="" class="grid-form">
|
||||
<?= csrfField() ?>
|
||||
<label class="checkbox"><input type="checkbox" name="points_enabled" <?= getSetting('points_enabled', '1')==='1' ? 'checked' : '' ?>> 启用积分系统</label>
|
||||
<label>每日签到可得积分<input type="number" min="0" name="points_sign" value="<?= h(getSetting('points_sign', '5')) ?>" placeholder="如 5"></label>
|
||||
<label>邀请好友成功可得积分<input type="number" min="0" name="points_invite" value="<?= h(getSetting('points_invite', '20')) ?>" placeholder="如 20"></label>
|
||||
<div class="form-actions span2">
|
||||
<button type="submit" name="savepoints" class="btn btn-primary"><i class="fas fa-save"></i> 保存积分设置</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel" style="max-width:680px;">
|
||||
<h3>SMTP 邮件配置</h3>
|
||||
<p class="muted">用于:用户提交工单后通知处理人员、处理回复通知用户。常见配置:QQ 邮箱 <code>smtp.qq.com</code> / 端口 <code>465</code> / 加密 <code>SSL</code> / 密码填「授权码」。</p>
|
||||
<form method="post" action="" class="grid-form">
|
||||
<?= csrfField() ?>
|
||||
<label>SMTP 主机<input type="text" name="smtp_host" value="<?= h($cfg['smtp_host']) ?>" placeholder="如 smtp.qq.com"></label>
|
||||
<label>端口<input type="text" name="smtp_port" value="<?= h($cfg['smtp_port']) ?>" placeholder="如 465"></label>
|
||||
<label>加密方式
|
||||
<select name="smtp_enc">
|
||||
<option value="" <?= $cfg['smtp_enc']==='' ?'selected':'' ?>>无</option>
|
||||
<option value="ssl" <?= $cfg['smtp_enc']==='ssl' ?'selected':'' ?>>SSL(465)</option>
|
||||
<option value="tls" <?= $cfg['smtp_enc']==='tls' ?'selected':'' ?>>STARTTLS(587)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>发件人名称<input type="text" name="smtp_fromname" value="<?= h($cfg['smtp_fromname']) ?>" placeholder="如 自由云商城"></label>
|
||||
<label class="span2">登录账号(邮箱)<input type="text" name="smtp_user" value="<?= h($cfg['smtp_user']) ?>" placeholder="SMTP 登录邮箱"></label>
|
||||
<label class="span2">登录密码 / 授权码<input type="password" name="smtp_pass" value="<?= h($cfg['smtp_pass']) ?>" placeholder="QQ 邮箱填授权码"></label>
|
||||
<label class="span2">发件人邮箱<input type="text" name="smtp_from" value="<?= h($cfg['smtp_from']) ?>" placeholder="留空则同登录账号"></label>
|
||||
<label class="span2">通知收件人(处理人员,逗号分隔)<input type="text" name="notify_emails" value="<?= h($cfg['notify_emails']) ?>" placeholder="如 admin@example.com,ops@example.com"></label>
|
||||
<div class="form-actions span2">
|
||||
<button type="submit" name="savesmtp" class="btn btn-primary"><i class="fas fa-save"></i> 保存 SMTP 配置</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr style="margin:22px 0;border:none;border-top:1px solid var(--border);">
|
||||
<h3>发送测试邮件</h3>
|
||||
<form method="post" action="" class="grid-form" style="max-width:480px;">
|
||||
<?= csrfField() ?>
|
||||
<label class="span2">测试收件邮箱<input type="email" name="test_to" placeholder="输入一个能收信的邮箱" required></label>
|
||||
<div class="form-actions span2">
|
||||
<button type="submit" name="testmail" class="btn btn-ghost"><i class="fas fa-paper-plane"></i> 发送测试邮件</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php if ($testResult): ?>
|
||||
<div class="smtp-log">
|
||||
<div class="muted" style="margin-bottom:6px;">SMTP 会话日志:</div>
|
||||
<pre><?= h(implode("\n", $testResult['log'])) ?></pre>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="panel" style="max-width:780px;">
|
||||
<h3><i class="fab fa-alipay"></i> 支付接口配置</h3>
|
||||
<p class="muted">配置后用户下单可选择对应支付方式。未启用(或未填密钥)的渠道不会在前台显示。沙箱模式仅支付宝支持,仅用于联调,正式上线请关闭。</p>
|
||||
|
||||
<h4 style="margin:18px 0 10px;">支付宝(电脑网站支付 · RSA2)</h4>
|
||||
<form method="post" action="" class="grid-form">
|
||||
<?= csrfField() ?>
|
||||
<label class="checkbox"><input type="checkbox" name="pay_alipay_enabled" <?= getSetting('pay_alipay_enabled','0')==='1'?'checked':'' ?>> 启用支付宝支付</label>
|
||||
<label class="checkbox"><input type="checkbox" name="pay_alipay_sandbox" <?= getSetting('pay_alipay_sandbox','0')==='1'?'checked':'' ?>> 沙箱模式(调试用)</label>
|
||||
<label class="span2">应用 APPID<input type="text" name="pay_alipay_appid" value="<?= h(getSetting('pay_alipay_appid','')) ?>" placeholder="如 2021000000000000"></label>
|
||||
<label class="span2">应用私钥(RSA2)<textarea name="pay_alipay_private_key" rows="3" placeholder="-----BEGIN PRIVATE KEY----- ..."><?= h(getSetting('pay_alipay_private_key','')) ?></textarea></label>
|
||||
<label class="span2">支付宝公钥<textarea name="pay_alipay_public_key" rows="3" placeholder="-----BEGIN PUBLIC KEY----- ..."><?= h(getSetting('pay_alipay_public_key','')) ?></textarea></label>
|
||||
<div class="form-actions span2">
|
||||
<button type="submit" name="savepay_ali" class="btn btn-primary"><i class="fas fa-save"></i> 保存支付宝配置</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr style="margin:24px 0;border:none;border-top:1px solid var(--border);">
|
||||
|
||||
<h4 style="margin:18px 0 10px;">微信支付(Native 扫码支付 · V3)</h4>
|
||||
<form method="post" action="" class="grid-form">
|
||||
<?= csrfField() ?>
|
||||
<label class="checkbox"><input type="checkbox" name="pay_wechat_enabled" <?= getSetting('pay_wechat_enabled','0')==='1'?'checked':'' ?>> 启用微信支付</label>
|
||||
<label class="span2">商户号 MCH ID<input type="text" name="pay_wechat_mch_id" value="<?= h(getSetting('pay_wechat_mch_id','')) ?>" placeholder="如 1900000000"></label>
|
||||
<label class="span2">API 密钥(Key / APIv3 Key)<input type="text" name="pay_wechat_api_key" value="<?= h(getSetting('pay_wechat_api_key','')) ?>" placeholder="32 位密钥"></label>
|
||||
<label class="span2">APPID(公众号 / 小程序,可选)<input type="text" name="pay_wechat_appid" value="<?= h(getSetting('pay_wechat_appid','')) ?>" placeholder="留空则用商户默认"></label>
|
||||
<div class="form-actions span2">
|
||||
<button type="submit" name="savepay_wx" class="btn btn-primary"><i class="fas fa-save"></i> 保存微信支付配置</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php adminFooter(); ?>
|
||||
@@ -0,0 +1,370 @@
|
||||
<?php
|
||||
// 后台工单管理:列表(按部门/状态筛选)+ 详情处理(多轮回复、状态流转、邮件通知用户)
|
||||
require __DIR__ . '/../includes/init.php';
|
||||
requireStaff();
|
||||
require '_common.php';
|
||||
$me = currentStaff();
|
||||
$isAdmin = isAdminStaff();
|
||||
|
||||
$db = db();
|
||||
$msg = '';
|
||||
$err = '';
|
||||
|
||||
$depts = $db->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 = '<div style="font-family:sans-serif;max-width:560px;margin:auto">'
|
||||
. '<h2 style="color:#2563eb">工单处理通知</h2>'
|
||||
. '<p>您好 ' . h($t['user_name']) . ',您提交的工单已更新:</p>'
|
||||
. '<p><strong>工单标题:</strong>' . h($t['subject']) . '</p>'
|
||||
. '<p><strong>当前状态:</strong>' . ticketStatusLabel($status) . '</p>'
|
||||
. ($reply !== '' ? '<div style="background:#f5f7fb;border-left:4px solid #2563eb;padding:12px 16px;margin:10px 0;white-space:pre-wrap">' . nl2br(h($reply)) . '</div>' : '')
|
||||
. '<p><a href="' . $url . '" style="background:#2563eb;color:#fff;padding:10px 18px;border-radius:6px;text-decoration:none;display:inline-block">查看工单详情</a></p>'
|
||||
. '</div>';
|
||||
$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 = '<div style="font-family:sans-serif;max-width:560px;margin:auto">'
|
||||
. '<h2 style="color:#1a73e8">续期成功</h2>'
|
||||
. '<p>你好 ' . h($t['user_name']) . ',你的续期申请已通过:</p>'
|
||||
. '<p><strong>关联订单:</strong>#' . (int)$t['order_id'] . '</p>'
|
||||
. '<p><strong>新到期时间:</strong>' . h($newExpiry) . '</p>'
|
||||
. '<div style="background:#f5f7fb;border-left:4px solid #1a73e8;padding:12px 16px;margin:10px 0;white-space:pre-wrap">' . nl2br(h($reply)) . '</div>'
|
||||
. '<p><a href="' . $url . '" style="background:#1a73e8;color:#fff;padding:10px 18px;border-radius:6px;text-decoration:none;display:inline-block">查看工单</a></p>'
|
||||
. '<p class="muted">可在「我的订单 → 详情」中查看完整凭据。</p></div>';
|
||||
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');
|
||||
?>
|
||||
<h1 class="page-title"><i class="fas fa-ticket-alt"></i> 工单管理(共 <?= (int) $total ?>)</h1>
|
||||
|
||||
<?php if ($msg): ?><p class="banner-ok"><i class="fas fa-check-circle"></i> <?= h($msg) ?></p><?php endif; ?>
|
||||
<?php if ($err): ?><p class="form-err"><?= h($err) ?></p><?php endif; ?>
|
||||
|
||||
<?php if ($ticket): ?>
|
||||
<div class="panel">
|
||||
<div class="ticket-meta">
|
||||
<span class="badge-status status-<?= h($ticket['status']) ?>"><?= ticketStatusLabel($ticket['status']) ?></span>
|
||||
<span class="muted">类型:<?= ticketTypeLabel($ticket['type']) ?></span>
|
||||
<span class="muted">提交人:<?= h($ticket['user_name'] ?? '游客') ?>(<?= h($ticket['user_email'] ?? '未填邮箱') ?>)</span>
|
||||
<span class="muted">部门:<?= h($ticket['dept_name'] ?? '未指定') ?></span>
|
||||
<span class="muted">优先级:<?= ticketPriorityLabel($ticket['priority']) ?></span>
|
||||
<span class="muted">负责人:<?= !empty($ticket['assignee_name']) ? h($ticket['assignee_name']) : '<span style="color:var(--muted)">未分配</span>' ?></span>
|
||||
<span class="muted">提交:<?= date('Y-m-d H:i', strtotime($ticket['created_at'])) ?></span>
|
||||
</div>
|
||||
<?php if ($ticket['type'] === 'renewal' && $ticket['order_id']): ?>
|
||||
<?php $ord = $db->prepare('SELECT order_no, period_days, expires_at, status FROM ' . tn('orders') . ' WHERE id = ?'); $ord->execute([$ticket['order_id']]); $ord = $ord->fetch(); ?>
|
||||
<div class="renew-box">
|
||||
<i class="fas fa-rotate"></i> 续期申请 · 关联订单
|
||||
#<?= (int)$ticket['order_id'] ?>(<?= $ord ? h($ord['order_no']) : '已删除' ?>)
|
||||
<?php if ($ord): ?>
|
||||
· 周期 <?= periodLabel($ord['period_days']) ?> · 当前到期 <?= expiryText($ord['expires_at']) ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="ticket-timeline">
|
||||
<div class="ticket-bubble user-bubble">
|
||||
<div class="bubble-head"><i class="fas fa-user"></i> <?= h($ticket['user_name'] ?? '用户') ?> · <?= date('Y-m-d H:i', strtotime($ticket['created_at'])) ?></div>
|
||||
<h3 style="margin:8px 0 6px;font-size:1.05rem;"><?= h($ticket['subject']) ?></h3>
|
||||
<div class="ticket-msg"><?= nl2br(h($ticket['message'])) ?></div>
|
||||
</div>
|
||||
|
||||
<?php foreach ($replies as $r): ?>
|
||||
<?php if ((int)$r['is_admin'] === 1): ?>
|
||||
<div class="ticket-bubble admin-bubble">
|
||||
<div class="bubble-head"><i class="fas fa-user-shield"></i> 处理人员(<?= h($r['username'] ?? '管理员') ?>) · <?= date('Y-m-d H:i', strtotime($r['created_at'])) ?></div>
|
||||
<div class="ticket-msg"><?= nl2br(h($r['message'])) ?></div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="ticket-bubble user-bubble">
|
||||
<div class="bubble-head"><i class="fas fa-user"></i> <?= h($ticket['user_name'] ?? '用户') ?> · <?= date('Y-m-d H:i', strtotime($r['created_at'])) ?></div>
|
||||
<div class="ticket-msg"><?= nl2br(h($r['message'])) ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<form method="post" action="" class="grid-form" style="margin-top:18px;">
|
||||
<?= csrfField() ?>
|
||||
<input type="hidden" name="id" value="<?= (int) $ticket['id'] ?>">
|
||||
<label class="span2">追加回复
|
||||
<textarea name="admin_reply" rows="5" autocomplete="off" placeholder="填写新的处理说明,每次保存都会新增一条回复记录,不会覆盖历史回复(留空可直接修改状态)"></textarea>
|
||||
</label>
|
||||
<p class="muted span2" style="font-size:.85rem;margin:-6px 0 4px;"><i class="fas fa-info-circle"></i> 提示:历史回复以上方时间线为准,此处内容仅用于新增回复。</p>
|
||||
<label>状态
|
||||
<select name="status">
|
||||
<option value="open" <?= $ticket['status']==='open'?'selected':'' ?>>待处理</option>
|
||||
<option value="pending" <?= $ticket['status']==='pending'?'selected':'' ?>>处理中</option>
|
||||
<option value="resolved" <?= $ticket['status']==='resolved'?'selected':'' ?>>已解决</option>
|
||||
<option value="closed" <?= $ticket['status']==='closed'?'selected':'' ?>>已关闭</option>
|
||||
</select>
|
||||
</label>
|
||||
<?php if ($isAdmin): ?>
|
||||
<label>负责人(分配)
|
||||
<select name="assignee_id">
|
||||
<option value="0" <?= empty($ticket['assignee_id']) ? 'selected' : '' ?>>未分配(仅管理员可见)</option>
|
||||
<?php foreach ($assignees as $a): ?>
|
||||
<option value="<?= (int)$a['id'] ?>" <?= (int)$ticket['assignee_id']===(int)$a['id'] ? 'selected' : '' ?>><?= h($a['username']) ?>(<?= $a['role']==='admin'?'管理员':'客服' ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="notify" <?= $ticket['user_email'] ? 'checked' : 'disabled' ?>> 邮件通知用户
|
||||
</label>
|
||||
<div class="form-actions span2">
|
||||
<button type="submit" name="reply" class="btn btn-primary"><i class="fas fa-save"></i> 保存回复</button>
|
||||
<?php if ($isAdmin): ?>
|
||||
<button type="submit" name="assign" class="btn btn-ghost"><i class="fas fa-user-check"></i> 保存分配</button>
|
||||
<?php endif; ?>
|
||||
<?php if ($ticket['status'] === 'closed'): ?>
|
||||
<button type="submit" name="reopen" class="btn btn-ok"><i class="fas fa-undo"></i> 重新开启</button>
|
||||
<?php endif; ?>
|
||||
<?php if ($ticket['type'] === 'renewal' && $ticket['order_id']): ?>
|
||||
<button type="submit" name="approve_renew" class="btn btn-ok"><i class="fas fa-check"></i> 批准续期</button>
|
||||
<?php endif; ?>
|
||||
<a href="tickets.php" class="btn btn-ghost">返回列表</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="panel">
|
||||
<form method="get" action="" class="filter-bar">
|
||||
<select name="status" onchange="this.form.submit()">
|
||||
<option value="">全部状态</option>
|
||||
<option value="open" <?= $filterStatus==='open'?'selected':'' ?>>待处理</option>
|
||||
<option value="pending" <?= $filterStatus==='pending'?'selected':'' ?>>处理中</option>
|
||||
<option value="resolved" <?= $filterStatus==='resolved'?'selected':'' ?>>已解决</option>
|
||||
<option value="closed" <?= $filterStatus==='closed'?'selected':'' ?>>已关闭</option>
|
||||
</select>
|
||||
<select name="dept" onchange="this.form.submit()">
|
||||
<option value="0">全部部门</option>
|
||||
<?php foreach ($depts as $d): ?>
|
||||
<option value="<?= (int) $d['id'] ?>" <?= $filterDept===$d['id']?'selected':'' ?>><?= h($d['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<a href="tickets.php" class="btn btn-ghost">重置</a>
|
||||
</form>
|
||||
<?php if (empty($list)): ?>
|
||||
<p class="empty">暂无工单。</p>
|
||||
<?php else: ?>
|
||||
<table class="data-table">
|
||||
<thead><tr><th>编号</th><th>标题</th><th>提交人</th><th>部门</th><th>类型</th><th>优先级</th><th>状态</th><th>负责人</th><th>更新</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($list as $t): ?>
|
||||
<tr>
|
||||
<td>#<?= (int) $t['id'] ?></td>
|
||||
<td><?= h($t['subject']) ?></td>
|
||||
<td><?= h($t['user_name'] ?? '游客') ?></td>
|
||||
<td><?= h($t['dept_name'] ?? '未指定') ?></td>
|
||||
<td><?= ticketTypeLabel($t['type']) ?></td>
|
||||
<td><?= ticketPriorityLabel($t['priority']) ?></td>
|
||||
<td><span class="badge-status status-<?= h($t['status']) ?>"><?= ticketStatusLabel($t['status']) ?></span></td>
|
||||
<td><?= !empty($t['assignee_name']) ? h($t['assignee_name']) : '<span class="muted">未分配</span>' ?></td>
|
||||
<td><?= date('m-d H:i', strtotime($t['updated_at'])) ?></td>
|
||||
<td><a href="tickets.php?view=<?= (int) $t['id'] ?>" class="mini-btn">处理</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php adminFooter(); ?>
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
// 后台:普通用户管理(添加 / 封禁 / 解封 / 删除)
|
||||
require __DIR__ . '/../includes/init.php';
|
||||
requireStaff();
|
||||
require '_common.php';
|
||||
$isAdmin = isAdminStaff();
|
||||
|
||||
$db = db();
|
||||
$msg = '';
|
||||
$err = '';
|
||||
// 客服账号仅可查看,禁止写操作
|
||||
if (!$isAdmin && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$err = '客服账号仅可查看,如需操作请联系管理员。';
|
||||
}
|
||||
// 封禁 / 解封
|
||||
if (isset($_POST['toggle'])) {
|
||||
verifyCsrf();
|
||||
$id = (int) ($_POST['toggle'] ?? 0);
|
||||
$stmt = $db->prepare('SELECT id, is_admin, status FROM ' . tn('users') . ' WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) {
|
||||
$err = '用户不存在';
|
||||
} elseif ((int) $row['is_admin'] === 1) {
|
||||
$err = '不能对管理员执行该操作';
|
||||
} else {
|
||||
$next = ((int) $row['status'] === 1) ? 0 : 1;
|
||||
$db->prepare('UPDATE ' . tn('users') . ' SET status = ? WHERE id = ?')->execute([$next, $id]);
|
||||
$msg = $next === 0 ? '该用户已被封禁,无法再登录。' : '已解除封禁。';
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
if (isset($_POST['delete'])) {
|
||||
verifyCsrf();
|
||||
$id = (int) ($_POST['delete'] ?? 0);
|
||||
$stmt = $db->prepare('SELECT id, is_admin FROM ' . tn('users') . ' WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) {
|
||||
$err = '用户不存在';
|
||||
} elseif ((int) $row['is_admin'] === 1) {
|
||||
$err = '不能删除管理员账户';
|
||||
} else {
|
||||
$db->prepare('DELETE FROM ' . tn('users') . ' WHERE id = ? AND is_admin = 0')->execute([$id]);
|
||||
$msg = '用户已删除';
|
||||
}
|
||||
}
|
||||
|
||||
// 添加
|
||||
if (isset($_POST['add'])) {
|
||||
verifyCsrf();
|
||||
$u = trim($_POST['username'] ?? '');
|
||||
$em = trim($_POST['email'] ?? '');
|
||||
$pw = $_POST['password'] ?? '';
|
||||
if (strlen($u) < 3) $err = '用户名至少 3 个字符';
|
||||
elseif (!filter_var($em, FILTER_VALIDATE_EMAIL)) $err = '请填写有效的邮箱';
|
||||
elseif (strlen($pw) < 6) $err = '密码至少 6 位';
|
||||
else {
|
||||
$chk = $db->prepare('SELECT id FROM ' . tn('users') . ' WHERE username = ?');
|
||||
$chk->execute([$u]);
|
||||
if ($chk->fetch()) {
|
||||
$err = '该用户名已存在';
|
||||
} else {
|
||||
$hash = password_hash($pw, PASSWORD_DEFAULT);
|
||||
$code = genInviteCode();
|
||||
$db->prepare('INSERT INTO ' . tn('users') . ' (username,password,email,invite_code,created_at) VALUES (?,?,?,?,NOW())')
|
||||
->execute([$u, $hash, $em, $code]);
|
||||
$msg = '用户已添加';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 手动调整积分
|
||||
if (isset($_POST['adjust'])) {
|
||||
verifyCsrf();
|
||||
$target = trim($_POST['target'] ?? '');
|
||||
$amount = (int) ($_POST['amount'] ?? 0);
|
||||
$remark = trim($_POST['remark'] ?? '');
|
||||
if ($target === '') $err = '请填写用户 ID 或用户名';
|
||||
elseif ($amount === 0) $err = '积分变动不能为 0';
|
||||
elseif ($remark === '') $err = '请填写调整备注(便于留痕)';
|
||||
else {
|
||||
if (is_numeric($target)) {
|
||||
$stmt = $db->prepare('SELECT id, is_admin FROM ' . tn('users') . ' WHERE id = ?');
|
||||
$stmt->execute([(int) $target]);
|
||||
} else {
|
||||
$stmt = $db->prepare('SELECT id, is_admin FROM ' . tn('users') . ' WHERE username = ?');
|
||||
$stmt->execute([$target]);
|
||||
}
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) {
|
||||
$err = '未找到该用户';
|
||||
} else {
|
||||
$res = addPoints((int) $row['id'], 'manual', $amount, '后台手动调整:' . $remark);
|
||||
if ($res['ok']) {
|
||||
$msg = '已为用户 #' . (int) $row['id'] . ' ' . ($amount > 0 ? '增加' : '扣减') . ' ' . abs($amount) . ' 积分,当前余额 ' . $res['balance'] . ' 分。';
|
||||
} else {
|
||||
$err = '调整失败:' . $res['error'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 切换邮箱验证状态
|
||||
if (isset($_POST['toggle_verify'])) {
|
||||
verifyCsrf();
|
||||
$id = (int) ($_POST['toggle_verify'] ?? 0);
|
||||
$stmt = $db->prepare('SELECT id, verified FROM ' . tn('users') . ' WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) {
|
||||
$err = '用户不存在';
|
||||
} else {
|
||||
$next = (int) $row['verified'] === 1 ? 0 : 1;
|
||||
$db->prepare('UPDATE ' . tn('users') . ' SET verified = ? WHERE id = ?')->execute([$next, $id]);
|
||||
$msg = $next === 1 ? '已标记该用户邮箱为已验证。' : '已将该用户邮箱设为未验证。';
|
||||
}
|
||||
}
|
||||
|
||||
$list = $db->query('SELECT id, username, email, status, points, verified, created_at FROM ' . tn('users') . ' WHERE is_admin = 0 ORDER BY created_at DESC')->fetchAll();
|
||||
$orderCnt = $db->query('SELECT user_id, COUNT(*) c FROM ' . tn('orders') . ' GROUP BY user_id')->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
adminHeader('用户管理', 'users');
|
||||
?>
|
||||
<h1 class="page-title"><i class="fas fa-users"></i> 用户管理(共 <?= count($list) ?> 人)</h1>
|
||||
|
||||
<?php if ($msg): ?><p class="banner-ok"><i class="fas fa-check-circle"></i> <?= h($msg) ?></p><?php endif; ?>
|
||||
<?php if ($err): ?><p class="form-err"><?= h($err) ?></p><?php endif; ?>
|
||||
|
||||
<div class="panel">
|
||||
<h3>添加用户</h3>
|
||||
<form method="post" action="" class="grid-form" style="max-width:680px;">
|
||||
<?= csrfField() ?>
|
||||
<label>用户名 *<input type="text" name="username" placeholder="至少3位" required></label>
|
||||
<label>邮箱 *<input type="email" name="email" required></label>
|
||||
<label class="span2">密码 *<input type="password" name="password" placeholder="至少6位" required></label>
|
||||
<div class="form-actions span2"><button type="submit" name="add" class="btn btn-primary"><i class="fas fa-user-plus"></i> 添加用户</button></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>手动调整积分</h3>
|
||||
<form method="post" action="" class="grid-form" style="max-width:720px;">
|
||||
<?= csrfField() ?>
|
||||
<label>用户 ID 或用户名 *<input type="text" name="target" placeholder="如 12 或 xiaoming" required></label>
|
||||
<label>积分变动 *<input type="number" name="amount" placeholder="正数=增加,负数=扣减" required></label>
|
||||
<label class="span2">调整备注 *<input type="text" name="remark" placeholder="如「活动奖励」「误扣退回」等" required></label>
|
||||
<div class="form-actions span2">
|
||||
<button type="submit" name="adjust" class="btn btn-primary"><i class="fas fa-coins"></i> 提交调整</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>用户列表</h3>
|
||||
<?php if (empty($list)): ?>
|
||||
<p class="empty">还没有普通用户。</p>
|
||||
<?php else: ?>
|
||||
<table class="data-table">
|
||||
<thead><tr><th>ID</th><th>用户名</th><th>邮箱</th><th>积分</th><th>订单数</th><th>验证</th><th>状态</th><th>注册时间</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($list as $u): ?>
|
||||
<tr>
|
||||
<td><?= (int) $u['id'] ?></td>
|
||||
<td><?= h($u['username']) ?></td>
|
||||
<td><?= h($u['email']) ?></td>
|
||||
<td><?= (int) ($u['points'] ?? 0) ?></td>
|
||||
<td><?= (int) ($orderCnt[$u['id']] ?? 0) ?></td>
|
||||
<td>
|
||||
<?php if ((int) $u['verified'] === 1): ?>
|
||||
<span class="badge-on">已验证</span>
|
||||
<?php else: ?>
|
||||
<span class="badge-off" style="background:var(--warn);color:#000">未验证</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= (int) $u['status'] === 1 ? '<span class="badge-on">正常</span>' : '<span class="badge-off">已封禁</span>' ?></td>
|
||||
<td><?= date('Y-m-d', strtotime($u['created_at'])) ?></td>
|
||||
<td class="row-actions">
|
||||
<form method="post" action="" style="display:inline" onsubmit="return confirm('确定<?= (int)$u['status']===1?'封禁':'解封' ?>该用户?');">
|
||||
<?= csrfField() ?>
|
||||
<button type="submit" name="toggle" value="<?= (int) $u['id'] ?>" class="mini-btn <?= (int)$u['status']===1?'warn':'ok' ?>"><i class="fas fa-<?= (int)$u['status']===1?'ban':'check' ?>"></i></button>
|
||||
</form>
|
||||
<form method="post" action="" style="display:inline" onsubmit="return confirm('确定<?= (int)$u['verified']===1?'取消验证':'标记已验证' ?>?');">
|
||||
<?= csrfField() ?>
|
||||
<button type="submit" name="toggle_verify" value="<?= (int) $u['id'] ?>" class="mini-btn <?= (int)$u['verified']===1?'warn':'ok' ?>" title="切换邮箱验证状态"><i class="fas fa-<?= (int)$u['verified']===1?'envelope':'envelope-circle-check' ?>"></i></button>
|
||||
</form>
|
||||
<form method="post" action="" style="display:inline" onsubmit="return confirm('确定删除该用户?此操作不可恢复。');">
|
||||
<?= csrfField() ?>
|
||||
<button type="submit" name="delete" value="<?= (int) $u['id'] ?>" class="mini-btn danger"><i class="fas fa-trash"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php adminFooter(); ?>
|
||||
Reference in New Issue
Block a user