第一版
This commit is contained in:
parent
bfaa777c82
commit
5fbbbc8804
|
|
@ -0,0 +1,493 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
require_once __DIR__ . '/includes/functions.php';
|
||||||
|
requireAdmin();
|
||||||
|
|
||||||
|
$pageTitle = '后台设置';
|
||||||
|
require __DIR__ . '/includes/header.php';
|
||||||
|
|
||||||
|
$msg = '';
|
||||||
|
|
||||||
|
// 生成邀请码
|
||||||
|
if (isset($_POST['gen_invite'])) {
|
||||||
|
if (!verifyCsrf($_POST['csrf_token'] ?? '')) { die('CSRF token无效'); }
|
||||||
|
$count = max(1, min(100, (int)($_POST['count'] ?? 1)));
|
||||||
|
$maxUses = max(0, (int)($_POST['max_uses'] ?? 1));
|
||||||
|
$inserted = 0;
|
||||||
|
$stmt = $pdo->prepare('INSERT INTO invite_codes (code, max_uses) VALUES (?, ?)');
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
$code = strtoupper(bin2hex(random_bytes(4)));
|
||||||
|
try {
|
||||||
|
$stmt->execute([$code, $maxUses]);
|
||||||
|
$inserted++;
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$_SESSION['flash_msg'] = '成功生成 ' . $inserted . ' 个邀请码(最大使用次数: ' . ($maxUses === 0 ? '不限' : $maxUses) . ')';
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
header('Location: admin_settings.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改邀请码次数
|
||||||
|
if (isset($_POST['edit_max_uses'])) {
|
||||||
|
if (!verifyCsrf($_POST['csrf_token'] ?? '')) { die('CSRF token无效'); }
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$maxUses = max(0, (int)($_POST['max_uses'] ?? 1));
|
||||||
|
$stmt = $pdo->prepare('UPDATE invite_codes SET max_uses = ? WHERE id = ?');
|
||||||
|
$stmt->execute([$maxUses, $id]);
|
||||||
|
$_SESSION['flash_msg'] = '已更新最大使用次数';
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
header('Location: admin_settings.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除邀请码(仅可删未使用的)
|
||||||
|
if (isset($_GET['del_invite']) && is_numeric($_GET['del_invite'])) {
|
||||||
|
if (!verifyCsrf($_GET['csrf_token'] ?? '')) { $msg = '<div class="alert alert-danger">CSRF token无效</div>'; }
|
||||||
|
else {
|
||||||
|
$id = (int)$_GET['del_invite'];
|
||||||
|
$stmt = $pdo->prepare('DELETE FROM invite_codes WHERE id = ? AND used_count = 0');
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$msg = $stmt->rowCount() ? '<div class="alert alert-success">邀请码已删除</div>' : '<div class="alert alert-danger">无法删除:已被使用或不存在</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增用户
|
||||||
|
if (isset($_POST['add_user'])) {
|
||||||
|
if (!verifyCsrf($_POST['csrf_token'] ?? '')) { die('CSRF token无效'); }
|
||||||
|
$username = trim($_POST['username'] ?? '');
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
$role = $_POST['role'] ?? 'user';
|
||||||
|
$remark = trim($_POST['remark'] ?? '');
|
||||||
|
if (!in_array($role, ['admin', 'user'])) $role = 'user';
|
||||||
|
if ($username === '' || $password === '') {
|
||||||
|
$msg = '<div class="alert alert-danger">用户名和密码不能为空</div>';
|
||||||
|
} elseif (strlen($username) < 3 || strlen($username) > 20) {
|
||||||
|
$msg = '<div class="alert alert-danger">用户名长度3-20个字符</div>';
|
||||||
|
} elseif (strlen($password) < 6) {
|
||||||
|
$msg = '<div class="alert alert-danger">密码长度至少6位</div>';
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare('SELECT id FROM users WHERE username = ?');
|
||||||
|
$stmt->execute([$username]);
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
$msg = '<div class="alert alert-danger">用户名已存在</div>';
|
||||||
|
} else {
|
||||||
|
$productIds = array_map('intval', $_POST['product_ids'] ?? []);
|
||||||
|
$productIds = array_filter($productIds, fn($v) => $v > 0);
|
||||||
|
if (empty($productIds)) {
|
||||||
|
$msg = '<div class="alert alert-danger">至少选择一个产品</div>';
|
||||||
|
} else {
|
||||||
|
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
$stmt = $pdo->prepare('INSERT INTO users (username, password, role, remark) VALUES (?, ?, ?, ?)');
|
||||||
|
$stmt->execute([$username, $hash, $role, $remark ?: null]);
|
||||||
|
$userId = (int)$pdo->lastInsertId();
|
||||||
|
$stmt = $pdo->prepare('INSERT INTO user_products (user_id, product_id) VALUES (?, ?)');
|
||||||
|
foreach ($productIds as $pid) {
|
||||||
|
$stmt->execute([$userId, $pid]);
|
||||||
|
}
|
||||||
|
$_SESSION['flash_msg'] = '用户 ' . h($username) . ' 添加成功';
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
header('Location: admin_settings.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改备注
|
||||||
|
if (isset($_POST['edit_remark'])) {
|
||||||
|
if (!verifyCsrf($_POST['csrf_token'] ?? '')) { die('CSRF token无效'); }
|
||||||
|
$id = (int)($_POST['user_id'] ?? 0);
|
||||||
|
$remark = trim($_POST['remark'] ?? '');
|
||||||
|
$stmt = $pdo->prepare('UPDATE users SET remark = ? WHERE id = ?');
|
||||||
|
$stmt->execute([$remark ?: null, $id]);
|
||||||
|
$_SESSION['flash_msg'] = '备注已更新';
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
header('Location: admin_settings.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分配产品
|
||||||
|
if (isset($_POST['edit_user_products'])) {
|
||||||
|
if (!verifyCsrf($_POST['csrf_token'] ?? '')) { die('CSRF token无效'); }
|
||||||
|
$id = (int)($_POST['user_id'] ?? 0);
|
||||||
|
$productIds = array_map('intval', $_POST['product_ids'] ?? []);
|
||||||
|
$productIds = array_filter($productIds, fn($v) => $v > 0);
|
||||||
|
if (empty($productIds)) {
|
||||||
|
$msg = '<div class="alert alert-danger">至少选择一个产品</div>';
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare('DELETE FROM user_products WHERE user_id = ?');
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$stmt = $pdo->prepare('INSERT INTO user_products (user_id, product_id) VALUES (?, ?)');
|
||||||
|
foreach ($productIds as $pid) {
|
||||||
|
$stmt->execute([$id, $pid]);
|
||||||
|
}
|
||||||
|
$_SESSION['flash_msg'] = '产品分配已更新';
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
header('Location: admin_settings.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁用/启用用户
|
||||||
|
if (isset($_GET['toggle_disable']) && is_numeric($_GET['toggle_disable'])) {
|
||||||
|
if (!verifyCsrf($_GET['csrf_token'] ?? '')) {
|
||||||
|
$_SESSION['flash_msg'] = 'CSRF token无效';
|
||||||
|
$_SESSION['flash_type'] = 'danger';
|
||||||
|
} else {
|
||||||
|
$id = (int)$_GET['toggle_disable'];
|
||||||
|
if ($id === getCurrentUserId()) {
|
||||||
|
$_SESSION['flash_msg'] = '不能禁用自己';
|
||||||
|
$_SESSION['flash_type'] = 'danger';
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare('SELECT disabled FROM users WHERE id = ?');
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$current = (int)$stmt->fetchColumn();
|
||||||
|
$newDisabled = $current ? 0 : 1;
|
||||||
|
$stmt = $pdo->prepare('UPDATE users SET disabled = ? WHERE id = ?');
|
||||||
|
$stmt->execute([$newDisabled, $id]);
|
||||||
|
$_SESSION['flash_msg'] = $newDisabled ? '用户已禁用' : '用户已启用';
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
header('Location: admin_settings.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改用户角色
|
||||||
|
if (isset($_GET['set_role']) && is_numeric($_GET['set_role'])) {
|
||||||
|
$id = (int)$_GET['set_role'];
|
||||||
|
$role = $_GET['role'] ?? '';
|
||||||
|
if ($id === getCurrentUserId()) {
|
||||||
|
$msg = '<div class="alert alert-danger">不能修改自己的角色</div>';
|
||||||
|
} elseif (!in_array($role, ['admin', 'user'])) {
|
||||||
|
$msg = '<div class="alert alert-danger">无效的角色</div>';
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->query("SELECT COUNT(*) FROM users WHERE role = 'admin'");
|
||||||
|
$adminCount = (int)$stmt->fetchColumn();
|
||||||
|
$stmt = $pdo->prepare('SELECT role FROM users WHERE id = ?');
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$targetRole = $stmt->fetchColumn();
|
||||||
|
if ($role === 'user' && $targetRole === 'admin' && $adminCount <= 1) {
|
||||||
|
$msg = '<div class="alert alert-danger">至少保留一名管理员</div>';
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare('UPDATE users SET role = ? WHERE id = ?');
|
||||||
|
$stmt->execute([$role, $id]);
|
||||||
|
$msg = '<div class="alert alert-success">角色已更新</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页 - 邀请码
|
||||||
|
$page = max(1, (int)($_GET['p'] ?? 1));
|
||||||
|
$perPage = 20;
|
||||||
|
$offset = ($page - 1) * $perPage;
|
||||||
|
|
||||||
|
$stmt = $pdo->query('SELECT COUNT(*) FROM invite_codes');
|
||||||
|
$inviteTotal = (int)$stmt->fetchColumn();
|
||||||
|
$invitePages = max(1, ceil($inviteTotal / $perPage));
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT * FROM invite_codes ORDER BY id DESC LIMIT ? OFFSET ?');
|
||||||
|
$stmt->execute([$perPage, $offset]);
|
||||||
|
$invites = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// 用户列表
|
||||||
|
$stmt = $pdo->query('SELECT * FROM users ORDER BY id ASC');
|
||||||
|
$users = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// 所有启用产品
|
||||||
|
$stmt = $pdo->query('SELECT id, name, code FROM products WHERE status = 1 ORDER BY id ASC');
|
||||||
|
$allProducts = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// 用户-产品关联
|
||||||
|
$userProducts = [];
|
||||||
|
$stmt = $pdo->query('SELECT user_id, product_id FROM user_products');
|
||||||
|
while ($row = $stmt->fetch()) {
|
||||||
|
$userProducts[$row['user_id']][] = (int)$row['product_id'];
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="card">
|
||||||
|
<h2>邀请码管理</h2>
|
||||||
|
<?php if (isset($_SESSION['flash_msg'])): ?>
|
||||||
|
<div class="alert alert-<?= h($_SESSION['flash_type'] ?? 'success') ?>"><?= h($_SESSION['flash_msg']) ?></div>
|
||||||
|
<?php unset($_SESSION['flash_msg'], $_SESSION['flash_type']); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?= $msg ?>
|
||||||
|
<form method="post" style="display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap;margin-bottom:16px;">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= csrfToken() ?>">
|
||||||
|
<div class="form-group" style="margin-bottom:0;">
|
||||||
|
<label>生成数量</label>
|
||||||
|
<input type="number" name="count" class="form-control" value="5" min="1" max="100" style="width:100px;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;">
|
||||||
|
<label>最大使用次数</label>
|
||||||
|
<input type="number" name="max_uses" class="form-control" value="1" min="0" style="width:120px;" title="0=不限次数">
|
||||||
|
</div>
|
||||||
|
<button type="submit" name="gen_invite" class="btn btn-success">生成邀请码</button>
|
||||||
|
</form>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>邀请码</th>
|
||||||
|
<th>使用次数</th>
|
||||||
|
<th>最大次数</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>创建时间</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($invites as $i): ?>
|
||||||
|
<?php
|
||||||
|
$expired = $i['max_uses'] > 0 && $i['used_count'] >= $i['max_uses'];
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td><?= $i['id'] ?></td>
|
||||||
|
<td><code><?= h($i['code']) ?></code></td>
|
||||||
|
<td><?= $i['used_count'] ?></td>
|
||||||
|
<td><?= $i['max_uses'] === 0 ? '不限' : $i['max_uses'] ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if ($expired): ?>
|
||||||
|
<span class="badge badge-danger">已用完</span>
|
||||||
|
<?php elseif ($i['used_count'] > 0): ?>
|
||||||
|
<span class="badge badge-warning">使用中</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="badge badge-success">未使用</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td><?= formatDateTime($i['created_at']) ?></td>
|
||||||
|
<td>
|
||||||
|
<div class="action-group">
|
||||||
|
<a href="javascript:void(0)" class="btn btn-primary btn-sm" onclick="editMaxUses(<?= $i['id'] ?>, <?= $i['max_uses'] ?>)">修改次数</a>
|
||||||
|
<?php if ($i['used_count'] === 0): ?>
|
||||||
|
<a href="?del_invite=<?= $i['id'] ?>&csrf_token=<?= csrfToken() ?>" class="btn btn-danger btn-sm" onclick="return confirm('确定删除?')">删除</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php renderPagination($page, $invitePages); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2 style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;">
|
||||||
|
用户管理
|
||||||
|
<a href="javascript:void(0)" class="btn btn-success btn-sm" onclick="showModal('addUserModal')">+ 新增用户</a>
|
||||||
|
</h2>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<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 ($users as $u): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= $u['id'] ?></td>
|
||||||
|
<td><?= h($u['username']) ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if ($u['role'] === 'admin'): ?>
|
||||||
|
<span class="badge badge-danger">管理员</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="badge badge-info">普通用户</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php if (!empty($u['disabled'])): ?>
|
||||||
|
<span class="badge badge-secondary">已禁用</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="badge badge-success">正常</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td style="max-width:180px;">
|
||||||
|
<?php
|
||||||
|
$upIds = $userProducts[(int)$u['id']] ?? [];
|
||||||
|
$upNames = array_map(fn($p) => $p['name'], array_filter($allProducts, fn($p) => in_array((int)$p['id'], $upIds)));
|
||||||
|
echo $upNames ? h(implode(', ', $upNames)) : '<span style="color:#ccc;">-</span>';
|
||||||
|
?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php if ($u['remark']): ?>
|
||||||
|
<span title="<?= h($u['remark']) ?>"><?= h(mb_substr($u['remark'], 0, 20)) ?><?= mb_strlen($u['remark']) > 20 ? '...' : '' ?></span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span style="color:#ccc;">-</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<a href="javascript:void(0)" class="btn btn-sm" style="padding:2px 6px;font-size:11px;" onclick="editRemark(<?= $u['id'] ?>, '<?= h($u['remark'] ?? '') ?>')">✏️</a>
|
||||||
|
</td>
|
||||||
|
<td><?= formatDateTime($u['created_at']) ?></td>
|
||||||
|
<td>
|
||||||
|
<div class="action-group">
|
||||||
|
<a href="javascript:void(0)" class="btn btn-info btn-sm" onclick="editUserProducts(<?= $u['id'] ?>, '<?= h($u['username']) ?>')">分配产品</a>
|
||||||
|
<?php if ((int)$u['id'] !== getCurrentUserId()): ?>
|
||||||
|
<?php if ($u['role'] === 'user'): ?>
|
||||||
|
<a href="?set_role=<?= $u['id'] ?>&role=admin&csrf_token=<?= csrfToken() ?>" class="btn btn-primary btn-sm">设为管理员</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<a href="?set_role=<?= $u['id'] ?>&role=user&csrf_token=<?= csrfToken() ?>" class="btn btn-warning btn-sm">设为普通用户</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($u['disabled'])): ?>
|
||||||
|
<a href="?toggle_disable=<?= $u['id'] ?>&csrf_token=<?= csrfToken() ?>" class="btn btn-success btn-sm">启用</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<a href="?toggle_disable=<?= $u['id'] ?>&csrf_token=<?= csrfToken() ?>" class="btn btn-danger btn-sm" onclick="return confirm('确定禁用用户 <?= h($u['username']) ?>?')">禁用</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<span style="color:#999;">当前用户</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 修改邀请码次数弹窗 -->
|
||||||
|
<div class="modal" id="maxUsesModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="modal-close" onclick="hideModal('maxUsesModal')">×</span>
|
||||||
|
<h3>修改最大使用次数</h3>
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= csrfToken() ?>">
|
||||||
|
<input type="hidden" name="edit_max_uses" value="1">
|
||||||
|
<input type="hidden" name="id" id="max_uses_id">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>最大使用次数(0=不限次数)</label>
|
||||||
|
<input type="number" name="max_uses" class="form-control" id="max_uses_value" min="0" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">保存</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增用户弹窗 -->
|
||||||
|
<div class="modal" id="addUserModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="modal-close" onclick="hideModal('addUserModal')">×</span>
|
||||||
|
<h3>新增用户</h3>
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= csrfToken() ?>">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>用户名 <span style="color:red">*</span></label>
|
||||||
|
<input type="text" name="username" class="form-control" required minlength="3" maxlength="20">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>密码 <span style="color:red">*</span></label>
|
||||||
|
<input type="password" name="password" class="form-control" required minlength="6">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>角色</label>
|
||||||
|
<select name="role" class="form-control">
|
||||||
|
<option value="user">普通用户</option>
|
||||||
|
<option value="admin">管理员</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>可管理产品 <span style="color:red">*</span></label>
|
||||||
|
<div style="max-height:150px;overflow-y:auto;border:1px solid #ddd;border-radius:4px;padding:8px;">
|
||||||
|
<?php foreach ($allProducts as $p): ?>
|
||||||
|
<label style="display:block;font-weight:normal;margin-bottom:4px;">
|
||||||
|
<input type="checkbox" name="product_ids[]" value="<?= $p['id'] ?>"> <?= h($p['name']) ?>
|
||||||
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:12px;color:#999;margin-top:4px;">至少选择一个产品</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>备注</label>
|
||||||
|
<input type="text" name="remark" class="form-control" placeholder="选填">
|
||||||
|
</div>
|
||||||
|
<button type="submit" name="add_user" class="btn btn-success">添加用户</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 编辑备注弹窗 -->
|
||||||
|
<div class="modal" id="remarkModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="modal-close" onclick="hideModal('remarkModal')">×</span>
|
||||||
|
<h3>编辑备注</h3>
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= csrfToken() ?>">
|
||||||
|
<input type="hidden" name="edit_remark" value="1">
|
||||||
|
<input type="hidden" name="user_id" id="remark_user_id">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>备注</label>
|
||||||
|
<input type="text" name="remark" class="form-control" id="remark_content" maxlength="255">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">保存</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分配产品弹窗 -->
|
||||||
|
<div class="modal" id="productModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="modal-close" onclick="hideModal('productModal')">×</span>
|
||||||
|
<h3>分配产品 - <span id="product_user_name"></span></h3>
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= csrfToken() ?>">
|
||||||
|
<input type="hidden" name="edit_user_products" value="1">
|
||||||
|
<input type="hidden" name="user_id" id="product_user_id">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>可管理产品 <span style="color:red">*</span></label>
|
||||||
|
<div id="product_checkbox_list" style="max-height:200px;overflow-y:auto;border:1px solid #ddd;border-radius:4px;padding:8px;">
|
||||||
|
<?php foreach ($allProducts as $p): ?>
|
||||||
|
<label style="display:block;font-weight:normal;margin-bottom:4px;">
|
||||||
|
<input type="checkbox" name="product_ids[]" value="<?= $p['id'] ?>" class="product-checkbox"> <?= h($p['name']) ?>
|
||||||
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:12px;color:#999;margin-top:4px;">至少选择一个产品</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">保存</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showModal(id) { document.getElementById(id).classList.add('active'); }
|
||||||
|
function hideModal(id) { document.getElementById(id).classList.remove('active'); }
|
||||||
|
function editRemark(id, content) {
|
||||||
|
document.getElementById('remark_user_id').value = id;
|
||||||
|
document.getElementById('remark_content').value = content;
|
||||||
|
showModal('remarkModal');
|
||||||
|
}
|
||||||
|
function editMaxUses(id, maxUses) {
|
||||||
|
document.getElementById('max_uses_id').value = id;
|
||||||
|
document.getElementById('max_uses_value').value = maxUses;
|
||||||
|
showModal('maxUsesModal');
|
||||||
|
}
|
||||||
|
function editUserProducts(id, name) {
|
||||||
|
document.getElementById('product_user_id').value = id;
|
||||||
|
document.getElementById('product_user_name').textContent = name;
|
||||||
|
// 取消所有选中
|
||||||
|
document.querySelectorAll('.product-checkbox').forEach(function(cb) { cb.checked = false; });
|
||||||
|
// 选中当前用户已有的产品
|
||||||
|
var data = <?= json_encode($userProducts) ?>;
|
||||||
|
var ids = data[id] || [];
|
||||||
|
document.querySelectorAll('.product-checkbox').forEach(function(cb) {
|
||||||
|
if (ids.indexOf(parseInt(cb.value)) !== -1) {
|
||||||
|
cb.checked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
showModal('productModal');
|
||||||
|
}
|
||||||
|
document.querySelectorAll('.modal').forEach(function(m) {
|
||||||
|
m.addEventListener('click', function(e) { if (e.target === this) this.classList.remove('active'); });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php require __DIR__ . '/includes/footer.php'; ?>
|
||||||
|
|
@ -0,0 +1,605 @@
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
background: #f0f2f5;
|
||||||
|
color: #333;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navbar */
|
||||||
|
.navbar {
|
||||||
|
background: #1a1a2e;
|
||||||
|
color: #fff;
|
||||||
|
padding: 0 16px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-container {
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 56px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-brand {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-toggle {
|
||||||
|
display: none;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
color: #ccc;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: background 0.2s, color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
background: rgba(255,255,255,0.12);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-active, .nav-link.nav-active {
|
||||||
|
background: rgba(74,108,247,0.35);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-user {
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-switcher {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-switcher select option {
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.product-switcher {
|
||||||
|
margin: 6px 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.product-switcher select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-logout {
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container */
|
||||||
|
.container {
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card */
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables */
|
||||||
|
.table-wrapper {
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover td {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
th:last-child,
|
||||||
|
td:last-child {
|
||||||
|
position: sticky;
|
||||||
|
right: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
th:last-child {
|
||||||
|
background: #f8f9fa;
|
||||||
|
box-shadow: -4px 0 8px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
td:last-child {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover td:last-child {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #4a6cf7;
|
||||||
|
box-shadow: 0 0 0 3px rgba(74,108,247,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.form-control {
|
||||||
|
min-height: 100px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
select.form-control {
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.2s, opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #4a6cf7;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: #22c55e;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #ef4444;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-default {
|
||||||
|
background: #fff;
|
||||||
|
color: #666;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-default:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-info {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges */
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-warning {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-success {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-danger {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-info {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-secondary {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert */
|
||||||
|
.alert {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
border: 1px solid #bfdbfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Auth pages */
|
||||||
|
.auth-page {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
||||||
|
padding: 32px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card h1 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card .form-control {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card .captcha-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card .captcha-row .form-control {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card .captcha-row img {
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-footer a {
|
||||||
|
color: #4a6cf7;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination */
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination a, .pagination span:not(.page-jump span) {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #4a6cf7;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination .active {
|
||||||
|
background: #4a6cf7;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #4a6cf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination a:hover {
|
||||||
|
background: #eef2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination span.disabled {
|
||||||
|
color: #bbb;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-color: #eee;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination span.ellipsis {
|
||||||
|
border: none;
|
||||||
|
color: #999;
|
||||||
|
background: transparent;
|
||||||
|
padding: 6px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination .page-jump {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination .page-jump input[type="number"] {
|
||||||
|
width: 60px;
|
||||||
|
padding: 5px 6px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination .page-jump button {
|
||||||
|
padding: 5px 10px;
|
||||||
|
border: 1px solid #4a6cf7;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #4a6cf7;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination .page-jump button:hover {
|
||||||
|
background: #3b5de7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats grid */
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .stat-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .stat-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #777;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 200;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content h3 {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
float: right;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action buttons group */
|
||||||
|
.action-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File input */
|
||||||
|
.form-control-file {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.nav-toggle {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 8px 0 16px;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link, .nav-user {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
margin: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-group {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-group .btn-sm {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,284 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
require_once __DIR__ . '/includes/functions.php';
|
||||||
|
requireLogin();
|
||||||
|
|
||||||
|
$pageTitle = '账单管理';
|
||||||
|
require __DIR__ . '/includes/header.php';
|
||||||
|
|
||||||
|
$isAdmin = isAdmin();
|
||||||
|
$userId = $isAdmin ? (int)($_GET['user_id'] ?? 0) : getCurrentUserId();
|
||||||
|
$detailDate = $_GET['date'] ?? '';
|
||||||
|
$detailUserId = (int)($_GET['duid'] ?? $userId);
|
||||||
|
$startDate = $_GET['start_date'] ?? '';
|
||||||
|
$endDate = $_GET['end_date'] ?? '';
|
||||||
|
|
||||||
|
// 管理员:修改账单状态
|
||||||
|
$statusMsg = '';
|
||||||
|
if ($isAdmin && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
|
if (!verifyCsrf($_POST['csrf_token'] ?? '')) { die('CSRF token无效'); }
|
||||||
|
if ($_POST['action'] === 'set_bill_status') {
|
||||||
|
$sDate = $_POST['bill_date'] ?? '';
|
||||||
|
$sUserId = (int)($_POST['user_id'] ?? 0);
|
||||||
|
$sStatus = $_POST['status'] ?? '';
|
||||||
|
$sStartDate = $_POST['start_date'] ?? '';
|
||||||
|
$sEndDate = $_POST['end_date'] ?? '';
|
||||||
|
if ($sDate && $sUserId > 0 && in_array($sStatus, ['未结算', '已结算', '已作废'])) {
|
||||||
|
$stmt = $pdo->prepare('INSERT INTO bill_status (bill_date, user_id, status, updated_at, updated_by) VALUES (?, ?, ?, NOW(), ?) ON DUPLICATE KEY UPDATE status = ?, updated_at = NOW(), updated_by = ?');
|
||||||
|
$stmt->execute([$sDate, $sUserId, $sStatus, getCurrentUserId(), $sStatus, getCurrentUserId()]);
|
||||||
|
$_SESSION['flash_msg'] = '账单状态已更新';
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
$queryRedirect = [];
|
||||||
|
$filterUserId = $userId; // 保留原始筛选条件
|
||||||
|
if ($filterUserId > 0) $queryRedirect[] = 'user_id=' . $filterUserId;
|
||||||
|
if ($sStartDate) $queryRedirect[] = 'start_date=' . $sStartDate;
|
||||||
|
if ($sEndDate) $queryRedirect[] = 'end_date=' . $sEndDate;
|
||||||
|
header('Location: bill_records.php' . ($queryRedirect ? '?' . implode('&', $queryRedirect) : ''));
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$statusMsg = '<div class="alert alert-danger">参数错误</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 详情模式
|
||||||
|
if ($detailDate) {
|
||||||
|
$pid = getCurrentProductId();
|
||||||
|
$where = 'WHERE r.status = 2 AND r.product_id = ? AND DATE(r.used_at) = ?';
|
||||||
|
$params = [$pid, $detailDate];
|
||||||
|
if ($detailUserId > 0) {
|
||||||
|
$where .= ' AND r.user_id = ?';
|
||||||
|
$params[] = $detailUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT r.*, u.username FROM claim_records r LEFT JOIN users u ON r.user_id = u.id $where ORDER BY r.id ASC");
|
||||||
|
$stmt->execute($params);
|
||||||
|
$details = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// 读取该日该用户的账单状态
|
||||||
|
$stmt = $pdo->prepare("SELECT status FROM bill_status WHERE bill_date = ? AND user_id = ?");
|
||||||
|
$stmt->execute([$detailDate, $detailUserId]);
|
||||||
|
$billStatus = $stmt->fetchColumn() ?: '未结算';
|
||||||
|
?>
|
||||||
|
<div class="card">
|
||||||
|
<h2>
|
||||||
|
账单明细 - <?= h($detailDate) ?>
|
||||||
|
<?php if ($detailUserId > 0 && isset($details[0]['username'])): ?>
|
||||||
|
(<?= h($details[0]['username']) ?>)
|
||||||
|
<?php endif; ?>
|
||||||
|
<span style="font-size:14px;font-weight:normal;margin-left:12px;">状态:<?= billStatusBadge($billStatus) ?></span>
|
||||||
|
</h2>
|
||||||
|
<div style="margin-bottom:16px;">
|
||||||
|
<a href="bill_records.php<?= $userId > 0 ? '?user_id=' . $userId : '' ?><?= $startDate ? '&start_date=' . $startDate : '' ?><?= $endDate ? '&end_date=' . $endDate : '' ?>" class="btn btn-primary btn-sm">← 返回</a>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>兑换码名称</th>
|
||||||
|
<th>批次号</th>
|
||||||
|
<th>兑换码</th>
|
||||||
|
<th>面值</th>
|
||||||
|
<th>使用时间</th>
|
||||||
|
<th>会员ID</th>
|
||||||
|
<th>用户</th>
|
||||||
|
<th>出货价一档</th>
|
||||||
|
<th>出货价二挡</th>
|
||||||
|
<th>备注</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($details as $r): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= $r['id'] ?></td>
|
||||||
|
<td><?= h($r['code_name']) ?></td>
|
||||||
|
<td><?= h($r['batch_no']) ?></td>
|
||||||
|
<td><?= h($r['code']) ?></td>
|
||||||
|
<td><?= h($r['value']) ?></td>
|
||||||
|
<td><?= formatDateTime($r['used_at']) ?></td>
|
||||||
|
<td><?= h($r['used_user_id'] ?? '-') ?></td>
|
||||||
|
<td><?= h($r['username'] ?? '-') ?></td>
|
||||||
|
<td><?= $r['price_tier1'] !== null ? h($r['price_tier1']) : '-' ?></td>
|
||||||
|
<td><?= $r['price_tier2'] !== null ? h($r['price_tier2']) : '-' ?></td>
|
||||||
|
<td><?= h($r['remark'] ?? '') ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($details)): ?>
|
||||||
|
<tr><td colspan="11" style="text-align:center;color:#999;">暂无数据</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
require __DIR__ . '/includes/footer.php';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 汇总模式
|
||||||
|
$page = max(1, (int)($_GET['p'] ?? 1));
|
||||||
|
$perPage = 30;
|
||||||
|
$offset = ($page - 1) * $perPage;
|
||||||
|
|
||||||
|
$pid = getCurrentProductId();
|
||||||
|
$where = 'WHERE r.status = 2 AND r.product_id = ?';
|
||||||
|
$params = [$pid];
|
||||||
|
if ($userId > 0) {
|
||||||
|
$where .= ' AND r.user_id = ?';
|
||||||
|
$params[] = $userId;
|
||||||
|
}
|
||||||
|
if ($startDate) {
|
||||||
|
$where .= ' AND DATE(r.used_at) >= ?';
|
||||||
|
$params[] = $startDate;
|
||||||
|
}
|
||||||
|
if ($endDate) {
|
||||||
|
$where .= ' AND DATE(r.used_at) <= ?';
|
||||||
|
$params[] = $endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT COUNT(DISTINCT DATE(r.used_at)" . ($userId > 0 ? "" : ", r.user_id") . ") FROM claim_records r $where");
|
||||||
|
$stmt->execute($params);
|
||||||
|
$total = (int)$stmt->fetchColumn();
|
||||||
|
$totalPages = max(1, ceil($total / $perPage));
|
||||||
|
|
||||||
|
$groupBy = $userId > 0 ? 'DATE(r.used_at)' : 'DATE(r.used_at), r.user_id';
|
||||||
|
$selectCols = $userId > 0
|
||||||
|
? "DATE(r.used_at) AS bill_date, r.user_id AS uid, COUNT(*) AS total, SUM(COALESCE(r.price_tier1, 0)) AS total_price1, SUM(COALESCE(r.price_tier2, 0)) AS total_price2"
|
||||||
|
: "DATE(r.used_at) AS bill_date, r.user_id AS uid, u.username, COUNT(*) AS total, SUM(COALESCE(r.price_tier1, 0)) AS total_price1, SUM(COALESCE(r.price_tier2, 0)) AS total_price2";
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT $selectCols FROM claim_records r LEFT JOIN users u ON r.user_id = u.id $where GROUP BY $groupBy ORDER BY bill_date DESC" . ($userId > 0 ? '' : ', u.username ASC') . " LIMIT ? OFFSET ?");
|
||||||
|
$stmt->execute(array_merge($params, [$perPage, $offset]));
|
||||||
|
$records = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// 批量查询账单状态
|
||||||
|
$statusMap = [];
|
||||||
|
if (!empty($records)) {
|
||||||
|
$cases = [];
|
||||||
|
foreach ($records as $r) {
|
||||||
|
$uid = $userId > 0 ? $userId : $r['uid'];
|
||||||
|
$cases[] = "('" . $r['bill_date'] . "', " . (int)$uid . ")";
|
||||||
|
}
|
||||||
|
if (!empty($cases)) {
|
||||||
|
$stmt = $pdo->query("SELECT CONCAT(bill_date, '_', user_id) AS k, status FROM bill_status WHERE (bill_date, user_id) IN (" . implode(',', $cases) . ")");
|
||||||
|
while ($row = $stmt->fetch()) {
|
||||||
|
$statusMap[$row['k']] = $row['status'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function billStatusBadge(string $status): string {
|
||||||
|
switch ($status) {
|
||||||
|
case '未结算': return '<span class="badge badge-warning">未结算</span>';
|
||||||
|
case '已结算': return '<span class="badge badge-success">已结算</span>';
|
||||||
|
case '已作废': return '<span class="badge badge-danger">已作废</span>';
|
||||||
|
default: return '<span class="badge">未知</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$allUsers = $isAdmin ? $pdo->query('SELECT id, username FROM users ORDER BY id ASC')->fetchAll() : [];
|
||||||
|
|
||||||
|
// 构建筛选条件 URL 参数
|
||||||
|
$queryParams = [];
|
||||||
|
if ($userId > 0) $queryParams['user_id'] = $userId;
|
||||||
|
if ($startDate) $queryParams['start_date'] = $startDate;
|
||||||
|
if ($endDate) $queryParams['end_date'] = $endDate;
|
||||||
|
?>
|
||||||
|
<div class="card">
|
||||||
|
<h2>账单管理</h2>
|
||||||
|
<?php if (isset($_SESSION['flash_msg'])): ?>
|
||||||
|
<div class="alert alert-<?= h($_SESSION['flash_type'] ?? 'success') ?>"><?= h($_SESSION['flash_msg']) ?></div>
|
||||||
|
<?php unset($_SESSION['flash_msg'], $_SESSION['flash_type']); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?= $statusMsg ?>
|
||||||
|
<?php if ($isAdmin): ?>
|
||||||
|
<form method="get" style="margin-bottom:12px;display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
||||||
|
<label>用户:</label>
|
||||||
|
<select name="user_id" class="form-control" style="max-width:160px;" onchange="this.form.submit()">
|
||||||
|
<option value="">全部</option>
|
||||||
|
<?php foreach ($allUsers as $u): ?>
|
||||||
|
<option value="<?= $u['id'] ?>" <?= $userId === (int)$u['id'] ? 'selected' : '' ?>><?= h($u['username']) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<label>开始日期:</label>
|
||||||
|
<input type="date" name="start_date" class="form-control" style="max-width:160px;" value="<?= h($startDate) ?>">
|
||||||
|
<label>结束日期:</label>
|
||||||
|
<input type="date" name="end_date" class="form-control" style="max-width:160px;" value="<?= h($endDate) ?>">
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm">查询</button>
|
||||||
|
<?php if ($startDate || $endDate || $userId > 0): ?>
|
||||||
|
<a href="bill_records.php" class="btn btn-sm" style="background:#999;color:#fff;">清空</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
<?php else: ?>
|
||||||
|
<form method="get" style="margin-bottom:12px;display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
||||||
|
<label>开始日期:</label>
|
||||||
|
<input type="date" name="start_date" class="form-control" style="max-width:160px;" value="<?= h($startDate) ?>">
|
||||||
|
<label>结束日期:</label>
|
||||||
|
<input type="date" name="end_date" class="form-control" style="max-width:160px;" value="<?= h($endDate) ?>">
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm">查询</button>
|
||||||
|
<?php if ($startDate || $endDate): ?>
|
||||||
|
<a href="bill_records.php" class="btn btn-sm" style="background:#999;color:#fff;">清空</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>日期</th>
|
||||||
|
<?php if (!$userId): ?><th>用户</th><?php endif; ?>
|
||||||
|
<th>使用数量</th>
|
||||||
|
<th>出货价一档合计</th>
|
||||||
|
<th>出货价二挡合计</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<?php if ($isAdmin): ?><th>操作</th><?php endif; ?>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($records as $r): ?>
|
||||||
|
<?php
|
||||||
|
$uid = $userId > 0 ? $userId : $r['uid'];
|
||||||
|
$statusKey = $r['bill_date'] . '_' . $uid;
|
||||||
|
$billStatus = $statusMap[$statusKey] ?? '未结算';
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td><?= h($r['bill_date']) ?></td>
|
||||||
|
<?php if (!$userId): ?><td><?= h($r['username'] ?? '-') ?></td><?php endif; ?>
|
||||||
|
<td><a href="bill_records.php?date=<?= h($r['bill_date']) ?>&duid=<?= $uid ?><?= $startDate ? '&start_date=' . $startDate : '' ?><?= $endDate ? '&end_date=' . $endDate : '' ?>" class="btn btn-info btn-sm"><?= (int)$r['total'] ?></a></td>
|
||||||
|
<td><?= $r['total_price1'] > 0 ? number_format($r['total_price1'], 2) : '-' ?></td>
|
||||||
|
<td><?= $r['total_price2'] > 0 ? number_format($r['total_price2'], 2) : '-' ?></td>
|
||||||
|
<td><?= billStatusBadge($billStatus) ?></td>
|
||||||
|
<?php if ($isAdmin): ?>
|
||||||
|
<td>
|
||||||
|
<form method="post" style="display:inline;">
|
||||||
|
<input type="hidden" name="action" value="set_bill_status">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= csrfToken() ?>">
|
||||||
|
<input type="hidden" name="bill_date" value="<?= h($r['bill_date']) ?>">
|
||||||
|
<input type="hidden" name="user_id" value="<?= $uid ?>">
|
||||||
|
<?php if ($startDate): ?><input type="hidden" name="start_date" value="<?= h($startDate) ?>"><?php endif; ?>
|
||||||
|
<?php if ($endDate): ?><input type="hidden" name="end_date" value="<?= h($endDate) ?>"><?php endif; ?>
|
||||||
|
<select name="status" class="form-control" style="display:inline-block;width:auto;max-width:100px;padding:4px 6px;font-size:12px;" onchange="this.form.submit()">
|
||||||
|
<option value="未结算" <?= $billStatus === '未结算' ? 'selected' : '' ?>>未结算</option>
|
||||||
|
<option value="已结算" <?= $billStatus === '已结算' ? 'selected' : '' ?>>已结算</option>
|
||||||
|
<option value="已作废" <?= $billStatus === '已作废' ? 'selected' : '' ?>>已作废</option>
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($records)): ?>
|
||||||
|
<tr><td colspan="<?= $userId ? 5 : 6 ?><?= $isAdmin ? 1 : 0 ?>" style="text-align:center;color:#999;">暂无数据</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
$extra = [];
|
||||||
|
if ($userId > 0) $extra['user_id'] = $userId;
|
||||||
|
if ($startDate) $extra['start_date'] = $startDate;
|
||||||
|
if ($endDate) $extra['end_date'] = $endDate;
|
||||||
|
renderPagination($page, $totalPages, $extra);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<?php require __DIR__ . '/includes/footer.php'; ?>
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$width = 120;
|
||||||
|
$height = 42;
|
||||||
|
$fontSize = 20;
|
||||||
|
|
||||||
|
$image = imagecreatetruecolor($width, $height);
|
||||||
|
|
||||||
|
$bgColor = imagecolorallocate($image, 255, 255, 255);
|
||||||
|
imagefill($image, 0, 0, $bgColor);
|
||||||
|
|
||||||
|
// 干扰线
|
||||||
|
for ($i = 0; $i < 5; $i++) {
|
||||||
|
$lineColor = imagecolorallocatealpha($image, rand(100, 200), rand(100, 200), rand(100, 200), 50);
|
||||||
|
imageline($image, rand(0, $width), rand(0, $height), rand(0, $width), rand(0, $height), $lineColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 干扰点
|
||||||
|
for ($i = 0; $i < 80; $i++) {
|
||||||
|
$pixelColor = imagecolorallocate($image, rand(150, 200), rand(150, 200), rand(150, 200));
|
||||||
|
imagesetpixel($image, rand(0, $width), rand(0, $height), $pixelColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||||
|
$code = '';
|
||||||
|
for ($i = 0; $i < 4; $i++) {
|
||||||
|
$code .= $chars[rand(0, strlen($chars) - 1)];
|
||||||
|
}
|
||||||
|
$_SESSION['captcha'] = $code;
|
||||||
|
|
||||||
|
// 尝试使用TTF字体,不存在则用内置字体
|
||||||
|
$fontFile = __DIR__ . '/assets/arial.ttf';
|
||||||
|
if (file_exists($fontFile)) {
|
||||||
|
for ($i = 0; $i < 4; $i++) {
|
||||||
|
$angle = rand(-15, 15);
|
||||||
|
$x = 8 + $i * 28;
|
||||||
|
$textColor = imagecolorallocate($image, rand(30, 100), rand(30, 100), rand(30, 100));
|
||||||
|
imagettftext($image, $fontSize, $angle, $x, 34, $textColor, $fontFile, $code[$i]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$textColor = imagecolorallocate($image, 50, 50, 50);
|
||||||
|
imagestring($image, 5, 10, 12, $code, $textColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: image/png');
|
||||||
|
header('Cache-Control: no-cache, no-store, must-revalidate');
|
||||||
|
imagepng($image);
|
||||||
|
imagedestroy($image);
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
require_once __DIR__ . '/includes/functions.php';
|
||||||
|
requireLogin();
|
||||||
|
|
||||||
|
$pageTitle = '兑换码领取';
|
||||||
|
require __DIR__ . '/includes/header.php';
|
||||||
|
|
||||||
|
$msg = '';
|
||||||
|
$pid = getCurrentProductId();
|
||||||
|
|
||||||
|
// 获取所有面值及对应可领取数量
|
||||||
|
$stmt = $pdo->prepare("SELECT value, COUNT(*) AS cnt FROM redemption_codes WHERE status = 1 AND (expired_at IS NULL OR expired_at > NOW()) AND product_id = ? GROUP BY value ORDER BY value ASC");
|
||||||
|
$stmt->execute([$pid]);
|
||||||
|
$valueOptions = $stmt->fetchAll();
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
if (!verifyCsrf($_POST['csrf_token'] ?? '')) { die('CSRF token无效'); }
|
||||||
|
$codeValue = trim($_POST['code_value'] ?? '');
|
||||||
|
$quantity = max(1, min(100, (int)($_POST['quantity'] ?? 1)));
|
||||||
|
|
||||||
|
// 校验面值是否有效
|
||||||
|
$validValues = array_map(fn($v) => $v['value'], $valueOptions);
|
||||||
|
if (!in_array($codeValue, $validValues, true)) {
|
||||||
|
$msg = '<div class="alert alert-danger">无效的面值</div>';
|
||||||
|
} else {
|
||||||
|
// 检查当前用户未使用兑换码数量
|
||||||
|
$stmt = $pdo->prepare("SELECT COUNT(*) FROM claim_records WHERE user_id = ? AND product_id = ? AND status = 1");
|
||||||
|
$stmt->execute([getCurrentUserId(), $pid]);
|
||||||
|
$unusedCount = (int)$stmt->fetchColumn();
|
||||||
|
if ($unusedCount >= 200) {
|
||||||
|
$msg = '<div class="alert alert-danger">您当前未使用的兑换码已达 ' . $unusedCount . ' 条(上限200条),请先使用后再领取</div>';
|
||||||
|
} else {
|
||||||
|
// 检查频繁领取(同面值距上一次领取不足3天)
|
||||||
|
$stmt = $pdo->prepare("SELECT MAX(claimed_at) FROM claim_records WHERE user_id = ? AND product_id = ? AND value = ?");
|
||||||
|
$stmt->execute([getCurrentUserId(), $pid, $codeValue]);
|
||||||
|
$lastClaim = $stmt->fetchColumn();
|
||||||
|
if ($lastClaim && strtotime($lastClaim) > strtotime('-3 days')) {
|
||||||
|
$remaining = ceil((strtotime($lastClaim) + 3 * 86400 - time()) / 3600);
|
||||||
|
$msg = '<div class="alert alert-danger">当前面值兑换码距上次领取不足3天,请 ' . $remaining . ' 小时后再领取</div>';
|
||||||
|
} else {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
try {
|
||||||
|
// 查找指定面值的可领取兑换码
|
||||||
|
$stmt = $pdo->prepare('SELECT * FROM redemption_codes WHERE value = ? AND status = 1 AND (expired_at IS NULL OR expired_at > NOW()) AND product_id = ? ORDER BY id ASC LIMIT ? FOR UPDATE');
|
||||||
|
$stmt->execute([$codeValue, $pid, $quantity]);
|
||||||
|
$available = $stmt->fetchAll();
|
||||||
|
|
||||||
|
if (count($available) < $quantity) {
|
||||||
|
$msg = '<div class="alert alert-danger">该面值的可领取兑换码不足,当前仅有 ' . count($available) . ' 个可用</div>';
|
||||||
|
$pdo->rollBack();
|
||||||
|
} else {
|
||||||
|
$claimed = 0;
|
||||||
|
foreach ($available as $code) {
|
||||||
|
$stmt = $pdo->prepare('UPDATE redemption_codes SET status = 2, claim_user_id = ?, claimed_at = NOW() WHERE id = ?');
|
||||||
|
$stmt->execute([getCurrentUserId(), $code['id']]);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('INSERT INTO claim_records (product_id, created_at, user_id, code_name, batch_no, code_type, code, value, status, expired_at, price_tier1, price_tier2, claim_user, claimed_at) VALUES (?, NOW(), ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, NOW())');
|
||||||
|
$stmt->execute([
|
||||||
|
$code['product_id'],
|
||||||
|
getCurrentUserId(),
|
||||||
|
$code['name'],
|
||||||
|
$code['batch_no'],
|
||||||
|
$code['type'],
|
||||||
|
$code['code'],
|
||||||
|
$code['value'],
|
||||||
|
$code['expired_at'],
|
||||||
|
$code['price_tier1'],
|
||||||
|
$code['price_tier2'],
|
||||||
|
getCurrentUsername()
|
||||||
|
]);
|
||||||
|
$claimed++;
|
||||||
|
}
|
||||||
|
$pdo->commit();
|
||||||
|
$_SESSION['flash_msg'] = '成功领取 ' . $claimed . ' 个兑换码!';
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
redirect('claim_code.php');
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
$msg = '<div class="alert alert-danger">领取失败,请重试</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalAvailable = array_sum(array_column($valueOptions, 'cnt'));
|
||||||
|
?>
|
||||||
|
<div class="card">
|
||||||
|
<h2>兑换码领取</h2>
|
||||||
|
<?php if (isset($_SESSION['flash_msg'])): ?>
|
||||||
|
<div class="alert alert-<?= h($_SESSION['flash_type'] ?? 'success') ?>"><?= h($_SESSION['flash_msg']) ?></div>
|
||||||
|
<?php unset($_SESSION['flash_msg'], $_SESSION['flash_type']); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?= $msg ?>
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= csrfToken() ?>">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>兑换码面值</label>
|
||||||
|
<select name="code_value" class="form-control" required>
|
||||||
|
<option value="">-- 请选择面值 --</option>
|
||||||
|
<?php foreach ($valueOptions as $opt): ?>
|
||||||
|
<option value="<?= h($opt['value']) ?>" <?= (isset($_POST['code_value']) && $_POST['code_value'] === $opt['value']) ? 'selected' : '' ?>>
|
||||||
|
<?= h($opt['value']) ?>(剩余 <?= $opt['cnt'] ?> 个)
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>领取数量</label>
|
||||||
|
<input type="number" name="quantity" class="form-control" value="1" min="1" max="100" required>
|
||||||
|
<div style="font-size:12px;color:#999;margin-top:4px;">当前共计可领取: <?= $totalAvailable ?> 个,单次最多100个</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-success" onclick="return confirm('确认领取?')">立即领取</button>
|
||||||
|
<button type="button" class="btn btn-default" onclick="history.back()" style="margin-left:8px;">取消</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php require __DIR__ . '/includes/footer.php'; ?>
|
||||||
|
|
@ -0,0 +1,367 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
require_once __DIR__ . '/includes/functions.php';
|
||||||
|
requireLogin();
|
||||||
|
|
||||||
|
// 导出 CSV
|
||||||
|
if (isset($_GET['export'])) {
|
||||||
|
$isAdmin = isAdmin();
|
||||||
|
$pid = getCurrentProductId();
|
||||||
|
$where = 'WHERE r.product_id = ?';
|
||||||
|
$params = [$pid];
|
||||||
|
if (!$isAdmin) {
|
||||||
|
$where .= ' AND r.user_id = ?';
|
||||||
|
$params[] = getCurrentUserId();
|
||||||
|
}
|
||||||
|
if (!empty($_GET['search'])) {
|
||||||
|
$s = '%' . $_GET['search'] . '%';
|
||||||
|
$where .= ' AND (r.code LIKE ? OR r.batch_no LIKE ? OR r.code_name LIKE ?)';
|
||||||
|
$params = array_merge($params, [$s, $s, $s]);
|
||||||
|
}
|
||||||
|
$stmt = $pdo->prepare("SELECT r.id, r.code_name, r.batch_no, r.code, r.value, r.status, r.expired_at FROM claim_records r $where ORDER BY r.id DESC");
|
||||||
|
$stmt->execute($params);
|
||||||
|
$rows = $stmt->fetchAll();
|
||||||
|
|
||||||
|
header('Content-Type: text/csv; charset=utf-8');
|
||||||
|
header('Content-Disposition: attachment; filename=claim_records.csv');
|
||||||
|
echo "\xEF\xBB\xBF";
|
||||||
|
$f = fopen('php://output', 'w');
|
||||||
|
fputcsv($f, ['ID', '兑换码名称', '批次号', '兑换码', '面值', '状态', '过期时间']);
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$statusMap = [1 => '未使用', 2 => '已使用', 3 => '已过期'];
|
||||||
|
fputcsv($f, [
|
||||||
|
$r['id'],
|
||||||
|
$r['code_name'],
|
||||||
|
$r['batch_no'],
|
||||||
|
(int)$r['status'] === 3 ? maskCode($r['code']) : $r['code'],
|
||||||
|
$r['value'],
|
||||||
|
$statusMap[(int)$r['status']] ?? '未知',
|
||||||
|
$r['expired_at'] ? date('Y-m-d H:i:s', strtotime($r['expired_at'])) : '',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
fclose($f);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageTitle = '兑换码领取记录';
|
||||||
|
require __DIR__ . '/includes/header.php';
|
||||||
|
|
||||||
|
$isAdmin = isAdmin();
|
||||||
|
$msg = '';
|
||||||
|
|
||||||
|
// 管理员:修改状态
|
||||||
|
if ($isAdmin && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
|
if (!verifyCsrf($_POST['csrf_token'] ?? '')) { die('CSRF token无效'); }
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($_POST['action'] === 'edit_status') {
|
||||||
|
$newStatus = (int)($_POST['status'] ?? 0);
|
||||||
|
$usedAt = ($_POST['used_at'] ?? '') !== '' ? str_replace('T', ' ', $_POST['used_at']) . ':00' : '';
|
||||||
|
$userId = (int)($_POST['user_id'] ?? 0);
|
||||||
|
$usedUserId = trim($_POST['used_user_id'] ?? '');
|
||||||
|
if ($newStatus < 1 || $newStatus > 3) {
|
||||||
|
$msg = '<div class="alert alert-danger">无效的状态值</div>';
|
||||||
|
} elseif ($newStatus === 2 && $usedUserId === '') {
|
||||||
|
$msg = '<div class="alert alert-danger">状态设为已使用时,必须填写会员ID</div>';
|
||||||
|
} else {
|
||||||
|
$update = ['status = ?'];
|
||||||
|
$params = [$newStatus];
|
||||||
|
if ($usedAt !== '') { $update[] = 'used_at = ?'; $params[] = $usedAt; }
|
||||||
|
if ($userId > 0) { $update[] = 'user_id = ?'; $params[] = $userId; }
|
||||||
|
if ($usedUserId !== '') { $update[] = 'used_user_id = ?'; $params[] = $usedUserId; }
|
||||||
|
$params[] = $id;
|
||||||
|
$stmt = $pdo->prepare('UPDATE claim_records SET ' . implode(', ', $update) . ' WHERE id = ?');
|
||||||
|
$stmt->execute($params);
|
||||||
|
$_SESSION['flash_msg'] = '状态已更新';
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
header('Location: claim_records.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} elseif ($_POST['action'] === 'manual_add') {
|
||||||
|
$codeName = trim($_POST['code_name'] ?? '');
|
||||||
|
$batchNo = trim($_POST['batch_no'] ?? '');
|
||||||
|
$codeType = (int)($_POST['code_type'] ?? 1);
|
||||||
|
$code = trim($_POST['code'] ?? '');
|
||||||
|
$value = trim($_POST['value'] ?? '');
|
||||||
|
$expiredAt = $_POST['expired_at'] ?: null;
|
||||||
|
$userId = (int)($_POST['user_id'] ?? 0);
|
||||||
|
$claimedAt = ($_POST['claimed_at'] ?? '') !== '' ? str_replace('T', ' ', $_POST['claimed_at']) . ':00' : null;
|
||||||
|
$price1 = $_POST['price_tier1'] !== '' ? (float)$_POST['price_tier1'] : null;
|
||||||
|
$price2 = $_POST['price_tier2'] !== '' ? (float)$_POST['price_tier2'] : null;
|
||||||
|
$recStatus = (int)($_POST['rec_status'] ?? 1);
|
||||||
|
if (!in_array($recStatus, [1, 2, 3])) $recStatus = 1;
|
||||||
|
$usedUserId = trim($_POST['used_user_id'] ?? '');
|
||||||
|
$usedAt = ($_POST['used_at'] ?? '') !== '' ? str_replace('T', ' ', $_POST['used_at']) . ':00' : null;
|
||||||
|
if (!$codeName || !$batchNo || !$code) {
|
||||||
|
$msg = '<div class="alert alert-danger">兑换码名称、批次号、兑换码为必填项</div>';
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare('INSERT INTO claim_records (product_id, created_at, user_id, code_name, batch_no, code_type, code, value, status, expired_at, price_tier1, price_tier2, claimed_at, used_user_id, used_at, remark) VALUES (?, NOW(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
||||||
|
$stmt->execute([getCurrentProductId(), $userId ?: null, $codeName, $batchNo, $codeType, $code, $value, $recStatus, $expiredAt, $price1, $price2, $claimedAt, $usedUserId ?: null, $usedAt, '手动添加']);
|
||||||
|
$_SESSION['flash_msg'] = '手动添加成功';
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
header('Location: claim_records.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页 + 条件
|
||||||
|
$page = max(1, (int)($_GET['p'] ?? 1));
|
||||||
|
$perPage = 20;
|
||||||
|
$offset = ($page - 1) * $perPage;
|
||||||
|
|
||||||
|
$pid = getCurrentProductId();
|
||||||
|
$where = 'WHERE r.product_id = ?';
|
||||||
|
$searchParams = [$pid];
|
||||||
|
|
||||||
|
if (!$isAdmin) {
|
||||||
|
$where .= ' AND r.user_id = ?';
|
||||||
|
$searchParams[] = getCurrentUserId();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($_GET['search'])) {
|
||||||
|
$search = '%' . $_GET['search'] . '%';
|
||||||
|
$where .= ' AND (r.code LIKE ? OR r.batch_no LIKE ? OR r.code_name LIKE ?)';
|
||||||
|
$searchParams = array_merge($searchParams, [$search, $search, $search]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT COUNT(*) FROM claim_records r $where");
|
||||||
|
$stmt->execute($searchParams);
|
||||||
|
$total = (int)$stmt->fetchColumn();
|
||||||
|
$totalPages = max(1, ceil($total / $perPage));
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT r.*, u.username FROM claim_records r LEFT JOIN users u ON r.user_id = u.id $where ORDER BY r.id DESC LIMIT ? OFFSET ?");
|
||||||
|
$stmt->execute(array_merge($searchParams, [$perPage, $offset]));
|
||||||
|
$records = $stmt->fetchAll();
|
||||||
|
?>
|
||||||
|
<div class="card">
|
||||||
|
<h2>兑换码领取记录</h2>
|
||||||
|
<?php if (isset($_SESSION['flash_msg'])): ?>
|
||||||
|
<div class="alert alert-<?= h($_SESSION['flash_type'] ?? 'success') ?>"><?= h($_SESSION['flash_msg']) ?></div>
|
||||||
|
<?php unset($_SESSION['flash_msg'], $_SESSION['flash_type']); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?= $msg ?>
|
||||||
|
<div style="margin-bottom:16px;display:flex;flex-wrap:wrap;gap:8px;">
|
||||||
|
<a href="claim_code.php" class="btn btn-info btn-sm">兑换码领取</a>
|
||||||
|
<?php if ($isAdmin): ?>
|
||||||
|
<a href="javascript:void(0)" class="btn btn-success btn-sm" onclick="showModal('addModal')">+ 手动添加</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<a href="?export=1<?= !empty($_GET['search']) ? '&search=' . urlencode($_GET['search']) : '' ?>" class="btn btn-primary btn-sm">下载 CSV</a>
|
||||||
|
<form method="get" style="display:flex;gap:8px;margin-left:auto;">
|
||||||
|
<input type="text" name="search" class="form-control" placeholder="搜索兑换码/批次号/名称" value="<?= h($_GET['search'] ?? '') ?>" style="max-width:300px;">
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm">搜索</button>
|
||||||
|
<?php if (!empty($_GET['search'])): ?>
|
||||||
|
<a href="claim_records.php" class="btn btn-sm" style="background:#999;color:#fff;">清空</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>创建时间</th>
|
||||||
|
<th>兑换码名称</th>
|
||||||
|
<th>批次号</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>兑换码</th>
|
||||||
|
<th>面值</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>过期时间</th>
|
||||||
|
<th>使用时间</th>
|
||||||
|
<th>会员ID</th>
|
||||||
|
<th>用户</th>
|
||||||
|
<th>领取时间</th>
|
||||||
|
<?php if ($isAdmin): ?>
|
||||||
|
<th>出货价一档</th>
|
||||||
|
<th>出货价二挡</th>
|
||||||
|
<?php endif; ?>
|
||||||
|
<th>备注</th>
|
||||||
|
<?php if ($isAdmin): ?><th>操作</th><?php endif; ?>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($records as $r): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= $r['id'] ?></td>
|
||||||
|
<td><?= formatDateTime($r['created_at']) ?></td>
|
||||||
|
<td><?= h($r['code_name']) ?></td>
|
||||||
|
<td><?= h($r['batch_no']) ?></td>
|
||||||
|
<td><?= h($r['code_type']) ?></td>
|
||||||
|
<td><?php if ((int)$r['status'] === 3): ?><code><?= h(maskCode($r['code'])) ?></code><?php else: ?><?= h($r['code']) ?><?php endif; ?></td>
|
||||||
|
<td><?= h($r['value']) ?></td>
|
||||||
|
<td><?= statusBadge((int)$r['status']) ?></td>
|
||||||
|
<td><?= formatDateTime($r['expired_at']) ?></td>
|
||||||
|
<td><?= formatDateTime($r['used_at']) ?></td>
|
||||||
|
<td><?= h($r['used_user_id'] ?? '-') ?></td>
|
||||||
|
<td><?= h($r['username'] ?? $r['user_id'] ?? '-') ?></td>
|
||||||
|
<td><?= formatDateTime($r['claimed_at']) ?></td>
|
||||||
|
<?php if ($isAdmin): ?>
|
||||||
|
<td><?= $r['price_tier1'] !== null ? h($r['price_tier1']) : '-' ?></td>
|
||||||
|
<td><?= $r['price_tier2'] !== null ? h($r['price_tier2']) : '-' ?></td>
|
||||||
|
<?php endif; ?>
|
||||||
|
<td><?= h($r['remark'] ?? '') ?></td>
|
||||||
|
<?php if ($isAdmin): ?>
|
||||||
|
<td>
|
||||||
|
<a href="javascript:void(0)" class="btn btn-warning btn-sm" onclick="editStatus(<?= $r['id'] ?>, <?= $r['status'] ?>, '<?= h($r['used_at'] ?? '') ?>', <?= (int)($r['user_id'] ?? 0) ?>, '<?= h($r['used_user_id'] ?? '') ?>')">修改状态</a>
|
||||||
|
</td>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($records)): ?>
|
||||||
|
<tr><td colspan="<?= $isAdmin ? 17 : 14 ?>" style="text-align:center;color:#999;">暂无数据</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php renderPagination($page, $totalPages, !empty($_GET['search']) ? ['search' => $_GET['search']] : []); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$allUsers = $pdo->query('SELECT id, username FROM users ORDER BY id ASC')->fetchAll();
|
||||||
|
if ($isAdmin):
|
||||||
|
?>
|
||||||
|
<!-- 修改状态弹窗 -->
|
||||||
|
<div class="modal" id="editModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="modal-close" onclick="hideModal('editModal')">×</span>
|
||||||
|
<h3>修改状态</h3>
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="action" value="edit_status">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= csrfToken() ?>">
|
||||||
|
<input type="hidden" name="id" id="edit_id">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>状态</label>
|
||||||
|
<select name="status" class="form-control" id="edit_status">
|
||||||
|
<option value="1">未使用</option>
|
||||||
|
<option value="2">已使用</option>
|
||||||
|
<option value="3">已过期</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>使用时间</label>
|
||||||
|
<input type="datetime-local" name="used_at" class="form-control" id="edit_used_at">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>用户</label>
|
||||||
|
<select name="user_id" class="form-control" id="edit_user_id">
|
||||||
|
<option value="">-- 不关联 --</option>
|
||||||
|
<?php foreach ($allUsers as $u): ?>
|
||||||
|
<option value="<?= $u['id'] ?>"><?= h($u['username']) ?> (ID: <?= $u['id'] ?>)</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" id="edit_used_user_group">
|
||||||
|
<label>会员ID <span id="edit_used_user_required" style="color:red;display:none;">*</span></label>
|
||||||
|
<input type="text" name="used_user_id" class="form-control" id="edit_used_user_id" placeholder="使用该兑换码的用户标识">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">保存</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 手动添加弹窗 -->
|
||||||
|
<div class="modal" id="addModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="modal-close" onclick="hideModal('addModal')">×</span>
|
||||||
|
<h3>手动添加记录</h3>
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="action" value="manual_add">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= csrfToken() ?>">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>兑换码名称 <span style="color:red">*</span></label>
|
||||||
|
<input type="text" name="code_name" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>批次号 <span style="color:red">*</span></label>
|
||||||
|
<input type="text" name="batch_no" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>兑换码类型</label>
|
||||||
|
<input type="number" name="code_type" class="form-control" value="1" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>兑换码 <span style="color:red">*</span></label>
|
||||||
|
<input type="text" name="code" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>面值</label>
|
||||||
|
<input type="text" name="value" class="form-control" placeholder="如: 100元">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>状态</label>
|
||||||
|
<select name="rec_status" class="form-control">
|
||||||
|
<option value="1">未使用</option>
|
||||||
|
<option value="2">已使用</option>
|
||||||
|
<option value="3">已过期</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>使用时间</label>
|
||||||
|
<input type="datetime-local" name="used_at" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>过期时间</label>
|
||||||
|
<input type="text" name="expired_at" class="form-control" placeholder="2027-06-01 12:26:53">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>会员ID</label>
|
||||||
|
<input type="text" name="used_user_id" class="form-control" placeholder="使用该兑换码的用户标识">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>用户</label>
|
||||||
|
<select name="user_id" class="form-control">
|
||||||
|
<option value="">-- 不关联 --</option>
|
||||||
|
<?php foreach ($allUsers as $u): ?>
|
||||||
|
<option value="<?= $u['id'] ?>"><?= h($u['username']) ?> (ID: <?= $u['id'] ?>)</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>领取时间</label>
|
||||||
|
<input type="datetime-local" name="claimed_at" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>出货价一档</label>
|
||||||
|
<input type="number" step="0.01" name="price_tier1" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>出货价二挡</label>
|
||||||
|
<input type="number" step="0.01" name="price_tier2" class="form-control">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-success">添加</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function showModal(id) { document.getElementById(id).classList.add('active'); }
|
||||||
|
function hideModal(id) { document.getElementById(id).classList.remove('active'); }
|
||||||
|
function editStatus(id, status, usedAt, userId, usedUserId) {
|
||||||
|
document.getElementById('edit_id').value = id;
|
||||||
|
document.getElementById('edit_status').value = status;
|
||||||
|
document.getElementById('edit_used_at').value = usedAt ? usedAt.replace(' ', 'T').substring(0, 16) : '';
|
||||||
|
document.getElementById('edit_user_id').value = userId;
|
||||||
|
document.getElementById('edit_used_user_id').value = usedUserId;
|
||||||
|
toggleUsedUserRequired();
|
||||||
|
showModal('editModal');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleUsedUserRequired() {
|
||||||
|
var status = document.getElementById('edit_status').value;
|
||||||
|
var indicator = document.getElementById('edit_used_user_required');
|
||||||
|
var input = document.getElementById('edit_used_user_id');
|
||||||
|
if (status === '2') {
|
||||||
|
indicator.style.display = 'inline';
|
||||||
|
input.required = true;
|
||||||
|
} else {
|
||||||
|
indicator.style.display = 'none';
|
||||||
|
input.required = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('edit_status').addEventListener('change', toggleUsedUserRequired);
|
||||||
|
|
||||||
|
document.querySelectorAll('.modal').forEach(function(m) {
|
||||||
|
m.addEventListener('click', function(e) { if (e.target === this) this.classList.remove('active'); });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php require __DIR__ . '/includes/footer.php'; ?>
|
||||||
|
|
@ -0,0 +1,372 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
require_once __DIR__ . '/includes/functions.php';
|
||||||
|
requireAdmin();
|
||||||
|
|
||||||
|
$pageTitle = '库存管理';
|
||||||
|
require __DIR__ . '/includes/header.php';
|
||||||
|
|
||||||
|
$msg = '';
|
||||||
|
$pid = getCurrentProductId();
|
||||||
|
|
||||||
|
// Excel导入 (CSV格式)
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'import') {
|
||||||
|
if (!verifyCsrf($_POST['csrf_token'] ?? '')) { die('CSRF token无效'); }
|
||||||
|
if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
|
||||||
|
$ext = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
|
||||||
|
if (!in_array($ext, ['csv', 'xls', 'xlsx'])) {
|
||||||
|
$msg = '<div class="alert alert-danger">仅支持 CSV、XLS、XLSX 格式</div>';
|
||||||
|
} else {
|
||||||
|
$imported = 0;
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
if ($ext === 'csv') {
|
||||||
|
$handle = fopen($_FILES['file']['tmp_name'], 'r');
|
||||||
|
if ($handle) {
|
||||||
|
$lineNo = 0;
|
||||||
|
while (($row = fgetcsv($handle)) !== false) {
|
||||||
|
$lineNo++;
|
||||||
|
if ($lineNo === 1) continue; // 跳过表头
|
||||||
|
|
||||||
|
if (count($row) < 5) {
|
||||||
|
$errors[] = "第{$lineNo}行: 列数不足(至少需要5列)";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = trim($row[0]);
|
||||||
|
$batchNo = trim($row[1]);
|
||||||
|
$type = (int)trim($row[2]);
|
||||||
|
$code = trim($row[3]);
|
||||||
|
$value = trim($row[4]);
|
||||||
|
$expiredAt = isset($row[5]) ? trim($row[5]) : '';
|
||||||
|
$price1 = isset($row[6]) && $row[6] !== '' ? (float)trim($row[6]) : null;
|
||||||
|
$price2 = isset($row[7]) && $row[7] !== '' ? (float)trim($row[7]) : null;
|
||||||
|
|
||||||
|
if (!$name || !$batchNo || !$code) {
|
||||||
|
$errors[] = "第{$lineNo}行: 兑换码名称、批次号、兑换码为必填项";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($type !== 1) {
|
||||||
|
$errors[] = "第{$lineNo}行: 兑换码类型必须为1,当前为{$type}";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('INSERT INTO redemption_codes (product_id, created_at, user_id, name, batch_no, type, code, value, expired_at, price_tier1, price_tier2) VALUES (?, NOW(), ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE name = VALUES(name), batch_no = VALUES(batch_no), type = VALUES(type), value = VALUES(value), expired_at = VALUES(expired_at), price_tier1 = VALUES(price_tier1), price_tier2 = VALUES(price_tier2)');
|
||||||
|
$stmt->execute([$pid, getCurrentUserId(), $name, $batchNo, $type, $code, $value, $expiredAt ?: null, $price1, $price2]);
|
||||||
|
$imported++;
|
||||||
|
}
|
||||||
|
fclose($handle);
|
||||||
|
}
|
||||||
|
} elseif ($ext === 'xlsx') {
|
||||||
|
$rows = readXlsx($_FILES['file']['tmp_name']);
|
||||||
|
if ($rows === false) {
|
||||||
|
$errors[] = '无法解析 XLSX 文件,请确认文件未损坏';
|
||||||
|
} else {
|
||||||
|
foreach ($rows as $lineNo => $row) {
|
||||||
|
if ($lineNo === 0) continue; // 跳过表头
|
||||||
|
$ln = $lineNo + 1;
|
||||||
|
if (count($row) < 5) {
|
||||||
|
$errors[] = "第{$ln}行: 列数不足(至少需要5列)";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$name = trim($row[0] ?? '');
|
||||||
|
$batchNo = trim($row[1] ?? '');
|
||||||
|
$type = (int)($row[2] ?? 0);
|
||||||
|
$code = trim($row[3] ?? '');
|
||||||
|
$value = trim($row[4] ?? '');
|
||||||
|
$expiredAt = isset($row[5]) ? trim($row[5]) : '';
|
||||||
|
$price1 = isset($row[6]) && $row[6] !== '' ? (float)$row[6] : null;
|
||||||
|
$price2 = isset($row[7]) && $row[7] !== '' ? (float)$row[7] : null;
|
||||||
|
if (!$name || !$batchNo || !$code) {
|
||||||
|
$errors[] = "第{$ln}行: 兑换码名称、批次号、兑换码为必填项";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($type !== 1) {
|
||||||
|
$errors[] = "第{$ln}行: 兑换码类型必须为1,当前为{$type}";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$stmt = $pdo->prepare('INSERT INTO redemption_codes (product_id, created_at, user_id, name, batch_no, type, code, value, expired_at, price_tier1, price_tier2) VALUES (?, NOW(), ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE name = VALUES(name), batch_no = VALUES(batch_no), type = VALUES(type), value = VALUES(value), expired_at = VALUES(expired_at), price_tier1 = VALUES(price_tier1), price_tier2 = VALUES(price_tier2)');
|
||||||
|
$stmt->execute([$pid, getCurrentUserId(), $name, $batchNo, $type, $code, $value, $expiredAt ?: null, $price1, $price2]);
|
||||||
|
$imported++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$errors[] = 'XLS(旧版Excel格式)不支持,请另存为 XLSX 或 CSV 格式后导入';
|
||||||
|
}
|
||||||
|
|
||||||
|
$flashMsg = '';
|
||||||
|
if ($imported > 0) {
|
||||||
|
$flashMsg = '成功导入 ' . $imported . ' 条记录';
|
||||||
|
if (!empty($errors)) $flashMsg .= ',' . count($errors) . ' 条失败';
|
||||||
|
}
|
||||||
|
if (!empty($errors) && $imported > 0) {
|
||||||
|
$flashMsg .= "\n" . implode("\n", array_slice($errors, 0, 20));
|
||||||
|
$_SESSION['flash_msg'] = $flashMsg;
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
header('Location: code_manage.php');
|
||||||
|
exit;
|
||||||
|
} elseif ($imported > 0) {
|
||||||
|
$_SESSION['flash_msg'] = $flashMsg;
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
header('Location: code_manage.php');
|
||||||
|
exit;
|
||||||
|
} elseif (!empty($errors)) {
|
||||||
|
$msg = '<div class="alert alert-warning"><pre>' . h(implode("\n", array_slice($errors, 0, 20))) . '</pre></div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$msg = '<div class="alert alert-danger">请选择文件</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除单个兑换码
|
||||||
|
if (isset($_GET['del']) && is_numeric($_GET['del'])) {
|
||||||
|
if (!verifyCsrf($_GET['csrf_token'] ?? '')) { $_SESSION['flash_msg'] = 'CSRF token无效'; $_SESSION['flash_type'] = 'danger'; header('Location: code_manage.php'); exit; }
|
||||||
|
$id = (int)$_GET['del'];
|
||||||
|
$pdo->prepare('DELETE FROM redemption_codes WHERE id = ?')->execute([$id]);
|
||||||
|
$_SESSION['flash_msg'] = '兑换码已删除';
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
header('Location: code_manage.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索条件
|
||||||
|
$where = 'WHERE c.product_id = ?';
|
||||||
|
$searchParams = [$pid];
|
||||||
|
$joinUser = false;
|
||||||
|
if (!empty($_GET['claim_user'])) {
|
||||||
|
$where .= ' AND u.username LIKE ?';
|
||||||
|
$searchParams[] = '%' . $_GET['claim_user'] . '%';
|
||||||
|
$joinUser = true;
|
||||||
|
}
|
||||||
|
foreach (['code' => 'c.code', 'name' => 'c.name', 'batch_no' => 'c.batch_no'] as $key => $col) {
|
||||||
|
if (!empty($_GET[$key])) {
|
||||||
|
$where .= " AND $col LIKE ?";
|
||||||
|
$searchParams[] = '%' . $_GET[$key] . '%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
$page = max(1, (int)($_GET['p'] ?? 1));
|
||||||
|
$perPage = 20;
|
||||||
|
$offset = ($page - 1) * $perPage;
|
||||||
|
|
||||||
|
$join = 'LEFT JOIN users u ON c.claim_user_id = u.id';
|
||||||
|
$stmt = $pdo->prepare("SELECT COUNT(*) FROM redemption_codes c $join $where");
|
||||||
|
$stmt->execute($searchParams);
|
||||||
|
$total = (int)$stmt->fetchColumn();
|
||||||
|
$totalPages = max(1, ceil($total / $perPage));
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT c.*, u.username AS claim_username FROM redemption_codes c $join $where ORDER BY c.id DESC LIMIT ? OFFSET ?");
|
||||||
|
$stmt->execute(array_merge($searchParams, [$perPage, $offset]));
|
||||||
|
$codes = $stmt->fetchAll();
|
||||||
|
?>
|
||||||
|
<div class="card">
|
||||||
|
<h2>库存管理</h2>
|
||||||
|
<?= $msg ?>
|
||||||
|
<?php if (isset($_SESSION['flash_msg'])): ?>
|
||||||
|
<div class="alert alert-<?= h($_SESSION['flash_type'] ?? 'success') ?>"><?= h($_SESSION['flash_msg']) ?></div>
|
||||||
|
<?php unset($_SESSION['flash_msg'], $_SESSION['flash_type']); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<form method="get" style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px;align-items:flex-end;">
|
||||||
|
<div class="form-group" style="margin-bottom:0;">
|
||||||
|
<label style="font-size:12px;">用户</label>
|
||||||
|
<input type="text" name="claim_user" class="form-control" value="<?= h($_GET['claim_user'] ?? '') ?>" placeholder="用户名" style="width:110px;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;">
|
||||||
|
<label style="font-size:12px;">兑换码</label>
|
||||||
|
<input type="text" name="code" class="form-control" value="<?= h($_GET['code'] ?? '') ?>" style="width:130px;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;">
|
||||||
|
<label style="font-size:12px;">名称</label>
|
||||||
|
<input type="text" name="name" class="form-control" value="<?= h($_GET['name'] ?? '') ?>" style="width:100px;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;">
|
||||||
|
<label style="font-size:12px;">批次号</label>
|
||||||
|
<input type="text" name="batch_no" class="form-control" value="<?= h($_GET['batch_no'] ?? '') ?>" style="width:120px;">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm">搜索</button>
|
||||||
|
<?php if (!empty($_GET['claim_user']) || !empty($_GET['code']) || !empty($_GET['name']) || !empty($_GET['batch_no'])): ?>
|
||||||
|
<a href="code_manage.php" class="btn btn-sm" style="background:#999;color:#fff;">清空</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
<div style="margin-bottom:16px;">
|
||||||
|
<a href="javascript:void(0)" class="btn btn-success" onclick="showModal('importModal')">+ Excel导入</a>
|
||||||
|
<a href="export_template.php" class="btn btn-info">下载导入模板</a>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>创建时间</th>
|
||||||
|
<th>名称</th>
|
||||||
|
<th>批次号</th>
|
||||||
|
<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 ($codes as $c): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= $c['id'] ?></td>
|
||||||
|
<td><?= formatDateTime($c['created_at']) ?></td>
|
||||||
|
<td><?= h($c['name']) ?></td>
|
||||||
|
<td><?= h($c['batch_no']) ?></td>
|
||||||
|
<td><?= h($c['type']) ?></td>
|
||||||
|
<td><?= h($c['code']) ?></td>
|
||||||
|
<td><?= h($c['value']) ?></td>
|
||||||
|
<td><?= claimStatusBadge((int)$c['status']) ?></td>
|
||||||
|
<td><?= formatDateTime($c['expired_at']) ?></td>
|
||||||
|
<td><?= h($c['claim_username'] ?? ($c['claim_user_id'] ? 'ID:' . $c['claim_user_id'] : '-')) ?></td>
|
||||||
|
<td><?= formatDateTime($c['claimed_at']) ?></td>
|
||||||
|
<td><?= $c['price_tier1'] !== null ? h($c['price_tier1']) : '-' ?></td>
|
||||||
|
<td><?= $c['price_tier2'] !== null ? h($c['price_tier2']) : '-' ?></td>
|
||||||
|
<td><a href="?del=<?= $c['id'] ?>&csrf_token=<?= csrfToken() ?>" class="btn btn-danger btn-sm" onclick="return confirm('确定删除此兑换码?')">删除</a></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($codes)): ?>
|
||||||
|
<tr><td colspan="14" style="text-align:center;color:#999;">暂无数据</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
$extra = [];
|
||||||
|
foreach (['claim_user', 'code', 'name', 'batch_no'] as $k) {
|
||||||
|
if (!empty($_GET[$k])) $extra[$k] = $_GET[$k];
|
||||||
|
}
|
||||||
|
renderPagination($page, $totalPages, $extra);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 导入弹窗 -->
|
||||||
|
<div class="modal" id="importModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="modal-close" onclick="hideModal('importModal')">×</span>
|
||||||
|
<h3>Excel导入兑换码</h3>
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<strong>导入说明:</strong><br>
|
||||||
|
1. 支持 CSV 和 XLSX 格式<br>
|
||||||
|
2. 文件必须包含表头,列顺序为:<br>
|
||||||
|
<code>兑换码名称, 批次号, 类型, 兑换码, 面值, 过期时间, 出货价一档, 出货价二挡</code><br>
|
||||||
|
3. 类型必须为 1<br>
|
||||||
|
4. 过期时间格式:2027-06-01 12:26:53(可选)<br>
|
||||||
|
5. 出货价字段可选<br>
|
||||||
|
6. 推荐先<a href="export_template.php">下载模板</a>
|
||||||
|
</div>
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="action" value="import">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= csrfToken() ?>">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>选择文件</label>
|
||||||
|
<input type="file" name="file" class="form-control-file" accept=".csv,.xls,.xlsx" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-success">开始导入</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showModal(id) { document.getElementById(id).classList.add('active'); }
|
||||||
|
function hideModal(id) { document.getElementById(id).classList.remove('active'); }
|
||||||
|
document.querySelectorAll('.modal').forEach(m => {
|
||||||
|
m.addEventListener('click', function(e) { if (e.target === this) this.classList.remove('active'); });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 使用内置 ZipArchive + SimpleXML 解析 XLSX 文件,无需第三方库
|
||||||
|
*/
|
||||||
|
function readXlsx($file) {
|
||||||
|
$zip = new ZipArchive;
|
||||||
|
if ($zip->open($file) !== true) return false;
|
||||||
|
|
||||||
|
// 读取共享字符串表
|
||||||
|
$sharedStrings = [];
|
||||||
|
$ssXml = $zip->getFromName('xl/sharedStrings.xml');
|
||||||
|
if ($ssXml !== false) {
|
||||||
|
$ss = simplexml_load_string($ssXml);
|
||||||
|
if ($ss) {
|
||||||
|
foreach ($ss->si as $si) {
|
||||||
|
$t = $si->t;
|
||||||
|
if ($t) {
|
||||||
|
$sharedStrings[] = (string)$t;
|
||||||
|
} else {
|
||||||
|
// 富文本,拼接所有 t 子元素
|
||||||
|
$parts = [];
|
||||||
|
foreach ($si->r->t as $rT) {
|
||||||
|
$parts[] = (string)$rT;
|
||||||
|
}
|
||||||
|
$sharedStrings[] = implode('', $parts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取第一个工作表
|
||||||
|
$sheetXml = $zip->getFromName('xl/worksheets/sheet1.xml');
|
||||||
|
$zip->close();
|
||||||
|
if ($sheetXml === false) return false;
|
||||||
|
|
||||||
|
$sheet = simplexml_load_string($sheetXml);
|
||||||
|
if (!$sheet) return false;
|
||||||
|
|
||||||
|
$ns = $sheet->getNamespaces(true);
|
||||||
|
$sheet->registerXPathNamespace('s', $ns[''] ?? '');
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
$maxCol = 0;
|
||||||
|
$cells = $sheet->xpath('//s:sheetData/s:row/s:c') ?: [];
|
||||||
|
|
||||||
|
// 第一遍:收集所有数据
|
||||||
|
$data = [];
|
||||||
|
foreach ($cells as $c) {
|
||||||
|
$ref = (string)$c['r'];
|
||||||
|
if (!preg_match('/^([A-Z]+)(\d+)$/', $ref, $m)) continue;
|
||||||
|
$colStr = $m[1];
|
||||||
|
$rowNum = (int)$m[2];
|
||||||
|
|
||||||
|
// 列字母转数字
|
||||||
|
$colNum = 0;
|
||||||
|
for ($i = 0; $i < strlen($colStr); $i++) {
|
||||||
|
$colNum = $colNum * 26 + (ord($colStr[$i]) - 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = (string)$c['t'];
|
||||||
|
$value = (string)$c->v;
|
||||||
|
|
||||||
|
if ($type === 's') {
|
||||||
|
$idx = (int)$value;
|
||||||
|
$cellValue = $sharedStrings[$idx] ?? '';
|
||||||
|
} else {
|
||||||
|
$cellValue = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($data[$rowNum])) $data[$rowNum] = [];
|
||||||
|
$data[$rowNum][$colNum] = $cellValue;
|
||||||
|
if ($colNum > $maxCol) $maxCol = $colNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按行排序输出
|
||||||
|
ksort($data);
|
||||||
|
foreach ($data as $rowNum => $cols) {
|
||||||
|
$row = [];
|
||||||
|
for ($c = 1; $c <= $maxCol; $c++) {
|
||||||
|
$row[] = $cols[$c] ?? '';
|
||||||
|
}
|
||||||
|
$rows[] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
require __DIR__ . '/includes/footer.php'; ?>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
session_set_cookie_params([
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Lax',
|
||||||
|
'secure' => isset($_SERVER['HTTPS']),
|
||||||
|
]);
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$db_host = '127.0.0.1';
|
||||||
|
$db_port = '3306';
|
||||||
|
$db_name = 'coupon_wenyitu';
|
||||||
|
$db_user = 'coupon_wenyitu';
|
||||||
|
$db_pass = 'xhEXhtG5cNEiWRKT';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = new PDO("mysql:host=$db_host;port=$db_port;dbname=$db_name;charset=utf8mb4", $db_user, $db_pass, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die('数据库连接失败: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
require_once __DIR__ . '/includes/functions.php';
|
||||||
|
requireAdmin();
|
||||||
|
|
||||||
|
header('Content-Type: text/csv; charset=utf-8');
|
||||||
|
header('Content-Disposition: attachment; filename=redemption_code_template.csv');
|
||||||
|
|
||||||
|
// BOM for Excel UTF-8
|
||||||
|
echo "\xEF\xBB\xBF";
|
||||||
|
|
||||||
|
$handle = fopen('php://output', 'w');
|
||||||
|
fputcsv($handle, ['兑换码名称', '批次号', '类型', '兑换码', '面值', '过期时间', '出货价一档', '出货价二挡']);
|
||||||
|
fputcsv($handle, ['示例券', 'BATCH001', '1', 'ABC123XYZ', '100', '2027-12-31 23:59:59', '80', '70']);
|
||||||
|
fputcsv($handle, ['示例券2', 'BATCH001', '1', 'DEF456UVW', '50', '', '', '']);
|
||||||
|
fclose($handle);
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config/db.php';
|
||||||
|
|
||||||
|
// 产品切换
|
||||||
|
if (isset($_GET['set_product'])) {
|
||||||
|
$pid = (int)$_GET['set_product'];
|
||||||
|
if ($pid > 0) {
|
||||||
|
if (isAdmin()) {
|
||||||
|
$stmt = $pdo->prepare('SELECT id FROM products WHERE id = ? AND status = 1');
|
||||||
|
$stmt->execute([$pid]);
|
||||||
|
} else if (isset($_SESSION['user_id'])) {
|
||||||
|
$stmt = $pdo->prepare('SELECT p.id FROM products p INNER JOIN user_products up ON p.id = up.product_id WHERE p.id = ? AND p.status = 1 AND up.user_id = ?');
|
||||||
|
$stmt->execute([$pid, $_SESSION['user_id']]);
|
||||||
|
}
|
||||||
|
if ($stmt && $stmt->fetch()) {
|
||||||
|
$_SESSION['current_product_id'] = $pid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$url = strtok($_SERVER['REQUEST_URI'], '?');
|
||||||
|
$params = $_GET;
|
||||||
|
unset($params['set_product']);
|
||||||
|
if ($params) {
|
||||||
|
$url .= '?' . http_build_query($params);
|
||||||
|
}
|
||||||
|
header("Location: $url");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLoggedIn(): bool {
|
||||||
|
return isset($_SESSION['user_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireLogin(): void {
|
||||||
|
if (!isLoggedIn()) {
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAdmin(): bool {
|
||||||
|
return isset($_SESSION['role']) && $_SESSION['role'] === 'admin';
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireAdmin(): void {
|
||||||
|
requireLogin();
|
||||||
|
if (!isAdmin()) {
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentUserId(): ?int {
|
||||||
|
return $_SESSION['user_id'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentUsername(): ?string {
|
||||||
|
return $_SESSION['username'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentProductId(): ?int {
|
||||||
|
$pid = $_SESSION['current_product_id'] ?? null;
|
||||||
|
if ($pid) {
|
||||||
|
$products = getEnabledProducts();
|
||||||
|
$ids = array_column($products, 'id');
|
||||||
|
if (!in_array($pid, $ids)) {
|
||||||
|
unset($_SESSION['current_product_id']);
|
||||||
|
$pid = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$pid) {
|
||||||
|
$products = getEnabledProducts();
|
||||||
|
if (!empty($products)) {
|
||||||
|
$_SESSION['current_product_id'] = (int)$products[0]['id'];
|
||||||
|
$pid = (int)$products[0]['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEnabledProducts(): array {
|
||||||
|
global $pdo;
|
||||||
|
if (isAdmin()) {
|
||||||
|
$stmt = $pdo->query('SELECT id, name FROM products WHERE status = 1 ORDER BY id ASC');
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare('SELECT p.id, p.name FROM products p INNER JOIN user_products up ON p.id = up.product_id WHERE up.user_id = ? AND p.status = 1 ORDER BY p.id ASC');
|
||||||
|
$stmt->execute([getCurrentUserId()]);
|
||||||
|
}
|
||||||
|
return $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var alerts = document.querySelectorAll('.alert');
|
||||||
|
for (var i = 0; i < alerts.length; i++) {
|
||||||
|
(function(el) {
|
||||||
|
setTimeout(function() {
|
||||||
|
el.style.transition = 'opacity 0.5s';
|
||||||
|
el.style.opacity = '0';
|
||||||
|
setTimeout(function() { el.style.display = 'none'; }, 500);
|
||||||
|
}, 5000);
|
||||||
|
})(alerts[i]);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
<?php
|
||||||
|
function h(?string $str): string {
|
||||||
|
return htmlspecialchars($str ?? '', ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirect(string $url): void {
|
||||||
|
header("Location: $url");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function csrfToken(): string {
|
||||||
|
if (empty($_SESSION['csrf_token'])) {
|
||||||
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
return $_SESSION['csrf_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyCsrf(string $token): bool {
|
||||||
|
return hash_equals($_SESSION['csrf_token'] ?? '', $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusBadge(int $status): string {
|
||||||
|
switch ($status) {
|
||||||
|
case 1: return '<span class="badge badge-warning">未使用</span>';
|
||||||
|
case 2: return '<span class="badge badge-success">已使用</span>';
|
||||||
|
case 3: return '<span class="badge badge-danger">已过期</span>';
|
||||||
|
default: return '<span class="badge">未知</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function claimStatusBadge(int $status): string {
|
||||||
|
switch ($status) {
|
||||||
|
case 1: return '<span class="badge badge-info">未领取</span>';
|
||||||
|
case 2: return '<span class="badge badge-success">已领取</span>';
|
||||||
|
default: return '<span class="badge">未知</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function workOrderStatusBadge(string $status): string {
|
||||||
|
switch ($status) {
|
||||||
|
case '未处理': return '<span class="badge badge-warning">未处理</span>';
|
||||||
|
case '已处理': return '<span class="badge badge-success">已处理</span>';
|
||||||
|
case '已驳回': return '<span class="badge badge-danger">已驳回</span>';
|
||||||
|
default: return '<span class="badge">未知</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(?string $datetime): string {
|
||||||
|
if (!$datetime || $datetime === '0000-00-00 00:00:00') return '-';
|
||||||
|
return date('Y-m-d H:i:s', strtotime($datetime));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPagination(int $current, int $total, array $extra = []): void {
|
||||||
|
if ($total <= 1) return;
|
||||||
|
$buildUrl = function(int $page) use ($extra): string {
|
||||||
|
$params = $extra;
|
||||||
|
$params['p'] = $page;
|
||||||
|
return '?' . http_build_query($params);
|
||||||
|
};
|
||||||
|
?>
|
||||||
|
<div class="pagination">
|
||||||
|
<?php if ($current > 1): ?>
|
||||||
|
<a href="<?= h($buildUrl($current - 1)) ?>">上一页</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="disabled">上一页</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php
|
||||||
|
$range = [];
|
||||||
|
$range[] = 1;
|
||||||
|
$start = max(2, $current - 2);
|
||||||
|
$end = min($total - 1, $current + 2);
|
||||||
|
if ($start > 2) $range[] = '...';
|
||||||
|
for ($i = $start; $i <= $end; $i++) $range[] = $i;
|
||||||
|
if ($end < $total - 1) $range[] = '...';
|
||||||
|
if ($total > 1) $range[] = $total;
|
||||||
|
foreach ($range as $p):
|
||||||
|
if ($p === '...'): ?>
|
||||||
|
<span class="ellipsis">…</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<a href="<?= h($buildUrl($p)) ?>" class="<?= $p === $current ? 'active' : '' ?>"><?= $p ?></a>
|
||||||
|
<?php endif;
|
||||||
|
endforeach; ?>
|
||||||
|
<?php if ($current < $total): ?>
|
||||||
|
<a href="<?= h($buildUrl($current + 1)) ?>">下一页</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="disabled">下一页</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<form method="get" class="page-jump" onsubmit="return (function(f){var p=f.querySelector('[name=p]');if(p.value<1||p.value><?= $total ?>){alert('页数超出范围');return false;}return true;})(this)">
|
||||||
|
<span>跳转</span>
|
||||||
|
<input type="number" name="p" value="<?= $current ?>" min="1" max="<?= $total ?>">
|
||||||
|
<?php foreach ($extra as $k => $v): ?>
|
||||||
|
<input type="hidden" name="<?= h($k) ?>" value="<?= h($v) ?>">
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<button type="submit">GO</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
function maskCode(string $code): string {
|
||||||
|
$len = mb_strlen($code);
|
||||||
|
if ($len <= 6) return mb_substr($code, 0, 1) . str_repeat('*', $len - 1);
|
||||||
|
$show = max(3, intval($len / 4));
|
||||||
|
return mb_substr($code, 0, $show) . str_repeat('*', $len - $show * 2) . mb_substr($code, -$show);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><?= h($pageTitle ?? '兑换码管理系统') ?></title>
|
||||||
|
<link rel="stylesheet" href="assets/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php $enabledProducts = getEnabledProducts(); ?>
|
||||||
|
<nav class="navbar">
|
||||||
|
<div class="nav-container">
|
||||||
|
<a href="index.php" class="nav-brand">兑换码管理系统</a>
|
||||||
|
<?php if (count($enabledProducts) > 0): $pid = getCurrentProductId(); ?>
|
||||||
|
<div class="product-switcher">
|
||||||
|
<select onchange="switchProduct(this)" style="padding:4px 8px;border:1px solid rgba(255,255,255,0.2);border-radius:4px;background:rgba(255,255,255,0.1);color:#fff;font-size:13px;cursor:pointer;">
|
||||||
|
<?php foreach ($enabledProducts as $p): ?>
|
||||||
|
<option value="<?= $p['id'] ?>" <?= $pid == $p['id'] ? 'selected' : '' ?>><?= h($p['name']) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<button class="nav-toggle" id="navToggle">☰</button>
|
||||||
|
<div class="nav-menu" id="navMenu">
|
||||||
|
<a href="query_code.php" class="nav-link">查询兑换码</a>
|
||||||
|
<a href="claim_records.php" class="nav-link">领取记录</a>
|
||||||
|
<a href="work_order_records.php" class="nav-link">工单记录</a>
|
||||||
|
<a href="bill_records.php" class="nav-link">账单管理</a>
|
||||||
|
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
|
||||||
|
<a href="work_order_manage.php" class="nav-link">工单管理</a>
|
||||||
|
<a href="code_manage.php" class="nav-link">库存管理</a>
|
||||||
|
<a href="product_manage.php" class="nav-link">产品管理</a>
|
||||||
|
<a href="admin_settings.php" class="nav-link">后台设置</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<span class="nav-user"><?= h($_SESSION['username'] ?? '') ?></span>
|
||||||
|
<a href="logout.php" class="nav-link nav-logout">退出</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<main class="container">
|
||||||
|
<script>
|
||||||
|
document.getElementById('navToggle')?.addEventListener('click', function() {
|
||||||
|
document.getElementById('navMenu').classList.toggle('active');
|
||||||
|
});
|
||||||
|
(function() {
|
||||||
|
var current = window.location.pathname.split('/').pop();
|
||||||
|
var links = document.querySelectorAll('.nav-menu .nav-link');
|
||||||
|
for (var i = 0; i < links.length; i++) {
|
||||||
|
var href = links[i].getAttribute('href');
|
||||||
|
if (href === current) {
|
||||||
|
links[i].classList.add('nav-active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
function switchProduct(el) {
|
||||||
|
var url = new URL(window.location.href);
|
||||||
|
url.searchParams.set('set_product', el.value);
|
||||||
|
window.location.href = url.toString();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
require_once __DIR__ . '/includes/functions.php';
|
||||||
|
requireLogin();
|
||||||
|
|
||||||
|
// 修改密码
|
||||||
|
$pwdMsg = '';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'change_password') {
|
||||||
|
if (!verifyCsrf($_POST['csrf_token'] ?? '')) { die('CSRF token无效'); }
|
||||||
|
$oldPwd = $_POST['old_password'] ?? '';
|
||||||
|
$newPwd = $_POST['new_password'] ?? '';
|
||||||
|
$confirmPwd = $_POST['confirm_password'] ?? '';
|
||||||
|
if ($oldPwd === '' || $newPwd === '' || $confirmPwd === '') {
|
||||||
|
$pwdMsg = '请填写所有字段';
|
||||||
|
} elseif (strlen($newPwd) < 6) {
|
||||||
|
$pwdMsg = '新密码长度至少6位';
|
||||||
|
} elseif ($newPwd !== $confirmPwd) {
|
||||||
|
$pwdMsg = '两次密码输入不一致';
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare('SELECT password FROM users WHERE id = ?');
|
||||||
|
$stmt->execute([getCurrentUserId()]);
|
||||||
|
$stored = $stmt->fetchColumn();
|
||||||
|
if (!password_verify($oldPwd, $stored)) {
|
||||||
|
$pwdMsg = '当前密码错误';
|
||||||
|
} else {
|
||||||
|
$hash = password_hash($newPwd, PASSWORD_DEFAULT);
|
||||||
|
$stmt = $pdo->prepare('UPDATE users SET password = ? WHERE id = ?');
|
||||||
|
$stmt->execute([$hash, getCurrentUserId()]);
|
||||||
|
$_SESSION['flash_msg'] = '密码修改成功';
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageTitle = '首页';
|
||||||
|
require __DIR__ . '/includes/header.php';
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
$stats = [];
|
||||||
|
$pid = getCurrentProductId();
|
||||||
|
|
||||||
|
// 总兑换码数量
|
||||||
|
$stmt = $pdo->prepare('SELECT COUNT(*) as total FROM redemption_codes WHERE product_id = ?');
|
||||||
|
$stmt->execute([$pid]);
|
||||||
|
$stats['total_codes'] = $stmt->fetch()['total'];
|
||||||
|
|
||||||
|
// 已领取数量
|
||||||
|
$stmt = $pdo->prepare('SELECT COUNT(*) as total FROM redemption_codes WHERE status = 2 AND product_id = ?');
|
||||||
|
$stmt->execute([$pid]);
|
||||||
|
$stats['claimed_codes'] = $stmt->fetch()['total'];
|
||||||
|
|
||||||
|
if (isAdmin()) {
|
||||||
|
$stmt = $pdo->prepare('SELECT COUNT(*) as total FROM claim_records WHERE product_id = ?');
|
||||||
|
$stmt->execute([$pid]);
|
||||||
|
$stats['total_records'] = $stmt->fetch()['total'];
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT COUNT(*) as total FROM work_orders WHERE product_id = ?');
|
||||||
|
$stmt->execute([$pid]);
|
||||||
|
$stats['total_orders'] = $stmt->fetch()['total'];
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT COUNT(*) as total FROM work_orders WHERE status = '未处理' AND product_id = ?");
|
||||||
|
$stmt->execute([$pid]);
|
||||||
|
$stats['pending_orders'] = $stmt->fetch()['total'];
|
||||||
|
|
||||||
|
$stmt = $pdo->query('SELECT COUNT(*) as total FROM users');
|
||||||
|
$stats['total_users'] = $stmt->fetch()['total'];
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare('SELECT COUNT(*) as total FROM claim_records WHERE user_id = ? AND product_id = ?');
|
||||||
|
$stmt->execute([getCurrentUserId(), $pid]);
|
||||||
|
$stats['my_records'] = $stmt->fetch()['total'];
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT COUNT(*) as total FROM work_orders WHERE creator_id = ? AND product_id = ?');
|
||||||
|
$stmt->execute([getCurrentUserId(), $pid]);
|
||||||
|
$stats['my_orders'] = $stmt->fetch()['total'];
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value"><?= $stats['total_codes'] ?></div>
|
||||||
|
<div class="stat-label">总兑换码</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value"><?= $stats['claimed_codes'] ?></div>
|
||||||
|
<div class="stat-label">已领取</div>
|
||||||
|
</div>
|
||||||
|
<?php if (isAdmin()): ?>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value"><?= $stats['total_records'] ?></div>
|
||||||
|
<div class="stat-label">领取记录</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value"><?= $stats['pending_orders'] ?>/<?= $stats['total_orders'] ?></div>
|
||||||
|
<div class="stat-label">待处理工单/总数</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value"><?= $stats['total_users'] ?></div>
|
||||||
|
<div class="stat-label">用户数</div>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value"><?= $stats['my_records'] ?></div>
|
||||||
|
<div class="stat-label">我的领取记录</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value"><?= $stats['my_orders'] ?></div>
|
||||||
|
<div class="stat-label">我的工单</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>快速入口</h2>
|
||||||
|
<div class="action-group">
|
||||||
|
<a href="claim_code.php" class="btn btn-success">领取兑换码</a>
|
||||||
|
<a href="work_order_create.php" class="btn btn-primary">发起工单</a>
|
||||||
|
<a href="query_code.php" class="btn btn-info">查询兑换码</a>
|
||||||
|
<a href="claim_records.php" class="btn btn-warning">领取记录</a>
|
||||||
|
<a href="bill_records.php" class="btn btn-info">账单管理</a>
|
||||||
|
<a href="work_order_records.php" class="btn btn-primary">工单记录</a>
|
||||||
|
<?php if (isAdmin()): ?>
|
||||||
|
<a href="code_manage.php" class="btn btn-success">库存管理</a>
|
||||||
|
<a href="work_order_manage.php" class="btn btn-info">工单管理</a>
|
||||||
|
<a href="admin_settings.php" class="btn btn-primary">后台设置</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:16px;">
|
||||||
|
<a href="javascript:void(0)" class="btn btn-primary btn-sm" onclick="showModal('pwdModal')">修改密码</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 修改密码弹窗 -->
|
||||||
|
<div class="modal<?= $pwdMsg ? ' active' : '' ?>" id="pwdModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="modal-close" onclick="hideModal('pwdModal')">×</span>
|
||||||
|
<h3>修改密码</h3>
|
||||||
|
<?php if ($pwdMsg): ?>
|
||||||
|
<div class="alert alert-danger"><?= h($pwdMsg) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="action" value="change_password">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= csrfToken() ?>">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>当前密码</label>
|
||||||
|
<input type="password" name="old_password" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>新密码</label>
|
||||||
|
<input type="password" name="new_password" class="form-control" required minlength="6">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>确认新密码</label>
|
||||||
|
<input type="password" name="confirm_password" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">确认修改</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function showModal(id) { document.getElementById(id).classList.add('active'); }
|
||||||
|
function hideModal(id) { document.getElementById(id).classList.remove('active'); }
|
||||||
|
document.getElementById('pwdModal')?.addEventListener('click', function(e) { if (e.target === this) this.classList.remove('active'); });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php require __DIR__ . '/includes/footer.php'; ?>
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
-- 数据库初始化脚本
|
||||||
|
-- 创建数据库
|
||||||
|
CREATE DATABASE IF NOT EXISTS redemption_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
USE redemption_system;
|
||||||
|
|
||||||
|
-- 用户表
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
role ENUM('admin', 'user') NOT NULL DEFAULT 'user',
|
||||||
|
disabled TINYINT(1) NOT NULL DEFAULT 0 COMMENT '0=启用 1=禁用',
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- 插入默认管理员 (密码: admin123)
|
||||||
|
INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin');
|
||||||
|
|
||||||
|
-- 邀请码表
|
||||||
|
CREATE TABLE IF NOT EXISTS invite_codes (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
code VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
used_by INT DEFAULT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (used_by) REFERENCES users(id) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- 插入默认邀请码
|
||||||
|
INSERT INTO invite_codes (code) VALUES ('DEFAULT2024');
|
||||||
|
|
||||||
|
-- 产品表
|
||||||
|
CREATE TABLE IF NOT EXISTS products (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
code VARCHAR(50) NOT NULL COMMENT '产品ID',
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
status TINYINT(1) NOT NULL DEFAULT 1 COMMENT '1=启用 0=禁用',
|
||||||
|
api_url VARCHAR(500) DEFAULT NULL COMMENT '接口地址',
|
||||||
|
token VARCHAR(500) DEFAULT NULL COMMENT '接口认证Token',
|
||||||
|
remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
||||||
|
updated_at DATETIME DEFAULT NULL,
|
||||||
|
updated_by INT DEFAULT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
INSERT INTO products (code, name) VALUES ('default', '默认产品');
|
||||||
|
|
||||||
|
-- 用户-产品关联表
|
||||||
|
CREATE TABLE IF NOT EXISTS user_products (
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
product_id INT NOT NULL,
|
||||||
|
PRIMARY KEY (user_id, product_id),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- 管理员关联到默认产品
|
||||||
|
INSERT INTO user_products (user_id, product_id)
|
||||||
|
SELECT u.id, p.id FROM users u, products p
|
||||||
|
WHERE u.role = 'admin' AND p.code = 'default'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM user_products up WHERE up.user_id = u.id AND up.product_id = p.id);
|
||||||
|
|
||||||
|
-- 兑换码表
|
||||||
|
CREATE TABLE IF NOT EXISTS redemption_codes (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
product_id INT DEFAULT NULL COMMENT '所属产品',
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
user_id INT DEFAULT NULL COMMENT '创建人(管理员ID)',
|
||||||
|
name VARCHAR(100) NOT NULL COMMENT '兑换码名称',
|
||||||
|
batch_no VARCHAR(100) NOT NULL COMMENT '批次号',
|
||||||
|
type INT NOT NULL DEFAULT 1 COMMENT '兑换码类型',
|
||||||
|
code VARCHAR(200) NOT NULL COMMENT '兑换码',
|
||||||
|
value DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '兑换码面值',
|
||||||
|
status TINYINT NOT NULL DEFAULT 1 COMMENT '1未领取 2已领取',
|
||||||
|
expired_at DATETIME DEFAULT NULL COMMENT '过期时间',
|
||||||
|
claim_user_id INT DEFAULT NULL COMMENT '领取人ID',
|
||||||
|
claimed_at DATETIME DEFAULT NULL COMMENT '领取时间',
|
||||||
|
price_tier1 DECIMAL(10,2) DEFAULT NULL COMMENT '出货价一档',
|
||||||
|
price_tier2 DECIMAL(10,2) DEFAULT NULL COMMENT '出货价二挡',
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (claim_user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- 领取记录表
|
||||||
|
CREATE TABLE IF NOT EXISTS claim_records (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
product_id INT DEFAULT NULL COMMENT '所属产品',
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
user_id INT DEFAULT NULL COMMENT '用户ID',
|
||||||
|
code_name VARCHAR(100) NOT NULL COMMENT '兑换码名称',
|
||||||
|
batch_no VARCHAR(100) NOT NULL COMMENT '批次号',
|
||||||
|
code_type INT NOT NULL DEFAULT 1 COMMENT '兑换码类型',
|
||||||
|
code VARCHAR(200) NOT NULL COMMENT '兑换码',
|
||||||
|
value DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '兑换码面值',
|
||||||
|
status TINYINT NOT NULL DEFAULT 1 COMMENT '1未使用 2已使用 3过期',
|
||||||
|
expired_at DATETIME DEFAULT NULL COMMENT '过期时间',
|
||||||
|
used_at DATETIME DEFAULT NULL COMMENT '使用时间',
|
||||||
|
price_tier1 DECIMAL(10,2) DEFAULT NULL COMMENT '出货价一档',
|
||||||
|
price_tier2 DECIMAL(10,2) DEFAULT NULL COMMENT '出货价二挡',
|
||||||
|
claim_user VARCHAR(50) DEFAULT NULL COMMENT '领取人(用户名)',
|
||||||
|
claimed_at DATETIME DEFAULT NULL COMMENT '领取时间',
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- 工单表
|
||||||
|
CREATE TABLE IF NOT EXISTS work_orders (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
product_id INT DEFAULT NULL COMMENT '所属产品',
|
||||||
|
content TEXT NOT NULL COMMENT '工单内容',
|
||||||
|
code VARCHAR(200) DEFAULT NULL COMMENT '关联兑换码',
|
||||||
|
attachment VARCHAR(500) DEFAULT NULL COMMENT '附件路径',
|
||||||
|
creator_id INT DEFAULT NULL COMMENT '发起人ID',
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '发起时间',
|
||||||
|
status ENUM('未处理', '已处理', '已驳回') NOT NULL DEFAULT '未处理' COMMENT '状态',
|
||||||
|
processed_at DATETIME DEFAULT NULL COMMENT '处理时间',
|
||||||
|
processor_id INT DEFAULT NULL COMMENT '处理人ID',
|
||||||
|
FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (processor_id) REFERENCES users(id) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX idx_code ON redemption_codes(code);
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/config/db.php';
|
||||||
|
require_once __DIR__ . '/includes/functions.php';
|
||||||
|
|
||||||
|
if (isset($_SESSION['user_id'])) {
|
||||||
|
redirect('index.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = '';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$username = trim($_POST['username'] ?? '');
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
$captcha = strtoupper(trim($_POST['captcha'] ?? ''));
|
||||||
|
|
||||||
|
if ($captcha !== ($_SESSION['captcha'] ?? '')) {
|
||||||
|
$error = '验证码错误';
|
||||||
|
} elseif ($username === '' || $password === '') {
|
||||||
|
$error = '请输入用户名和密码';
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = ?');
|
||||||
|
$stmt->execute([$username]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($user && password_verify($password, $user['password'])) {
|
||||||
|
if (!empty($user['disabled'])) {
|
||||||
|
$error = '账户已被禁用';
|
||||||
|
} else {
|
||||||
|
session_regenerate_id(true);
|
||||||
|
$_SESSION['user_id'] = (int)$user['id'];
|
||||||
|
$_SESSION['username'] = $user['username'];
|
||||||
|
$_SESSION['role'] = $user['role'];
|
||||||
|
redirect('index.php');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$error = '用户名或密码错误';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$_SESSION['captcha'] = '';
|
||||||
|
}
|
||||||
|
?><!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>登录 - 兑换码管理系统</title>
|
||||||
|
<link rel="stylesheet" href="assets/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body class="auth-page">
|
||||||
|
<div class="auth-card">
|
||||||
|
<h1>兑换码管理系统</h1>
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="alert alert-danger"><?= h($error) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<form method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>用户名</label>
|
||||||
|
<input type="text" name="username" class="form-control" required autocomplete="username">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>密码</label>
|
||||||
|
<input type="password" name="password" class="form-control" required autocomplete="current-password">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>验证码</label>
|
||||||
|
<div class="captcha-row">
|
||||||
|
<input type="text" name="captcha" class="form-control" required maxlength="4" autocomplete="off">
|
||||||
|
<img src="captcha.php?<?= time() ?>" alt="验证码" onclick="this.src='captcha.php?'+Date.now()" title="点击刷新">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">登 录</button>
|
||||||
|
</form>
|
||||||
|
<div class="auth-footer">没有账号?<a href="register.php">立即注册</a></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
$_SESSION = [];
|
||||||
|
setcookie(session_name(), '', time() - 42000, '/');
|
||||||
|
session_destroy();
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
|
@ -0,0 +1,230 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
require_once __DIR__ . '/includes/functions.php';
|
||||||
|
requireAdmin();
|
||||||
|
|
||||||
|
$pageTitle = '产品管理';
|
||||||
|
require __DIR__ . '/includes/header.php';
|
||||||
|
|
||||||
|
$msg = '';
|
||||||
|
|
||||||
|
// 新增
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add'])) {
|
||||||
|
if (!verifyCsrf($_POST['csrf_token'] ?? '')) { die('CSRF token无效'); }
|
||||||
|
$code = trim($_POST['code'] ?? '');
|
||||||
|
$name = trim($_POST['name'] ?? '');
|
||||||
|
$apiUrl = trim($_POST['api_url'] ?? '');
|
||||||
|
$token = trim($_POST['token'] ?? '');
|
||||||
|
$remark = trim($_POST['remark'] ?? '');
|
||||||
|
if ($code === '') {
|
||||||
|
$msg = '<div class="alert alert-danger">产品ID不能为空</div>';
|
||||||
|
} elseif ($name === '') {
|
||||||
|
$msg = '<div class="alert alert-danger">产品名称不能为空</div>';
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare('SELECT id FROM products WHERE code = ?');
|
||||||
|
$stmt->execute([$code]);
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
$msg = '<div class="alert alert-danger">产品ID已存在</div>';
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare('INSERT INTO products (code, name, api_url, token, remark) VALUES (?, ?, ?, ?, ?)');
|
||||||
|
$stmt->execute([$code, $name, $apiUrl ?: null, $token ?: null, $remark ?: null]);
|
||||||
|
$_SESSION['flash_msg'] = '产品已添加';
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
header('Location: product_manage.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['edit'])) {
|
||||||
|
if (!verifyCsrf($_POST['csrf_token'] ?? '')) { die('CSRF token无效'); }
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$code = trim($_POST['code'] ?? '');
|
||||||
|
$name = trim($_POST['name'] ?? '');
|
||||||
|
$apiUrl = trim($_POST['api_url'] ?? '');
|
||||||
|
$token = trim($_POST['token'] ?? '');
|
||||||
|
$remark = trim($_POST['remark'] ?? '');
|
||||||
|
if ($code === '') {
|
||||||
|
$msg = '<div class="alert alert-danger">产品ID不能为空</div>';
|
||||||
|
} elseif ($name === '') {
|
||||||
|
$msg = '<div class="alert alert-danger">产品名称不能为空</div>';
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare('SELECT id FROM products WHERE code = ? AND id != ?');
|
||||||
|
$stmt->execute([$code, $id]);
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
$msg = '<div class="alert alert-danger">产品ID已被其他产品使用</div>';
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare('UPDATE products SET code = ?, name = ?, api_url = ?, token = ?, remark = ?, updated_at = NOW(), updated_by = ? WHERE id = ?');
|
||||||
|
$stmt->execute([$code, $name, $apiUrl ?: null, $token ?: null, $remark ?: null, getCurrentUserId(), $id]);
|
||||||
|
$_SESSION['flash_msg'] = '产品已更新';
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
header('Location: product_manage.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换状态
|
||||||
|
if (isset($_GET['toggle']) && is_numeric($_GET['toggle'])) {
|
||||||
|
if (!verifyCsrf($_GET['csrf_token'] ?? '')) {
|
||||||
|
$_SESSION['flash_msg'] = 'CSRF token无效';
|
||||||
|
$_SESSION['flash_type'] = 'danger';
|
||||||
|
} else {
|
||||||
|
$id = (int)$_GET['toggle'];
|
||||||
|
$stmt = $pdo->prepare('SELECT status FROM products WHERE id = ?');
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$current = (int)$stmt->fetchColumn();
|
||||||
|
$newStatus = $current ? 0 : 1;
|
||||||
|
$stmt = $pdo->prepare('UPDATE products SET status = ?, updated_at = NOW(), updated_by = ? WHERE id = ?');
|
||||||
|
$stmt->execute([$newStatus, getCurrentUserId(), $id]);
|
||||||
|
$_SESSION['flash_msg'] = $newStatus ? '产品已启用' : '产品已禁用';
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
}
|
||||||
|
header('Location: product_manage.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->query('SELECT p.*, u.username FROM products p LEFT JOIN users u ON p.updated_by = u.id ORDER BY p.id ASC');
|
||||||
|
$products = $stmt->fetchAll();
|
||||||
|
?>
|
||||||
|
<div class="card">
|
||||||
|
<h2>产品管理</h2>
|
||||||
|
<?php if (isset($_SESSION['flash_msg'])): ?>
|
||||||
|
<div class="alert alert-<?= h($_SESSION['flash_type'] ?? 'success') ?>"><?= h($_SESSION['flash_msg']) ?></div>
|
||||||
|
<?php unset($_SESSION['flash_msg'], $_SESSION['flash_type']); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?= $msg ?>
|
||||||
|
<div style="margin-bottom:16px;">
|
||||||
|
<a href="javascript:void(0)" class="btn btn-success" onclick="showModal('addModal')">+ 新增产品</a>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>产品ID</th>
|
||||||
|
<th>产品名称</th>
|
||||||
|
<th>接口地址</th>
|
||||||
|
<th>Token</th>
|
||||||
|
<th>备注</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>修改时间</th>
|
||||||
|
<th>修改用户</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($products as $p): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= $p['id'] ?></td>
|
||||||
|
<td><code><?= h($p['code']) ?></code></td>
|
||||||
|
<td><?= h($p['name']) ?></td>
|
||||||
|
<td><?= h($p['api_url'] ?? '-') ?></td>
|
||||||
|
<td><?= $p['token'] ? h(maskCode($p['token'])) : '-' ?></td>
|
||||||
|
<td><?= h($p['remark'] ?? '-') ?></td>
|
||||||
|
<td><?= $p['status'] ? '<span class="badge badge-success">启用</span>' : '<span class="badge badge-danger">禁用</span>' ?></td>
|
||||||
|
<td><?= formatDateTime($p['updated_at']) ?></td>
|
||||||
|
<td><?= h($p['username'] ?? '-') ?></td>
|
||||||
|
<td>
|
||||||
|
<div class="action-group">
|
||||||
|
<a href="javascript:void(0)" class="btn btn-primary btn-sm" onclick="editProduct(<?= $p['id'] ?>, '<?= h($p['code']) ?>', '<?= h($p['name']) ?>', '<?= h($p['api_url'] ?? '') ?>', '<?= h($p['token'] ?? '') ?>', '<?= h($p['remark'] ?? '') ?>')">编辑</a>
|
||||||
|
<?php if ($p['status']): ?>
|
||||||
|
<a href="?toggle=<?= $p['id'] ?>&csrf_token=<?= csrfToken() ?>" class="btn btn-warning btn-sm" onclick="return confirm('确定禁用此产品?')">禁用</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<a href="?toggle=<?= $p['id'] ?>&csrf_token=<?= csrfToken() ?>" class="btn btn-success btn-sm">启用</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($products)): ?>
|
||||||
|
<tr><td colspan="10" style="text-align:center;color:#999;">暂无产品</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增弹窗 -->
|
||||||
|
<div class="modal" id="addModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="modal-close" onclick="hideModal('addModal')">×</span>
|
||||||
|
<h3>新增产品</h3>
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= csrfToken() ?>">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>产品ID <span style="color:red">*</span></label>
|
||||||
|
<input type="text" name="code" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>产品名称 <span style="color:red">*</span></label>
|
||||||
|
<input type="text" name="name" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>接口地址</label>
|
||||||
|
<input type="text" name="api_url" class="form-control" placeholder="https://example.com/api">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Token</label>
|
||||||
|
<input type="text" name="token" class="form-control" placeholder="接口认证Token">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>备注</label>
|
||||||
|
<input type="text" name="remark" class="form-control" placeholder="选填">
|
||||||
|
</div>
|
||||||
|
<button type="submit" name="add" class="btn btn-success">添加</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 编辑弹窗 -->
|
||||||
|
<div class="modal" id="editModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="modal-close" onclick="hideModal('editModal')">×</span>
|
||||||
|
<h3>编辑产品</h3>
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= csrfToken() ?>">
|
||||||
|
<input type="hidden" name="id" id="edit_id">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>产品ID <span style="color:red">*</span></label>
|
||||||
|
<input type="text" name="code" class="form-control" id="edit_code" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>产品名称 <span style="color:red">*</span></label>
|
||||||
|
<input type="text" name="name" class="form-control" id="edit_name" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>接口地址</label>
|
||||||
|
<input type="text" name="api_url" class="form-control" id="edit_api_url" placeholder="https://example.com/api">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Token</label>
|
||||||
|
<input type="text" name="token" class="form-control" id="edit_token" placeholder="接口认证Token">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>备注</label>
|
||||||
|
<input type="text" name="remark" class="form-control" id="edit_remark" placeholder="选填">
|
||||||
|
</div>
|
||||||
|
<button type="submit" name="edit" class="btn btn-primary">保存</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showModal(id) { document.getElementById(id).classList.add('active'); }
|
||||||
|
function hideModal(id) { document.getElementById(id).classList.remove('active'); }
|
||||||
|
function editProduct(id, code, name, apiUrl, token, remark) {
|
||||||
|
document.getElementById('edit_id').value = id;
|
||||||
|
document.getElementById('edit_code').value = code;
|
||||||
|
document.getElementById('edit_name').value = name;
|
||||||
|
document.getElementById('edit_api_url').value = apiUrl;
|
||||||
|
document.getElementById('edit_token').value = token;
|
||||||
|
document.getElementById('edit_remark').value = remark;
|
||||||
|
showModal('editModal');
|
||||||
|
}
|
||||||
|
document.querySelectorAll('.modal').forEach(function(m) {
|
||||||
|
m.addEventListener('click', function(e) { if (e.target === this) this.classList.remove('active'); });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php require __DIR__ . '/includes/footer.php'; ?>
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
require_once __DIR__ . '/includes/functions.php';
|
||||||
|
requireLogin();
|
||||||
|
|
||||||
|
$pageTitle = '查询兑换码';
|
||||||
|
require __DIR__ . '/includes/header.php';
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
$searched = false;
|
||||||
|
$q = trim($_GET['q'] ?? '');
|
||||||
|
$page = max(1, (int)($_GET['p'] ?? 1));
|
||||||
|
$perPage = 100;
|
||||||
|
$pid = getCurrentProductId();
|
||||||
|
|
||||||
|
if ($q !== '') {
|
||||||
|
$searched = true;
|
||||||
|
$offset = ($page - 1) * $perPage;
|
||||||
|
|
||||||
|
// 统计总数
|
||||||
|
$stmt = $pdo->prepare('SELECT COUNT(*) FROM redemption_codes WHERE product_id = ? AND (code = ? OR batch_no = ?)');
|
||||||
|
$stmt->execute([$pid, $q, $q]);
|
||||||
|
$total = (int)$stmt->fetchColumn();
|
||||||
|
$totalPages = max(1, ceil($total / $perPage));
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
$stmt = $pdo->prepare("SELECT r.*, u.username FROM redemption_codes r LEFT JOIN users u ON r.claim_user_id = u.id WHERE r.product_id = ? AND (r.code = ? OR r.batch_no = ?) ORDER BY r.id DESC LIMIT ? OFFSET ?");
|
||||||
|
$stmt->execute([$pid, $q, $q, $perPage, $offset]);
|
||||||
|
$results = $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="card">
|
||||||
|
<h2>查询兑换码</h2>
|
||||||
|
<form method="get" style="display:flex;gap:8px;flex-wrap:wrap;">
|
||||||
|
<input type="text" name="q" class="form-control" placeholder="输入兑换码或批次号" value="<?= h($q) ?>" required style="flex:1;min-width:200px;">
|
||||||
|
<button type="submit" class="btn btn-primary">查询</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($searched): ?>
|
||||||
|
<div class="card">
|
||||||
|
<h2>查询结果 <?php if ($total > 0): ?><span style="font-size:14px;color:#999;font-weight:normal;">(共 <?= $total ?> 条)</span><?php endif; ?></h2>
|
||||||
|
<?php if (empty($results)): ?>
|
||||||
|
<div class="alert alert-info">未找到匹配的兑换码</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>序号</th>
|
||||||
|
<th>兑换码名称</th>
|
||||||
|
<th>批次号</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>兑换码</th>
|
||||||
|
<th>面值</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>过期时间</th>
|
||||||
|
<?php if (isAdmin()): ?>
|
||||||
|
<th>创建时间</th>
|
||||||
|
<th>用户</th>
|
||||||
|
<th>领取时间</th>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php $idx = $offset; foreach ($results as $r): $idx++; ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= $idx ?></td>
|
||||||
|
<td><?= h($r['name']) ?></td>
|
||||||
|
<td><?= h($r['batch_no']) ?></td>
|
||||||
|
<td><?= h($r['type']) ?></td>
|
||||||
|
<td><code><?= h(maskCode($r['code'])) ?></code></td>
|
||||||
|
<td><?= h($r['value']) ?></td>
|
||||||
|
<td><?= claimStatusBadge((int)$r['status']) ?></td>
|
||||||
|
<td><?= formatDateTime($r['expired_at']) ?></td>
|
||||||
|
<?php if (isAdmin()): ?>
|
||||||
|
<td><?= formatDateTime($r['created_at']) ?></td>
|
||||||
|
<td><?= h($r['username'] ?? '-') ?></td>
|
||||||
|
<td><?= formatDateTime($r['claimed_at']) ?></td>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php renderPagination($page, $totalPages, ['q' => $q]); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php require __DIR__ . '/includes/footer.php'; ?>
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/config/db.php';
|
||||||
|
require_once __DIR__ . '/includes/functions.php';
|
||||||
|
|
||||||
|
if (isset($_SESSION['user_id'])) {
|
||||||
|
redirect('index.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = '';
|
||||||
|
$success = '';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$username = trim($_POST['username'] ?? '');
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
$confirm = $_POST['confirm'] ?? '';
|
||||||
|
$invite = trim($_POST['invite'] ?? '');
|
||||||
|
|
||||||
|
if ($username === '' || $password === '' || $confirm === '' || $invite === '') {
|
||||||
|
$error = '请填写所有字段';
|
||||||
|
} elseif (strlen($username) < 3 || strlen($username) > 20) {
|
||||||
|
$error = '用户名长度3-20个字符';
|
||||||
|
} elseif (strlen($password) < 6) {
|
||||||
|
$error = '密码长度至少6位';
|
||||||
|
} elseif ($password !== $confirm) {
|
||||||
|
$error = '两次密码输入不一致';
|
||||||
|
} else {
|
||||||
|
// 检查用户名是否已存在
|
||||||
|
$stmt = $pdo->prepare('SELECT id FROM users WHERE username = ?');
|
||||||
|
$stmt->execute([$username]);
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
$error = '用户名已存在';
|
||||||
|
} else {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
try {
|
||||||
|
// 验证邀请码(次数校验:未超限 或 不限次数)
|
||||||
|
$stmt = $pdo->prepare("SELECT id, max_uses, used_count FROM invite_codes WHERE code = ? AND (used_count < max_uses OR max_uses = 0) FOR UPDATE");
|
||||||
|
$stmt->execute([$invite]);
|
||||||
|
$ic = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$ic) {
|
||||||
|
$error = '邀请码无效或已超出使用次数';
|
||||||
|
$pdo->rollBack();
|
||||||
|
} else {
|
||||||
|
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
$stmt = $pdo->prepare('INSERT INTO users (username, password) VALUES (?, ?)');
|
||||||
|
$stmt->execute([$username, $hash]);
|
||||||
|
|
||||||
|
// 增加邀请码使用次数
|
||||||
|
$stmt = $pdo->prepare('UPDATE invite_codes SET used_count = used_count + 1 WHERE id = ?');
|
||||||
|
$stmt->execute([$ic['id']]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
redirect('login.php');
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
$error = '注册失败,请重试';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?><!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>注册 - 兑换码管理系统</title>
|
||||||
|
<link rel="stylesheet" href="assets/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body class="auth-page">
|
||||||
|
<div class="auth-card">
|
||||||
|
<h1>用户注册</h1>
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="alert alert-danger"><?= h($error) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($success): ?>
|
||||||
|
<div class="alert alert-success"><?= h($success) ?></div>
|
||||||
|
<div class="auth-footer"><a href="login.php">前往登录</a></div>
|
||||||
|
<?php else: ?>
|
||||||
|
<form method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>用户名</label>
|
||||||
|
<input type="text" name="username" class="form-control" required minlength="3" maxlength="20">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>密码</label>
|
||||||
|
<input type="password" name="password" class="form-control" required minlength="6">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>确认密码</label>
|
||||||
|
<input type="password" name="confirm" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>邀请码</label>
|
||||||
|
<input type="text" name="invite" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">注 册</button>
|
||||||
|
</form>
|
||||||
|
<div class="auth-footer">已有账号?<a href="login.php">立即登录</a></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
|
|
@ -0,0 +1,147 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
require_once __DIR__ . '/includes/functions.php';
|
||||||
|
requireLogin();
|
||||||
|
|
||||||
|
$msg = '';
|
||||||
|
$oldCodes = $_POST['codes'] ?? [];
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
if (!verifyCsrf($_POST['csrf_token'] ?? '')) { die('CSRF token无效'); }
|
||||||
|
$content = trim($_POST['content'] ?? '');
|
||||||
|
$codes = array_filter($_POST['codes'] ?? [], fn($v) => trim($v) !== '');
|
||||||
|
$codes = array_map('trim', $codes);
|
||||||
|
$codesStr = implode("\n", $codes);
|
||||||
|
$attachment = '';
|
||||||
|
|
||||||
|
if ($content === '') {
|
||||||
|
$msg = '<div class="alert alert-danger">工单内容不能为空</div>';
|
||||||
|
} elseif (empty($codes)) {
|
||||||
|
$msg = '<div class="alert alert-danger">请至少关联一个兑换码</div>';
|
||||||
|
} elseif (count($codes) > 10) {
|
||||||
|
$msg = '<div class="alert alert-danger">最多关联10个兑换码</div>';
|
||||||
|
} elseif (count(array_unique($codes)) !== count($codes)) {
|
||||||
|
$msg = '<div class="alert alert-danger">关联的兑换码不能重复</div>';
|
||||||
|
} else {
|
||||||
|
$userId = getCurrentUserId();
|
||||||
|
$pid = getCurrentProductId();
|
||||||
|
$placeholders = implode(',', array_fill(0, count($codes), '?'));
|
||||||
|
$stmt = $pdo->prepare("SELECT code, status FROM claim_records WHERE user_id = ? AND product_id = ? AND code IN ($placeholders)");
|
||||||
|
$stmt->execute(array_merge([$userId, $pid], $codes));
|
||||||
|
$foundCodes = $stmt->fetchAll();
|
||||||
|
$expiredCodes = array_map(fn($r) => $r['code'], array_filter($foundCodes, fn($r) => (int)$r['status'] === 3));
|
||||||
|
$invalidCodes = array_diff($codes, array_column($foundCodes, 'code'));
|
||||||
|
if (!empty($expiredCodes)) {
|
||||||
|
$msg = '<div class="alert alert-danger">以下兑换码已过期,无法提交工单:' . h(implode(', ', $expiredCodes)) . '</div>';
|
||||||
|
} elseif (!empty($invalidCodes)) {
|
||||||
|
$msg = '<div class="alert alert-danger">以下兑换码不在您的领取记录中:' . h(implode(', ', $invalidCodes)) . '</div>';
|
||||||
|
} else {
|
||||||
|
// 检查是否已在未处理工单中
|
||||||
|
$stmt = $pdo->prepare("SELECT code FROM work_orders WHERE creator_id = ? AND product_id = ? AND status = '未处理'");
|
||||||
|
$stmt->execute([$userId, $pid]);
|
||||||
|
$pendingOrderCodes = [];
|
||||||
|
while ($row = $stmt->fetch()) {
|
||||||
|
$pendingOrderCodes = array_merge($pendingOrderCodes, array_filter(explode("\n", $row['code'] ?? '')));
|
||||||
|
}
|
||||||
|
$pendingOrderCodes = array_map('trim', $pendingOrderCodes);
|
||||||
|
$inPending = array_intersect($codes, $pendingOrderCodes);
|
||||||
|
if (!empty($inPending)) {
|
||||||
|
$msg = '<div class="alert alert-danger">以下兑换码已在未处理的工单中,请处理后再提交:' . h(implode(', ', $inPending)) . '</div>';
|
||||||
|
} else {
|
||||||
|
if (isset($_FILES['attachment']) && $_FILES['attachment']['error'] === UPLOAD_ERR_OK) {
|
||||||
|
$ext = strtolower(pathinfo($_FILES['attachment']['name'], PATHINFO_EXTENSION));
|
||||||
|
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'zip', 'rar', 'txt'];
|
||||||
|
if (!in_array($ext, $allowed)) {
|
||||||
|
$msg = '<div class="alert alert-danger">不支持的文件格式</div>';
|
||||||
|
} elseif ($_FILES['attachment']['size'] > 10 * 1024 * 1024) {
|
||||||
|
$msg = '<div class="alert alert-danger">文件大小不能超过10MB</div>';
|
||||||
|
} else {
|
||||||
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||||
|
$mime = finfo_file($finfo, $_FILES['attachment']['tmp_name']);
|
||||||
|
finfo_close($finfo);
|
||||||
|
if (!in_array($mime, ['image/jpeg','image/png','image/gif','application/pdf','application/msword','application/vnd.openxmlformats-officedocument.wordprocessingml.document','application/vnd.ms-excel','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet','application/zip','application/x-rar-compressed','text/plain'])) {
|
||||||
|
$msg = '<div class="alert alert-danger">不支持的文件格式</div>';
|
||||||
|
} else {
|
||||||
|
$filename = bin2hex(random_bytes(16)) . '.' . $ext;
|
||||||
|
$dest = __DIR__ . '/uploads/' . $filename;
|
||||||
|
if (move_uploaded_file($_FILES['attachment']['tmp_name'], $dest)) {
|
||||||
|
$attachment = 'uploads/' . $filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$msg) {
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare('INSERT INTO work_orders (product_id, content, code, attachment, creator_id, created_at, status) VALUES (?, ?, ?, ?, ?, NOW(), ?)');
|
||||||
|
$stmt->execute([$pid, $content, $codesStr ?: null, $attachment ?: null, getCurrentUserId(), '未处理']);
|
||||||
|
$_SESSION['flash_msg'] = '工单提交成功';
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
header('Location: work_order_records.php');
|
||||||
|
exit;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$msg = '<div class="alert alert-danger">提交失败,请重试</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageTitle = '发起工单';
|
||||||
|
require __DIR__ . '/includes/header.php';
|
||||||
|
?>
|
||||||
|
<div class="card">
|
||||||
|
<h2>发起工单</h2>
|
||||||
|
<?= $msg ?>
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= csrfToken() ?>">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>工单内容 <span style="color:red">*</span></label>
|
||||||
|
<textarea name="content" class="form-control" required><?= h($_POST['content'] ?? '') ?></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>关联兑换码 <span style="color:red">*</span></label>
|
||||||
|
<div id="codesContainer">
|
||||||
|
<?php if (empty($oldCodes)): ?>
|
||||||
|
<div class="code-row" style="display:flex;gap:6px;margin-bottom:6px;">
|
||||||
|
<input type="text" name="codes[]" class="form-control" placeholder="输入兑换码" style="flex:1;">
|
||||||
|
<button type="button" class="btn btn-danger btn-sm" onclick="removeCode(this)" style="display:none;">删除</button>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($oldCodes as $v): ?>
|
||||||
|
<div class="code-row" style="display:flex;gap:6px;margin-bottom:6px;">
|
||||||
|
<input type="text" name="codes[]" class="form-control" value="<?= h($v) ?>" placeholder="输入兑换码" style="flex:1;">
|
||||||
|
<button type="button" class="btn btn-danger btn-sm" onclick="removeCode(this)">删除</button>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-info btn-sm" onclick="addCode()">+ 添加兑换码</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>附件</label>
|
||||||
|
<input type="file" name="attachment" class="form-control-file">
|
||||||
|
<div style="font-size:12px;color:#999;margin-top:4px;">支持 jpg/png/gif/pdf/doc/xls/zip/rar/txt,最大10MB</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">提交工单</button>
|
||||||
|
<button type="button" class="btn btn-default" onclick="history.back()" style="margin-left:8px;">取消</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function addCode() {
|
||||||
|
var c = document.getElementById('codesContainer');
|
||||||
|
if (c.children.length >= 10) { alert('最多添加10个兑换码'); return; }
|
||||||
|
var row = document.createElement('div');
|
||||||
|
row.className = 'code-row';
|
||||||
|
row.style.cssText = 'display:flex;gap:6px;margin-bottom:6px;';
|
||||||
|
row.innerHTML = '<input type="text" name="codes[]" class="form-control" placeholder="输入兑换码" style="flex:1;">' +
|
||||||
|
'<button type="button" class="btn btn-danger btn-sm" onclick="removeCode(this)">删除</button>';
|
||||||
|
c.appendChild(row);
|
||||||
|
}
|
||||||
|
function removeCode(btn) {
|
||||||
|
var row = btn.parentNode;
|
||||||
|
row.parentNode.removeChild(row);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<?php require __DIR__ . '/includes/footer.php'; ?>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
# 发起工单页面校验条件
|
||||||
|
|
||||||
|
文件:`work_order_create.php`
|
||||||
|
|
||||||
|
| # | 校验项 | 校验方式 | 错误提示 |
|
||||||
|
|---|--------|----------|----------|
|
||||||
|
| 1 | CSRF Token | `verifyCsrf()` 验证 | 直接 `die('CSRF token无效')` |
|
||||||
|
| 2 | 工单内容 | `trim($_POST['content'])` 非空 | 工单内容不能为空 |
|
||||||
|
| 3 | 关联兑换码至少一个 | `array_filter` 去除空白后非空 | 请至少关联一个兑换码 |
|
||||||
|
| 4 | 兑换码数量 ≤ 10 | `count($codes) > 10` | 最多关联10个兑换码 |
|
||||||
|
| 5 | 兑换码不重复 | `array_unique` 对比长度 | 关联的兑换码不能重复 |
|
||||||
|
| 6 | 兑换码属于当前用户 | 查询 `claim_records` 匹配 `user_id` 和 `code` | 以下兑换码不在您的领取记录中 |
|
||||||
|
| 7 | 兑换码未过期 | `status !== 3` | 以下兑换码已过期,无法提交工单 |
|
||||||
|
| 8 | 兑换码不在未处理工单中 | 查询 `work_orders`(`creator_id` + `status='未处理'`) | 以下兑换码已在未处理的工单中,请处理后再提交 |
|
||||||
|
| 9 | 附件扩展名 | `pathinfo` 白名单:jpg/jpeg/png/gif/pdf/doc/docx/xls/xlsx/zip/rar/txt | 不支持的文件格式 |
|
||||||
|
| 10 | 附件大小 | `$_FILES['size']` ≤ 10MB | 文件大小不能超过10MB |
|
||||||
|
| 11 | 附件 MIME 类型 | `finfo` 白名单验证 | 不支持的文件格式 |
|
||||||
|
|
@ -0,0 +1,228 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
require_once __DIR__ . '/includes/functions.php';
|
||||||
|
requireAdmin();
|
||||||
|
|
||||||
|
$pageTitle = '工单管理';
|
||||||
|
require __DIR__ . '/includes/header.php';
|
||||||
|
|
||||||
|
$msg = '';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
|
if (!verifyCsrf($_POST['csrf_token'] ?? '')) { die('CSRF token无效'); }
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($_POST['action'] === 'set_done') {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM work_orders WHERE id = ? AND status = '未处理'");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$order = $stmt->fetch();
|
||||||
|
if (!$order) {
|
||||||
|
$msg = '<div class="alert alert-danger">工单不存在或状态已变更</div>';
|
||||||
|
$pdo->rollBack();
|
||||||
|
} else {
|
||||||
|
$codes = array_filter(explode("\n", $order['code'] ?? ''));
|
||||||
|
$missingClaim = 0;
|
||||||
|
$noStock = 0;
|
||||||
|
|
||||||
|
foreach ($codes as $code) {
|
||||||
|
$code = trim($code);
|
||||||
|
if ($code === '') continue;
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM claim_records WHERE code = ? AND user_id = ? AND product_id = ? FOR UPDATE");
|
||||||
|
$stmt->execute([$code, $order['creator_id'], $order['product_id']]);
|
||||||
|
$cr = $stmt->fetch();
|
||||||
|
if (!$cr) { $missingClaim++; continue; }
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT 1 FROM redemption_codes WHERE value = ? AND status = 1 AND (expired_at IS NULL OR expired_at > NOW()) AND product_id = ? LIMIT 1 FOR UPDATE");
|
||||||
|
$stmt->execute([$cr['value'], $order['product_id']]);
|
||||||
|
if (!$stmt->fetch()) { $noStock++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($missingClaim > 0 || $noStock > 0) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
$errParts = [];
|
||||||
|
if ($missingClaim > 0) $errParts[] = $missingClaim . ' 个兑换码未找到领取记录';
|
||||||
|
if ($noStock > 0) $errParts[] = '库存不足,无法补发';
|
||||||
|
$msg = '<div class="alert alert-danger">' . implode(';', $errParts) . ',工单无法处理</div>';
|
||||||
|
} else {
|
||||||
|
$replaced = 0;
|
||||||
|
foreach ($codes as $code) {
|
||||||
|
$code = trim($code);
|
||||||
|
if ($code === '') continue;
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM claim_records WHERE code = ? AND user_id = ? AND product_id = ?");
|
||||||
|
$stmt->execute([$code, $order['creator_id'], $order['product_id']]);
|
||||||
|
$cr = $stmt->fetch();
|
||||||
|
if (!$cr) continue;
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("UPDATE claim_records SET status = 3, used_at = COALESCE(used_at, NOW()), remark = '该码已过期,新码已补发' WHERE id = ?");
|
||||||
|
$stmt->execute([$cr['id']]);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM redemption_codes WHERE value = ? AND status = 1 AND (expired_at IS NULL OR expired_at > NOW()) AND product_id = ? LIMIT 1 FOR UPDATE");
|
||||||
|
$stmt->execute([$cr['value'], $order['product_id']]);
|
||||||
|
$newCode = $stmt->fetch();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("UPDATE redemption_codes SET status = 2, claim_user_id = ?, claimed_at = NOW() WHERE id = ?");
|
||||||
|
$stmt->execute([$order['creator_id'], $newCode['id']]);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO claim_records (product_id, created_at, user_id, code_name, batch_no, code_type, code, value, status, expired_at, price_tier1, price_tier2, claim_user, claimed_at, remark) VALUES (?, NOW(), ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, NOW(), '系统补发')");
|
||||||
|
$stmt->execute([
|
||||||
|
$newCode['product_id'],
|
||||||
|
$order['creator_id'],
|
||||||
|
$newCode['name'],
|
||||||
|
$newCode['batch_no'],
|
||||||
|
$newCode['type'],
|
||||||
|
$newCode['code'],
|
||||||
|
$newCode['value'],
|
||||||
|
$newCode['expired_at'],
|
||||||
|
$newCode['price_tier1'],
|
||||||
|
$newCode['price_tier2'],
|
||||||
|
$cr['claim_user']
|
||||||
|
]);
|
||||||
|
$replaced++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("UPDATE work_orders SET status = '已处理', processed_at = NOW(), processor_id = ? WHERE id = ?");
|
||||||
|
$stmt->execute([getCurrentUserId(), $id]);
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
$_SESSION['flash_msg'] = '工单已处理,' . $replaced . ' 个兑换码已过期并补发新码';
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
header('Location: work_order_manage.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
$msg = '<div class="alert alert-danger">处理失败,请重试</div>';
|
||||||
|
}
|
||||||
|
} elseif ($_POST['action'] === 'reject') {
|
||||||
|
$stmt = $pdo->prepare("SELECT attachment FROM work_orders WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$order = $stmt->fetch();
|
||||||
|
$stmt = $pdo->prepare("UPDATE work_orders SET status = '已驳回', processed_at = NOW(), processor_id = ? WHERE id = ? AND status = '未处理'");
|
||||||
|
$stmt->execute([getCurrentUserId(), $id]);
|
||||||
|
if ($stmt->rowCount()) {
|
||||||
|
if ($order && $order['attachment'] && file_exists(__DIR__ . '/' . $order['attachment'])) {
|
||||||
|
unlink(__DIR__ . '/' . $order['attachment']);
|
||||||
|
}
|
||||||
|
$_SESSION['flash_msg'] = '工单已驳回';
|
||||||
|
$_SESSION['flash_type'] = 'warning';
|
||||||
|
header('Location: work_order_manage.php');
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$msg = '<div class="alert alert-danger">工单状态已变更,无法驳回</div>';
|
||||||
|
}
|
||||||
|
} elseif ($_POST['action'] === 'delete') {
|
||||||
|
$stmt = $pdo->prepare("SELECT attachment, status FROM work_orders WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$order = $stmt->fetch();
|
||||||
|
if (!$order) {
|
||||||
|
$msg = '<div class="alert alert-danger">工单不存在</div>';
|
||||||
|
} elseif ($order['status'] === '已处理') {
|
||||||
|
$msg = '<div class="alert alert-danger">已处理的工单不能删除</div>';
|
||||||
|
} else {
|
||||||
|
if ($order['attachment'] && file_exists(__DIR__ . '/' . $order['attachment'])) {
|
||||||
|
unlink(__DIR__ . '/' . $order['attachment']);
|
||||||
|
}
|
||||||
|
$pdo->prepare('DELETE FROM work_orders WHERE id = ?')->execute([$id]);
|
||||||
|
$_SESSION['flash_msg'] = '工单已删除';
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
header('Location: work_order_manage.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
$page = max(1, (int)($_GET['p'] ?? 1));
|
||||||
|
$perPage = 20;
|
||||||
|
$offset = ($page - 1) * $perPage;
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT COUNT(*) FROM work_orders WHERE product_id = ?');
|
||||||
|
$stmt->execute([$pid]);
|
||||||
|
$total = (int)$stmt->fetchColumn();
|
||||||
|
$totalPages = max(1, ceil($total / $perPage));
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT w.*, c.username AS creator_name, p.username AS processor_name FROM work_orders w LEFT JOIN users c ON w.creator_id = c.id LEFT JOIN users p ON w.processor_id = p.id WHERE w.product_id = ? ORDER BY w.created_at DESC LIMIT ? OFFSET ?');
|
||||||
|
$stmt->execute([$pid, $perPage, $offset]);
|
||||||
|
$orders = $stmt->fetchAll();
|
||||||
|
?>
|
||||||
|
<div class="card">
|
||||||
|
<h2>工单管理</h2>
|
||||||
|
<?php if (isset($_SESSION['flash_msg'])): ?>
|
||||||
|
<div class="alert alert-<?= h($_SESSION['flash_type'] ?? 'success') ?>"><?= h($_SESSION['flash_msg']) ?></div>
|
||||||
|
<?php unset($_SESSION['flash_msg'], $_SESSION['flash_type']); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?= $msg ?>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>内容</th>
|
||||||
|
<th>兑换码</th>
|
||||||
|
<th>附件</th>
|
||||||
|
<th>发起人</th>
|
||||||
|
<th>发起时间</th>
|
||||||
|
<th>处理时间</th>
|
||||||
|
<th>处理人</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($orders as $o): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= $o['id'] ?></td>
|
||||||
|
<td style="max-width:200px;white-space:pre-wrap;word-break:break-all;"><?= h(mb_substr($o['content'], 0, 100)) ?><?= mb_strlen($o['content']) > 100 ? '...' : '' ?></td>
|
||||||
|
<td style="white-space:pre-wrap;"><?php if ($o['code']): $lines = explode("\n", $o['code']); echo h(implode("\n", $lines)); else: ?>-<?php endif; ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if ($o['attachment']): ?>
|
||||||
|
<a href="<?= h($o['attachment']) ?>" target="_blank" class="btn btn-info btn-sm">查看</a>
|
||||||
|
<?php else: ?>
|
||||||
|
-
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td><?= h($o['creator_name'] ?? '-') ?></td>
|
||||||
|
<td><?= formatDateTime($o['created_at']) ?></td>
|
||||||
|
<td><?= formatDateTime($o['processed_at']) ?></td>
|
||||||
|
<td><?= h($o['processor_name'] ?? '-') ?></td>
|
||||||
|
<td><?= workOrderStatusBadge($o['status']) ?></td>
|
||||||
|
<td>
|
||||||
|
<div class="action-group">
|
||||||
|
<?php if ($o['status'] === '未处理'): ?>
|
||||||
|
<form method="post" style="display:inline;">
|
||||||
|
<input type="hidden" name="action" value="set_done">
|
||||||
|
<input type="hidden" name="id" value="<?= $o['id'] ?>">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= csrfToken() ?>">
|
||||||
|
<button type="submit" class="btn btn-success btn-sm">设为已处理</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" style="display:inline;">
|
||||||
|
<input type="hidden" name="action" value="reject">
|
||||||
|
<input type="hidden" name="id" value="<?= $o['id'] ?>">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= csrfToken() ?>">
|
||||||
|
<button type="submit" class="btn btn-warning btn-sm" onclick="return confirm('确定驳回此工单?驳回后数据保留但不可操作')">驳回</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" style="display:inline;" onsubmit="return confirm('确定删除此工单?')">
|
||||||
|
<input type="hidden" name="action" value="delete">
|
||||||
|
<input type="hidden" name="id" value="<?= $o['id'] ?>">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= csrfToken() ?>">
|
||||||
|
<button type="submit" class="btn btn-danger btn-sm">删除</button>
|
||||||
|
</form>
|
||||||
|
<?php elseif ($o['status'] === '已驳回'): ?>
|
||||||
|
<span style="color:#999;font-size:12px;">已驳回,不可操作</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($orders)): ?>
|
||||||
|
<tr><td colspan="10" style="text-align:center;color:#999;">暂无工单</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php renderPagination($page, $totalPages); ?>
|
||||||
|
</div>
|
||||||
|
<?php require __DIR__ . '/includes/footer.php'; ?>
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
require_once __DIR__ . '/includes/functions.php';
|
||||||
|
requireLogin();
|
||||||
|
|
||||||
|
// 撤销工单(在输出 HTML 前处理)
|
||||||
|
if (isset($_GET['cancel']) && is_numeric($_GET['cancel'])) {
|
||||||
|
$id = (int)$_GET['cancel'];
|
||||||
|
$pid = getCurrentProductId();
|
||||||
|
$stmt = $pdo->prepare("SELECT attachment FROM work_orders WHERE id = ? AND creator_id = ? AND product_id = ? AND status = '未处理'");
|
||||||
|
$stmt->execute([$id, getCurrentUserId(), $pid]);
|
||||||
|
$order = $stmt->fetch();
|
||||||
|
if ($order) {
|
||||||
|
if ($order['attachment'] && file_exists(__DIR__ . '/' . $order['attachment'])) {
|
||||||
|
unlink(__DIR__ . '/' . $order['attachment']);
|
||||||
|
}
|
||||||
|
$pdo->prepare('DELETE FROM work_orders WHERE id = ? AND product_id = ?')->execute([$id, $pid]);
|
||||||
|
$_SESSION['flash_msg'] = '工单已撤销';
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
} else {
|
||||||
|
$_SESSION['flash_msg'] = '无法撤销:工单不存在、非本人发起或状态已变更';
|
||||||
|
$_SESSION['flash_type'] = 'danger';
|
||||||
|
}
|
||||||
|
header('Location: work_order_records.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageTitle = '工单记录';
|
||||||
|
require __DIR__ . '/includes/header.php';
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
$page = max(1, (int)($_GET['p'] ?? 1));
|
||||||
|
$perPage = 20;
|
||||||
|
$offset = ($page - 1) * $perPage;
|
||||||
|
$pid = getCurrentProductId();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT COUNT(*) FROM work_orders WHERE creator_id = ? AND product_id = ?');
|
||||||
|
$stmt->execute([getCurrentUserId(), $pid]);
|
||||||
|
$total = (int)$stmt->fetchColumn();
|
||||||
|
$totalPages = max(1, ceil($total / $perPage));
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT * FROM work_orders WHERE creator_id = ? AND product_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?');
|
||||||
|
$stmt->execute([getCurrentUserId(), $pid, $perPage, $offset]);
|
||||||
|
$orders = $stmt->fetchAll();
|
||||||
|
?>
|
||||||
|
<div style="margin-bottom:16px;">
|
||||||
|
<a href="work_order_create.php" class="btn btn-primary">+ 发起工单</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (isset($_SESSION['flash_msg'])): ?>
|
||||||
|
<div class="alert alert-<?= h($_SESSION['flash_type'] ?? 'success') ?>"><?= h($_SESSION['flash_msg']) ?></div>
|
||||||
|
<?php unset($_SESSION['flash_msg'], $_SESSION['flash_type']); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>我的工单</h2>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>内容</th>
|
||||||
|
<th>兑换码</th>
|
||||||
|
<th>附件</th>
|
||||||
|
<th>发起时间</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($orders as $o): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= $o['id'] ?></td>
|
||||||
|
<td style="max-width:200px;white-space:pre-wrap;word-break:break-all;"><?= h(mb_substr($o['content'], 0, 100)) ?><?= mb_strlen($o['content']) > 100 ? '...' : '' ?></td>
|
||||||
|
<td style="white-space:pre-wrap;"><?php if ($o['code']): $lines = explode("\n", $o['code']); echo h(implode("\n", $lines)); else: ?>-<?php endif; ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if ($o['attachment']): ?>
|
||||||
|
<a href="<?= h($o['attachment']) ?>" target="_blank" class="btn btn-info btn-sm">查看</a>
|
||||||
|
<?php else: ?>
|
||||||
|
-
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td><?= formatDateTime($o['created_at']) ?></td>
|
||||||
|
<td><?= workOrderStatusBadge($o['status']) ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if ($o['status'] === '未处理'): ?>
|
||||||
|
<a href="?cancel=<?= $o['id'] ?>&csrf_token=<?= csrfToken() ?>" class="btn btn-danger btn-sm" onclick="return confirm('确定撤销此工单?')">撤销</a>
|
||||||
|
<?php elseif ($o['status'] === '已驳回'): ?>
|
||||||
|
<span style="color:#999;font-size:12px;">已驳回</span>
|
||||||
|
<?php else: ?>
|
||||||
|
-
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($orders)): ?>
|
||||||
|
<tr><td colspan="7" style="text-align:center;color:#999;">暂无工单</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php renderPagination($page, $totalPages); ?>
|
||||||
|
</div>
|
||||||
|
<?php require __DIR__ . '/includes/footer.php'; ?>
|
||||||
Loading…
Reference in New Issue