0.9.2
parent
1712031528
commit
bc897706b3
|
|
@ -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=系统管理员
|
||||
|
|
@ -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 <backup_file>
|
||||
```
|
||||
|
||||
### 📊 配置示例
|
||||
|
||||
#### 开发环境配置
|
||||
```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/ # 项目文档存储
|
||||
│ ├── <uuid1>/ # 项目 1
|
||||
│ │ ├── README.md
|
||||
│ │ ├── docs/
|
||||
│ │ └── _assets/ # 项目资源
|
||||
│ └── <uuid2>/ # 项目 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
|
||||
|
|
@ -0,0 +1,314 @@
|
|||
# NEX Docus Docker 部署文档
|
||||
|
||||
完整的 Docker 容器化部署方案,支持一键部署、升级、备份等功能。
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 环境要求
|
||||
|
||||
- Docker 20.10+
|
||||
- Docker Compose 2.0+
|
||||
- 至少 2GB 可用内存
|
||||
- 至少 10GB 可用磁盘空间
|
||||
|
||||
### 2. 首次部署
|
||||
|
||||
```bash
|
||||
# 1. 克隆项目(如果还没有)
|
||||
git clone <your-repo-url>
|
||||
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 部署方案
|
||||
- ✨ 一键初始化和升级
|
||||
- ✨ 数据库备份恢复
|
||||
- ✨ 国内镜像加速
|
||||
- ✨ 完善的文档和脚本
|
||||
|
||||
---
|
||||
|
||||
**祝部署顺利!** 🎉
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
# NEX Docus
|
||||
|
||||
<div align="center">
|
||||
|
||||
**现代化的文档管理系统**
|
||||
|
||||
[](LICENSE)
|
||||
[](DEPLOY.md)
|
||||
[](backend/)
|
||||
[](forntend/)
|
||||
|
||||
</div>
|
||||
|
||||
## ✨ 特性
|
||||
|
||||
- 📝 **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 <your-repo-url>
|
||||
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 <backup_file>
|
||||
```
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
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**
|
||||
|
|
@ -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/
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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=["角色管理"])
|
||||
|
|
|
|||
|
|
@ -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="角色权限更新成功"
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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="密码已重置为初始密码"
|
||||
)
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ AsyncSessionLocal = sessionmaker(
|
|||
autoflush=False,
|
||||
)
|
||||
|
||||
# 别名,用于脚本中使用
|
||||
async_session = AsyncSessionLocal
|
||||
|
||||
# 创建基础模型类
|
||||
Base = declarative_base()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
@ -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;
|
||||
|
|
@ -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())
|
||||
|
|
@ -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())
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
@ -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 <backup_file>"
|
||||
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 <command> [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 <file> 恢复数据库"
|
||||
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 "$@"
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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;"]
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
|||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
{/* 角色权限管理 */}
|
||||
<Route
|
||||
path="/system/permissions"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Permissions />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
{/* 用户管理 */}
|
||||
<Route
|
||||
path="/system/users"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Users />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
{/* 角色管理 */}
|
||||
<Route
|
||||
path="/system/roles"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Roles />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="/" element={<Navigate to="/projects" replace />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
@ -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',
|
||||
})
|
||||
}
|
||||
|
|
@ -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 }) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<Tabs activeKey={activeTab} onChange={setActiveTab} centered>
|
||||
<Tabs.TabPane tab="登录" key="login">
|
||||
<Form
|
||||
form={loginForm}
|
||||
name="login"
|
||||
onFinish={handleLogin}
|
||||
autoComplete="off"
|
||||
|
|
@ -109,6 +128,15 @@ function Login() {
|
|||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Checkbox
|
||||
checked={rememberMe}
|
||||
onChange={(e) => setRememberMe(e.target.checked)}
|
||||
>
|
||||
记住用户
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
|
|
@ -204,7 +232,7 @@ function Login() {
|
|||
</Card>
|
||||
|
||||
<div className="login-footer">
|
||||
<p>默认管理员账号: admin / admin123</p>
|
||||
<p>重置密码请联系管理员</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,236 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { Tree, Button, Card, Row, Col, Tag, Space, message } from 'antd'
|
||||
import { SafetyOutlined, SaveOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'
|
||||
import {
|
||||
getAllRoles,
|
||||
getMenuTree,
|
||||
getRolePermissions,
|
||||
updateRolePermissions,
|
||||
} from '@/api/rolePermissions'
|
||||
import MainLayout from '@/components/MainLayout/MainLayout'
|
||||
import ListTable from '@/components/ListTable/ListTable'
|
||||
import Toast from '@/components/Toast/Toast'
|
||||
|
||||
function Permissions() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [roles, setRoles] = useState([])
|
||||
const [menuTree, setMenuTree] = useState([])
|
||||
const [selectedRole, setSelectedRole] = useState(null)
|
||||
const [checkedKeys, setCheckedKeys] = useState([])
|
||||
const [expandedKeys, setExpandedKeys] = useState([])
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
loadRoles()
|
||||
loadMenuTree()
|
||||
}, [])
|
||||
|
||||
const loadRoles = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const res = await getAllRoles()
|
||||
if (res.data) {
|
||||
setRoles(res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Load roles error:', error)
|
||||
Toast.error('加载角色列表失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const loadMenuTree = async () => {
|
||||
try {
|
||||
const res = await getMenuTree()
|
||||
if (res.data) {
|
||||
const treeData = buildTreeData(res.data)
|
||||
setMenuTree(treeData)
|
||||
// 默认展开第一层
|
||||
const firstLevelKeys = res.data.filter((m) => !m.parent_id).map((m) => m.id.toString())
|
||||
setExpandedKeys(firstLevelKeys)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Load menu tree error:', error)
|
||||
Toast.error('加载菜单树失败')
|
||||
}
|
||||
}
|
||||
|
||||
const buildTreeData = (menus) => {
|
||||
// Backend already returns a nested tree structure
|
||||
// We just need to transform it to Ant Design Tree format
|
||||
const transformNode = (node) => {
|
||||
const treeNode = {
|
||||
key: node.id.toString(),
|
||||
title: node.menu_name,
|
||||
}
|
||||
|
||||
// Recursively transform children if they exist
|
||||
if (node.children && node.children.length > 0) {
|
||||
treeNode.children = node.children.map(transformNode)
|
||||
}
|
||||
|
||||
return treeNode
|
||||
}
|
||||
|
||||
return menus.map(transformNode)
|
||||
}
|
||||
|
||||
const handleRoleClick = async (role) => {
|
||||
if (role.is_system === 1) {
|
||||
Toast.warning('系统角色不允许修改权限')
|
||||
return
|
||||
}
|
||||
|
||||
setSelectedRole(role)
|
||||
try {
|
||||
const res = await getRolePermissions(role.id)
|
||||
if (res.data && res.data.menu_ids) {
|
||||
const keys = res.data.menu_ids.map((id) => id.toString())
|
||||
setCheckedKeys(keys)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Load role permissions error:', error)
|
||||
Toast.error('加载角色权限失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleCheck = (checkedKeysValue) => {
|
||||
setCheckedKeys(checkedKeysValue)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!selectedRole) {
|
||||
Toast.warning('请先选择一个角色')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setSaving(true)
|
||||
const menuIds = checkedKeys.map((key) => parseInt(key))
|
||||
await updateRolePermissions(selectedRole.id, menuIds)
|
||||
Toast.success('权限保存成功')
|
||||
} catch (error) {
|
||||
console.error('Save permissions error:', error)
|
||||
Toast.error(error.response?.data?.detail || '保存权限失败')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '角色名称',
|
||||
dataIndex: 'role_name',
|
||||
key: 'role_name',
|
||||
},
|
||||
{
|
||||
title: '角色编码',
|
||||
dataIndex: 'role_code',
|
||||
key: 'role_code',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status) =>
|
||||
status === 1 ? (
|
||||
<Tag icon={<CheckCircleOutlined />} color="success">
|
||||
启用
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag icon={<CloseCircleOutlined />} color="error">
|
||||
禁用
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '系统角色',
|
||||
dataIndex: 'is_system',
|
||||
key: 'is_system',
|
||||
render: (isSystem) => (isSystem === 1 ? <Tag color="orange">是</Tag> : <Tag>否</Tag>),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<MainLayout>
|
||||
<div style={{ padding: '24px' }}>
|
||||
<h1 style={{ marginBottom: '24px', fontSize: '24px', fontWeight: 600 }}>
|
||||
<SafetyOutlined style={{ marginRight: '8px' }} />
|
||||
角色权限管理
|
||||
</h1>
|
||||
|
||||
<Row gutter={16}>
|
||||
{/* 左侧:功能权限树 */}
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title="功能权限树"
|
||||
extra={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSave}
|
||||
loading={saving}
|
||||
disabled={!selectedRole}
|
||||
>
|
||||
保存权限
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{selectedRole ? (
|
||||
<div style={{ marginBottom: 16, padding: '12px', background: '#f0f2f5', borderRadius: 4 }}>
|
||||
<Space>
|
||||
<span style={{ fontWeight: 500 }}>当前角色:</span>
|
||||
<Tag color="blue">{selectedRole.role_name}</Tag>
|
||||
{selectedRole.is_system === 1 && <Tag color="orange">系统角色(只读)</Tag>}
|
||||
</Space>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
padding: '12px',
|
||||
background: '#fff7e6',
|
||||
borderRadius: 4,
|
||||
color: '#ad6800',
|
||||
}}
|
||||
>
|
||||
请从右侧选择一个角色来查看和编辑权限
|
||||
</div>
|
||||
)}
|
||||
<Tree
|
||||
checkable
|
||||
expandedKeys={expandedKeys}
|
||||
onExpand={setExpandedKeys}
|
||||
checkedKeys={checkedKeys}
|
||||
onCheck={handleCheck}
|
||||
treeData={menuTree}
|
||||
disabled={!selectedRole || selectedRole.is_system === 1}
|
||||
style={{ minHeight: 400 }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 右侧:角色列表 */}
|
||||
<Col span={12}>
|
||||
<Card title="角色列表">
|
||||
<ListTable
|
||||
columns={columns}
|
||||
dataSource={roles}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
scroll={{ y: 600 }}
|
||||
onRowClick={handleRoleClick}
|
||||
selectedRow={selectedRole}
|
||||
onSelectionChange={() => {}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default Permissions
|
||||
|
|
@ -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) => <Tag color="blue">{count}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status) =>
|
||||
status === 1 ? (
|
||||
<Tag icon={<CheckCircleOutlined />} color="success">
|
||||
启用
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag icon={<CloseCircleOutlined />} color="error">
|
||||
禁用
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '系统角色',
|
||||
dataIndex: 'is_system',
|
||||
key: 'is_system',
|
||||
render: (isSystem) =>
|
||||
isSystem === 1 ? <Tag color="orange">是</Tag> : <Tag>否</Tag>,
|
||||
},
|
||||
{
|
||||
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) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<TeamOutlined />}
|
||||
onClick={() => handleViewUsers(record)}
|
||||
>
|
||||
用户列表
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
disabled={record.is_system === 1}
|
||||
onClick={() => {
|
||||
setCurrentRole(record)
|
||||
editForm.setFieldsValue({
|
||||
role_name: record.role_name,
|
||||
role_code: record.role_code,
|
||||
description: record.description,
|
||||
status: record.status,
|
||||
})
|
||||
setEditModalVisible(true)
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定删除该角色吗?"
|
||||
description="删除后无法恢复,且该角色下必须没有用户。"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
disabled={record.is_system === 1}
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
disabled={record.is_system === 1}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
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 ? (
|
||||
<Tag icon={<CheckCircleOutlined />} color="success">
|
||||
启用
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag icon={<CloseCircleOutlined />} color="error">
|
||||
停用
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
render: (text) => (text ? new Date(text).toLocaleString('zh-CN') : '-'),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<MainLayout>
|
||||
<div style={{ padding: '24px' }}>
|
||||
<h1 style={{ marginBottom: '24px', fontSize: '24px', fontWeight: 600 }}>角色管理</h1>
|
||||
|
||||
{/* 搜索和操作栏 */}
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Input
|
||||
placeholder="搜索角色名称、编码"
|
||||
prefix={<SearchOutlined />}
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Select
|
||||
placeholder="筛选状态"
|
||||
style={{ width: '100%' }}
|
||||
value={statusFilter}
|
||||
onChange={setStatusFilter}
|
||||
allowClear
|
||||
>
|
||||
<Select.Option value={1}>启用</Select.Option>
|
||||
<Select.Option value={0}>禁用</Select.Option>
|
||||
</Select>
|
||||
</Col>
|
||||
<Col span={10} style={{ textAlign: 'right' }}>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateModalVisible(true)}>
|
||||
新增角色
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 角色列表 */}
|
||||
<Card>
|
||||
<ListTable
|
||||
columns={columns}
|
||||
dataSource={roles}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
scroll={{ x: 1200 }}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: pageSize,
|
||||
total: total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (page, pageSize) => {
|
||||
setPage(page)
|
||||
setPageSize(pageSize)
|
||||
},
|
||||
}}
|
||||
onSelectionChange={() => {}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 创建角色对话框 */}
|
||||
<Modal
|
||||
title="新增角色"
|
||||
open={createModalVisible}
|
||||
onCancel={() => {
|
||||
setCreateModalVisible(false)
|
||||
createForm.resetFields()
|
||||
}}
|
||||
onOk={() => createForm.submit()}
|
||||
>
|
||||
<Form form={createForm} layout="vertical" onFinish={handleCreate}>
|
||||
<Form.Item
|
||||
label="角色名称"
|
||||
name="role_name"
|
||||
rules={[{ required: true, message: '请输入角色名称' }]}
|
||||
>
|
||||
<Input placeholder="请输入角色名称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="角色编码"
|
||||
name="role_code"
|
||||
rules={[
|
||||
{ required: true, message: '请输入角色编码' },
|
||||
{ pattern: /^[a-z_]+$/, message: '编码只能包含小写字母和下划线' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入角色编码(如:project_manager)" />
|
||||
</Form.Item>
|
||||
<Form.Item label="描述" name="description">
|
||||
<Input.TextArea rows={3} placeholder="请输入角色描述" />
|
||||
</Form.Item>
|
||||
<Form.Item label="状态" name="status" initialValue={1}>
|
||||
<Select>
|
||||
<Select.Option value={1}>启用</Select.Option>
|
||||
<Select.Option value={0}>禁用</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 编辑角色对话框 */}
|
||||
<Modal
|
||||
title="编辑角色"
|
||||
open={editModalVisible}
|
||||
onCancel={() => {
|
||||
setEditModalVisible(false)
|
||||
editForm.resetFields()
|
||||
}}
|
||||
onOk={() => editForm.submit()}
|
||||
>
|
||||
<Form form={editForm} layout="vertical" onFinish={handleEdit}>
|
||||
<Form.Item
|
||||
label="角色名称"
|
||||
name="role_name"
|
||||
rules={[{ required: true, message: '请输入角色名称' }]}
|
||||
>
|
||||
<Input placeholder="请输入角色名称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="角色编码"
|
||||
name="role_code"
|
||||
rules={[
|
||||
{ required: true, message: '请输入角色编码' },
|
||||
{ pattern: /^[a-z_]+$/, message: '编码只能包含小写字母和下划线' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入角色编码" />
|
||||
</Form.Item>
|
||||
<Form.Item label="描述" name="description">
|
||||
<Input.TextArea rows={3} placeholder="请输入角色描述" />
|
||||
</Form.Item>
|
||||
<Form.Item label="状态" name="status">
|
||||
<Select>
|
||||
<Select.Option value={1}>启用</Select.Option>
|
||||
<Select.Option value={0}>禁用</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 角色用户列表对话框 */}
|
||||
<Modal
|
||||
title={`角色用户列表 - ${currentRole?.role_name || ''}`}
|
||||
open={usersModalVisible}
|
||||
onCancel={() => {
|
||||
setUsersModalVisible(false)
|
||||
setRoleUsers([])
|
||||
setUsersPage(1)
|
||||
}}
|
||||
footer={null}
|
||||
width={1000}
|
||||
>
|
||||
<ListTable
|
||||
columns={userColumns}
|
||||
dataSource={roleUsers}
|
||||
rowKey="id"
|
||||
loading={usersLoading}
|
||||
pagination={{
|
||||
current: usersPage,
|
||||
pageSize: usersPageSize,
|
||||
total: usersTotal,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (page, pageSize) => {
|
||||
setUsersPage(page)
|
||||
setUsersPageSize(pageSize)
|
||||
},
|
||||
}}
|
||||
onSelectionChange={() => {}}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default Roles
|
||||
|
|
@ -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) => (
|
||||
<Tag key={role.id} color="blue">
|
||||
{role.role_name}
|
||||
</Tag>
|
||||
))}
|
||||
{(!roles || roles.length === 0) && <Tag>无角色</Tag>}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status, record) => (
|
||||
<Switch
|
||||
checked={status === 1}
|
||||
onChange={(checked) => 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) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<TeamOutlined />}
|
||||
onClick={() => {
|
||||
setCurrentUser(record)
|
||||
rolesForm.setFieldsValue({
|
||||
role_ids: record.roles?.map((r) => r.id) || [],
|
||||
})
|
||||
setRolesModalVisible(true)
|
||||
}}
|
||||
>
|
||||
分配角色
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => {
|
||||
setCurrentUser(record)
|
||||
editForm.setFieldsValue({
|
||||
nickname: record.nickname,
|
||||
email: record.email,
|
||||
phone: record.phone,
|
||||
})
|
||||
setEditModalVisible(true)
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定重置该用户的密码吗?"
|
||||
onConfirm={() => handleResetPassword(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button type="link" size="small" icon={<KeyOutlined />}>
|
||||
重置密码
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Popconfirm
|
||||
title="确定删除该用户吗?"
|
||||
description="删除后无法恢复,且该用户必须没有归属的项目。"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<MainLayout>
|
||||
<div style={{ padding: '24px' }}>
|
||||
<h1 style={{ marginBottom: '24px', fontSize: '24px', fontWeight: 600 }}>用户管理</h1>
|
||||
|
||||
{/* 搜索和操作栏 */}
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Input
|
||||
placeholder="搜索用户名、昵称、邮箱"
|
||||
prefix={<SearchOutlined />}
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Select
|
||||
placeholder="筛选状态"
|
||||
style={{ width: '100%' }}
|
||||
value={statusFilter}
|
||||
onChange={setStatusFilter}
|
||||
allowClear
|
||||
>
|
||||
<Select.Option value={1}>启用</Select.Option>
|
||||
<Select.Option value={0}>停用</Select.Option>
|
||||
</Select>
|
||||
</Col>
|
||||
<Col span={10} style={{ textAlign: 'right' }}>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateModalVisible(true)}>
|
||||
新增用户
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 用户列表 */}
|
||||
<Card>
|
||||
<ListTable
|
||||
columns={columns}
|
||||
dataSource={users}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
scroll={{ x: 1300 }}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: pageSize,
|
||||
total: total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (page, pageSize) => {
|
||||
setPage(page)
|
||||
setPageSize(pageSize)
|
||||
},
|
||||
}}
|
||||
onSelectionChange={() => {}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 创建用户对话框 */}
|
||||
<Modal
|
||||
title="新增用户"
|
||||
open={createModalVisible}
|
||||
onCancel={() => {
|
||||
setCreateModalVisible(false)
|
||||
createForm.resetFields()
|
||||
}}
|
||||
onOk={() => createForm.submit()}
|
||||
>
|
||||
<Form form={createForm} layout="vertical" onFinish={handleCreate}>
|
||||
<Form.Item
|
||||
label="用户名"
|
||||
name="username"
|
||||
rules={[
|
||||
{ required: true, message: '请输入用户名' },
|
||||
{ min: 3, message: '用户名至少3个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入用户名" />
|
||||
</Form.Item>
|
||||
<Form.Item label="昵称" name="nickname">
|
||||
<Input placeholder="请输入昵称(默认与用户名相同)" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="邮箱"
|
||||
name="email"
|
||||
rules={[{ type: 'email', message: '请输入有效的邮箱地址' }]}
|
||||
>
|
||||
<Input placeholder="请输入邮箱" />
|
||||
</Form.Item>
|
||||
<Form.Item label="手机号" name="phone">
|
||||
<Input placeholder="请输入手机号" />
|
||||
</Form.Item>
|
||||
<Form.Item label="分配角色" name="role_ids">
|
||||
<Select mode="multiple" placeholder="请选择角色(可多选)">
|
||||
{roles.map((role) => (
|
||||
<Select.Option key={role.id} value={role.id}>
|
||||
{role.role_name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<div style={{ padding: '8px 0', background: '#fff7e6', border: '1px solid #ffd591', borderRadius: 4 }}>
|
||||
<div style={{ padding: '0 12px', fontSize: 12, color: '#ad6800' }}>
|
||||
注意:新用户的初始密码将在创建成功后显示,请妥善保管!
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 编辑用户对话框 */}
|
||||
<Modal
|
||||
title="编辑用户"
|
||||
open={editModalVisible}
|
||||
onCancel={() => {
|
||||
setEditModalVisible(false)
|
||||
editForm.resetFields()
|
||||
}}
|
||||
onOk={() => editForm.submit()}
|
||||
>
|
||||
<Form form={editForm} layout="vertical" onFinish={handleEdit}>
|
||||
<Form.Item label="昵称" name="nickname">
|
||||
<Input placeholder="请输入昵称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="邮箱"
|
||||
name="email"
|
||||
rules={[{ type: 'email', message: '请输入有效的邮箱地址' }]}
|
||||
>
|
||||
<Input placeholder="请输入邮箱" />
|
||||
</Form.Item>
|
||||
<Form.Item label="手机号" name="phone">
|
||||
<Input placeholder="请输入手机号" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 分配角色对话框 */}
|
||||
<Modal
|
||||
title="分配角色"
|
||||
open={rolesModalVisible}
|
||||
onCancel={() => {
|
||||
setRolesModalVisible(false)
|
||||
rolesForm.resetFields()
|
||||
}}
|
||||
onOk={() => rolesForm.submit()}
|
||||
>
|
||||
<Form form={rolesForm} layout="vertical" onFinish={handleAssignRoles}>
|
||||
<Form.Item label="选择角色" name="role_ids">
|
||||
<Select mode="multiple" placeholder="请选择角色(可多选)">
|
||||
{roles.map((role) => (
|
||||
<Select.Option key={role.id} value={role.id}>
|
||||
{role.role_name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default Users
|
||||
Loading…
Reference in New Issue