|
|
@@ -0,0 +1,297 @@
|
|
|
+# 订单-计划-产品-记录 全链路 & 无计划拦截 实施方案
|
|
|
+
|
|
|
+> 关联提交:`a7167fc fix: 打通生产订单-生产计划-工件关联链路`(已完成 1/3:订单→计划→产品)
|
|
|
+> 本次做 2/3:**工位结果层面的无计划拦截** + **按订单/计划查询**
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 一、现状分析
|
|
|
+
|
|
|
+### 1.1 已有的链路(上次提交已完成)
|
|
|
+
|
|
|
+```
|
|
|
+mes_product_order (订单) --order_id--> mes_product_plan (计划: order_id/day/shift_id/nums)
|
|
|
+ |
|
|
|
+ plan_id
|
|
|
+ v
|
|
|
+ mes_product (产品/工件)
|
|
|
+```
|
|
|
+
|
|
|
+| 环节 | 代码位置 | 说明 |
|
|
|
+|---|---|---|
|
|
|
+| 排产 | `MesProductPlanService.save()` | 校验订单已下发(`state=1`)、BOM 的 `product_cate_id`、同日期+班次+产品唯一、累计不超订单数量 |
|
|
|
+| 创建工件时匹配计划 | `MesProductPlanService.getCurrentPlan(cateSn)` | 按 **当前班次的开始日期 + 班次 + 工件类型** 唯一匹配;`c.state='1'`(订单已下发) |
|
|
|
+| 工件绑定计划 | `MesProductRecordService.resolvePlan()` → `mes_product.plan_id` | 创建工件时无计划直接抛 `当前班次没有有效的生产计划,请先排产!` |
|
|
|
+| 超产校验 | `MesProductPlanService.validatePlanCapacity()` | 按 `mes_product.plan_id` 计数 |
|
|
|
+
|
|
|
+### 1.2 缺口(本次要补的)
|
|
|
+
|
|
|
+| # | 缺口 | 现状 |
|
|
|
+|---|---|---|
|
|
|
+| 1 | `mes_product_record`(生产记录/工位结果)**没有计划字段** | 记录层面看不到属于哪个计划,履历无法按订单/计划追溯 |
|
|
|
+| 2 | **提交工位结果不校验计划** | `updateRecordResult()` / `updateRecord()` 等直接写库;历史件、返修件、非正常创建路径产生的"无计划件"照样能过站 |
|
|
|
+| 3 | **查询侧没有订单/计划维度** | 生产记录列表/导出只能按工件码、工位号查;看不出哪些件"无计划";计划页只能按 orderId 精确查 |
|
|
|
+| 4 | 计划实际产出统计不完整 | `MesProductPlanController.listData` 只按 `mes_product` 计数,返修件/无计划件统计不到 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 二、拦截规则(业务口径)
|
|
|
+
|
|
|
+**拦截主体 = 工件(sn)+ 计划(plan)**,在 **提交工位结果** 时校验。
|
|
|
+
|
|
|
+### 2.1 计划来源(三级兜底,逐级回写)
|
|
|
+
|
|
|
+```
|
|
|
+1) mes_product_record.plan_id ← 新增字段,本次加
|
|
|
+2) mes_product.plan_id ← 已有字段
|
|
|
+3) getCurrentPlan(cateSn) ← 按 当前班次开始日期+班次+工件类型 自动匹配(自愈),匹配到即回写 1)/2)
|
|
|
+```
|
|
|
+
|
|
|
+三级都取不到 → **拦截**。
|
|
|
+
|
|
|
+### 2.2 拦截条件(任一命中即拒绝)
|
|
|
+
|
|
|
+| 条件 | 提示语 |
|
|
|
+|---|---|
|
|
|
+| 找不到有效计划 | `该工件未关联生产计划(订单-计划-工件),当前班次无有效生产计划,不能作业!` |
|
|
|
+| 计划已删除/停用 (`status != '0'`) | `该工件关联的生产计划已失效,不能作业!` |
|
|
|
+| 计划对应订单未下发 (`order.state != '1'`) | `该工件关联的生产订单未下发,不能作业!` |
|
|
|
+
|
|
|
+### 2.3 放行例外
|
|
|
+
|
|
|
+| 例外 | 处理 |
|
|
|
+|---|---|
|
|
|
+| 返修提交 (`isRepair = '1'`) | 放行,沿用原记录 plan(返修件不占用计划产量) |
|
|
|
+| 全局开关 `mes.plan.intercept = '0'` | 全量关闭拦截(应急/灰度),拦截代码保留 |
|
|
|
+| 计划超产 | **提交工位结果时不再校验数量**(数量在创建工件时已扣),只校验计划有效性 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 三、数据库改动
|
|
|
+
|
|
|
+新增脚本:`db/mes_product_record_plan.sql`
|
|
|
+
|
|
|
+```sql
|
|
|
+-- 1) 生产记录表增加生产计划字段
|
|
|
+ALTER TABLE `mes_product_record`
|
|
|
+ ADD COLUMN `plan_id` VARCHAR(64) NULL COMMENT '生产计划' AFTER `cate_sn`;
|
|
|
+
|
|
|
+-- 2) 索引(按计划/订单查记录)
|
|
|
+CREATE INDEX `idx_mpr_plan_id` ON `mes_product_record` (`plan_id`);
|
|
|
+
|
|
|
+-- 3) 历史数据回填:工件有计划 -> 回填到记录
|
|
|
+UPDATE `mes_product_record` r
|
|
|
+ JOIN `mes_product` p ON p.`sn` = r.`sn`
|
|
|
+ SET r.`plan_id` = p.`plan_id`
|
|
|
+ WHERE (r.`plan_id` IS NULL OR r.`plan_id` = '')
|
|
|
+ AND p.`plan_id` IS NOT NULL AND p.`plan_id` <> '';
|
|
|
+
|
|
|
+-- 4) 拦截总开关(0=不拦截,1=拦截)
|
|
|
+DELETE FROM `js_sys_config` WHERE `config_key` = 'mes.plan.intercept';
|
|
|
+INSERT INTO `js_sys_config`
|
|
|
+ (id, config_name, config_key, config_value, is_sys, create_by, create_date, update_by, update_date, remarks)
|
|
|
+VALUES
|
|
|
+ ('2026102018000001','生产计划无计划拦截','mes.plan.intercept','1','0','system',NOW(),'system',NOW(),
|
|
|
+ '提交工位结果时校验工件是否关联有效生产计划;0=关闭拦截');
|
|
|
+```
|
|
|
+
|
|
|
+> 注:`js_sys_config` 直插数据库后需重启服务或在"系统配置"页面重新保存一次触发缓存刷新(沿用 `cz060_cz061_routing_align.sql` 的做法)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 四、代码改动清单
|
|
|
+
|
|
|
+### 4.1 实体 `MesProductRecord.java`
|
|
|
+
|
|
|
+1. 列定义新增:
|
|
|
+ ```java
|
|
|
+ @Column(name="plan_id", attrName="planId", label="生产计划"),
|
|
|
+ ```
|
|
|
+2. 字段新增:`planId`、`planDay`(计划日期)、`planOrderId`(计划所属订单)、`orderSn`(生产订单号)、`shiftTitle`(班次)
|
|
|
+3. 恢复并启用 `joinTable`(当前是注释状态),做三级左连接:
|
|
|
+ ```java
|
|
|
+ joinTable={
|
|
|
+ @JoinTable(type=JoinTable.Type.LEFT_JOIN, entity=MesProductPlan.class, attrName="this", alias="b",
|
|
|
+ on="b.id = a.plan_id",
|
|
|
+ columns={ @Column(name="day", attrName="planDay", label="计划日期", queryType=QueryType.EQ) }),
|
|
|
+ @JoinTable(type=JoinTable.Type.LEFT_JOIN, entity=MesProductOrder.class, attrName="this", alias="c",
|
|
|
+ on="c.id = b.order_id",
|
|
|
+ columns={ @Column(name="sn", attrName="orderSn", label="生产订单号", queryType=QueryType.LIKE) }),
|
|
|
+ @JoinTable(type=JoinTable.Type.LEFT_JOIN, entity=MesShift.class, attrName="this", alias="d",
|
|
|
+ on="d.id = b.shift_id",
|
|
|
+ columns={ @Column(name="title", attrName="shiftTitle", label="班次", queryType=QueryType.LIKE) })
|
|
|
+ }
|
|
|
+ ```
|
|
|
+ > 注意:JeeSite 的 join 不会带出被 join 实体自身的 join 字段,所以 `orderSn` / `shiftTitle` 必须在 `MesProductRecord` 上重新声明(别名 b/c/d 之前被注释占用,现已释放)。
|
|
|
+4. `ExcelField` 增列:`生产订单号`、`计划日期`(导出用)
|
|
|
+
|
|
|
+### 4.2 `MesProductPlanService.java` 新增方法
|
|
|
+
|
|
|
+```java
|
|
|
+// 按 id 取计划并校验有效性(计划存在 + status='0' + 订单已下发)
|
|
|
+public MesProductPlan getValidPlan(String planId)
|
|
|
+
|
|
|
+// 取工件的 plan_id(来自 mes_product)
|
|
|
+public String getPlanIdByProductSn(String sn)
|
|
|
+
|
|
|
+// 把计划回写到工件
|
|
|
+public void bindPlanToProduct(String sn, String planId)
|
|
|
+```
|
|
|
+
|
|
|
+### 4.3 `MesProductRecordService.java`(核心)
|
|
|
+
|
|
|
+**新增统一校验方法**(放在 service 层,controller 只做参数解析,所有通道自动覆盖):
|
|
|
+
|
|
|
+```java
|
|
|
+/**
|
|
|
+ * 提交工位结果前校验:工件必须关联有效生产计划,否则拦截。
|
|
|
+ * @return 命中的计划;开关关闭时返回 null
|
|
|
+ */
|
|
|
+private MesProductPlan requirePlan(String sn, String cateSn, String isRepair)
|
|
|
+```
|
|
|
+执行顺序:
|
|
|
+1. 开关:`"0".equals(Global.getConfig("mes.plan.intercept"))` → 直接返回 null(不拦截)
|
|
|
+2. 返修:`"1".equals(isRepair)` → 跳过拦截(只做查询,不强制)
|
|
|
+3. 依次尝试 2.1 的三级来源;三级都取不到 → 抛 `ServiceException`
|
|
|
+4. 校验计划有效性(2.2 的 2、3 条)→ 不合法抛 `ServiceException`
|
|
|
+5. 命中第 3 级(自动匹配)时,回写 `mes_product.plan_id` 和 `mes_product_record.plan_id`(自愈)
|
|
|
+
|
|
|
+**接入点**(覆盖所有工位结果提交通道):
|
|
|
+
|
|
|
+| 接入方法 | 说明 |
|
|
|
+|---|---|
|
|
|
+| `updateRecordResult()` | **主路径**(PC 结果上传 / PDA / CCD 质量上传 / 返修 / `apiUpdateRecordResult` / `updateMultipleRecords` 内部都走这里)→ 在方法最前面校验 |
|
|
|
+| `updateRecord()` | 单工位结果回写(`update` 接口、解绑、终检等) |
|
|
|
+| `updateRecordParam()` | 带参数的结果提交 |
|
|
|
+| `updateUnbindRecord()` | 解绑回写 |
|
|
|
+
|
|
|
+**记录写入时回填 plan_id**:
|
|
|
+- `pcadd()` / `add()` / `sdadd()`:创建 `mpr1` 时 `mpr1.setPlanId(mesProductPlan.getId())`(这 3 处已调用 `resolvePlan()`,直接复用)
|
|
|
+- `save()` 兜底:`planId` 为空时按 `mes_product.plan_id` 补齐
|
|
|
+
|
|
|
+### 4.4 `MesProductRecordController.java`
|
|
|
+
|
|
|
+- `listData` / `listData2` / `exportData`:支持按 **生产订单号**、**计划日期** 过滤
|
|
|
+ ```java
|
|
|
+ if (StringUtils.isNotBlank(mesProductRecord.getOrderSn())) {
|
|
|
+ mesProductRecord.getSqlMap().getWhere().and("c.sn", QueryType.LIKE, mesProductRecord.getOrderSn());
|
|
|
+ }
|
|
|
+ ```
|
|
|
+- `listData`:批量回填「无计划」标识(`planId` 为空的记录标红)
|
|
|
+
|
|
|
+### 4.5 视图
|
|
|
+
|
|
|
+| 文件 | 改动 |
|
|
|
+|---|---|
|
|
|
+| `mesProductRecordList.html` | 搜索项加「生产订单号」「计划日期」;列表加列「生产订单」「计划日期」「班次」「计划状态」(无计划标红) |
|
|
|
+| `mesProductPlanList.html` | 搜索项「生产订单」由 orderId 精确下拉改为 **订单号模糊查询**;列表加「完成率」(nums2/nums) |
|
|
|
+| `mesProductOrderList.html` | 订单列表加「已排产 / 未排产」数量列(`mes_product_order_plan.html` 已有排产入口,可复用) |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 五、实施步骤(按序执行,每步可独立验证)
|
|
|
+
|
|
|
+| 步骤 | 内容 | 产出 |
|
|
|
+|---|---|---|
|
|
|
+| 1 | 执行 `db/mes_product_record_plan.sql`(加字段+索引+回填+开关) | DB 就绪 |
|
|
|
+| 2 | `MesProductRecord` 实体加 `plan_id` + 三级 join + 导出列 | 可查订单/计划 |
|
|
|
+| 3 | `MesProductPlanService` 加 `getValidPlan` / `getPlanIdByProductSn` / `bindPlanToProduct` | 复用能力 |
|
|
|
+| 4 | `MesProductRecordService` 加 `requirePlan()` + 4 个接入点 + 记录回填 | **拦截生效** |
|
|
|
+| 5 | 列表/导出/计划页/订单页加订单、计划维度 | 查询闭环 |
|
|
|
+| 6 | 回归测试(见下) | 验收 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 六、验证用例
|
|
|
+
|
|
|
+| # | 场景 | 预期 |
|
|
|
+|---|---|---|
|
|
|
+| 1 | 白班排了计划(订单已下发、BOM 维护了工件类型),创建 1 件 | 工件 `plan_id` 有值;记录 `plan_id` 有值 |
|
|
|
+| 2 | 当前班次**无计划**,创建工件 | `当前班次没有有效的生产计划,请先排产!`(已有逻辑保持) |
|
|
|
+| 3 | 当前班次无计划,用历史件(`mes_product.plan_id` 为空)提交工位结果 | 拦截:`该工件未关联生产计划...不能作业!` |
|
|
|
+| 4 | 工件有计划但计划被删除/停用 | 拦截:`该工件关联的生产计划已失效,不能作业!` |
|
|
|
+| 5 | 工件有计划但订单改成未下发 | 拦截:`该工件关联的生产订单未下发,不能作业!` |
|
|
|
+| 6 | 返修件提交(isRepair=1) | 放行,不影响计划产量 |
|
|
|
+| 7 | `mes.plan.intercept='0'` | 全部放行(应急) |
|
|
|
+| 8 | 生产记录列表按订单号过滤 | 只返回该订单下的记录;无计划件标红 |
|
|
|
+| 9 | 计划页「生产数量」统计 | 与 `mes_product` 中该计划下的件数一致 |
|
|
|
+
|
|
|
+**核查 SQL**
|
|
|
+```sql
|
|
|
+-- 无计划仍在生产的工件
|
|
|
+SELECT p.sn, p.cate, p.plan_id FROM mes_product p
|
|
|
+ WHERE (p.plan_id IS NULL OR p.plan_id='')
|
|
|
+ AND EXISTS (SELECT 1 FROM mes_product_record r WHERE r.sn = p.sn AND r.content <> 'UDF');
|
|
|
+
|
|
|
+-- 记录与工件计划不一致(应为 0)
|
|
|
+SELECT r.id, r.sn, r.plan_id, p.plan_id FROM mes_product_record r
|
|
|
+ JOIN mes_product p ON p.sn = r.sn
|
|
|
+ WHERE IFNULL(r.plan_id,'') <> IFNULL(p.plan_id,'');
|
|
|
+
|
|
|
+-- 计划未关联订单
|
|
|
+SELECT id, order_id, day, shift_id, nums FROM mes_product_plan
|
|
|
+ WHERE order_id IS NULL OR order_id = '';
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 七、风险与兼容
|
|
|
+
|
|
|
+| 风险 | 处理 |
|
|
|
+|---|---|
|
|
|
+| 存量工件无 `plan_id`,上线后立即被拦 | 步骤 1 的回填 SQL 解决大部分;剩余用开关 `mes.plan.intercept='0'` 过渡,排产后逐步收紧 |
|
|
|
+| 返修/补录/换件等非标准流程被误拦 | `isRepair='1'` 放行;必要时再加 `mes.plan.intercept.exempt.oprno` 白名单 |
|
|
|
+| join 增加列表查询成本 | 走 `plan_id` 索引 + 左连接;列表页本身已分页,影响可控;必要时改为按需批量回填(现有 listData 已是批量 Map 回填模式) |
|
|
|
+| `js_sys_config` 缓存 | 直插后需重启服务或后台重新保存一次 |
|
|
|
+| 多产线/多班次 | 计划取自**工件创建时**的班次,跨零点夜班沿用"班次开始日期"口径(`getCurrentPlan` 已处理) |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 八、待确认事项
|
|
|
+
|
|
|
+1. **拦截范围**:只在"提交工位结果"拦截,还是"创建工件"也要再次强校验(现在创建已强校验)?建议两处都保留。
|
|
|
+2. **返修是否放行**:建议放行(返修不占产量),若管理上要求返修也必须挂在有效计划下,去掉例外即可。
|
|
|
+3. **无计划件是否允许补录**:建议提供"按 日期+班次+工件类型 批量补关联计划"的后台工具/SQL,用于历史数据归集。
|
|
|
+4. **计划超产是否在提交时拦截**:建议否(数量在创建时已扣),如需则按 `mes_product.plan_id` 计数再校验一次。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 九、实施结果(已落地)
|
|
|
+
|
|
|
+### 9.1 改动清单
|
|
|
+
|
|
|
+| 文件 | 改动 |
|
|
|
+|---|---|
|
|
|
+| `db/mes_product_record_plan.sql`(新增) | `mes_product_record.plan_id` 字段 + `plan_id`/`sn` 索引 + 存量回填 + `mes.plan.intercept` 开关 + 验证 SQL |
|
|
|
+| `entity/MesProductRecord.java` | 新增 `plan_id` 列;启用 joinTable(b=计划 / c=订单 / d=班次);新增 `planId`/`planDay`/`orderSn`/`shiftTitle` 属性;导出加「生产订单号」「计划日期」 |
|
|
|
+| `entity/MesProductOrder.java` | 新增 `planNums`(已排产)/`unPlanNums`(未排产)展示字段 |
|
|
|
+| `dao/MesProductRecordDao.java` + `MesProductRecordDao.xml` | 新增 `findPlanIdBySn`(取工件已关联计划)、`updatePlanIdBySn`(回填记录计划);NG 查询 join 计划/订单/班次并支持订单号过滤;`updateDate` 去掉 `sqlMap.table.toSql()` |
|
|
|
+| `service/MesProductPlanService.java` | 新增 `getValidPlan`(计划有效+订单已下发校验)、`getPlanIdByProductSn`、`bindPlanToProduct` |
|
|
|
+| `service/MesProductRecordService.java` | 新增 `requirePlan()`(三级计划来源 + 拦截 + 自愈回写)、`isPlanInterceptEnabled()`(开关);接入 `updateRecordResult`/`updateRecord`/`updateRecordParam`/`updateUnbindRecord`;`pcadd`/`add`/`sdadd` 写入记录计划;`save`/`insertBatch` 兜底补计划 |
|
|
|
+| `web/MesProductRecordController.java` | 新增 `applyPlanFilter()`(订单号/计划日期过滤),应用于 `listData`/`listData2`/`exportData`;后面板新记录继承计划 |
|
|
|
+| `web/MesProductPlanController.java` | 计划列表支持按订单号模糊查询 |
|
|
|
+| `web/MesProductOrderController.java` | 订单列表统计已排产/未排产数量 |
|
|
|
+| 视图 `mesProductRecordList/List2/ListNg.html` | 搜索项加「生产订单号」,列表加「生产订单号」「计划日期」「班次」「计划状态(无计划标红)」 |
|
|
|
+| 视图 `mesProductPlanList.html` | 搜索项加「订单号」,列表加「完成率」 |
|
|
|
+| 视图 `mesProductOrderList.html` | 列表加「已排产」「未排产」 |
|
|
|
+
|
|
|
+### 9.2 顺带修复的两个问题
|
|
|
+
|
|
|
+| 问题 | 原因 | 处理 |
|
|
|
+|---|---|---|
|
|
|
+| `pcresult` 接口返修标记判断错误 | `isRepair = StringUtils.isEmpty(isRepair)?"0":"1"` → 客户端传 `isRepair=0`(正常)被当成返修,导致拦截被绕过 | 改为 `StringUtils.isEmpty(isRepair)?"0":isRepair`;服务层 `isRepair.equals("1")` 改为 `"1".equals(isRepair)` 防 NPE |
|
|
|
+| `updateDate` 原始 SQL 在有 join 的实体上报错 | `update ${sqlMap.table.toSql()}` 会带上 LEFT JOIN,`update_date`/`id` 出现二义性 | 改为写死表名 `update mes_product_record ...` / `update mes_product ...` |
|
|
|
+
|
|
|
+### 9.3 发布步骤
|
|
|
+
|
|
|
+1. **先执行** `db/mes_product_record_plan.sql`(必须先加字段,否则生产记录相关查询直接报错)
|
|
|
+2. 再发布应用
|
|
|
+3. `js_sys_config` 直插后需重启服务,或在"系统配置"页面重新保存一次触发缓存刷新
|
|
|
+4. 灰度建议:先把 `mes.plan.intercept` 置为 `0` 上线,确认存量数据回填正常后再置 `1` 开启拦截
|
|
|
+
|
|
|
+### 9.4 关键实现说明
|
|
|
+
|
|
|
+- **热路径无额外查询**:新建工件的记录在创建时就写入 `plan_id`,提交工位结果时直接从已加载的记录上取计划,不产生额外 SQL;只有历史无计划件才会走回退查询并自动回写(自愈)。
|
|
|
+- **关联字段查询**:JeeSite 会把 join 上的字段自动绑定为查询条件(`orderSn` 声明为 LIKE、`planDay` 声明为 EQ),控制器里另加了兜底条件,值一致不会冲突。
|
|
|
+- **拦截范围**:返修(`isRepair=1`)放行;开关关闭时全部放行;提交时**不再**校验超产数量(数量在创建工件时已扣)。
|
|
|
+
|