feat(phase4): 全部业务模块迁移到 modules/(结构迁移)

- 21 个模块目录(auth/system/dictionary/configuration/cms/member/region/link/notice/job/log/upload/index)
- 每个模块 router.py(合并模块导出多个 router)/schemas.py/service.py/repository.py/models.py
- routers/v1.py 与 routers/__init__.py 改为直接引用 modules,前缀与权限串原样保留
- core/middleware.py 懒加载指向 modules.system.menu / modules.log;core/redis_client.py 指向 modules.job.scheduler
- apps/views|services|forms 全部改为 re-export 垫片
- 迁移后 grep 守门通过:modules/core 无 apps/extensions 残留引用
- 实测登录、27+ 模块接口、写操作 commit、操作日志落库均正常

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2026-08-07 10:13:30 +08:00
co-authored by Claude
parent b2c7b93786
commit 64a5504396
156 changed files with 7977 additions and 7533 deletions
+9
View File
@@ -0,0 +1,9 @@
# +========== 迁移新增:本模块 Repository 层
"""modules/system/dept 数据访问层。"""
from modules.system.dept.models import Dept
from shared.base_repository import BaseRepository
class DeptRepository(BaseRepository[Dept]):
pass
dept_repo = DeptRepository(Dept)
+73
View File
@@ -0,0 +1,73 @@
from fastapi import APIRouter, Request
from modules.system.dept.schemas import DeptForm
from core.middleware import check_demo
from core.middleware import permission_required
from modules.system.dept import service as dept
from core import R
# 创建路由
router = APIRouter()
# 查询分页数据
@router.get('/list', summary='查询分页数据')
@permission_required("sys:dept:list")
def list(request: Request):
# 调用查询分页数据服务方法
result = dept.get_dept_page(request)
# 返回结果
return result
# 查询部门详情
@router.get('/detail/{id}', summary='查询部门详情')
@permission_required("sys:dept:detail")
def detail(request: Request, id):
# 调用查询部门详情服务方法
data = dept.get_dept_detail(id)
# 返回结果
return R.ok(data=data)
# 添加部门
@router.post('/add', summary='添加部门')
@permission_required("sys:dept:add")
@check_demo
def add(request: Request, data: DeptForm):
# 调用添加部门服务
result = dept.add_dept(request, data)
# 返回结果
return result
# 更新部门
@router.put('/update', summary='更新部门')
@permission_required("sys:dept:update")
@check_demo
def update(request: Request, data: DeptForm):
# 调用更新部门服务方法
result = dept.update_dept(request, data)
# 返回结果
return result
# 删除部门
@router.delete('/delete/{id}', summary='删除部门')
@permission_required("sys:dept:delete")
@check_demo
def delete(request: Request, id):
# 调用删除部门服务方法
result = dept.delete_dept(id)
# 返回结果
return result
# 获取部门列表
@router.get('/getDeptList', summary='获取部门列表')
def get_dept_list():
# 调用查询部门数据方法
result = dept.get_dept_list()
# 返回结果
return R.ok(data=result)
+14
View File
@@ -0,0 +1,14 @@
from typing import Optional
from pydantic import BaseModel, Field
# 部门表单验证
class DeptForm(BaseModel):
id: Optional[int] = Field(None, description="部门ID")
name: str = Field(..., min_length=1, max_length=150, description="部门名称")
code: str = Field(..., min_length=1, max_length=150, description="部门编码")
type: int = Field(..., ge=1, le=2, description="部门类型")
pid: Optional[int] = Field(None, description="上级ID")
sort: int = Field(..., ge=0, le=99999, description="部门排序")
note: Optional[str] = Field(None, max_length=255, description="部门备注")
+114
View File
@@ -0,0 +1,114 @@
from datetime import datetime, timezone
from fastapi.logger import logger
from sqlalchemy import and_
from modules.system.dept.schemas import DeptForm
from modules.system.dept.models import Dept
from core.database import get_db_session
from core import R
from core.security import get_user_id
from utils.request import like_contains, parse_id_list
# 查询部门数据
def get_dept_page(request):
db = get_db_session()
# 实例化查询对象
query = db.query(Dept).filter(Dept.is_delete == 0)
# 部门名称
name = request.query_params.get('name')
if name:
# 部门名称模糊查询
query = query.filter(Dept.name.like(like_contains(name), escape='\\'))
# 查询数据
dept_list = query.order_by(Dept.sort.asc()).all()
# 实例化数组对象
result = []
# 遍历数据源
if dept_list:
for item in dept_list:
# 对象转字典
data = item.to_dict()
# 加入列表
result.append(data)
# 返回结果
return R.ok(data=result)
# 根据ID查询部门
def get_dept_detail(dept_id):
db = get_db_session()
# 根据ID查询部门
dept = db.query(Dept).filter(and_(Dept.id == dept_id, Dept.is_delete == 0)).first()
# 查询结果判空
if not dept:
return None
# 对象转字典
data = dept.to_dict()
# 返回结果
return data
# 添加部门
def add_dept(request, data: DeptForm):
db = get_db_session()
# 唯一性校验
if db.query(Dept).filter(and_(Dept.name == data.name, Dept.is_delete == 0)).first():
return R.failed("部门名称不能重复")
# 表单数据赋值给对象
dept = Dept(**data.model_dump(exclude={'id'}))
dept.create_user = get_user_id(request)
# 插入数据
dept.save()
# 返回结果
return R.ok(msg="添加成功")
# 更新部门
def update_dept(request, data: DeptForm):
db = get_db_session()
# 记录ID判空
if not data.id or data.id <= 0:
return R.failed("记录ID不能为空")
# 唯一性校验(排除自身)
if db.query(Dept).filter(and_(Dept.name == data.name, Dept.id != data.id, Dept.is_delete == 0)).first():
return R.failed("部门名称不能重复")
# 根据ID查询记录
dept = db.query(Dept).filter(and_(Dept.id == data.id, Dept.is_delete == 0)).first()
# 查询结果判空
if not dept:
return R.failed("记录不存在")
# 构造更新数据(排除 id)
updData = data.model_dump(exclude={'id'})
updData['update_user'] = get_user_id(request)
updData['update_time'] = datetime.now(tz=timezone.utc)
result = db.query(Dept).filter_by(id=data.id).update(updData)
# 提交数据
if not result:
return R.failed("更新失败")
# 返回结果
return R.ok(msg="更新成功")
# 删除部门
def delete_dept(dept_id):
return Dept.batch_delete_r(dept_id)
# 获取部门列表
def get_dept_list():
db = get_db_session()
# 查询部门数据列表
dept_rows = db.query(Dept).filter(Dept.is_delete == 0).order_by(Dept.sort.asc()).all()
# 实例化列表
dept_list = []
# 遍历数据源
if dept_rows:
for v in dept_rows:
# 对象转字典
item = v.to_dict()
# 加入数组
dept_list.append(item)
# 返回结果
return dept_list
+9
View File
@@ -0,0 +1,9 @@
# +========== 迁移新增:本模块 Repository 层
"""modules/system/level 数据访问层。"""
from modules.system.level.models import Level
from shared.base_repository import BaseRepository
class LevelRepository(BaseRepository[Level]):
pass
level_repo = LevelRepository(Level)
+108
View File
@@ -0,0 +1,108 @@
import asyncio
from fastapi import APIRouter, Request, UploadFile, File
from modules.system.level.schemas import LevelForm, LevelStatusForm
from core.middleware import check_demo
from core.middleware import permission_required
from modules.system.level import service as level
from core import R
from utils.request import parse_batch_ids
# 创建路由
router = APIRouter()
# 查询分页数据
@router.get('/page', summary='查询分页数据')
@permission_required("sys:level:page")
def page(request: Request):
result = level.get_level_page(request)
return result
# 查询职级详情
@router.get('/detail/{id}', summary='查询职级详情')
@permission_required("sys:level:detail")
def detail(request: Request, id):
data = level.get_level_detail(id)
return R.ok(data=data)
# 添加职级
@router.post('/add', summary='添加职级')
@permission_required("sys:level:add")
@check_demo
def add(request: Request, data: LevelForm):
result = level.add_level(request, data)
return result
# 更新职级
@router.put('/update', summary='更新职级')
@permission_required("sys:level:update")
@check_demo
def update(request: Request, data: LevelForm):
result = level.update_level(request, data)
return result
# 删除职级
@router.delete('/delete/{id}', summary='删除职级')
@permission_required("sys:level:delete")
@check_demo
def delete(request: Request, id):
result = level.delete_level(id)
return result
# 设置职级状态
@router.put('/status', summary='设置职级状态')
@permission_required("sys:level:status")
@check_demo
def status(request: Request, data: LevelStatusForm):
result = level.update_level_status(request, data)
return result
# 导入Excel
@router.post('/import', summary='导入Excel')
@permission_required("sys:level:import")
@check_demo
async def imports(request: Request, file: UploadFile = File(...)):
result = await level.import_level(request, file)
return result
# 导出Excel
@router.post('/export', summary='导出Excel')
@permission_required("sys:level:export")
@check_demo
def export(request: Request):
result = level.export_level(request)
return result
# 获取职级列表
@router.get('/getLevelList', summary='获取职级列表')
def get_level_list(request: Request):
result = level.get_level_list()
return R.ok(data=result)
# 批量删除职级
@router.delete('/batchDelete', summary='批量删除职级')
@permission_required("sys:level:batch_delete")
@check_demo
async def batch_delete(request: Request):
"""
批量删除职级
Request Body:
[1, 2, 3] # 职级ID数组
"""
id_str, err = await parse_batch_ids(request)
if err:
return err
return await asyncio.to_thread(level.delete_level, id_str)
+15
View File
@@ -0,0 +1,15 @@
from typing import Optional
from pydantic import BaseModel, Field
# 职级表单验证
class LevelForm(BaseModel):
id: Optional[int] = Field(None, description="职级ID")
name: str = Field(..., min_length=1, max_length=150, description="职级名称")
status: int = Field(..., ge=1, le=2, description="职级状态")
sort: int = Field(..., ge=0, le=99999, description="职级排序")
# 设置状态表单
class LevelStatusForm(BaseModel):
id: int = Field(..., gt=0, description="职级ID")
status: int = Field(..., ge=1, le=2, description="职级状态")
+309
View File
@@ -0,0 +1,309 @@
# +======================================================================
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
# +----------------------------------------------------------------------
# | Copyright (c) 2021-2026 一米阳光团队 版权所有
# +----------------------------------------------------------------------
# | 开源协议: Apache-2.0
# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。
# +----------------------------------------------------------------------
# | 官方网站: https://www.djangoadmin.cn
# +----------------------------------------------------------------------
# | 研发团队: 一米阳光团队 荣誉出品
# +======================================================================
# | 版权和免责声明:
# | 一、知识产权声明
# | 本团队对该软件框架产品(以下简称"本产品")拥有完整知识产权,包括但不限于商标权、专利权、
# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权,
# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。
# | 二、使用限制与禁止条款
# | 您理解并同意,禁止将本产品用于以下任何行为:
# | 1. 任何违反中华人民共和国法律法规的用途;
# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为;
# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。
# | 三、责任豁免
# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失
# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三
# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。
# | 四、违约与法律责任
# | 如发现任何违反本声明的行为,本团队有权:
# | 1. 无条件配合公安机关及其他有权机关的调查取证工作;
# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失;
# | 3. 保留进一步采取刑事控告等法律措施的权利。
# | 五、许可范围说明
# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包
# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通
# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、
# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造
# | 成的一切经济损失及商誉损害。
# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件
# | 许可协议》附件为准。
# +======================================================================
import asyncio
import os
import random
import time
from datetime import datetime, timezone
from fastapi.logger import logger
from openpyxl.reader.excel import load_workbook
from openpyxl.workbook import Workbook
from sqlalchemy import and_
from shared.constants import PAGE_SIZE
from modules.system.level.schemas import LevelForm, LevelStatusForm
from modules.system.level.models import Level
from modules.upload import service as upload
from config import FASTAPI_TEMP_PATH, FASTAPI_UPLOAD_DIR, FASTAPI_IMAGE_URL
from core.database import get_db_session
from core import R
from utils.file import mkdir
from core.security import get_user_id
from utils.request import like_contains
# 查询职级分页数据
def get_level_page(request):
db = get_db_session()
# 页码
page = int(request.query_params.get("pageNo", 1))
# 每页数
limit = int(request.query_params.get("pageSize", PAGE_SIZE))
# 查询数据
query = db.query(Level).filter(Level.is_delete == 0)
# 职级名称
name = request.query_params.get('name')
if name:
query = query.filter(Level.name.like(like_contains(name), escape='\\'))
# 职级状态
status = request.query_params.get('status')
if status:
query = query.filter(Level.status == status)
# 排序
query = query.order_by(Level.sort.asc())
# 记录总数
count = query.count()
# 分页查询
level_list = query.limit(limit).offset((page - 1) * limit).all()
# 若当前页超出末页(数据被并发删除),自动回退到第一页
if page > 1 and not level_list:
page = 1
count = query.count()
level_list = query.limit(limit).offset(0).all()
# 遍历数据源
result = []
if len(level_list) > 0:
for item in level_list:
# 对象转字典
data = item.to_dict()
# 加入列表
result.append(data)
# 返回结果
return R.ok(data=result, count=count)
# 根据ID查询职级详情
def get_level_detail(level_id):
db = get_db_session()
# 根据ID查询职级
level = db.query(Level).filter(and_(Level.id == level_id, Level.is_delete == 0)).first()
# 查询结果为空判断
if not level:
return None
# 对象转字典
data = level.to_dict()
# 返回结果
return data
# 添加职级
def add_level(request, data: LevelForm):
db = get_db_session()
# 唯一性校验
if db.query(Level).filter(and_(Level.name == data.name, Level.is_delete == 0)).first():
return R.failed("职级名称不能重复")
# 表单数据赋值给对象
level = Level(**data.model_dump(exclude={'id'}))
level.create_user = get_user_id(request)
# 插入数据
level.save()
# 返回结果
return R.ok(msg="添加成功")
# 更新职级
def update_level(request, data: LevelForm):
db = get_db_session()
# 记录ID判空
if not data.id or data.id <= 0:
return R.failed("记录ID不能为空")
# 唯一性校验(排除自身)
if db.query(Level).filter(and_(Level.name == data.name, Level.id != data.id, Level.is_delete == 0)).first():
return R.failed("职级名称不能重复")
# 根据ID查询记录
level = db.query(Level).filter(and_(Level.id == data.id, Level.is_delete == 0)).first()
# 查询结果判空
if not level:
return R.failed("记录不存在")
# 构造更新数据(排除 id)
updData = data.model_dump(exclude={'id'})
updData['update_user'] = get_user_id(request)
updData['update_time'] = datetime.now(tz=timezone.utc)
result = db.query(Level).filter_by(id=data.id).update(updData)
# 提交数据
if not result:
return R.failed("更新失败")
# 返回结果
return R.ok(msg="更新成功")
# 删除职级
def delete_level(level_id):
return Level.batch_delete_r(level_id)
# 设置状态
def update_level_status(request, data: LevelStatusForm):
db = get_db_session()
# 根据ID查询记录
level = db.query(Level).filter(and_(Level.id == data.id, Level.is_delete == 0)).first()
# 查询结果判空
if not level:
return R.failed("记录不存在")
# 更新记录
result = db.query(Level).filter_by(id=data.id).update({
"status": data.status,
"update_user": get_user_id(request),
"update_time": datetime.now(tz=timezone.utc)
})
# 提交数据
if not result:
return R.failed("设置失败")
# 返回结果
return R.ok(msg="设置成功")
# 导入Excel
async def import_level(request, file):
# 读取上传的文件
result = await upload.upload_file(file, ".xls|.xlsx")
if result.get('code') != 0:
return result
# 本地文件地址
relative_path = result['data']['filePath']
path = FASTAPI_UPLOAD_DIR + relative_path
def _import():
try:
# 打开工作簿
wb = load_workbook(path)
# 获取表单
ws = wb.worksheets[0]
# 读取最大行
max_row = ws.max_row
# 计数器
count = 0
# 遍历读取数据源
for item in range(2, max_row + 1):
# 职级名称
name = ws.cell(item, 2).value
# 职级排序
sort = ws.cell(item, 3).value
# 职级状态
status = ws.cell(item, 4).value
# 创建数据
level = Level(
name=name,
status=1 if status == "正常" else 2,
sort=sort,
)
level.create_user = get_user_id(request)
level.save()
count += 1
# 关闭工作簿
wb.close()
return count
finally:
# 清理临时文件
if os.path.exists(path):
os.remove(path)
# Excel解析与同步DB写入放入线程池,避免阻塞事件循环
count = await asyncio.to_thread(_import)
# 返回结果
return R.ok(msg="本次共导入【{}】条数据".format(count))
# 导出Excel
def export_level(request):
db = get_db_session()
# 查询数据
query = db.query(Level).filter(Level.is_delete == 0)
# 职级名称
name = request.query_params.get('name')
if name:
query = query.filter(Level.name.like(like_contains(name), escape='\\'))
# 职级状态
status = request.query_params.get('status')
if status:
query = query.filter(Level.status == status)
# 排序
query = query.order_by(Level.sort.asc())
# 导出数量限制,防止数据量过大导致内存溢出
EXPORT_MAX_ROWS = 10000
BATCH_SIZE = 500
# 实例化工作薄
wb = Workbook()
# 指定第一个sheet
ws = wb.worksheets[0]
# 添加表头
ws.append(['职级ID', '职级名称', '职级排序', '职级状态', '创建时间', '更新时间'])
# yield_per 分批加载,边查边写,避免双倍内存占用
for item in query.limit(EXPORT_MAX_ROWS).yield_per(BATCH_SIZE):
ws.append([
item.id, item.name, item.sort,
"正常" if item.status == 1 else "禁用",
item.create_time.strftime('%Y-%m-%d %H:%M:%S') if item.create_time else None,
item.update_time.strftime('%Y-%m-%d %H:%M:%S') if item.update_time else None,
])
db.expunge(item)
# 保存文件
save_path = FASTAPI_TEMP_PATH + "/" + time.strftime('%Y%m%d')
# 创建存放目录
mkdir(save_path)
# 定义文件名,年月日时分秒随机数
file_name = time.strftime('%Y%m%d%H%M%S') + '%05d' % random.randint(0, 99999)
# 重写合成文件名
file_name = os.path.join(file_name + ".xlsx")
# 拼接文件路径
path = save_path + "/" + file_name
# 写入文件
wb.save(r'' + path + '')
# 文件访问地址
file_url = FASTAPI_IMAGE_URL + path.replace(FASTAPI_UPLOAD_DIR, "")
# 返回结果
return R.ok(data=file_url)
# 获取职级数据列表
def get_level_list():
db = get_db_session()
# 查询职级数据列表
level_rows = db.query(Level).filter(and_(Level.is_delete == 0, Level.status == 1)).order_by(Level.sort.asc()).all()
# 实例化列表
level_list = []
# 遍历数据源
if level_rows:
for v in level_rows:
# 对象转字典
item = v.to_dict()
# 加入数组
level_list.append(item)
# 返回结果
return level_list
+9
View File
@@ -0,0 +1,9 @@
# +========== 迁移新增:本模块 Repository 层
"""modules/system/menu 数据访问层。"""
from modules.system.menu.models import Menu
from shared.base_repository import BaseRepository
class MenuRepository(BaseRepository[Menu]):
pass
menu_repo = MenuRepository(Menu)
+53
View File
@@ -0,0 +1,53 @@
from fastapi import APIRouter, Request
from modules.system.menu.schemas import MenuForm
from core.middleware import check_demo
from core.middleware import permission_required
from modules.system.menu import service as menu
from core import R
# 创建路由
router = APIRouter()
# 查询分页数据
@router.get('/list', summary='查询分页数据')
@permission_required("sys:menu:list")
def list(request: Request):
result = menu.get_menu_page(request)
return result
# 查询菜单详情
@router.get('/detail/{id}', summary='查询菜单详情')
@permission_required("sys:menu:detail")
def detail(request: Request, id):
data = menu.get_menu_detail(id)
return R.ok(data=data)
# 添加菜单
@router.post('/add', summary='添加菜单')
@permission_required("sys:menu:add")
@check_demo
async def add(request: Request, data: MenuForm):
result = await menu.add_menu(request, data)
return result
# 更新菜单
@router.put('/update', summary='更新菜单')
@permission_required("sys:menu:update")
@check_demo
async def update(request: Request, data: MenuForm):
result = await menu.update_menu(request, data)
return result
# 删除菜单
@router.delete('/delete/{id}', summary='删除菜单')
@permission_required("sys:menu:delete")
@check_demo
async def delete(request: Request, id):
result = await menu.delete_menu(id)
return result
+21
View File
@@ -0,0 +1,21 @@
from typing import Any, List, Optional
from pydantic import BaseModel, Field
# 菜单表单验证
class MenuForm(BaseModel):
id: Optional[int] = Field(None, description="菜单ID")
title: str = Field(..., min_length=1, max_length=150, description="菜单标题")
icon: Optional[str] = Field(None, max_length=50, description="菜单图标")
path: Optional[str] = Field(None, max_length=255, description="路由地址")
component: Optional[str] = Field(None, max_length=255, description="组件路径")
parent_id: int = Field(..., ge=0, description="上级ID")
type: Optional[int] = Field(None, ge=0, le=1, description="菜单类型")
target: Optional[str] = Field(None, max_length=30, description="打开方式")
permission: Optional[str] = Field(None, max_length=150, description="权限节点")
hide: Optional[int] = Field(None, ge=0, le=1, description="是否可见")
status: int = Field(..., ge=1, le=2, description="菜单状态")
sort: int = Field(..., ge=0, le=99999, description="菜单排序")
note: Optional[str] = Field(None, max_length=255, description="菜单备注")
checked_list: Optional[List[int]] = Field(default=[], description="权限节点")
+449
View File
@@ -0,0 +1,449 @@
# +======================================================================
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
# +----------------------------------------------------------------------
# | Copyright (c) 2021-2026 一米阳光团队 版权所有
# +----------------------------------------------------------------------
# | 开源协议: Apache-2.0
# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。
# +----------------------------------------------------------------------
# | 官方网站: https://www.djangoadmin.cn
# +----------------------------------------------------------------------
# | 研发团队: 一米阳光团队 荣誉出品
# +======================================================================
# | 版权和免责声明:
# | 一、知识产权声明
# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、
# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权,
# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。
# | 二、使用限制与禁止条款
# | 您理解并同意,禁止将本产品用于以下任何行为:
# | 1. 任何违反中华人民共和国法律法规的用途;
# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为;
# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。
# | 三、责任豁免
# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失
# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三
# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。
# | 四、违约与法律责任
# | 如发现任何违反本声明的行为,本团队有权:
# | 1. 无条件配合公安机关及其他有权机关的调查取证工作;
# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失;
# | 3. 保留进一步采取刑事控告等法律措施的权利。
# | 五、许可范围说明
# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包
# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通
# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、
# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造
# | 成的一切经济损失及商誉损害。
# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件
# | 许可协议》附件为准。
# +======================================================================
import asyncio
from datetime import datetime, timezone
from fastapi.logger import logger
from sqlalchemy import and_, or_
from modules.system.menu.schemas import MenuForm
from modules.system.menu.models import Menu
from modules.system.role.models import RoleMenu
from modules.system.user.models import UserRole
from core.database import get_db_session
from core import R
from core.security import get_user_id
from utils.request import like_contains, parse_id_list
from utils.perm_cache import get_cached_permissions, set_cached_permissions, get_cached_menus, set_cached_menus, invalidate_menu_change
# 查询菜单列表
def get_menu_page(request):
db = get_db_session()
# 实例化查询对象
query = db.query(Menu).filter(Menu.is_delete == 0)
# 菜单名称
name = request.query_params.get('name')
if name:
# 菜单名称模糊查询
query = query.filter(Menu.name.like(like_contains(name), escape='\\'))
# 查询数据
menu_list = query.order_by(Menu.sort.asc()).all()
# 实例化数组对象
result = []
# 遍历数据源
if menu_list:
for item in menu_list:
# 对象转字典
data = item.to_dict()
# 加入列表
result.append(data)
# 返回结果
return R.ok(data=result)
# 根据ID查询菜单详情
def get_menu_detail(menu_id):
db = get_db_session()
# 根据ID查询菜单
menu = db.query(Menu).filter(and_(Menu.id == menu_id, Menu.is_delete == 0)).first()
# 查询结果判空
if not menu:
return None
# 对象转字典
data = menu.to_dict()
# 查询菜单权限节点
permission_list = db.query(Menu).filter(
and_(Menu.parent_id == menu_id, Menu.type == 1, Menu.is_delete == 0)).all()
# 选中的权限节点
checked_list = []
if permission_list:
for v in permission_list:
checked_list.append(v.sort)
# 权限节点
data['checked_list'] = checked_list
# 返回结果
return data
# 添加菜单
async def add_menu(request, data: MenuForm):
def _add():
db = get_db_session()
# 权限节点
checkedList = data.checked_list or []
# 表单数据赋值给对象
menu = Menu(
title=data.title,
icon=data.icon,
path=data.path,
parent_id=data.parent_id,
type=data.type,
permission=data.permission,
component=data.component,
status=data.status,
hide=data.hide,
target=data.target,
sort=data.sort
)
menu.create_user = get_user_id(request)
# 插入数据
menu.save()
# 保存节点数据
set_permission(data.type, checkedList, data.title, data.path, menu.id, get_user_id(request))
# 同步DB操作放入线程池,避免阻塞事件循环
await asyncio.to_thread(_add)
# 清理权限缓存
await invalidate_menu_change()
# 返回结果
return R.ok(msg="添加成功")
# 更新菜单
async def update_menu(request, data: MenuForm):
def _update():
db = get_db_session()
# 记录ID判空
if not data.id or data.id <= 0:
return R.failed("记录ID不能为空")
# 根据ID查询记录
menu = db.query(Menu).filter(and_(Menu.id == data.id, Menu.is_delete == 0)).first()
# 查询结果判空
if not menu:
return R.failed("记录不存在")
# 权限节点
checkedList = data.checked_list or []
# 更新数据源(排除 id 和 checked_list)
updData = data.model_dump(exclude={'id', 'checked_list'})
updData['update_user'] = get_user_id(request)
updData['update_time'] = datetime.now(tz=timezone.utc)
# 更新数据源
result = db.query(Menu).filter_by(id=data.id).update(updData)
# 提交数据
if not result:
return R.failed("更新失败")
# 保存节点数据
set_permission(data.type, checkedList, data.title, data.path, menu.id, get_user_id(request))
return None
# 同步DB操作放入线程池,避免阻塞事件循环
err = await asyncio.to_thread(_update)
if err:
return err
# 清理权限缓存
await invalidate_menu_change()
# 返回结果
return R.ok(msg="更新成功")
# 删除菜单
async def delete_menu(menu_id):
def _delete():
db = get_db_session()
# 记录ID为空判断
if not menu_id:
return R.failed("记录ID不存在"), None
# 分裂字符串
id_list = parse_id_list(menu_id)
if not id_list:
return R.failed("记录ID不存在"), None
# 批量查询并校验
records = db.query(Menu).filter(and_(Menu.id.in_(id_list), Menu.is_delete == 0)).all()
if len(records) != len(id_list):
return R.failed("记录不存在"), None
# 批量设置删除标识
for record in records:
record.is_delete = 1
return None, records
# 同步DB操作放入线程池,避免阻塞事件循环
err, records = await asyncio.to_thread(_delete)
if err:
return err
# 清理权限缓存
await invalidate_menu_change()
# 返回结果
return R.ok(msg="本次共删除{0}条数据".format(len(records)))
# 保存节点数据
def set_permission(menu_type, checkedList, title, path, parent_id, user_id):
db = get_db_session()
# 参数判空
if menu_type != 0 or not checkedList or path == "":
return
# 删除现有节点
db.query(Menu).filter(and_(Menu.parent_id == parent_id, Menu.is_delete == 0)).delete()
# 模块名称
moduleTitle = title.replace("管理", "")
# 请求path处理
pathArr = path.split('/')
if len(pathArr) < 3:
return
# 模块名
moduleName = pathArr[len(pathArr) - 1]
# 遍历数据源
for v in checkedList:
# 实例化菜单对象
entity = Menu()
# 节点值
value = int(v)
if value == 1:
# 查询
entity.title = "查询" + moduleTitle
entity.path = "/" + moduleName + "/list"
entity.permission = "sys:" + moduleName + ":list"
elif value == 5:
# 添加
entity.title = "添加" + moduleTitle
entity.path = "/" + moduleName + "/add"
entity.permission = "sys:" + moduleName + ":add"
elif value == 10:
# 修改
entity.title = "修改" + moduleTitle
entity.path = "/" + moduleName + "/update"
entity.permission = "sys:" + moduleName + ":update"
elif value == 15:
# 删除
entity.title = "删除" + moduleTitle
entity.path = "/" + moduleName + "/delete"
entity.permission = "sys:" + moduleName + ":delete"
elif value == 20:
# 详情
entity.title = moduleTitle + "详情"
entity.path = "/" + moduleName + "/detail"
entity.permission = "sys:" + moduleName + ":detail"
elif value == 25:
# 设置状态
entity.title = "设置状态"
entity.path = "/" + moduleName + "/status"
entity.permission = "sys:" + moduleName + ":status"
elif value == 30:
# 批量删除
entity.title = "批量删除"
entity.path = "/" + moduleName + "/dall"
entity.permission = "sys:" + moduleName + ":dall"
elif value == 35:
# 添加子级
entity.title = "添加子级"
entity.path = "/" + moduleName + "/addz"
entity.permission = "sys:" + moduleName + ":addz"
elif value == 40:
# 全部展开
entity.title = "全部展开"
entity.path = "/" + moduleName + "/expand"
entity.permission = "sys:" + moduleName + ":expand"
elif value == 45:
# 全部折叠
entity.title = "全部折叠"
entity.path = "/" + moduleName + "/collapse"
entity.permission = "sys:" + moduleName + ":collapse"
elif value == 50:
# 导出数据
entity.title = "导出" + moduleTitle
entity.path = "/" + moduleName + "/export"
entity.permission = "sys:" + moduleName + ":export"
elif value == 55:
# 导入数据
entity.title = "导入" + moduleTitle
entity.path = "/" + moduleName + "/import"
entity.permission = "sys:" + moduleName + ":import"
elif value == 60:
# 分配权限
entity.title = "分配权限"
entity.path = "/" + moduleName + "/permission"
entity.permission = "sys:" + moduleName + ":permission"
elif value == 65:
# 重置密码
entity.title = "重置密码"
entity.path = "/" + moduleName + "/resetPwd"
entity.permission = "sys:" + moduleName + ":resetPwd"
# 设置默认值
entity.parent_id = parent_id
entity.type = 1
entity.status = 1
entity.target = "_self"
entity.sort = value
entity.create_user = user_id
# 插入数据
entity.save()
# 获取用户权限节点
def get_permissions_list_sync(user_id):
"""同步查询用户权限节点(直查DB,不读缓存),供同步鉴权路径使用"""
db = get_db_session()
if user_id == 1:
# 超级管理员
menu_list = db.query(Menu).filter(and_(Menu.is_delete == 0, Menu.type == 1)).all()
else:
# 其他用户:通过 ORM join 查询权限节点
menu_list = (
db.query(Menu)
.join(RoleMenu, RoleMenu.menu_id == Menu.id)
.join(UserRole, UserRole.role_id == RoleMenu.role_id)
.filter(
and_(
UserRole.user_id == user_id,
or_(Menu.type == 1, and_(Menu.type == 0, Menu.permission != "")),
Menu.status == 1,
Menu.is_delete == 0,
)
)
.all()
)
return [item.permission for item in (menu_list or [])]
async def get_permissions_list(user_id):
# 尝试读取缓存
cached = await get_cached_permissions(user_id)
if cached is not None:
return cached
permission_list = await asyncio.to_thread(get_permissions_list_sync, user_id)
# 写入缓存
await set_cached_permissions(user_id, permission_list)
return permission_list
# 根据用户ID查询菜单列表
def get_permission_menu_list_sync(user_id):
"""同步查询用户菜单树(直查DB,不读缓存),供线程池内使用"""
db = get_db_session()
if user_id == 1:
# 超级管理员
# 查询全部菜单列表
menu_list = db.query(Menu).filter(and_(Menu.is_delete == 0, Menu.status == 1, Menu.type == 0)).order_by(
Menu.sort.asc())
return get_tree_list(menu_list)
# 其他用户:通过 ORM join 查询菜单列表
menu_list = (
db.query(Menu)
.join(RoleMenu, RoleMenu.menu_id == Menu.id)
.join(UserRole, UserRole.role_id == RoleMenu.role_id)
.filter(
and_(
UserRole.user_id == user_id,
Menu.type == 0,
Menu.status == 1,
Menu.is_delete == 0,
)
)
.order_by(Menu.sort.asc())
.all()
)
return get_tree_list(menu_list)
async def get_permission_menu_list(user_id):
# 尝试读取缓存
cached = await get_cached_menus(user_id)
if cached is not None:
return cached
menu_list = await asyncio.to_thread(get_permission_menu_list_sync, user_id)
# 写入缓存
await set_cached_menus(user_id, menu_list)
return menu_list
# 根据数据源获取树状结构
def get_tree_list(source_list):
# 实例化数组
menu_list = []
if source_list:
for item in source_list:
data = {
'id': item.id,
'title': item.title,
'icon': item.icon,
'path': item.path,
'parent_id': item.parent_id,
'type': item.type,
'component': item.component,
'permission': item.permission,
'target': item.target,
'hide': item.hide,
}
menu_list.append(data)
# 处理数据源为树状结构
result = get_tree(menu_list, 0)
# 返回结果
return result
# 获取树状结构
def get_tree(data, parent_id):
# 按 parent_id 建立索引,O(n) 构建 + O(n) 遍历
children_map = {}
for item in data:
pid = item["parent_id"]
if pid not in children_map:
children_map[pid] = []
children_map[pid].append(item)
def build(pid):
result = []
for item in children_map.get(pid, []):
item["children"] = build(item["id"])
result.append(item)
return result
return build(parent_id)
+9
View File
@@ -0,0 +1,9 @@
# +========== 迁移新增:本模块 Repository 层
"""modules/system/position 数据访问层。"""
from modules.system.position.models import Position
from shared.base_repository import BaseRepository
class PositionRepository(BaseRepository[Position]):
pass
position_repo = PositionRepository(Position)
+90
View File
@@ -0,0 +1,90 @@
import asyncio
from fastapi import APIRouter, Request
from modules.system.position.schemas import PositionForm, PositionStatusForm
from core.middleware import check_demo
from core.middleware import permission_required
from modules.system.position import service as position
from core import R
from utils.request import parse_batch_ids
# 创建路由
router = APIRouter()
# 查询分页数据
@router.get('/page', summary='查询分页数据')
@permission_required("sys:position:page")
def page(request: Request):
result = position.get_position_page(request)
return result
# 查询岗位详情
@router.get('/detail/{id}', summary='查询岗位详情')
@permission_required("sys:position:detail")
def detail(request: Request, id):
data = position.get_position_detail(id)
return R.ok(data=data)
# 添加岗位
@router.post('/add', summary='添加岗位')
@permission_required("sys:position:add")
@check_demo
def add(request: Request, data: PositionForm):
result = position.add_position(request, data)
return result
# 更新岗位
@router.put('/update', summary='更新职级')
@permission_required("sys:position:update")
@check_demo
def update(request: Request, data: PositionForm):
result = position.update_position(request, data)
return result
# 删除岗位
@router.delete('/delete/{id}', summary='删除岗位')
@permission_required("sys:position:delete")
@check_demo
def delete(request: Request, id):
result = position.delete_position(id)
return result
# 设置岗位状态
@router.put('/status', summary='设置岗位状态')
@permission_required("sys:position:status")
@check_demo
def status(request: Request, data: PositionStatusForm):
result = position.update_position_status(request, data)
return result
# 获取岗位列表
@router.get('/getPositionList', summary='获取岗位列表')
def get_position_list():
result = position.get_position_list()
return R.ok(data=result)
# 批量删除岗位
@router.delete('/batchDelete', summary='批量删除岗位')
@permission_required("sys:position:batch_delete")
@check_demo
async def batch_delete(request: Request):
"""
批量删除岗位
Request Body:
[1, 2, 3] # 岗位ID数组
"""
id_str, err = await parse_batch_ids(request)
if err:
return err
return await asyncio.to_thread(position.delete_position, id_str)
+15
View File
@@ -0,0 +1,15 @@
from typing import Optional
from pydantic import BaseModel, Field
# 岗位表单验证
class PositionForm(BaseModel):
id: Optional[int] = Field(None, description="岗位ID")
name: str = Field(..., min_length=1, max_length=150, description="岗位名称")
status: int = Field(..., ge=1, le=2, description="岗位状态")
sort: int = Field(..., ge=0, le=99999, description="岗位排序")
# 设置状态表单
class PositionStatusForm(BaseModel):
id: int = Field(..., gt=0, description="岗位ID")
status: int = Field(..., ge=1, le=2, description="岗位状态")
+152
View File
@@ -0,0 +1,152 @@
from datetime import datetime, timezone
from fastapi.logger import logger
from sqlalchemy import and_
from shared.constants import PAGE_SIZE
from modules.system.position.schemas import PositionForm, PositionStatusForm
from modules.system.position.models import Position
from core.database import get_db_session
from core import R
from core.security import get_user_id
from utils.request import like_contains
# 查询岗位分页数据
def get_position_page(request):
db = get_db_session()
# 页码
page = int(request.query_params.get("pageNo", 1))
# 每页数
limit = int(request.query_params.get("pageSize", PAGE_SIZE))
# 实例化查询对象
query = db.query(Position).filter(Position.is_delete == 0)
# 岗位名称
name = request.query_params.get('name')
if name:
query = query.filter(Position.name.like(like_contains(name), escape='\\'))
# 岗位状态筛选
status = request.query_params.get('status')
if status:
query = query.filter(Position.status == status)
# 排序
query = query.order_by(Position.sort.asc())
# 记录总数
count = query.count()
# 分页查询
position_list = query.limit(limit).offset((page - 1) * limit).all()
# 若当前页超出末页(数据被并发删除),自动回退到第一页
if page > 1 and not position_list:
page = 1
count = query.count()
position_list = query.limit(limit).offset(0).all()
# 实例化结果
result = []
# 遍历数据源
if len(position_list) > 0:
for item in position_list:
# 对象转字典
data = item.to_dict()
# 加入列表
result.append(data)
# 返回结果
return R.ok(data=result, count=count)
# 根据ID查询岗位详情
def get_position_detail(position_id):
db = get_db_session()
# 根据ID查询岗位
position = db.query(Position).filter(and_(Position.id == position_id, Position.is_delete == 0)).first()
# 查询结果判空
if not position:
return None
# 对象转字典
data = position.to_dict()
# 返回结果
return data
# 添加岗位
def add_position(request, data: PositionForm):
db = get_db_session()
# 唯一性校验
if db.query(Position).filter(and_(Position.name == data.name, Position.is_delete == 0)).first():
return R.failed("岗位名称不能重复")
# 表单数据赋值给对象
position = Position(**data.model_dump(exclude={'id'}))
position.create_user = get_user_id(request)
# 插入数据
position.save()
# 返回结果
return R.ok(msg="添加成功")
# 更新岗位
def update_position(request, data: PositionForm):
db = get_db_session()
# 记录ID判空
if not data.id or data.id <= 0:
return R.failed("记录ID不能为空")
# 唯一性校验(排除自身)
if db.query(Position).filter(and_(Position.name == data.name, Position.id != data.id, Position.is_delete == 0)).first():
return R.failed("岗位名称不能重复")
# 根据ID查询记录
position = db.query(Position).filter(and_(Position.id == data.id, Position.is_delete == 0)).first()
# 查询结果判空
if not position:
return R.failed("记录不存在")
# 构造更新数据(排除 id)
updData = data.model_dump(exclude={'id'})
updData['update_user'] = get_user_id(request)
updData['update_time'] = datetime.now(tz=timezone.utc)
result = db.query(Position).filter_by(id=data.id).update(updData)
# 提交数据
if not result:
return R.failed("更新失败")
# 返回结果
return R.ok(msg="更新成功")
# 删除岗位
def delete_position(position_id):
return Position.batch_delete_r(position_id)
# 设置状态
def update_position_status(request, data: PositionStatusForm):
db = get_db_session()
# 根据ID查询记录
position = db.query(Position).filter(and_(Position.id == data.id, Position.is_delete == 0)).first()
# 查询结果判空
if not position:
return R.failed("记录不存在")
# 更新记录
result = db.query(Position).filter_by(id=data.id).update({
"status": data.status,
"update_user": get_user_id(request),
"update_time": datetime.now(tz=timezone.utc)
})
# 提交数据
if not result:
return R.failed("设置失败")
# 返回结果
return R.ok(msg="设置成功")
# 获取岗位数据列表
def get_position_list():
db = get_db_session()
# 查询岗位数据列表
position_rows = db.query(Position).filter(and_(Position.is_delete == 0, Position.status == 1)).order_by(
Position.sort.asc()).all()
# 实例化列表
position_list = []
# 遍历数据源
if position_rows:
for v in position_rows:
# 对象转字典
item = v.to_dict()
# 加入数组
position_list.append(item)
# 返回结果
return position_list
+14
View File
@@ -0,0 +1,14 @@
# +========== 迁移新增:本模块 Repository 层
"""modules/system/role 数据访问层。"""
from modules.system.role.models import Role, RoleMenu
from shared.base_repository import BaseRepository
class RoleRepository(BaseRepository[Role]):
pass
role_repo = RoleRepository(Role)
class RoleMenuRepository(BaseRepository[RoleMenu]):
pass
rolemenu_repo = RoleMenuRepository(RoleMenu)
+130
View File
@@ -0,0 +1,130 @@
from fastapi import APIRouter, Request
from modules.system.role.schemas import RoleForm, RoleStatusForm
from core.middleware import check_demo
from core.middleware import permission_required
from modules.system.role import service as role
from core import R
from utils.request import parse_batch_ids
# 创建路由
role_router = APIRouter()
# 查询分页数据
@role_router.get('/page', summary='查询分页数据')
@permission_required("sys:role:page")
def page(request: Request):
# 调用查询分页数据服务方法
result = role.get_role_page(request)
# 返回结果
return result
# 查询角色详情
@role_router.get('/detail/{id}', summary='查询角色详情')
@permission_required("sys:role:detail")
def detail(request: Request, id):
# 调用查询角色详情服务方法
data = role.get_role_detail(id)
# 返回结果
return R.ok(data=data)
# 添加角色
@role_router.post('/add', summary='添加角色')
@permission_required("sys:role:add")
@check_demo
def add(request: Request, data: RoleForm):
# 调用添加角色服务
result = role.add_role(request, data)
# 返回结果
return result
# 更新角色
@role_router.put('/update', summary='更新角色')
@permission_required("sys:role:update")
@check_demo
def update(request: Request, data: RoleForm):
# 调用更新角色服务方法
result = role.update_role(request, data)
# 返回结果
return result
# 删除角色
@role_router.delete('/delete/{id}', summary='删除角色')
@permission_required("sys:role:delete")
@check_demo
async def delete(request: Request, id):
# 调用删除角色服务方法
result = await role.delete_role(id)
# 返回结果
return result
# 设置角色状态
@role_router.put('/status', summary='设置角色状态')
@permission_required("sys:role:status")
@check_demo
def status(request: Request, data: RoleStatusForm):
# 调用设置角色状态服务方法
result = role.update_role_status(request, data)
# 返回结果
return result
# 获取角色列表
@role_router.get('/getRoleList', summary='获取角色列表')
def get_role_list(request: Request):
# 调用获取角色列表方法
result = role.get_role_list()
# 返回结果
return R.ok(data=result)
# 批量删除角色
@role_router.delete('/batchDelete', summary='批量删除角色')
@permission_required("sys:role:batch_delete")
@check_demo
async def batch_delete(request: Request):
"""
批量删除角色
Request Body:
[1, 2, 3] # 角色ID数组
"""
id_str, err = await parse_batch_ids(request)
if err:
return err
return await role.delete_role(id_str)
from fastapi import APIRouter, Request
from modules.system.role.schemas import RoleMenuForm
from core.middleware import check_demo
from core.middleware import permission_required
from modules.system.role import service as role_menu
from core import R
# 创建路由
role_menu_router = APIRouter()
# 查询角色菜单列表
@role_menu_router.get('/index/{role_id}', summary='查询角色菜单列表')
@permission_required("sys:rolemenu:index")
def index(request: Request, role_id):
menu_list = role_menu.get_role_menu_list(role_id)
return R.ok(data=menu_list)
# 保存角色菜单数据
@role_menu_router.post('/save', summary='保存角色菜单数据')
@permission_required("sys:rolemenu:save")
@check_demo
async def save(request: Request, data: RoleMenuForm):
result = await role_menu.save_role_menu(request, data)
return result
+28
View File
@@ -0,0 +1,28 @@
from typing import Optional
from pydantic import BaseModel, Field
# 角色表单验证
class RoleForm(BaseModel):
id: Optional[int] = Field(None, description="角色ID")
name: str = Field(..., min_length=1, max_length=150, description="角色名称")
code: str = Field(..., min_length=1, max_length=30, description="角色编码")
status: int = Field(..., ge=1, le=2, description="角色状态")
sort: int = Field(..., ge=0, le=99999, description="角色排序")
note: Optional[str] = Field(None, max_length=255, description="角色备注")
# 设置状态表单
class RoleStatusForm(BaseModel):
id: int = Field(..., gt=0, description="角色ID")
status: int = Field(..., ge=1, le=2, description="角色状态")
from typing import List, Optional
from pydantic import BaseModel, Field
# 角色菜单表单
class RoleMenuForm(BaseModel):
role_id: int = Field(..., gt=0, description="角色ID")
menu_id: Optional[List[int]] = Field(default=[], description="菜单ID列表")
+259
View File
@@ -0,0 +1,259 @@
import asyncio
from datetime import datetime, timezone
from fastapi.logger import logger
from sqlalchemy import and_
from shared.constants import PAGE_SIZE
from modules.system.role.schemas import RoleForm, RoleStatusForm
from modules.system.role.models import Role
from core.database import get_db_session
from core import R
from core.security import get_user_id
from utils.request import like_contains, parse_id_list
from utils.perm_cache import invalidate_role_perms
# 查询角色分页数据
def get_role_page(request):
db = get_db_session()
# 页码
page = int(request.query_params.get("pageNo", 1))
# 每页数
limit = int(request.query_params.get("pageSize", PAGE_SIZE))
# 分页查询
query = db.query(Role).filter(Role.is_delete == 0)
# 角色名称
name = request.query_params.get('name')
if name:
query = query.filter(Role.name.like(like_contains(name), escape='\\'))
# 排序
query = query.order_by(Role.sort.asc())
# 记录总数
count = query.count()
# 查询分页数据
role_list = query.limit(limit).offset((page - 1) * limit).all()
# 若当前页超出末页(数据被并发删除),自动回退到第一页
if page > 1 and not role_list:
page = 1
count = query.count()
role_list = query.limit(limit).offset(0).all()
# 实例化返回对象
result = []
# 遍历数据源
if len(role_list) > 0:
for item in role_list:
# 对象转字典
data = item.to_dict()
# 加入列表
result.append(data)
# 返回结果
return R.ok(data=result, count=count)
# 根据角色ID查询详情
def get_role_detail(role_id):
db = get_db_session()
# 根据ID查询角色
role = db.query(Role).filter(and_(Role.id == role_id, Role.is_delete == 0)).first()
# 查询结果判空
if not role:
return None
# 对象转字典
data = role.to_dict()
# 返回结果
return data
# 添加角色
def add_role(request, data: RoleForm):
db = get_db_session()
# 唯一性校验
if db.query(Role).filter(and_(Role.name == data.name, Role.is_delete == 0)).first():
return R.failed("角色名称不能重复")
# 表单数据赋值给对象
role = Role(**data.model_dump(exclude={'id'}))
role.create_user = get_user_id(request)
# 插入数据
role.save()
# 返回结果
return R.ok(msg="添加成功")
# 更新角色
def update_role(request, data: RoleForm):
db = get_db_session()
# 记录ID判空
if not data.id or data.id <= 0:
return R.failed("记录ID不能为空")
# 唯一性校验(排除自身)
if db.query(Role).filter(and_(Role.name == data.name, Role.id != data.id, Role.is_delete == 0)).first():
return R.failed("角色名称不能重复")
# 根据ID查询记录
role = db.query(Role).filter(and_(Role.id == data.id, Role.is_delete == 0)).first()
# 查询结果判空
if not role:
return R.failed("记录不存在")
# 构造更新数据(排除 id)
updData = data.model_dump(exclude={'id'})
updData['update_user'] = get_user_id(request)
updData['update_time'] = datetime.now(tz=timezone.utc)
result = db.query(Role).filter_by(id=data.id).update(updData)
# 提交数据
if not result:
return R.failed("更新失败")
# 返回结果
return R.ok(msg="更新成功")
# 删除角色
async def delete_role(role_id):
def _delete():
db = get_db_session()
# 记录ID为空判断
if not role_id:
return R.failed("记录ID不存在"), None
# 分裂字符串
id_list = parse_id_list(role_id)
if not id_list:
return R.failed("记录ID不存在"), None
# 批量查询并校验
records = db.query(Role).filter(and_(Role.id.in_(id_list), Role.is_delete == 0)).all()
if len(records) != len(id_list):
return R.failed("记录不存在"), None
# 批量设置删除标识
for record in records:
record.is_delete = 1
return None, (id_list, records)
# 同步DB操作放入线程池,避免阻塞事件循环
err, result = await asyncio.to_thread(_delete)
if err:
return err
id_list, records = result
# 清理被删角色下所有用户的权限缓存
for rid in id_list:
await invalidate_role_perms(rid)
# 返回结果
return R.ok(msg="本次共删除{0}条数据".format(len(records)))
# 设置状态
def update_role_status(request, data: RoleStatusForm):
db = get_db_session()
# 根据ID查询记录
role = db.query(Role).filter(and_(Role.id == data.id, Role.is_delete == 0)).first()
# 查询结果判空
if not role:
return R.failed("记录不存在")
# 更新记录
result = db.query(Role).filter_by(id=data.id).update({
"status": data.status,
"update_user": get_user_id(request),
"update_time": datetime.now(tz=timezone.utc)
})
# 提交数据
if not result:
return R.failed("设置失败")
# 返回结果
return R.ok(msg="设置成功")
# 获取角色列表
def get_role_list():
db = get_db_session()
# 查询角色列表
role_rows = db.query(Role).filter(and_(Role.is_delete == 0, Role.status == 1)).order_by(Role.sort.asc()).all()
# 实例化角色列表
role_list = []
# 遍历数据源
if role_rows:
for v in role_rows:
# 对象转字典
item = v.to_dict()
# 加入数组
role_list.append(item)
# 返回结果
return role_list
import asyncio
from fastapi.logger import logger
from sqlalchemy import and_
from modules.system.role.schemas import RoleMenuForm
from modules.system.menu.models import Menu
from modules.system.role.models import RoleMenu
from core.database import get_db_session
from core import R
from utils.perm_cache import invalidate_role_perms
# 根据角色ID查询菜单列表
def get_role_menu_list(role_id):
db = get_db_session()
# 获取全部菜单列表(仅查询 id、title、parent_id,避免 SELECT * 全量拉取大字段)
menuList = db.query(Menu.id, Menu.title, Menu.parent_id).filter(and_(Menu.is_delete == 0, Menu.status == 1)).order_by(Menu.sort.asc()).all()
if len(menuList) == 0:
return None
# 根据角色ID查询角色菜单关系数据
role_menu = db.query(RoleMenu).filter(RoleMenu.role_id == role_id).all()
# 菜单ID集合
idList = []
# 遍历角色菜单数据源
if role_menu:
for v in role_menu:
# 加入数组
idList.append(v.menu_id)
# 实例化菜单列表
menu_list = []
# 遍历菜单数据
for menu in menuList:
# 菜单ID
menu_id = menu.id
data = {
'id': menu_id,
'title': menu.title,
'open': True,
'parentId': menu.parent_id,
}
if menu_id in idList:
data['checked'] = True
# 加入数组
menu_list.append(data)
# 返回结果
return menu_list
# 保存角色菜单数据
async def save_role_menu(request, data: RoleMenuForm):
def _save():
db = get_db_session()
# 角色ID
role_id = data.role_id
# 菜单ID列表
menuIdList = data.menu_id or []
# 删除当前角色ID相关菜单权限
db.query(RoleMenu).filter(RoleMenu.role_id == role_id).delete()
# 处理菜单数据
if menuIdList:
new_menus = []
for menu_id in menuIdList:
if not menu_id:
continue
new_menus.append(RoleMenu(role_id=role_id, menu_id=menu_id))
db.add_all(new_menus)
return role_id
# 同步DB操作放入线程池,避免阻塞事件循环
role_id = await asyncio.to_thread(_save)
# 清理该角色下所有用户的权限缓存
await invalidate_role_perms(role_id)
# 返回结果
return R.ok()
+14
View File
@@ -0,0 +1,14 @@
# +========== 迁移新增:本模块 Repository 层
"""modules/system/user 数据访问层。"""
from modules.system.user.models import User, UserRole
from shared.base_repository import BaseRepository
class UserRepository(BaseRepository[User]):
pass
user_repo = UserRepository(User)
class UserRoleRepository(BaseRepository[UserRole]):
pass
userrole_repo = UserRoleRepository(UserRole)
+107
View File
@@ -0,0 +1,107 @@
import asyncio
from fastapi import APIRouter
from starlette.requests import Request
from modules.system.user.schemas import UserForm, UserStatusForm, ResetPwdForm
from core.middleware import check_demo
from core.middleware import permission_required
from modules.system.user import service as user
from core import R
from utils.request import parse_batch_ids
# 创建路由
router = APIRouter()
# 查询分页数据
@router.get('/page', summary='查询分页数据')
@permission_required("sys:user:page")
def page(request: Request):
# 调用查询分页数据服务方法
result = user.get_user_page(request)
# 返回结果
return result
# 查询用户详情
@router.get('/detail/{id}', summary='查询用户详情')
@permission_required("sys:user:detail")
def detail(request: Request, id):
# 调用查询用户详情服务方法
data = user.get_user_detail(id)
# 返回结果
return R.ok(data=data)
# 添加用户
@router.post('/add', summary='添加用户')
@permission_required("sys:user:add")
@check_demo
async def add(request: Request, data: UserForm):
# 调用添加用户服务
result = await user.add_user(request, data)
# 返回结果
return result
# 更新用户
@router.put('/update', summary='更新用户')
@permission_required("sys:user:update")
@check_demo
async def update(request: Request, data: UserForm):
# 调用更新用户服务方法
result = await user.update_user(request, data)
# 返回结果
return result
# 删除用户
@router.delete('/delete/{id}', summary='删除用户')
@permission_required("sys:user:delete")
@check_demo
def delete(request: Request, id):
# 调用删除用户服务方法
result = user.delete_user(id)
# 返回结果
return result
# 设置用户状态
@router.put('/status', summary='设置用户状态')
@permission_required("sys:user:status")
@check_demo
def status(request: Request, data: UserStatusForm):
# 调用设置用户状态服务方法
result = user.update_user_status(request, data)
# 返回结果
return result
# 重置密码
@router.post('/resetPwd', summary='重置密码')
@permission_required("sys:user:resetPwd")
@check_demo
def reset_pwd(request: Request, data: ResetPwdForm):
# 调用重置密码服务方法
result = user.reset_user_pwd(request, data)
# 返回结果
return result
# 批量删除用户
@router.delete('/batchDelete', summary='批量删除用户')
@permission_required("sys:user:batch_delete")
@check_demo
async def batch_delete(request: Request):
"""
批量删除用户
Request Body:
[1, 2, 3] # 用户ID数组
"""
id_str, err = await parse_batch_ids(request)
if err:
return err
return await asyncio.to_thread(user.delete_user, id_str)
+61
View File
@@ -0,0 +1,61 @@
from datetime import date
from typing import Any, Optional
from pydantic import BaseModel, Field, model_validator
# 用户表单验证
class UserForm(BaseModel):
id: Optional[int] = Field(None, description="用户ID")
realname: str = Field(..., min_length=1, max_length=150, description="用户姓名")
nickname: str = Field(..., min_length=1, max_length=150, description="用户昵称")
gender: int = Field(..., ge=1, le=3, description="性别")
avatar: str = Field(..., min_length=1, max_length=255, description="头像")
mobile: str = Field(..., min_length=1, max_length=30, description="手机号")
email: str = Field(..., min_length=1, max_length=30, description="邮箱")
birthday: date
dept_id: int = Field(..., ge=0, description="部门ID")
level_id: int = Field(..., ge=0, description="职级ID")
position_id: int = Field(..., ge=0, description="岗位ID")
city: Any = Field(..., description="行政区划")
province_code: Optional[str] = Field(None, max_length=30, description="省份编码")
city_code: Optional[str] = Field(None, max_length=30, description="城市编码")
district_code: Optional[str] = Field(None, max_length=30, description="县区编码")
address: str = Field(..., min_length=1, max_length=255, description="详细地址")
username: str = Field(..., min_length=1, max_length=30, description="登录账号")
password: Optional[str] = Field(None, min_length=6, max_length=128, description="登录密码")
intro: Optional[str] = Field(None, max_length=255, description="个人简介")
status: int = Field(..., ge=1, le=2, description="用户状态")
note: Optional[str] = Field(None, max_length=255, description="用户备注")
sort: int = Field(..., ge=0, le=99999, description="用户排序")
roles: Optional[Any] = Field(default=[], description="用户角色")
# 设置状态表单
class UserStatusForm(BaseModel):
id: int = Field(..., gt=0, description="用户ID")
status: int = Field(..., ge=1, le=2, description="用户状态")
# 修改个人信息表单验证
class UserInfoForm(BaseModel):
realname: str = Field(..., min_length=1, max_length=150, description="用户姓名")
nickname: str = Field(..., min_length=1, max_length=150, description="用户昵称")
gender: int = Field(..., ge=1, le=3, description="性别")
mobile: str = Field(..., min_length=1, max_length=30, description="手机号")
email: str = Field(..., min_length=1, max_length=30, description="邮箱")
address: str = Field(..., min_length=1, max_length=255, description="详细地址")
intro: Optional[str] = Field(None, max_length=255, description="个人简介")
# 修改密码表单验证
class UpdatePwdForm(BaseModel):
oldPassword: str = Field(..., min_length=6, max_length=128, description="原始密码")
newPassword: str = Field(..., min_length=6, max_length=128, description="新密码")
rePassword: str = Field(..., min_length=6, max_length=128, description="确认密码")
@model_validator(mode='after')
def check_passwords_match(self):
if self.newPassword != self.rePassword:
raise ValueError('rePassword: 两次输入的密码不一致')
return self
# 重置密码表单
class ResetPwdForm(BaseModel):
id: int = Field(..., gt=0, description="用户ID")
+430
View File
@@ -0,0 +1,430 @@
from datetime import datetime, timezone
import asyncio
from fastapi.logger import logger
from sqlalchemy import and_
from shared.enums import GENDER_LIST
from shared.constants import PAGE_SIZE, DEFAULT_PASSWORD
from modules.system.user.schemas import UserForm, UserStatusForm, UpdatePwdForm, UserInfoForm, ResetPwdForm
from modules.system.dept.models import Dept
from modules.system.level.models import Level
from modules.system.position.models import Position
from modules.system.user.models import User
from modules.system.user.models import UserRole
from core.database import get_db_session
from core import R
from core import password
from utils.rich_text import get_file_url, save_file
from utils.request import like_contains
from core.security import get_user_id
from utils.perm_cache import invalidate_user_perms
# 查询用户分页数据
def get_user_page(request):
db = get_db_session()
# 页码
page = int(request.query_params.get("pageNo", 1))
# 每页数
limit = int(request.query_params.get("pageSize", PAGE_SIZE))
# 实例化查询对象
query = db.query(User).filter(User.is_delete == 0)
# 用户账号
username = request.query_params.get('username')
if username:
query = query.filter(User.username.like(like_contains(username), escape='\\'))
# 性别
gender = request.query_params.get('gender')
if gender:
query = query.filter(User.gender == gender)
# 用户状态
status = request.query_params.get('status')
if status:
query = query.filter(User.status == status)
# 排序
query = query.order_by(User.id.asc())
# 记录总数
count = query.count()
# 分页查询
user_list = query.limit(limit).offset((page - 1) * limit).all()
# 若当前页超出末页(数据被并发删除),自动回退到第一页
if page > 1 and not user_list:
page = 1
count = query.count()
user_list = query.limit(limit).offset(0).all()
# 实例化结果
result = []
# 遍历数据源
if len(user_list) > 0:
# 仅查询当前页用到的部门ID
dept_ids = {item.dept_id for item in user_list if item.dept_id}
deptList = {}
if dept_ids:
for dept in db.query(Dept).filter(Dept.id.in_(dept_ids), Dept.is_delete == 0).all():
deptList[dept.id] = dept.name
# 仅查询当前页用到的职级ID
level_ids = {item.level_id for item in user_list if item.level_id}
levelList = {}
if level_ids:
for level in db.query(Level).filter(Level.id.in_(level_ids), Level.is_delete == 0).all():
levelList[level.id] = level.name
# 仅查询当前页用到的岗位ID
position_ids = {item.position_id for item in user_list if item.position_id}
positionList = {}
if position_ids:
for position in db.query(Position).filter(Position.id.in_(position_ids), Position.is_delete == 0).all():
positionList[position.id] = position.name
# 批量查询用户角色
user_ids = [item.id for item in user_list]
roleMap = get_user_role_map(user_ids)
for item in user_list:
# 城市
city_list = []
# 省份编码
city_list.append(item.province_code)
# 城市编码
city_list.append(item.city_code)
# 县区编码
city_list.append(item.district_code)
# 对象转字典
data = item.to_dict()
# 性别
data['gender_name'] = GENDER_LIST.get(item.gender)
# 头像
data['avatar'] = get_file_url(item.avatar) if item.avatar else ""
# 部门名称
data['dept_name'] = deptList.get(item.dept_id) if deptList else None
# 职级名称
data['level_name'] = levelList.get(item.level_id) if levelList else None
# 岗位名称
data['position_name'] = positionList.get(item.position_id) if positionList else None
# 角色列表
data['roleList'] = roleMap.get(item.id, [])
# 行政区划
data['city'] = city_list
# 加入列表
result.append(data)
# 返回结果
return R.ok(data=result, count=count)
# 根据ID查询用户详情
def get_user_detail(user_id):
db = get_db_session()
# 根据ID查询用户
user = db.query(User).filter(and_(User.id == user_id, User.is_delete == 0)).first()
# 查询结果判空
if not user:
return None
# 获取用户角色数据
roleList = db.query(UserRole).filter(UserRole.user_id == user.id).all()
roles = []
for role in roleList:
roles.append(role.role_id)
# 城市编码
cityList = []
# 省份编号
cityList.append(user.province_code)
# 城市编码
cityList.append(user.city_code)
# 县区编码
cityList.append(user.district_code)
# 对象转字典
data = user.to_dict()
# 头像
data['avatar'] = get_file_url(user.avatar) if user.avatar else ""
# 角色
data['roles'] = roles
# 行政区划
data['city'] = cityList
# 返回结果
return data
# 添加用户
async def add_user(request, data: UserForm):
def _add():
db = get_db_session()
# 唯一性校验
if db.query(User).filter(and_(User.username == data.username, User.is_delete == 0)).first():
return R.failed("登录账号不能重复"), None
# 获取表单数据(排除非数据列)
formData = data.model_dump(exclude={'city', 'roles'})
# 图片处理
avatar = data.avatar
if avatar:
formData['avatar'] = save_file(avatar, "user")
# 密码加密:未传密码时使用默认密码
pwd = data.password or DEFAULT_PASSWORD
salt = password.generate_salt()
formData['password'] = password.encrypt_password(pwd, salt)
formData['salt'] = salt
# 行政区划处理
citys = data.city
if citys and len(citys) == 3:
formData['province_code'] = citys[0]
formData['city_code'] = citys[1]
formData['district_code'] = citys[2]
# 表单数据赋值给对象
user = User(**formData)
user.create_user = get_user_id(request)
# 插入数据
user.save()
return None, user.id
# 同步DB/文件操作放入线程池,避免阻塞事件循环
err, user_id = await asyncio.to_thread(_add)
if err:
return err
# 创建用户角色数据 + 清理权限缓存
await add_user_role(user_id, data.roles)
# 返回结果
return R.ok(msg="添加成功")
# 更新用户
async def update_user(request, data: UserForm):
def _update():
db = get_db_session()
# 记录ID判空
user_id = data.id
if not user_id or int(user_id) <= 0:
return R.failed("记录ID不能为空"), None
# 唯一性校验(排除自身)
if db.query(User).filter(and_(User.username == data.username, User.id != data.id, User.is_delete == 0)).first():
return R.failed("登录账号不能重复"), None
# 根据ID查询记录
user = db.query(User).filter(and_(User.id == user_id, User.is_delete == 0)).first()
# 查询结果判空
if not user:
return R.failed("记录不存在"), None
# 获取表单数据(排除敏感字段和非数据列)
formData = data.model_dump(exclude={'id', 'password', 'city', 'roles'})
# 图片处理
avatar = data.avatar
if avatar:
formData['avatar'] = save_file(avatar, "user")
# 密码加密:仅传入新密码时才更新,否则保留原值
pwd = data.password
if pwd:
salt = password.generate_salt()
formData['password'] = password.encrypt_password(pwd, salt)
formData['salt'] = salt
# 行政区划处理
citys = data.city
if citys and len(citys) == 3:
formData['province_code'] = citys[0]
formData['city_code'] = citys[1]
formData['district_code'] = citys[2]
updData = formData
updData['update_user'] = get_user_id(request)
updData['update_time'] = datetime.now(tz=timezone.utc)
# 更新数据
result = db.query(User).filter_by(id=user_id).update(updData)
if not result:
return R.failed("更新失败"), None
return None, user_id
# 同步DB/文件操作放入线程池,避免阻塞事件循环
err, user_id = await asyncio.to_thread(_update)
if err:
return err
# 创建用户角色数据 + 清理权限缓存
await add_user_role(user_id, data.roles)
# 返回结果
return R.ok(msg="更新成功")
# 创建用户角色信息
async def add_user_role(user_id, roleIdList):
def _save():
db = get_db_session()
# 删除用户角色关系数据
db.query(UserRole).filter(UserRole.user_id == user_id).delete()
# 创建新的用户角色关系
if roleIdList:
new_roles = []
for roleId in roleIdList:
if not roleId:
continue
new_roles.append(UserRole(user_id=user_id, role_id=roleId))
db.add_all(new_roles)
# 同步DB操作放入线程池,避免阻塞事件循环
await asyncio.to_thread(_save)
# 清理该用户权限缓存
await invalidate_user_perms(user_id)
# 删除用户
def delete_user(user_id):
return User.batch_delete_r(user_id)
# 设置状态
def update_user_status(request, data: UserStatusForm):
db = get_db_session()
# 记录ID
id = data.id
# 状态值
status = data.status
# 根据ID查询记录
user = db.query(User).filter(and_(User.id == id, User.is_delete == 0)).first()
# 查询结果判空
if not user:
return R.failed("记录不存在")
# 更新记录
result = db.query(User).filter_by(id=id).update({
"status": status,
"update_user": get_user_id(request),
"update_time": datetime.now(tz=timezone.utc)
})
# 提交数据
if not result:
return R.failed("设置失败")
# 返回结果
return R.ok(msg="设置成功")
# 更新用户信息
def update_user_info(request, data: UserInfoForm):
db = get_db_session()
# 用户ID
user_id = get_user_id(request)
# 根据ID查询记录
user = db.query(User).filter(and_(User.id == user_id, User.is_delete == 0)).first()
# 查询结果判空
if not user:
return R.failed("记录不存在")
# 更新记录
updData = data.model_dump()
result = db.query(User).filter_by(id=user_id).update(updData)
# 提交数据
if not result:
return R.failed("更新失败")
# 返回结果
return R.ok(msg="更新成功")
# 更新密码
def update_pwd(request, data: UpdatePwdForm):
db = get_db_session()
# 用户ID
user_id = get_user_id(request)
# 根据ID查询用户
user = db.query(User).filter(and_(User.is_delete == 0, User.id == user_id)).first()
if not user:
return R.failed("用户不存在")
# 验证原始密码
if not password.verify_password(data.oldPassword, user.salt or '', user.password or ''):
return R.failed("原始密码不正确")
# 加密新密码
salt = password.generate_salt()
new_hash = password.encrypt_password(data.newPassword, salt)
result = db.query(User).filter_by(id=user_id).update({
"password": new_hash,
"salt": salt
})
# 提交数据
if not result:
return R.failed("更新失败")
# 返回结果
return R.ok(msg="更新成功")
# 重置密码
def reset_user_pwd(request, data: ResetPwdForm):
db = get_db_session()
# 用户ID
user_id = data.id
# 根据ID查询用户
user = db.query(User).filter(and_(User.is_delete == 0, User.id == user_id)).first()
if not user:
return R.failed("用户不存在")
# 加密新密码
salt = password.generate_salt()
new_hash = password.encrypt_password(DEFAULT_PASSWORD, salt)
result = db.query(User).filter_by(id=user_id).update({
"password": new_hash,
"salt": salt
})
# 提交数据
if not result:
return R.failed("重置密码失败")
# 返回结果
return R.ok(msg="重置密码成功")
from modules.system.user.models import UserRole
from modules.system.role.models import Role
from core.database import get_db_session
# 根据用户ID查询角色列表
def get_user_role_list(userId):
db = get_db_session()
rows = (
db.query(Role.id, Role.name)
.join(UserRole, UserRole.role_id == Role.id)
.filter(UserRole.user_id == userId, Role.is_delete == 0)
.all()
)
role_list = []
if rows:
for v in rows:
item = {
'id': v.id,
'name': v.name,
}
role_list.append(item)
return role_list
# 根据用户ID列表批量查询角色,返回 {user_id: [{id, name}, ...]} 字典
def get_user_role_map(user_ids):
if not user_ids:
return {}
db = get_db_session()
# 用ORM查询替代原生SQL,避免tuple单元素/空列表的IN语法问题
from modules.system.user.models import UserRole
from modules.system.role.models import Role
rows = (
db.query(UserRole.user_id, Role.id.label('role_id'), Role.name.label('role_name'))
.join(Role, Role.id == UserRole.role_id)
.filter(UserRole.user_id.in_(user_ids), Role.is_delete == 0)
.all()
)
role_map = {}
for row in rows:
uid = row.user_id
if uid not in role_map:
role_map[uid] = []
role_map[uid].append({'id': row.role_id, 'name': row.role_name})
return role_map