From bc897706b3cabba817d2714bd518b305075a3ac6 Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Tue, 23 Dec 2025 13:02:10 +0800 Subject: [PATCH] 0.9.2 --- .env.example | 43 ++ CHANGELOG_DEPLOY.md | 156 ++++++ DEPLOY.md | 314 +++++++++++ DEPLOYE.md => DEPLOYEE.md | 0 README_DOCKER.md | 193 +++++++ backend/.dockerignore | 46 ++ backend/Dockerfile | 36 ++ backend/app/api/v1/__init__.py | 5 +- backend/app/api/v1/role_permissions.py | 193 +++++++ backend/app/api/v1/roles.py | 342 ++++++++++++ backend/app/api/v1/users.py | 421 +++++++++++++++ backend/app/core/config.py | 3 + backend/app/core/database.py | 3 + backend/requirements.txt | 64 ++- backend/scripts/add_role_permissions_menu.py | 103 ++++ backend/scripts/add_role_permissions_menu.sql | 73 +++ backend/scripts/add_user_role_menus.py | 147 ++++++ backend/scripts/fix_role_system_flag.py | 95 ++++ backend/scripts/init_database.sql | 3 +- backend/scripts/init_db.py | 401 +++++++++++++- backend/scripts/update_system_menu_paths.py | 75 +++ deploy.sh | 382 ++++++++++++++ docker-compose.yml | 113 ++++ forntend/.dockerignore | 37 ++ forntend/Dockerfile | 35 ++ forntend/nginx.conf | 30 ++ forntend/src/App.jsx | 30 ++ forntend/src/api/rolePermissions.js | 47 ++ forntend/src/api/roles.js | 68 +++ forntend/src/api/users.js | 89 ++++ .../src/components/MainLayout/AppSider.jsx | 4 + forntend/src/pages/Login/Login.jsx | 34 +- forntend/src/pages/System/Permissions.jsx | 236 +++++++++ forntend/src/pages/System/Roles.jsx | 492 ++++++++++++++++++ forntend/src/pages/System/Users.jsx | 466 +++++++++++++++++ 35 files changed, 4730 insertions(+), 49 deletions(-) create mode 100644 .env.example create mode 100644 CHANGELOG_DEPLOY.md create mode 100644 DEPLOY.md rename DEPLOYE.md => DEPLOYEE.md (100%) create mode 100644 README_DOCKER.md create mode 100644 backend/.dockerignore create mode 100644 backend/Dockerfile create mode 100644 backend/app/api/v1/role_permissions.py create mode 100644 backend/app/api/v1/roles.py create mode 100644 backend/app/api/v1/users.py create mode 100644 backend/scripts/add_role_permissions_menu.py create mode 100644 backend/scripts/add_role_permissions_menu.sql create mode 100644 backend/scripts/add_user_role_menus.py create mode 100644 backend/scripts/fix_role_system_flag.py create mode 100644 backend/scripts/update_system_menu_paths.py create mode 100755 deploy.sh create mode 100644 docker-compose.yml create mode 100644 forntend/.dockerignore create mode 100644 forntend/Dockerfile create mode 100644 forntend/nginx.conf create mode 100644 forntend/src/api/rolePermissions.js create mode 100644 forntend/src/api/roles.js create mode 100644 forntend/src/api/users.js create mode 100644 forntend/src/pages/System/Permissions.jsx create mode 100644 forntend/src/pages/System/Roles.jsx create mode 100644 forntend/src/pages/System/Users.jsx diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..cca15ba --- /dev/null +++ b/.env.example @@ -0,0 +1,43 @@ +# ==================== 数据库配置 ==================== +# MySQL Root 密码 +MYSQL_ROOT_PASSWORD=root_password_change_me + +# 应用数据库配置 +DB_NAME=nex_docus +DB_USER=nexdocus +DB_PASSWORD=password_change_me +MYSQL_PORT=3306 + +# ==================== Redis 配置 ==================== +REDIS_PASSWORD=redis_password_change_me +REDIS_PORT=6379 +REDIS_DB=8 + +# ==================== 应用配置 ==================== +# JWT 密钥(请务必修改为随机字符串) +SECRET_KEY=your-secret-key-change-me-in-production-use-openssl-rand-hex-32 + +# 调试模式(生产环境设置为 false) +DEBUG=false + +# ==================== 服务端口配置 ==================== +# 后端服务端口 +BACKEND_PORT=8000 + +# 前端服务端口 +FRONTEND_PORT=8080 + +# ==================== 前端配置 ==================== +# API 基础 URL(根据实际部署地址修改) +VITE_API_BASE_URL=http://localhost:8000 + +# ==================== 存储配置 ==================== +# 文件存储路径(宿主机路径,用于存储项目文档和上传文件) +STORAGE_PATH=./storage + +# ==================== 管理员账号配置 ==================== +# 初始管理员账号信息 +ADMIN_USERNAME=admin +ADMIN_PASSWORD=Admin@123456 +ADMIN_EMAIL=admin@example.com +ADMIN_NICKNAME=系统管理员 diff --git a/CHANGELOG_DEPLOY.md b/CHANGELOG_DEPLOY.md new file mode 100644 index 0000000..000095f --- /dev/null +++ b/CHANGELOG_DEPLOY.md @@ -0,0 +1,156 @@ +# 部署配置更新日志 + +## v1.0.1 (2024-12-23) + +### 🔧 配置变更 + +#### 1. 前端端口调整 +- **变更**: 前端默认端口从 `80` 改为 `8080` +- **原因**: 避免与常见服务冲突,提高兼容性 +- **影响**: + - 访问地址变更为: `http://localhost:8080` + - 可在 `.env` 中配置 `FRONTEND_PORT` 自定义端口 + +#### 2. 存储目录映射优化 +- **变更**: Storage 目录从 Docker Volume 改为宿主机目录映射 +- **配置项**: 新增环境变量 `STORAGE_PATH` + - 默认值: `./storage` + - 支持相对路径和绝对路径 + - 示例: + ```bash + STORAGE_PATH=./storage # 相对路径 + STORAGE_PATH=/data/nex-docus-data # 绝对路径 + ``` +- **优势**: + - ✅ 便于直接访问和管理文件 + - ✅ 方便备份和迁移 + - ✅ 支持挂载到独立磁盘或网络存储 + - ✅ 数据独立于容器生命周期 + +### 📝 配置文件更新 + +已更新以下文件: +- `.env.example` - 添加 `STORAGE_PATH` 配置,修改 `FRONTEND_PORT` 默认值 +- `docker-compose.yml` - 修改 storage 为宿主机目录映射 +- `deploy.sh` - 更新访问信息显示 +- `DEPLOY.md` - 更新部署文档 + +### 🔄 迁移指南 + +如果您已经部署了旧版本,需要进行以下操作: + +#### 方案 1: 保留现有数据(推荐) + +```bash +# 1. 停止服务 +./deploy.sh stop + +# 2. 备份现有数据 +docker run --rm -v nex-docus_storage_data:/from -v $(pwd)/storage:/to alpine sh -c "cd /from && cp -r . /to" + +# 3. 更新配置文件 +cp .env.example .env +vim .env # 配置 STORAGE_PATH=./storage + +# 4. 重新启动 +./deploy.sh start + +# 5. 删除旧的 volume(可选) +docker volume rm nex-docus_storage_data +``` + +#### 方案 2: 全新部署 + +```bash +# 1. 备份数据库 +./deploy.sh backup + +# 2. 完全卸载 +./deploy.sh uninstall + +# 3. 重新初始化 +./deploy.sh init + +# 4. 恢复数据库(如需要) +./deploy.sh restore +``` + +### 📊 配置示例 + +#### 开发环境配置 +```bash +FRONTEND_PORT=8080 +BACKEND_PORT=8000 +STORAGE_PATH=./storage +DEBUG=true +``` + +#### 生产环境配置 +```bash +FRONTEND_PORT=80 +BACKEND_PORT=8000 +STORAGE_PATH=/data/nex-docus-storage +DEBUG=false +``` + +#### 多实例部署配置 +```bash +# 实例 1 +FRONTEND_PORT=8081 +BACKEND_PORT=8001 +STORAGE_PATH=/data/instance1/storage + +# 实例 2 +FRONTEND_PORT=8082 +BACKEND_PORT=8002 +STORAGE_PATH=/data/instance2/storage +``` + +### ⚙️ 存储路径说明 + +`STORAGE_PATH` 目录结构: +``` +storage/ +├── projects/ # 项目文档存储 +│ ├── / # 项目 1 +│ │ ├── README.md +│ │ ├── docs/ +│ │ └── _assets/ # 项目资源 +│ └── / # 项目 2 +└── temp/ # 临时文件 +``` + +### 🔒 安全建议 + +1. **权限设置** + ```bash + # 设置适当的目录权限 + chmod 755 storage + chown -R 1000:1000 storage # Docker 容器内用户 + ``` + +2. **备份策略** + ```bash + # 定时备份 storage 目录 + tar -czf storage_backup_$(date +%Y%m%d).tar.gz storage/ + ``` + +3. **网络存储** + ```bash + # 挂载 NFS + mount -t nfs server:/share /data/nex-docus-storage + + # 配置 .env + STORAGE_PATH=/data/nex-docus-storage + ``` + +### 📞 支持 + +如有问题,请查看: +- [部署文档](DEPLOY.md) +- [项目文档](README_DOCKER.md) +- 或提交 Issue + +--- + +**更新时间**: 2024-12-23 diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..b90d03d --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,314 @@ +# NEX Docus Docker 部署文档 + +完整的 Docker 容器化部署方案,支持一键部署、升级、备份等功能。 + +## 🚀 快速开始 + +### 1. 环境要求 + +- Docker 20.10+ +- Docker Compose 2.0+ +- 至少 2GB 可用内存 +- 至少 10GB 可用磁盘空间 + +### 2. 首次部署 + +```bash +# 1. 克隆项目(如果还没有) +git clone +cd "NEX Docus" + +# 2. 配置环境变量 +cp .env.example .env +vim .env # 编辑配置文件 + +# 3. 初始化并启动 +./deploy.sh init +``` + +### 3. 配置说明 + +编辑 `.env` 文件,修改以下关键配置: + +```bash +# 数据库配置 +DB_NAME=nex_docus +DB_USER=nexdocus +DB_PASSWORD=your_secure_password_here + +# Redis 配置 +REDIS_PASSWORD=your_redis_password_here + +# JWT 密钥(必须修改!) +SECRET_KEY=your-secret-key-change-me-in-production + +# 服务端口配置 +FRONTEND_PORT=8080 # 前端访问端口 +BACKEND_PORT=8000 # 后端 API 端口 + +# 存储路径配置(用于存储项目文档和上传文件) +STORAGE_PATH=./storage # 可修改为绝对路径,如 /data/nex-docus-storage + +# 管理员账号 +ADMIN_USERNAME=admin +ADMIN_PASSWORD=Your_Secure_Password_123 +ADMIN_EMAIL=admin@yourdomain.com + +# API 地址(根据实际域名修改) +VITE_API_BASE_URL=http://yourdomain.com:8000 +``` + +**⚠️ 重要提示:** +- `SECRET_KEY` 必须修改为随机字符串(可用 `openssl rand -hex 32` 生成) +- 生产环境必须修改所有默认密码 +- `DEBUG` 设置为 `false` +- `STORAGE_PATH` 可配置为绝对路径,便于数据管理和备份 + +## 📋 部署脚本使用 + +### 基本命令 + +```bash +# 查看帮助 +./deploy.sh help + +# 初始化部署(首次部署) +./deploy.sh init + +# 启动服务 +./deploy.sh start + +# 停止服务 +./deploy.sh stop + +# 重启服务 +./deploy.sh restart + +# 查看服务状态 +./deploy.sh status +``` + +### 日志管理 + +```bash +# 查看所有服务日志 +./deploy.sh logs + +# 查看后端日志 +./deploy.sh logs backend + +# 查看前端日志 +./deploy.sh logs frontend + +# 查看数据库日志 +./deploy.sh logs mysql + +# 查看 Redis 日志 +./deploy.sh logs redis +``` + +### 升级部署 + +```bash +# 升级到最新版本 +./deploy.sh upgrade +``` + +升级流程: +1. 拉取最新代码 +2. 停止服务 +3. 构建新镜像 +4. 更新数据库 +5. 启动服务 +6. 清理旧镜像 + +### 数据库管理 + +```bash +# 备份数据库 +./deploy.sh backup + +# 恢复数据库 +./deploy.sh restore ./backups/nex_docus_20240101_120000.sql +``` + +### 卸载 + +```bash +# 完全卸载(删除所有容器、镜像和数据) +./deploy.sh uninstall +``` + +## 🏗️ 架构说明 + +### 服务组成 + +| 服务 | 端口 | 说明 | +|------|------|------| +| frontend | 8080 | 前端 Nginx 服务 | +| backend | 8000 | 后端 FastAPI 服务 | +| mysql | 3306 | MySQL 8.0 数据库 | +| redis | 6379 | Redis 缓存 | + +### 目录结构 + +``` +NEX Docus/ +├── backend/ # 后端代码 +│ ├── app/ # 应用代码 +│ ├── scripts/ # 脚本文件 +│ │ └── init_db.py # 数据库初始化 +│ ├── Dockerfile # 后端镜像 +│ └── requirements.txt # Python 依赖 +├── forntend/ # 前端代码 +│ ├── src/ # 源代码 +│ ├── Dockerfile # 前端镜像 +│ └── nginx.conf # Nginx 配置 +├── docker-compose.yml # Docker 编排文件 +├── .env.example # 环境变量示例 +├── deploy.sh # 部署管理脚本 +└── DEPLOY.md # 部署文档 +``` + +### 数据持久化 + +所有重要数据都已持久化存储: + +- `mysql_data`: MySQL 数据(Docker Volume) +- `redis_data`: Redis 数据(Docker Volume) +- `${STORAGE_PATH}`: 用户上传的文件和项目文档(宿主机目录映射) + - 默认路径: `./storage` + - 可在 `.env` 中配置 `STORAGE_PATH` 修改为其他路径 + - 建议使用绝对路径,便于备份和迁移 + +**存储目录说明:** +- 该目录存储所有项目文档和上传的文件 +- 支持独立备份和迁移 +- 可配置到独立磁盘或网络存储 + +## 🔧 常见问题 + +### 1. 端口被占用 + +如果默认端口被占用,可以在 `.env` 中修改: + +```bash +FRONTEND_PORT=8080 +BACKEND_PORT=8001 +MYSQL_PORT=3307 +REDIS_PORT=6380 +``` + +### 2. 数据库连接失败 + +检查 MySQL 容器状态: +```bash +docker logs nex-docus-mysql +``` + +确保数据库完全启动(约需 10-15 秒) + +### 3. 前端无法访问后端 API + +检查 `.env` 中的 `VITE_API_BASE_URL` 配置,确保与实际部署地址匹配。 + +### 4. 内存不足 + +如果服务器内存小于 2GB,可能导致 MySQL 无法启动。建议: +- 增加服务器内存 +- 或使用外部 MySQL 服务 + +### 5. 镜像下载缓慢 + +已配置国内镜像加速: +- Python 使用清华源 +- Node 使用淘宝镜像 + +如需更换 Docker 镜像源,编辑 `/etc/docker/daemon.json`: +```json +{ + "registry-mirrors": [ + "https://mirror.ccs.tencentyun.com", + "https://docker.mirrors.ustc.edu.cn" + ] +} +``` + +## 🔐 安全建议 + +1. **修改默认密码** + - 管理员账号密码 + - 数据库密码 + - Redis 密码 + - JWT 密钥 + +2. **配置防火墙** + ```bash + # 只开放必要端口 + ufw allow 80/tcp + ufw allow 443/tcp + ufw enable + ``` + +3. **使用 HTTPS** + - 建议配置 Nginx 反向代理 + - 使用 Let's Encrypt 免费证书 + +4. **定期备份** + ```bash + # 添加定时任务 + crontab -e + # 每天凌晨 2 点备份 + 0 2 * * * cd /path/to/nex-docus && ./deploy.sh backup + ``` + +5. **关闭调试模式** + ```bash + DEBUG=false + ``` + +## 📊 监控与维护 + +### 查看资源使用情况 + +```bash +# Docker 容器资源使用 +docker stats + +# 磁盘使用情况 +df -h + +# 清理 Docker 缓存 +docker system prune -a +``` + +### 定期维护 + +```bash +# 每周检查日志大小 +du -sh backend/logs + +# 清理旧日志 +find backend/logs -name "*.log" -mtime +30 -delete +``` + +## 🆘 技术支持 + +如遇问题,请检查: + +1. 服务日志: `./deploy.sh logs <服务名>` +2. Docker 状态: `docker ps -a` +3. 系统资源: `top` 或 `htop` + +## 📝 更新日志 + +### v1.0.0 (2024-12-20) +- ✨ 完整的 Docker 部署方案 +- ✨ 一键初始化和升级 +- ✨ 数据库备份恢复 +- ✨ 国内镜像加速 +- ✨ 完善的文档和脚本 + +--- + +**祝部署顺利!** 🎉 diff --git a/DEPLOYE.md b/DEPLOYEE.md similarity index 100% rename from DEPLOYE.md rename to DEPLOYEE.md diff --git a/README_DOCKER.md b/README_DOCKER.md new file mode 100644 index 0000000..9916954 --- /dev/null +++ b/README_DOCKER.md @@ -0,0 +1,193 @@ +# NEX Docus + +
+ +**现代化的文档管理系统** + +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +[![Docker](https://img.shields.io/badge/Docker-Ready-brightgreen.svg)](DEPLOY.md) +[![Python](https://img.shields.io/badge/Python-3.9+-blue.svg)](backend/) +[![React](https://img.shields.io/badge/React-18.2-61DAFB.svg)](forntend/) + +
+ +## ✨ 特性 + +- 📝 **Markdown 编辑器** - 强大的 Markdown 实时预览和编辑 +- 📁 **项目管理** - 多项目支持,灵活的文件组织结构 +- 🔐 **权限控制** - 基于 RBAC 的完善权限管理 +- 🔗 **文档分享** - 支持密码保护的公开分享链接 +- 👥 **协作功能** - 项目成员管理和协作编辑 +- 📱 **响应式设计** - 完美支持桌面和移动端 +- 🐳 **Docker 部署** - 一键容器化部署 +- 🚀 **高性能** - 基于 FastAPI 和 React 构建 + +## 🏗️ 技术栈 + +### 后端 +- **框架**: FastAPI (Python 3.9+) +- **数据库**: MySQL 8.0 +- **缓存**: Redis 7 +- **ORM**: SQLAlchemy 2.0 +- **认证**: JWT (python-jose) +- **异步**: Asyncio + +### 前端 +- **框架**: React 18 + Vite +- **UI**: Ant Design 5 +- **路由**: React Router 6 +- **状态管理**: Zustand +- **Markdown**: react-markdown + rehype +- **HTTP**: Axios + +## 📦 快速开始 + +### 使用 Docker 部署(推荐) + +```bash +# 1. 克隆项目 +git clone +cd "NEX Docus" + +# 2. 配置环境变量 +cp .env.example .env +vim .env # 修改配置 + +# 3. 一键部署 +./deploy.sh init +``` + +详细部署文档请查看 [DEPLOY.md](DEPLOY.md) + +### 本地开发 + +#### 后端开发 + +```bash +cd backend + +# 创建虚拟环境 +python -m venv venv +source venv/bin/activate # Windows: venv\Scripts\activate + +# 安装依赖 +pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple + +# 配置环境变量 +cp .env.example .env +vim .env + +# 初始化数据库 +python scripts/init_db.py + +# 启动服务 +python main.py +``` + +后端服务将运行在 `http://localhost:8000` +API 文档: `http://localhost:8000/docs` + +#### 前端开发 + +```bash +cd forntend + +# 安装依赖 +npm install +# 或使用国内镜像 +npm install --registry=https://registry.npmmirror.com + +# 启动开发服务器 +npm run dev +``` + +前端服务将运行在 `http://localhost:5173` + +## 📖 文档 + +- [部署文档](DEPLOY.md) - Docker 容器化部署指南 +- [API 文档](http://localhost:8000/docs) - FastAPI 自动生成的 API 文档 +- [数据库设计](DATABASE.md) - 数据库表结构说明 + +## 🔑 默认账号 + +首次部署后,系统会自动创建管理员账号: + +- 用户名: `admin`(可在 .env 中配置) +- 密码: `Admin@123456`(可在 .env 中配置) + +**⚠️ 重要提示:请在首次登录后立即修改默认密码!** + +## 🛠️ 部署脚本 + +```bash +# 查看帮助 +./deploy.sh help + +# 初始化部署 +./deploy.sh init + +# 启动/停止/重启 +./deploy.sh start|stop|restart + +# 查看状态和日志 +./deploy.sh status +./deploy.sh logs [服务名] + +# 升级部署 +./deploy.sh upgrade + +# 数据库备份/恢复 +./deploy.sh backup +./deploy.sh restore +``` + +## 📁 项目结构 + +``` +NEX Docus/ +├── backend/ # 后端服务 +│ ├── app/ +│ │ ├── api/ # API 路由 +│ │ ├── core/ # 核心配置 +│ │ ├── models/ # 数据模型 +│ │ ├── schemas/ # Pydantic Schemas +│ │ └── services/ # 业务逻辑 +│ ├── scripts/ # 脚本文件 +│ ├── Dockerfile # 后端镜像 +│ └── requirements.txt # Python 依赖 +├── forntend/ # 前端应用 +│ ├── src/ +│ │ ├── api/ # API 请求 +│ │ ├── components/ # 组件 +│ │ ├── pages/ # 页面 +│ │ ├── stores/ # 状态管理 +│ │ └── utils/ # 工具函数 +│ ├── Dockerfile # 前端镜像 +│ └── package.json # npm 依赖 +├── docker-compose.yml # Docker 编排 +├── deploy.sh # 部署脚本 +├── .env.example # 环境变量示例 +└── README.md # 项目文档 +``` + +## 🤝 贡献 + +欢迎提交 Issue 和 Pull Request! + +## 📄 开源协议 + +本项目采用 [MIT](LICENSE) 协议 + +## 🙏 致谢 + +感谢以下开源项目: + +- [FastAPI](https://fastapi.tiangolo.com/) +- [React](https://react.dev/) +- [Ant Design](https://ant.design/) +- [SQLAlchemy](https://www.sqlalchemy.org/) + +--- + +**Made with ❤️ by NEX Docus Team** diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..49e3500 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,46 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python + +# Virtual Environment +venv/ +env/ +ENV/ +.venv + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Testing +.pytest_cache/ +.coverage +htmlcov/ +.tox/ + +# Logs +logs/ +*.log + +# Database +*.db +*.sqlite + +# OS +.DS_Store +Thumbs.db + +# Git +.git/ +.gitignore + +# Project specific +storage/ +temp/ +data/ diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..8bbd306 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,36 @@ +# 使用国内镜像加速 +FROM python:3.9-slim + +# 设置工作目录 +WORKDIR /app + +# 设置环境变量 +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +# 安装系统依赖 +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + default-libmysqlclient-dev \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* + +# 复制依赖文件 +COPY requirements.txt . + +# 使用清华源安装 Python 依赖 +RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple + +# 复制项目文件 +COPY . . + +# 创建必要的目录 +RUN mkdir -p /data/nex_docus_store/projects /data/nex_docus_store/temp logs + +# 暴露端口 +EXPOSE 8000 + +# 启动命令 +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/app/api/v1/__init__.py b/backend/app/api/v1/__init__.py index ef6281a..7bdfd7a 100644 --- a/backend/app/api/v1/__init__.py +++ b/backend/app/api/v1/__init__.py @@ -2,7 +2,7 @@ API v1 路由汇总 """ from fastapi import APIRouter -from app.api.v1 import auth, projects, files, menu, dashboard, preview +from app.api.v1 import auth, projects, files, menu, dashboard, preview, role_permissions, users, roles api_router = APIRouter() @@ -13,3 +13,6 @@ api_router.include_router(files.router, prefix="/files", tags=["文件系统"]) api_router.include_router(menu.router, prefix="/menu", tags=["权限菜单"]) api_router.include_router(dashboard.router, prefix="/dashboard", tags=["管理员仪表盘"]) api_router.include_router(preview.router, prefix="/preview", tags=["项目预览"]) +api_router.include_router(role_permissions.router, prefix="/role-permissions", tags=["角色权限管理"]) +api_router.include_router(users.router, prefix="/users", tags=["用户管理"]) +api_router.include_router(roles.router, prefix="/roles", tags=["角色管理"]) diff --git a/backend/app/api/v1/role_permissions.py b/backend/app/api/v1/role_permissions.py new file mode 100644 index 0000000..6701961 --- /dev/null +++ b/backend/app/api/v1/role_permissions.py @@ -0,0 +1,193 @@ +""" +角色权限管理 API +""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, delete +from typing import List, Dict, Any +from pydantic import BaseModel + +from app.core.database import get_db +from app.core.deps import get_current_user +from app.models.user import User +from app.models.role import Role +from app.models.menu import SystemMenu, RoleMenu +from app.schemas.response import success_response, error_response + +router = APIRouter() + + +class RolePermissionUpdate(BaseModel): + """角色权限更新请求""" + menu_ids: List[int] + + +def build_menu_tree(menus: List[SystemMenu], parent_id: int = 0) -> List[Dict[str, Any]]: + """构建菜单树(包含所有菜单,用于权限分配)""" + result = [] + for menu in menus: + if menu.parent_id == parent_id: + menu_dict = { + "id": menu.id, + "parent_id": menu.parent_id, + "menu_name": menu.menu_name, + "menu_code": menu.menu_code, + "menu_type": menu.menu_type, + "path": menu.path, + "component": menu.component, + "icon": menu.icon, + "sort_order": menu.sort_order, + "visible": menu.visible, + "status": menu.status, + "permission": menu.permission, + } + + # 递归构建子菜单 + children = build_menu_tree(menus, menu.id) + if children: + menu_dict["children"] = children + + result.append(menu_dict) + + # 按 sort_order 排序 + result.sort(key=lambda x: x.get("sort_order", 0)) + return result + + +@router.get("/roles", response_model=dict) +async def get_all_roles( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """获取所有角色列表""" + + # 查询所有角色 + result = await db.execute( + select(Role).order_by(Role.created_at.desc()) + ) + roles = result.scalars().all() + + # 构建角色数据 + roles_data = [] + for role in roles: + roles_data.append({ + "id": role.id, + "role_name": role.role_name, + "role_code": role.role_code, + "description": role.description, + "status": role.status, + "is_system": role.is_system, + "created_at": role.created_at.isoformat() if role.created_at else None, + "updated_at": role.updated_at.isoformat() if role.updated_at else None, + }) + + return success_response(data=roles_data) + + +@router.get("/menu-tree", response_model=dict) +async def get_menu_tree( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """获取完整的菜单权限树(用于权限分配)""" + + # 获取所有菜单(包括禁用的,管理员需要看到所有菜单) + result = await db.execute( + select(SystemMenu).order_by(SystemMenu.sort_order) + ) + all_menus = result.scalars().all() + + # 构建菜单树 + menu_tree = build_menu_tree(all_menus) + + return success_response(data=menu_tree) + + +@router.get("/roles/{role_id}/permissions", response_model=dict) +async def get_role_permissions( + role_id: int, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """获取指定角色的权限菜单ID列表""" + + # 检查角色是否存在 + role_result = await db.execute( + select(Role).where(Role.id == role_id) + ) + role = role_result.scalar_one_or_none() + if not role: + raise HTTPException(status_code=404, detail="角色不存在") + + # 获取角色的菜单权限 + permissions_result = await db.execute( + select(RoleMenu.menu_id).where(RoleMenu.role_id == role_id) + ) + menu_ids = [row[0] for row in permissions_result.all()] + + return success_response(data={ + "role_id": role_id, + "role_name": role.role_name, + "role_code": role.role_code, + "menu_ids": menu_ids + }) + + +@router.put("/roles/{role_id}/permissions", response_model=dict) +async def update_role_permissions( + role_id: int, + permission_update: RolePermissionUpdate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """更新角色的权限菜单""" + + # 检查角色是否存在 + role_result = await db.execute( + select(Role).where(Role.id == role_id) + ) + role = role_result.scalar_one_or_none() + if not role: + raise HTTPException(status_code=404, detail="角色不存在") + + # 检查是否是系统角色 + if role.is_system == 1: + raise HTTPException(status_code=400, detail="系统角色不允许修改权限") + + # 验证菜单ID是否都存在 + if permission_update.menu_ids: + menus_result = await db.execute( + select(SystemMenu.id).where(SystemMenu.id.in_(permission_update.menu_ids)) + ) + valid_menu_ids = [row[0] for row in menus_result.all()] + + invalid_ids = set(permission_update.menu_ids) - set(valid_menu_ids) + if invalid_ids: + raise HTTPException( + status_code=400, + detail=f"以下菜单ID不存在: {', '.join(map(str, invalid_ids))}" + ) + + # 删除原有权限 + await db.execute( + delete(RoleMenu).where(RoleMenu.role_id == role_id) + ) + + # 添加新权限 + if permission_update.menu_ids: + for menu_id in permission_update.menu_ids: + role_menu = RoleMenu( + role_id=role_id, + menu_id=menu_id + ) + db.add(role_menu) + + await db.commit() + + return success_response( + data={ + "role_id": role_id, + "menu_ids": permission_update.menu_ids + }, + message="角色权限更新成功" + ) diff --git a/backend/app/api/v1/roles.py b/backend/app/api/v1/roles.py new file mode 100644 index 0000000..6099a96 --- /dev/null +++ b/backend/app/api/v1/roles.py @@ -0,0 +1,342 @@ +""" +角色管理 API +""" +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, func, delete +from typing import Optional +from pydantic import BaseModel + +from app.core.database import get_db +from app.core.deps import get_current_user +from app.models.user import User +from app.models.role import Role, UserRole +from app.schemas.response import success_response + +router = APIRouter() + + +# === Pydantic Schemas === + +class RoleCreateRequest(BaseModel): + """创建角色请求""" + role_name: str + role_code: str + description: Optional[str] = None + status: int = 1 + + +class RoleUpdateRequest(BaseModel): + """更新角色请求""" + role_name: Optional[str] = None + role_code: Optional[str] = None + description: Optional[str] = None + status: Optional[int] = None + + +# === API Endpoints === + +@router.get("/", response_model=dict) +async def get_roles_list( + page: int = Query(1, ge=1), + page_size: int = Query(10, ge=1, le=100), + keyword: Optional[str] = Query(None, description="搜索关键词(角色名称、编码)"), + status: Optional[int] = Query(None, description="状态筛选:0-禁用 1-启用"), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """获取角色列表(分页)""" + + # 构建查询条件 + conditions = [] + if keyword: + conditions.append( + (Role.role_name.like(f"%{keyword}%")) | (Role.role_code.like(f"%{keyword}%")) + ) + if status is not None: + conditions.append(Role.status == status) + + # 查询总数 + count_query = select(func.count(Role.id)) + if conditions: + count_query = count_query.where(*conditions) + total_result = await db.execute(count_query) + total = total_result.scalar() + + # 查询角色列表 + query = select(Role).order_by(Role.created_at.desc()) + if conditions: + query = query.where(*conditions) + query = query.offset((page - 1) * page_size).limit(page_size) + + result = await db.execute(query) + roles = result.scalars().all() + + # 获取每个角色的用户数量 + roles_data = [] + for role in roles: + # 查询该角色的用户数量 + user_count_result = await db.execute( + select(func.count(UserRole.user_id)).where(UserRole.role_id == role.id) + ) + user_count = user_count_result.scalar() + + roles_data.append({ + "id": role.id, + "role_name": role.role_name, + "role_code": role.role_code, + "description": role.description, + "status": role.status, + "is_system": role.is_system, + "user_count": user_count, + "created_at": role.created_at.isoformat() if role.created_at else None, + "updated_at": role.updated_at.isoformat() if role.updated_at else None, + }) + + return { + "code": 200, + "message": "success", + "data": roles_data, + "total": total, + "page": page, + "page_size": page_size + } + + +@router.post("/", response_model=dict) +async def create_role( + role_data: RoleCreateRequest, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """创建新角色""" + + # 检查角色编码是否已存在 + result = await db.execute( + select(Role).where(Role.role_code == role_data.role_code) + ) + existing_role = result.scalar_one_or_none() + if existing_role: + raise HTTPException(status_code=400, detail="角色编码已存在") + + # 检查角色名称是否已存在 + result = await db.execute( + select(Role).where(Role.role_name == role_data.role_name) + ) + existing_name = result.scalar_one_or_none() + if existing_name: + raise HTTPException(status_code=400, detail="角色名称已存在") + + # 创建角色 + new_role = Role( + role_name=role_data.role_name, + role_code=role_data.role_code, + description=role_data.description, + status=role_data.status, + is_system=0 # 新创建的角色都不是系统角色 + ) + + db.add(new_role) + await db.commit() + await db.refresh(new_role) + + return success_response( + data={ + "id": new_role.id, + "role_name": new_role.role_name, + "role_code": new_role.role_code + }, + message="角色创建成功" + ) + + +@router.get("/{role_id}", response_model=dict) +async def get_role( + role_id: int, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """获取角色详情""" + + result = await db.execute( + select(Role).where(Role.id == role_id) + ) + role = result.scalar_one_or_none() + if not role: + raise HTTPException(status_code=404, detail="角色不存在") + + # 获取该角色的用户数量 + user_count_result = await db.execute( + select(func.count(UserRole.user_id)).where(UserRole.role_id == role.id) + ) + user_count = user_count_result.scalar() + + return success_response(data={ + "id": role.id, + "role_name": role.role_name, + "role_code": role.role_code, + "description": role.description, + "status": role.status, + "is_system": role.is_system, + "user_count": user_count, + "created_at": role.created_at.isoformat() if role.created_at else None, + "updated_at": role.updated_at.isoformat() if role.updated_at else None, + }) + + +@router.put("/{role_id}", response_model=dict) +async def update_role( + role_id: int, + role_data: RoleUpdateRequest, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """更新角色信息""" + + # 查询角色 + result = await db.execute( + select(Role).where(Role.id == role_id) + ) + role = result.scalar_one_or_none() + if not role: + raise HTTPException(status_code=404, detail="角色不存在") + + # 系统角色不允许修改 + if role.is_system == 1: + raise HTTPException(status_code=400, detail="系统角色不允许修改") + + # 更新字段 + if role_data.role_name is not None: + # 检查角色名称是否与其他角色冲突 + if role_data.role_name != role.role_name: + name_result = await db.execute( + select(Role).where(Role.role_name == role_data.role_name, Role.id != role_id) + ) + if name_result.scalar_one_or_none(): + raise HTTPException(status_code=400, detail="角色名称已被其他角色使用") + role.role_name = role_data.role_name + + if role_data.role_code is not None: + # 检查角色编码是否与其他角色冲突 + if role_data.role_code != role.role_code: + code_result = await db.execute( + select(Role).where(Role.role_code == role_data.role_code, Role.id != role_id) + ) + if code_result.scalar_one_or_none(): + raise HTTPException(status_code=400, detail="角色编码已被其他角色使用") + role.role_code = role_data.role_code + + if role_data.description is not None: + role.description = role_data.description + + if role_data.status is not None: + role.status = role_data.status + + await db.commit() + await db.refresh(role) + + return success_response(data={"id": role.id}, message="角色信息更新成功") + + +@router.delete("/{role_id}", response_model=dict) +async def delete_role( + role_id: int, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """删除角色""" + + # 查询角色 + result = await db.execute( + select(Role).where(Role.id == role_id) + ) + role = result.scalar_one_or_none() + if not role: + raise HTTPException(status_code=404, detail="角色不存在") + + # 系统角色不允许删除 + if role.is_system == 1: + raise HTTPException(status_code=400, detail="系统角色不允许删除") + + # 检查是否有用户使用该角色 + user_count_result = await db.execute( + select(func.count(UserRole.user_id)).where(UserRole.role_id == role_id) + ) + user_count = user_count_result.scalar() + if user_count > 0: + raise HTTPException( + status_code=400, + detail=f"该角色下有 {user_count} 个用户,无法删除。请先移除这些用户的角色。" + ) + + # 删除角色的菜单权限关联 + from app.models.menu import RoleMenu + await db.execute( + delete(RoleMenu).where(RoleMenu.role_id == role_id) + ) + + # 删除角色 + await db.delete(role) + await db.commit() + + return success_response(message="角色删除成功") + + +@router.get("/{role_id}/users", response_model=dict) +async def get_role_users( + role_id: int, + page: int = Query(1, ge=1), + page_size: int = Query(10, ge=1, le=100), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """获取角色下的用户列表""" + + # 检查角色是否存在 + role_result = await db.execute( + select(Role).where(Role.id == role_id) + ) + role = role_result.scalar_one_or_none() + if not role: + raise HTTPException(status_code=404, detail="角色不存在") + + # 查询总数 + count_result = await db.execute( + select(func.count(UserRole.user_id)).where(UserRole.role_id == role_id) + ) + total = count_result.scalar() + + # 查询用户列表 + result = await db.execute( + select(User) + .join(UserRole, UserRole.user_id == User.id) + .where(UserRole.role_id == role_id) + .order_by(User.created_at.desc()) + .offset((page - 1) * page_size) + .limit(page_size) + ) + users = result.scalars().all() + + users_data = [{ + "id": user.id, + "username": user.username, + "nickname": user.nickname, + "email": user.email, + "phone": user.phone, + "status": user.status, + "created_at": user.created_at.isoformat() if user.created_at else None, + } for user in users] + + return { + "code": 200, + "message": "success", + "data": users_data, + "total": total, + "page": page, + "page_size": page_size, + "role_info": { + "id": role.id, + "role_name": role.role_name, + "role_code": role.role_code + } + } diff --git a/backend/app/api/v1/users.py b/backend/app/api/v1/users.py new file mode 100644 index 0000000..4c752c1 --- /dev/null +++ b/backend/app/api/v1/users.py @@ -0,0 +1,421 @@ +""" +用户管理 API +""" +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, func, delete, or_ +from typing import List, Optional +from pydantic import BaseModel, EmailStr + +from app.core.database import get_db +from app.core.deps import get_current_user +from app.core.security import get_password_hash +from app.core.config import settings +from app.models.user import User +from app.models.role import Role, UserRole +from app.models.project import Project, ProjectMember +from app.schemas.response import success_response, error_response + +router = APIRouter() + + +# === Pydantic Schemas === + +class UserCreateRequest(BaseModel): + """创建用户请求""" + username: str + nickname: Optional[str] = None + email: Optional[EmailStr] = None + phone: Optional[str] = None + role_ids: List[int] = [] # 分配的角色ID列表 + + +class UserUpdateRequest(BaseModel): + """更新用户请求""" + nickname: Optional[str] = None + email: Optional[EmailStr] = None + phone: Optional[str] = None + status: Optional[int] = None + + +class UserRolesUpdateRequest(BaseModel): + """更新用户角色请求""" + role_ids: List[int] + + +# === API Endpoints === + +@router.get("/", response_model=dict) +async def get_users( + page: int = Query(1, ge=1), + page_size: int = Query(10, ge=1, le=100), + keyword: Optional[str] = Query(None, description="搜索关键词(用户名、昵称、邮箱)"), + status: Optional[int] = Query(None, description="状态筛选:0-禁用 1-启用"), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """获取用户列表(分页)""" + + # 构建查询条件 + conditions = [] + if keyword: + conditions.append( + or_( + User.username.like(f"%{keyword}%"), + User.nickname.like(f"%{keyword}%"), + User.email.like(f"%{keyword}%") + ) + ) + if status is not None: + conditions.append(User.status == status) + + # 查询总数 + count_query = select(func.count(User.id)) + if conditions: + count_query = count_query.where(*conditions) + total_result = await db.execute(count_query) + total = total_result.scalar() + + # 查询用户列表 + query = select(User).order_by(User.created_at.desc()) + if conditions: + query = query.where(*conditions) + query = query.offset((page - 1) * page_size).limit(page_size) + + result = await db.execute(query) + users = result.scalars().all() + + # 获取每个用户的角色信息 + users_data = [] + for user in users: + # 获取用户角色 + roles_result = await db.execute( + select(Role) + .join(UserRole, UserRole.role_id == Role.id) + .where(UserRole.user_id == user.id) + ) + roles = roles_result.scalars().all() + + users_data.append({ + "id": user.id, + "username": user.username, + "nickname": user.nickname, + "email": user.email, + "phone": user.phone, + "avatar": user.avatar, + "status": user.status, + "is_superuser": user.is_superuser, + "last_login_at": user.last_login_at.isoformat() if user.last_login_at else None, + "created_at": user.created_at.isoformat() if user.created_at else None, + "roles": [{"id": r.id, "role_name": r.role_name, "role_code": r.role_code} for r in roles] + }) + + return { + "code": 200, + "message": "success", + "data": users_data, + "total": total, + "page": page, + "page_size": page_size + } + + +@router.post("/", response_model=dict) +async def create_user( + user_data: UserCreateRequest, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """创建新用户""" + + # 检查用户名是否已存在 + result = await db.execute( + select(User).where(User.username == user_data.username) + ) + existing_user = result.scalar_one_or_none() + if existing_user: + raise HTTPException(status_code=400, detail="用户名已存在") + + # 检查邮箱是否已存在 + if user_data.email: + result = await db.execute( + select(User).where(User.email == user_data.email) + ) + existing_email = result.scalar_one_or_none() + if existing_email: + raise HTTPException(status_code=400, detail="邮箱已被使用") + + # 创建用户 + new_user = User( + username=user_data.username, + password_hash=get_password_hash(settings.DEFAULT_USER_PASSWORD), + nickname=user_data.nickname or user_data.username, + email=user_data.email, + phone=user_data.phone, + status=1, + is_superuser=0 + ) + + db.add(new_user) + await db.flush() + + # 分配角色 + if user_data.role_ids: + # 验证角色ID是否存在 + roles_result = await db.execute( + select(Role.id).where(Role.id.in_(user_data.role_ids)) + ) + valid_role_ids = [row[0] for row in roles_result.all()] + + for role_id in valid_role_ids: + user_role = UserRole(user_id=new_user.id, role_id=role_id) + db.add(user_role) + + await db.commit() + await db.refresh(new_user) + + return success_response( + data={ + "id": new_user.id, + "username": new_user.username, + "default_password": settings.DEFAULT_USER_PASSWORD + }, + message="用户创建成功,请记住初始密码" + ) + + +@router.get("/{user_id}", response_model=dict) +async def get_user( + user_id: int, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """获取用户详情""" + + # 查询用户 + result = await db.execute( + select(User).where(User.id == user_id) + ) + user = result.scalar_one_or_none() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + + # 获取用户角色 + roles_result = await db.execute( + select(Role) + .join(UserRole, UserRole.role_id == Role.id) + .where(UserRole.user_id == user.id) + ) + roles = roles_result.scalars().all() + + return success_response(data={ + "id": user.id, + "username": user.username, + "nickname": user.nickname, + "email": user.email, + "phone": user.phone, + "avatar": user.avatar, + "status": user.status, + "is_superuser": user.is_superuser, + "last_login_at": user.last_login_at.isoformat() if user.last_login_at else None, + "created_at": user.created_at.isoformat() if user.created_at else None, + "roles": [{"id": r.id, "role_name": r.role_name, "role_code": r.role_code} for r in roles] + }) + + +@router.put("/{user_id}", response_model=dict) +async def update_user( + user_id: int, + user_data: UserUpdateRequest, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """更新用户信息""" + + # 查询用户 + result = await db.execute( + select(User).where(User.id == user_id) + ) + user = result.scalar_one_or_none() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + + # 更新字段 + if user_data.nickname is not None: + user.nickname = user_data.nickname + if user_data.email is not None: + # 检查邮箱是否被其他用户使用 + if user_data.email != user.email: + email_result = await db.execute( + select(User).where(User.email == user_data.email, User.id != user_id) + ) + if email_result.scalar_one_or_none(): + raise HTTPException(status_code=400, detail="邮箱已被其他用户使用") + user.email = user_data.email + if user_data.phone is not None: + user.phone = user_data.phone + if user_data.status is not None: + user.status = user_data.status + + await db.commit() + await db.refresh(user) + + return success_response(data={"id": user.id}, message="用户信息更新成功") + + +@router.delete("/{user_id}", response_model=dict) +async def delete_user( + user_id: int, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """删除用户(需要检查是否有归属项目)""" + + # 查询用户 + result = await db.execute( + select(User).where(User.id == user_id) + ) + user = result.scalar_one_or_none() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + + # 不允许删除超级管理员 + if user.is_superuser == 1: + raise HTTPException(status_code=400, detail="不允许删除超级管理员") + + # 不允许删除自己 + if user_id == current_user.id: + raise HTTPException(status_code=400, detail="不允许删除当前登录用户") + + # 检查用户是否拥有项目 + owned_projects_result = await db.execute( + select(func.count(Project.id)).where(Project.owner_id == user_id) + ) + owned_projects_count = owned_projects_result.scalar() + if owned_projects_count > 0: + raise HTTPException( + status_code=400, + detail=f"该用户拥有 {owned_projects_count} 个项目,无法删除。请先转移或删除这些项目。" + ) + + # 删除用户的角色关联 + await db.execute( + delete(UserRole).where(UserRole.user_id == user_id) + ) + + # 删除用户的项目成员关联 + await db.execute( + delete(ProjectMember).where(ProjectMember.user_id == user_id) + ) + + # 删除用户 + await db.delete(user) + await db.commit() + + return success_response(message="用户删除成功") + + +@router.put("/{user_id}/status", response_model=dict) +async def update_user_status( + user_id: int, + status: int = Query(..., ge=0, le=1, description="状态:0-禁用 1-启用"), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """更新用户状态(停用/启用)""" + + # 查询用户 + result = await db.execute( + select(User).where(User.id == user_id) + ) + user = result.scalar_one_or_none() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + + # 不允许停用超级管理员 + if user.is_superuser == 1 and status == 0: + raise HTTPException(status_code=400, detail="不允许停用超级管理员") + + # 不允许停用自己 + if user_id == current_user.id and status == 0: + raise HTTPException(status_code=400, detail="不允许停用当前登录用户") + + user.status = status + await db.commit() + + status_text = "启用" if status == 1 else "停用" + return success_response(message=f"用户已{status_text}") + + +@router.put("/{user_id}/roles", response_model=dict) +async def update_user_roles( + user_id: int, + roles_data: UserRolesUpdateRequest, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """更新用户角色""" + + # 查询用户 + result = await db.execute( + select(User).where(User.id == user_id) + ) + user = result.scalar_one_or_none() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + + # 验证角色ID是否存在 + if roles_data.role_ids: + roles_result = await db.execute( + select(Role.id).where(Role.id.in_(roles_data.role_ids)) + ) + valid_role_ids = [row[0] for row in roles_result.all()] + + invalid_ids = set(roles_data.role_ids) - set(valid_role_ids) + if invalid_ids: + raise HTTPException( + status_code=400, + detail=f"以下角色ID不存在: {', '.join(map(str, invalid_ids))}" + ) + else: + valid_role_ids = [] + + # 删除原有角色关联 + await db.execute( + delete(UserRole).where(UserRole.user_id == user_id) + ) + + # 添加新角色关联 + for role_id in valid_role_ids: + user_role = UserRole(user_id=user_id, role_id=role_id) + db.add(user_role) + + await db.commit() + + return success_response(message="用户角色更新成功") + + +@router.post("/{user_id}/reset-password", response_model=dict) +async def reset_user_password( + user_id: int, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """重置用户密码为初始密码""" + + # 查询用户 + result = await db.execute( + select(User).where(User.id == user_id) + ) + user = result.scalar_one_or_none() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + + # 重置密码 + user.password_hash = get_password_hash(settings.DEFAULT_USER_PASSWORD) + await db.commit() + + return success_response( + data={"default_password": settings.DEFAULT_USER_PASSWORD}, + message="密码已重置为初始密码" + ) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 479854f..ebe05e3 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -57,6 +57,9 @@ class Settings(BaseSettings): ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440 # 24小时 + # 用户配置 + DEFAULT_USER_PASSWORD: str = "User@123456" # 新用户默认密码 + # 文件存储配置 STORAGE_ROOT: str = "/data/nex_docus_store" PROJECTS_PATH: str = "/data/nex_docus_store/projects" diff --git a/backend/app/core/database.py b/backend/app/core/database.py index ba7685c..392120b 100644 --- a/backend/app/core/database.py +++ b/backend/app/core/database.py @@ -23,6 +23,9 @@ AsyncSessionLocal = sessionmaker( autoflush=False, ) +# 别名,用于脚本中使用 +async_session = AsyncSessionLocal + # 创建基础模型类 Base = declarative_base() diff --git a/backend/requirements.txt b/backend/requirements.txt index 191f488..cdc45e7 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,31 +1,45 @@ -# FastAPI 核心 -fastapi==0.109.0 -uvicorn[standard]==0.27.0 -python-multipart==0.0.6 -email-validator==2.1.0 - -# 数据库 -SQLAlchemy==2.0.25 -pymysql==1.1.0 +aiofiles==23.2.1 aiomysql==0.2.0 -alembic==1.13.1 -greenlet==3.0.3 - -# Redis -redis==5.0.1 aioredis==2.0.1 - -# 认证与安全 -python-jose[cryptography]==3.3.0 -passlib[bcrypt]==1.7.4 -python-dotenv==1.0.0 +alembic==1.13.1 +annotated-types==0.7.0 +anyio==4.12.0 +async-timeout==5.0.1 +bcrypt==4.3.0 +cffi==2.0.0 +click==8.1.8 +cryptography==46.0.3 +dnspython==2.7.0 +ecdsa==0.19.1 +email-validator==2.3.0 +exceptiongroup==1.3.1 +fastapi==0.109.0 +greenlet==3.2.4 +h11==0.16.0 +httptools==0.7.1 +idna==3.11 +loguru==0.7.2 +Mako==1.3.10 +MarkupSafe==3.0.3 +passlib==1.7.4 +pyasn1==0.6.1 +pycparser==2.23 pydantic==2.5.3 pydantic-settings==2.1.0 - -# 文件处理 -aiofiles==23.2.1 +pydantic_core==2.14.6 +PyMySQL==1.1.0 +python-dotenv==1.0.0 +python-jose==3.3.0 python-magic==0.4.27 - -# 工具库 +python-multipart==0.0.6 PyYAML==6.0.1 -loguru==0.7.2 +redis==5.0.1 +rsa==4.9.1 +six==1.17.0 +SQLAlchemy==2.0.25 +starlette==0.35.1 +typing_extensions==4.15.0 +uvicorn==0.27.0 +uvloop==0.22.1 +watchfiles==1.1.1 +websockets==15.0.1 diff --git a/backend/scripts/add_role_permissions_menu.py b/backend/scripts/add_role_permissions_menu.py new file mode 100644 index 0000000..34755b9 --- /dev/null +++ b/backend/scripts/add_role_permissions_menu.py @@ -0,0 +1,103 @@ +""" +添加角色权限管理菜单到数据库 +""" +import sys +import asyncio +from pathlib import Path + +# 添加项目根目录到 Python 路径 +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from sqlalchemy import text +from app.core.database import async_session + + +async def add_role_permissions_menu(): + """添加角色权限管理菜单""" + print("正在添加角色权限管理菜单...") + + async with async_session() as session: + # 1. 检查菜单是否已存在 + result = await session.execute( + text("SELECT COUNT(*) FROM system_menus WHERE id = 14") + ) + count = result.scalar() + + if count > 0: + print(" 菜单已存在,跳过添加") + return + + # 2. 插入菜单项 + await session.execute( + text(""" + INSERT INTO system_menus ( + id, parent_id, menu_name, menu_code, menu_type, + path, component, icon, sort_order, visible, status, + created_at, updated_at + ) VALUES ( + 14, 4, '角色权限管理', 'system:role_permissions', 1, + '/role-permissions', 'RolePermissions', 'SafetyOutlined', + 6, 1, 1, NOW(), NOW() + ) + """) + ) + print("✓ 菜单项添加成功") + + # 3. 为超级管理员角色分配菜单权限 + result = await session.execute( + text("SELECT id FROM roles WHERE role_code = 'super_admin'") + ) + super_admin_role_id = result.scalar() + + if super_admin_role_id: + await session.execute( + text(""" + INSERT INTO role_menus (role_id, menu_id, created_at) + VALUES (:role_id, 14, NOW()) + """), + {"role_id": super_admin_role_id} + ) + print(f"✓ 已为超级管理员角色分配权限") + + # 4. 为管理员角色分配菜单权限 + result = await session.execute( + text("SELECT id FROM roles WHERE role_code = 'admin'") + ) + admin_role_id = result.scalar() + + if admin_role_id: + await session.execute( + text(""" + INSERT INTO role_menus (role_id, menu_id, created_at) + VALUES (:role_id, 14, NOW()) + """), + {"role_id": admin_role_id} + ) + print(f"✓ 已为管理员角色分配权限") + + await session.commit() + print("\n✓ 角色权限管理菜单添加完成!") + + +async def main(): + """主函数""" + print("=" * 60) + print("添加角色权限管理菜单") + print("=" * 60) + print() + + try: + await add_role_permissions_menu() + print() + print("=" * 60) + print("✓ 操作完成!") + print("=" * 60) + except Exception as e: + print(f"\n✗ 操作失败: {str(e)}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/backend/scripts/add_role_permissions_menu.sql b/backend/scripts/add_role_permissions_menu.sql new file mode 100644 index 0000000..39cac10 --- /dev/null +++ b/backend/scripts/add_role_permissions_menu.sql @@ -0,0 +1,73 @@ +-- 添加角色权限管理菜单 +-- 执行时间: 2024-12-23 + +-- 1. 插入菜单项 +INSERT INTO system_menus ( + id, + parent_id, + menu_name, + menu_code, + menu_type, + path, + component, + icon, + sort_order, + visible, + status, + created_at, + updated_at +) VALUES ( + 14, + 4, + '角色权限管理', + 'system:role_permissions', + 1, + '/role-permissions', + 'RolePermissions', + 'SafetyOutlined', + 6, + 1, + 1, + NOW(), + NOW() +); + +-- 2. 为超级管理员角色分配该菜单权限 +INSERT INTO role_menus (role_id, menu_id, created_at) +SELECT r.id, 14, NOW() +FROM roles r +WHERE r.role_code = 'super_admin' +AND NOT EXISTS ( + SELECT 1 FROM role_menus rm + WHERE rm.role_id = r.id AND rm.menu_id = 14 +); + +-- 3. 为管理员角色分配该菜单权限 +INSERT INTO role_menus (role_id, menu_id, created_at) +SELECT r.id, 14, NOW() +FROM roles r +WHERE r.role_code = 'admin' +AND NOT EXISTS ( + SELECT 1 FROM role_menus rm + WHERE rm.role_id = r.id AND rm.menu_id = 14 +); + +-- 验证结果 +SELECT + m.id, + m.menu_name, + m.menu_code, + m.path, + CASE WHEN m.parent_id = 0 THEN '根菜单' ELSE CONCAT('子菜单 (父ID: ', m.parent_id, ')') END as menu_level +FROM system_menus m +WHERE m.id = 14; + +-- 查看哪些角色拥有此菜单权限 +SELECT + r.role_name, + r.role_code, + m.menu_name +FROM roles r +JOIN role_menus rm ON r.id = rm.role_id +JOIN system_menus m ON rm.menu_id = m.id +WHERE m.id = 14; diff --git a/backend/scripts/add_user_role_menus.py b/backend/scripts/add_user_role_menus.py new file mode 100644 index 0000000..d8f75d8 --- /dev/null +++ b/backend/scripts/add_user_role_menus.py @@ -0,0 +1,147 @@ +""" +添加用户管理和角色管理菜单到数据库 +""" +import sys +import asyncio +from pathlib import Path + +# 添加项目根目录到 Python 路径 +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from sqlalchemy import text +from app.core.database import async_session + + +async def add_user_role_menus(): + """添加用户管理和角色管理菜单""" + print("正在添加用户管理和角色管理菜单...") + + async with async_session() as session: + # 1. 检查菜单是否已存在 + result = await session.execute( + text("SELECT COUNT(*) FROM system_menus WHERE id IN (15, 16)") + ) + count = result.scalar() + + if count > 0: + print(f" 菜单已存在({count}个),跳过添加") + return + + # 2. 插入用户管理菜单(ID: 15) + await session.execute( + text(""" + INSERT INTO system_menus ( + id, parent_id, menu_name, menu_code, menu_type, + path, component, icon, sort_order, visible, status, + created_at, updated_at + ) VALUES ( + 15, 4, '用户管理', 'system:users', 1, + '/users', 'UserManagement', 'UserOutlined', + 1, 1, 1, NOW(), NOW() + ) + """) + ) + print("✓ 用户管理菜单添加成功(ID: 15)") + + # 3. 插入角色管理菜单(ID: 16) + await session.execute( + text(""" + INSERT INTO system_menus ( + id, parent_id, menu_name, menu_code, menu_type, + path, component, icon, sort_order, visible, status, + created_at, updated_at + ) VALUES ( + 16, 4, '角色管理', 'system:roles', 1, + '/roles', 'RoleManagement', 'TeamOutlined', + 2, 1, 1, NOW(), NOW() + ) + """) + ) + print("✓ 角色管理菜单添加成功(ID: 16)") + + # 4. 更新已有菜单的sort_order,避免冲突 + await session.execute( + text(""" + UPDATE system_menus + SET sort_order = sort_order + 2 + WHERE parent_id = 4 AND id NOT IN (15, 16) AND sort_order >= 1 + """) + ) + print("✓ 已调整其他子菜单的排序") + + # 5. 为超级管理员角色分配这两个菜单权限 + result = await session.execute( + text("SELECT id FROM roles WHERE role_code = 'super_admin'") + ) + super_admin_role_id = result.scalar() + + if super_admin_role_id: + # 用户管理菜单 + await session.execute( + text(""" + INSERT INTO role_menus (role_id, menu_id, created_at) + VALUES (:role_id, 15, NOW()) + """), + {"role_id": super_admin_role_id} + ) + # 角色管理菜单 + await session.execute( + text(""" + INSERT INTO role_menus (role_id, menu_id, created_at) + VALUES (:role_id, 16, NOW()) + """), + {"role_id": super_admin_role_id} + ) + print(f"✓ 已为超级管理员角色分配权限") + + # 6. 为管理员角色分配这两个菜单权限(如果存在) + result = await session.execute( + text("SELECT id FROM roles WHERE role_code = 'admin'") + ) + admin_role_id = result.scalar() + + if admin_role_id: + # 用户管理菜单 + await session.execute( + text(""" + INSERT INTO role_menus (role_id, menu_id, created_at) + VALUES (:role_id, 15, NOW()) + """), + {"role_id": admin_role_id} + ) + # 角色管理菜单 + await session.execute( + text(""" + INSERT INTO role_menus (role_id, menu_id, created_at) + VALUES (:role_id, 16, NOW()) + """), + {"role_id": admin_role_id} + ) + print(f"✓ 已为管理员角色分配权限") + + await session.commit() + print("\n✓ 用户管理和角色管理菜单添加完成!") + + +async def main(): + """主函数""" + print("=" * 80) + print("添加用户管理和角色管理菜单") + print("=" * 80) + print() + + try: + await add_user_role_menus() + print() + print("=" * 80) + print("✓ 操作完成!现在可以在系统管理菜单中访问用户管理和角色管理了") + print("=" * 80) + except Exception as e: + print(f"\n✗ 操作失败: {str(e)}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/backend/scripts/fix_role_system_flag.py b/backend/scripts/fix_role_system_flag.py new file mode 100644 index 0000000..bea1834 --- /dev/null +++ b/backend/scripts/fix_role_system_flag.py @@ -0,0 +1,95 @@ +""" +检查和修复角色的 is_system 字段 +""" +import sys +import asyncio +from pathlib import Path + +# 添加项目根目录到 Python 路径 +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from sqlalchemy import text +from app.core.database import async_session + + +async def check_and_fix_roles(): + """检查和修复角色的 is_system 字段""" + print("正在检查角色数据...") + + async with async_session() as session: + # 查询所有角色 + result = await session.execute( + text("SELECT id, role_name, role_code, is_system FROM roles ORDER BY id") + ) + roles = result.fetchall() + + print("\n当前角色列表:") + print("-" * 80) + print(f"{'ID':<5} {'角色名称':<20} {'角色编码':<20} {'是否系统角色':<15}") + print("-" * 80) + for role in roles: + is_system_text = "是" if role[3] == 1 else "否" + print(f"{role[0]:<5} {role[1]:<20} {role[2]:<20} {is_system_text:<15}") + print("-" * 80) + + # 修复建议:只有 super_admin 应该是系统角色 + print("\n开始修复角色 is_system 字段...") + + # 将 super_admin 设置为系统角色 + await session.execute( + text("UPDATE roles SET is_system = 1 WHERE role_code = 'super_admin'") + ) + print("✓ 已将 super_admin 设置为系统角色") + + # 将其他角色设置为非系统角色 + await session.execute( + text("UPDATE roles SET is_system = 0 WHERE role_code != 'super_admin'") + ) + print("✓ 已将其他角色设置为非系统角色") + + await session.commit() + + # 再次查询验证 + result = await session.execute( + text("SELECT id, role_name, role_code, is_system FROM roles ORDER BY id") + ) + roles = result.fetchall() + + print("\n修复后的角色列表:") + print("-" * 80) + print(f"{'ID':<5} {'角色名称':<20} {'角色编码':<20} {'是否系统角色':<15}") + print("-" * 80) + for role in roles: + is_system_text = "是" if role[3] == 1 else "否" + print(f"{role[0]:<5} {role[1]:<20} {role[2]:<20} {is_system_text:<15}") + print("-" * 80) + + print("\n✓ 角色数据修复完成!") + print("\n说明:") + print(" - super_admin (超级管理员): 系统角色,不允许修改权限") + print(" - admin (管理员): 非系统角色,可以修改权限") + print(" - user (普通用户): 非系统角色,可以修改权限") + + +async def main(): + """主函数""" + print("=" * 80) + print("检查和修复角色 is_system 字段") + print("=" * 80) + print() + + try: + await check_and_fix_roles() + print() + print("=" * 80) + print("✓ 操作完成!现在可以在前端管理非系统角色的权限了") + print("=" * 80) + except Exception as e: + print(f"\n✗ 操作失败: {str(e)}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/backend/scripts/init_database.sql b/backend/scripts/init_database.sql index 4b4ba26..62795de 100644 --- a/backend/scripts/init_database.sql +++ b/backend/scripts/init_database.sql @@ -170,8 +170,7 @@ CREATE TABLE IF NOT EXISTS `operation_logs` ( INSERT INTO `roles` (`role_name`, `role_code`, `description`, `is_system`) VALUES ('超级管理员', 'super_admin', '拥有系统所有权限', 1), ('项目管理员', 'project_admin', '可以创建和管理项目', 1), -('普通用户', 'user', '可以查看和编辑被授权的项目', 1), -('访客', 'guest', '只读权限', 1); +('普通用户', 'user', '可以查看和编辑被授权的项目', 1); -- 插入初始菜单数据 INSERT INTO `system_menus` (`id`, `parent_id`, `menu_name`, `menu_code`, `menu_type`, `path`, `icon`, `sort_order`, `permission`) VALUES diff --git a/backend/scripts/init_db.py b/backend/scripts/init_db.py index ded6866..6b5c878 100644 --- a/backend/scripts/init_db.py +++ b/backend/scripts/init_db.py @@ -1,36 +1,401 @@ """ -数据库初始化 Python 脚本 -使用 SQLAlchemy 创建表结构 +数据库初始化脚本 +用于创建数据库表并插入基础数据 """ import sys import os +import asyncio from pathlib import Path # 添加项目根目录到 Python 路径 sys.path.insert(0, str(Path(__file__).parent.parent)) -from sqlalchemy import create_engine -from app.core.config import settings -from app.core.database import Base -from app.models import * # 导入所有模型 +from sqlalchemy import text +from app.core.database import engine, async_session +from app.models.user import User +from app.models.role import Role +from app.models.menu import SystemMenu +from app.models.project import Project, ProjectMember from app.core.security import get_password_hash -def init_database(): - """初始化数据库""" - print("开始初始化数据库...") +async def init_tables(): + """创建所有数据库表""" + print("正在创建数据库表...") + + # 导入所有模型以确保它们被注册 + from app.models import Base + + async with engine.begin() as conn: + # 创建所有表 + await conn.run_sync(Base.metadata.create_all) + + print("✓ 数据库表创建成功") - # 创建同步引擎(用于表创建) - engine = create_engine(settings.SYNC_DATABASE_URL, echo=True) - # 创建所有表 - print("创建数据库表...") - Base.metadata.create_all(bind=engine) +async def init_roles(): + """初始化系统角色""" + print("正在初始化系统角色...") + + async with async_session() as session: + # 检查是否已存在角色 + result = await session.execute(text("SELECT COUNT(*) FROM roles")) + count = result.scalar() + + if count > 0: + print(" 角色已存在,跳过初始化") + return + + # 创建默认角色 + roles = [ + Role( + role_name="超级管理员", + role_code="super_admin", + description="系统超级管理员,拥有所有权限", + status=1, + sort_order=1 + ), + Role( + role_name="管理员", + role_code="admin", + description="系统管理员", + status=1, + sort_order=2 + ), + Role( + role_name="普通用户", + role_code="user", + description="普通用户", + status=1, + sort_order=3 + ), + ] + + for role in roles: + session.add(role) + + await session.commit() + print("✓ 系统角色初始化成功") - print("✓ 数据库表创建完成") - print("\n请执行 SQL 脚本插入初始数据:") - print(f" mysql -h{settings.DB_HOST} -u{settings.DB_USER} -p{settings.DB_PASSWORD} {settings.DB_NAME} < scripts/init_database.sql") + +async def init_menus(): + """初始化系统菜单""" + print("正在初始化系统菜单...") + + async with async_session() as session: + # 检查是否已存在菜单 + result = await session.execute(text("SELECT COUNT(*) FROM system_menus")) + count = result.scalar() + + if count > 0: + print(" 菜单已存在,跳过初始化") + return + + # 创建系统菜单 + menus = [ + # 一级菜单 + SystemMenu( + id=1, + parent_id=0, + menu_name="项目空间", + menu_code="projects", + menu_type=0, + path="/projects", + icon="ProjectOutlined", + sort_order=3, + visible=1, + status=1 + ), + SystemMenu( + id=8, + parent_id=0, + menu_name="个人桌面", + menu_code="dashboard", + menu_type=1, + path="/dashboard", + component="Dashboard", + icon="DashboardOutlined", + sort_order=1, + visible=1, + status=1 + ), + SystemMenu( + id=9, + parent_id=0, + menu_name="管理面板", + menu_code="admin_panel", + menu_type=1, + path="/admin", + component="AdminPanel", + icon="ControlOutlined", + sort_order=2, + visible=1, + status=1 + ), + SystemMenu( + id=10, + parent_id=0, + menu_name="知识库空间", + menu_code="knowledge", + menu_type=0, + path="/knowledge", + icon="BookOutlined", + sort_order=4, + visible=1, + status=1 + ), + SystemMenu( + id=20, + parent_id=0, + menu_name="系统管理", + menu_code="system", + menu_type=0, + path="/system", + icon="SettingOutlined", + sort_order=5, + visible=1, + status=1 + ), + + # 项目空间子菜单 + SystemMenu( + id=2, + parent_id=1, + menu_name="我的项目", + menu_code="projects:my", + menu_type=1, + path="/projects/my", + component="MyProjects", + icon="FolderOutlined", + sort_order=1, + visible=1, + status=1 + ), + + # 我的项目子菜单(按钮权限) + SystemMenu( + id=3, + parent_id=2, + menu_name="创建项目", + menu_code="projects:create", + menu_type=2, + permission="projects:create", + sort_order=1, + visible=1, + status=1 + ), + SystemMenu( + id=4, + parent_id=2, + menu_name="编辑项目", + menu_code="projects:edit", + menu_type=2, + permission="projects:edit", + sort_order=2, + visible=1, + status=1 + ), + SystemMenu( + id=5, + parent_id=2, + menu_name="删除项目", + menu_code="projects:delete", + menu_type=2, + permission="projects:delete", + sort_order=3, + visible=1, + status=1 + ), + + # 知识库空间子菜单 + SystemMenu( + id=11, + parent_id=10, + menu_name="我的知识库", + menu_code="knowledge:my", + menu_type=1, + path="/knowledge/my", + component="MyKnowledge", + icon="ReadOutlined", + sort_order=1, + visible=1, + status=1 + ), + + # 我的知识库子菜单(按钮权限) + SystemMenu( + id=12, + parent_id=11, + menu_name="编辑知识库", + menu_code="knowledge:edit", + menu_type=2, + permission="knowledge:edit", + sort_order=1, + visible=1, + status=1 + ), + SystemMenu( + id=13, + parent_id=11, + menu_name="删除知识库", + menu_code="knowledge:delete", + menu_type=2, + permission="knowledge:delete", + sort_order=2, + visible=1, + status=1 + ), + + # 系统管理子菜单 + SystemMenu( + id=14, + parent_id=20, + menu_name="权限管理", + menu_code="system:permissions", + menu_type=1, + path="/system/permissions", + component="System/Permissions", + icon="SafetyOutlined", + sort_order=3, + visible=1, + status=1 + ), + SystemMenu( + id=15, + parent_id=20, + menu_name="用户管理", + menu_code="system:users", + menu_type=1, + path="/system/users", + component="System/Users", + icon="UserOutlined", + sort_order=1, + visible=1, + status=1 + ), + SystemMenu( + id=16, + parent_id=20, + menu_name="角色管理", + menu_code="system:roles", + menu_type=1, + path="/system/roles", + component="System/Roles", + icon="TeamOutlined", + sort_order=2, + visible=1, + status=1 + ), + ] + + for menu in menus: + session.add(menu) + + await session.commit() + print("✓ 系统菜单初始化成功") + + +async def init_admin_user(): + """初始化管理员用户""" + print("正在初始化管理员账号...") + + # 从环境变量获取管理员信息 + admin_username = os.getenv("ADMIN_USERNAME", "admin") + admin_password = os.getenv("ADMIN_PASSWORD", "Admin@123456") + admin_email = os.getenv("ADMIN_EMAIL", "admin@example.com") + admin_nickname = os.getenv("ADMIN_NICKNAME", "系统管理员") + + async with async_session() as session: + # 检查管理员是否已存在 + result = await session.execute( + text("SELECT COUNT(*) FROM users WHERE username = :username"), + {"username": admin_username} + ) + count = result.scalar() + + if count > 0: + print(f" 管理员账号 {admin_username} 已存在,跳过初始化") + return + + # 创建管理员用户 + admin_user = User( + username=admin_username, + password_hash=get_password_hash(admin_password), + nickname=admin_nickname, + email=admin_email, + status=1, + is_superuser=True + ) + + session.add(admin_user) + await session.flush() + + # 获取超级管理员角色 + result = await session.execute( + text("SELECT id FROM roles WHERE role_code = 'super_admin' LIMIT 1") + ) + role_id = result.scalar() + + if role_id: + # 分配角色 + await session.execute( + text("INSERT INTO user_roles (user_id, role_id) VALUES (:user_id, :role_id)"), + {"user_id": admin_user.id, "role_id": role_id} + ) + + # 为超级管理员角色分配菜单权限:ID 9、20 及其所有子菜单 + # ID 9: 管理面板 + # ID 20: 系统管理及其子菜单 (14, 15, 16) + authorized_menu_ids = [9, 20, 14, 15, 16] + + if role_id: + for menu_id in authorized_menu_ids: + await session.execute( + text("INSERT INTO role_menus (role_id, menu_id) VALUES (:role_id, :menu_id)"), + {"role_id": role_id, "menu_id": menu_id} + ) + + await session.commit() + + print("✓ 管理员账号初始化成功") + print(f" 用户名: {admin_username}") + print(f" 密码: {admin_password}") + print(f" 邮箱: {admin_email}") + print(f" 已授权菜单: ID {', '.join(map(str, authorized_menu_ids))}") + print(" ⚠️ 请登录后及时修改默认密码!") + + +async def main(): + """主函数""" + print("=" * 60) + print("NEX Docus 数据库初始化") + print("=" * 60) + + try: + # 1. 创建数据库表 + await init_tables() + + # 2. 初始化角色 + await init_roles() + + # 3. 初始化菜单 + await init_menus() + + # 4. 初始化管理员 + await init_admin_user() + + print("\n" + "=" * 60) + print("✓ 数据库初始化完成!") + print("=" * 60) + + except Exception as e: + print(f"\n✗ 初始化失败: {str(e)}") + import traceback + traceback.print_exc() + sys.exit(1) + finally: + await engine.dispose() if __name__ == "__main__": - init_database() + asyncio.run(main()) diff --git a/backend/scripts/update_system_menu_paths.py b/backend/scripts/update_system_menu_paths.py new file mode 100644 index 0000000..263fad7 --- /dev/null +++ b/backend/scripts/update_system_menu_paths.py @@ -0,0 +1,75 @@ +""" +更新系统管理菜单的路径 +""" +import sys +import asyncio +from pathlib import Path + +# 添加项目根目录到 Python 路径 +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from sqlalchemy import text +from app.core.database import async_session + + +async def update_menu_paths(): + """更新菜单路径""" + print("正在更新系统管理菜单路径...") + + async with async_session() as session: + # 1. 更新角色权限管理菜单路径 + result = await session.execute( + text(""" + UPDATE system_menus + SET path = '/system/permissions', component = 'System/Permissions' + WHERE id = 14 + """) + ) + print(f"✓ 角色权限管理菜单路径已更新: /system/permissions") + + # 2. 更新用户管理菜单路径 + result = await session.execute( + text(""" + UPDATE system_menus + SET path = '/system/users', component = 'System/Users' + WHERE id = 15 + """) + ) + print(f"✓ 用户管理菜单路径已更新: /system/users") + + # 3. 更新角色管理菜单路径 + result = await session.execute( + text(""" + UPDATE system_menus + SET path = '/system/roles', component = 'System/Roles' + WHERE id = 16 + """) + ) + print(f"✓ 角色管理菜单路径已更新: /system/roles") + + await session.commit() + print("\n✓ 所有菜单路径更新完成!") + + +async def main(): + """主函数""" + print("=" * 80) + print("更新系统管理菜单路径") + print("=" * 80) + print() + + try: + await update_menu_paths() + print() + print("=" * 80) + print("✓ 操作完成!现在可以通过新路径访问系统管理菜单了") + print("=" * 80) + except Exception as e: + print(f"\n✗ 操作失败: {str(e)}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..87b77ce --- /dev/null +++ b/deploy.sh @@ -0,0 +1,382 @@ +#!/bin/bash + +# NEX Docus 部署管理脚本 +# 支持:初始化、启动、停止、重启、升级、日志查看等功能 + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 项目目录 +PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$PROJECT_DIR" + +# 检查 .env 文件 +check_env() { + if [ ! -f ".env" ]; then + echo -e "${YELLOW}警告: .env 文件不存在${NC}" + echo -e "${BLUE}正在从 .env.example 创建 .env 文件...${NC}" + cp .env.example .env + echo -e "${GREEN}✓ .env 文件已创建${NC}" + echo -e "${YELLOW}请编辑 .env 文件,配置数据库和其他参数后再继续!${NC}" + exit 1 + fi +} + +# 检查 Docker 和 Docker Compose +check_docker() { + if ! command -v docker &> /dev/null; then + echo -e "${RED}错误: Docker 未安装${NC}" + echo "请访问 https://docs.docker.com/get-docker/ 安装 Docker" + exit 1 + fi + + if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null 2>&1; then + echo -e "${RED}错误: Docker Compose 未安装${NC}" + echo "请访问 https://docs.docker.com/compose/install/ 安装 Docker Compose" + exit 1 + fi +} + +# 获取 docker-compose 命令 +get_compose_cmd() { + if docker compose version &> /dev/null 2>&1; then + echo "docker compose" + else + echo "docker-compose" + fi +} + +# 初始化部署 +init() { + echo -e "${BLUE}开始初始化 NEX Docus...${NC}" + echo "======================================" + + check_env + check_docker + + local COMPOSE_CMD=$(get_compose_cmd) + + # 1. 拉取镜像 + echo -e "\n${BLUE}1. 拉取 Docker 镜像...${NC}" + $COMPOSE_CMD pull + + # 2. 构建服务 + echo -e "\n${BLUE}2. 构建应用镜像...${NC}" + $COMPOSE_CMD build --no-cache + + # 3. 启动数据库和 Redis + echo -e "\n${BLUE}3. 启动数据库和缓存服务...${NC}" + $COMPOSE_CMD up -d mysql redis + + # 等待数据库就绪 + echo -e "${BLUE}等待数据库就绪...${NC}" + sleep 15 + + # 4. 初始化数据库 + echo -e "\n${BLUE}4. 初始化数据库...${NC}" + $COMPOSE_CMD run --rm backend python scripts/init_db.py + + # 5. 启动所有服务 + echo -e "\n${BLUE}5. 启动所有服务...${NC}" + $COMPOSE_CMD up -d + + # 6. 显示状态 + echo -e "\n${GREEN}======================================" + echo -e "✓ NEX Docus 初始化完成!" + echo -e "======================================${NC}" + show_info +} + +# 启动服务 +start() { + echo -e "${BLUE}启动 NEX Docus 服务...${NC}" + check_docker + local COMPOSE_CMD=$(get_compose_cmd) + $COMPOSE_CMD up -d + echo -e "${GREEN}✓ 服务已启动${NC}" + show_status +} + +# 停止服务 +stop() { + echo -e "${YELLOW}停止 NEX Docus 服务...${NC}" + check_docker + local COMPOSE_CMD=$(get_compose_cmd) + $COMPOSE_CMD stop + echo -e "${GREEN}✓ 服务已停止${NC}" +} + +# 重启服务 +restart() { + echo -e "${BLUE}重启 NEX Docus 服务...${NC}" + stop + sleep 2 + start +} + +# 查看服务状态 +status() { + check_docker + local COMPOSE_CMD=$(get_compose_cmd) + $COMPOSE_CMD ps +} + +# 查看日志 +logs() { + check_docker + local COMPOSE_CMD=$(get_compose_cmd) + + if [ -z "$1" ]; then + echo -e "${BLUE}查看所有服务日志 (Ctrl+C 退出)${NC}" + $COMPOSE_CMD logs -f + else + echo -e "${BLUE}查看 $1 服务日志 (Ctrl+C 退出)${NC}" + $COMPOSE_CMD logs -f "$1" + fi +} + +# 升级部署 +upgrade() { + echo -e "${BLUE}开始升级 NEX Docus...${NC}" + echo "======================================" + + check_docker + local COMPOSE_CMD=$(get_compose_cmd) + + # 1. 备份数据 + echo -e "\n${YELLOW}1. 建议先备份数据库...${NC}" + read -p "是否继续升级?(y/n) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo -e "${YELLOW}升级已取消${NC}" + exit 0 + fi + + # 2. 拉取最新代码 + echo -e "\n${BLUE}2. 拉取最新代码...${NC}" + if [ -d ".git" ]; then + git pull + else + echo -e "${YELLOW}未检测到 Git 仓库,跳过拉取代码${NC}" + fi + + # 3. 停止服务 + echo -e "\n${BLUE}3. 停止当前服务...${NC}" + $COMPOSE_CMD stop backend frontend + + # 4. 构建新镜像 + echo -e "\n${BLUE}4. 构建新镜像...${NC}" + $COMPOSE_CMD build --no-cache backend frontend + + # 5. 运行数据库迁移 + echo -e "\n${BLUE}5. 更新数据库...${NC}" + $COMPOSE_CMD run --rm backend python scripts/init_db.py + + # 6. 启动服务 + echo -e "\n${BLUE}6. 启动服务...${NC}" + $COMPOSE_CMD up -d + + # 7. 清理旧镜像 + echo -e "\n${BLUE}7. 清理旧镜像...${NC}" + docker image prune -f + + echo -e "\n${GREEN}======================================" + echo -e "✓ NEX Docus 升级完成!" + echo -e "======================================${NC}" + show_info +} + +# 完全卸载 +uninstall() { + echo -e "${RED}警告: 此操作将删除所有容器、镜像和数据!${NC}" + read -p "确认要卸载吗?(yes/no) " -r + echo + if [[ ! $REPLY == "yes" ]]; then + echo -e "${YELLOW}卸载已取消${NC}" + exit 0 + fi + + check_docker + local COMPOSE_CMD=$(get_compose_cmd) + + echo -e "${YELLOW}停止并删除所有容器和数据...${NC}" + $COMPOSE_CMD down -v --remove-orphans + + echo -e "${YELLOW}删除所有镜像...${NC}" + docker images | grep "nex-docus" | awk '{print $3}' | xargs -r docker rmi -f + + echo -e "${GREEN}✓ NEX Docus 已卸载${NC}" +} + +# 备份数据库 +backup() { + check_docker + local COMPOSE_CMD=$(get_compose_cmd) + + BACKUP_DIR="./backups" + mkdir -p "$BACKUP_DIR" + + BACKUP_FILE="$BACKUP_DIR/nex_docus_$(date +%Y%m%d_%H%M%S).sql" + + echo -e "${BLUE}正在备份数据库...${NC}" + + # 从 .env 读取数据库配置 + source .env + + $COMPOSE_CMD exec -T mysql mysqldump \ + -u"${DB_USER}" \ + -p"${DB_PASSWORD}" \ + "${DB_NAME}" > "$BACKUP_FILE" + + if [ -f "$BACKUP_FILE" ]; then + echo -e "${GREEN}✓ 数据库备份成功!${NC}" + echo -e "备份文件: ${BACKUP_FILE}" + else + echo -e "${RED}✗ 数据库备份失败${NC}" + exit 1 + fi +} + +# 恢复数据库 +restore() { + if [ -z "$1" ]; then + echo -e "${RED}错误: 请指定备份文件${NC}" + echo "用法: $0 restore " + exit 1 + fi + + if [ ! -f "$1" ]; then + echo -e "${RED}错误: 备份文件不存在: $1${NC}" + exit 1 + fi + + check_docker + local COMPOSE_CMD=$(get_compose_cmd) + + echo -e "${YELLOW}警告: 此操作将覆盖当前数据库!${NC}" + read -p "确认要恢复吗?(yes/no) " -r + echo + if [[ ! $REPLY == "yes" ]]; then + echo -e "${YELLOW}恢复已取消${NC}" + exit 0 + fi + + echo -e "${BLUE}正在恢复数据库...${NC}" + + # 从 .env 读取数据库配置 + source .env + + $COMPOSE_CMD exec -T mysql mysql \ + -u"${DB_USER}" \ + -p"${DB_PASSWORD}" \ + "${DB_NAME}" < "$1" + + echo -e "${GREEN}✓ 数据库恢复成功!${NC}" +} + +# 显示访问信息 +show_info() { + source .env 2>/dev/null || true + + echo "" + echo -e "${GREEN}访问信息:${NC}" + echo " 前端地址: http://localhost:${FRONTEND_PORT:-8080}" + echo " 后端地址: http://localhost:${BACKEND_PORT:-8000}" + echo " API 文档: http://localhost:${BACKEND_PORT:-8000}/docs" + echo "" + echo -e "${GREEN}管理员账号:${NC}" + echo " 用户名: ${ADMIN_USERNAME:-admin}" + echo " 密码: ${ADMIN_PASSWORD:-Admin@123456}" + echo "" + echo -e "${YELLOW}提示: 请及时修改默认密码!${NC}" + echo "" +} + +# 显示状态 +show_status() { + echo "" + echo -e "${BLUE}服务状态:${NC}" + status +} + +# 显示帮助 +help() { + echo "NEX Docus 部署管理脚本" + echo "" + echo "用法: $0 [options]" + echo "" + echo "命令:" + echo " init 初始化并部署(首次部署使用)" + echo " start 启动所有服务" + echo " stop 停止所有服务" + echo " restart 重启所有服务" + echo " status 查看服务状态" + echo " logs [服务名] 查看日志(可选指定服务:backend/frontend/mysql/redis)" + echo " upgrade 升级部署" + echo " backup 备份数据库" + echo " restore 恢复数据库" + echo " uninstall 完全卸载" + echo " help 显示帮助信息" + echo "" + echo "示例:" + echo " $0 init # 首次部署" + echo " $0 start # 启动服务" + echo " $0 logs backend # 查看后端日志" + echo " $0 backup # 备份数据库" + echo " $0 upgrade # 升级到最新版本" + echo "" +} + +# 主函数 +main() { + case "${1:-help}" in + init) + init + ;; + start) + start + ;; + stop) + stop + ;; + restart) + restart + ;; + status) + status + ;; + logs) + logs "$2" + ;; + upgrade) + upgrade + ;; + backup) + backup + ;; + restore) + restore "$2" + ;; + uninstall) + uninstall + ;; + help|--help|-h) + help + ;; + *) + echo -e "${RED}错误: 未知命令 '$1'${NC}" + echo "" + help + exit 1 + ;; + esac +} + +# 执行主函数 +main "$@" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..84e3d47 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,113 @@ +version: '3.8' + +services: + # MySQL 数据库 + mysql: + image: mysql:8.0 + container_name: nex-docus-mysql + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root_password_change_me} + MYSQL_DATABASE: ${DB_NAME:-nex_docus} + MYSQL_USER: ${DB_USER:-nexdocus} + MYSQL_PASSWORD: ${DB_PASSWORD:-password_change_me} + TZ: Asia/Shanghai + volumes: + - mysql_data:/var/lib/mysql + - ./backend/scripts/init.sql:/docker-entrypoint-initdb.d/init.sql:ro + ports: + - "${MYSQL_PORT:-3306}:3306" + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + - --default-authentication-plugin=mysql_native_password + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-root_password_change_me}"] + interval: 10s + timeout: 5s + retries: 5 + + # Redis 缓存 + redis: + image: redis:7-alpine + container_name: nex-docus-redis + restart: unless-stopped + command: redis-server --requirepass ${REDIS_PASSWORD:-redis_password_change_me} --appendonly yes + environment: + TZ: Asia/Shanghai + volumes: + - redis_data:/data + ports: + - "${REDIS_PORT:-6379}:6379" + healthcheck: + test: ["CMD", "redis-cli", "--raw", "incr", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + # 后端服务 + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: nex-docus-backend + restart: unless-stopped + environment: + - DB_HOST=mysql + - DB_PORT=3306 + - DB_USER=${DB_USER:-nexdocus} + - DB_PASSWORD=${DB_PASSWORD:-password_change_me} + - DB_NAME=${DB_NAME:-nex_docus} + - REDIS_HOST=redis + - REDIS_PORT=6379 + - REDIS_PASSWORD=${REDIS_PASSWORD:-redis_password_change_me} + - REDIS_DB=${REDIS_DB:-8} + - SECRET_KEY=${SECRET_KEY:-your-secret-key-change-me-in-production} + - DEBUG=${DEBUG:-false} + - TZ=Asia/Shanghai + volumes: + - ${STORAGE_PATH:-./storage}:/data/nex_docus_store + - ./backend/logs:/app/logs + ports: + - "${BACKEND_PORT:-8000}:8000" + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/docs"] + interval: 30s + timeout: 10s + retries: 3 + + # 前端服务 + frontend: + build: + context: ./forntend + dockerfile: Dockerfile + args: + - VITE_API_BASE_URL=${VITE_API_BASE_URL:-http://localhost:8000} + container_name: nex-docus-frontend + restart: unless-stopped + environment: + - TZ=Asia/Shanghai + ports: + - "${FRONTEND_PORT:-8080}:80" + depends_on: + - backend + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80"] + interval: 30s + timeout: 10s + retries: 3 + +volumes: + mysql_data: + driver: local + redis_data: + driver: local + +networks: + default: + name: nex-docus-network diff --git a/forntend/.dockerignore b/forntend/.dockerignore new file mode 100644 index 0000000..acc1111 --- /dev/null +++ b/forntend/.dockerignore @@ -0,0 +1,37 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Build +dist/ +build/ +.cache/ + +# Testing +coverage/ + +# IDE +.idea/ +.vscode/ +*.swp +*.sw? + +# OS +.DS_Store +Thumbs.db + +# Environment +.env +.env.local +.env.*.local + +# Git +.git/ +.gitignore + +# Logs +logs/ +*.log diff --git a/forntend/Dockerfile b/forntend/Dockerfile new file mode 100644 index 0000000..1fa91cc --- /dev/null +++ b/forntend/Dockerfile @@ -0,0 +1,35 @@ +# 构建阶段 +FROM node:18-alpine AS builder + +# 设置工作目录 +WORKDIR /app + +# 设置 npm 使用淘宝镜像 +RUN npm config set registry https://registry.npmmirror.com + +# 复制 package 文件 +COPY package.json yarn.lock* package-lock.json* ./ + +# 安装依赖 +RUN npm install + +# 复制项目文件 +COPY . . + +# 构建生产版本 +RUN npm run build + +# 生产阶段 +FROM nginx:alpine + +# 复制构建产物到 nginx +COPY --from=builder /app/dist /usr/share/nginx/html + +# 复制 nginx 配置 +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# 暴露端口 +EXPOSE 80 + +# 启动 nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/forntend/nginx.conf b/forntend/nginx.conf new file mode 100644 index 0000000..062b269 --- /dev/null +++ b/forntend/nginx.conf @@ -0,0 +1,30 @@ +server { + listen 80; + server_name localhost; + + root /usr/share/nginx/html; + index index.html; + + # Gzip 压缩 + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml+rss; + + # 前端路由支持 + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } + + # 静态资源缓存 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # 禁止访问隐藏文件 + location ~ /\. { + deny all; + } +} diff --git a/forntend/src/App.jsx b/forntend/src/App.jsx index c5ff612..251b300 100644 --- a/forntend/src/App.jsx +++ b/forntend/src/App.jsx @@ -10,6 +10,9 @@ import Desktop from '@/pages/Desktop' import Constructing from '@/pages/Constructing' import PreviewPage from '@/pages/Preview/PreviewPage' import ProfilePage from '@/pages/Profile/ProfilePage' +import Permissions from '@/pages/System/Permissions' +import Users from '@/pages/System/Users' +import Roles from '@/pages/System/Roles' import ProtectedRoute from '@/components/ProtectedRoute' import '@/App.css' @@ -81,6 +84,33 @@ function App() { } /> + {/* 角色权限管理 */} + + + + } + /> + {/* 用户管理 */} + + + + } + /> + {/* 角色管理 */} + + + + } + /> } /> diff --git a/forntend/src/api/rolePermissions.js b/forntend/src/api/rolePermissions.js new file mode 100644 index 0000000..33e3c56 --- /dev/null +++ b/forntend/src/api/rolePermissions.js @@ -0,0 +1,47 @@ +/** + * 角色权限管理相关 API + */ +import request from '@/utils/request' + +/** + * 获取所有角色列表 + */ +export function getAllRoles() { + return request({ + url: '/role-permissions/roles', + method: 'get', + }) +} + +/** + * 获取完整的菜单权限树 + */ +export function getMenuTree() { + return request({ + url: '/role-permissions/menu-tree', + method: 'get', + }) +} + +/** + * 获取指定角色的权限 + */ +export function getRolePermissions(roleId) { + return request({ + url: `/role-permissions/roles/${roleId}/permissions`, + method: 'get', + }) +} + +/** + * 更新角色权限 + */ +export function updateRolePermissions(roleId, menuIds) { + return request({ + url: `/role-permissions/roles/${roleId}/permissions`, + method: 'put', + data: { + menu_ids: menuIds, + }, + }) +} diff --git a/forntend/src/api/roles.js b/forntend/src/api/roles.js new file mode 100644 index 0000000..5e0e2d5 --- /dev/null +++ b/forntend/src/api/roles.js @@ -0,0 +1,68 @@ +/** + * 角色管理相关 API + */ +import request from '@/utils/request' + +/** + * 获取角色列表 + */ +export function getRoleList(params) { + return request({ + url: '/roles/', + method: 'get', + params, + }) +} + +/** + * 获取角色详情 + */ +export function getRoleDetail(roleId) { + return request({ + url: `/roles/${roleId}`, + method: 'get', + }) +} + +/** + * 创建新角色 + */ +export function createRole(data) { + return request({ + url: '/roles/', + method: 'post', + data, + }) +} + +/** + * 更新角色信息 + */ +export function updateRole(roleId, data) { + return request({ + url: `/roles/${roleId}`, + method: 'put', + data, + }) +} + +/** + * 删除角色 + */ +export function deleteRole(roleId) { + return request({ + url: `/roles/${roleId}`, + method: 'delete', + }) +} + +/** + * 获取角色下的用户列表 + */ +export function getRoleUsers(roleId, params) { + return request({ + url: `/roles/${roleId}/users`, + method: 'get', + params, + }) +} diff --git a/forntend/src/api/users.js b/forntend/src/api/users.js new file mode 100644 index 0000000..9cb2c3e --- /dev/null +++ b/forntend/src/api/users.js @@ -0,0 +1,89 @@ +/** + * 用户管理相关 API + */ +import request from '@/utils/request' + +/** + * 获取用户列表 + */ +export function getUserList(params) { + return request({ + url: '/users/', + method: 'get', + params, + }) +} + +/** + * 获取用户详情 + */ +export function getUserDetail(userId) { + return request({ + url: `/users/${userId}`, + method: 'get', + }) +} + +/** + * 创建新用户 + */ +export function createUser(data) { + return request({ + url: '/users/', + method: 'post', + data, + }) +} + +/** + * 更新用户信息 + */ +export function updateUser(userId, data) { + return request({ + url: `/users/${userId}`, + method: 'put', + data, + }) +} + +/** + * 删除用户 + */ +export function deleteUser(userId) { + return request({ + url: `/users/${userId}`, + method: 'delete', + }) +} + +/** + * 更新用户状态(停用/启用) + */ +export function updateUserStatus(userId, status) { + return request({ + url: `/users/${userId}/status`, + method: 'put', + params: { status }, + }) +} + +/** + * 更新用户角色 + */ +export function updateUserRoles(userId, roleIds) { + return request({ + url: `/users/${userId}/roles`, + method: 'put', + data: { role_ids: roleIds }, + }) +} + +/** + * 重置用户密码 + */ +export function resetUserPassword(userId) { + return request({ + url: `/users/${userId}/reset-password`, + method: 'post', + }) +} diff --git a/forntend/src/components/MainLayout/AppSider.jsx b/forntend/src/components/MainLayout/AppSider.jsx index 7bddd6b..d20cae7 100644 --- a/forntend/src/components/MainLayout/AppSider.jsx +++ b/forntend/src/components/MainLayout/AppSider.jsx @@ -12,6 +12,8 @@ import { BlockOutlined, FolderOutlined, FileTextOutlined, + SafetyOutlined, + TeamOutlined, } from '@ant-design/icons' import { getUserMenus } from '@/api/menu' import './AppSider.css' @@ -30,6 +32,8 @@ const iconMap = { BlockOutlined, FolderOutlined, FileTextOutlined, + SafetyOutlined, + TeamOutlined, } function AppSider({ collapsed, onToggle }) { diff --git a/forntend/src/pages/Login/Login.jsx b/forntend/src/pages/Login/Login.jsx index fbb353c..5ca5fa7 100644 --- a/forntend/src/pages/Login/Login.jsx +++ b/forntend/src/pages/Login/Login.jsx @@ -1,6 +1,6 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' -import { Form, Input, Button, Card, Tabs } from 'antd' +import { Form, Input, Button, Card, Tabs, Checkbox } from 'antd' import { UserOutlined, LockOutlined, MailOutlined } from '@ant-design/icons' import { login, register } from '@/api/auth' import { getUserMenus } from '@/api/menu' @@ -11,15 +11,33 @@ import './Login.css' function Login() { const [loading, setLoading] = useState(false) const [activeTab, setActiveTab] = useState('login') + const [rememberMe, setRememberMe] = useState(false) + const [loginForm] = Form.useForm() const navigate = useNavigate() const { setUser, setToken } = useUserStore() + // 组件加载时检查是否有保存的用户名 + useEffect(() => { + const savedUsername = localStorage.getItem('remembered_username') + if (savedUsername) { + loginForm.setFieldsValue({ username: savedUsername }) + setRememberMe(true) + } + }, []) + const handleLogin = async (values) => { setLoading(true) try { const res = await login(values) Toast.success('登录成功') + // 处理"记住用户" + if (rememberMe) { + localStorage.setItem('remembered_username', values.username) + } else { + localStorage.removeItem('remembered_username') + } + // 保存 token 和用户信息 localStorage.setItem('access_token', res.data.access_token) localStorage.setItem('user_info', JSON.stringify(res.data.user)) @@ -84,6 +102,7 @@ function Login() {
+ + setRememberMe(e.target.checked)} + > + 记住用户 + + + + } + > + {selectedRole ? ( +
+ + 当前角色: + {selectedRole.role_name} + {selectedRole.is_system === 1 && 系统角色(只读)} + +
+ ) : ( +
+ 请从右侧选择一个角色来查看和编辑权限 +
+ )} + + + + + {/* 右侧:角色列表 */} + + + {}} + /> + + + + + + ) +} + +export default Permissions diff --git a/forntend/src/pages/System/Roles.jsx b/forntend/src/pages/System/Roles.jsx new file mode 100644 index 0000000..a2febc8 --- /dev/null +++ b/forntend/src/pages/System/Roles.jsx @@ -0,0 +1,492 @@ +import { useState, useEffect } from 'react' +import { + Button, + Modal, + Form, + Input, + Select, + Tag, + Space, + Popconfirm, + Card, + Row, + Col, +} from 'antd' +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, + TeamOutlined, + CheckCircleOutlined, + CloseCircleOutlined, + SearchOutlined, +} from '@ant-design/icons' +import { + getRoleList, + createRole, + updateRole, + deleteRole, + getRoleUsers, +} from '@/api/roles' +import MainLayout from '@/components/MainLayout/MainLayout' +import ListTable from '@/components/ListTable/ListTable' +import Toast from '@/components/Toast/Toast' + +function Roles() { + const [loading, setLoading] = useState(false) + const [roles, setRoles] = useState([]) + const [total, setTotal] = useState(0) + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(10) + const [keyword, setKeyword] = useState('') + const [statusFilter, setStatusFilter] = useState(null) + + const [createModalVisible, setCreateModalVisible] = useState(false) + const [editModalVisible, setEditModalVisible] = useState(false) + const [usersModalVisible, setUsersModalVisible] = useState(false) + const [currentRole, setCurrentRole] = useState(null) + const [roleUsers, setRoleUsers] = useState([]) + const [usersLoading, setUsersLoading] = useState(false) + const [usersPage, setUsersPage] = useState(1) + const [usersPageSize, setUsersPageSize] = useState(10) + const [usersTotal, setUsersTotal] = useState(0) + + const [createForm] = Form.useForm() + const [editForm] = Form.useForm() + + useEffect(() => { + loadRoles() + }, [page, pageSize, keyword, statusFilter]) + + const loadRoles = async () => { + try { + setLoading(true) + const params = { + page, + page_size: pageSize, + } + if (keyword) params.keyword = keyword + if (statusFilter !== null) params.status = statusFilter + + const res = await getRoleList(params) + if (res.data) { + setRoles(res.data) + setTotal(res.total) + } + } catch (error) { + console.error('Load roles error:', error) + Toast.error('加载角色列表失败') + } finally { + setLoading(false) + } + } + + const loadRoleUsers = async (roleId) => { + try { + setUsersLoading(true) + const res = await getRoleUsers(roleId, { + page: usersPage, + page_size: usersPageSize, + }) + if (res.data) { + setRoleUsers(res.data) + setUsersTotal(res.total) + } + } catch (error) { + console.error('Load role users error:', error) + Toast.error('加载用户列表失败') + } finally { + setUsersLoading(false) + } + } + + // 创建角色 + const handleCreate = async (values) => { + try { + await createRole(values) + Toast.success('角色创建成功') + setCreateModalVisible(false) + createForm.resetFields() + loadRoles() + } catch (error) { + Toast.error(error.response?.data?.detail || '创建角色失败') + } + } + + // 编辑角色 + const handleEdit = async (values) => { + try { + await updateRole(currentRole.id, values) + Toast.success('角色信息更新成功') + setEditModalVisible(false) + editForm.resetFields() + loadRoles() + } catch (error) { + Toast.error(error.response?.data?.detail || '更新角色失败') + } + } + + // 删除角色 + const handleDelete = async (roleId) => { + try { + await deleteRole(roleId) + Toast.success('角色删除成功') + loadRoles() + } catch (error) { + Toast.error(error.response?.data?.detail || '删除角色失败') + } + } + + // 查看角色下的用户 + const handleViewUsers = (role) => { + setCurrentRole(role) + setUsersPage(1) + setUsersModalVisible(true) + } + + useEffect(() => { + if (usersModalVisible && currentRole) { + loadRoleUsers(currentRole.id) + } + }, [usersModalVisible, currentRole, usersPage, usersPageSize]) + + const columns = [ + { + title: 'ID', + dataIndex: 'id', + key: 'id', + width: 80, + }, + { + title: '角色名称', + dataIndex: 'role_name', + key: 'role_name', + }, + { + title: '角色编码', + dataIndex: 'role_code', + key: 'role_code', + }, + { + title: '描述', + dataIndex: 'description', + key: 'description', + ellipsis: true, + }, + { + title: '用户数', + dataIndex: 'user_count', + key: 'user_count', + render: (count) => {count}, + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + render: (status) => + status === 1 ? ( + } color="success"> + 启用 + + ) : ( + } color="error"> + 禁用 + + ), + }, + { + title: '系统角色', + dataIndex: 'is_system', + key: 'is_system', + render: (isSystem) => + isSystem === 1 ? : , + }, + { + title: '创建时间', + dataIndex: 'created_at', + key: 'created_at', + render: (text) => (text ? new Date(text).toLocaleString('zh-CN') : '-'), + }, + { + title: '操作', + key: 'action', + fixed: 'right', + width: 220, + render: (_, record) => ( + + + + handleDelete(record.id)} + okText="确定" + cancelText="取消" + disabled={record.is_system === 1} + > + + + + ), + }, + ] + + const userColumns = [ + { + title: 'ID', + dataIndex: 'id', + key: 'id', + width: 80, + }, + { + title: '用户名', + dataIndex: 'username', + key: 'username', + }, + { + title: '昵称', + dataIndex: 'nickname', + key: 'nickname', + }, + { + title: '邮箱', + dataIndex: 'email', + key: 'email', + }, + { + title: '手机号', + dataIndex: 'phone', + key: 'phone', + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + render: (status) => + status === 1 ? ( + } color="success"> + 启用 + + ) : ( + } color="error"> + 停用 + + ), + }, + { + title: '创建时间', + dataIndex: 'created_at', + key: 'created_at', + render: (text) => (text ? new Date(text).toLocaleString('zh-CN') : '-'), + }, + ] + + return ( + +
+

角色管理

+ + {/* 搜索和操作栏 */} + + + + } + value={keyword} + onChange={(e) => setKeyword(e.target.value)} + allowClear + /> + + + + + + + + + + + {/* 角色列表 */} + + `共 ${total} 条`, + onChange: (page, pageSize) => { + setPage(page) + setPageSize(pageSize) + }, + }} + onSelectionChange={() => {}} + /> + + + {/* 创建角色对话框 */} + { + setCreateModalVisible(false) + createForm.resetFields() + }} + onOk={() => createForm.submit()} + > + + + + + + + + + + + + + + + + + {/* 编辑角色对话框 */} + { + setEditModalVisible(false) + editForm.resetFields() + }} + onOk={() => editForm.submit()} + > +
+ + + + + + + + + + + + +
+
+ + {/* 角色用户列表对话框 */} + { + setUsersModalVisible(false) + setRoleUsers([]) + setUsersPage(1) + }} + footer={null} + width={1000} + > + `共 ${total} 条`, + onChange: (page, pageSize) => { + setUsersPage(page) + setUsersPageSize(pageSize) + }, + }} + onSelectionChange={() => {}} + /> + +
+
+ ) +} + +export default Roles diff --git a/forntend/src/pages/System/Users.jsx b/forntend/src/pages/System/Users.jsx new file mode 100644 index 0000000..24f9ae1 --- /dev/null +++ b/forntend/src/pages/System/Users.jsx @@ -0,0 +1,466 @@ +import { useState, useEffect } from 'react' +import { + Button, + Modal, + Form, + Input, + Select, + Tag, + Space, + Popconfirm, + Switch, + Card, + Row, + Col, +} from 'antd' +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, + KeyOutlined, + TeamOutlined, + SearchOutlined, + CheckCircleOutlined, + CloseCircleOutlined, +} from '@ant-design/icons' +import { + getUserList, + createUser, + updateUser, + deleteUser, + updateUserStatus, + updateUserRoles, + resetUserPassword, +} from '@/api/users' +import { getAllRoles } from '@/api/rolePermissions' +import MainLayout from '@/components/MainLayout/MainLayout' +import ListTable from '@/components/ListTable/ListTable' +import Toast from '@/components/Toast/Toast' + +function Users() { + const [loading, setLoading] = useState(false) + const [users, setUsers] = useState([]) + const [total, setTotal] = useState(0) + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(10) + const [keyword, setKeyword] = useState('') + const [statusFilter, setStatusFilter] = useState(null) + + const [roles, setRoles] = useState([]) + const [createModalVisible, setCreateModalVisible] = useState(false) + const [editModalVisible, setEditModalVisible] = useState(false) + const [rolesModalVisible, setRolesModalVisible] = useState(false) + const [currentUser, setCurrentUser] = useState(null) + + const [createForm] = Form.useForm() + const [editForm] = Form.useForm() + const [rolesForm] = Form.useForm() + + useEffect(() => { + loadUsers() + loadRoles() + }, [page, pageSize, keyword, statusFilter]) + + const loadUsers = async () => { + try { + setLoading(true) + const params = { + page, + page_size: pageSize, + } + if (keyword) params.keyword = keyword + if (statusFilter !== null) params.status = statusFilter + + const res = await getUserList(params) + if (res.data) { + setUsers(res.data) + setTotal(res.total) + } + } catch (error) { + console.error('Load users error:', error) + Toast.error('加载用户列表失败') + } finally { + setLoading(false) + } + } + + const loadRoles = async () => { + try { + const res = await getAllRoles() + if (res.data) { + setRoles(res.data) + } + } catch (error) { + console.error('Load roles error:', error) + } + } + + // 创建用户 + const handleCreate = async (values) => { + try { + const res = await createUser(values) + if (res.code === 200) { + Toast.success(`用户创建成功!初始密码:${res.data.default_password}`) + setCreateModalVisible(false) + createForm.resetFields() + loadUsers() + } + } catch (error) { + Toast.error(error.response?.data?.detail || '创建用户失败') + } + } + + // 编辑用户 + const handleEdit = async (values) => { + try { + await updateUser(currentUser.id, values) + Toast.success('用户信息更新成功') + setEditModalVisible(false) + editForm.resetFields() + loadUsers() + } catch (error) { + Toast.error(error.response?.data?.detail || '更新用户失败') + } + } + + // 删除用户 + const handleDelete = async (userId) => { + try { + await deleteUser(userId) + Toast.success('用户删除成功') + loadUsers() + } catch (error) { + Toast.error(error.response?.data?.detail || '删除用户失败') + } + } + + // 切换用户状态 + const handleStatusChange = async (userId, checked) => { + try { + const status = checked ? 1 : 0 + await updateUserStatus(userId, status) + Toast.success(checked ? '用户已启用' : '用户已停用') + loadUsers() + } catch (error) { + Toast.error(error.response?.data?.detail || '操作失败') + } + } + + // 分配角色 + const handleAssignRoles = async (values) => { + try { + await updateUserRoles(currentUser.id, values.role_ids || []) + Toast.success('角色分配成功') + setRolesModalVisible(false) + rolesForm.resetFields() + loadUsers() + } catch (error) { + Toast.error(error.response?.data?.detail || '角色分配失败') + } + } + + // 重置密码 + const handleResetPassword = async (userId) => { + try { + const res = await resetUserPassword(userId) + if (res.code === 200) { + Modal.success({ + title: '密码重置成功', + content: `新密码为:${res.data.default_password},请妥善保管!`, + }) + } + } catch (error) { + Toast.error(error.response?.data?.detail || '密码重置失败') + } + } + + const columns = [ + { + title: 'ID', + dataIndex: 'id', + key: 'id', + width: 80, + }, + { + title: '用户名', + dataIndex: 'username', + key: 'username', + }, + { + title: '昵称', + dataIndex: 'nickname', + key: 'nickname', + }, + { + title: '邮箱', + dataIndex: 'email', + key: 'email', + }, + { + title: '手机号', + dataIndex: 'phone', + key: 'phone', + }, + { + title: '角色', + dataIndex: 'roles', + key: 'roles', + render: (roles) => ( + <> + {roles?.map((role) => ( + + {role.role_name} + + ))} + {(!roles || roles.length === 0) && 无角色} + + ), + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + render: (status, record) => ( + handleStatusChange(record.id, checked)} + checkedChildren="启用" + unCheckedChildren="停用" + /> + ), + }, + { + title: '创建时间', + dataIndex: 'created_at', + key: 'created_at', + render: (text) => (text ? new Date(text).toLocaleString('zh-CN') : '-'), + }, + { + title: '操作', + key: 'action', + fixed: 'right', + width: 280, + render: (_, record) => ( + + + + handleResetPassword(record.id)} + okText="确定" + cancelText="取消" + > + + + handleDelete(record.id)} + okText="确定" + cancelText="取消" + > + + + + ), + }, + ] + + return ( + +
+

用户管理

+ + {/* 搜索和操作栏 */} + + + + } + value={keyword} + onChange={(e) => setKeyword(e.target.value)} + allowClear + /> + + + + + + + + + + + {/* 用户列表 */} + + `共 ${total} 条`, + onChange: (page, pageSize) => { + setPage(page) + setPageSize(pageSize) + }, + }} + onSelectionChange={() => {}} + /> + + + {/* 创建用户对话框 */} + { + setCreateModalVisible(false) + createForm.resetFields() + }} + onOk={() => createForm.submit()} + > +
+ + + + + + + + + + + + + + + +
+
+ 注意:新用户的初始密码将在创建成功后显示,请妥善保管! +
+
+
+
+ + {/* 编辑用户对话框 */} + { + setEditModalVisible(false) + editForm.resetFields() + }} + onOk={() => editForm.submit()} + > +
+ + + + + + + + + +
+
+ + {/* 分配角色对话框 */} + { + setRolesModalVisible(false) + rolesForm.resetFields() + }} + onOk={() => rolesForm.submit()} + > +
+ + + +
+
+
+
+ ) +} + +export default Users