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