- 后端新增 /api/auth/wechat-login 接口,用手机号自动匹配系统用户 - 登录时自动绑定 open_id,为微信订阅消息通知做好准备 - 小程序登录页替换为微信手机号授权按钮,保留密码登录为备用入口 - 新增 get_user_by_mobile 按手机号查询用户方法 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
381 lines
13 KiB
Python
381 lines
13 KiB
Python
"""系统管理模块 Repository 层
|
||
|
||
负责用户、角色、菜单等系统基础数据的 CRUD 操作,
|
||
包括 RBAC 权限模型中的角色-菜单关联管理。
|
||
被 SystemService(系统管理服务)、AuthService(认证服务)调用。
|
||
"""
|
||
|
||
from sqlalchemy import select
|
||
from sqlalchemy.orm import Session
|
||
|
||
from backend.app.models.system import Menu, Role, RoleMenu, User
|
||
|
||
|
||
class SystemRepository:
|
||
"""系统管理数据仓储
|
||
|
||
封装用户、角色、菜单的增删改查以及角色-菜单关联管理。
|
||
被 SystemService(系统管理服务)、AuthService(认证服务)调用。
|
||
"""
|
||
|
||
def get_user_by_username(self, session: Session, username: str) -> User | None:
|
||
"""根据用户名查询用户
|
||
|
||
参数:
|
||
session: 数据库会话
|
||
username: 用户名
|
||
|
||
返回:
|
||
匹配的用户对象,不存在则返回 None
|
||
"""
|
||
stmt = select(User).where(User.username == username)
|
||
return session.execute(stmt).scalar_one_or_none()
|
||
|
||
def get_user_by_mobile(self, session: Session, mobile: str) -> User | None:
|
||
"""根据手机号查询启用状态的用户
|
||
|
||
参数:
|
||
session: 数据库会话
|
||
mobile: 手机号
|
||
|
||
返回:
|
||
匹配的启用状态用户对象,不存在则返回 None
|
||
"""
|
||
stmt = select(User).where(User.mobile == mobile, User.status == 1)
|
||
return session.execute(stmt).scalar_one_or_none()
|
||
|
||
def get_user(self, session: Session, user_id: int) -> User | None:
|
||
"""根据 ID 查询用户
|
||
|
||
参数:
|
||
session: 数据库会话
|
||
user_id: 用户 ID
|
||
|
||
返回:
|
||
匹配的用户对象,不存在则返回 None
|
||
"""
|
||
stmt = select(User).where(User.id == user_id)
|
||
return session.execute(stmt).scalar_one_or_none()
|
||
|
||
def create_user(self, session: Session, payload: dict) -> User:
|
||
"""创建新用户
|
||
|
||
参数:
|
||
session: 数据库会话
|
||
payload: 用户数据字典,包含 username / password_hash / real_name /
|
||
mobile / role_id / status 等字段
|
||
|
||
返回:
|
||
新创建的用户对象
|
||
"""
|
||
user = User(
|
||
username=payload["username"],
|
||
password_hash=payload["password_hash"],
|
||
real_name=payload["real_name"],
|
||
mobile=payload.get("mobile") or "",
|
||
role_id=payload["role_id"],
|
||
status=payload.get("status", 1),
|
||
)
|
||
session.add(user)
|
||
session.flush()
|
||
return user
|
||
|
||
def update_user(self, user: User, payload: dict) -> User:
|
||
"""更新用户基本信息
|
||
|
||
参数:
|
||
user: 待更新的用户对象(已从数据库加载)
|
||
payload: 更新数据字典,包含 real_name / mobile / role_id / status
|
||
|
||
返回:
|
||
更新后的用户对象
|
||
"""
|
||
user.real_name = payload["real_name"]
|
||
user.mobile = payload.get("mobile") or ""
|
||
user.role_id = payload["role_id"]
|
||
user.status = payload.get("status", 1)
|
||
return user
|
||
|
||
def reset_password(self, user: User, password_hash: str) -> User:
|
||
"""重置用户密码
|
||
|
||
参数:
|
||
user: 待重置密码的用户对象
|
||
password_hash: 新密码的哈希值
|
||
|
||
返回:
|
||
密码已更新的用户对象
|
||
"""
|
||
user.password_hash = password_hash
|
||
return user
|
||
|
||
def get_role(self, session: Session, role_id: int) -> Role | None:
|
||
"""根据 ID 查询角色
|
||
|
||
参数:
|
||
session: 数据库会话
|
||
role_id: 角色 ID
|
||
|
||
返回:
|
||
匹配的角色对象,不存在则返回 None
|
||
"""
|
||
stmt = select(Role).where(Role.id == role_id)
|
||
return session.execute(stmt).scalar_one_or_none()
|
||
|
||
def get_role_by_code(self, session: Session, role_code: str) -> Role | None:
|
||
"""根据角色编码查询角色
|
||
|
||
参数:
|
||
session: 数据库会话
|
||
role_code: 角色编码(如 admin / sales 等)
|
||
|
||
返回:
|
||
匹配的角色对象,不存在则返回 None
|
||
"""
|
||
stmt = select(Role).where(Role.role_code == role_code)
|
||
return session.execute(stmt).scalar_one_or_none()
|
||
|
||
def create_role(self, session: Session, payload: dict) -> Role:
|
||
"""创建新角色
|
||
|
||
参数:
|
||
session: 数据库会话
|
||
payload: 角色数据字典,包含 role_name / role_code / status / remark
|
||
|
||
返回:
|
||
新创建的角色对象
|
||
"""
|
||
role = Role(
|
||
role_name=payload["role_name"],
|
||
role_code=payload["role_code"],
|
||
status=payload.get("status", 1),
|
||
remark=payload.get("remark"),
|
||
)
|
||
session.add(role)
|
||
session.flush()
|
||
return role
|
||
|
||
def update_role(self, role: Role, payload: dict) -> Role:
|
||
"""更新角色信息
|
||
|
||
参数:
|
||
role: 待更新的角色对象
|
||
payload: 更新数据字典,包含 role_name / status / remark
|
||
|
||
返回:
|
||
更新后的角色对象
|
||
"""
|
||
role.role_name = payload["role_name"]
|
||
role.status = payload.get("status", 1)
|
||
role.remark = payload.get("remark")
|
||
return role
|
||
|
||
def list_role_menu_relations(self, session: Session, role_id: int) -> list[RoleMenu]:
|
||
"""查询指定角色的菜单关联列表
|
||
|
||
参数:
|
||
session: 数据库会话
|
||
role_id: 角色 ID
|
||
|
||
返回:
|
||
该角色关联的菜单关系列表,按 id 升序排列
|
||
"""
|
||
stmt = select(RoleMenu).where(RoleMenu.role_id == role_id).order_by(RoleMenu.id.asc())
|
||
return list(session.execute(stmt).scalars())
|
||
|
||
def replace_role_menu_relations(self, session: Session, role_id: int, menu_ids: list[int]) -> None:
|
||
"""替换角色的菜单关联关系
|
||
|
||
先删除该角色原有的所有菜单关联,再批量插入新的关联。
|
||
用于角色权限配置的全量更新。
|
||
|
||
参数:
|
||
session: 数据库会话
|
||
role_id: 角色 ID
|
||
menu_ids: 新的菜单 ID 列表
|
||
"""
|
||
existed = self.list_role_menu_relations(session, role_id)
|
||
for relation in existed:
|
||
session.delete(relation)
|
||
for menu_id in menu_ids:
|
||
session.add(RoleMenu(role_id=role_id, menu_id=menu_id))
|
||
session.flush()
|
||
|
||
def get_menu(self, session: Session, menu_id: int) -> Menu | None:
|
||
"""根据 ID 查询菜单
|
||
|
||
参数:
|
||
session: 数据库会话
|
||
menu_id: 菜单 ID
|
||
|
||
返回:
|
||
匹配的菜单对象,不存在则返回 None
|
||
"""
|
||
stmt = select(Menu).where(Menu.id == menu_id)
|
||
return session.execute(stmt).scalar_one_or_none()
|
||
|
||
def get_menu_by_permission_code(self, session: Session, permission_code: str) -> Menu | None:
|
||
"""根据权限编码查询菜单
|
||
|
||
参数:
|
||
session: 数据库会话
|
||
permission_code: 权限编码(如 user:list / order:create)
|
||
|
||
返回:
|
||
匹配的菜单对象,不存在则返回 None
|
||
"""
|
||
stmt = select(Menu).where(Menu.permission_code == permission_code)
|
||
return session.execute(stmt).scalar_one_or_none()
|
||
|
||
def create_menu(self, session: Session, payload: dict) -> Menu:
|
||
"""创建新菜单
|
||
|
||
参数:
|
||
session: 数据库会话
|
||
payload: 菜单数据字典,包含 parent_id / menu_name / menu_path /
|
||
menu_type / permission_code / icon / sort_no / status
|
||
|
||
返回:
|
||
新创建的菜单对象
|
||
"""
|
||
menu = Menu(
|
||
parent_id=payload.get("parent_id", 0),
|
||
menu_name=payload["menu_name"],
|
||
menu_path=payload.get("menu_path") or "",
|
||
menu_type=payload.get("menu_type", "page"),
|
||
permission_code=payload["permission_code"],
|
||
icon=payload.get("icon"),
|
||
sort_no=payload.get("sort_no", 0),
|
||
status=payload.get("status", 1),
|
||
)
|
||
session.add(menu)
|
||
session.flush()
|
||
return menu
|
||
|
||
def update_menu(self, menu: Menu, payload: dict) -> Menu:
|
||
"""更新菜单信息
|
||
|
||
参数:
|
||
menu: 待更新的菜单对象
|
||
payload: 更新数据字典,包含 parent_id / menu_name / menu_path /
|
||
menu_type / permission_code / icon / sort_no / status
|
||
|
||
返回:
|
||
更新后的菜单对象
|
||
"""
|
||
menu.parent_id = payload.get("parent_id", 0)
|
||
menu.menu_name = payload["menu_name"]
|
||
menu.menu_path = payload.get("menu_path") or ""
|
||
menu.menu_type = payload.get("menu_type", "page")
|
||
menu.permission_code = payload["permission_code"]
|
||
menu.icon = payload.get("icon")
|
||
menu.sort_no = payload.get("sort_no", 0)
|
||
menu.status = payload.get("status", 1)
|
||
return menu
|
||
|
||
def list_users(self, session: Session, filters: dict) -> list[tuple[User, Role | None]]:
|
||
"""查询用户列表(关联角色信息)
|
||
|
||
联表查询用户及其关联角色,支持按用户名、姓名、手机号、角色和状态过滤。
|
||
|
||
参数:
|
||
session: 数据库会话
|
||
filters: 过滤条件字典,支持 username / real_name / mobile / role_id / status
|
||
|
||
返回:
|
||
(用户对象, 角色对象) 的元组列表,按 id 升序排列
|
||
"""
|
||
stmt = select(User, Role).outerjoin(Role, User.role_id == Role.id)
|
||
|
||
if filters.get("username"):
|
||
stmt = stmt.where(User.username.contains(filters["username"]))
|
||
if filters.get("real_name"):
|
||
stmt = stmt.where(User.real_name.contains(filters["real_name"]))
|
||
if filters.get("mobile"):
|
||
stmt = stmt.where(User.mobile.contains(filters["mobile"]))
|
||
if filters.get("role_id") is not None:
|
||
stmt = stmt.where(User.role_id == filters["role_id"])
|
||
if filters.get("status") is not None:
|
||
stmt = stmt.where(User.status == filters["status"])
|
||
|
||
stmt = stmt.order_by(User.id.asc())
|
||
return list(session.execute(stmt).all())
|
||
|
||
def list_roles(self, session: Session, filters: dict) -> list[Role]:
|
||
"""查询角色列表
|
||
|
||
支持按角色名称、角色编码和状态过滤。
|
||
|
||
参数:
|
||
session: 数据库会话
|
||
filters: 过滤条件字典,支持 role_name / role_code / status
|
||
|
||
返回:
|
||
角色列表,按 id 升序排列
|
||
"""
|
||
stmt = select(Role)
|
||
|
||
if filters.get("role_name"):
|
||
stmt = stmt.where(Role.role_name.contains(filters["role_name"]))
|
||
if filters.get("role_code"):
|
||
stmt = stmt.where(Role.role_code.contains(filters["role_code"]))
|
||
if filters.get("status") is not None:
|
||
stmt = stmt.where(Role.status == filters["status"])
|
||
|
||
stmt = stmt.order_by(Role.id.asc())
|
||
return list(session.execute(stmt).scalars())
|
||
|
||
def count_active_users_by_role(self, session: Session, role_id: int) -> int:
|
||
"""统计指定角色下启用状态的用户数量
|
||
|
||
用于删除角色前检查是否仍有活跃用户关联。
|
||
|
||
参数:
|
||
session: 数据库会话
|
||
role_id: 角色 ID
|
||
|
||
返回:
|
||
启用状态的用户数量
|
||
"""
|
||
stmt = select(User).where(User.role_id == role_id, User.status == 1)
|
||
return len(list(session.execute(stmt).scalars()))
|
||
|
||
def list_menus_by_ids(self, session: Session, menu_ids: list[int]) -> list[Menu]:
|
||
"""根据菜单 ID 列表批量查询菜单
|
||
|
||
按 sort_no 和 id 升序排列,空列表时直接返回空结果。
|
||
|
||
参数:
|
||
session: 数据库会话
|
||
menu_ids: 菜单 ID 列表
|
||
|
||
返回:
|
||
匹配的菜单列表
|
||
"""
|
||
if not menu_ids:
|
||
return []
|
||
stmt = select(Menu).where(Menu.id.in_(menu_ids)).order_by(Menu.sort_no.asc(), Menu.id.asc())
|
||
return list(session.execute(stmt).scalars())
|
||
|
||
def list_menus(self, session: Session, filters: dict) -> list[Menu]:
|
||
"""查询菜单列表
|
||
|
||
支持按状态和菜单类型过滤,按 sort_no 和 id 升序排列。
|
||
|
||
参数:
|
||
session: 数据库会话
|
||
filters: 过滤条件字典,支持 status / menu_type
|
||
|
||
返回:
|
||
菜单列表
|
||
"""
|
||
stmt = select(Menu)
|
||
|
||
if filters.get("status") is not None:
|
||
stmt = stmt.where(Menu.status == filters["status"])
|
||
if filters.get("menu_type"):
|
||
stmt = stmt.where(Menu.menu_type == filters["menu_type"])
|
||
|
||
stmt = stmt.order_by(Menu.sort_no.asc(), Menu.id.asc())
|
||
return list(session.execute(stmt).scalars())
|