"""物料选择器 — 读 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 前缀(IRIS/…)。 # # ⚠️ 必须用**前缀** LIKE,不能反推成 ILIKE '%IRIS%':两套实例共用同一个 MOM 库, # LICA 的物料是 `LICA/<中文>`(LICA/生产配件 687、LICA/销售产品 89、 # LICA/维修服务 16 …),而 IRIS 分类树里另有 `IRIS/成品/LICA/…` # (野外便携 59 / 无人机 38 / 实验室内 34 / 高塔监测 30,共 171 条)—— # 那是**挂在 IRIS 名下、给 LICA 做的成品**,本来就属于本部门。 # 前缀匹配天然把前者排除、把后者包含,不需要再加特例。 # # 另注:IRIS 的分类是多段式(`IRIS/半成品/无人机U`、`IRIS/原材料/光学/光电Opt1`), # 拿「成品/半成品」这类类型词过滤没有意义,一律走前缀。 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 HTTPException: raise 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 HTTPException: raise except Exception as e: raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail=f"MOM material_base 查询失败: {str(e)}", ) finally: db.close()