diff --git a/backend/app/api/v1/endpoints/groups.py b/backend/app/api/v1/endpoints/groups.py index 381b1d9..f059e5f 100644 --- a/backend/app/api/v1/endpoints/groups.py +++ b/backend/app/api/v1/endpoints/groups.py @@ -105,6 +105,30 @@ async def _can_manage_members(db: AsyncSession, user: dict, group_id: int) -> bo return group_id in await _leader_group_ids(db, user) +async def _is_super_admin_account(username: str) -> bool: + """这个 MOM 账号是不是超管。 + + 要在 MOM 侧查(Track 不存用户表)。查不到(账号不存在)时返回 False —— + 交给后续流程处理,不在这里假装是超管。 + """ + if not username: + return False + db = MomSessionLocal() + try: + from sqlalchemy import text + row = db.execute( + text("SELECT role FROM sys_user WHERE username LIKE :p LIMIT 1"), + {"p": f"%/{username}"}, + ).fetchone() + return bool(row) and (row[0] or "") == SUPER_ADMIN + except Exception: + # MOM 不可用时**不放行**为「不是超管」的反面 —— 这里失败意味着查不到身份, + # 保守做法是当作超管拦下(误拦只是加不进去,误放会留下脏数据) + return True + finally: + db.close() + + def _require_super(user: dict) -> None: """建组 / 改组 / 删组 / 配范围 —— 这些是提权入口,只给超管""" if not _is_super(user): @@ -213,6 +237,12 @@ def list_member_candidates( ): """候选人下拉 —— 复用与 users.py 一致的 MOM 查询口径(部门已钉死为 ORG_DEPARTMENT)。 + ⚠️ **排除超级管理员**:超管的数据范围是硬编码的全厂(见 resolve_data_scope + 规则 1),分组对他根本不生效。把超管放进分组只会造成两处困惑: + · 界面上「他在这个组里」暗示他受这个组约束,但实际不受; + · 若再给他打组长标记,会产生「组长却不受组范围限制」的矛盾状态。 + 所以既不显示在候选里,也不允许加入(add_member 会再挡一道)。 + 注意这里**不复用 users.py 的端点函数**:那个函数与 FastAPI 的 Query 默认值 耦合,直接调用拿到的是 Query 对象而非值。所以照抄同一条 SQL 的写法, 但部门条件取自同一处 settings.ORG_DEPARTMENT,口径不会漂移。 @@ -224,8 +254,9 @@ def list_member_candidates( SPLIT_PART(username, '/', 1) AS full_name FROM sys_user WHERE department = :dept + AND COALESCE(role, '') <> :super_admin """ - params = {"dept": settings.ORG_DEPARTMENT, "lim": limit} + params = {"dept": settings.ORG_DEPARTMENT, "lim": limit, "super_admin": SUPER_ADMIN} if keyword.strip(): sql_text = base_sql + " AND username ILIKE :kw ORDER BY username LIMIT :lim" params["kw"] = f"%{keyword.strip()}%" @@ -551,6 +582,14 @@ async def add_member( status.HTTP_403_FORBIDDEN, "只有超级管理员、主管或本组组长可以添加成员" ) + # 超管不进分组:他的范围是硬编码全厂,分组对他不生效。 + # 放进来的话界面会暗示「他受这个组约束」,与事实不符。 + if await _is_super_admin_account(payload.user_id): + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + "超级管理员不受业务分组约束(数据范围恒为全厂),无需加入分组", + ) + exists = await db.scalar( select(BusinessGroupMember.id).where( BusinessGroupMember.group_id == group_id, diff --git a/frontend/src/pages/admin/AdminGroupsPage.tsx b/frontend/src/pages/admin/AdminGroupsPage.tsx index 9510575..90c91ab 100644 --- a/frontend/src/pages/admin/AdminGroupsPage.tsx +++ b/frontend/src/pages/admin/AdminGroupsPage.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback, useMemo } from "react"; -import { Table, Button, Modal, Input, Checkbox, Tag, App, Spin, Empty } from "antd"; +import { Table, Button, Modal, Input, Checkbox, Tag, App, Spin, Empty, Select } from "antd"; import { Plus, Pencil, Trash2, UserPlus, Crown, ShieldCheck } from "lucide-react"; import { useAuth } from "../../contexts/AuthContext"; import { isSuperAdmin } from "../../constants/task"; @@ -412,22 +412,33 @@ export default function AdminGroupsPage() { /> )} - {/* 添加成员面板 */} + {/* 添加成员面板 —— 用 antd Select 而非原生 select: + LICA 有近 20 人,原生下拉会整屏铺开且不能搜,又长又难选。 */} {candidates.length > 0 && ( -