main
mula.liu 2025-12-23 13:02:10 +08:00
parent 1712031528
commit bc897706b3
35 changed files with 4730 additions and 49 deletions

43
.env.example 100644
View File

@ -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=系统管理员

156
CHANGELOG_DEPLOY.md 100644
View File

@ -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

314
DEPLOY.md 100644
View File

@ -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 部署方案
- ✨ 一键初始化和升级
- ✨ 数据库备份恢复
- ✨ 国内镜像加速
- ✨ 完善的文档和脚本
---
**祝部署顺利!** 🎉

193
README_DOCKER.md 100644
View File

@ -0,0 +1,193 @@
# NEX Docus
<div align="center">
**现代化的文档管理系统**
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Docker](https://img.shields.io/badge/Docker-Ready-brightgreen.svg)](DEPLOY.md)
[![Python](https://img.shields.io/badge/Python-3.9+-blue.svg)](backend/)
[![React](https://img.shields.io/badge/React-18.2-61DAFB.svg)](forntend/)
</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**

View File

@ -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/

36
backend/Dockerfile 100644
View File

@ -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"]

View File

@ -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=["角色管理"])

View File

@ -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="角色权限更新成功"
)

View File

@ -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
}
}

View File

@ -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="密码已重置为初始密码"
)

View File

@ -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"

View File

@ -23,6 +23,9 @@ AsyncSessionLocal = sessionmaker(
autoflush=False,
)
# 别名,用于脚本中使用
async_session = AsyncSessionLocal
# 创建基础模型类
Base = declarative_base()

View File

@ -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

View File

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

View File

@ -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;

View File

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

View File

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

View File

@ -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

View File

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

View File

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

382
deploy.sh 100755
View File

@ -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 "$@"

113
docker-compose.yml 100644
View File

@ -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

View File

@ -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

View File

@ -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;"]

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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,
},
})
}

View File

@ -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,
})
}

View File

@ -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',
})
}

View File

@ -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 }) {

View File

@ -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>
)

View File

@ -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

View File

@ -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

View File

@ -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