2026-09-21 16:10:52 +08:00
|
|
|
|
"""物料选择器 — 读 MOM material_base,按 category 手风琴分组(仅本部门)"""
|
2026-09-21 15:56:52 +08:00
|
|
|
|
from fastapi import APIRouter, Query, HTTPException, status, Depends
|
|
|
|
|
|
from pydantic import BaseModel
|
2026-09-21 16:10:52 +08:00
|
|
|
|
from app.core.config import settings
|
2026-09-21 15:56:52 +08:00
|
|
|
|
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=["物料选择"])
|
|
|
|
|
|
|
2026-09-21 16:10:52 +08:00
|
|
|
|
# 部门隔离:只放行本部门 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}%"
|
|
|
|
|
|
|
2026-09-21 15:56:52 +08:00
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 响应模型
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
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 分组汇总,前端渲染手风琴外层。
|
2026-09-21 16:10:52 +08:00
|
|
|
|
只返回本部门(ORG_DEPARTMENT)名下的分类。
|
2026-09-21 15:56:52 +08:00
|
|
|
|
"""
|
|
|
|
|
|
db = MomSessionLocal()
|
|
|
|
|
|
try:
|
|
|
|
|
|
if keyword.strip():
|
|
|
|
|
|
sql = text("""
|
|
|
|
|
|
SELECT category, COUNT(*) AS count
|
|
|
|
|
|
FROM material_base
|
|
|
|
|
|
WHERE is_enabled = TRUE
|
2026-09-21 16:10:52 +08:00
|
|
|
|
AND category LIKE :cat_prefix
|
2026-09-21 15:56:52 +08:00
|
|
|
|
AND (name ILIKE :kw OR spec_model ILIKE :kw)
|
|
|
|
|
|
GROUP BY category
|
|
|
|
|
|
ORDER BY category
|
|
|
|
|
|
""")
|
2026-09-21 16:10:52 +08:00
|
|
|
|
result = db.execute(
|
|
|
|
|
|
sql, {"cat_prefix": CATEGORY_PREFIX_LIKE, "kw": f"%{keyword.strip()}%"}
|
|
|
|
|
|
)
|
2026-09-21 15:56:52 +08:00
|
|
|
|
else:
|
|
|
|
|
|
sql = text("""
|
|
|
|
|
|
SELECT category, COUNT(*) AS count
|
|
|
|
|
|
FROM material_base
|
|
|
|
|
|
WHERE is_enabled = TRUE
|
2026-09-21 16:10:52 +08:00
|
|
|
|
AND category LIKE :cat_prefix
|
2026-09-21 15:56:52 +08:00
|
|
|
|
GROUP BY category
|
|
|
|
|
|
ORDER BY category
|
|
|
|
|
|
""")
|
2026-09-21 16:10:52 +08:00
|
|
|
|
result = db.execute(sql, {"cat_prefix": CATEGORY_PREFIX_LIKE})
|
2026-09-21 15:56:52 +08:00
|
|
|
|
|
|
|
|
|
|
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 下的物料条目,前端展开手风琴时懒加载。
|
2026-09-21 16:10:52 +08:00
|
|
|
|
|
|
|
|
|
|
这里同样要加部门前缀条件(纵深防御):`category` 完全由客户端提供,
|
|
|
|
|
|
只靠 `category = :cat` 精确匹配的话,构造一个跨部门的 category 就能
|
|
|
|
|
|
把别的部门的物料捞出来。
|
2026-09-21 15:56:52 +08:00
|
|
|
|
"""
|
|
|
|
|
|
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
|
2026-09-21 16:10:52 +08:00
|
|
|
|
AND category LIKE :cat_prefix
|
2026-09-21 15:56:52 +08:00
|
|
|
|
AND category = :cat
|
|
|
|
|
|
AND (name ILIKE :kw OR spec_model ILIKE :kw)
|
|
|
|
|
|
ORDER BY name
|
|
|
|
|
|
LIMIT :lim
|
|
|
|
|
|
""")
|
|
|
|
|
|
result = db.execute(
|
2026-09-21 16:10:52 +08:00
|
|
|
|
sql,
|
|
|
|
|
|
{
|
|
|
|
|
|
"cat_prefix": CATEGORY_PREFIX_LIKE,
|
|
|
|
|
|
"cat": category,
|
|
|
|
|
|
"kw": f"%{keyword.strip()}%",
|
|
|
|
|
|
"lim": limit,
|
|
|
|
|
|
},
|
2026-09-21 15:56:52 +08:00
|
|
|
|
)
|
|
|
|
|
|
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
|
2026-09-21 16:10:52 +08:00
|
|
|
|
AND category LIKE :cat_prefix
|
2026-09-21 15:56:52 +08:00
|
|
|
|
AND category = :cat
|
|
|
|
|
|
ORDER BY name
|
|
|
|
|
|
LIMIT :lim
|
|
|
|
|
|
""")
|
2026-09-21 16:10:52 +08:00
|
|
|
|
result = db.execute(
|
|
|
|
|
|
sql, {"cat_prefix": CATEGORY_PREFIX_LIKE, "cat": category, "lim": limit}
|
|
|
|
|
|
)
|
2026-09-21 15:56:52 +08:00
|
|
|
|
|
|
|
|
|
|
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()
|