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 一致。
This commit is contained in:
@ -10,6 +10,11 @@ from app.models.message import ProductMessage
|
||||
from app.models.holiday import Holiday
|
||||
from app.models.audit_log import AuditLog
|
||||
from app.models.user_daily_seen import UserDailySeen
|
||||
from app.models.business_group import (
|
||||
BusinessGroup,
|
||||
BusinessGroupPhase,
|
||||
BusinessGroupMember,
|
||||
)
|
||||
__all__ = [
|
||||
"Base",
|
||||
"ProductionOrder",
|
||||
@ -23,4 +28,7 @@ __all__ = [
|
||||
"Holiday",
|
||||
"AuditLog",
|
||||
"UserDailySeen",
|
||||
"BusinessGroup",
|
||||
"BusinessGroupPhase",
|
||||
"BusinessGroupMember",
|
||||
]
|
||||
|
||||
112
backend/app/models/business_group.py
Normal file
112
backend/app/models/business_group.py
Normal file
@ -0,0 +1,112 @@
|
||||
"""业务分组 — LICA 部门内部的数据范围隔离(生产大组 / 维修大组)
|
||||
|
||||
系统已有 lifecycle_phase(生产制造 / 售后回流),但此前**只用于展示与校验**,
|
||||
没有任何权限含义。本模块补上「数据范围」这一层:组决定成员能看到哪个阶段的数据。
|
||||
|
||||
为什么要两级(大组 > 小组):
|
||||
生产大组下可能再分「生产小组」「测试小组」,它们看的是同一片数据(都属生产
|
||||
阶段),彼此互通;维修大组则与生产隔离。用 parent_id 表达归属,子组默认继承
|
||||
父组的可见范围 —— 这样「生产大组配一次 PRODUCTION,下面的小组都不用再配」。
|
||||
|
||||
为什么组存在 Track 库而不是 MOM:
|
||||
组是 Track 自己的业务概念(对应 lifecycle_phase),MOM 里没有对应表,且组名要
|
||||
能由业务随时改。只有「人」是 String(64) 逻辑外键指向 MOM sys_user。
|
||||
|
||||
为什么可见范围是独立的 business_group_phases 表而不是一个字段:
|
||||
需求明确要求「范围可配置、不要写死」,且一个组可能同时要看生产和售后 ——
|
||||
单列存不下多选。
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import Base
|
||||
from app.core.time_utils import get_beijing_time
|
||||
|
||||
|
||||
class BusinessGroup(Base):
|
||||
"""业务分组定义(两级:parent_id 为空即大组)"""
|
||||
__tablename__ = "business_groups"
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
primary_key=True, autoincrement=True,
|
||||
comment="分组ID(数字,对外稳定不变;显示名可改而引用不变)",
|
||||
)
|
||||
parent_id: Mapped[int | None] = mapped_column(
|
||||
Integer, ForeignKey("business_groups.id", ondelete="CASCADE"),
|
||||
nullable=True, index=True,
|
||||
comment="上级大组ID(为空=大组;有值=挂在某大组下的小组,默认继承其可见范围)",
|
||||
)
|
||||
name: Mapped[str] = mapped_column(
|
||||
String(50), nullable=False, unique=True, comment="分组显示名(可修改,如 生产小组/维修组)",
|
||||
)
|
||||
description: Mapped[str | None] = mapped_column(
|
||||
String(200), nullable=True, comment="备注说明",
|
||||
)
|
||||
sort_order: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, default=0, server_default="0", comment="同层排序(升序)",
|
||||
)
|
||||
is_active: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, default=True, server_default="true",
|
||||
comment="是否启用。⚠️ 停用后该组所有成员立即退回「未分组」状态(等同被移出所有组)",
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=get_beijing_time, comment="创建时间",
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=get_beijing_time,
|
||||
onupdate=get_beijing_time, comment="最后修改时间",
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<BusinessGroup {self.id} {self.name}>"
|
||||
|
||||
|
||||
class BusinessGroupPhase(Base):
|
||||
"""分组的可见范围 —— 该组成员能看到哪些 lifecycle_phase 的数据。
|
||||
|
||||
⚠️ 这是「不写死」的落点:给不给某个组看售后,完全由管理员在这里配置,
|
||||
不在代码里硬编码任何「生产组只能看生产」这类规则。
|
||||
"""
|
||||
__tablename__ = "business_group_phases"
|
||||
|
||||
group_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("business_groups.id", ondelete="CASCADE"),
|
||||
primary_key=True, comment="分组ID",
|
||||
)
|
||||
phase: Mapped[str] = mapped_column(
|
||||
String(20), primary_key=True,
|
||||
comment="可见的生命周期阶段: PRODUCTION(生产制造) | AFTER_SALES(售后回流)",
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<BusinessGroupPhase g={self.group_id} {self.phase}>"
|
||||
|
||||
|
||||
class BusinessGroupMember(Base):
|
||||
"""分组成员。一人可属多个组 —— 这是「让某人同时看生产+维修」的实现方式。"""
|
||||
__tablename__ = "business_group_members"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("group_id", "user_id", name="uq_business_group_members_group_user"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
group_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("business_groups.id", ondelete="CASCADE"),
|
||||
nullable=False, index=True, comment="所属分组ID",
|
||||
)
|
||||
user_id: Mapped[str] = mapped_column(
|
||||
String(64), nullable=False, index=True,
|
||||
comment="成员账号(逻辑外键→MOM sys_user,与 Task.assignee_id / JWT.username 同口径)",
|
||||
)
|
||||
is_leader: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, default=False, server_default="false",
|
||||
comment="是否组长(可管理本组成员;数据范围与组员相同)",
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=get_beijing_time, comment="加入时间",
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<BusinessGroupMember g={self.group_id} u={self.user_id}>"
|
||||
Reference in New Issue
Block a user