coupon/admin_settings.php

550 lines
26 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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')">&times;</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="card" style="margin-bottom:20px;">
<h2>兑换码状态同步</h2>
<?php
require_once __DIR__ . '/includes/coupon_sync.php';
$syncResult = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['sync_coupon'])) {
if (!verifyCsrf($_POST['csrf_token'] ?? '')) { die('CSRF token无效'); }
$startTime = (int)($_POST['start_time'] ?? 0);
$endTime = (int)($_POST['end_time'] ?? 0);
if ($startTime <= 0 || $endTime <= 0 || $startTime > $endTime) {
$syncResult = '<div class="alert alert-danger">请选择有效的时间范围</div>';
} else {
$stmt = $pdo->query("SELECT id, code, name, api_url, token FROM products WHERE status = 1 AND api_url IS NOT NULL AND api_url != ''");
$products = $stmt->fetchAll();
if (empty($products)) {
$syncResult = '<div class="alert alert-warning">没有已配置接口地址的产品</div>';
} else {
$output = '';
foreach ($products as $p) {
$ret = syncCouponStatus($pdo, $p, $startTime, $endTime);
if ($ret['success']) {
$output .= "<strong>{$p['name']}</strong>: API返回{$ret['total']}条, 匹配{$ret['matched']}条, 更新{$ret['updated']}条";
if (!empty($ret['matched_codes'])) {
$output .= '<br><span style="font-size:12px;color:#166534;">✓ 已匹配 (' . count($ret['matched_codes']) . '个): ' . h(implode(', ', $ret['matched_codes'])) . '</span>';
}
if (!empty($ret['unmatched_codes'])) {
$output .= '<br><span style="font-size:12px;color:#991b1b;">✗ 未匹配 (' . count($ret['unmatched_codes']) . '个): ' . h(implode(', ', array_slice($ret['unmatched_codes'], 0, 50))) . (count($ret['unmatched_codes']) > 50 ? '...' : '') . '</span>';
}
$output .= '<br>';
} else {
$output .= "<strong>{$p['name']}</strong>: 失败 - {$ret['message']}<br>";
}
}
$syncResult = '<div class="alert alert-success" style="font-size:13px;word-break:break-all;">' . $output . '</div>';
}
}
}
?>
<?= $syncResult ?>
<p style="font-size:14px;color:#666;margin-bottom:12px;">通过远程接口查询兑换码的使用状态,并将已使用的兑换码更新到本地。</p>
<form method="post" style="display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap;" onsubmit="document.getElementById('sync_start').value=Math.floor(new Date(document.getElementById('sync_start_input').value).getTime()/1000);document.getElementById('sync_end').value=Math.floor(new Date(document.getElementById('sync_end_input').value).getTime()/1000);">
<input type="hidden" name="csrf_token" value="<?= csrfToken() ?>">
<div class="form-group" style="margin-bottom:0;">
<label>开始时间</label>
<input type="datetime-local" id="sync_start_input" class="form-control" required style="width:200px;">
<input type="hidden" name="start_time" id="sync_start" value="">
</div>
<div class="form-group" style="margin-bottom:0;">
<label>结束时间</label>
<input type="datetime-local" id="sync_end_input" class="form-control" required style="width:200px;">
<input type="hidden" name="end_time" id="sync_end" value="">
</div>
<button type="submit" name="sync_coupon" class="btn btn-primary">立即同步</button>
</form>
</div>
<!-- 新增用户弹窗 -->
<div class="modal" id="addUserModal">
<div class="modal-content">
<span class="modal-close" onclick="hideModal('addUserModal')">&times;</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')">&times;</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')">&times;</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'; ?>