Files
track-LICA/backend/app/api/v1/endpoints/materials.py
duxingchen 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

164 lines
5.6 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.

"""物料选择器 — 读 MOM material_base按 category 手风琴分组(仅本部门)"""
from fastapi import APIRouter, Query, HTTPException, status, Depends
from pydantic import BaseModel
from app.core.config import settings
from app.core.mom_database import MomSessionLocal
from app.services.auth_service import get_current_user
from sqlalchemy import text
router = APIRouter(prefix="/materials", tags=["物料选择"])
# 部门隔离:只放行本部门 category 前缀LICA/…)。
#
# ⚠️ 绝不能用 ILIKE '%LICA%' 之类的模糊匹配MOM 里存在 171 条
# `IRIS/成品/LICA/...`(无人机 38 / 野外便携 59 / 高塔监测 30 / 实验室内 34
# 等),模糊匹配会把 IRIS 的物料漏给 LICA。
# 实测 `category LIKE 'LICA/%'` → 795 条 / 5 个分组。
#
# 另注LICA 的分类是 `LICA/<中文>` 两段式,不含「成品/半成品」字样,
# 所以不能用类型词过滤(那会返回 0 条)。
CATEGORY_PREFIX_LIKE = f"{settings.MATERIAL_CATEGORY_PREFIX}%"
# ============================================================
# 响应模型
# ============================================================
class MaterialGroup(BaseModel):
category: str
count: int
class MaterialItem(BaseModel):
id: int
name: str
spec: str
category: str
type: str
unit: str
is_enabled: bool
# ============================================================
# 端点
# ============================================================
@router.get("/groups", response_model=list[MaterialGroup])
def get_material_groups(
keyword: str = Query("", description="搜索(按名称/规格)"),
current_user: dict = Depends(get_current_user),
):
"""
按 category 分组汇总,前端渲染手风琴外层。
只返回本部门ORG_DEPARTMENT名下的分类。
"""
db = MomSessionLocal()
try:
if keyword.strip():
sql = text("""
SELECT category, COUNT(*) AS count
FROM material_base
WHERE is_enabled = TRUE
AND category LIKE :cat_prefix
AND (name ILIKE :kw OR spec_model ILIKE :kw)
GROUP BY category
ORDER BY category
""")
result = db.execute(
sql, {"cat_prefix": CATEGORY_PREFIX_LIKE, "kw": f"%{keyword.strip()}%"}
)
else:
sql = text("""
SELECT category, COUNT(*) AS count
FROM material_base
WHERE is_enabled = TRUE
AND category LIKE :cat_prefix
GROUP BY category
ORDER BY category
""")
result = db.execute(sql, {"cat_prefix": CATEGORY_PREFIX_LIKE})
rows = result.fetchall()
return [MaterialGroup(category=row.category, count=row.count) for row in rows]
except Exception as e:
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=f"MOM material_base 查询失败: {str(e)}",
)
finally:
db.close()
@router.get("/items", response_model=list[MaterialItem])
def get_material_items(
category: str = Query(..., description="物料分类"),
keyword: str = Query("", description="分组内搜索"),
limit: int = Query(500, ge=1, le=9999),
current_user: dict = Depends(get_current_user),
):
"""
获取指定 category 下的物料条目,前端展开手风琴时懒加载。
这里同样要加部门前缀条件(纵深防御):`category` 完全由客户端提供,
只靠 `category = :cat` 精确匹配的话,构造一个跨部门的 category 就能
把别的部门的物料捞出来。
"""
db = MomSessionLocal()
try:
if keyword.strip():
sql = text("""
SELECT id, name, spec_model AS spec, category, material_type AS type,
COALESCE(unit, '') AS unit, is_enabled
FROM material_base
WHERE is_enabled = TRUE
AND category LIKE :cat_prefix
AND category = :cat
AND (name ILIKE :kw OR spec_model ILIKE :kw)
ORDER BY name
LIMIT :lim
""")
result = db.execute(
sql,
{
"cat_prefix": CATEGORY_PREFIX_LIKE,
"cat": category,
"kw": f"%{keyword.strip()}%",
"lim": limit,
},
)
else:
sql = text("""
SELECT id, name, spec_model AS spec, category, material_type AS type,
COALESCE(unit, '') AS unit, is_enabled
FROM material_base
WHERE is_enabled = TRUE
AND category LIKE :cat_prefix
AND category = :cat
ORDER BY name
LIMIT :lim
""")
result = db.execute(
sql, {"cat_prefix": CATEGORY_PREFIX_LIKE, "cat": category, "lim": limit}
)
rows = result.fetchall()
return [
MaterialItem(
id=row.id,
name=row.name,
spec=row.spec,
category=row.category,
type=row.type,
unit=row.unit,
is_enabled=row.is_enabled,
)
for row in rows
]
except Exception as e:
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=f"MOM material_base 查询失败: {str(e)}",
)
finally:
db.close()