From ebd4b5faa9dd8a3826cbe7ffddd3b7a54510378d Mon Sep 17 00:00:00 2001
From: zhang1106 <849185023@qq.com>
Date: Thu, 5 Feb 2026 15:12:31 +0800
Subject: [PATCH] =?UTF-8?q?feat(=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86):=20?=
=?UTF-8?q?=E5=AE=9E=E7=8E=B0=E7=94=A8=E6=88=B7=E6=B3=A8=E5=86=8C=E5=AE=A1?=
=?UTF-8?q?=E6=A0=B8=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
添加用户注册审核流程,包括以下变更:
- 用户注册后状态为 pending 待审核
- 管理员可批准或拒绝注册申请
- 待审核用户无法登录系统
- 用户管理页面添加审核操作和状态筛选
- 添加数据库迁移脚本支持 pending 状态
- 更新前后端相关接口和页面逻辑
---
README.md | 128 +------------
backend/models/User.js | 2 +-
backend/routes/auth.js | 64 +++++--
backend/routes/users.js | 78 ++++++++
backend/scripts/migrate-add-pending-status.js | 119 ++++++++++++
frontend/src/api/index.js | 4 +-
frontend/src/context/AuthContext.jsx | 16 +-
frontend/src/pages/Login.jsx | 18 +-
frontend/src/pages/UserManagement.jsx | 180 +++++++++++++-----
9 files changed, 404 insertions(+), 205 deletions(-)
create mode 100644 backend/scripts/migrate-add-pending-status.js
diff --git a/README.md b/README.md
index abf44a8..4f7859c 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,6 @@
## 代码仓库
-- **GitHub**: https://github.com/gituib/idc_assest
- **Gitee**: https://gitee.com/zhang96110/idc_assest
## 系统概述
@@ -119,7 +118,7 @@ jigui/
```bash
# 克隆项目
-git clone https://github.com/gituib/idc_assest.git
+git clone https://gitee.com/zhang96110/idc_assest.git
cd idc_assest
# 运行交互式安装脚本
@@ -192,7 +191,7 @@ node install.js
```bash
# 1. 克隆项目
-git clone https://github.com/gituib/idc_assest.git
+git clone https://gitee.com/zhang96110/idc_assest.git
cd idc_assest
# 2. 安装后端依赖
@@ -221,129 +220,6 @@ npm run dev
- 后端API:http://localhost:8000/api
- 健康检查:http://localhost:8000/health
-## 安装部署脚本
-
-项目提供多个实用脚本,简化安装、更新、卸载等操作:
-
-### 脚本列表
-
-| 脚本 | 命令 | 功能说明 |
-|------|------|----------|
-| **install.js** | `npm run deploy` / `node install.js` | 交互式安装部署脚本 |
-| **update.js** | `npm run update` / `node update.js` | 一键更新脚本 |
-| **uninstall.js** | `node uninstall.js` | 卸载清理脚本 |
-| **check.js** | `node check.js` | 环境检查脚本 |
-| **modify.js** | `node modify.js` | 配置修改脚本 |
-
-### install.js - 交互式安装部署
-
-**功能特性:**
-- ✅ 自动检测 Node.js、npm、PM2、Nginx 环境
-- ✅ **Linux 支持自动安装 Node.js**(交互式)
-- ✅ 交互式配置数据库(SQLite/MySQL)
-- ✅ 交互式选择运行环境(development/production)
-- ✅ 交互式选择前端部署方式(Nginx/PM2 serve)
-- ✅ 自动安装项目依赖
-- ✅ 自动初始化数据库
-- ✅ 自动构建前端项目
-- ✅ 使用 PM2 启动和管理服务
-
-**使用方式:**
-```bash
-# 方式一:使用 npm 命令
-npm run deploy
-
-# 方式二:直接运行脚本
-node install.js
-```
-
-### update.js - 一键更新
-
-**功能特性:**
-- ✅ 自动备份数据
-- ✅ 拉取最新代码
-- ✅ 更新前后端依赖
-- ✅ 重建前端项目
-- ✅ 重启服务
-
-**使用方式:**
-```bash
-# 方式一:使用 npm 命令
-npm run update
-
-# 方式二:直接运行脚本
-node update.js
-```
-
-### uninstall.js - 卸载清理
-
-**功能特性:**
-- ✅ 停止 PM2 服务
-- ✅ 删除 PM2 进程配置
-- ✅ 清理 Nginx 配置(可选)
-- ✅ 备份数据(可选)
-- ✅ 清理日志文件(可选)
-
-**使用方式:**
-```bash
-node uninstall.js
-```
-
-**卸载流程:**
-```
-▶ 停止服务
-✓ 已停止 idc-backend
-✓ 已停止 idc-frontend
-
-▶ 删除 PM2 配置
-✓ 已删除 PM2 进程
-
-▶ 清理 Nginx 配置
-是否删除 Nginx 配置? (y/N): n
-
-▶ 数据备份
-是否备份数据库? (Y/n): y
-✓ 数据库已备份到 backup/database_20240205_143022.sql
-
-▶ 清理完成
-✓ 卸载完成,感谢使用!
-```
-
-**注意事项:**
-- 卸载前建议备份数据
-- 默认保留数据库文件,可手动删除
-- 上传的文件(avatars、uploads)需手动清理
-
-### check.js - 环境检查
-
-**功能特性:**
-- ✅ 检查 Node.js 版本
-- ✅ 检查 npm 版本
-- ✅ 检查 PM2 安装状态
-- ✅ 检查 Nginx 安装状态
-- ✅ 检查端口占用情况
-- ✅ 检查磁盘空间
-
-**使用方式:**
-```bash
-node check.js
-```
-
-### modify.js - 配置修改
-
-**功能特性:**
-- ✅ 修改数据库配置
-- ✅ 修改服务端口
-- ✅ 修改运行环境
-- ✅ 修改前端部署方式
-
-**使用方式:**
-```bash
-node modify.js
-```
-
----
-
## 更新升级
### 一键更新(推荐)⭐
diff --git a/backend/models/User.js b/backend/models/User.js
index c960569..fe9e0aa 100644
--- a/backend/models/User.js
+++ b/backend/models/User.js
@@ -36,7 +36,7 @@ const User = sequelize.define('User', {
allowNull: true
},
status: {
- type: DataTypes.ENUM('active', 'inactive', 'locked'),
+ type: DataTypes.ENUM('active', 'inactive', 'locked', 'pending'),
defaultValue: 'active'
},
lastLoginTime: {
diff --git a/backend/routes/auth.js b/backend/routes/auth.js
index 29d833a..113a15a 100644
--- a/backend/routes/auth.js
+++ b/backend/routes/auth.js
@@ -58,12 +58,12 @@ router.post('/register', async (req, res) => {
email,
phone,
realName: realName || username,
- status: 'active'
+ status: isFirstUser ? 'active' : 'pending'
});
if (isFirstUser) {
let adminRole = await Role.findOne({ where: { roleCode: 'admin' } });
-
+
if (!adminRole) {
adminRole = await Role.create({
roleId: 'role_admin',
@@ -74,37 +74,53 @@ router.post('/register', async (req, res) => {
permissions: []
});
}
-
+
await UserRole.create({
UserId: user.userId,
RoleId: adminRole.roleId
});
+
+ const token = generateToken(user);
+
+ res.status(201).json({
+ success: true,
+ message: '注册成功,已为您分配管理员权限',
+ data: {
+ user: {
+ userId: user.userId,
+ username: user.username,
+ email: user.email,
+ realName: user.realName
+ },
+ token,
+ isFirstUser: true
+ }
+ });
} else {
const defaultRole = await Role.findOne({ where: { roleCode: 'viewer' } });
-
+
if (defaultRole) {
await UserRole.create({
UserId: user.userId,
RoleId: defaultRole.roleId
});
}
+
+ res.status(201).json({
+ success: true,
+ message: '注册成功,请等待管理员审核',
+ data: {
+ user: {
+ userId: user.userId,
+ username: user.username,
+ email: user.email,
+ realName: user.realName
+ },
+ isFirstUser: false,
+ pendingApproval: true
+ }
+ });
}
-
- const token = generateToken(user);
-
- res.status(201).json({
- success: true,
- message: isFirstUser ? '注册成功,已为您分配管理员权限' : '注册成功',
- data: {
- user: {
- userId: user.userId,
- username: user.username,
- email: user.email,
- realName: user.realName
- },
- token
- }
- });
} catch (error) {
console.error('注册错误:', error);
res.status(500).json({
@@ -148,6 +164,14 @@ router.post('/login', async (req, res) => {
});
}
+ if (user.status === 'pending') {
+ return res.status(403).json({
+ success: false,
+ code: 'PENDING_APPROVAL',
+ message: '账户待审核,请联系管理员激活'
+ });
+ }
+
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
user.loginCount = (user.loginCount || 0) + 1;
diff --git a/backend/routes/users.js b/backend/routes/users.js
index 5958cac..032c070 100644
--- a/backend/routes/users.js
+++ b/backend/routes/users.js
@@ -465,4 +465,82 @@ router.delete('/:userId/avatar', authMiddleware, async (req, res) => {
}
});
+router.put('/:userId/approve', authMiddleware, async (req, res) => {
+ try {
+ const user = await User.findByPk(req.params.userId);
+
+ if (!user) {
+ return res.status(404).json({
+ success: false,
+ message: '用户不存在'
+ });
+ }
+
+ if (user.status !== 'pending') {
+ return res.status(400).json({
+ success: false,
+ message: '该用户不在待审核状态'
+ });
+ }
+
+ user.status = 'active';
+ await user.save();
+
+ res.json({
+ success: true,
+ message: '用户审核通过',
+ data: {
+ userId: user.userId,
+ username: user.username,
+ status: user.status
+ }
+ });
+ } catch (error) {
+ console.error('审核用户错误:', error);
+ res.status(500).json({
+ success: false,
+ message: '审核用户失败'
+ });
+ }
+});
+
+router.put('/:userId/reject', authMiddleware, async (req, res) => {
+ try {
+ const user = await User.findByPk(req.params.userId);
+
+ if (!user) {
+ return res.status(404).json({
+ success: false,
+ message: '用户不存在'
+ });
+ }
+
+ if (user.status !== 'pending') {
+ return res.status(400).json({
+ success: false,
+ message: '该用户不在待审核状态'
+ });
+ }
+
+ user.status = 'inactive';
+ await user.save();
+
+ res.json({
+ success: true,
+ message: '已拒绝该用户的注册申请',
+ data: {
+ userId: user.userId,
+ username: user.username,
+ status: user.status
+ }
+ });
+ } catch (error) {
+ console.error('拒绝用户错误:', error);
+ res.status(500).json({
+ success: false,
+ message: '操作失败'
+ });
+ }
+});
+
module.exports = router;
diff --git a/backend/scripts/migrate-add-pending-status.js b/backend/scripts/migrate-add-pending-status.js
new file mode 100644
index 0000000..bc13c03
--- /dev/null
+++ b/backend/scripts/migrate-add-pending-status.js
@@ -0,0 +1,119 @@
+const { sequelize } = require('../db');
+
+async function migrate() {
+ try {
+ console.log('开始迁移:添加 pending 状态到用户表...');
+
+ const queryInterface = sequelize.getQueryInterface();
+ const dialect = sequelize.getDialect();
+
+ if (dialect === 'sqlite') {
+ // SQLite 不支持 ALTER COLUMN,需要重建表
+ console.log('检测到 SQLite 数据库,使用重建表方式迁移...');
+
+ // 1. 创建新表
+ await queryInterface.createTable('users_new', {
+ userId: {
+ type: sequelize.Sequelize.STRING,
+ primaryKey: true,
+ allowNull: false
+ },
+ username: {
+ type: sequelize.Sequelize.STRING,
+ allowNull: false,
+ unique: true
+ },
+ password: {
+ type: sequelize.Sequelize.STRING,
+ allowNull: false
+ },
+ email: {
+ type: sequelize.Sequelize.STRING,
+ allowNull: true
+ },
+ phone: {
+ type: sequelize.Sequelize.STRING,
+ allowNull: true
+ },
+ realName: {
+ type: sequelize.Sequelize.STRING,
+ allowNull: true
+ },
+ avatar: {
+ type: sequelize.Sequelize.STRING,
+ allowNull: true
+ },
+ status: {
+ type: sequelize.Sequelize.ENUM('active', 'inactive', 'locked', 'pending'),
+ defaultValue: 'active'
+ },
+ lastLoginTime: {
+ type: sequelize.Sequelize.DATE,
+ allowNull: true
+ },
+ lastLoginIp: {
+ type: sequelize.Sequelize.STRING,
+ allowNull: true
+ },
+ loginCount: {
+ type: sequelize.Sequelize.INTEGER,
+ defaultValue: 0
+ },
+ remark: {
+ type: sequelize.Sequelize.TEXT,
+ allowNull: true
+ },
+ createdAt: {
+ type: sequelize.Sequelize.DATE,
+ allowNull: false
+ },
+ updatedAt: {
+ type: sequelize.Sequelize.DATE,
+ allowNull: false
+ }
+ });
+
+ // 2. 复制数据
+ await sequelize.query(`
+ INSERT INTO users_new (
+ userId, username, password, email, phone, realName, avatar,
+ status, lastLoginTime, lastLoginIp, loginCount, remark, createdAt, updatedAt
+ )
+ SELECT
+ userId, username, password, email, phone, realName, avatar,
+ status, lastLoginTime, lastLoginIp, loginCount, remark, createdAt, updatedAt
+ FROM users
+ `);
+
+ // 3. 删除旧表
+ await queryInterface.dropTable('users');
+
+ // 4. 重命名新表
+ await queryInterface.renameTable('users_new', 'users');
+
+ // 5. 重新创建索引
+ await queryInterface.addIndex('users', ['status']);
+ await queryInterface.addIndex('users', ['username']);
+ await queryInterface.addIndex('users', ['email']);
+
+ console.log('SQLite 迁移完成');
+ } else if (dialect === 'mysql') {
+ // MySQL 可以直接修改 ENUM
+ console.log('检测到 MySQL 数据库,直接修改 ENUM...');
+ await sequelize.query(`
+ ALTER TABLE users
+ MODIFY COLUMN status ENUM('active', 'inactive', 'locked', 'pending') DEFAULT 'active'
+ `);
+ console.log('MySQL 迁移完成');
+ } else {
+ console.log(`不支持的数据库类型: ${dialect},请手动迁移`);
+ }
+
+ console.log('迁移成功完成!');
+ } catch (error) {
+ console.error('迁移失败:', error);
+ process.exit(1);
+ }
+}
+
+migrate();
diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js
index d75f0be..85a7548 100644
--- a/frontend/src/api/index.js
+++ b/frontend/src/api/index.js
@@ -95,7 +95,9 @@ export const userAPI = {
headers: { 'Content-Type': 'multipart/form-data' }
});
},
- deleteAvatar: (userId) => api.delete(`/users/${userId}/avatar`)
+ deleteAvatar: (userId) => api.delete(`/users/${userId}/avatar`),
+ approve: (userId) => api.put(`/users/${userId}/approve`),
+ reject: (userId) => api.put(`/users/${userId}/reject`)
};
export const roleAPI = {
diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx
index 7d8322b..4f6c59b 100644
--- a/frontend/src/context/AuthContext.jsx
+++ b/frontend/src/context/AuthContext.jsx
@@ -91,7 +91,7 @@ export const AuthProvider = ({ children }) => {
setUser(userData);
return { success: true };
}
- return { success: false, message: response.message };
+ return { success: false, message: response.message, code: response.code };
} catch (error) {
return { success: false, message: error };
}
@@ -101,12 +101,14 @@ export const AuthProvider = ({ children }) => {
try {
const response = await authAPI.register(userData);
if (response.success) {
- const { token: newToken, user: newUser } = response.data;
- localStorage.setItem('token', newToken);
- localStorage.setItem('user', JSON.stringify(newUser));
- setToken(newToken);
- setUser(newUser);
- return { success: true, isFirstUser: response.data.isFirstUser };
+ const { token: newToken, user: newUser, isFirstUser, pendingApproval } = response.data;
+ if (newToken) {
+ localStorage.setItem('token', newToken);
+ localStorage.setItem('user', JSON.stringify(newUser));
+ setToken(newToken);
+ setUser(newUser);
+ }
+ return { success: true, isFirstUser, pendingApproval };
}
return { success: false, message: response.message };
} catch (error) {
diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx
index 82528fa..9856237 100644
--- a/frontend/src/pages/Login.jsx
+++ b/frontend/src/pages/Login.jsx
@@ -48,7 +48,11 @@ const Login = () => {
message.success('登录成功');
navigate('/');
} else {
- message.error(result.message || '登录失败');
+ if (result.code === 'PENDING_APPROVAL') {
+ message.warning('账户待审核,请联系管理员激活');
+ } else {
+ message.error(result.message || '登录失败');
+ }
}
} catch (error) {
message.error(error || '登录失败');
@@ -91,8 +95,16 @@ const Login = () => {
});
if (result.success) {
- message.success(result.isFirstUser ? '注册成功,已为您创建管理员账户' : '注册成功');
- navigate('/');
+ if (result.isFirstUser) {
+ message.success('注册成功,已为您创建管理员账户');
+ navigate('/');
+ } else if (result.pendingApproval) {
+ message.success('注册成功,请等待管理员审核');
+ setRegisterMode(false);
+ } else {
+ message.success('注册成功');
+ navigate('/');
+ }
} else {
message.error(result.message || '注册失败');
}
diff --git a/frontend/src/pages/UserManagement.jsx b/frontend/src/pages/UserManagement.jsx
index 49cd771..003ff89 100644
--- a/frontend/src/pages/UserManagement.jsx
+++ b/frontend/src/pages/UserManagement.jsx
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { Card, Table, Button, Space, Modal, Form, Input, Select, message, Tag, Popconfirm, Avatar, Tooltip, Badge } from 'antd';
-import { PlusOutlined, EditOutlined, DeleteOutlined, UserOutlined, ReloadOutlined, LockOutlined, CameraOutlined } from '@ant-design/icons';
+import { PlusOutlined, EditOutlined, DeleteOutlined, UserOutlined, ReloadOutlined, LockOutlined, CameraOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons';
import { userAPI, roleAPI } from '../api';
const { Option } = Select;
@@ -17,6 +17,7 @@ const UserManagement = () => {
const [passwordUser, setPasswordUser] = useState(null);
const [avatarUser, setAvatarUser] = useState(null);
const [uploadLoading, setUploadLoading] = useState(false);
+ const [activeTab, setActiveTab] = useState('all');
const [form] = Form.useForm();
const [passwordForm] = Form.useForm();
const fileInputRef = useRef(null);
@@ -29,10 +30,14 @@ const UserManagement = () => {
const fetchUsers = useCallback(async () => {
setLoading(true);
try {
- const response = await userAPI.list({
+ const params = {
page: pagination.current,
pageSize: pagination.pageSize
- });
+ };
+ if (activeTab !== 'all') {
+ params.status = activeTab;
+ }
+ const response = await userAPI.list(params);
if (response.success) {
setUsers(response.data.users);
setPagination(prev => ({ ...prev, total: response.data.total }));
@@ -42,7 +47,7 @@ const UserManagement = () => {
} finally {
setLoading(false);
}
- }, [pagination.current, pagination.pageSize]);
+ }, [pagination.current, pagination.pageSize, activeTab]);
const fetchRoles = useCallback(async () => {
try {
@@ -207,7 +212,8 @@ const UserManagement = () => {
const colors = {
active: 'green',
inactive: 'red',
- locked: 'orange'
+ locked: 'orange',
+ pending: 'blue'
};
return colors[status] || 'default';
};
@@ -216,11 +222,40 @@ const UserManagement = () => {
const texts = {
active: '正常',
inactive: '禁用',
- locked: '锁定'
+ locked: '锁定',
+ pending: '待审核'
};
return texts[status] || status;
};
+ const handleApprove = useCallback(async (userId) => {
+ try {
+ const response = await userAPI.approve(userId);
+ if (response.success) {
+ message.success('审核通过');
+ fetchUsers();
+ } else {
+ message.error(response.message || '审核失败');
+ }
+ } catch (error) {
+ message.error('审核失败');
+ }
+ }, [fetchUsers]);
+
+ const handleReject = useCallback(async (userId) => {
+ try {
+ const response = await userAPI.reject(userId);
+ if (response.success) {
+ message.success('已拒绝该用户的注册申请');
+ fetchUsers();
+ } else {
+ message.error(response.message || '操作失败');
+ }
+ } catch (error) {
+ message.error('操作失败');
+ }
+ }, [fetchUsers]);
+
const getAvatarUrl = (user) => {
if (!user?.avatar) return null;
return user.avatar;
@@ -302,52 +337,88 @@ const UserManagement = () => {
)
},
- {title: '操作',
+ {
+ title: '操作',
key: 'action',
render: (_, record) => (
-
- }
- onClick={() => handleEdit(record)}
- />
-
-
- }
- onClick={() => handleResetPassword(record)}
- />
-
- handleLockUnlock(record)}
- okText="确定"
- cancelText="取消"
- >
-
- : }
- style={{ color: record.status === 'locked' ? '#52c41a' : '#faad14' }}
- />
-
-
- handleDelete(record.userId)}
- okText="确定"
- cancelText="取消"
- >
-
- } />
-
-
+ {record.status === 'pending' ? (
+ <>
+ handleApprove(record.userId)}
+ okText="通过"
+ cancelText="取消"
+ >
+
+ }
+ style={{ color: '#52c41a' }}
+ />
+
+
+ handleReject(record.userId)}
+ okText="拒绝"
+ cancelText="取消"
+ >
+
+ }
+ style={{ color: '#ff4d4f' }}
+ />
+
+
+ >
+ ) : (
+ <>
+
+ }
+ onClick={() => handleEdit(record)}
+ />
+
+
+ }
+ onClick={() => handleResetPassword(record)}
+ />
+
+ handleLockUnlock(record)}
+ okText="确定"
+ cancelText="取消"
+ >
+
+ : }
+ style={{ color: record.status === 'locked' ? '#52c41a' : '#faad14' }}
+ />
+
+
+ handleDelete(record.userId)}
+ okText="确定"
+ cancelText="取消"
+ >
+
+ } />
+
+
+ >
+ )}
)
}
- ], [handleAvatarClick, handleEdit, handleResetPassword, handleDelete, handleLockUnlock]);
+ ], [handleAvatarClick, handleEdit, handleResetPassword, handleDelete, handleLockUnlock, handleApprove, handleReject]);
const pageHeaderStyle = {
marginBottom: '24px',
@@ -454,7 +525,22 @@ const UserManagement = () => {
-
+ {
+ setActiveTab(key);
+ setPagination(prev => ({ ...prev, current: 1 }));
+ }}
+ >