Files
track-LICA/backend/app/schemas/group.py
duxingchen 3c1c5d6fb5 feat(分组权限): 分组管理接口 + 管理页 + 端到端验收
后端 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(符合「能看、不能操作」的既定决策)
  停用维修大组 → 成员立即退回未分组
  上述测试数据已还原
2026-09-21 17:10:33 +08:00

66 lines
2.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""业务分组 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 # 真实姓名,仅供界面展示