后端 endpoints/groups.py(**仅 SUPER_ADMIN**): · 组的 CRUD(两级,子组不配范围则继承父组 —— 生产大组配一次,下面的 生产/测试小组都不用再配) · 成员增删 / 设组长 / 候选人下拉(复用 MOM 查询口径,部门已钉死为 ORG_DEPARTMENT) · **删组仅限空组**:级联删是一次静默的批量权限变更,误点一下一批人就突然 看不到数据了;强制「先移人再删组」多一步,但出错时是可见的 · 停用组的语义写死在接口文档:成员**立即**退回未分组状态 为什么只有超管能管分组(不是偏好,是必须): 被显式分进组的 SUPERVISOR 会从「全厂」降级为只看本组;若允许主管管理分组, 他把自己移出组就能恢复全厂视野 —— 这是一条现成的提权路径,分组对他无效。 前端: · AdminGroupsPage:组列表 + 可见范围勾选 + 成员管理 + 组长标记 + 二次确认 · AdminLayout 加菜单项,页头显示数据范围徽标 —— 空范围(未分组)用橙色显眼 提示,否则用户看到空列表会以为系统坏了,这是最难排查的一类反馈 · AuthContext 登录后补拉一次 /auth/me 拿 scope(登录接口不查库、不返回它) · constants/task.ts 新增 isSuperAdmin,不手写 === 比较 端到端验收(实测):先造 1 生产 + 1 售后产品,然后 生产组员 → 1 条,全 PRODUCTION;范围经「生产小组 → 生产大组」继承而来 维修组员 → 1 条,全 AFTER_SALES 超管 → 2 条,全量 任务列表 total 与 returned 一致(验证 count/select 双过滤) 扫码跨组仍 200(符合「能看、不能操作」的既定决策) 停用维修大组 → 成员立即退回未分组 上述测试数据已还原
66 lines
2.0 KiB
Python
66 lines
2.0 KiB
Python
"""业务分组 Schemas"""
|
||
from pydantic import BaseModel, Field
|
||
|
||
|
||
class PhaseOption(BaseModel):
|
||
"""可选的生命周期阶段 —— 由服务端下发,前端不要写死这两个值"""
|
||
value: str
|
||
label: str
|
||
|
||
|
||
class GroupMemberOut(BaseModel):
|
||
user_id: str # MOM 短账号(与 Task.assignee_id 同口径)
|
||
display_name: str
|
||
is_leader: bool
|
||
|
||
|
||
class GroupOut(BaseModel):
|
||
id: int
|
||
name: str
|
||
parent_id: int | None = None
|
||
parent_name: str | None = None
|
||
description: str | None = None
|
||
sort_order: int = 0
|
||
is_active: bool = True
|
||
phases: list[str] = [] # 该组自己的可见范围(不含继承)
|
||
effective_phases: list[str] = [] # 实际生效范围(自己没配则取父组的)
|
||
phase_labels: list[str] = [] # 生效范围的中文标签
|
||
member_count: int = 0
|
||
|
||
|
||
class GroupDetailOut(GroupOut):
|
||
members: list[GroupMemberOut] = []
|
||
|
||
|
||
class GroupCreate(BaseModel):
|
||
name: str = Field(..., min_length=1, max_length=50, description="分组显示名")
|
||
parent_id: int | None = Field(None, description="上级大组ID;为空则建的是大组")
|
||
description: str | None = Field(None, max_length=200)
|
||
sort_order: int = 0
|
||
phases: list[str] = Field(default_factory=list, description="可见的生命周期阶段")
|
||
|
||
|
||
class GroupUpdate(BaseModel):
|
||
name: str | None = Field(None, min_length=1, max_length=50)
|
||
parent_id: int | None = None
|
||
description: str | None = Field(None, max_length=200)
|
||
sort_order: int | None = None
|
||
is_active: bool | None = None
|
||
phases: list[str] | None = Field(
|
||
None, description="整组覆盖式更新;传 [] 表示清空(此时继承父组)",
|
||
)
|
||
|
||
|
||
class GroupMemberAdd(BaseModel):
|
||
user_id: str = Field(..., min_length=1, max_length=64, description="MOM 账号")
|
||
is_leader: bool = False
|
||
|
||
|
||
class GroupMemberUpdate(BaseModel):
|
||
is_leader: bool
|
||
|
||
|
||
class MemberCandidate(BaseModel):
|
||
username: str # 短账号,存进组的就是它
|
||
full_name: str # 真实姓名,仅供界面展示
|