feat: 组织隔离(IRIS 单实例)与出料功能基础

本轮之前累积的未提交工作,一并固化:

- 组织隔离:同一份代码部署给不同部门只需改 config 的 ORG_DEPARTMENT 与
  MATERIAL_CATEGORY_PREFIX。过滤点在登录/人员列表/物料/MOM 出库单四处,
  全部服务端钉死,客户端传什么都放不大。
  ★ 物料必须用**前缀** LIKE,不能反推成 ILIKE '%IRIS%':MOM 里 LICA 的物料是
  `LICA/<中文>`,而本部门分类树里另有 `IRIS/成品/LICA/…`(本就属于本部门),
  前缀匹配天然区分得开。
- MOM 出库单只读查询(直连 MOM 库):不走 MOM 现成的 /outbound 接口 ——
  那个要 JWT + permission_required,且对非特权账号按 consumer_name 做行级
  隔离,服务账号只能拿到自己名下的单。分页必须两段式(先按单号 GROUP BY
  分页,再 IN 捞明细),对宽表直接分页会得到明细行数而不是单据数。
- 出料功能:产品 ↔ 出库单存档(product_outbounds)与任务 ↔ 出库明细
  (task_outbound_materials),供「这台设备对应 MOM 哪张单」的展示。
  ⚠️ 快照一律由后端拿 ID 去 MOM 现查,不接受前端传入,否则前端可伪造单据。
This commit is contained in:
2026-09-23 15:18:06 +08:00
parent edd43fec29
commit e45c97bd1f
22 changed files with 2000 additions and 48 deletions

View File

@ -0,0 +1,48 @@
/** MOM 出库单相关接口 —— 任务挂载出库物料时搜索选择用 */
import api from "./api";
import type { MomOutboundSearchResponse } from "../types/api";
export interface MomOutboundSearchParams {
/** 出库单号 / 物料名称 / 规格型号 / SKU / 领用人,任一命中 */
keyword?: string;
/** YYYY-MM-DD,含当日 */
start_date?: string;
/** YYYY-MM-DD,含当日 */
end_date?: string;
/** 按领用人(中文名)过滤 */
consumer?: string;
/** 跳过**单据数**(不是明细行数) */
skip?: number;
/** 返回**单据数**,后端上限 100 */
limit?: number;
}
/**
* 搜索 MOM 出库单(按单据分页,带回每张单的明细)。
* 对应后端 GET /api/v1/mom-outbounds
*
* ⚠️ 可见范围(公司隔离 + 跨部门例外)由后端服务层钉死,本接口**没有任何**
* 能放大范围的参数 —— 界面筛选只能收窄。
*/
export async function searchMomOutbounds(
params: MomOutboundSearchParams = {},
): Promise<MomOutboundSearchResponse> {
const { data } = await api.get<MomOutboundSearchResponse>("/mom-outbounds", { params });
return data;
}
/**
* 本部门出库单里出现过的**领用人姓名**(去重,按出现次数降序)。
* 对应后端 GET /api/v1/mom-outbounds/consumers
*
* ⚠️ 后端**已按权限范围过滤** —— 下拉里不会出现用户看不到的人名。
*/
export async function listMomOutboundConsumers(): Promise<string[]> {
const { data } = await api.get<string[]>("/mom-outbounds/consumers");
return data;
}
// 注:原先这里有 addTaskOutboundMaterials / removeTaskOutboundMaterial
// 两个**任务级**的读写函数。物料已统一为**设备级**,任务级的三个端点连同
// 实现一起删除 —— 挂载/查看/删除/报废一律走 productApi 里的
// mountProductOutboundMaterials / removeProductOutboundMaterial。

View File

@ -26,6 +26,19 @@ export interface TaskCompletePayload {
remark: string | null;
}
export interface TaskCreatePayload {
product_id: string;
task_name: string;
assignee_id?: string | null;
remark?: string;
/**
* 创建时一并挂载的 MOM 出库**明细行** ID(trans_outbound.id)。
* 前端按整张出库单勾选,提交时把该单全部明细 ID 带过来。
* 不传 = 不挂载。
*/
mom_line_ids?: number[];
}
// ============================================================
// API 方法
// ============================================================
@ -43,6 +56,16 @@ export async function listTasks(productId?: string): Promise<TaskListResponse> {
return data;
}
/**
* 创建任务 — 对应后端 POST /api/v1/tasks/
* payload.mom_line_ids 非空时,后端会在**同一事务**里把对应的 MOM 出库明细
* 挂到新任务上,不存在「任务建好了但物料没挂上」的中间态。
*/
export async function createTask(payload: TaskCreatePayload): Promise<TaskResponse> {
const { data } = await api.post<TaskResponse>("/tasks/", payload);
return data;
}
/** 创建子任务 */
export async function createSubtask(
parentTaskId: string,

View File

@ -0,0 +1,21 @@
/** 用户列表 —— 对接 MOM sys_user,只返回本部门人员 */
import api from "./api";
export interface UserOption {
id: string;
/** 登录账号,即任务里的 assignee_id 口径 */
username: string;
/** 中文姓名 */
full_name: string;
department: string;
}
/**
* 获取本部门人员列表。
* 对应后端 GET /api/v1/users/ —— 部门隔离由服务端按 ORG_DEPARTMENT 钉死,
* 客户端传什么都没用。
*/
export async function listUsers(keyword = "", limit = 200): Promise<UserOption[]> {
const { data } = await api.get<UserOption[]>("/users/", { params: { keyword, limit } });
return data;
}