NOW() ' . 'ORDER BY s.expires_at DESC LIMIT 1) AS plan_name ' . 'FROM users u'; $pdo = Db::pdo(); if ($q !== '') { $like = '%' . $q . '%'; $stmt = $pdo->prepare($baseSql . ' WHERE u.email LIKE :q ORDER BY u.id DESC LIMIT :lim OFFSET :off'); $stmt->bindValue('q', $like); $stmt->bindValue('lim', $size, \PDO::PARAM_INT); $stmt->bindValue('off', $offset, \PDO::PARAM_INT); $stmt->execute(); $countStmt = $pdo->prepare('SELECT COUNT(*) FROM users WHERE email LIKE :q'); $countStmt->execute(['q' => $like]); $count = (int)$countStmt->fetchColumn(); } else { $stmt = $pdo->prepare($baseSql . ' ORDER BY u.id DESC LIMIT :lim OFFSET :off'); $stmt->bindValue('lim', $size, \PDO::PARAM_INT); $stmt->bindValue('off', $offset, \PDO::PARAM_INT); $stmt->execute(); $count = (int)$pdo->query('SELECT COUNT(*) FROM users')->fetchColumn(); } AuditService::log($adminId, 'users.list', 'users', ['page' => $page, 'size' => $size, 'q' => $q]); Json::ok(['items' => $stmt->fetchAll(), 'total' => $count, 'page' => $page, 'size' => $size]); } public function create(int $adminId): void { $body = Json::readBody(); $email = trim((string)($body['email'] ?? '')); $password = (string)($body['password'] ?? ''); $role = (string)($body['role'] ?? 'user'); if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { Json::fail('bad_request', '邮箱格式不正确', 400); } if (strlen($password) < 8) { Json::fail('bad_request', '密码至少 8 位', 400); } if (!in_array($role, ['user', 'admin'], true)) { Json::fail('bad_request', 'role 不合法', 400); } if ($role === 'admin') { AdminPermission::requireFull($adminId); } $adminLevel = null; if ($role === 'admin') { $adminLevel = (string)($body['admin_level'] ?? 'ops'); if (!in_array($adminLevel, ['full', 'ops'], true)) { Json::fail('bad_request', 'admin_level 不合法', 400); } } $pdo = Db::pdo(); $dup = $pdo->prepare('SELECT id FROM users WHERE email = :e'); $dup->execute(['e' => $email]); if ($dup->fetch()) { Json::fail('conflict', '邮箱已存在', 409); } $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]); $stmt = $pdo->prepare( 'INSERT INTO users (email, password_hash, role, admin_level, status, created_at) ' . 'VALUES (:e, :h, :r, :lv, \'active\', :ts)' ); $stmt->execute([ 'e' => $email, 'h' => $hash, 'r' => $role, 'lv' => $adminLevel, 'ts' => date('Y-m-d H:i:s'), ]); $id = (int)$pdo->lastInsertId(); AuditService::log($adminId, 'users.create', 'user:' . $id, ['email' => $email, 'role' => $role]); Json::ok(['id' => $id, 'email' => $email, 'role' => $role]); } public function detail(int $adminId, int $id): void { $pdo = Db::pdo(); $user = self::findUser($pdo, $id); if ($user === null) { Json::fail('not_found', '用户不存在', 404); } $subStmt = $pdo->prepare( 'SELECT s.id AS subscription_id, s.plan_id, s.status, s.started_at, s.expires_at, p.name AS plan_name, p.code AS plan_code ' . 'FROM subscriptions s JOIN plans p ON p.id = s.plan_id ' . 'WHERE s.user_id = :uid ORDER BY (s.status = \'active\') DESC, s.id DESC LIMIT 1' ); $subStmt->execute(['uid' => $id]); $subscription = $subStmt->fetch() ?: null; $usageStmt = $pdo->prepare( 'SELECT COUNT(*) AS files_count, COALESCE(SUM(size), 0) AS storage_bytes ' . 'FROM soon_files WHERE user_id = :uid AND deleted_at IS NULL' ); $usageStmt->execute(['uid' => $id]); $usage = $usageStmt->fetch() ?: ['files_count' => 0, 'storage_bytes' => 0]; $orderStmt = $pdo->prepare( 'SELECT id, order_no, amount_cents, status, channel, created_at FROM pay_orders ' . 'WHERE user_id = :uid ORDER BY id DESC LIMIT 5' ); $orderStmt->execute(['uid' => $id]); $recentOrders = $orderStmt->fetchAll(); AuditService::log($adminId, 'users.detail', 'user:' . $id); Json::ok([ 'user' => $user, 'subscription' => $subscription, 'usage' => [ 'files_count' => (int)$usage['files_count'], 'storage_bytes' => (int)$usage['storage_bytes'], ], 'recent_orders' => $recentOrders, ]); } public function update(int $adminId, int $id): void { $pdo = Db::pdo(); $user = self::findUser($pdo, $id); if ($user === null) { Json::fail('not_found', '用户不存在', 404); } $body = Json::readBody(); $email = array_key_exists('email', $body) ? trim((string)$body['email']) : null; $role = array_key_exists('role', $body) ? (string)$body['role'] : null; $adminLevel = array_key_exists('admin_level', $body) ? (string)$body['admin_level'] : null; if ($email !== null) { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { Json::fail('bad_request', '邮箱格式不正确', 400); } $dup = $pdo->prepare('SELECT id FROM users WHERE email = :e AND id != :id'); $dup->execute(['e' => $email, 'id' => $id]); if ($dup->fetch()) { Json::fail('conflict', '邮箱已被使用', 409); } } if ($role !== null && !in_array($role, ['user', 'admin'], true)) { Json::fail('bad_request', 'role 不合法', 400); } if ($role === 'user' && $user['role'] === 'admin') { self::assertAdminRemovable($pdo, $id); } if ($role === 'admin' || $user['role'] === 'admin' || $adminLevel !== null) { AdminPermission::requireFull($adminId); } if ($adminLevel !== null && !in_array($adminLevel, ['full', 'ops'], true)) { Json::fail('bad_request', 'admin_level 不合法', 400); } if ($role !== null && $role !== 'admin' && $adminLevel !== null) { Json::fail('bad_request', '仅管理员账号可设置 admin_level', 400); } $sets = []; $params = ['id' => $id]; if ($email !== null) { $sets[] = 'email = :e'; $params['e'] = $email; } if ($role !== null) { $sets[] = 'role = :r'; $params['r'] = $role; if ($role === 'user') { $sets[] = 'admin_level = NULL'; } } if ($adminLevel !== null) { $sets[] = 'admin_level = :lv'; $params['lv'] = $adminLevel; } if ($sets === []) { Json::fail('bad_request', '无更新字段', 400); } $pdo->prepare('UPDATE users SET ' . implode(', ', $sets) . ' WHERE id = :id')->execute($params); AuditService::log($adminId, 'users.update', 'user:' . $id, [ 'email' => $email, 'role' => $role, 'admin_level' => $adminLevel, ]); Json::ok(['id' => $id]); } public function delete(int $adminId, int $id): void { if ($id === $adminId) { Json::fail('bad_request', '不能删除自己的账号', 400); } $pdo = Db::pdo(); $user = self::findUser($pdo, $id); if ($user === null) { Json::fail('not_found', '用户不存在', 404); } if ($user['role'] === 'admin') { AdminPermission::requireFull($adminId); self::assertAdminRemovable($pdo, $id); } $pdo->beginTransaction(); try { $pdo->prepare('UPDATE soon_files SET deleted_at = :ts WHERE user_id = :u AND deleted_at IS NULL') ->execute(['ts' => date('Y-m-d H:i:s'), 'u' => $id]); $pdo->prepare('DELETE FROM subscriptions WHERE user_id = :u')->execute(['u' => $id]); $pdo->prepare('DELETE FROM users WHERE id = :id')->execute(['id' => $id]); $pdo->commit(); } catch (\Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } throw $e; } AuditService::log($adminId, 'users.delete', 'user:' . $id, ['email' => $user['email']]); Json::ok(['id' => $id]); } public function setPassword(int $adminId, int $id): void { $pdo = Db::pdo(); if (self::findUser($pdo, $id) === null) { Json::fail('not_found', '用户不存在', 404); } $password = (string)(Json::readBody()['password'] ?? ''); if (strlen($password) < 8) { Json::fail('bad_request', '密码至少 8 位', 400); } $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]); $pdo->prepare('UPDATE users SET password_hash = :h WHERE id = :id')->execute(['h' => $hash, 'id' => $id]); AuditService::log($adminId, 'users.setPassword', 'user:' . $id); Json::ok(['id' => $id]); } public function setSubscription(int $adminId, int $id): void { $pdo = Db::pdo(); if (self::findUser($pdo, $id) === null) { Json::fail('not_found', '用户不存在', 404); } $body = Json::readBody(); $planId = (int)($body['plan_id'] ?? 0); $days = (int)($body['days'] ?? 0); if ($planId <= 0) { Json::fail('bad_request', 'plan_id 必填', 400); } $planStmt = $pdo->prepare('SELECT * FROM plans WHERE id = :id'); $planStmt->execute(['id' => $planId]); $plan = $planStmt->fetch(); if (!$plan) { Json::fail('not_found', '套餐不存在', 404); } $durationDays = $days > 0 ? $days : (int)$plan['duration_days']; $pdo->beginTransaction(); try { $pdo->prepare('UPDATE subscriptions SET status = \'expired\' WHERE user_id = :u AND status = \'active\'') ->execute(['u' => $id]); if ($plan['code'] !== 'free') { $now = date('Y-m-d H:i:s'); $expires = MembershipService::subscriptionExpiresAt($durationDays); $pdo->prepare( 'INSERT INTO subscriptions (user_id, plan_id, status, started_at, expires_at) ' . 'VALUES (:u, :p, \'active\', :sa, :ea)' )->execute(['u' => $id, 'p' => $planId, 'sa' => $now, 'ea' => $expires]); } $pdo->commit(); } catch (\Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } throw $e; } AuditService::log($adminId, 'users.setSubscription', 'user:' . $id, [ 'plan_id' => $planId, 'days' => $durationDays, ]); Json::ok(['id' => $id, 'plan_id' => $planId]); } public function setStatus(int $adminId, int $id): void { $body = Json::readBody(); $status = (string)($body['status'] ?? ''); if (!in_array($status, ['active', 'disabled'], true)) { Json::fail('bad_request', 'status 必须为 active 或 disabled', 400); } if ($id === $adminId && $status === 'disabled') { Json::fail('bad_request', '不能停用自己的账号', 400); } $pdo = Db::pdo(); $user = self::findUser($pdo, $id); if ($user === null) { Json::fail('not_found', '用户不存在', 404); } if ($status === 'disabled' && $user['role'] === 'admin') { self::assertAdminRemovable($pdo, $id); } $pdo->prepare('UPDATE users SET status = :s WHERE id = :id')->execute(['s' => $status, 'id' => $id]); AuditService::log($adminId, 'users.setStatus', 'user:' . $id, ['status' => $status]); Json::ok(['id' => $id, 'status' => $status]); } /** @return array|null */ private static function findUser(\PDO $pdo, int $id): ?array { $stmt = $pdo->prepare('SELECT id, email, role, admin_level, status, created_at FROM users WHERE id = :id'); $stmt->execute(['id' => $id]); $row = $stmt->fetch(); return $row ?: null; } private static function assertAdminRemovable(\PDO $pdo, int $excludeId): void { $stmt = $pdo->prepare('SELECT COUNT(*) FROM users WHERE role = \'admin\' AND status = \'active\' AND id != :id'); $stmt->execute(['id' => $excludeId]); if ((int)$stmt->fetchColumn() < 1) { Json::fail('bad_request', '至少保留一名活跃管理员', 400); } } }