Files
track-LICA/backend/app/schemas/user.py
duxingchen cfcfca7269 feat(分组权限): 业务分组模型 + DataScope 判定 + 列表接口接入
第一阶段:模型 → 判定 → /auth/me → 列表。统计接口与前端管理页随后。

1) 模型与迁移(head 从 k1l2m3n4o5p6 推进到 l1m2n3o4p5q6)
   · business_groups        组定义,parent_id 表达「大组 > 小组」
   · business_group_phases  可见范围,独立成表以支持多选 —— 需求要求
                            「范围可配置、不要写死」,单列存不下多个 phase
   · business_group_members 成员,一人可属多组(这是「同时看生产+维修」的实现)
   只建表、不写种子数据,所以可以先部署代码再建组。

2) DataScope 判定模块(app/services/data_scope_service.py)
   全仓库唯一的权限谓词来源,业务代码里不准再出现 lifecycle_phase 过滤。
   两条红线照抄部门隔离的教训:
   · None(不限) 与 frozenset()(空) 语义相反,绝不共用一个哨兵值
   · 空集合必须显式 false() —— SQLAlchemy 对 in_(()) 生 成 IN (NULL),
     一旦退化成不过滤就是全量泄漏
   解析优先级:SUPER_ADMIN 硬放行(不可被分组覆盖)
             > 显式分组(分组优先于角色)
             > 未分组 SUPERVISOR 默认全厂
             > 未分组普通用户

3) 过渡期开关 DATA_SCOPE_UNGROUPED(默认 ALL)
   直接上严格模式会让所有未分组工人当场看不到自己的任务、现场停摆。
   默认 ALL 先放行并打 WARNING 记录「谁还没分组」,配好组后再改 NONE。

4) /auth/me 返回 scope,phase 中文标签由服务端下发
   —— 前端已有两份 phase 词表副本,不再加第三份。

5) 列表接口接入
   · get_all_products:过滤加在 offset/limit 之前(其下有 6 段基于 product_ids
     的批量预计算,过滤晚了等于算完再丢)
   · get_all_tasks:谓词进【共享 filters】,保证 count 与 select 两条独立语句
     同时生效,否则 total 与实际页不一致、移动端 hasMore 判断跟着错
   · 扫码 get_product_by_serial 刻意不过滤,理由写死在 docstring 里

实测:空 scope 生成 false、受限 scope 生成 JOIN + IN 谓词;
/products 返回 2 条、/tasks 的 total 与 returned 一致。
2026-09-21 17:07:46 +08:00

53 lines
1.8 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 — 对接 MOM sys_user 表 + 双 Token"""
from pydantic import BaseModel, Field
class LoginRequest(BaseModel):
username: str = Field(..., max_length=64)
password: str = Field(..., max_length=128)
class DataScopeInfo(BaseModel):
"""业务分组数据范围 —— 供前端展示「我为什么只看到这些」。
空范围is_empty必须让用户看得见否则他会以为系统坏了
列表全空却没有任何提示,是最难排查的一类反馈。
"""
is_unrestricted: bool = False # True = 全厂(超管 / 未分组主管)
is_empty: bool = False # True = 未分组,看不到任何数据
phases: list[str] = [] # 原始阶段码,如 ["PRODUCTION"]
phase_labels: list[str] = [] # 中文标签 —— 服务端下发,避免前端再抄一份词表
groups: list[str] = [] # 所属组的显示名
is_leader: bool = False # 是否至少是一个组的组长
reason: str = "" # super_admin / grouped / supervisor_default / ungrouped
class UserResponse(BaseModel):
id: str
username: str
display_name: str
role: str
is_active: bool = True
created_at: str | None = None
# 业务分组数据范围。只有 /auth/me 会填(登录接口不查库),故为可选,
# 老的客户端读不到这个字段也不受影响。
scope: DataScopeInfo | None = None
model_config = {"from_attributes": True}
class LoginResponse(BaseModel):
access_token: str
refresh_token: str
token_type: str = "bearer"
user: UserResponse
class RefreshRequest(BaseModel):
refresh_token: str = Field(..., description="Refresh Token")
class RefreshResponse(BaseModel):
access_token: str
token_type: str = "bearer"