Files
zkxw/backend/app/services/web_auth_roles.py
T

405 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.
"""Web 管理端登录:基于 V0.3 sys_user / sys_user_role / sys_role 的角色与菜单权限解析
有效角色 = 用户直接角色 ∪ 经启用职位继承的角色(sys_post_role)。
"""
from __future__ import annotations
from typing import List, Optional, Set, Collection
from sqlalchemy import and_
from sqlalchemy.orm import Session
from app.core.config import settings
from app.models.models import (
SysMenu,
SysPost,
SysPostRole,
SysRole,
SysRoleMenu,
SysUserPost,
SysUserRole,
)
# 无有效角色绑定时的 Web 端角色(不可为 super_admin,避免越权)
NO_ROLE_WEB_ROLE = "guest"
# 一级菜单权限码(用于顶部菜单权限控制)
TOP_MENU_PERMS = frozenset({"menu:market", "menu:production", "menu:quality", "menu:operations", "menu:system-manage"})
def _map_role_name_to_web(role_name: str) -> str:
"""将 sys_role.role_name 映射为 web-admin 使用的 role 字符串。"""
n = (role_name or "").strip()
if not n:
return "admin"
if "超级" in n or n in ("系统管理员", "超级管理员"):
return "super_admin"
if "管理" in n or "运营" in n:
return "admin"
return "admin"
def _dedupe_role_id_rows(rows) -> List[str]:
out: List[str] = []
seen: Set[str] = set()
for row in rows:
rid = str(row[0]).strip() if row and row[0] else ""
if rid and rid not in seen:
seen.add(rid)
out.append(rid)
return out
def get_direct_enabled_role_ids(
db: Session, user_id: str, tenant_no: Optional[str] = None
) -> List[str]:
"""用户直接绑定且启用的角色 ID(sys_user_role → sys_role)。"""
if not user_id:
return []
tenant = (tenant_no or settings.DEFAULT_TENANT_NO).strip()
rows = (
db.query(SysRole.id)
.join(SysUserRole, SysUserRole.role_id == SysRole.id)
.filter(
SysUserRole.user_id == user_id,
SysUserRole.del_flag == 0,
SysUserRole.tenant_no == tenant,
SysRole.del_flag == 0,
SysRole.status == "1",
SysRole.tenant_no == tenant,
)
.order_by(SysRole.create_time.desc())
.all()
)
return _dedupe_role_id_rows(rows)
def get_post_inherited_role_ids(
db: Session, user_id: str, tenant_no: Optional[str] = None
) -> List[str]:
"""经启用职位继承的启用角色 ID(sys_user_post → sys_post → sys_post_role → sys_role)。"""
if not user_id:
return []
tenant = (tenant_no or settings.DEFAULT_TENANT_NO).strip()
rows = (
db.query(SysRole.id)
.join(SysPostRole, SysPostRole.role_id == SysRole.id)
.join(
SysPost,
and_(
SysPost.id == SysPostRole.post_id,
SysPost.del_flag == 0,
SysPost.status == "1",
SysPost.tenant_no == tenant,
),
)
.join(
SysUserPost,
and_(
SysUserPost.post_id == SysPost.id,
SysUserPost.user_id == user_id,
SysUserPost.del_flag == 0,
SysUserPost.tenant_no == tenant,
),
)
.filter(
SysPostRole.del_flag == 0,
SysPostRole.tenant_no == tenant,
SysRole.del_flag == 0,
SysRole.status == "1",
SysRole.tenant_no == tenant,
)
.order_by(SysRole.create_time.desc())
.all()
)
return _dedupe_role_id_rows(rows)
def get_effective_role_ids(
db: Session, user_id: str, tenant_no: Optional[str] = None
) -> List[str]:
"""用户有效角色 ID:直接绑定 ∪ 启用职位关联的启用角色(去重保序)。"""
if not user_id:
return []
out: List[str] = []
seen: Set[str] = set()
for rid in get_direct_enabled_role_ids(
db, user_id, tenant_no=tenant_no
) + get_post_inherited_role_ids(
db, user_id, tenant_no=tenant_no
):
if rid not in seen:
seen.add(rid)
out.append(rid)
return out
def _normalize_candidate_user_ids(
candidate_user_ids: Optional[Collection[str]],
) -> Optional[List[str]]:
if candidate_user_ids is None:
return None
return list(dict.fromkeys([str(u).strip() for u in candidate_user_ids if u and str(u).strip()]))
def list_user_ids_with_effective_role_name(
db: Session,
*,
tenant_no: str,
role_name: str,
candidate_user_ids: Optional[Collection[str]] = None,
) -> Set[str]:
"""拥有指定角色名的用户 ID(直接绑定 ∪ 职位继承)。"""
tenant = (tenant_no or "").strip()
rn = (role_name or "").strip()
if not tenant or not rn:
return set()
candidates = _normalize_candidate_user_ids(candidate_user_ids)
if candidates is not None and not candidates:
return set()
direct_q = (
db.query(SysUserRole.user_id)
.join(SysRole, SysRole.id == SysUserRole.role_id)
.filter(
SysUserRole.del_flag == 0,
SysUserRole.tenant_no == tenant,
SysRole.del_flag == 0,
SysRole.status == "1",
SysRole.tenant_no == tenant,
SysRole.role_name == rn,
)
)
post_q = (
db.query(SysUserPost.user_id)
.join(
SysPost,
and_(
SysPost.id == SysUserPost.post_id,
SysPost.del_flag == 0,
SysPost.status == "1",
SysPost.tenant_no == tenant,
),
)
.join(
SysPostRole,
and_(
SysPostRole.post_id == SysPost.id,
SysPostRole.del_flag == 0,
SysPostRole.tenant_no == tenant,
),
)
.join(
SysRole,
and_(
SysRole.id == SysPostRole.role_id,
SysRole.del_flag == 0,
SysRole.status == "1",
SysRole.tenant_no == tenant,
SysRole.role_name == rn,
),
)
.filter(SysUserPost.del_flag == 0, SysUserPost.tenant_no == tenant)
)
if candidates is not None:
direct_q = direct_q.filter(SysUserRole.user_id.in_(candidates))
post_q = post_q.filter(SysUserPost.user_id.in_(candidates))
out: Set[str] = set()
for row in direct_q.all():
uid = str(row[0]).strip() if row and row[0] else ""
if uid:
out.add(uid)
for row in post_q.all():
uid = str(row[0]).strip() if row and row[0] else ""
if uid:
out.add(uid)
return out
def list_user_ids_with_any_effective_role_ids(
db: Session,
*,
tenant_no: str,
role_ids: List[str],
candidate_user_ids: Optional[Collection[str]] = None,
) -> List[str]:
"""拥有任一指定角色 ID 的用户 ID 列表(直接绑定 ∪ 职位继承,去重)。"""
tenant = (tenant_no or "").strip()
rids = list(dict.fromkeys([str(r).strip() for r in (role_ids or []) if r and str(r).strip()]))
if not tenant or not rids:
return []
candidates = _normalize_candidate_user_ids(candidate_user_ids)
if candidates is not None and not candidates:
return []
direct_q = (
db.query(SysUserRole.user_id)
.join(SysRole, SysRole.id == SysUserRole.role_id)
.filter(
SysUserRole.del_flag == 0,
SysUserRole.tenant_no == tenant,
SysUserRole.role_id.in_(rids),
SysRole.del_flag == 0,
SysRole.status == "1",
SysRole.tenant_no == tenant,
)
)
post_q = (
db.query(SysUserPost.user_id)
.join(
SysPost,
and_(
SysPost.id == SysUserPost.post_id,
SysPost.del_flag == 0,
SysPost.status == "1",
SysPost.tenant_no == tenant,
),
)
.join(
SysPostRole,
and_(
SysPostRole.post_id == SysPost.id,
SysPostRole.del_flag == 0,
SysPostRole.tenant_no == tenant,
SysPostRole.role_id.in_(rids),
),
)
.join(
SysRole,
and_(
SysRole.id == SysPostRole.role_id,
SysRole.del_flag == 0,
SysRole.status == "1",
SysRole.tenant_no == tenant,
),
)
.filter(SysUserPost.del_flag == 0, SysUserPost.tenant_no == tenant)
)
if candidates is not None:
direct_q = direct_q.filter(SysUserRole.user_id.in_(candidates))
post_q = post_q.filter(SysUserPost.user_id.in_(candidates))
out: List[str] = []
seen: Set[str] = set()
for row in list(direct_q.all()) + list(post_q.all()):
uid = str(row[0]).strip() if row and row[0] else ""
if uid and uid not in seen:
seen.add(uid)
out.append(uid)
return out
def get_role_names_by_ids(
db: Session, role_ids: List[str], tenant_no: Optional[str] = None
) -> List[str]:
"""按给定角色 ID 返回启用角色名称(保序、去重)。"""
ordered = list(dict.fromkeys([str(r).strip() for r in (role_ids or []) if r]))
if not ordered:
return []
tenant = (tenant_no or settings.DEFAULT_TENANT_NO).strip()
rows = (
db.query(SysRole.id, SysRole.role_name)
.filter(
SysRole.id.in_(ordered),
SysRole.del_flag == 0,
SysRole.status == "1",
SysRole.tenant_no == tenant,
)
.all()
)
name_by_id = {
str(rid).strip(): str(name).strip()
for rid, name in rows
if rid and name
}
out: List[str] = []
seen: Set[str] = set()
for rid in ordered:
name = name_by_id.get(rid)
if name and name not in seen:
seen.add(name)
out.append(name)
return out
def _active_role_name_rows(
db: Session, user_id: str, tenant_no: Optional[str] = None
) -> list:
"""查询用户当前有效的 sys_role.role_name 列表(含职位继承)。"""
return get_role_names_by_ids(
db,
get_effective_role_ids(db, user_id, tenant_no=tenant_no),
tenant_no=tenant_no,
)
def user_has_active_roles(db: Session, user_id: str) -> bool:
"""用户是否至少拥有一个有效角色(直接或经职位继承)。"""
return bool(get_effective_role_ids(db, user_id))
def get_user_role_names(db: Session, user_id: str) -> list:
"""用户当前有效的 sys_role.role_name 列表(去重、保序)。"""
names = _active_role_name_rows(db, user_id)
return list(dict.fromkeys(names))
def resolve_web_role_for_user(
db: Session, user_id: str, tenant_no: Optional[str] = None
) -> str:
"""根据用户有效角色解析 Web 端 role(admin / super_admin / guest)。"""
names = _active_role_name_rows(db, user_id, tenant_no=tenant_no)
if not names:
return NO_ROLE_WEB_ROLE
mapped = [_map_role_name_to_web(n) for n in names]
if "super_admin" in mapped:
return "super_admin"
return "admin"
def iter_user_menu_perm_codes(
db: Session, user_id: str, tenant_no: Optional[str] = None
) -> Set[str]:
"""用户通过有效角色关联得到的菜单权限标识(sys_menu.perms,空则退回菜单 id)。
注:不过滤 visible——F 类型按钮等 visible=0 的菜单仍可通过角色授权生效;
visible 仅控制侧栏/菜单树展示,不影响权限码解析。
一级菜单(menu:market 等)同样经本函数返回,用于前端顶部菜单权限控制。
"""
if not user_id:
return set()
tenant = (tenant_no or settings.DEFAULT_TENANT_NO).strip()
role_ids = get_effective_role_ids(db, user_id, tenant_no=tenant)
if not role_ids:
return set()
try:
q = (
db.query(SysMenu.perms, SysMenu.id)
.join(
SysRoleMenu,
and_(
SysRoleMenu.menu_id == SysMenu.id,
SysRoleMenu.del_flag == 0,
SysRoleMenu.tenant_no == tenant,
SysRoleMenu.role_id.in_(role_ids),
),
)
.filter(
SysMenu.del_flag == 0,
SysMenu.tenant_no == tenant,
SysMenu.status == "1",
)
.distinct()
)
out: Set[str] = set()
for perms, mid in q.all():
p = (perms or "").strip()
if p:
out.add(p)
elif mid:
out.add(str(mid).strip())
return out
except Exception:
return set()