| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769 |
- """角色 CRUD、权限矩阵与用户关联维护。"""
- from __future__ import annotations
- from datetime import datetime, timezone
- from typing import Any
- from sqlalchemy import func, select
- from sqlalchemy.exc import IntegrityError
- from dms.common.enums import (
- AuditAction,
- AuditTarget,
- DeptScope,
- EnabledStatus,
- RoleCode,
- SecurityLevel,
- )
- from dms.common.errors import (
- ConflictError,
- InvalidArgumentError,
- ResourceNotFoundError,
- RoleInUseError,
- )
- from dms.common.pagination import PageRequest, page_result
- from dms.common.response import serialize_id
- from dms.database.transaction import transaction
- from dms.extensions import db
- from dms.models import Organization, Role, RolePermission, User
- from dms.security.auth_context import get_auth_context
- from dms.services.admin_permission import (
- MANAGED_ORG_SELF,
- MANAGED_ORG_SELF_ANCHOR,
- SUPER_ADMIN_USERNAME,
- )
- from dms.services.audit_service import business_audit
- ROLE_CREATE_FIELDS = {
- "roleName",
- "sortNo",
- "status",
- "remark",
- "isDefault",
- "manageCategoryEdit",
- "manageOrgEdit",
- "manageUserEdit",
- "manageSystemManage",
- "manageAuditLog",
- "manageRoleEdit",
- "managedOrgId",
- }
- ROLE_UPDATE_FIELDS = {
- "roleName",
- "sortNo",
- "status",
- "remark",
- "isDefault",
- "rowVersion",
- "manageCategoryEdit",
- "manageOrgEdit",
- "manageUserEdit",
- "manageSystemManage",
- "manageAuditLog",
- "manageRoleEdit",
- "managedOrgId",
- }
- ROLE_PERMISSION_FIELDS = {"matrix", "rowVersion"}
- ROLE_USER_FIELDS = {"userIds", "rowVersion"}
- # 矩阵 8 个操作布尔位(契约1.31:canUpload 仅 OWN 行有意义,OTHER 行恒false;
- # canBind/canUnbind 界面合并为单一"挂载"开关,两列恒同值)
- PERMISSION_FIELDS: tuple[tuple[str, str], ...] = (
- ("canView", "can_view"),
- ("canUpload", "can_upload"),
- ("canDownload", "can_download"),
- ("canEdit", "can_edit"),
- ("canDelete", "can_delete"),
- ("canConfigPermission", "can_config_permission"),
- ("canBindAttachment", "can_bind_attachment"),
- ("canUnbindAttachment", "can_unbind_attachment"),
- )
- MATRIX_KEYS = {
- (level.value, scope.value)
- for level in SecurityLevel
- for scope in DeptScope
- }
- def _utc_now() -> datetime:
- return datetime.now(timezone.utc).replace(tzinfo=None)
- def _required_text(
- value: Any,
- *,
- field: str,
- max_length: int,
- ) -> str:
- if not isinstance(value, str) or not value.strip():
- raise InvalidArgumentError(f"{field}不能为空")
- normalized = value.strip()
- if len(normalized) > max_length:
- raise InvalidArgumentError(f"{field}长度不能超过{max_length}")
- return normalized
- def _optional_text(value: Any, *, field: str, max_length: int) -> str | None:
- if value is None:
- return None
- if not isinstance(value, str):
- raise InvalidArgumentError(f"{field}必须是字符串")
- normalized = value.strip()
- if len(normalized) > max_length:
- raise InvalidArgumentError(f"{field}长度不能超过{max_length}")
- return normalized or None
- def _sort_no(value: Any) -> int:
- if type(value) is not int or value < 0 or value > 999999:
- raise InvalidArgumentError("sortNo必须是0~999999的整数")
- return value
- def _status_value(value: Any) -> str:
- if not isinstance(value, str):
- raise InvalidArgumentError("status必须是字符串")
- try:
- return EnabledStatus(value).value
- except ValueError as exc:
- raise InvalidArgumentError("status必须是ENABLED或DISABLED") from exc
- def _boolean(value: Any, *, field: str) -> bool:
- if type(value) is not bool:
- raise InvalidArgumentError(f"{field}必须是布尔值")
- return value
- def _optional_org_id(value: Any) -> int | None:
- """managedOrgId 解析:空串/NULL 视为全局,"SELF" 为跟随用户所在部门
- 哨兵(落库 0),否则须指向未删除机构。"""
- if value is None or value == "":
- return None
- if value == MANAGED_ORG_SELF:
- return MANAGED_ORG_SELF_ANCHOR
- if not isinstance(value, str) or not value.isdecimal() or int(value) <= 0:
- raise InvalidArgumentError("managedOrgId必须是正整数形式的字符串ID")
- org_id = int(value)
- exists = db.session.scalar(
- select(Organization.id).where(
- Organization.id == org_id,
- Organization.is_deleted.is_(False),
- )
- )
- if exists is None:
- raise InvalidArgumentError("managedOrgId必须指向有效机构")
- return org_id
- def _managed_org_value(org_id: int | None) -> str | None:
- """锚点序列化:SELF 哨兵(0)还原为 "SELF",其余按机构ID。"""
- if org_id == MANAGED_ORG_SELF_ANCHOR:
- return MANAGED_ORG_SELF
- return serialize_id(org_id)
- def _row_version(value: Any) -> int:
- if type(value) is not int or value < 0:
- raise InvalidArgumentError("rowVersion必须是非负整数")
- return value
- def _id_list(value: Any, *, field: str) -> list[int]:
- if not isinstance(value, list) or any(
- not isinstance(item, str) or not item.isdecimal() or int(item) <= 0
- for item in value
- ):
- raise InvalidArgumentError(f"{field}必须是正整数形式的字符串ID数组")
- ids = [int(item) for item in value]
- if len(set(ids)) != len(ids):
- raise InvalidArgumentError(f"{field}不能包含重复ID")
- return ids
- def _exact_payload(payload: Any, expected: set[str]) -> dict[str, Any]:
- if not isinstance(payload, dict) or set(payload) != expected:
- raise InvalidArgumentError(
- "请求体字段必须且只能包含:" + "、".join(sorted(expected))
- )
- return payload
- def _role_row(session, role_id: int, *, for_update: bool = False) -> Role:
- stmt = select(Role).where(Role.id == role_id, Role.is_deleted.is_(False))
- if for_update:
- stmt = stmt.with_for_update()
- role = session.scalar(stmt)
- if role is None:
- raise ResourceNotFoundError("角色不存在")
- return role
- def _member_counts(session, role_ids: list[int]) -> dict[int, int]:
- if not role_ids:
- return {}
- rows = session.execute(
- select(User.role_id, func.count(User.id))
- .where(User.role_id.in_(role_ids), User.is_deleted.is_(False))
- .group_by(User.role_id)
- ).all()
- return {role_id: count for role_id, count in rows}
- def role_summary(role: Role, member_count: int) -> dict[str, object]:
- return {
- "id": serialize_id(role.id),
- "roleName": role.role_name,
- "sortNo": role.sort_no,
- "status": role.status,
- "remark": role.remark,
- "isDefault": role.is_default,
- "manageCategoryEdit": bool(role.manage_category_edit),
- "manageOrgEdit": bool(role.manage_org_edit),
- "manageUserEdit": bool(role.manage_user_edit),
- "manageSystemManage": bool(role.manage_system),
- "manageAuditLog": bool(role.manage_audit_log),
- "manageRoleEdit": bool(role.manage_role_edit),
- "managedOrgId": _managed_org_value(role.managed_org_id),
- "memberCount": member_count,
- "rowVersion": role.row_version,
- }
- def list_roles(
- *,
- keyword: str | None,
- status: str | None,
- page_request: PageRequest,
- ) -> dict[str, object]:
- filters = [Role.is_deleted.is_(False)]
- if keyword:
- filters.append(Role.role_name.like(f"%{keyword.strip()}%"))
- if status:
- try:
- normalized_status = EnabledStatus(status).value
- except ValueError as exc:
- raise InvalidArgumentError("status必须是ENABLED或DISABLED") from exc
- filters.append(Role.status == normalized_status)
- total = db.session.scalar(select(func.count(Role.id)).where(*filters)) or 0
- roles = db.session.scalars(
- select(Role)
- .where(*filters)
- .order_by(Role.sort_no.asc(), Role.id.asc())
- .offset(page_request.offset)
- .limit(page_request.page_size)
- ).all()
- counts = _member_counts(db.session, [role.id for role in roles])
- return page_result(
- [role_summary(role, counts.get(role.id, 0)) for role in roles],
- page=page_request.page,
- page_size=page_request.page_size,
- total=total,
- )
- def _clear_other_defaults(session, *, keep_id: int | None) -> None:
- """将除 keep_id 外的活跃默认角色取消默认标记(先清后设避免唯一约束冲突)。"""
- stmt = select(Role).where(
- Role.is_default.is_(True),
- Role.is_deleted.is_(False),
- )
- if keep_id is not None:
- stmt = stmt.where(Role.id != keep_id)
- actor_id = get_auth_context().user_id
- now = _utc_now()
- for existing in session.scalars(stmt).all():
- existing.is_default = False
- existing.updated_by = actor_id
- existing.updated_at = now
- existing.row_version += 1
- def _ensure_role_name_free(session, role_name: str, *, exclude_id: int) -> None:
- duplicated = session.scalar(
- select(Role.id).where(
- Role.role_name == role_name,
- Role.is_deleted.is_(False),
- Role.id != exclude_id,
- )
- )
- if duplicated is not None:
- raise ConflictError("角色名已存在")
- def _seed_permission_rows(session, role_id: int, actor_id: int | None) -> None:
- for level in SecurityLevel:
- for scope in DeptScope:
- session.add(
- RolePermission(
- role_id=role_id,
- security_level=level.value,
- dept_scope=scope.value,
- created_by=actor_id,
- updated_by=actor_id,
- )
- )
- def create_role(payload: Any) -> dict[str, object]:
- body = _exact_payload(payload, ROLE_CREATE_FIELDS)
- role_name = _required_text(body["roleName"], field="roleName", max_length=64)
- sort_no = _sort_no(body["sortNo"])
- status = _status_value(body["status"])
- remark = _optional_text(body["remark"], field="remark", max_length=255)
- is_default = _boolean(body["isDefault"], field="isDefault")
- manage_category = _boolean(body["manageCategoryEdit"], field="manageCategoryEdit")
- manage_org = _boolean(body["manageOrgEdit"], field="manageOrgEdit")
- manage_user = _boolean(body["manageUserEdit"], field="manageUserEdit")
- manage_system = _boolean(body["manageSystemManage"], field="manageSystemManage")
- manage_audit_log = _boolean(body["manageAuditLog"], field="manageAuditLog")
- manage_role_edit = _boolean(body["manageRoleEdit"], field="manageRoleEdit")
- managed_org_id = _optional_org_id(body["managedOrgId"])
- actor_id = get_auth_context().user_id
- try:
- with transaction() as session:
- _ensure_role_name_free(session, role_name, exclude_id=0)
- if is_default:
- _clear_other_defaults(session, keep_id=None)
- session.flush()
- role = Role(
- role_name=role_name,
- sort_no=sort_no,
- status=status,
- remark=remark,
- is_default=is_default,
- manage_category_edit=manage_category,
- manage_org_edit=manage_org,
- manage_user_edit=manage_user,
- manage_system=manage_system,
- manage_audit_log=manage_audit_log,
- manage_role_edit=manage_role_edit,
- managed_org_id=managed_org_id,
- created_by=actor_id,
- updated_by=actor_id,
- )
- session.add(role)
- session.flush()
- _seed_permission_rows(session, role.id, actor_id)
- session.add(
- business_audit(
- action=AuditAction.CREATE_ROLE,
- target=AuditTarget.ROLE,
- target_id=role.id,
- target_name=role.role_name,
- detail=_role_detail(role),
- )
- )
- return role_summary(role, 0)
- except IntegrityError as exc:
- raise ConflictError("角色名已存在或默认角色设置冲突") from exc
- def _role_detail(role: Role) -> dict[str, object]:
- return {
- "roleName": role.role_name,
- "sortNo": role.sort_no,
- "status": role.status,
- "remark": role.remark,
- "isDefault": role.is_default,
- "manageCategoryEdit": bool(role.manage_category_edit),
- "manageOrgEdit": bool(role.manage_org_edit),
- "manageUserEdit": bool(role.manage_user_edit),
- "manageSystemManage": bool(role.manage_system),
- "manageAuditLog": bool(role.manage_audit_log),
- "manageRoleEdit": bool(role.manage_role_edit),
- "managedOrgId": _managed_org_value(role.managed_org_id),
- }
- def update_role(role_id: int, payload: Any) -> dict[str, object]:
- body = _exact_payload(payload, ROLE_UPDATE_FIELDS)
- role_name = _required_text(body["roleName"], field="roleName", max_length=64)
- sort_no = _sort_no(body["sortNo"])
- status = _status_value(body["status"])
- remark = _optional_text(body["remark"], field="remark", max_length=255)
- is_default = _boolean(body["isDefault"], field="isDefault")
- manage_category = _boolean(body["manageCategoryEdit"], field="manageCategoryEdit")
- manage_org = _boolean(body["manageOrgEdit"], field="manageOrgEdit")
- manage_user = _boolean(body["manageUserEdit"], field="manageUserEdit")
- manage_system = _boolean(body["manageSystemManage"], field="manageSystemManage")
- manage_audit_log = _boolean(body["manageAuditLog"], field="manageAuditLog")
- manage_role_edit = _boolean(body["manageRoleEdit"], field="manageRoleEdit")
- managed_org_id = _optional_org_id(body["managedOrgId"])
- expected_version = _row_version(body["rowVersion"])
- actor_id = get_auth_context().user_id
- now = _utc_now()
- with transaction() as session:
- role = _role_row(session, role_id, for_update=True)
- if role.row_version != expected_version:
- raise ConflictError(
- "数据已被其他用户修改,请刷新后重试",
- details={"currentRowVersion": role.row_version},
- )
- _ensure_role_name_free(session, role_name, exclude_id=role.id)
- before = _role_detail(role)
- # 新设默认:先清旧默认,避免 default_marker 唯一约束冲突
- if is_default and not role.is_default:
- _clear_other_defaults(session, keep_id=role.id)
- session.flush()
- role.role_name = role_name
- role.sort_no = sort_no
- role.status = status
- role.remark = remark
- role.is_default = is_default
- role.manage_category_edit = manage_category
- role.manage_org_edit = manage_org
- role.manage_user_edit = manage_user
- role.manage_system = manage_system
- role.manage_audit_log = manage_audit_log
- role.manage_role_edit = manage_role_edit
- role.managed_org_id = managed_org_id
- role.updated_by = actor_id
- role.updated_at = now
- role.row_version += 1
- after = _role_detail(role)
- session.add(
- business_audit(
- action=AuditAction.EDIT_ROLE,
- target=AuditTarget.ROLE,
- target_id=role.id,
- target_name=role.role_name,
- detail={"before": before, "after": after},
- )
- )
- member_count = _member_counts(db.session, [role.id]).get(role.id, 0)
- return role_summary(role, member_count)
- def delete_role(role_id: int, row_version: int) -> dict[str, object]:
- actor_id = get_auth_context().user_id
- now = _utc_now()
- with transaction() as session:
- role = _role_row(session, role_id, for_update=True)
- if role.row_version != row_version:
- raise ConflictError(
- "数据已被其他用户修改,请刷新后重试",
- details={"currentRowVersion": role.row_version},
- )
- bound_users = (
- session.scalar(
- select(func.count(User.id)).where(
- User.role_id == role_id,
- User.is_deleted.is_(False),
- )
- )
- or 0
- )
- if bound_users:
- raise RoleInUseError(
- details={"memberCount": bound_users},
- )
- target_name = role.role_name
- role.is_deleted = True
- role.deleted_at = now
- role.updated_by = actor_id
- role.updated_at = now
- role.row_version += 1
- # 矩阵行随角色软删,保留审计追溯
- for row in session.scalars(
- select(RolePermission).where(
- RolePermission.role_id == role_id,
- RolePermission.is_deleted.is_(False),
- )
- ).all():
- row.is_deleted = True
- row.deleted_at = now
- row.updated_by = actor_id
- row.updated_at = now
- session.add(
- business_audit(
- action=AuditAction.DELETE_ROLE,
- target=AuditTarget.ROLE,
- target_id=role_id,
- target_name=target_name,
- detail={"roleName": target_name, "rowVersion": role.row_version},
- )
- )
- return {"id": serialize_id(role_id), "deleted": True}
- def _permission_row_dto(row: RolePermission) -> dict[str, object]:
- dto: dict[str, object] = {
- "securityLevel": row.security_level,
- "deptScope": row.dept_scope,
- }
- for name, attr in PERMISSION_FIELDS:
- dto[name] = getattr(row, attr)
- return dto
- def list_role_permissions(role_id: int) -> list[dict[str, object]]:
- _role_row(db.session, role_id)
- rows = db.session.scalars(
- select(RolePermission)
- .where(
- RolePermission.role_id == role_id,
- RolePermission.is_deleted.is_(False),
- )
- .order_by(RolePermission.id.asc())
- ).all()
- return [_permission_row_dto(row) for row in rows]
- def _validate_matrix(value: Any) -> list[dict[str, Any]]:
- expected_row_fields = {"securityLevel", "deptScope"} | {
- name for name, _ in PERMISSION_FIELDS
- }
- if not isinstance(value, list) or len(value) != len(MATRIX_KEYS):
- raise InvalidArgumentError(f"权限矩阵必须为{len(MATRIX_KEYS)}行")
- rows: dict[tuple[str, str], dict[str, Any]] = {}
- for item in value:
- if not isinstance(item, dict) or set(item) != expected_row_fields:
- raise InvalidArgumentError(
- "矩阵行字段必须且只能包含:" + "、".join(sorted(expected_row_fields))
- )
- key = (item["securityLevel"], item["deptScope"])
- if key not in MATRIX_KEYS:
- raise InvalidArgumentError("矩阵行的密级或部门范围不是有效枚举值")
- if key in rows:
- raise InvalidArgumentError("矩阵行不能重复")
- for name, _ in PERMISSION_FIELDS:
- if type(item[name]) is not bool:
- raise InvalidArgumentError(f"{name}必须是布尔值")
- # 上传权限仅限本部门(OWN)行:其他部门行恒不可选(契约1.31)
- if item["deptScope"] == DeptScope.OTHER.value and item["canUpload"]:
- raise InvalidArgumentError("上传权限仅可设置本部门方案")
- rows[key] = item
- return list(value)
- def update_role_permissions(role_id: int, payload: Any) -> dict[str, object]:
- body = _exact_payload(payload, ROLE_PERMISSION_FIELDS)
- matrix = _validate_matrix(body["matrix"])
- expected_version = _row_version(body["rowVersion"])
- actor_id = get_auth_context().user_id
- now = _utc_now()
- with transaction() as session:
- role = _role_row(session, role_id, for_update=True)
- if role.row_version != expected_version:
- raise ConflictError(
- "数据已被其他用户修改,请刷新后重试",
- details={"currentRowVersion": role.row_version},
- )
- existing_rows = session.scalars(
- select(RolePermission).where(
- RolePermission.role_id == role_id,
- RolePermission.is_deleted.is_(False),
- )
- ).all()
- by_key = {(row.security_level, row.dept_scope): row for row in existing_rows}
- if set(by_key) != MATRIX_KEYS:
- raise ConflictError("角色权限矩阵数据不完整,请联系管理员核查")
- changed_keys: list[dict[str, object]] = []
- for item in matrix:
- row = by_key[(item["securityLevel"], item["deptScope"])]
- before_flags = {
- name: getattr(row, attr) for name, attr in PERMISSION_FIELDS
- }
- after_flags = {name: item[name] for name, _ in PERMISSION_FIELDS}
- if before_flags != after_flags:
- row.updated_by = actor_id
- row.updated_at = now
- for name, attr in PERMISSION_FIELDS:
- setattr(row, attr, item[name])
- changed_keys.append(
- {
- "securityLevel": row.security_level,
- "deptScope": row.dept_scope,
- "before": before_flags,
- "after": after_flags,
- }
- )
- role.updated_by = actor_id
- role.updated_at = now
- role.row_version += 1
- session.add(
- business_audit(
- action=AuditAction.CHANGE_ROLE_PERMISSION,
- target=AuditTarget.ROLE,
- target_id=role_id,
- target_name=role.role_name,
- detail={"scope": "matrix", "changedRows": changed_keys},
- )
- )
- return {
- "id": serialize_id(role_id),
- "matrix": list_role_permissions(role_id),
- "rowVersion": role.row_version,
- }
- def list_role_users(role_id: int) -> dict[str, object]:
- _role_row(db.session, role_id)
- user_ids = db.session.scalars(
- select(User.id)
- .where(User.role_id == role_id, User.is_deleted.is_(False))
- .order_by(User.id.asc())
- ).all()
- return {"id": serialize_id(role_id), "userIds": [serialize_id(v) for v in user_ids]}
- def update_role_users(role_id: int, payload: Any) -> dict[str, object]:
- body = _exact_payload(payload, ROLE_USER_FIELDS)
- user_ids = _id_list(body["userIds"], field="userIds")
- expected_version = _row_version(body["rowVersion"])
- actor_id = get_auth_context().user_id
- now = _utc_now()
- with transaction() as session:
- role = _role_row(session, role_id, for_update=True)
- if role.row_version != expected_version:
- raise ConflictError(
- "数据已被其他用户修改,请刷新后重试",
- details={"currentRowVersion": role.row_version},
- )
- target_ids = set(user_ids)
- found: dict[int, User] = {}
- if target_ids:
- for user in session.scalars(
- select(User)
- .where(User.id.in_(target_ids), User.is_deleted.is_(False))
- .with_for_update()
- ).all():
- found[user.id] = user
- if set(found) != target_ids:
- raise InvalidArgumentError("userIds包含无效用户")
- for user in found.values():
- if user.username == SUPER_ADMIN_USERNAME:
- raise InvalidArgumentError("内置admin账号不允许关联自定义角色")
- if user.role_code not in (
- RoleCode.USER.value,
- RoleCode.ADMIN.value,
- ):
- raise InvalidArgumentError("仅普通用户或系统管理员可关联自定义角色")
- current_users = session.scalars(
- select(User)
- .where(User.role_id == role_id, User.is_deleted.is_(False))
- .with_for_update()
- ).all()
- added_ids: list[int] = []
- removed_ids: list[int] = []
- for user in current_users:
- if user.id not in target_ids:
- user.role_id = None
- user.auth_version += 1
- user.updated_by = actor_id
- user.updated_at = now
- user.row_version += 1
- removed_ids.append(user.id)
- for user_id in sorted(target_ids):
- user = found[user_id]
- if user.role_id != role_id:
- user.role_id = role_id
- user.auth_version += 1
- user.updated_by = actor_id
- user.updated_at = now
- user.row_version += 1
- added_ids.append(user_id)
- role.updated_by = actor_id
- role.updated_at = now
- role.row_version += 1
- session.add(
- business_audit(
- action=AuditAction.CHANGE_ROLE_USER,
- target=AuditTarget.ROLE,
- target_id=role_id,
- target_name=role.role_name,
- detail={
- "addedUserIds": [serialize_id(v) for v in added_ids],
- "removedUserIds": [serialize_id(v) for v in removed_ids],
- },
- )
- )
- return {
- "id": serialize_id(role_id),
- "userIds": [serialize_id(v) for v in sorted(target_ids)],
- "rowVersion": role.row_version,
- }
- ROLE_REORDER_FIELDS = {"orderedIds"}
- def reorder_roles(payload: Any) -> dict[str, object]:
- """角色列表拖动重排(契约1.27):全体角色槽位置换,步长10重编号 sort_no。
- orderedIds 允许为当前分页视图的子集,被提交角色按原槽位次序互换
- (未提交角色相对次序不变),与分类同级重排的置换语义一致;
- 权限由路由层 role_edit_required 把关。
- """
- body = _exact_payload(payload, ROLE_REORDER_FIELDS)
- ordered_ids = body["orderedIds"]
- if not isinstance(ordered_ids, list) or any(
- not isinstance(item, str) or not item.isdecimal() or int(item) <= 0
- for item in ordered_ids
- ):
- raise InvalidArgumentError("orderedIds必须是正整数形式的字符串ID数组")
- if len(set(ordered_ids)) != len(ordered_ids):
- raise InvalidArgumentError("orderedIds存在重复ID")
- if len(ordered_ids) < 2:
- raise InvalidArgumentError("orderedIds必须是至少包含2个ID的数组")
- actor_id = get_auth_context().user_id
- with transaction() as session:
- rows = session.scalars(
- select(Role)
- .where(Role.is_deleted.is_(False))
- .order_by(Role.sort_no.asc(), Role.id.asc())
- .with_for_update()
- ).all()
- row_by_id = {row.id: row for row in rows}
- all_ids = [row.id for row in rows]
- ordered = [int(item) for item in ordered_ids]
- if any(role_id not in row_by_id for role_id in ordered):
- raise InvalidArgumentError("orderedIds包含无效角色")
- # 槽位置换:被提交ID按原槽位顺序排好,依次填入目标顺序
- slot_ids = sorted(ordered, key=all_ids.index)
- new_order = list(all_ids)
- for position, role_id in enumerate(slot_ids):
- new_order[all_ids.index(role_id)] = ordered[position]
- now = _utc_now()
- reordered_count = 0
- for index, role_id in enumerate(new_order):
- row = row_by_id[role_id]
- target = (index + 1) * 10
- if row.sort_no == target:
- continue
- row.sort_no = target
- row.updated_by = actor_id
- row.updated_at = now
- row.row_version += 1
- reordered_count += 1
- first = row_by_id[ordered[0]]
- session.add(
- business_audit(
- action=AuditAction.REORDER_ROLE,
- target=AuditTarget.ROLE,
- target_id=first.id,
- target_name=first.role_name,
- detail={
- "orderedIds": list(ordered_ids),
- "reorderedCount": reordered_count,
- },
- )
- )
- return {"reorderedCount": reordered_count}
|