| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457 |
- """角色 CRUD(sys_role / sys_role_menu / sys_role_data_scope_detail)"""
- from __future__ import annotations
- import math
- from datetime import datetime
- from typing import Any, Dict, List, Optional, Tuple
- from sqlalchemy import and_, func
- from sqlalchemy.orm import Session
- from app.core.utils import generate_uuid
- from app.models.models import (
- SysPost,
- SysPostRole,
- SysRole,
- SysRoleDataScopeDetail,
- SysRoleMenu,
- SysUser,
- SysUserPost,
- SysUserRole,
- )
- # ---------------------------------------------------------------------------
- # helpers
- # ---------------------------------------------------------------------------
- def _user_display_name(nick_name: Optional[str], user_no: Optional[str]) -> str:
- n = (nick_name or "").strip()
- if n:
- return n
- return (user_no or "").strip()
- def _append_unique(out: Dict[str, List[str]], key: str, value: str) -> None:
- if not key or not value:
- return
- bucket = out.setdefault(key, [])
- if value not in bucket:
- bucket.append(value)
- def _bound_user_names_by_role_ids(
- db: Session, role_ids: List[str], tenant_no: str
- ) -> Dict[str, List[str]]:
- """批量查询有效拥有角色的用户显示名(直接绑定 ∪ 启用职位继承)。"""
- out: Dict[str, List[str]] = {}
- ids = [rid for rid in (role_ids or []) if rid]
- tenant = (tenant_no or "").strip()
- if not ids or not tenant:
- return out
- direct_rows = (
- db.query(SysUserRole.role_id, SysUser.nick_name, SysUser.user_no)
- .join(
- SysUser,
- and_(SysUser.id == SysUserRole.user_id, SysUser.del_flag == 0),
- )
- .join(
- SysRole,
- and_(
- SysRole.id == SysUserRole.role_id,
- SysRole.del_flag == 0,
- SysRole.status == "1",
- SysRole.tenant_no == tenant,
- ),
- )
- .filter(
- and_(
- SysUserRole.role_id.in_(ids),
- SysUserRole.tenant_no == tenant,
- SysUserRole.del_flag == 0,
- )
- )
- .order_by(SysUser.nick_name.asc(), SysUser.user_no.asc())
- .all()
- )
- for role_id, nick_name, user_no in direct_rows:
- _append_unique(out, role_id, _user_display_name(nick_name, user_no))
- post_rows = (
- db.query(SysPostRole.role_id, SysUser.nick_name, SysUser.user_no)
- .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.del_flag == 0,
- SysUserPost.tenant_no == tenant,
- ),
- )
- .join(
- SysUser,
- and_(SysUser.id == SysUserPost.user_id, SysUser.del_flag == 0),
- )
- .join(
- SysRole,
- and_(
- SysRole.id == SysPostRole.role_id,
- SysRole.del_flag == 0,
- SysRole.status == "1",
- SysRole.tenant_no == tenant,
- ),
- )
- .filter(
- and_(
- SysPostRole.role_id.in_(ids),
- SysPostRole.tenant_no == tenant,
- SysPostRole.del_flag == 0,
- )
- )
- .order_by(SysUser.nick_name.asc(), SysUser.user_no.asc())
- .all()
- )
- for role_id, nick_name, user_no in post_rows:
- _append_unique(out, role_id, _user_display_name(nick_name, user_no))
- return out
- def _bound_post_names_by_role_ids(
- db: Session, role_ids: List[str], tenant_no: str
- ) -> Dict[str, List[str]]:
- """批量查询角色关联的启用职位名称。"""
- out: Dict[str, List[str]] = {}
- ids = [rid for rid in (role_ids or []) if rid]
- tenant = (tenant_no or "").strip()
- if not ids or not tenant:
- return out
- rows = (
- db.query(SysPostRole.role_id, SysPost.post_name)
- .join(
- SysPost,
- and_(
- SysPost.id == SysPostRole.post_id,
- SysPost.del_flag == 0,
- SysPost.status == "1",
- SysPost.tenant_no == tenant,
- ),
- )
- .filter(
- and_(
- SysPostRole.role_id.in_(ids),
- SysPostRole.tenant_no == tenant,
- SysPostRole.del_flag == 0,
- )
- )
- .order_by(SysPost.post_name.asc())
- .all()
- )
- for role_id, post_name in rows:
- name = (post_name or "").strip()
- _append_unique(out, role_id, name)
- return out
- def _row_to_dict(
- r: SysRole,
- *,
- bound_user_names: Optional[List[str]] = None,
- bound_post_names: Optional[List[str]] = None,
- ) -> Dict[str, Any]:
- return {
- "id": r.id,
- "roleName": r.role_name,
- "dataScope": r.data_scope or "1",
- "status": r.status or "1",
- "tenantNo": r.tenant_no or "",
- "orderNum": r.order_num if r.order_num is not None else 0,
- "remark": r.remark or "",
- "boundUserNames": list(bound_user_names or []),
- "boundPostNames": list(bound_post_names or []),
- "createTime": r.create_time.strftime("%Y-%m-%d %H:%M:%S") if r.create_time else "",
- "updateTime": r.update_time.strftime("%Y-%m-%d %H:%M:%S") if r.update_time else "",
- }
- def total_pages(total: int, page_size: int) -> int:
- if total <= 0:
- return 0
- return int(math.ceil(total / float(page_size)))
- # ---------------------------------------------------------------------------
- # list / get
- # ---------------------------------------------------------------------------
- def list_roles(
- db: Session,
- *,
- page: int = 1,
- page_size: int = 10,
- keyword: Optional[str] = None,
- tenant_no: Optional[str] = None,
- user_info=None,
- ) -> Tuple[List[Dict[str, Any]], int]:
- def _filtered():
- q0 = db.query(SysRole).filter(SysRole.del_flag == 0)
- if tenant_no:
- q0 = q0.filter(SysRole.tenant_no == tenant_no)
- kw = (keyword or "").strip()
- if kw:
- like = f"%{kw}%"
- q0 = q0.filter(SysRole.role_name.like(like))
- if user_info:
- from app.core.data_scope import apply_data_scope
- q0 = apply_data_scope(q0, SysRole, user_info, db, tenant_no)
- return q0
- total = _filtered().order_by(None).with_entities(func.count(SysRole.id)).scalar() or 0
- page = max(1, page)
- page_size = min(max(1, page_size), 100)
- rows = (
- _filtered()
- .order_by(SysRole.create_time.desc())
- .offset((page - 1) * page_size)
- .limit(page_size)
- .all()
- )
- role_ids = [x.id for x in rows]
- tenant = tenant_no or ""
- user_name_map = _bound_user_names_by_role_ids(db, role_ids, tenant)
- post_name_map = _bound_post_names_by_role_ids(db, role_ids, tenant)
- return [
- _row_to_dict(
- x,
- bound_user_names=user_name_map.get(x.id, []),
- bound_post_names=post_name_map.get(x.id, []),
- )
- for x in rows
- ], int(total)
- def get_role(db: Session, role_id: str) -> Optional[SysRole]:
- return (
- db.query(SysRole)
- .filter(and_(SysRole.id == role_id, SysRole.del_flag == 0))
- .first()
- )
- def role_name_exists(db: Session, name: str, tenant_no: str, exclude_id: Optional[str] = None) -> bool:
- q = db.query(SysRole).filter(
- and_(SysRole.role_name == name.strip(), SysRole.tenant_no == tenant_no, SysRole.del_flag == 0)
- )
- if exclude_id:
- q = q.filter(SysRole.id != exclude_id)
- return q.first() is not None
- # ---------------------------------------------------------------------------
- # create / update / delete
- # ---------------------------------------------------------------------------
- def create_role(
- db: Session,
- *,
- role_name: str,
- data_scope: str = "4",
- status: str = "1",
- tenant_no: str = "zkxw",
- order_num: Optional[int] = None,
- remark: Optional[str] = None,
- user_id: Optional[str] = None,
- ) -> SysRole:
- rn = (role_name or "").strip()
- if not rn:
- raise ValueError("角色名称不能为空")
- if role_name_exists(db, rn, tenant_no):
- raise ValueError("角色名称已存在")
- obj = SysRole(
- id=generate_uuid(),
- role_name=rn,
- data_scope=data_scope,
- status=status,
- tenant_no=tenant_no,
- order_num=order_num if order_num is not None else 0,
- remark=(remark or "").strip() or None,
- create_by=user_id,
- create_dept_id="",
- create_time=datetime.now(),
- update_by=user_id,
- update_time=datetime.now(),
- del_flag=0,
- )
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def update_role(
- db: Session,
- role_id: str,
- *,
- role_name: Optional[str] = None,
- data_scope: Optional[str] = None,
- status: Optional[str] = None,
- order_num: Optional[int] = None,
- remark: Optional[str] = None,
- user_id: Optional[str] = None,
- ) -> SysRole:
- obj = get_role(db, role_id)
- if not obj:
- raise ValueError("角色不存在")
- if role_name is not None:
- rn = role_name.strip()
- if not rn:
- raise ValueError("角色名称不能为空")
- if rn != obj.role_name and role_name_exists(db, rn, obj.tenant_no, exclude_id=role_id):
- raise ValueError("角色名称已存在")
- obj.role_name = rn
- if data_scope is not None:
- obj.data_scope = data_scope
- if status is not None:
- obj.status = status
- if order_num is not None:
- obj.order_num = order_num
- if remark is not None:
- obj.remark = (remark or "").strip() or None
- obj.update_by = user_id
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def soft_delete_role(db: Session, role_id: str) -> None:
- obj = get_role(db, role_id)
- if not obj:
- raise ValueError("角色不存在")
- obj.del_flag = 1
- obj.update_time = datetime.now()
- db.add(obj)
- # cascade soft-delete role_menu and role_data_scope_detail
- db.query(SysRoleMenu).filter(
- and_(SysRoleMenu.role_id == role_id, SysRoleMenu.del_flag == 0)
- ).update({"del_flag": 1, "update_time": datetime.now()})
- db.query(SysRoleDataScopeDetail).filter(
- and_(SysRoleDataScopeDetail.role_id == role_id, SysRoleDataScopeDetail.del_flag == 0)
- ).update({"del_flag": 1, "update_time": datetime.now()})
- db.commit()
- # ---------------------------------------------------------------------------
- # role-menu
- # ---------------------------------------------------------------------------
- def get_role_menu_ids(db: Session, role_id: str) -> List[str]:
- rows = (
- db.query(SysRoleMenu.menu_id)
- .filter(and_(SysRoleMenu.role_id == role_id, SysRoleMenu.del_flag == 0))
- .all()
- )
- return [r.menu_id for r in rows]
- def update_role_menus(
- db: Session,
- role_id: str,
- menu_ids: List[str],
- tenant_no: str,
- user_id: Optional[str] = None,
- ) -> None:
- # uk_role_menu 为 (role_id, menu_id, del_flag):先全量软删再插入会在再次保存时
- # 出现两条 (role_id, menu_id, del_flag=1),触发 1062。关联表采用整角色物理清理后重建。
- unique_ids = list(dict.fromkeys(menu_ids or []))
- db.query(SysRoleMenu).filter(SysRoleMenu.role_id == role_id).delete(synchronize_session=False)
- now = datetime.now()
- for mid in unique_ids:
- obj = SysRoleMenu(
- id=generate_uuid(),
- role_id=role_id,
- menu_id=mid,
- tenant_no=tenant_no,
- order_num=0,
- create_by=user_id,
- create_dept_id="",
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- )
- db.add(obj)
- db.commit()
- # ---------------------------------------------------------------------------
- # role data-scope
- # ---------------------------------------------------------------------------
- def get_role_data_scope(db: Session, role_id: str) -> List[Dict[str, str]]:
- rows = (
- db.query(SysRoleDataScopeDetail)
- .filter(and_(SysRoleDataScopeDetail.role_id == role_id, SysRoleDataScopeDetail.del_flag == 0))
- .all()
- )
- return [{"scopeType": r.scope_type, "scopeValue": r.scope_value} for r in rows]
- def update_role_data_scope(
- db: Session,
- role_id: str,
- data_scope: str,
- scope_details: Optional[List[Dict[str, str]]] = None,
- tenant_no: str = "zkxw",
- user_id: Optional[str] = None,
- ) -> None:
- obj = get_role(db, role_id)
- if not obj:
- raise ValueError("角色不存在")
- obj.data_scope = data_scope
- obj.update_by = user_id
- obj.update_time = datetime.now()
- db.add(obj)
- # soft-delete old scope details
- db.query(SysRoleDataScopeDetail).filter(
- and_(SysRoleDataScopeDetail.role_id == role_id, SysRoleDataScopeDetail.del_flag == 0)
- ).update({"del_flag": 1, "update_time": datetime.now()})
- # insert new scope details (only when custom data_scope == '5')
- if data_scope == "5" and scope_details:
- now = datetime.now()
- for detail in scope_details:
- sd = SysRoleDataScopeDetail(
- id=generate_uuid(),
- role_id=role_id,
- scope_type=detail.get("scopeType", "dept"),
- scope_value=detail.get("scopeValue", ""),
- tenant_no=tenant_no,
- order_num=0,
- create_by=user_id,
- create_dept_id="",
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- )
- db.add(sd)
- db.commit()
|