fix(分组): 把超管排除在分组之外 + 添加成员改用带搜索的下拉
1) 超管不进分组
超管的数据范围是硬编码全厂(resolve_data_scope 规则 1),业务分组对他
根本不生效。把他放进成员名单会造成两处矛盾:
· 界面上「他在这个组里」暗示他受该组约束,但实际不受;
· 若再给他打组长标记,会出现「组长却不受组范围限制」的怪状态。
所以:
· member-candidates 的 SQL 加 `COALESCE(role,'') <> 'SUPER_ADMIN'`
(LICA 19 人 → 17 人)
· add_member 再挡一道并给出明确原因,防止绕过界面直接调接口
· _is_super_admin_account 在 MOM 查询失败时**保守当作超管拦下** ——
误拦只是加不进去,误放会留下脏数据
2) 添加成员的下拉改用 antd Select
原先是原生 <select>:LICA 近 20 人,展开会整屏铺开、且不能搜索,又长又难选。
改为带 showSearch 的 Select(按姓名或账号过滤,optionFilterProp=label),
并在未选人时禁用「添加」按钮,避免无意义报错。
实测:
候选人数 19 → 17,超管已不在列表中
直接 POST 加 sunxia / xingyouwu → 400 并给出原因
普通成员 duwensheng 加入 → 201,移出 → 204(对照组正常)
This commit is contained in:
@ -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)
|
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:
|
def _require_super(user: dict) -> None:
|
||||||
"""建组 / 改组 / 删组 / 配范围 —— 这些是提权入口,只给超管"""
|
"""建组 / 改组 / 删组 / 配范围 —— 这些是提权入口,只给超管"""
|
||||||
if not _is_super(user):
|
if not _is_super(user):
|
||||||
@ -213,6 +237,12 @@ def list_member_candidates(
|
|||||||
):
|
):
|
||||||
"""候选人下拉 —— 复用与 users.py 一致的 MOM 查询口径(部门已钉死为 ORG_DEPARTMENT)。
|
"""候选人下拉 —— 复用与 users.py 一致的 MOM 查询口径(部门已钉死为 ORG_DEPARTMENT)。
|
||||||
|
|
||||||
|
⚠️ **排除超级管理员**:超管的数据范围是硬编码的全厂(见 resolve_data_scope
|
||||||
|
规则 1),分组对他根本不生效。把超管放进分组只会造成两处困惑:
|
||||||
|
· 界面上「他在这个组里」暗示他受这个组约束,但实际不受;
|
||||||
|
· 若再给他打组长标记,会产生「组长却不受组范围限制」的矛盾状态。
|
||||||
|
所以既不显示在候选里,也不允许加入(add_member 会再挡一道)。
|
||||||
|
|
||||||
注意这里**不复用 users.py 的端点函数**:那个函数与 FastAPI 的 Query 默认值
|
注意这里**不复用 users.py 的端点函数**:那个函数与 FastAPI 的 Query 默认值
|
||||||
耦合,直接调用拿到的是 Query 对象而非值。所以照抄同一条 SQL 的写法,
|
耦合,直接调用拿到的是 Query 对象而非值。所以照抄同一条 SQL 的写法,
|
||||||
但部门条件取自同一处 settings.ORG_DEPARTMENT,口径不会漂移。
|
但部门条件取自同一处 settings.ORG_DEPARTMENT,口径不会漂移。
|
||||||
@ -224,8 +254,9 @@ def list_member_candidates(
|
|||||||
SPLIT_PART(username, '/', 1) AS full_name
|
SPLIT_PART(username, '/', 1) AS full_name
|
||||||
FROM sys_user
|
FROM sys_user
|
||||||
WHERE department = :dept
|
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():
|
if keyword.strip():
|
||||||
sql_text = base_sql + " AND username ILIKE :kw ORDER BY username LIMIT :lim"
|
sql_text = base_sql + " AND username ILIKE :kw ORDER BY username LIMIT :lim"
|
||||||
params["kw"] = f"%{keyword.strip()}%"
|
params["kw"] = f"%{keyword.strip()}%"
|
||||||
@ -551,6 +582,14 @@ async def add_member(
|
|||||||
status.HTTP_403_FORBIDDEN, "只有超级管理员、主管或本组组长可以添加成员"
|
status.HTTP_403_FORBIDDEN, "只有超级管理员、主管或本组组长可以添加成员"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 超管不进分组:他的范围是硬编码全厂,分组对他不生效。
|
||||||
|
# 放进来的话界面会暗示「他受这个组约束」,与事实不符。
|
||||||
|
if await _is_super_admin_account(payload.user_id):
|
||||||
|
raise HTTPException(
|
||||||
|
status.HTTP_400_BAD_REQUEST,
|
||||||
|
"超级管理员不受业务分组约束(数据范围恒为全厂),无需加入分组",
|
||||||
|
)
|
||||||
|
|
||||||
exists = await db.scalar(
|
exists = await db.scalar(
|
||||||
select(BusinessGroupMember.id).where(
|
select(BusinessGroupMember.id).where(
|
||||||
BusinessGroupMember.group_id == group_id,
|
BusinessGroupMember.group_id == group_id,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
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 { Plus, Pencil, Trash2, UserPlus, Crown, ShieldCheck } from "lucide-react";
|
||||||
import { useAuth } from "../../contexts/AuthContext";
|
import { useAuth } from "../../contexts/AuthContext";
|
||||||
import { isSuperAdmin } from "../../constants/task";
|
import { isSuperAdmin } from "../../constants/task";
|
||||||
@ -412,22 +412,33 @@ export default function AdminGroupsPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 添加成员面板 */}
|
{/* 添加成员面板 —— 用 antd Select 而非原生 select:
|
||||||
|
LICA 有近 20 人,原生下拉会整屏铺开且不能搜,又长又难选。 */}
|
||||||
{candidates.length > 0 && (
|
{candidates.length > 0 && (
|
||||||
<div className="mt-3 flex items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 p-3">
|
<div className="mt-3 flex flex-wrap items-center gap-3 rounded-lg border border-blue-200 bg-blue-50 p-3">
|
||||||
<select
|
<Select
|
||||||
className="flex-1 rounded-lg border border-gray-200 px-3 py-2 text-sm focus:border-blue-400 focus:outline-none"
|
className="min-w-[260px] flex-1"
|
||||||
value={picked ?? ""}
|
placeholder="搜索姓名或账号…"
|
||||||
onChange={(e) => setPicked(e.target.value || undefined)}
|
showSearch
|
||||||
>
|
allowClear
|
||||||
<option value="">选择人员…</option>
|
autoFocus
|
||||||
{candidates.map((c) => (
|
value={picked}
|
||||||
<option key={c.username} value={c.username}>{c.full_name}({c.username})</option>
|
onChange={(v) => setPicked(v)}
|
||||||
))}
|
optionFilterProp="label"
|
||||||
</select>
|
options={candidates.map((c) => ({
|
||||||
<Checkbox checked={pickedLeader} onChange={(e) => setPickedLeader(e.target.checked)}>设为组长</Checkbox>
|
value: c.username,
|
||||||
<Button type="primary" size="small" loading={adding} onClick={handleAddMember}>添加</Button>
|
label: `${c.full_name}(${c.username})`,
|
||||||
<Button size="small" onClick={() => setCandidates([])}>取消</Button>
|
}))}
|
||||||
|
/>
|
||||||
|
<Checkbox checked={pickedLeader} onChange={(e) => setPickedLeader(e.target.checked)}>
|
||||||
|
设为组长
|
||||||
|
</Checkbox>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button type="primary" size="small" loading={adding} disabled={!picked} onClick={handleAddMember}>
|
||||||
|
添加
|
||||||
|
</Button>
|
||||||
|
<Button size="small" onClick={() => setCandidates([])}>取消</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user