Files
track-LICA/AGENTS.md

157 lines
10 KiB
Markdown
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.

# AGENTS.md
本仓库Track 生产流转系统)的工作笔记。仅在验证过之后才写入,避免传谣。
> ## ⚠️ 本仓库是 **LICA 部门实例**
>
> 派生自 IRIS 实例(`git.iris-rs.cn/duxingchen/track.git` 的
> `feature/ai-audit-update` 分支 @ `192c8ee`)。两套系统在**同一台机器**上并行
> 运行、共用 MOM 主数据,但人员、物料、业务数据**完全隔离**。
>
> | | IRIS 实例 | LICA 实例(本仓库) |
> |---|---|---|
> | 代码目录 | `/home/yueli/track` | `/home/yueli/track-lica` |
> | 容器 | `track_db` / `track_backend` / `track_frontend` | `lica_db` / `lica_backend` / `lica_frontend` |
> | compose 项目 | `track` | `track-lica` |
> | PC 管理端 | 8010 | **8030** |
> | 后端 API | 8011 | **8031** |
> | 数据库 | 8012 | **8032** |
> | 数据卷 | `track_pgdata` | `lica_pgdata` |
> | 部门过滤 | `IRIS` | `LICA` |
>
> **两条红线:**
> 1. **永远不要在 `/home/yueli/track` 目录下执行 `docker compose down -v`** ——
> 那会删掉 IRIS 的生产数据。两个卷相互独立,在 `track-lica` 下执行不影响 IRIS。
> 2. 改后端地址时别只改一半PC 端走 `docker-compose.yml` 的 `CORS_ORIGINS`
> 移动端走 `track-uniapp/src/utils/config.js`uni-app 地址的**唯一来源**
> 和 `track-uniapp/.env`。只改一处会出现「接口通了但图片/上传 404」。
## 架构速览
- `backend/` FastAPI + SQLAlchemy 2.x(async) + AlembicPostgreSQL。
- `frontend/` React 19 + Vite + antd + Tailwind。路由见 `src/App.tsx`
管理端菜单见 `src/components/layout/AdminLayout.tsx``MENU` 数组)。
- 登录不走 Track 自己的用户表,而是**只读** MOM(KCGL) 的 `sys_user`
`sys_user.username``"真实姓名/登录账号"``login()`
`WHERE username LIKE '%/<账号>'` 匹配,`display_name``/` 拆解得到。
MOM 连接配置在 `app/core/mom_database.py`(同步 psycopg2 引擎)。
- **组织隔离**:本实例只服务 LICA 部门,唯一开关是 `app/core/config.py`
`ORG_DEPARTMENT`(对应 MOM `sys_user.department`)与
`MATERIAL_CATEGORY_PREFIX`(对应 MOM `material_base.category` 前缀)。
过滤点共 4 处:登录 `auth_service.login`、人员列表 `endpoints/users.py`
物料 `endpoints/materials.py``groups`/`items`
人员操作统计 `dashboard_service.get_user_operations`
- **登录**是四个过滤点里唯一区分角色的普通角色INBOUND / SUPERVISOR /
WAREHOUSE_MGR / SALES必须严格属于本部门否则一律 401
**但 `SUPER_ADMIN` 跨部门放行** —— IRIS 的超管也能登 LICA 实例,
供运维/管理员在两个实例之间切换。角色常量取自 `app/core/roles.py`
不要写裸字符串 `'SUPER_ADMIN'`
(这条曾经被改错过一次:把豁免去掉导致 IRIS 超管登不进 LICA。
- 人员与物料都是**服务端钉死**部门,客户端传什么部门参数都不采纳
(这样 uni-app 里写死的 `dept` 不会造成跨部门影响)。
- 刻意**不做**「查询失败退回全表」的降级 —— 那是跨部门数据泄漏,
宁可查不出,不可查过头。
- ⚠️ 物料必须用前缀 `category LIKE 'LICA/%'`**不能用** `ILIKE '%LICA%'`
MOM 里存在 171 条 `IRIS/成品/LICA/...`(无人机/野外便携/高塔监测等),
模糊匹配会把 IRIS 的物料漏给 LICA。已实测前缀匹配命中 795 条 / 5 个分组。
- **业务分组数据权限**2026-09 新增,是**部门隔离之内**的第二层)
LICA 部门内部再分组(生产大组 / 维修大组,各自可建小组),组对应
`lifecycle_phase`,成员只看本组阶段的数据。组存在 Track 库(`business_groups`
/ `business_group_phases` / `business_group_members`),人在 MOM。
**唯一判定来源**`app/services/data_scope_service.py``DataScope`
- `scope.product_where()` —— 主体是 Product 的语句用
- `scope.task_where()` —— 主体是 Task 的语句用,**前置条件:调用方必须已
`join(Product, Task.product_id == Product.id)`**tasks 表没有 lifecycle_phase
- ⚠️ **业务代码里不准再手写 `lifecycle_phase` 过滤**。评审检查:
`grep -rn "lifecycle_phase.in_" backend/app/` 应只命中 `data_scope_service.py`
- ⚠️ 两条红线:`None`(不限) 与 `frozenset()`(空) 语义相反,不共用哨兵值;
空集合必须显式 `false()` —— SQLAlchemy 对 `in_(())` 生成 `IN (NULL)`
一旦退化成不过滤就是全量泄漏
解析优先级(`resolve_data_scope`
1. `SUPER_ADMIN` → 永远全厂(硬编码,不可被分组覆盖)
2. 被显式分进组 → 该组生效(**分组优先于角色**SUPERVISOR 被分组也会受限)
3. 未分组的 `SUPERVISOR` → 默认全厂
4. 未分组的普通用户 → 按 `DATA_SCOPE_UNGROUPED`(见下)
**过渡开关** `config.DATA_SCOPE_UNGROUPED`
- `"ALL"`(当前默认)= 未分组用户先放行,同时打 WARNING 记录「谁还没分组」
- `"NONE"` = 目标态:未分组 = 看不到任何数据(列表返回 `[]`,不是 403
- 直接上 NONE 会让所有未分组工人当场看不到自己的任务、现场停摆。
推荐节奏ALL 上线 → 看日志收集名单 → 建组配人 → 改 NONE 重启。
**其它约定**
- 扫码 `get_product_by_serial` **刻意不过滤**(现场要能判断设备走没走错流程),
理由写死在该函数 docstring 里;「不能操作」由 `_check_permission` 保证。
- 分组**管理**仅 `SUPER_ADMIN``require_roles(SUPER_ADMIN)`,不能用
`require_admin`)。原因:被分组的主管若能把人移出组,就等于获得提权路径。
- 组支持两级(`parent_id`),子组不配范围则继承父组。
- 删组**仅限空组** —— 级联删是静默的批量权限变更。
- 统计接口dashboard/analytics/screen 共 20 个路由)**已从匿名可访问变为需登录**
并受范围过滤。注意:大屏挂满 7 天后 refresh token 过期会跳登录页。
## 本地起环境(关键,踩过的坑都在这)
1. **本机没有 Postgres 时需要先装**(容器内 `sudo` 可用):
`sudo -n apt-get install -y --fix-missing postgresql postgresql-contrib`
然后 `sudo -n pg_ctlcluster 17 main start`
本仓库不使用 pgvector无需额外扩展。
2. **数据库端口与生产默认值不同**,必须用环境变量覆盖:
- `DATABASE_URL=postgresql+asyncpg://lica:<密码>@127.0.0.1:8032/lica_production`
- `MOM_DB_HOST=inventory_db``MOM_DB_PORT=5432`(容器内走 projects_default
网络的服务名;从宿主机连则用 `127.0.0.1:5435`
- `SECRET_KEY=<≥32 字符>``DEBUG=false` 时配置项会**拒绝**默认 SECRET_KEY
(见 `app/core/config.py` 的校验),不设会直接 import 失败。
3. 迁移:`cd backend && python3 -m alembic upgrade head`(没有全局 `alembic` 命令,
要用 `python3 -m alembic`)。校验纯 SQL 用 `alembic upgrade head --sql`
4. 前端 proxy 指向 Docker 服务名 `lica_backend:8000`(本实例的服务名**刻意不叫**
`backend`,避免在 `projects_default` 网络上与 IRIS 实例重名,否则将来任何一方
`http://backend:8000` 会随机打到另一个部门)。本机跑要么把
`127.0.0.1 lica_backend` 写进 `/etc/hosts`,要么直接给
`VITE_API_BASE_URL=http://localhost:<port>/api/v1` 绕过 proxy。
注意 dev server 由 `basicSsl` 起 HTTPS —— **必须用 `https://` 访问,
`http://` 会直接连不上curl 报 000**。跨域需要后端
`CORS_ORIGINS` 加上 `https://localhost:1420`
## 测试的坑(重要)
- **不要用 `starlette.testclient.TestClient` 测异步 SQLAlchemy 应用。**
它每个请求新建事件循环,而引擎是模块级单例、池里挂着 asyncpg 连接,
跨循环复用会报 `got Future attached to a different loop`,表现为随机 500。
正确做法:`httpx.AsyncClient(transport=httpx.ASGITransport(app=app))`
并在单个 `asyncio.run()` 里跑完全部请求。生产 uvicorn 单循环无此问题。
- 仓库目前**没有** pytest 基建,也没有前端测试脚本。
## 已知的待修问题(继承自 IRIS 侧 `192c8ee`,本仓库**未修**
- **读接口大面积未鉴权**(已实测,非推测):无 token 直接 200 的包括
`/api/v1/users/`、全部 `/api/v1/dashboard/*`(含
`people-history/export` —— 匿名即可批量导出个人工时台账)、
`/api/v1/analytics/*``/api/v1/screen/*``/api/v1/orders/`
写操作和 `/api/v1/tasks``/api/v1/products` 是有鉴权的。
新增接口请统一用 `app/core/deps.py``require_admin` / `require_roles`
- **对 LICA 的影响面**`/api/v1/users/` 虽无鉴权,但部门过滤在 SQL 层钉死,
实测无 token 也只能拿到 19 个 LICA 人员,**不会泄露 IRIS 的人**
其余匿名接口读的是本实例自己的库,跨不到 IRIS 库。真正的风险是
「本部门内部」的越权(如普通操作员能看管理端统计),不是跨部门。
- `AdminLayout` 只判登录不判角色LICA 的 15 个 `INBOUND` 账号能进 PC 管理端。
- `dashboard_service.py` 查 tasks 时用**小写** `pending/in_progress/completed`
`Task.status` 实际存大写(`PENDING`/`WIP`/`COMPLETED`
导致管理端「待接收/进行中/已完成」三个数字恒为 0。
(注意 `Product.status` 确实是小写,修的时候别一起改。)
- 「管理员角色」这份规则此前散在 4 处(后端 `task_service``products.py` 内联、
前端 `constants/task.ts``AdminProductsPage` 内联),已因此出过事故。
**后端唯一事实来源是 `app/core/roles.py`,前端用 `constants/task.ts::isAdminRole`。**
新增判断不要手写 `===` 比较。
- `task_logs.task_id` 是 NOT NULL 外键,只能挂任务,不是通用审计。通用审计是
`audit_logs`(本轮新增,由 `app/core/audit_middleware.py` 自动采集)。
## 约定
- 时间统一北京时间(`app/core/time_utils.py`),库里存 timestamptz。
- 中文枚举标签尽量由服务端下发(如审计接口的 `module_label`/`action_label`
避免前端再抄一份映射开始漂移。
- 本仓库的提交信息用中文,说明「为什么」而非「改了什么」。