Commit Graph

10 Commits

Author SHA1 Message Date
9598c3fb9f feat(分组): 成员改为标签云展示 + 添加成员支持多选批量
1) 成员不再一人一行
   原先用 antd Table:成员一多就要滚很久,右侧还大片留白。
   改为标签云(flex-wrap),一行能放好几个。
   交互一并简化:
     · 点标签正文 → 切换组长(悬停有提示)
     · 点 ×     → 移出分组
     · 无权限时两者都不出现,退化成纯展示
   组长用金色标签 + 皇冠图标,一眼可辨。

2) 添加成员支持多选
   Select 加 mode="multiple" + maxTagCount="responsive",可搜可多选、批量提交。
   后端 POST /groups/{id}/members 由单个 user_id 改为 user_ids 数组,
   返回 {added, skipped, skipped_names}:
     · 已在组内的**静默跳过**而非整批 409 —— 多选时难免选中已在组里的人,
       为此让整批失败体验很差;前端会把跳过的名单说清楚,
       否则用户会疑惑「明明选了 5 个,怎么只多了 3 个」
     · 批量里混入超管则**整批拒绝并指名道姓**,不制造「部分成功」这种
       难以解释的中间状态

实测(后端):
  批量加 3 人           → added 3
  重复提交(2 旧 1 新) → added 1 / skipped 2,整批仍成功
  混入超管              → 400「以下账号是超级管理员,业务分组对其不生效:sunxia」
  空列表                → 422
  测试成员已清理
2026-09-21 17:44:06 +08:00
5095756571 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(对照组正常)
2026-09-21 17:41:17 +08:00
15097baa2b feat(权限): 业务分组对全员开放(操作分层)+ 操作审计仅超管可见
需求:业务分组开放给所有人,主管可操作、其余人只读;操作审计前端不显示。

⚠️ 这里有个必须收窄的边界:按数据范围规则,被分进组的 SUPERVISOR 会从
「全厂」降级为只看本组。若允许他改可见范围,他把自己那组改成「生产+售后」
就恢复全厂视野;若允许建组,他新建一个组再把自己塞进去,同样绕过。
**「能管成员」与「能配范围」必须分开** —— 前者安全(组长给自己加组会被
唯一约束挡住、移出自己只是失去权限),后者是提权入口。

因此分层如下:

  | 谁               | 看           | 改                                 |
  |------------------|--------------|------------------------------------|
  | SUPER_ADMIN      | 所有组       | 全部(建/改/删组、配范围、管成员) |
  | 主管 / 本组组长  | 自己所属的组 | **仅本组成员**(加人/移人/设组长) |
  | 普通成员         | 自己所属的组 | 无(只读)                         |

后端(groups.py):
· 去掉路由级的 require_roles(SUPER_ADMIN),改为逐端点校验
· list_groups / get_group:非超管只返回自己所属的组;看别组详情 403
  (连「这个组存在但你没份」都不暴露,避免被拿来推测组织架构)
· create/update/delete:_require_super —— 这三个 + 配范围是提权入口
· add/update/remove member:_can_manage_members(超管 / 主管 / 本组组长)
· GroupOut 新增 can_manage_members / can_manage_group / is_my_leader
  —— 由服务端算好下发,前端不按 role 自行推导(组长身份是按组算的)

前端:
· AdminGroupsPage 去掉整页超管门禁,改为按能力显示按钮:
  无权限时「组长」列退化成纯展示而非可点按钮
  没有任何组时给「你还没有被分配到任何业务分组」的引导提示
· AdminLayout 的 MENU 支持 superOnly 标记,「操作审计」只对超管显示
  (后端 /audit/* 本就挂了 require_admin,这里只是不给入口,
   否则普通用户点进去只会看到一堆 403)

实测(四类账号):
  建组/改范围:仅超管通过,其余全 403
  管成员:超管/主管/本组组长通过;非本组 403;跨组组长 403
  可见性:主管未分组看到 0 个组、维修组长只看到维修大组、生产组员只看到生产小组
  详情页能力标记正确下发;越权查别组详情 403
2026-09-21 17:38:07 +08:00
394e1e39c3 feat(分组权限): 统计接口接入数据范围(dashboard / analytics / screen 共 20 个路由)
这三组接口此前是「上帝视角」且**匿名可访问**,现在全部挂 get_data_scope ——
既要求登录、又按业务分组范围过滤。顺带堵上了 AGENTS.md 点名的风险:
/dashboard/people-history/export 此前匿名即可批量导出全员工时台账。

dashboard(11 个函数 / 24 处注入)
· Product 主体 → product_where();Task、TaskLog 主体 → 先 join(Product) 再 task_where()
· 「卡片数字」与「下钻明细」成对出现的地方用同一谓词,避免「按钮显示 2、点开却是 0 条」
· get_user_operations 的 func.count() 改为 func.count(TaskLog.id) —— 显式化,不依赖
  join 形状(当前是 many-to-one 不会放大,但这样写更稳)
· unread_notif **刻意不过滤**:Notification.task_id 可空,按 Product 过滤会漏掉
  无任务关联的提醒(就地注释说明)
· get_my_stats 本轮不动 —— 它按本人归因,语义上不受分组影响

analytics(4 个函数 / 9 条语句)
· get_analytics_options 的 4 条独立语句全部处理 —— 它是筛选栏下拉的选项源,
  不过滤的话维修组能在下拉里看到生产组的人(最易漏的一处)
· get_device_records 的 product_id 查号是安全闸:范围外 SN 查不出 → 直接返回 []

screen(3 个函数)
· month_production **不做特判** —— 维修组的「本月生产数」本来就该是 0
· get_wip_distribution 按范围裁剪工序柱子,但坚持「恒 0 才裁、有数必现」,
  保证 total == sum(items) 在任何 scope 下都成立

实测(17 个端点):超管全部 200、匿名全部 401。
造 1 生产 + 1 售后产品后:
  超管   products=2 / wip-matrix 2 行 / options 2 个型号
  生产组 products=1 / wip-matrix 1 行 / options 1 个型号
  维修组 products=1 / wip-matrix 1 行 / options 1 个型号
列表与统计口径一致;未分组用户在过渡期开关下仍走 ungrouped_fallback。
测试数据已还原。
2026-09-21 17:22:01 +08:00
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
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
f9f3d90f96 fix: 二维码接口去鉴权(修复破图)+ 产品序列号加部门前缀
1) 二维码破图
   /products/qrcode/{sn} 带了 Depends(get_current_user),而前端是用
   <img src="/api/v1/products/qrcode/{sn}"> 引用它的 —— <img> 无法携带
   Authorization 头,请求必然 401,页面上就是破图(同时刷大量 401 审计)。
   该接口不查库、只把调用方传进来的字符串渲染成二维码,没有数据泄露面,
   故去掉鉴权。刻意不做 ?token= 兜底:JWT 进 URL 会渗进访问日志、浏览器
   历史与 Referer,比它想解决的问题更糟。
   实测:HTTP 200 / image/png / 300x300。

2) 序列号部门前缀
   新增 config.SERIAL_PREFIX(LICA 为 "L"),counter_service 生成
   {前缀}{15 位 HEX},总长仍严格 16 位 —— products.serial_number 是
   String(16),前端 TaskTreeViewer / ManualInput / ScanPage 多处按 16 位
   校验,不能改总长。IRIS 实例该值为空串,格式保持原样。
   实测 LICA 生成 L000000000000002。
   前端本就兼容带字母的序列号:TaskTreeViewer 的占位符示例就是
   X20260801000001,ScanPage 的 replace(/[^a-zA-Z0-9]/g,"") 也不会滤掉 L。
2026-09-21 16:34:56 +08:00
a2940d265d fix: 超管跨部门放行 —— 修正登录部门过滤过严
原实现把所有账号一律收敛到 department = ORG_DEPARTMENT,导致 IRIS 的
超级管理员登不进 LICA 实例。需求本意是「超管不拦截,其余角色只能本部门
登录」,实现时把超管的豁免一起去掉了。

改为:
  WHERE username LIKE :pattern
    AND (department = :dept OR role = 'SUPER_ADMIN')

role 取自 app/core/roles.py::SUPER_ADMIN,不新增硬编码字符串。

已用 MOM 真实数据验证:
  duxingchen / zhuxiangning(IRIS 超管)      -> 放行
  xingyouwu / sunxia(LICA 超管)             -> 放行
  weihuijun(LICA 普通)、gaoxue(LICA 主管)  -> 放行
  zhangxinxin(IRIS 普通)、renlixin(IRIS 主管)-> 仍拒
2026-09-21 16:17:52 +08:00
347b7b6a68 feat: LICA 部门独立实例 — 组织隔离与端口/标识改造
派生自 IRIS 实例的 feature/ai-audit-update @ 192c8ee,在同一台机器上独立运行。

隔离机制(开关集中在 app/core/config.py 的 ORG_DEPARTMENT / MATERIAL_CATEGORY_PREFIX):
- 登录:sys_user 查询增加 department 条件,非本部门账号一律 401
- 人员列表:服务端钉死部门、忽略客户端传参;删除「异常退回全表」的降级分支
- 物料:groups 与 items 都增加 category LIKE 'LICA/%' 前缀过滤
- 人员操作统计:把硬编码的 department='IRIS' 改为配置项

物料为什么用前缀而不是 LIKE '%LICA%':
MOM 里存在 171 条 IRIS/成品/LICA/...(无人机/野外便携/高塔监测等),
模糊匹配会把这些 IRIS 物料漏给 LICA。实测前缀匹配命中 795 条 / 5 个分组。

部署隔离:
- 端口 8030/8031/8032,容器名 lica_*,卷 lica_pgdata(与 IRIS 完全独立)
- 服务名改为 lica_backend,避免在 projects_default 网络上与 IRIS 的 backend
  重名 —— 否则将来任何一方写 http://backend:8000 会随机打到另一个部门
- SECRET_KEY 重新生成:实测两边 token 互不通用(双向 401)

客户端标识(不改会导致两个部门的客户端互相覆盖):
- Tauri identifier 改 com.lica.production(否则桌面端互相覆盖安装,且共用
  WebView 数据目录会让 track_admin_token 串号)
- uni-app appid 改 __UNI__D2F4A19(否则同机 APK 互相覆盖、wgt 热更新串号)
- uni-app 地址端口 8011 → 8031(收敛在 utils/config.js 单一来源)
- sync-watch.sh 的 DST 指向 LICA 专属 HBuilderX 目录(否则会把源码灌进 IRIS 工程)

排除项:未复制 deploy.sh / deploy_full.sh / docker-compose.prod.yml ——
它们写死了 IRIS 的生产服务器,误跑会覆盖线上系统。
2026-09-21 16:10:52 +08:00
3286a11bc7 chore: fork from IRIS track 供 LICA 部门独立运行
- 复制来源: /home/yueli/track @ 192c8ee (feature/ai-audit-update)
- 组织隔离目标: LICA
- 端口规划: 前端 8030 / 后端 8031 / 数据库 8032
- 已排除 deploy.sh、deploy_full.sh、docker-compose.prod.yml(IRIS 生产发布脚本)
- 已排除工作区未提交改动,取干净的 192c8ee 状态
2026-09-21 15:56:52 +08:00