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 的生产服务器,误跑会覆盖线上系统。
This commit is contained in:
@ -1,12 +1,24 @@
|
||||
"""物料选择器 — 读 MOM material_base,按成品/半成品 category 手风琴分组"""
|
||||
"""物料选择器 — 读 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}%"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 响应模型
|
||||
@ -38,7 +50,7 @@ def get_material_groups(
|
||||
):
|
||||
"""
|
||||
按 category 分组汇总,前端渲染手风琴外层。
|
||||
只返回成品/半成品分类。
|
||||
只返回本部门(ORG_DEPARTMENT)名下的分类。
|
||||
"""
|
||||
db = MomSessionLocal()
|
||||
try:
|
||||
@ -47,20 +59,24 @@ def get_material_groups(
|
||||
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, {"kw": f"%{keyword.strip()}%"})
|
||||
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)
|
||||
result = db.execute(sql, {"cat_prefix": CATEGORY_PREFIX_LIKE})
|
||||
|
||||
rows = result.fetchall()
|
||||
return [MaterialGroup(category=row.category, count=row.count) for row in rows]
|
||||
@ -82,6 +98,10 @@ def get_material_items(
|
||||
):
|
||||
"""
|
||||
获取指定 category 下的物料条目,前端展开手风琴时懒加载。
|
||||
|
||||
这里同样要加部门前缀条件(纵深防御):`category` 完全由客户端提供,
|
||||
只靠 `category = :cat` 精确匹配的话,构造一个跨部门的 category 就能
|
||||
把别的部门的物料捞出来。
|
||||
"""
|
||||
db = MomSessionLocal()
|
||||
try:
|
||||
@ -91,13 +111,20 @@ def get_material_items(
|
||||
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": category, "kw": f"%{keyword.strip()}%", "lim": limit}
|
||||
sql,
|
||||
{
|
||||
"cat_prefix": CATEGORY_PREFIX_LIKE,
|
||||
"cat": category,
|
||||
"kw": f"%{keyword.strip()}%",
|
||||
"lim": limit,
|
||||
},
|
||||
)
|
||||
else:
|
||||
sql = text("""
|
||||
@ -105,11 +132,14 @@ def get_material_items(
|
||||
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": category, "lim": limit})
|
||||
result = db.execute(
|
||||
sql, {"cat_prefix": CATEGORY_PREFIX_LIKE, "cat": category, "lim": limit}
|
||||
)
|
||||
|
||||
rows = result.fetchall()
|
||||
return [
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"""用户列表 — 对接 MOM sys_user"""
|
||||
from fastapi import APIRouter, Query, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
from app.core.config import settings
|
||||
from app.core.mom_database import MomSessionLocal
|
||||
from sqlalchemy import text
|
||||
|
||||
@ -20,42 +21,31 @@ class UserOption(BaseModel):
|
||||
def list_users(
|
||||
keyword: str = Query("", description="按用户名/姓名模糊搜索"),
|
||||
limit: int = Query(100, ge=1, le=500),
|
||||
dept: str = Query("IRIS", description="部门过滤"),
|
||||
dept: str = Query("", description="已废弃:部门由服务端按 ORG_DEPARTMENT 钉死,此参数不参与过滤"),
|
||||
):
|
||||
"""获取 MOM 系统用户列表,默认只返回 IRIS 部门"""
|
||||
"""获取 MOM 系统用户列表,只返回本部门(ORG_DEPARTMENT)人员"""
|
||||
# 部门隔离由服务端钉死:无论客户端传什么(含旧版 App 里写死的 dept=IRIS),
|
||||
# 一律只按 ORG_DEPARTMENT 过滤。这样同一份 App 源码不必按部门分叉。
|
||||
#
|
||||
# 这里刻意【不做】「查询异常就退回全表」的降级:那等于把另一个部门的人员
|
||||
# 名单也列出来供本部门挑选,是跨部门数据泄漏。查不出来就报错 ——
|
||||
# 宁可查不出,不可查过头。
|
||||
db = MomSessionLocal()
|
||||
try:
|
||||
# 尝试按部门过滤;若 sys_user 无 department 列则降级全量查询
|
||||
try:
|
||||
base_sql = """
|
||||
SELECT id, username,
|
||||
SPLIT_PART(username, '/', 1) AS full_name,
|
||||
COALESCE(department, '') AS department
|
||||
FROM sys_user
|
||||
WHERE department = :dept
|
||||
"""
|
||||
params = {"dept": dept, "lim": limit}
|
||||
if keyword.strip():
|
||||
sql = text(base_sql + " AND username ILIKE :kw ORDER BY username LIMIT :lim")
|
||||
params["kw"] = f"%{keyword.strip()}%"
|
||||
else:
|
||||
sql = text(base_sql + " ORDER BY username LIMIT :lim")
|
||||
rows = db.execute(sql, params).fetchall()
|
||||
except Exception:
|
||||
# 降级:不使用 department 列过滤
|
||||
fallback_sql = """
|
||||
SELECT id, username,
|
||||
SPLIT_PART(username, '/', 1) AS full_name,
|
||||
'' AS department
|
||||
FROM sys_user
|
||||
"""
|
||||
params = {"lim": limit}
|
||||
if keyword.strip():
|
||||
sql = text(fallback_sql + " WHERE username ILIKE :kw ORDER BY username LIMIT :lim")
|
||||
params["kw"] = f"%{keyword.strip()}%"
|
||||
else:
|
||||
sql = text(fallback_sql + " ORDER BY username LIMIT :lim")
|
||||
rows = db.execute(sql, params).fetchall()
|
||||
base_sql = """
|
||||
SELECT id, username,
|
||||
SPLIT_PART(username, '/', 1) AS full_name,
|
||||
COALESCE(department, '') AS department
|
||||
FROM sys_user
|
||||
WHERE department = :dept
|
||||
"""
|
||||
params = {"dept": settings.ORG_DEPARTMENT, "lim": limit}
|
||||
if keyword.strip():
|
||||
sql = text(base_sql + " AND username ILIKE :kw ORDER BY username LIMIT :lim")
|
||||
params["kw"] = f"%{keyword.strip()}%"
|
||||
else:
|
||||
sql = text(base_sql + " ORDER BY username LIMIT :lim")
|
||||
rows = db.execute(sql, params).fetchall()
|
||||
|
||||
return [
|
||||
UserOption(
|
||||
@ -66,6 +56,8 @@ def list_users(
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
|
||||
Reference in New Issue
Block a user