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) => ( - -