dingdanquanliucheng/backend/app/repositories/system_repository.py
taiyi feaf4298ca feat: 小程序微信手机号一键登录,替代账号密码输入
- 后端新增 /api/auth/wechat-login 接口,用手机号自动匹配系统用户
- 登录时自动绑定 open_id,为微信订阅消息通知做好准备
- 小程序登录页替换为微信手机号授权按钮,保留密码登录为备用入口
- 新增 get_user_by_mobile 按手机号查询用户方法

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 17:41:11 +08:00

381 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""系统管理模块 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())