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:
62
AGENTS.md
62
AGENTS.md
@ -2,6 +2,30 @@
|
||||
|
||||
本仓库(Track 生产流转系统)的工作笔记。仅在验证过之后才写入,避免传谣。
|
||||
|
||||
> ## ⚠️ 本仓库是 **LICA 部门实例**
|
||||
>
|
||||
> 派生自 IRIS 实例(`git.iris-rs.cn/duxingchen/track.git` 的
|
||||
> `feature/ai-audit-update` 分支 @ `192c8ee`)。两套系统在**同一台机器**上并行
|
||||
> 运行、共用 MOM 主数据,但人员、物料、业务数据**完全隔离**。
|
||||
>
|
||||
> | | IRIS 实例 | LICA 实例(本仓库) |
|
||||
> |---|---|---|
|
||||
> | 代码目录 | `/home/yueli/track` | `/home/yueli/track-lica` |
|
||||
> | 容器 | `track_db` / `track_backend` / `track_frontend` | `lica_db` / `lica_backend` / `lica_frontend` |
|
||||
> | compose 项目 | `track` | `track-lica` |
|
||||
> | PC 管理端 | 8010 | **8030** |
|
||||
> | 后端 API | 8011 | **8031** |
|
||||
> | 数据库 | 8012 | **8032** |
|
||||
> | 数据卷 | `track_pgdata` | `lica_pgdata` |
|
||||
> | 部门过滤 | `IRIS` | `LICA` |
|
||||
>
|
||||
> **两条红线:**
|
||||
> 1. **永远不要在 `/home/yueli/track` 目录下执行 `docker compose down -v`** ——
|
||||
> 那会删掉 IRIS 的生产数据。两个卷相互独立,在 `track-lica` 下执行不影响 IRIS。
|
||||
> 2. 改后端地址时别只改一半:PC 端走 `docker-compose.yml` 的 `CORS_ORIGINS`,
|
||||
> 移动端走 `track-uniapp/src/utils/config.js`(uni-app 地址的**唯一来源**)
|
||||
> 和 `track-uniapp/.env`。只改一处会出现「接口通了但图片/上传 404」。
|
||||
|
||||
## 架构速览
|
||||
|
||||
- `backend/` FastAPI + SQLAlchemy 2.x(async) + Alembic,PostgreSQL。
|
||||
@ -11,6 +35,19 @@
|
||||
`sys_user.username` 存 `"真实姓名/登录账号"`,`login()` 用
|
||||
`WHERE username LIKE '%/<账号>'` 匹配,`display_name` 由 `/` 拆解得到。
|
||||
MOM 连接配置在 `app/core/mom_database.py`(同步 psycopg2 引擎)。
|
||||
- **组织隔离**:本实例只服务 LICA 部门,唯一开关是 `app/core/config.py` 的
|
||||
`ORG_DEPARTMENT`(对应 MOM `sys_user.department`)与
|
||||
`MATERIAL_CATEGORY_PREFIX`(对应 MOM `material_base.category` 前缀)。
|
||||
过滤点共 4 处:登录 `auth_service.login`、人员列表 `endpoints/users.py`、
|
||||
物料 `endpoints/materials.py` 的 `groups`/`items`、
|
||||
人员操作统计 `dashboard_service.get_user_operations`。
|
||||
- 人员与物料都是**服务端钉死**部门,客户端传什么部门参数都不采纳
|
||||
(这样 uni-app 里写死的 `dept` 不会造成跨部门影响)。
|
||||
- 刻意**不做**「查询失败退回全表」的降级 —— 那是跨部门数据泄漏,
|
||||
宁可查不出,不可查过头。
|
||||
- ⚠️ 物料必须用前缀 `category LIKE 'LICA/%'`,**不能用** `ILIKE '%LICA%'`:
|
||||
MOM 里存在 171 条 `IRIS/成品/LICA/...`(无人机/野外便携/高塔监测等),
|
||||
模糊匹配会把 IRIS 的物料漏给 LICA。已实测前缀匹配命中 795 条 / 5 个分组。
|
||||
|
||||
## 本地起环境(关键,踩过的坑都在这)
|
||||
|
||||
@ -19,16 +56,20 @@
|
||||
然后 `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.1`、`MOM_DB_PORT=5432`
|
||||
- `DATABASE_URL=postgresql+asyncpg://lica:<密码>@127.0.0.1:8032/lica_production`
|
||||
- `MOM_DB_HOST=inventory_db`、`MOM_DB_PORT=5432`(容器内走 projects_default
|
||||
网络的服务名;从宿主机连则用 `127.0.0.1:5435`)
|
||||
- `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`,要么直接给
|
||||
4. 前端 proxy 指向 Docker 服务名 `lica_backend:8000`(本实例的服务名**刻意不叫**
|
||||
`backend`,避免在 `projects_default` 网络上与 IRIS 实例重名,否则将来任何一方
|
||||
写 `http://backend:8000` 会随机打到另一个部门)。本机跑要么把
|
||||
`127.0.0.1 lica_backend` 写进 `/etc/hosts`,要么直接给
|
||||
`VITE_API_BASE_URL=http://localhost:<port>/api/v1` 绕过 proxy。
|
||||
注意 dev server 由 `basicSsl` 起 HTTPS,跨域需要后端
|
||||
注意 dev server 由 `basicSsl` 起 HTTPS —— **必须用 `https://` 访问,
|
||||
用 `http://` 会直接连不上(curl 报 000)**。跨域需要后端
|
||||
`CORS_ORIGINS` 加上 `https://localhost:1420`。
|
||||
|
||||
## 测试的坑(重要)
|
||||
@ -40,7 +81,7 @@
|
||||
并在单个 `asyncio.run()` 里跑完全部请求。生产 uvicorn 单循环无此问题。
|
||||
- 仓库目前**没有** pytest 基建,也没有前端测试脚本。
|
||||
|
||||
## 已知的待修问题(截至 1.0应用 分支)
|
||||
## 已知的待修问题(继承自 IRIS 侧 `192c8ee`,本仓库**未修**)
|
||||
|
||||
- **读接口大面积未鉴权**(已实测,非推测):无 token 直接 200 的包括
|
||||
`/api/v1/users/`、全部 `/api/v1/dashboard/*`(含
|
||||
@ -48,6 +89,15 @@
|
||||
`/api/v1/analytics/*`、`/api/v1/screen/*`、`/api/v1/orders/`。
|
||||
写操作和 `/api/v1/tasks`、`/api/v1/products` 是有鉴权的。
|
||||
新增接口请统一用 `app/core/deps.py` 的 `require_admin` / `require_roles`。
|
||||
- **对 LICA 的影响面**:`/api/v1/users/` 虽无鉴权,但部门过滤在 SQL 层钉死,
|
||||
实测无 token 也只能拿到 19 个 LICA 人员,**不会泄露 IRIS 的人**;
|
||||
其余匿名接口读的是本实例自己的库,跨不到 IRIS 库。真正的风险是
|
||||
「本部门内部」的越权(如普通操作员能看管理端统计),不是跨部门。
|
||||
- `AdminLayout` 只判登录不判角色:LICA 的 15 个 `INBOUND` 账号能进 PC 管理端。
|
||||
- `dashboard_service.py` 查 tasks 时用**小写** `pending/in_progress/completed`,
|
||||
而 `Task.status` 实际存大写(`PENDING`/`WIP`/`COMPLETED`),
|
||||
导致管理端「待接收/进行中/已完成」三个数字恒为 0。
|
||||
(注意 `Product.status` 确实是小写,修的时候别一起改。)
|
||||
- 「管理员角色」这份规则此前散在 4 处(后端 `task_service`、`products.py` 内联、
|
||||
前端 `constants/task.ts`、`AdminProductsPage` 内联),已因此出过事故。
|
||||
**后端唯一事实来源是 `app/core/roles.py`,前端用 `constants/task.ts::isAdminRole`。**
|
||||
|
||||
@ -1,10 +1,26 @@
|
||||
DATABASE_URL=postgresql+asyncpg://track:track_prod_2026@localhost:5433/track_production
|
||||
# ============================================================
|
||||
# LICA 实例 — 本地裸跑后端时的配置示例
|
||||
# 容器部署(docker compose)无需此文件,所有变量由 docker-compose.yml 注入。
|
||||
# 复制为 .env 后按需修改;.env 已被 .gitignore 忽略,不会提交。
|
||||
# ============================================================
|
||||
|
||||
# 本机直连 LICA 库(容器内用的是 lica_db,端口 5432)
|
||||
DATABASE_URL=postgresql+asyncpg://lica:lica_dev_password@localhost:8032/lica_production
|
||||
SECRET_KEY=change-me-to-a-random-secret-key-in-production
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||
DEBUG=true
|
||||
CORS_ORIGINS='["http://localhost:1420", "tauri://localhost"]'
|
||||
CORS_ORIGINS='["http://localhost:8030", "tauri://localhost"]'
|
||||
|
||||
# 日志:LOG_JSON=true 输出单行 JSON(便于采集),本地调试可设 false 换可读格式
|
||||
# ---- 组织隔离(本实例唯一的部门开关)----
|
||||
ORG_DEPARTMENT=LICA
|
||||
MATERIAL_CATEGORY_PREFIX=LICA/
|
||||
|
||||
# ---- MOM 老系统库(只读,用于登录校验/人员/物料)----
|
||||
# 容器内用容器名 inventory_db:5432;本机裸跑走宿主机映射端口 5435
|
||||
MOM_DB_HOST=localhost
|
||||
MOM_DB_PORT=5435
|
||||
|
||||
# ---- 日志:LOG_JSON=true 输出单行 JSON(便于采集),本地调试可设 false 换可读格式 ----
|
||||
LOG_LEVEL=INFO
|
||||
LOG_JSON=true
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -33,6 +33,12 @@ class Settings(BaseSettings):
|
||||
# ---- MOM 仓储系统回调 Webhook(Track 作为接收方,验签用) ----
|
||||
TRACK_WEBHOOK_KEY: str | None = None # MOM 回调 POST 时 Header X-API-Key 须等于此值
|
||||
|
||||
# ---- 组织隔离 ----
|
||||
# 同一套代码部署给不同部门时,只需改这两个值(+ compose 里的项目名/容器名/端口)。
|
||||
# 全仓库的部门过滤点只有三处 SQL:登录、人员列表、物料选择器,全部引用这里。
|
||||
ORG_DEPARTMENT: str = "LICA" # MOM sys_user.department 的取值
|
||||
MATERIAL_CATEGORY_PREFIX: str = "LICA/" # MOM material_base.category 的部门前缀
|
||||
|
||||
@property
|
||||
def CORS_ORIGINS_LIST(self) -> list[str]:
|
||||
"""将 JSON 字符串解析为 Python list,供 CORSMiddleware 使用"""
|
||||
|
||||
@ -25,14 +25,19 @@ def login(username: str, password: str) -> LoginResponse:
|
||||
db = MomSessionLocal()
|
||||
try:
|
||||
# 1. 普通用户:LIKE '%/username' 模糊匹配 MOM sys_user 表
|
||||
# 并且只允许本部门(settings.ORG_DEPARTMENT)的人登录 —— IRIS 与 LICA
|
||||
# 共用同一个 MOM 库,这里不加部门条件的话,另一个部门的人也能登进来。
|
||||
# 不匹配时统一报「用户名或密码错误」,不区分「账号不存在」与「存在但
|
||||
# 不属于本部门」,避免给账号探测者提供线索。
|
||||
from sqlalchemy import text
|
||||
result = db.execute(
|
||||
text(
|
||||
"SELECT id, username, department, role, password_hash "
|
||||
"FROM sys_user "
|
||||
"WHERE username LIKE :pattern"
|
||||
"WHERE username LIKE :pattern "
|
||||
" AND department = :dept"
|
||||
),
|
||||
{"pattern": f"%/{username}"},
|
||||
{"pattern": f"%/{username}", "dept": settings.ORG_DEPARTMENT},
|
||||
)
|
||||
row = result.fetchone()
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ from sqlalchemy import select, func, or_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.lifecycle import normalize_after_sales_step
|
||||
|
||||
|
||||
@ -791,8 +792,8 @@ async def get_user_operations(
|
||||
) -> list[UserOperation]:
|
||||
"""上帝视角 — 统计**全部人员**的操作次数(按时段过滤)。
|
||||
|
||||
返回所有 IRIS 部门人员(该时段无操作的计 0),并追加有操作记录但不在
|
||||
人员清单中的账号(如历史/已离职)。
|
||||
返回本部门(ORG_DEPARTMENT)全部人员(该时段无操作的计 0),并追加有操作
|
||||
记录但不在人员清单中的账号(如历史/已离职)。
|
||||
|
||||
操作口径(均不修改数据库):
|
||||
- 接收: task_logs.action_type='receive'(按操作人)
|
||||
@ -840,20 +841,20 @@ async def get_user_operations(
|
||||
rcd_rows = (await db.execute(rcd_stmt)).all()
|
||||
record_map = {r[0]: r[1] or 0 for r in rcd_rows}
|
||||
|
||||
# ── 3. 获取全部 MOM 用户清单(IRIS 部门)──
|
||||
# ── 3. 获取全部 MOM 用户清单(本部门)──
|
||||
# 部门由 ORG_DEPARTMENT 钉死;刻意不保留「查询失败退回全表」的降级 ——
|
||||
# 那会把另一个部门的人员也列进本部门的统计表。
|
||||
users: list[dict] = []
|
||||
try:
|
||||
dbm = MomSessionLocal()
|
||||
try:
|
||||
rows = dbm.execute(text("""
|
||||
SELECT username, SPLIT_PART(username, '/', 1) AS full_name
|
||||
FROM sys_user WHERE department = 'IRIS'
|
||||
""")).fetchall()
|
||||
except Exception:
|
||||
rows = dbm.execute(text("""
|
||||
SELECT username, SPLIT_PART(username, '/', 1) AS full_name
|
||||
FROM sys_user
|
||||
""")).fetchall()
|
||||
rows = dbm.execute(
|
||||
text("""
|
||||
SELECT username, SPLIT_PART(username, '/', 1) AS full_name
|
||||
FROM sys_user WHERE department = :dept
|
||||
"""),
|
||||
{"dept": settings.ORG_DEPARTMENT},
|
||||
).fetchall()
|
||||
finally:
|
||||
dbm.close()
|
||||
for row in rows:
|
||||
|
||||
@ -1,58 +1,76 @@
|
||||
# ============================================================
|
||||
# 生产流转管理系统 — 全栈 Docker 热更新开发环境
|
||||
# LICA 生产流转管理系统 — 独立实例(与 IRIS 版同机共存)
|
||||
# 启动: docker compose up -d --build
|
||||
# 停止: docker compose down
|
||||
# 日志: docker compose logs -f
|
||||
#
|
||||
# ⚠️ 首次部署必须先跑数据库迁移,否则业务接口全部 500:
|
||||
# docker compose exec lica_backend python -m alembic upgrade head
|
||||
# ============================================================
|
||||
|
||||
name: track-lica
|
||||
|
||||
services:
|
||||
# ============================================================
|
||||
# PostgreSQL 15 + pgvector
|
||||
# PostgreSQL 15 + pgvector — LICA 独立库
|
||||
# ============================================================
|
||||
db:
|
||||
image: pgvector/pgvector:pg15
|
||||
container_name: track_db
|
||||
container_name: lica_db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: track
|
||||
POSTGRES_PASSWORD: track_prod_2026
|
||||
POSTGRES_DB: track_production
|
||||
POSTGRES_USER: lica
|
||||
POSTGRES_PASSWORD: bc7e44ba7aa424b5a1d95a53dec824fc
|
||||
POSTGRES_DB: lica_production
|
||||
ports:
|
||||
- "8012:5432"
|
||||
- "8032:5432"
|
||||
volumes:
|
||||
- track_pgdata:/var/lib/postgresql/data
|
||||
# ⚠️ 绝不能与 IRIS 的 track_pgdata 共用,否则两个部门写同一个库
|
||||
- lica_pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U track -d track_production"]
|
||||
test: ["CMD-SHELL", "pg_isready -U lica -d lica_production"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# ============================================================
|
||||
# FastAPI 后端 — --reload 热更新
|
||||
# 服务名用 lica_backend 而非 backend:服务名会作为网络别名注册到
|
||||
# projects_default,与 IRIS 实例的 backend 重名会导致将来任何一方
|
||||
# 写 http://backend:8000 时随机打到另一个部门。
|
||||
# ============================================================
|
||||
backend:
|
||||
lica_backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile.dev
|
||||
container_name: track_backend
|
||||
container_name: lica_backend
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# 🚀 track 主库用容器名 track_db,避免与 MOM 的 inventory_db(别名 db) 在同一网络下 DNS 解析冲突
|
||||
DATABASE_URL: postgresql+asyncpg://track:track_prod_2026@track_db:5432/track_production
|
||||
SECRET_KEY: change-me-in-production
|
||||
# ⚠️ 主机名必须是 lica_db,绝不能写 db ——
|
||||
# MOM 的 compose 服务名就叫 db,在 projects_default 网络上
|
||||
# 会把本后端解析到 MOM 生产库并写入。
|
||||
DATABASE_URL: postgresql+asyncpg://lica:bc7e44ba7aa424b5a1d95a53dec824fc@lica_db:5432/lica_production
|
||||
# ⚠️ 必须与 IRIS 不同:get_current_user 只验签名不查库,
|
||||
# 两边共用密钥则 IRIS 签发的 token 可直接访问 LICA 接口。
|
||||
SECRET_KEY: aQy1zyEJY50OJlYrZ2OkNgQN3N46F4bvjYVdva0y4N3wpfaZotf9NKBTZ0CTDeLD
|
||||
DEBUG: "true"
|
||||
# 8010 = PC 管理端;5173/8020 = track-uniapp 的 H5 调试服务
|
||||
# ---- 组织隔离(本实例唯一的部门开关,供登录/人员/物料三处 SQL 引用)----
|
||||
ORG_DEPARTMENT: LICA
|
||||
MATERIAL_CATEGORY_PREFIX: "LICA/"
|
||||
# 8030 = PC 管理端;5173/8020 = track-uniapp 的 H5 调试服务
|
||||
# (HBuilderX「运行到浏览器」起在 5173,直接跑 vite 按 track-uniapp/vite.config.ts 是 8020)。
|
||||
# ⚠️ 漏了 uniapp 的来源时,浏览器会在 CORS 预检阶段就掐掉请求,
|
||||
# 表现是「点登录没反应」,且控制台只报一句笼统的 404/400。
|
||||
CORS_ORIGINS: '["http://localhost:8010","https://localhost:8010","http://192.168.9.80:8010","https://192.168.9.80:8010","http://localhost:5173","https://localhost:5173","http://192.168.9.80:5173","https://192.168.9.80:5173","http://localhost:8020","https://localhost:8020","tauri://localhost"]'
|
||||
# 🚀 MOM 老系统数据库 — 通过容器名解析(projects_default 网络内 DNS),不再写死 IP
|
||||
CORS_ORIGINS: '["http://localhost:8030","https://localhost:8030","http://192.168.9.80:8030","https://192.168.9.80:8030","http://localhost:5173","https://localhost:5173","http://192.168.9.80:5173","https://192.168.9.80:5173","http://localhost:8020","https://localhost:8020","tauri://localhost"]'
|
||||
# MOM 老系统数据库 — 通过容器名解析(projects_default 网络内 DNS),不要写死 IP
|
||||
MOM_DB_HOST: inventory_db
|
||||
MOM_DB_PORT: "5432"
|
||||
# 🚀 MOM 仓储系统回调 Webhook 验签 Key(与 MOM 侧 TRACK_WEBHOOK_KEY 保持一致)
|
||||
# MOM 回调验签 Key — 暂与 IRIS 相同:MOM 侧目前只有单个 TRACK_WEBHOOK_KEY,
|
||||
# 且回调地址写死指向 IRIS 的 track_backend。待 MOM 支持按部门双推后,
|
||||
# 再决定是否给 LICA 单独签发。(在此之前 LICA 收不到出入库联动)
|
||||
TRACK_WEBHOOK_KEY: 2ce5fedb48fde3fd7e0abf67472a5027b03e9ae6f19cf768
|
||||
ports:
|
||||
- "8011:8000"
|
||||
- "8031:8000"
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
depends_on:
|
||||
@ -69,23 +87,23 @@ services:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile.dev
|
||||
container_name: track_frontend
|
||||
container_name: lica_frontend
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
VITE_API_BASE_URL: /api/v1
|
||||
ports:
|
||||
- "8010:1420"
|
||||
- "8030:1420"
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- /app/node_modules # 匿名卷,不覆盖容器里的 node_modules
|
||||
depends_on:
|
||||
- backend
|
||||
- lica_backend
|
||||
|
||||
networks:
|
||||
mom_net:
|
||||
external: true
|
||||
name: projects_default
|
||||
name: projects_default # MOM 系统所在网络(IRIS 实例也在此网络上)
|
||||
|
||||
volumes:
|
||||
track_pgdata:
|
||||
name: track_pgdata
|
||||
lica_pgdata:
|
||||
name: lica_pgdata
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/nickolay/tauri-docs/refs/heads/v2/src/content/docs/_schema/config.schema.json",
|
||||
"productName": "track",
|
||||
"productName": "track-lica",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.track.production",
|
||||
"identifier": "com.lica.production",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
"devUrl": "http://localhost:1420",
|
||||
@ -10,10 +10,10 @@
|
||||
"beforeBuildCommand": "npm run build"
|
||||
},
|
||||
"app": {
|
||||
"title": "生产流转",
|
||||
"title": "生产流转 · LICA",
|
||||
"windows": [
|
||||
{
|
||||
"title": "工厂生产流转管理系统",
|
||||
"title": "工厂生产流转管理系统 · LICA",
|
||||
"width": 390,
|
||||
"height": 844,
|
||||
"resizable": true,
|
||||
|
||||
@ -65,7 +65,7 @@ export default function AdminLoginPage() {
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder="如 IRIS 或工号"
|
||||
placeholder="如 LICA 或工号"
|
||||
className="w-full rounded-lg border border-gray-200 px-3 py-2.5 text-sm focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-100"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
@ -39,7 +39,7 @@ export default defineConfig({
|
||||
},
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://backend:8000", // Docker 内部用服务名
|
||||
target: "http://lica_backend:8000", // Docker 内部用服务名(LICA 实例)
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
{
|
||||
"name" : "Track",
|
||||
"appid" : "__UNI__B572616",
|
||||
"description" : "Track - 生产流转管理",
|
||||
"versionName" : "T1.0.8",
|
||||
"versionCode" : 108,
|
||||
"name" : "Track-LICA",
|
||||
"appid" : "__UNI__D2F4A19",
|
||||
"description" : "LICA - 生产流转管理",
|
||||
"versionName" : "T1.0.0",
|
||||
"versionCode" : 100,
|
||||
"transformPx" : false,
|
||||
"vueVersion" : "3",
|
||||
"app-plus" : {
|
||||
|
||||
@ -455,7 +455,10 @@ export default {
|
||||
openEditProduct() { this.editForm = { order_no: this.product.order_no || "", external_serial: this.product.external_serial || "" }; this.editProductVisible = true; },
|
||||
async doEditProduct() { this.editSaving = true; try { this.product = await patch(`/products/${this.product.id}`, { order_no: this.editForm.order_no.trim(), external_serial: this.editForm.external_serial.trim() }); uni.showToast({ title: "已保存", icon: "success" }); this.editProductVisible = false; } catch {} finally { this.editSaving = false; } },
|
||||
|
||||
async loadUsers() { try { const res = await get("/users/", { limit: 200, dept: "IRIS" }); this.users = (res || []).filter(u => u.department === "IRIS"); this.userOptions = this.users.map(u => ({ id: u.username || u.id, name: u.full_name || u.username })); setUserNameMap(this.users); this.dictVersion += 1; } catch (e) { console.error('[loadUsers] 获取用户列表失败:', e); } },
|
||||
// 部门隔离由后端按 ORG_DEPARTMENT 钉死,客户端不再传 dept、也不再二次过滤。
|
||||
// 原先写死的 department === "IRIS" 会把服务端返回的 LICA 人员全部滤成空数组,
|
||||
// 表现为「指派负责人」下拉框没有任何可选人员、无法建任务。
|
||||
async loadUsers() { try { const res = await get("/users/", { limit: 200 }); this.users = res || []; this.userOptions = this.users.map(u => ({ id: u.username || u.id, name: u.full_name || u.username })); setUserNameMap(this.users); this.dictVersion += 1; } catch (e) { console.error('[loadUsers] 获取用户列表失败:', e); } },
|
||||
loadCurrentUser() { try { let user = uni.getStorageSync("user"); if (typeof user === "string" && user) { try { user = JSON.parse(user); } catch (e) { user = null; } } if (user && typeof user === "object") { this.currentUser = user; this.currentUserId = String(user.id || ""); this.currentUsername = user.username || ""; this.currentUserRole = user.role || ""; } } catch {} },
|
||||
|
||||
onTaskNameChange(e) { const idx = e.detail.value; this.firstForm.taskNameIdx = idx; this.firstForm.task_name = this.availableTaskOptions[idx]; },
|
||||
|
||||
@ -4,19 +4,20 @@
|
||||
* 此前 request.js 与 App.vue 各自硬编码了一份生产域名,改域名要改两处,
|
||||
* 漏一处就会出现「接口通了但 OTA 检查更新接不上」这类极难排查的偏差,
|
||||
* 故统一收口到本文件。新增环境地址一律加在这里,不要在业务代码里写死。
|
||||
*
|
||||
* ⚠️ 本文件属于 **LICA 部门实例**,后端端口 8031。
|
||||
* IRIS 实例用的是 8011 —— 两套系统是两个独立后端、独立数据库。
|
||||
* 改这里之前先确认端口,填错会让 LICA 的 App 连到 IRIS 的后端,
|
||||
* 表现为「能登录、能扫码,但看到的全是别的部门的单」。
|
||||
*/
|
||||
|
||||
// 生产环境 API 地址
|
||||
//
|
||||
// ⚠️ HTTPS 说明(2026-09 实测):生产域名 track_back.iris-rs.cn 的 443 端口
|
||||
// 是通的、也确实配有证书,但证书【已过期】—— 现在切 https 会让 App 的
|
||||
// 全部请求直接失败。待运维完成证书续期后,只需把下面 PROD_URL 与
|
||||
// LOCAL_URL 的协议头改成 https:// 即可,业务代码无需任何改动。
|
||||
// 续期前请勿改动,保持 http:// 以保证可用性。
|
||||
export const PROD_URL = "http://track_back.iris-rs.cn/api/v1";
|
||||
// LICA 暂无独立域名,车间局域网内直连后端机器。
|
||||
// 待有专属域名/HTTPS 证书后,把下面地址整体替换即可,业务代码无需改动。
|
||||
export const PROD_URL = "http://192.168.9.80:8031/api/v1";
|
||||
|
||||
// 本地开发地址 — 真机调试时指向开发机局域网 IP
|
||||
export const LOCAL_URL = "http://192.168.9.80:8011/api/v1";
|
||||
export const LOCAL_URL = "http://192.168.9.80:8031/api/v1";
|
||||
|
||||
// 生产基础地址:供只认「生产环境」的场景直接引用
|
||||
export const BASE_URL = PROD_URL;
|
||||
|
||||
@ -2,7 +2,12 @@
|
||||
# ============================================================
|
||||
# track-uniapp 源码 → HBuilderX 项目 同步
|
||||
#
|
||||
# 方向:单向 WSL track-uniapp/src/ → G:\Track\track-app\track
|
||||
# 方向:单向 WSL track-lica/track-uniapp/src/ → G:\Track\track-lica-app\track
|
||||
#
|
||||
# ⚠️ LICA 与 IRIS 是两套独立的 App 工程,DST 必须指向各自在 Windows 侧的
|
||||
# HBuilderX 目录。若两边共用同一个 DST,本脚本会持续把 LICA 的源码灌进
|
||||
# IRIS 的工程(而且带 --delete,会删掉对方的文件),因为带监听循环,
|
||||
# 改一次就会反复覆盖。
|
||||
#
|
||||
# ⚠️ 为什么是「src/ → 项目根」且单向(2026-09-15 修正):
|
||||
# HBuilderX 是以 G:\Track\track-app\track 为项目根打开的(经典布局,
|
||||
@ -24,8 +29,8 @@
|
||||
# 停止: Ctrl+C
|
||||
# ============================================================
|
||||
|
||||
SRC="/home/yueli/track/track-uniapp/src"
|
||||
DST="/mnt/g/Track/track-app/track"
|
||||
SRC="/home/yueli/track-lica/track-uniapp/src"
|
||||
DST="/mnt/g/Track/track-lica-app/track"
|
||||
|
||||
# ── 只在 Windows 侧存在、绝对不能被覆盖或删除的项 ──
|
||||
EXCLUDES=(
|
||||
|
||||
Reference in New Issue
Block a user