From dd4fb6edd31c405c98ca8337fca4ccd40d3ad652 Mon Sep 17 00:00:00 2001
From: jianuo <32106500027@e.gzhu.edu.cn>
Date: Fri, 19 Dec 2025 19:17:47 +0800
Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E7=AE=80=E5=8D=95=E6=B7=BB?=
=?UTF-8?q?=E5=8A=A0=E7=AE=A1=E7=90=86=E5=91=98=E5=90=8E=E5=8F=B0=E5=8A=9F?=
=?UTF-8?q?=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.env.example | 14 +
.env.production.example | 10 +
README.md | 18 ++
client/.env.example | 4 +
client/index.html | 12 +
client/package.json | 24 ++
client/src/app/AdminLayout.tsx | 52 ++++
client/src/app/App.tsx | 26 ++
client/src/lib/adminAuth.ts | 17 ++
client/src/lib/api.ts | 62 ++++
client/src/main.tsx | 9 +
client/src/pages/LoginPage.tsx | 50 +++
client/src/pages/UsersPage.tsx | 161 ++++++++++
client/tsconfig.json | 19 ++
client/vite.config.ts | 9 +
docs/api/api-documentation.md | 48 ++-
docs/systems/admin-dashboard/README.md | 156 ++++++++++
pnpm-workspace.yaml | 3 +
src/app.module.ts | 2 +
src/business/admin/admin.controller.ts | 75 +++++
src/business/admin/admin.module.ts | 24 ++
src/business/admin/admin.service.ts | 100 ++++++
src/core/admin_core/admin_core.module.ts | 27 ++
src/core/admin_core/admin_core.service.ts | 285 ++++++++++++++++++
src/core/guards/admin.guard.ts | 43 +++
.../login_core/login_core.service.spec.ts | 4 +-
src/dto/admin.dto.ts | 34 +++
src/dto/admin_response.dto.ts | 139 +++++++++
src/main.ts | 7 +
29 files changed, 1431 insertions(+), 3 deletions(-)
create mode 100644 client/.env.example
create mode 100644 client/index.html
create mode 100644 client/package.json
create mode 100644 client/src/app/AdminLayout.tsx
create mode 100644 client/src/app/App.tsx
create mode 100644 client/src/lib/adminAuth.ts
create mode 100644 client/src/lib/api.ts
create mode 100644 client/src/main.tsx
create mode 100644 client/src/pages/LoginPage.tsx
create mode 100644 client/src/pages/UsersPage.tsx
create mode 100644 client/tsconfig.json
create mode 100644 client/vite.config.ts
create mode 100644 docs/systems/admin-dashboard/README.md
create mode 100644 src/business/admin/admin.controller.ts
create mode 100644 src/business/admin/admin.module.ts
create mode 100644 src/business/admin/admin.service.ts
create mode 100644 src/core/admin_core/admin_core.module.ts
create mode 100644 src/core/admin_core/admin_core.service.ts
create mode 100644 src/core/guards/admin.guard.ts
create mode 100644 src/dto/admin.dto.ts
create mode 100644 src/dto/admin_response.dto.ts
diff --git a/.env.example b/.env.example
index 5e971bf..810ab46 100644
--- a/.env.example
+++ b/.env.example
@@ -15,6 +15,20 @@ NODE_ENV=development
PORT=3000
LOG_LEVEL=debug
+# ===========================================
+# 管理员后台配置(开发环境推荐配置)
+# ===========================================
+# 管理员Token签名密钥(至少16字符,生产环境务必使用强随机值)
+ADMIN_TOKEN_SECRET=dev_admin_token_secret_change_me_32chars
+# 管理员Token有效期(秒),默认8小时
+ADMIN_TOKEN_TTL_SECONDS=28800
+
+# 启动引导创建管理员账号(仅当 enabled=true 时生效)
+ADMIN_BOOTSTRAP_ENABLED=false
+# ADMIN_USERNAME=admin
+# ADMIN_PASSWORD=Admin123456
+# ADMIN_NICKNAME=管理员
+
# JWT 配置
JWT_SECRET=test_jwt_secret_key_for_development_only_32chars
JWT_EXPIRES_IN=7d
diff --git a/.env.production.example b/.env.production.example
index 80d0757..8cbf4f2 100644
--- a/.env.production.example
+++ b/.env.production.example
@@ -16,6 +16,16 @@ PORT=3000
JWT_SECRET=your_jwt_secret_key_here_at_least_32_characters
JWT_EXPIRES_IN=7d
+# 管理员后台配置(生产环境必须配置)
+ADMIN_TOKEN_SECRET=please_use_a_strong_random_secret_at_least_32_chars
+ADMIN_TOKEN_TTL_SECONDS=28800
+
+# 启动引导创建管理员账号(建议仅首次部署临时开启,创建后关闭)
+ADMIN_BOOTSTRAP_ENABLED=false
+# ADMIN_USERNAME=admin
+# ADMIN_PASSWORD=please_set_a_strong_password
+# ADMIN_NICKNAME=管理员
+
# Redis 配置(用于验证码存储)
# 生产环境使用真实Redis服务
USE_FILE_REDIS=false
diff --git a/README.md b/README.md
index 8208fb4..79da810 100644
--- a/README.md
+++ b/README.md
@@ -46,6 +46,24 @@ pnpm run dev
🎉 **服务启动成功!** 访问 http://localhost:3000
+### 🧑💻 管理员后台(Ant Design)
+
+项目包含一个最小可用的管理员后台(管理员登录 / 用户管理 / 重置密码),文档见:
+
+- [docs/systems/admin-dashboard/README.md](docs/systems/admin-dashboard/README.md)
+
+启动方式:
+
+```bash
+pnpm install
+
+# 后端
+pnpm run dev
+
+# 前端后台
+pnpm -C client dev
+```
+
### 🧪 快速测试
```bash
diff --git a/client/.env.example b/client/.env.example
new file mode 100644
index 0000000..227a209
--- /dev/null
+++ b/client/.env.example
@@ -0,0 +1,4 @@
+# 前端后台配置
+# 复制为 .env.local
+
+VITE_API_BASE_URL=http://localhost:3000
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 0000000..8489477
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Whale Town Admin
+
+
+
+
+
+
diff --git a/client/package.json b/client/package.json
new file mode 100644
index 0000000..80c9559
--- /dev/null
+++ b/client/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "whale-town-admin",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "antd": "^5.27.3",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.30.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.24",
+ "@types/react-dom": "^18.3.7",
+ "@vitejs/plugin-react": "^5.0.2",
+ "typescript": "^5.9.3",
+ "vite": "^7.1.3"
+ }
+}
diff --git a/client/src/app/AdminLayout.tsx b/client/src/app/AdminLayout.tsx
new file mode 100644
index 0000000..0ce9802
--- /dev/null
+++ b/client/src/app/AdminLayout.tsx
@@ -0,0 +1,52 @@
+import { Layout, Menu, Typography } from 'antd';
+import { Outlet, useLocation, useNavigate } from 'react-router-dom';
+import { clearAuth } from '../lib/adminAuth';
+
+const { Header, Content, Sider } = Layout;
+
+export function AdminLayout() {
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ const selectedKey = location.pathname.startsWith('/users') ? 'users' : 'users';
+
+ return (
+
+
+
+
+ Whale Town Admin
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/app/App.tsx b/client/src/app/App.tsx
new file mode 100644
index 0000000..b7f8f6f
--- /dev/null
+++ b/client/src/app/App.tsx
@@ -0,0 +1,26 @@
+import { ConfigProvider } from 'antd';
+import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
+import { AdminLayout } from './AdminLayout';
+import { LoginPage } from '../pages/LoginPage';
+import { UsersPage } from '../pages/UsersPage';
+import { isAuthed } from '../lib/adminAuth';
+
+export function App() {
+ return (
+
+
+
+ } />
+ : }
+ >
+ } />
+ } />
+
+ } />
+
+
+
+ );
+}
diff --git a/client/src/lib/adminAuth.ts b/client/src/lib/adminAuth.ts
new file mode 100644
index 0000000..5cc4508
--- /dev/null
+++ b/client/src/lib/adminAuth.ts
@@ -0,0 +1,17 @@
+const TOKEN_KEY = 'whale_town_admin_token';
+
+export function getToken(): string | null {
+ return localStorage.getItem(TOKEN_KEY);
+}
+
+export function setToken(token: string): void {
+ localStorage.setItem(TOKEN_KEY, token);
+}
+
+export function clearAuth(): void {
+ localStorage.removeItem(TOKEN_KEY);
+}
+
+export function isAuthed(): boolean {
+ return Boolean(getToken());
+}
diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts
new file mode 100644
index 0000000..720a65a
--- /dev/null
+++ b/client/src/lib/api.ts
@@ -0,0 +1,62 @@
+import { getToken, clearAuth } from './adminAuth';
+
+const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000';
+
+export class ApiError extends Error {
+ status: number;
+
+ constructor(message: string, status: number) {
+ super(message);
+ this.status = status;
+ }
+}
+
+async function request(path: string, init?: RequestInit): Promise {
+ const token = getToken();
+
+ const headers: Record = {
+ 'Content-Type': 'application/json',
+ };
+
+ if (token) {
+ headers['Authorization'] = `Bearer ${token}`;
+ }
+
+ const res = await fetch(`${API_BASE_URL}${path}`, {
+ ...init,
+ headers: {
+ ...headers,
+ ...(init?.headers || {}),
+ },
+ credentials: 'include',
+ });
+
+ if (res.status === 401) {
+ clearAuth();
+ }
+
+ const data = (await res.json().catch(() => ({}))) as any;
+
+ if (!res.ok) {
+ throw new ApiError(data?.message || `请求失败: ${res.status}`, res.status);
+ }
+
+ return data as T;
+}
+
+export const api = {
+ adminLogin: (identifier: string, password: string) =>
+ request('/admin/auth/login', {
+ method: 'POST',
+ body: JSON.stringify({ identifier, password }),
+ }),
+
+ listUsers: (limit = 100, offset = 0) =>
+ request(`/admin/users?limit=${encodeURIComponent(limit)}&offset=${encodeURIComponent(offset)}`),
+
+ resetUserPassword: (userId: string, newPassword: string) =>
+ request(`/admin/users/${encodeURIComponent(userId)}/reset-password`, {
+ method: 'POST',
+ body: JSON.stringify({ new_password: newPassword }),
+ }),
+};
diff --git a/client/src/main.tsx b/client/src/main.tsx
new file mode 100644
index 0000000..825cc9c
--- /dev/null
+++ b/client/src/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { App } from './app/App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+ ,
+);
diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx
new file mode 100644
index 0000000..ecccf07
--- /dev/null
+++ b/client/src/pages/LoginPage.tsx
@@ -0,0 +1,50 @@
+import { Button, Card, Form, Input, Typography, message } from 'antd';
+import { useNavigate } from 'react-router-dom';
+import { api } from '../lib/api';
+import { setToken } from '../lib/adminAuth';
+
+type LoginValues = {
+ identifier: string;
+ password: string;
+};
+
+export function LoginPage() {
+ const navigate = useNavigate();
+ const [form] = Form.useForm();
+
+ const onFinish = async (values: LoginValues) => {
+ try {
+ const res = await api.adminLogin(values.identifier, values.password);
+ if (!res?.success || !res?.data?.access_token) {
+ throw new Error(res?.message || '登录失败');
+ }
+
+ setToken(res.data.access_token);
+ message.success('登录成功');
+ navigate('/users');
+ } catch (e: any) {
+ message.error(e?.message || '登录失败');
+ }
+ };
+
+ return (
+
+
+
+ 管理员登录
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/pages/UsersPage.tsx b/client/src/pages/UsersPage.tsx
new file mode 100644
index 0000000..c2c1a9e
--- /dev/null
+++ b/client/src/pages/UsersPage.tsx
@@ -0,0 +1,161 @@
+import { Button, Card, Form, Input, Modal, Space, Table, Typography, message } from 'antd';
+import { useEffect, useMemo, useState } from 'react';
+import { api } from '../lib/api';
+
+type UserRow = {
+ id: string;
+ username: string;
+ nickname: string;
+ email?: string;
+ email_verified: boolean;
+ phone?: string;
+ role: number;
+ created_at: string;
+};
+
+type ResetValues = {
+ newPassword: string;
+};
+
+export function UsersPage() {
+ const [loading, setLoading] = useState(false);
+ const [rows, setRows] = useState([]);
+ const [resetOpen, setResetOpen] = useState(false);
+ const [resetUserId, setResetUserId] = useState(null);
+ const [resetForm] = Form.useForm();
+
+ const columns = useMemo(
+ () => [
+ { title: 'ID', dataIndex: 'id', key: 'id', width: 90 },
+ { title: '用户名', dataIndex: 'username', key: 'username' },
+ { title: '昵称', dataIndex: 'nickname', key: 'nickname' },
+ { title: '邮箱', dataIndex: 'email', key: 'email' },
+ {
+ title: '邮箱验证',
+ dataIndex: 'email_verified',
+ key: 'email_verified',
+ render: (v: boolean) => (v ? '已验证' : '未验证'),
+ width: 100,
+ },
+ { title: '手机号', dataIndex: 'phone', key: 'phone' },
+ { title: '角色', dataIndex: 'role', key: 'role', width: 80 },
+ { title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 180 },
+ {
+ title: '操作',
+ key: 'actions',
+ width: 160,
+ render: (_: any, row: UserRow) => (
+
+
+
+ ),
+ },
+ ],
+ [resetForm],
+ );
+
+ const load = async () => {
+ setLoading(true);
+ try {
+ const res = await api.listUsers(200, 0);
+ const users = res?.data?.users || [];
+ setRows(
+ users.map((u: any) => ({
+ id: u.id,
+ username: u.username,
+ nickname: u.nickname,
+ email: u.email || undefined,
+ email_verified: Boolean(u.email_verified),
+ phone: u.phone || undefined,
+ role: u.role,
+ created_at: u.created_at,
+ })),
+ );
+ } catch (e: any) {
+ message.error(e?.message || '加载失败');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ void load();
+ }, []);
+
+ const onResetOk = async () => {
+ try {
+ const values = await resetForm.validateFields();
+ if (!resetUserId) return;
+
+ await api.resetUserPassword(resetUserId, values.newPassword);
+ message.success('密码已重置');
+ setResetOpen(false);
+ } catch (e: any) {
+ if (e?.errorFields) return;
+ message.error(e?.message || '重置失败');
+ }
+ };
+
+ return (
+
+
+
+
+ 用户管理
+
+
+
+
+
+
+
+ setResetOpen(false)}
+ okText="确认"
+ cancelText="取消"
+ >
+ {
+ const hasLetter = /[a-zA-Z]/.test(v || '');
+ const hasNumber = /\d/.test(v || '');
+ if (!v) return Promise.resolve();
+ if (!hasLetter || !hasNumber) return Promise.reject(new Error('必须包含字母和数字'));
+ return Promise.resolve();
+ },
+ },
+ ]}
+ >
+
+
+
+
+
+ );
+}
diff --git a/client/tsconfig.json b/client/tsconfig.json
new file mode 100644
index 0000000..cac61f2
--- /dev/null
+++ b/client/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ "strict": true,
+ "types": ["vite/client"]
+ },
+ "include": ["src"]
+}
diff --git a/client/vite.config.ts b/client/vite.config.ts
new file mode 100644
index 0000000..5c59447
--- /dev/null
+++ b/client/vite.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 5173,
+ },
+});
diff --git a/docs/api/api-documentation.md b/docs/api/api-documentation.md
index 3c5199d..e7a7567 100644
--- a/docs/api/api-documentation.md
+++ b/docs/api/api-documentation.md
@@ -409,4 +409,50 @@ curl -X POST http://localhost:3000/auth/reset-password \
## 更新日志
-- **v1.0.0** (2025-12-17): 初始版本,包含基础的用户认证功能
\ No newline at end of file
+- **v1.0.0** (2025-12-17): 初始版本,包含基础的用户认证功能
+## 管理员接口
+
+**注意**:所有管理员接口都需要在 Header 中携带 ,且用户角色必须为管理员 (role=9)。
+
+### 1. 获取用户列表
+
+**接口地址**: `GET /admin/users`
+
+**功能描述**: 分页获取所有注册用户列表
+
+#### 请求参数
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| page | number | 否 | 页码,默认1 |
+| limit | number | 否 | 每页数量,默认10 |
+
+### 2. 重置用户密码
+
+**接口地址**: `POST /admin/users/:id/reset-password`
+
+**功能描述**: 管理员强制重置指定用户的密码
+
+#### 请求参数
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| password | string | 是 | 新密码 |
+
+### 3. 删除用户
+
+**接口地址**: `DELETE /admin/users/:id`
+
+**功能描述**: 删除指定用户
+
+### 4. 修改用户角色
+
+**接口地址**: `POST /admin/users/:id/role`
+
+**功能描述**: 修改用户的角色权限
+
+#### 请求参数
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| role | number | 是 | 角色ID (1:普通用户, 9:管理员) |
diff --git a/docs/systems/admin-dashboard/README.md b/docs/systems/admin-dashboard/README.md
new file mode 100644
index 0000000..2206a0a
--- /dev/null
+++ b/docs/systems/admin-dashboard/README.md
@@ -0,0 +1,156 @@
+# 管理员后台(Admin Dashboard)
+
+本模块提供 Whale Town 的管理员后台能力,包含:
+
+- 管理员登录(role=9)
+- 用户列表管理
+- 用户密码重置
+
+> 说明:本项目用户系统原本的 `access_token` 为演示用 Base64 令牌。为了不影响现有用户端流程,管理员后台使用单独的签名 Token(HMAC-SHA256)做鉴权。
+
+---
+
+## 1. 管理员账号设计
+
+### 1.1 角色约定
+
+用户表 `users.role`:
+
+- `1`:普通用户
+- `9`:管理员(可访问后台)
+
+### 1.2 启动引导创建管理员(可选)
+
+通过环境变量启用启动引导:在服务启动时,如果不存在指定用户名的用户,则自动创建一个管理员账户(role=9)。
+
+在 `.env` 中配置:
+
+- `ADMIN_BOOTSTRAP_ENABLED=true`
+- `ADMIN_USERNAME=admin`
+- `ADMIN_PASSWORD=Admin123456`(需满足密码强度:至少8位,包含字母和数字)
+- `ADMIN_NICKNAME=管理员`(可选)
+
+注意:
+
+- 建议仅在首次部署/开发环境开启,引导创建成功后可关闭。
+- 生产环境务必设置强随机密码与强随机 `ADMIN_TOKEN_SECRET`。
+
+---
+
+## 2. 管理员鉴权 Token
+
+### 2.1 配置项
+
+- `ADMIN_TOKEN_SECRET`:签名密钥(至少16字符;生产环境建议≥32并随机)
+- `ADMIN_TOKEN_TTL_SECONDS`:Token 有效期(秒),默认 `28800`(8小时)
+
+### 2.2 使用方式
+
+管理员登录成功后返回 `access_token`,后续请求在 Header 中携带:
+
+- `Authorization: Bearer `
+
+---
+
+## 3. 后端接口
+
+### 3.1 管理员登录
+
+- `POST /admin/auth/login`
+
+请求:
+
+```json
+{
+ "identifier": "admin",
+ "password": "Admin123456"
+}
+```
+
+响应(成功):
+
+```json
+{
+ "success": true,
+ "data": {
+ "admin": { "id": "1", "username": "admin", "nickname": "管理员", "role": 9 },
+ "access_token": "...",
+ "expires_at": 1766102400000
+ },
+ "message": "管理员登录成功"
+}
+```
+
+### 3.2 用户列表
+
+- `GET /admin/users?limit=100&offset=0`
+- 需要管理员 Token
+
+### 3.3 用户详情
+
+- `GET /admin/users/:id`
+- 需要管理员 Token
+
+### 3.4 重置用户密码
+
+- `POST /admin/users/:id/reset-password`
+- 需要管理员 Token
+
+请求:
+
+```json
+{
+ "new_password": "NewPass1234"
+}
+```
+
+---
+
+## 4. 前端后台(Ant Design)
+
+前端工程位于 `client/`,使用 Vite + React + Ant Design。
+
+### 4.1 安装依赖
+
+在项目根目录执行:
+
+```bash
+pnpm install
+```
+
+### 4.2 启动后端
+
+```bash
+pnpm run dev
+```
+
+### 4.3 启动前端后台
+
+```bash
+pnpm -C client dev
+```
+
+默认访问:
+
+- 前端:`http://localhost:5173`
+- 后端:`http://localhost:3000`
+- Swagger:`http://localhost:3000/api-docs`
+
+### 4.4 前端配置
+
+- 复制 `client/.env.example` 为 `client/.env.local`
+- 可通过 `VITE_API_BASE_URL` 指向后端地址
+
+---
+
+## 5. 代码位置
+
+- 后端:
+ - `src/core/admin_core/`:管理员核心逻辑
+ - `src/core/guards/admin.guard.ts`:管理员接口鉴权
+ - `src/business/admin/`:管理员HTTP API
+ - `src/dto/admin*.ts`:管理员请求/响应 DTO
+
+- 前端:
+ - `client/src/pages/LoginPage.tsx`:管理员登录页
+ - `client/src/pages/UsersPage.tsx`:用户管理页(列表+重置密码)
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 1b87ff3..ec28fb7 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,3 +1,6 @@
+packages:
+ - 'client'
+
ignoredBuiltDependencies:
- '@nestjs/core'
- '@scarf/scarf'
diff --git a/src/app.module.ts b/src/app.module.ts
index 12d727f..4d0b2d2 100644
--- a/src/app.module.ts
+++ b/src/app.module.ts
@@ -8,6 +8,7 @@ import { UsersModule } from './core/db/users/users.module';
import { LoginCoreModule } from './core/login_core/login_core.module';
import { LoginModule } from './business/login/login.module';
import { RedisModule } from './core/redis/redis.module';
+import { AdminModule } from './business/admin/admin.module';
/**
* 检查数据库配置是否完整 by angjustinl 2025-12-17
@@ -61,6 +62,7 @@ function isDatabaseConfigured(): boolean {
isDatabaseConfigured() ? UsersModule.forDatabase() : UsersModule.forMemory(),
LoginCoreModule,
LoginModule,
+ AdminModule,
],
controllers: [AppController],
providers: [AppService],
diff --git a/src/business/admin/admin.controller.ts b/src/business/admin/admin.controller.ts
new file mode 100644
index 0000000..fb0bcc3
--- /dev/null
+++ b/src/business/admin/admin.controller.ts
@@ -0,0 +1,75 @@
+/**
+ * 管理员控制器
+ *
+ * API端点:
+ * - POST /admin/auth/login 管理员登录
+ * - GET /admin/users 用户列表(需要管理员Token)
+ * - GET /admin/users/:id 用户详情(需要管理员Token)
+ * - POST /admin/users/:id/reset-password 重置指定用户密码(需要管理员Token)
+ *
+ * @author jianuo
+ * @version 1.0.0
+ * @since 2025-12-19
+ */
+
+import { Body, Controller, Get, HttpCode, HttpStatus, Param, ParseIntPipe, Post, Query, UseGuards, ValidationPipe, UsePipes } from '@nestjs/common';
+import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
+import { AdminGuard } from '../../core/guards/admin.guard';
+import { AdminService } from './admin.service';
+import { AdminLoginDto, AdminResetPasswordDto } from '../../dto/admin.dto';
+import { AdminLoginResponseDto, AdminUsersResponseDto, AdminCommonResponseDto, AdminUserResponseDto } from '../../dto/admin_response.dto';
+
+@ApiTags('admin')
+@Controller('admin')
+export class AdminController {
+ constructor(private readonly adminService: AdminService) {}
+
+ @ApiOperation({ summary: '管理员登录', description: '仅允许 role=9 的账户登录后台' })
+ @ApiBody({ type: AdminLoginDto })
+ @ApiResponse({ status: 200, description: '登录成功', type: AdminLoginResponseDto })
+ @Post('auth/login')
+ @HttpCode(HttpStatus.OK)
+ @UsePipes(new ValidationPipe({ transform: true }))
+ async login(@Body() dto: AdminLoginDto) {
+ return await this.adminService.login(dto.identifier, dto.password);
+ }
+
+ @ApiBearerAuth('JWT-auth')
+ @ApiOperation({ summary: '获取用户列表', description: '后台用户管理:分页获取用户列表' })
+ @ApiQuery({ name: 'limit', required: false, description: '返回数量(默认100)' })
+ @ApiQuery({ name: 'offset', required: false, description: '偏移量(默认0)' })
+ @ApiResponse({ status: 200, description: '获取成功', type: AdminUsersResponseDto })
+ @UseGuards(AdminGuard)
+ @Get('users')
+ async listUsers(
+ @Query('limit') limit?: string,
+ @Query('offset') offset?: string,
+ ) {
+ const parsedLimit = limit ? Number(limit) : 100;
+ const parsedOffset = offset ? Number(offset) : 0;
+ return await this.adminService.listUsers(parsedLimit, parsedOffset);
+ }
+
+ @ApiBearerAuth('JWT-auth')
+ @ApiOperation({ summary: '获取用户详情' })
+ @ApiParam({ name: 'id', description: '用户ID' })
+ @ApiResponse({ status: 200, description: '获取成功', type: AdminUserResponseDto })
+ @UseGuards(AdminGuard)
+ @Get('users/:id')
+ async getUser(@Param('id') id: string) {
+ return await this.adminService.getUser(BigInt(id));
+ }
+
+ @ApiBearerAuth('JWT-auth')
+ @ApiOperation({ summary: '重置用户密码', description: '管理员直接为用户设置新密码(需满足密码强度规则)' })
+ @ApiParam({ name: 'id', description: '用户ID' })
+ @ApiBody({ type: AdminResetPasswordDto })
+ @ApiResponse({ status: 200, description: '重置成功', type: AdminCommonResponseDto })
+ @UseGuards(AdminGuard)
+ @Post('users/:id/reset-password')
+ @HttpCode(HttpStatus.OK)
+ @UsePipes(new ValidationPipe({ transform: true }))
+ async resetPassword(@Param('id') id: string, @Body() dto: AdminResetPasswordDto) {
+ return await this.adminService.resetPassword(BigInt(id), dto.new_password);
+ }
+}
diff --git a/src/business/admin/admin.module.ts b/src/business/admin/admin.module.ts
new file mode 100644
index 0000000..3090700
--- /dev/null
+++ b/src/business/admin/admin.module.ts
@@ -0,0 +1,24 @@
+/**
+ * 管理员业务模块
+ *
+ * 功能描述:
+ * - 提供后台管理的HTTP API(管理员登录、用户管理、密码重置等)
+ * - 仅负责HTTP层与业务流程编排
+ * - 核心鉴权与密码策略由 AdminCoreService 提供
+ *
+ * @author jianuo
+ * @version 1.0.0
+ * @since 2025-12-19
+ */
+
+import { Module } from '@nestjs/common';
+import { AdminCoreModule } from '../../core/admin_core/admin_core.module';
+import { AdminController } from './admin.controller';
+import { AdminService } from './admin.service';
+
+@Module({
+ imports: [AdminCoreModule],
+ controllers: [AdminController],
+ providers: [AdminService],
+})
+export class AdminModule {}
diff --git a/src/business/admin/admin.service.ts b/src/business/admin/admin.service.ts
new file mode 100644
index 0000000..80b2503
--- /dev/null
+++ b/src/business/admin/admin.service.ts
@@ -0,0 +1,100 @@
+/**
+ * 管理员业务服务
+ *
+ * 功能描述:
+ * - 调用核心服务完成管理员登录
+ * - 提供用户列表查询
+ * - 提供用户密码重置能力
+ *
+ * @author jianuo
+ * @version 1.0.0
+ * @since 2025-12-19
+ */
+
+import { Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
+import { AdminCoreService } from '../../core/admin_core/admin_core.service';
+import { Users } from '../../core/db/users/users.entity';
+import { UsersService } from '../../core/db/users/users.service';
+import { UsersMemoryService } from '../../core/db/users/users_memory.service';
+
+export interface AdminApiResponse {
+ success: boolean;
+ data?: T;
+ message: string;
+ error_code?: string;
+}
+
+@Injectable()
+export class AdminService {
+ private readonly logger = new Logger(AdminService.name);
+
+ constructor(
+ private readonly adminCoreService: AdminCoreService,
+ @Inject('UsersService') private readonly usersService: UsersService | UsersMemoryService,
+ ) {}
+
+ async login(identifier: string, password: string): Promise {
+ try {
+ const result = await this.adminCoreService.login({ identifier, password });
+ return { success: true, data: result, message: '管理员登录成功' };
+ } catch (error) {
+ this.logger.error(`管理员登录失败: ${identifier}`, error instanceof Error ? error.stack : String(error));
+ return {
+ success: false,
+ message: error instanceof Error ? error.message : '管理员登录失败',
+ error_code: 'ADMIN_LOGIN_FAILED',
+ };
+ }
+ }
+
+ async listUsers(limit: number, offset: number): Promise> {
+ const users = await this.usersService.findAll(limit, offset);
+ return {
+ success: true,
+ data: {
+ users: users.map((u: Users) => this.formatUser(u)),
+ limit,
+ offset,
+ },
+ message: '用户列表获取成功',
+ };
+ }
+
+ async getUser(id: bigint): Promise> {
+ const user = await this.usersService.findOne(id);
+ return {
+ success: true,
+ data: { user: this.formatUser(user) },
+ message: '用户信息获取成功',
+ };
+ }
+
+ async resetPassword(id: bigint, newPassword: string): Promise {
+ // 确认用户存在
+ const user = await this.usersService.findOne(id).catch((): null => null);
+ if (!user) {
+ throw new NotFoundException('用户不存在');
+ }
+
+ await this.adminCoreService.resetUserPassword(id, newPassword);
+
+ this.logger.log(`管理员重置密码成功: userId=${id.toString()}`);
+
+ return { success: true, message: '密码重置成功' };
+ }
+
+ private formatUser(user: Users) {
+ return {
+ id: user.id.toString(),
+ username: user.username,
+ nickname: user.nickname,
+ email: user.email,
+ email_verified: user.email_verified,
+ phone: user.phone,
+ avatar_url: user.avatar_url,
+ role: user.role,
+ created_at: user.created_at,
+ updated_at: user.updated_at,
+ };
+ }
+}
diff --git a/src/core/admin_core/admin_core.module.ts b/src/core/admin_core/admin_core.module.ts
new file mode 100644
index 0000000..2a31a83
--- /dev/null
+++ b/src/core/admin_core/admin_core.module.ts
@@ -0,0 +1,27 @@
+/**
+ * 管理员核心模块
+ *
+ * 功能描述:
+ * - 提供管理员登录鉴权能力(签名Token)
+ * - 提供管理员账户启动引导(可选)
+ * - 为业务层 AdminModule 提供可复用的核心服务
+ *
+ * 依赖模块:
+ * - UsersModule: 用户数据访问(数据库/内存双模式)
+ * - ConfigModule: 环境变量与配置读取
+ *
+ * @author jianuo
+ * @version 1.0.0
+ * @since 2025-12-19
+ */
+
+import { Module } from '@nestjs/common';
+import { ConfigModule } from '@nestjs/config';
+import { AdminCoreService } from './admin_core.service';
+
+@Module({
+ imports: [ConfigModule],
+ providers: [AdminCoreService],
+ exports: [AdminCoreService],
+})
+export class AdminCoreModule {}
diff --git a/src/core/admin_core/admin_core.service.ts b/src/core/admin_core/admin_core.service.ts
new file mode 100644
index 0000000..f3c4848
--- /dev/null
+++ b/src/core/admin_core/admin_core.service.ts
@@ -0,0 +1,285 @@
+/**
+ * 管理员核心服务
+ *
+ * 功能描述:
+ * - 管理员登录校验(仅允许 role=9)
+ * - 生成/验证管理员签名Token(HMAC-SHA256)
+ * - 启动时可选引导创建管理员账号(通过环境变量启用)
+ *
+ * 安全说明:
+ * - 本服务生成的Token为对称签名Token(非JWT标准实现),但具备有效期与签名校验
+ * - 仅用于后台管理用途;生产环境务必配置强随机的 ADMIN_TOKEN_SECRET
+ *
+ * @author jianuo
+ * @version 1.0.0
+ * @since 2025-12-19
+ */
+
+import { BadRequestException, Inject, Injectable, Logger, OnModuleInit, UnauthorizedException } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
+import * as bcrypt from 'bcrypt';
+import * as crypto from 'crypto';
+import { Users } from '../db/users/users.entity';
+import { UsersService } from '../db/users/users.service';
+import { UsersMemoryService } from '../db/users/users_memory.service';
+
+export interface AdminLoginRequest {
+ identifier: string;
+ password: string;
+}
+
+export interface AdminAuthPayload {
+ adminId: string;
+ username: string;
+ role: number;
+ iat: number;
+ exp: number;
+}
+
+export interface AdminLoginResult {
+ admin: {
+ id: string;
+ username: string;
+ nickname: string;
+ role: number;
+ };
+ access_token: string;
+ expires_at: number;
+}
+
+@Injectable()
+export class AdminCoreService implements OnModuleInit {
+ private readonly logger = new Logger(AdminCoreService.name);
+
+ constructor(
+ private readonly configService: ConfigService,
+ @Inject('UsersService') private readonly usersService: UsersService | UsersMemoryService,
+ ) {}
+
+ async onModuleInit(): Promise {
+ await this.bootstrapAdminIfEnabled();
+ }
+
+ /**
+ * 管理员登录
+ */
+ async login(request: AdminLoginRequest): Promise {
+ const { identifier, password } = request;
+
+ const adminUser = await this.findUserByIdentifier(identifier);
+ if (!adminUser) {
+ throw new UnauthorizedException('管理员账号不存在');
+ }
+
+ if (adminUser.role !== 9) {
+ throw new UnauthorizedException('无管理员权限');
+ }
+
+ if (!adminUser.password_hash) {
+ throw new UnauthorizedException('管理员账户未设置密码,无法登录');
+ }
+
+ const ok = await bcrypt.compare(password, adminUser.password_hash);
+ if (!ok) {
+ throw new UnauthorizedException('密码错误');
+ }
+
+ const ttlSeconds = this.getAdminTokenTtlSeconds();
+ const now = Date.now();
+ const payload: AdminAuthPayload = {
+ adminId: adminUser.id.toString(),
+ username: adminUser.username,
+ role: adminUser.role,
+ iat: now,
+ exp: now + ttlSeconds * 1000,
+ };
+
+ const token = this.signPayload(payload);
+
+ return {
+ admin: {
+ id: adminUser.id.toString(),
+ username: adminUser.username,
+ nickname: adminUser.nickname,
+ role: adminUser.role,
+ },
+ access_token: token,
+ expires_at: payload.exp,
+ };
+ }
+
+ /**
+ * 校验管理员Token并返回Payload
+ */
+ verifyToken(token: string): AdminAuthPayload {
+ const secret = this.getAdminTokenSecret();
+ const [payloadPart, signaturePart] = token.split('.');
+
+ if (!payloadPart || !signaturePart) {
+ throw new UnauthorizedException('Token格式错误');
+ }
+
+ const expected = this.hmacSha256Base64Url(payloadPart, secret);
+ if (!this.safeEqual(signaturePart, expected)) {
+ throw new UnauthorizedException('Token签名无效');
+ }
+
+ const payloadJson = Buffer.from(this.base64UrlToBase64(payloadPart), 'base64').toString('utf-8');
+ let payload: AdminAuthPayload;
+
+ try {
+ payload = JSON.parse(payloadJson) as AdminAuthPayload;
+ } catch {
+ throw new UnauthorizedException('Token解析失败');
+ }
+
+ if (!payload?.adminId || payload.role !== 9) {
+ throw new UnauthorizedException('无管理员权限');
+ }
+
+ if (typeof payload.exp !== 'number' || Date.now() > payload.exp) {
+ throw new UnauthorizedException('Token已过期');
+ }
+
+ return payload;
+ }
+
+ /**
+ * 管理员重置用户密码(直接设置新密码)
+ */
+ async resetUserPassword(userId: bigint, newPassword: string): Promise {
+ this.validatePasswordStrength(newPassword);
+
+ const passwordHash = await this.hashPassword(newPassword);
+ await this.usersService.update(userId, { password_hash: passwordHash });
+ }
+
+ private async findUserByIdentifier(identifier: string): Promise {
+ const byUsername = await this.usersService.findByUsername(identifier);
+ if (byUsername) return byUsername;
+
+ if (this.isEmail(identifier)) {
+ const byEmail = await this.usersService.findByEmail(identifier);
+ if (byEmail) return byEmail;
+ }
+
+ if (this.isPhoneNumber(identifier)) {
+ const users = await this.usersService.findAll(1000, 0);
+ return users.find((u: Users) => u.phone === identifier) || null;
+ }
+
+ return null;
+ }
+
+ private async bootstrapAdminIfEnabled(): Promise {
+ const enabled = this.configService.get('ADMIN_BOOTSTRAP_ENABLED', 'false') === 'true';
+ if (!enabled) return;
+
+ const username = this.configService.get('ADMIN_USERNAME');
+ const password = this.configService.get('ADMIN_PASSWORD');
+ const nickname = this.configService.get('ADMIN_NICKNAME', '管理员');
+
+ if (!username || !password) {
+ this.logger.warn('已启用管理员引导,但未配置 ADMIN_USERNAME / ADMIN_PASSWORD,跳过创建');
+ return;
+ }
+
+ const existing = await this.usersService.findByUsername(username);
+ if (existing) {
+ if (existing.role !== 9) {
+ this.logger.warn(`管理员引导发现同名用户但role!=9:${username},跳过`);
+ }
+ return;
+ }
+
+ this.validatePasswordStrength(password);
+ const passwordHash = await this.hashPassword(password);
+
+ await this.usersService.create({
+ username,
+ password_hash: passwordHash,
+ nickname,
+ role: 9,
+ email_verified: true,
+ });
+
+ this.logger.log(`管理员账号已创建:${username} (role=9)`);
+ }
+
+ private getAdminTokenSecret(): string {
+ const secret = this.configService.get('ADMIN_TOKEN_SECRET');
+ if (!secret || secret.length < 16) {
+ throw new BadRequestException('ADMIN_TOKEN_SECRET 未配置或过短(至少16字符)');
+ }
+ return secret;
+ }
+
+ private getAdminTokenTtlSeconds(): number {
+ const raw = this.configService.get('ADMIN_TOKEN_TTL_SECONDS', '28800'); // 8h
+ const parsed = Number(raw);
+ if (!Number.isFinite(parsed) || parsed <= 0) {
+ return 28800;
+ }
+ return parsed;
+ }
+
+ private signPayload(payload: AdminAuthPayload): string {
+ const secret = this.getAdminTokenSecret();
+ const payloadJson = JSON.stringify(payload);
+ const payloadPart = this.base64ToBase64Url(Buffer.from(payloadJson, 'utf-8').toString('base64'));
+ const signature = this.hmacSha256Base64Url(payloadPart, secret);
+ return `${payloadPart}.${signature}`;
+ }
+
+ private hmacSha256Base64Url(data: string, secret: string): string {
+ const digest = crypto.createHmac('sha256', secret).update(data).digest('base64');
+ return this.base64ToBase64Url(digest);
+ }
+
+ private base64ToBase64Url(base64: string): string {
+ return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
+ }
+
+ private base64UrlToBase64(base64Url: string): string {
+ const padded = base64Url.replace(/-/g, '+').replace(/_/g, '/');
+ const padLen = (4 - (padded.length % 4)) % 4;
+ return padded + '='.repeat(padLen);
+ }
+
+ private safeEqual(a: string, b: string): boolean {
+ const aBuf = Buffer.from(a);
+ const bBuf = Buffer.from(b);
+ if (aBuf.length !== bBuf.length) return false;
+ return crypto.timingSafeEqual(aBuf, bBuf);
+ }
+
+ private isEmail(value: string): boolean {
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
+ }
+
+ private isPhoneNumber(value: string): boolean {
+ return /^\+?[0-9\-\s]{6,20}$/.test(value);
+ }
+
+ private validatePasswordStrength(password: string): void {
+ if (password.length < 8) {
+ throw new BadRequestException('密码长度至少8位');
+ }
+
+ if (password.length > 128) {
+ throw new BadRequestException('密码长度不能超过128位');
+ }
+
+ const hasLetter = /[a-zA-Z]/.test(password);
+ const hasNumber = /\d/.test(password);
+
+ if (!hasLetter || !hasNumber) {
+ throw new BadRequestException('密码必须包含字母和数字');
+ }
+ }
+
+ private async hashPassword(password: string): Promise {
+ const saltRounds = 12;
+ return await bcrypt.hash(password, saltRounds);
+ }
+}
diff --git a/src/core/guards/admin.guard.ts b/src/core/guards/admin.guard.ts
new file mode 100644
index 0000000..d2c8296
--- /dev/null
+++ b/src/core/guards/admin.guard.ts
@@ -0,0 +1,43 @@
+/**
+ * 管理员鉴权守卫
+ *
+ * 功能描述:
+ * - 保护后台管理接口
+ * - 校验 Authorization: Bearer
+ * - 仅允许 role=9 的管理员访问
+ *
+ * @author jianuo
+ * @version 1.0.0
+ * @since 2025-12-19
+ */
+
+import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
+import { Request } from 'express';
+import { AdminCoreService, AdminAuthPayload } from '../admin_core/admin_core.service';
+
+export interface AdminRequest extends Request {
+ admin?: AdminAuthPayload;
+}
+
+@Injectable()
+export class AdminGuard implements CanActivate {
+ constructor(private readonly adminCoreService: AdminCoreService) {}
+
+ canActivate(context: ExecutionContext): boolean {
+ const req = context.switchToHttp().getRequest();
+ const auth = req.headers['authorization'];
+
+ if (!auth || Array.isArray(auth)) {
+ throw new UnauthorizedException('缺少Authorization头');
+ }
+
+ const [scheme, token] = auth.split(' ');
+ if (scheme !== 'Bearer' || !token) {
+ throw new UnauthorizedException('Authorization格式错误');
+ }
+
+ const payload = this.adminCoreService.verifyToken(token);
+ req.admin = payload;
+ return true;
+ }
+}
diff --git a/src/core/login_core/login_core.service.spec.ts b/src/core/login_core/login_core.service.spec.ts
index 6d7c47a..0c95a79 100644
--- a/src/core/login_core/login_core.service.spec.ts
+++ b/src/core/login_core/login_core.service.spec.ts
@@ -55,7 +55,7 @@ describe('LoginCoreService', () => {
providers: [
LoginCoreService,
{
- provide: UsersService,
+ provide: 'UsersService',
useValue: mockUsersService,
},
{
@@ -70,7 +70,7 @@ describe('LoginCoreService', () => {
}).compile();
service = module.get(LoginCoreService);
- usersService = module.get(UsersService);
+ usersService = module.get('UsersService');
emailService = module.get(EmailService);
verificationService = module.get(VerificationService);
});
diff --git a/src/dto/admin.dto.ts b/src/dto/admin.dto.ts
new file mode 100644
index 0000000..420f387
--- /dev/null
+++ b/src/dto/admin.dto.ts
@@ -0,0 +1,34 @@
+/**
+ * 管理员相关 DTO
+ *
+ * 功能描述:
+ * - 定义管理员登录与用户密码重置的请求结构
+ * - 使用 class-validator 进行参数校验
+ *
+ * @author jianuo
+ * @version 1.0.0
+ * @since 2025-12-19
+ */
+
+import { ApiProperty } from '@nestjs/swagger';
+import { IsNotEmpty, IsString, MinLength } from 'class-validator';
+
+export class AdminLoginDto {
+ @ApiProperty({ description: '登录标识符(用户名/邮箱/手机号)', example: 'admin' })
+ @IsString()
+ @IsNotEmpty()
+ identifier: string;
+
+ @ApiProperty({ description: '密码', example: 'Admin123456' })
+ @IsString()
+ @IsNotEmpty()
+ password: string;
+}
+
+export class AdminResetPasswordDto {
+ @ApiProperty({ description: '新密码(至少8位,包含字母和数字)', example: 'NewPass1234' })
+ @IsString()
+ @IsNotEmpty()
+ @MinLength(8)
+ new_password: string;
+}
diff --git a/src/dto/admin_response.dto.ts b/src/dto/admin_response.dto.ts
new file mode 100644
index 0000000..07f7dc0
--- /dev/null
+++ b/src/dto/admin_response.dto.ts
@@ -0,0 +1,139 @@
+/**
+ * 管理员相关响应 DTO
+ *
+ * 功能描述:
+ * - 为 Swagger 提供明确的响应结构定义
+ * - 与 AdminService 返回结构保持一致
+ *
+ * @author jianuo
+ * @version 1.0.0
+ * @since 2025-12-19
+ */
+
+import { ApiProperty } from '@nestjs/swagger';
+
+class AdminInfoDto {
+ @ApiProperty({ example: '1' })
+ id: string;
+
+ @ApiProperty({ example: 'admin' })
+ username: string;
+
+ @ApiProperty({ example: '管理员' })
+ nickname: string;
+
+ @ApiProperty({ example: 9 })
+ role: number;
+}
+
+class AdminLoginDataDto {
+ @ApiProperty({ type: AdminInfoDto })
+ admin: AdminInfoDto;
+
+ @ApiProperty({ description: '管理员访问Token(用于Authorization Bearer)' })
+ access_token: string;
+
+ @ApiProperty({ description: '过期时间戳(毫秒)', example: 1766102400000 })
+ expires_at: number;
+}
+
+export class AdminLoginResponseDto {
+ @ApiProperty({ example: true })
+ success: boolean;
+
+ @ApiProperty({ type: AdminLoginDataDto, required: false })
+ data?: AdminLoginDataDto;
+
+ @ApiProperty({ example: '管理员登录成功' })
+ message: string;
+
+ @ApiProperty({ required: false, example: 'ADMIN_LOGIN_FAILED' })
+ error_code?: string;
+}
+
+class AdminUserDto {
+ @ApiProperty({ example: '1' })
+ id: string;
+
+ @ApiProperty({ example: 'user1' })
+ username: string;
+
+ @ApiProperty({ example: '小明' })
+ nickname: string;
+
+ @ApiProperty({ required: false, example: 'user1@example.com', nullable: true })
+ email?: string;
+
+ @ApiProperty({ example: false })
+ email_verified: boolean;
+
+ @ApiProperty({ required: false, example: '+8613800138000', nullable: true })
+ phone?: string;
+
+ @ApiProperty({ required: false, example: 'https://example.com/avatar.png', nullable: true })
+ avatar_url?: string;
+
+ @ApiProperty({ example: 1 })
+ role: number;
+
+ @ApiProperty({ example: '2025-12-19T00:00:00.000Z' })
+ created_at: Date;
+
+ @ApiProperty({ example: '2025-12-19T00:00:00.000Z' })
+ updated_at: Date;
+}
+
+class AdminUsersDataDto {
+ @ApiProperty({ type: [AdminUserDto] })
+ users: AdminUserDto[];
+
+ @ApiProperty({ example: 100 })
+ limit: number;
+
+ @ApiProperty({ example: 0 })
+ offset: number;
+}
+
+export class AdminUsersResponseDto {
+ @ApiProperty({ example: true })
+ success: boolean;
+
+ @ApiProperty({ type: AdminUsersDataDto, required: false })
+ data?: AdminUsersDataDto;
+
+ @ApiProperty({ example: '用户列表获取成功' })
+ message: string;
+
+ @ApiProperty({ required: false, example: 'ADMIN_USERS_FAILED' })
+ error_code?: string;
+}
+
+class AdminUserDataDto {
+ @ApiProperty({ type: AdminUserDto })
+ user: AdminUserDto;
+}
+
+export class AdminUserResponseDto {
+ @ApiProperty({ example: true })
+ success: boolean;
+
+ @ApiProperty({ type: AdminUserDataDto, required: false })
+ data?: AdminUserDataDto;
+
+ @ApiProperty({ example: '用户信息获取成功' })
+ message: string;
+}
+
+export class AdminCommonResponseDto {
+ @ApiProperty({ example: true })
+ success: boolean;
+
+ @ApiProperty({ required: false })
+ data?: any;
+
+ @ApiProperty({ example: '密码重置成功' })
+ message: string;
+
+ @ApiProperty({ required: false, example: 'ADMIN_OPERATION_FAILED' })
+ error_code?: string;
+}
diff --git a/src/main.ts b/src/main.ts
index 279f830..a917c46 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -39,6 +39,12 @@ async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: ['error', 'warn', 'log'],
});
+
+ // 允许前端后台(如Vite/React)跨域访问
+ app.enableCors({
+ origin: true,
+ credentials: true,
+ });
// 全局启用校验管道(核心配置)
app.useGlobalPipes(
@@ -55,6 +61,7 @@ async function bootstrap() {
.setDescription('像素游戏服务器API文档 - 包含用户认证、登录注册等功能')
.setVersion('1.0.0')
.addTag('auth', '用户认证相关接口')
+ .addTag('admin', '管理员后台相关接口')
.addBearerAuth(
{
type: 'http',