Files
track/AGENTS.md
duxingchen e45c97bd1f feat: 组织隔离(IRIS 单实例)与出料功能基础
本轮之前累积的未提交工作,一并固化:

- 组织隔离:同一份代码部署给不同部门只需改 config 的 ORG_DEPARTMENT 与
  MATERIAL_CATEGORY_PREFIX。过滤点在登录/人员列表/物料/MOM 出库单四处,
  全部服务端钉死,客户端传什么都放不大。
  ★ 物料必须用**前缀** LIKE,不能反推成 ILIKE '%IRIS%':MOM 里 LICA 的物料是
  `LICA/<中文>`,而本部门分类树里另有 `IRIS/成品/LICA/…`(本就属于本部门),
  前缀匹配天然区分得开。
- MOM 出库单只读查询(直连 MOM 库):不走 MOM 现成的 /outbound 接口 ——
  那个要 JWT + permission_required,且对非特权账号按 consumer_name 做行级
  隔离,服务账号只能拿到自己名下的单。分页必须两段式(先按单号 GROUP BY
  分页,再 IN 捞明细),对宽表直接分页会得到明细行数而不是单据数。
- 出料功能:产品 ↔ 出库单存档(product_outbounds)与任务 ↔ 出库明细
  (task_outbound_materials),供「这台设备对应 MOM 哪张单」的展示。
  ⚠️ 快照一律由后端拿 ID 去 MOM 现查,不接受前端传入,否则前端可伪造单据。
2026-09-23 15:18:06 +08:00

8.9 KiB
Raw Blame History

AGENTS.md

本仓库Track 生产流转系统)的工作笔记。仅在验证过之后才写入,避免传谣。

架构速览

  • backend/ FastAPI + SQLAlchemy 2.x(async) + AlembicPostgreSQL。
  • frontend/ React 19 + Vite + antd + Tailwind。路由见 src/App.tsx 管理端菜单见 src/components/layout/AdminLayout.tsxMENU 数组)。
  • 登录不走 Track 自己的用户表,而是只读 MOM(KCGL) 的 sys_user sys_user.username"真实姓名/登录账号"login()WHERE username LIKE '%/<账号>' 匹配,display_name/ 拆解得到。 MOM 连接配置在 app/core/mom_database.py(同步 psycopg2 引擎)。

组织隔离2026-09 新增)

本实例只服务 IRIS 部门,开关是 app/core/config.py 的两个值:ORG_DEPARTMENT (对 MOM sys_user.department)与 MATERIAL_CATEGORY_PREFIX(对 MOM material_base.category 前缀)。同一份代码部署给别的部门只需改这两处。

过滤点共四处,全部服务端钉死,客户端传什么参数都不采纳:

位置 过滤条件
登录 auth_service.login department = ORG_DEPARTMENT OR role = SUPER_ADMIN
人员列表 endpoints/users.py department = ORG_DEPARTMENT
物料 endpoints/materials.py category LIKE 'IRIS/%'groupsitems 都要加)
MOM 出库单 services/mom_outbound_service.py 同上前缀 + 跨部门领用人例外(见下)
  • 登录是四处里唯一区分角色的SUPER_ADMIN 跨部门放行IRIS 超管也能登 LICA 实例反之亦然供运维在两个实例之间切换。其余角色INBOUND / SUPERVISOR / WAREHOUSE_MGR / SALES必须严格属于本部门。
  • ⚠️ 物料必须用前缀 LIKE 'IRIS/%',不能反推成 ILIKE '%IRIS%'MOM 里 LICA 的物料是 LICA/<中文>(生产配件 687 / 销售产品 89 / 维修服务 16… 而本部门分类树里另有 IRIS/成品/LICA/…(野外便携 59 / 无人机 38 / 实验室内 34 / 高塔监测 30共 171 条)—— 那是挂在 IRIS 名下、给 LICA 做的 成品,本来就属于本部门。前缀匹配天然把前者排除、后者包含,不需要特例。
  • 刻意不做「查询失败退回全表」的降级 —— 那是跨部门数据泄漏。宁可查不出, 不可查过头。
  • 已实测:人员列表 20 人IRIS 部门)、物料 100 个分类 / 2248 条、 LICA/IRIS/ 前缀交叉命中 0 条4 个 LICA 普通账号全部登不进来, 2 个超管(含 LICA 的)正常放行。

出库单的跨部门领用人例外

config.EXTRA_VISIBLE_CONSUMERS(默认 依锐思,石利LICA)里的领用人,其在 MOM trans_outbound.consumer_name 名下的单据,即使物料分类不属于本部门也放行 —— 他们跨两个部门领料,只按物料前缀过滤会把他们的单整批漏掉。

⚠️ 这是放行条件SQL 里是 OR),与界面筛选(AND,只收窄)方向相反, 两者的集合运算在 mom_outbound_service 里必须分开写。名单为空时整段不拼, 退化成纯前缀过滤。已实测:空白名单 438 单 → 填入一个真实跨部门领用人后 440 单, 填入不存在的名字仍是 438 单(不放大范围)。

出料功能2026-09 新增)

回答两个问题:这台设备对应 MOM 的哪张出库单这个任务用了哪些出库物料

  • product_outbounds —— 产品 ↔ 出库单存档。两条写入路径共用一张表,靠 source 区分:webhookMOM 出库回调自动存档)/ manual(人在界面上挂的)。 一次出库一行;撤回只置 is_revoked 不删行(「出过又撤了」也是历史)。 唯一约束是 (serial_number, outbound_no) 而非只约束单号 —— MOM 的批量出库 是多个商品共用一个单号,只约束单号会把正常的批量单误杀。
  • task_outbound_materials —— 任务挂载的出库物料,明细级快照(一行 = MOM trans_outbound 的一行。为什么存快照而不只存单号MOM 的物料名/规格要经 COALESCE 三表 JOINstock_buy/stock_semi/stock_productmaterial_base 才能解析Track 跨库 JOIN 不了,只存单号则 MOM 一挂就看不到已挂内容。 纯引用不记用量quantity 是出库单原值,不是本任务用量)。
  • mom_outbounds.py / mom_outbound_service.py —— 直连 MOM 库的只读查询,供选择器 搜索用。不走 MOM 现成的 GET /api/v1/outbound:那个接口要 JWT + permission_required,且对非特权账号按 consumer_name 做行级隔离Track 用 服务账号调只能拿到该账号名下的单,不是全量。分页必须两段式(先 GROUP BY outbound_no 分页拿单号,再 WHERE outbound_no IN (…) 捞明细), 绝不能对 join 后的宽表直接分页 —— 那是明细行数不是单据数。
  • ⚠️ 快照一律由后端拿 ID 去 MOM 现查,不接受前端传入,否则前端可伪造单据。
  • ⚠️ 写挂载行时不要 append 到 ORM 集合(task.outbound_materials):集合在 flush 后处于「未加载」态,碰它会触发懒加载,异步 session 下直接抛 MissingGreenlet。只写 FK响应构造前走一次真正的查询。

与 LICA 实例(~/track-lica)的差异LICA 那边出库单还要按业务分组数据 范围再收敛一层(范围 ∩ 组 ∩ 个人),本实例没有分组体系,故 mom-outbounds 没有 group_id 参数,挂载时也不做额外的可见性校验。不要为了「对齐」加回来 —— 那会引入一份没有数据支撑的过滤。

⚠️ 两套实例共用同一个 MOM 库,但数据卷相互独立。永远不要在 /home/yueli/track 下执行 docker compose down -v

本地起环境(关键,踩过的坑都在这)

  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://track:track_prod_2026@127.0.0.1:5432/track_production
    • MOM_DB_HOST=127.0.0.1MOM_DB_PORT=5432
    • 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 服务名 backend:8000。本机跑要么把 127.0.0.1 backend 写进 /etc/hosts,要么直接给 VITE_API_BASE_URL=http://localhost:<port>/api/v1 绕过 proxy。 注意 dev server 由 basicSsl 起 HTTPS跨域需要后端 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 基建,也没有前端测试脚本。

已知的待修问题(截至 1.0应用 分支)

  • 读接口大面积未鉴权(已实测,非推测):无 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.pyrequire_admin / require_roles
  • 「管理员角色」这份规则此前散在 4 处(后端 task_serviceproducts.py 内联、 前端 constants/task.tsAdminProductsPage 内联),已因此出过事故。 后端唯一事实来源是 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 避免前端再抄一份映射开始漂移。
  • 本仓库的提交信息用中文,说明「为什么」而非「改了什么」。