3 Komitmen a7167fc8a3 ... 234beeb03b

Pembuat SHA1 Pesan Tanggal
  hbx 234beeb03b feat: 下单选择产品类型,新增当前计划/当前产品接口 1 Minggu lalu
  hbx 353725095d docs: 补充无计划拦截方案的本地库执行验证记录 1 Minggu lalu
  hbx 0fdfaa4ea9 feat: 生产记录关联生产计划,提交工位结果无计划拦截 1 Minggu lalu
24 mengubah file dengan 1590 tambahan dan 31 penghapusan
  1. 9 1
      api.http
  2. 79 0
      db/mes_product_cate_link.sql
  3. 73 0
      db/mes_product_record_plan.sql
  4. 415 0
      docs/生产计划-无计划拦截-实施方案.md
  5. 6 0
      src/main/java/com/jeesite/modules/mes/dao/MesProductRecordDao.java
  6. 43 0
      src/main/java/com/jeesite/modules/mes/entity/MesProductOrder.java
  7. 26 1
      src/main/java/com/jeesite/modules/mes/entity/MesProductPlan.java
  8. 59 10
      src/main/java/com/jeesite/modules/mes/entity/MesProductRecord.java
  9. 111 0
      src/main/java/com/jeesite/modules/mes/resp/MesCurrentPlanItemResp.java
  10. 115 0
      src/main/java/com/jeesite/modules/mes/resp/MesCurrentPlanResp.java
  11. 45 1
      src/main/java/com/jeesite/modules/mes/service/MesProductOrderService.java
  12. 248 11
      src/main/java/com/jeesite/modules/mes/service/MesProductPlanService.java
  13. 132 1
      src/main/java/com/jeesite/modules/mes/service/MesProductRecordService.java
  14. 46 0
      src/main/java/com/jeesite/modules/mes/web/MesProductOrderController.java
  15. 29 0
      src/main/java/com/jeesite/modules/mes/web/MesProductPlanController.java
  16. 31 1
      src/main/java/com/jeesite/modules/mes/web/MesProductRecordController.java
  17. 1 1
      src/main/resources/mappings/modules/mes/MesProductDao.xml
  18. 26 3
      src/main/resources/mappings/modules/mes/MesProductRecordDao.xml
  19. 39 0
      src/main/resources/views/modules/mes/mesProductOrderForm.html
  20. 4 0
      src/main/resources/views/modules/mes/mesProductOrderList.html
  21. 16 1
      src/main/resources/views/modules/mes/mesProductPlanList.html
  22. 24 0
      src/main/resources/views/modules/mes/mesProductRecordList.html
  23. 6 0
      src/main/resources/views/modules/mes/mesProductRecordList2.html
  24. 7 0
      src/main/resources/views/modules/mes/mesProductRecordListNg.html

+ 9 - 1
api.http

@@ -454,4 +454,12 @@ token=1&pageNo=1&pageSize=20&__ajax=json&__sid=40ca1cc4f8a54d3a8c0cd327d30b4998&
 
 ###
 
-###__ajax=json&oprno=OP120A&lineSn=XT&sn=12345678&workNum=system&cxm=3&result=&testPressure=KyAxNzguMDA%3D&testPressureUnit=kPa&leakVal=KyAgIDAuMDA%3D&leakValUnit=Pa&testTime=MjAyNS0wOS0yNSAxMToyNzoyOA%3D%3D&title=5Y2K5oiQ5ZOB&remark=Stop&deviceType=innomatec&cq=&by=&cs=&leakRate=&leakRateUnit=&type=
+###__ajax=json&oprno=OP120A&lineSn=XT&sn=12345678&workNum=system&cxm=3&result=&testPressure=KyAxNzguMDA%3D&testPressureUnit=kPa&leakVal=KyAgIDAuMDA%3D&leakValUnit=Pa&testTime=MjAyNS0wOS0yNSAxMToyNzoyOA%3D%3D&title=5Y2K5oiQ5ZOB&remark=Stop&deviceType=innomatec&cq=&by=&cs=&leakRate=&leakRateUnit=&type=
+###
+
+# 获取当前班次的生产计划与当前产品(工件类型)
+# 返回:当前班次信息 + 当前产品(cateSn/cateTitle) + 当前计划明细(计划数量/已生产数量/订单号)
+POST http://127.0.0.1:8980/js/a/mes/mesProductPlan/currentPlan
+Content-Type: application/x-www-form-urlencoded
+
+__ajax=json&__sid=f9cea112ef6242cf820d950f870fb520

+ 79 - 0
db/mes_product_cate_link.sql

@@ -0,0 +1,79 @@
+-- 订单-计划-产品 全链路:增加"产品类型(工件类型)"字段
+-- 背景:
+--   1) 生产订单没有产品类型字段,产品类型只能从 BOM 的 product_cate_id 推导,
+--      BOM 未维护工件类型时订单无法排产、计划无法匹配产品;
+--   2) 生产计划没有产品类型字段,只能通过 计划->订单->BOM 三级关联反查,
+--      既慢又容易因 BOM 变更导致历史计划的产品类型错乱;
+--   3) 需要"当前计划/当前产品"接口,客户端/下单页面据此判断当前在生产哪种产品。
+-- 本脚本负责:
+--   1) mes_product_order 增加 product_cate_id(产品类型,存 mes_product_cate.sn)
+--   2) mes_product_plan  增加 product_cate_id(产品类型,存 mes_product_cate.sn)
+--   3) 存量数据回填
+-- 说明:字段必须先执行本脚本再发布应用。
+
+-- ---------- 1. 生产订单增加产品类型 ----------
+SET @col_exists = (SELECT COUNT(1) FROM information_schema.COLUMNS
+  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mes_product_order' AND COLUMN_NAME = 'product_cate_id');
+SET @ddl = IF(@col_exists = 0,
+  'ALTER TABLE `mes_product_order` ADD COLUMN `product_cate_id` VARCHAR(50) DEFAULT NULL COMMENT ''产品类型(工件类型编号)'' AFTER `bom_id`',
+  'SELECT ''mes_product_order.product_cate_id 已存在'' AS message');
+PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- ---------- 2. 生产计划增加产品类型 ----------
+SET @col_exists = (SELECT COUNT(1) FROM information_schema.COLUMNS
+  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mes_product_plan' AND COLUMN_NAME = 'product_cate_id');
+SET @ddl = IF(@col_exists = 0,
+  'ALTER TABLE `mes_product_plan` ADD COLUMN `product_cate_id` VARCHAR(50) DEFAULT NULL COMMENT ''产品类型(工件类型编号)'' AFTER `order_id`',
+  'SELECT ''mes_product_plan.product_cate_id 已存在'' AS message');
+PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- ---------- 3. 存量回填:订单按 BOM 的工件类型补齐 ----------
+UPDATE `mes_product_order` o
+  JOIN `wms_bom` b ON b.`id` = o.`bom_id`
+  SET o.`product_cate_id` = b.`product_cate_id`
+ WHERE (o.`product_cate_id` IS NULL OR o.`product_cate_id` = '')
+   AND b.`product_cate_id` IS NOT NULL AND b.`product_cate_id` <> '';
+
+-- ---------- 4. 存量回填:计划按订单的产品类型补齐(订单为空时用 BOM 兜底) ----------
+UPDATE `mes_product_plan` p
+  JOIN `mes_product_order` o ON o.`id` = p.`order_id`
+  SET p.`product_cate_id` = IFNULL(NULLIF(o.`product_cate_id`, ''),
+        (SELECT b.`product_cate_id` FROM `wms_bom` b WHERE b.`id` = o.`bom_id`))
+ WHERE p.`product_cate_id` IS NULL OR p.`product_cate_id` = '';
+
+-- ---------- 5. 索引 ----------
+SET @idx_exists = (SELECT COUNT(1) FROM information_schema.STATISTICS
+  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mes_product_order' AND INDEX_NAME = 'idx_mpo_product_cate');
+SET @ddl = IF(@idx_exists = 0,
+  'CREATE INDEX `idx_mpo_product_cate` ON `mes_product_order` (`product_cate_id`)',
+  'SELECT ''idx_mpo_product_cate 已存在'' AS message');
+PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @idx_exists = (SELECT COUNT(1) FROM information_schema.STATISTICS
+  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mes_product_plan' AND INDEX_NAME = 'idx_mpp_product_cate');
+SET @ddl = IF(@idx_exists = 0,
+  'CREATE INDEX `idx_mpp_product_cate` ON `mes_product_plan` (`product_cate_id`)',
+  'SELECT ''idx_mpp_product_cate 已存在'' AS message');
+PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- ---------- 6. 验证 ----------
+-- 6.1 字段
+SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, COLUMN_COMMENT FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ('mes_product_order','mes_product_plan')
+   AND COLUMN_NAME = 'product_cate_id';
+-- 6.2 订单未回填(这些订单需在下单时补选产品类型)
+SELECT id, sn, bom_id FROM mes_product_order
+ WHERE product_cate_id IS NULL OR product_cate_id = '';
+-- 6.3 计划未回填(同上,排产时按订单自动补齐)
+SELECT p.id, p.order_id, p.day, p.nums FROM mes_product_plan p
+ WHERE p.product_cate_id IS NULL OR p.product_cate_id = '';
+-- 6.4 计划与订单产品类型不一致(应为 0 行)
+SELECT p.id, p.product_cate_id AS plan_cate, o.product_cate_id AS order_cate
+  FROM mes_product_plan p JOIN mes_product_order o ON o.id = p.order_id
+ WHERE IFNULL(p.product_cate_id,'') <> IFNULL(o.product_cate_id,'');
+-- 6.5 计划与产品类型主数据不一致(应为 0 行)
+SELECT DISTINCT p.product_cate_id FROM mes_product_plan p
+ WHERE p.product_cate_id IS NOT NULL AND p.product_cate_id <> ''
+   AND NOT EXISTS (SELECT 1 FROM mes_product_cate c WHERE c.sn = p.product_cate_id);
+
+SELECT '订单-计划 产品类型脚本执行完成' AS message;

+ 73 - 0
db/mes_product_record_plan.sql

@@ -0,0 +1,73 @@
+-- 生产记录关联生产计划(订单-计划-产品-记录 全链路 / 无计划拦截)
+-- 背景:
+--   1) mes_product_record(生产记录/工位结果)没有计划字段,工位结果层面无法追溯订单与计划;
+--   2) 提交工位结果时没有"无计划"校验,无计划工件照样可以过站;
+--   3) 生产记录列表/导出无法按订单、计划查询。
+-- 本脚本负责:加字段 + 加索引 + 存量回填 + 拦截总开关。
+-- 说明:结构补齐脚本,字段必须先执行本脚本再发布应用,否则生产记录相关查询会报错。
+
+-- ---------- 1. 生产记录表增加生产计划字段 ----------
+SET @col_exists = (SELECT COUNT(1) FROM information_schema.COLUMNS
+  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mes_product_record' AND COLUMN_NAME = 'plan_id');
+SET @ddl = IF(@col_exists = 0,
+  'ALTER TABLE `mes_product_record` ADD COLUMN `plan_id` VARCHAR(64) DEFAULT NULL COMMENT ''生产计划'' AFTER `cate_sn`',
+  'SELECT ''mes_product_record.plan_id 已存在'' AS message');
+PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- ---------- 2. 索引:按计划/订单查记录 ----------
+SET @idx_exists = (SELECT COUNT(1) FROM information_schema.STATISTICS
+  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mes_product_record' AND INDEX_NAME = 'idx_mpr_plan_id');
+SET @ddl = IF(@idx_exists = 0,
+  'CREATE INDEX `idx_mpr_plan_id` ON `mes_product_record` (`plan_id`)',
+  'SELECT ''idx_mpr_plan_id 已存在'' AS message');
+PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- ---------- 2.1 索引:按工件码查记录(无计划拦截回退查询用)----------
+-- 注意:mes_product_record 数据量较大时建索引耗时较长,可在低峰期执行
+SET @idx_exists = (SELECT COUNT(1) FROM information_schema.STATISTICS
+  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mes_product_record' AND COLUMN_NAME = 'sn');
+SET @ddl = IF(@idx_exists = 0,
+  'CREATE INDEX `idx_mpr_sn` ON `mes_product_record` (`sn`)',
+  'SELECT ''mes_product_record.sn 已有索引,跳过'' AS message');
+PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- ---------- 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=拦截(提交工位结果时工件必须关联有效生产计划)
+-- 注意:直插 js_sys_config 后需重启服务,或在"系统配置"页面重新保存一次触发缓存刷新
+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
+  ('2026102110000001','生产计划无计划拦截','mes.plan.intercept','1','0','system',NOW(),'system',NOW(),
+   '提交工位结果时校验工件是否关联有效生产计划;0=关闭拦截,1=开启拦截');
+
+-- ---------- 5. 验证 ----------
+-- 5.1 字段与索引
+SELECT COLUMN_NAME, COLUMN_TYPE, COLUMN_COMMENT FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mes_product_record' AND COLUMN_NAME = 'plan_id';
+-- 5.2 仍有记录未回填(这些工件在生产但没有计划,发布后会拦截)
+SELECT r.sn, r.cate_sn, p.plan_id AS product_plan_id
+  FROM mes_product_record r
+  LEFT JOIN mes_product p ON p.sn = r.sn
+ WHERE (r.plan_id IS NULL OR r.plan_id = '') AND r.content <> 'UDF'
+ LIMIT 100;
+-- 5.3 记录与工件计划不一致(应为 0 行)
+SELECT r.id, r.sn, r.plan_id AS record_plan, p.plan_id AS product_plan
+  FROM mes_product_record r
+  JOIN mes_product p ON p.sn = r.sn
+ WHERE IFNULL(r.plan_id,'') <> IFNULL(p.plan_id,'')
+ LIMIT 100;
+-- 5.4 未关联订单的计划(应为 0 行)
+SELECT id, order_id, day, shift_id, nums FROM mes_product_plan
+ WHERE order_id IS NULL OR order_id = '';
+-- 5.5 开关
+SELECT config_key, config_value FROM js_sys_config WHERE config_key = 'mes.plan.intercept';
+
+SELECT '生产记录关联生产计划脚本执行完成' AS message;

+ 415 - 0
docs/生产计划-无计划拦截-实施方案.md

@@ -0,0 +1,415 @@
+# 订单-计划-产品-记录 全链路 & 无计划拦截 实施方案
+
+> 关联提交:`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` 开启拦截
+
+**本地库已执行验证**(localhost:3307 / mes_cloud_xz,MySQL 5.7):
+
+| 检查项 | 结果 |
+|---|---|
+| `mes_product_record.plan_id` 字段 | 已创建 `varchar(64)` |
+| 索引 `idx_mpr_plan_id` | 已创建;`sn` 索引原本就有,脚本自动跳过 |
+| 存量回填 | 47 条记录(对应唯一有计划的工件) |
+| 记录与工件计划不一致 | 0 行 ✓ |
+| 未关联订单的计划 | 0 行 ✓ |
+| 三级 join 查询 / 订单号+计划日期过滤 / 回退查询 / 回填语句 / NG 查询 | 全部实跑通过 |
+
+> 本地库现状:工件 6361 个,仅 1 个有计划;`mes.plan.intercept` 当前为 `1`(拦截已开)。
+> 本地调试提交工位结果前请先排产,否则会被"无计划"拦截;生产环境同样建议先置 `0` 灰度。
+> 库中另有 2 条历史脏计划(2026-04-16/17,nums=400,`shift_id='1'` 占位值,关联订单已不存在),
+> 因无工件引用、也匹配不到真实班次,不影响拦截,建议按 `mes_product_plan_link_fix.sql` 的说明人工清理。
+
+### 9.4 关键实现说明
+
+- **热路径无额外查询**:新建工件的记录在创建时就写入 `plan_id`,提交工位结果时直接从已加载的记录上取计划,不产生额外 SQL;只有历史无计划件才会走回退查询并自动回写(自愈)。
+- **关联字段查询**:JeeSite 会把 join 上的字段自动绑定为查询条件(`orderSn` 声明为 LIKE、`planDay` 声明为 EQ),控制器里另加了兜底条件,值一致不会冲突。
+- **拦截范围**:返修(`isRepair=1`)放行;开关关闭时全部放行;提交时**不再**校验超产数量(数量在创建工件时已扣)。
+
+---
+
+## 十、下单选择产品类型 + 当前计划/当前产品接口(已落地)
+
+### 10.1 目标
+
+把 **订单 → 计划 → 产品** 三段链路显式化:
+
+```
+生产订单.product_cate_id  →  生产计划.product_cate_id  →  工件(mes_product.cate)
+        (下单时选择)              (排产时继承订单)
+```
+
+- 下单时可直接选择产品类型(工件类型),不再强依赖 BOM 是否维护了工件类型;
+- 选择 BOM 时自动带出其工件类型;新建订单时按**当前计划的产品类型**默认带出;
+- 提供接口获取当前班次的计划与当前产品,客户端/下单页面据此判断"现在在生产什么"。
+
+### 10.2 数据模型(脚本 `db/mes_product_cate_link.sql`,已执行)
+
+| 表 | 新增字段 | 说明 |
+|---|---|---|
+| `mes_product_order` | `product_cate_id` | 产品类型(存 `mes_product_cate.sn`) |
+| `mes_product_plan` | `product_cate_id` | 产品类型(存 `mes_product_cate.sn`) |
+
+存量回填:订单按 BOM 的工件类型补齐;计划按订单的产品类型补齐(订单为空时用 BOM 兜底)。
+
+> 约定:全系统"工件类型"都用 `mes_product_cate.sn`(如 `CZ060`),与 `mes_line.product_cate`、
+> `wms_bom.product_cate_id` 保持一致(`wms_bom` 实体里按 id 关联的那处是历史遗留,不影响本链路)。
+
+### 10.3 下单(生产订单)
+
+| 位置 | 改动 |
+|---|---|
+| `MesProductOrderService.checkProductCate()` | 保存时校验:未选择产品类型 → 按 BOM 的工件类型自动补齐;所选类型必须存在且启用;与 BOM 的工件类型不一致 → 报错「所选产品类型与BOM的工件类型(CZ060)不一致,请核对后重试!」 |
+| `MesProductOrderController.form()` | 下发产品类型下拉(`mes_product_cate`,status=0)、BOM→工件类型映射、当前计划的产品类型 |
+| `mesProductOrderForm.html` | 新增「产品类型」下拉(必填);选择 BOM 自动带出工件类型;新建订单按当前计划的产品类型默认带出 |
+| `mesProductOrderList.html` | 新增「产品类型」「产品类型编号」列 |
+
+### 10.4 排产(生产计划)
+
+| 位置 | 改动 |
+|---|---|
+| `MesProductPlanService.save()` | 产品类型优先取订单选择的产品类型,订单未维护时用 BOM 兜底;写入 `plan.product_cate_id`;"同日期+班次+产品唯一"校验改用 `plan.product_cate_id`;编辑计划时产品类型跟随订单同步 |
+| `MesProductPlanService.getCurrentPlan(cateSn)` | 匹配条件由 BOM 关联改为 `plan.product_cate_id = cateSn`(不再依赖 BOM 是否维护工件类型) |
+| `mesProductPlanList.html` | 新增「产品类型」列 |
+
+### 10.5 接口:获取当前计划 / 当前产品
+
+```
+POST /mes/mesProductPlan/currentPlan
+```
+
+返回示例:
+
+```json
+{
+  "result": "true",
+  "message": "成功",
+  "data": {
+    "shiftId": "1700688350585237504",
+    "shiftTitle": "白班",
+    "shiftStart": "2026-10-21 08:00",
+    "shiftEnd": "2026-10-21 20:00",
+    "day": "2026-10-21",
+    "cateSn": "CZ060",
+    "cateTitle": "CZ060",
+    "planNums": 3,
+    "producedNums": 0,
+    "plans": [{
+      "planId": "2112728390803562496",
+      "orderId": "2112728164835434496",
+      "orderSn": "202610216998016",
+      "bomTitle": "初版AD02-BOM",
+      "cateSn": "CZ060",
+      "cateTitle": "CZ060",
+      "day": "2026-10-21",
+      "shiftTitle": "白班",
+      "nums": 3,
+      "produced": 0
+    }]
+  }
+}
+```
+
+- `cateSn`/`cateTitle`:**当前产品**(当前班次只有一个产品类型时给出;存在多个产品类型时为 null,用 `plans` 自行判断)
+- `plans`:**当前计划**明细(含订单号、计划数量、已生产数量)
+- 其它实现:`MesProductPlanService.getCurrentShiftPlans()` / `getCurrentProductCate()` / `getCurrentPlanData()`
+
+### 10.6 本地库执行验证(localhost:3307 / mes_cloud_xz)
+
+| 检查项 | 结果 |
+|---|---|
+| `mes_product_order.product_cate_id` | 已创建,4 个订单全部回填为 `CZ060` |
+| `mes_product_plan.product_cate_id` | 已创建,5/7 条计划回填为 `CZ060` |
+| 计划与订单产品类型不一致 | 0 行 ✓ |
+| 当前计划查询(白班 2026-10-21) | 返回 1 条:订单 `202610216998016`,CZ060,3 件 ✓ |
+| 订单列表 join 产品类型 | 正常 ✓ |
+
+> 剩余 2 条未回填的计划是历史脏数据(2026-04-16/17,关联订单已不存在、`shift_id='1'` 占位值),
+> 产品类型无从推导,且匹配不到真实班次,不影响排产与拦截,建议人工清理。
+
+

+ 6 - 0
src/main/java/com/jeesite/modules/mes/dao/MesProductRecordDao.java

@@ -27,4 +27,10 @@ public interface MesProductRecordDao extends CrudDao<MesProductRecord> {
 
     // 新增特定查询接口
     List<MesProductRecord> findNgList(MesProductRecord mesProductRecord);
+
+    // 查询工件已关联的生产计划(记录级)
+    String findPlanIdBySn(@Param("sn") String sn);
+
+    // 回填工件所有记录的生产计划
+    int updatePlanIdBySn(@Param("sn") String sn, @Param("planId") String planId);
 }

+ 43 - 0
src/main/java/com/jeesite/modules/mes/entity/MesProductOrder.java

@@ -23,6 +23,7 @@ import com.jeesite.modules.wms.entity.*;
 		@Column(name="nums", attrName="nums", label="生产数量", isUpdateForce=true),
 		@Column(name="delivery_date", attrName="deliveryDate", label="交货日期", isUpdateForce=true),
 		@Column(name="bom_id", attrName="bomId", label="BOM"),
+		@Column(name="product_cate_id", attrName="productCateId", label="产品类型"),
 		@Column(name="wms_order_id", attrName="wmsOrderId", label="销售单"),
 		@Column(name="remark", attrName="remark", label="备注"),
 		@Column(name="state", attrName="state", label="状态"),
@@ -42,6 +43,12 @@ import com.jeesite.modules.wms.entity.*;
 				columns={
 						@Column(name="order", attrName="wmsOrderSn", label="销售单号", queryType=QueryType.LIKE),
 				}),
+		// 产品类型(工件类型):存 mes_product_cate.sn
+		@JoinTable(type= JoinTable.Type.LEFT_JOIN, entity= MesProductCate.class, attrName="this", alias="e",
+				on="e.sn = a.product_cate_id",
+				columns={
+						@Column(name="title", attrName="productCateTitle", label="产品类型", queryType=QueryType.LIKE),
+				}),
 	}, orderBy="a.update_date DESC"
 )
 public class MesProductOrder extends DataEntity<MesProductOrder> {
@@ -51,11 +58,15 @@ public class MesProductOrder extends DataEntity<MesProductOrder> {
 	private Integer nums;		// 生产数量
 	private Date deliveryDate;		// 交货日期
 	private String bomId;		// BOM
+	private String productCateId;		// 产品类型(工件类型编号)
+	private String productCateTitle;		// 产品类型名称
 	private String remark;		// 备注
 	private String state;		// 状态
 	private String bomTitle;
 	private String wmsOrderId;
 	private String wmsOrderSn;
+	private Integer planNums;		// 已排产数量
+	private Integer unPlanNums;		// 未排产数量
 
 	public MesProductOrder() {
 		this(null);
@@ -99,6 +110,22 @@ public class MesProductOrder extends DataEntity<MesProductOrder> {
 	public void setBomId(String bomId) {
 		this.bomId = bomId;
 	}
+
+	public String getProductCateId() {
+		return productCateId;
+	}
+
+	public void setProductCateId(String productCateId) {
+		this.productCateId = productCateId;
+	}
+
+	public String getProductCateTitle() {
+		return productCateTitle;
+	}
+
+	public void setProductCateTitle(String productCateTitle) {
+		this.productCateTitle = productCateTitle;
+	}
 	
 	@Size(min=0, max=255, message="备注长度不能超过 255 个字符")
 	public String getRemark() {
@@ -141,4 +168,20 @@ public class MesProductOrder extends DataEntity<MesProductOrder> {
 	public void setWmsOrderId(String wmsOrderId) {
 		this.wmsOrderId = wmsOrderId;
 	}
+
+	public Integer getPlanNums() {
+		return planNums;
+	}
+
+	public void setPlanNums(Integer planNums) {
+		this.planNums = planNums;
+	}
+
+	public Integer getUnPlanNums() {
+		return unPlanNums;
+	}
+
+	public void setUnPlanNums(Integer unPlanNums) {
+		this.unPlanNums = unPlanNums;
+	}
 }

+ 26 - 1
src/main/java/com/jeesite/modules/mes/entity/MesProductPlan.java

@@ -22,6 +22,7 @@ import com.jeesite.modules.wms.entity.WmsSupplier;
 @Table(name="mes_product_plan", alias="a", label="生产计划信息", columns={
 		@Column(name="id", attrName="id", label="id", isPK=true),
 		@Column(name="order_id", attrName="orderId", label="生产订单"),
+		@Column(name="product_cate_id", attrName="productCateId", label="产品类型"),
 		@Column(name="day", attrName="day", label="日期", isUpdateForce=true),
 		@Column(name="shift_id", attrName="shiftId", label="班次"),
 		@Column(name="is_pick", attrName="isPick", label="是否已生成领料单"),
@@ -46,7 +47,13 @@ import com.jeesite.modules.wms.entity.WmsSupplier;
 				on="d.id = c.bom_id",
 				columns={
 						@Column(name="title", attrName="bomTitle", label="BOM名称", queryType=QueryType.LIKE),
-						@Column(name="product_cate_id", attrName="bomProductCateId", label="工件类型", queryType=QueryType.EQ),
+						@Column(name="product_cate_id", attrName="bomProductCateId", label="BOM工件类型", queryType=QueryType.EQ),
+				}),
+		// 产品类型名称(取自生产计划自身的 product_cate_id)
+		@JoinTable(type= JoinTable.Type.LEFT_JOIN, entity= MesProductCate.class, attrName="this", alias="e",
+				on="e.sn = a.product_cate_id",
+				columns={
+						@Column(name="title", attrName="productCateTitle", label="产品类型", queryType=QueryType.LIKE),
 				}),
 }, orderBy="a.update_date DESC"
 )
@@ -54,6 +61,8 @@ public class MesProductPlan extends DataEntity<MesProductPlan> {
 	
 	private static final long serialVersionUID = 1L;
 	private String orderId;		// 生产订单
+	private String productCateId;		// 产品类型(工件类型编号)
+	private String productCateTitle;		// 产品类型名称
 	@JsonFormat(pattern = "yyyy-MM-dd")
 	private Date day;		// 日期
 	private String shiftId;		// 班次
@@ -81,6 +90,22 @@ public class MesProductPlan extends DataEntity<MesProductPlan> {
 	public void setOrderId(String orderId) {
 		this.orderId = orderId;
 	}
+
+	public String getProductCateId() {
+		return productCateId;
+	}
+
+	public void setProductCateId(String productCateId) {
+		this.productCateId = productCateId;
+	}
+
+	public String getProductCateTitle() {
+		return productCateTitle;
+	}
+
+	public void setProductCateTitle(String productCateTitle) {
+		this.productCateTitle = productCateTitle;
+	}
 	
 	@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
 	public Date getDay() {

+ 59 - 10
src/main/java/com/jeesite/modules/mes/entity/MesProductRecord.java

@@ -28,6 +28,7 @@ import java.util.List;
 		@Column(name="craft", attrName="craft", label="工艺号", queryType=QueryType.EQ),
 		@Column(name="serial", attrName="serial", label="排序", isUpdateForce=true),
 		@Column(name="cate_sn", attrName="cateSn", label="工件类型", queryType=QueryType.LIKE),
+		@Column(name="plan_id", attrName="planId", label="生产计划"),
 		@Column(name="line_sn", attrName="lineSn", label="产线编号", queryType=QueryType.LIKE),
 		@Column(name="source", attrName="source", label="来源", queryType=QueryType.EQ),
 		@Column(name="materiel_sn", attrName="materielSn", label="物料批次二维码", queryType=QueryType.LIKE),
@@ -40,17 +41,26 @@ import java.util.List;
 		@Column(name="create_date", attrName="createDate", label="create_date", isUpdate=false,  isUpdateForce=true),
 		@Column(name="update_by", attrName="updateBy", label="update_by", isQuery=false),
 		@Column(name="update_date", attrName="updateDate", label="update_date", isUpdateForce=true),
+}, joinTable={
+		// 1. 关联生产计划(别名 b):记录 -> 计划
+		@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),
+				}),
+		// 2. 关联生产订单(别名 c):计划 -> 订单
+		@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),
+				}),
+		// 3. 关联班次(别名 d):计划 -> 班次
+		@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),
+				}),
 }
-//		joinTable={
-				// 1. 关联 MesTemplateItems 模板表 (别名 b)
-				// 注意:这里强烈建议用 LEFT_JOIN,如果用 JOIN(内连接),那么没有关联模板的记录也会查不出来
-//				@JoinTable(type= JoinTable.Type.LEFT_JOIN, entity=MesTemplateItems.class, attrName="this", alias="b",
-//						on="b.id = a.tid",
-//						columns={
-//								@Column(name="cate", attrName="messageType", label="报文类型"),
-//								@Column(name="title", attrName="title", label="报文名称"),
-//								@Column(name="oprno", attrName="oprno2", label="模板工位号"),
-//						}),
 )
 public class MesProductRecord extends DataEntity<MesProductRecord> {
 
@@ -86,6 +96,10 @@ public class MesProductRecord extends DataEntity<MesProductRecord> {
 	private String auto;
 	private String oprnoName; //工位名称
 	private String isRepair;//是否返修;1=是,2=否
+	private String planId;		// 生产计划
+	private Date planDay;		// 计划日期(关联生产计划)
+	private String orderSn;		// 生产订单号(关联生产订单)
+	private String shiftTitle;		// 班次(关联班次)
 
 
 
@@ -101,6 +115,8 @@ public class MesProductRecord extends DataEntity<MesProductRecord> {
 			@ExcelField(title="绑定内容", attrName="materielSn", align= ExcelField.Align.CENTER, sort=60),
 			@ExcelField(title="工件类型", attrName="cateSn", align= ExcelField.Align.CENTER, sort=70),
 			@ExcelField(title="产线编号", attrName="lineSn", align= ExcelField.Align.CENTER, sort=80),
+			@ExcelField(title="生产订单号", attrName="orderSn", align= ExcelField.Align.CENTER, sort=85),
+			@ExcelField(title="计划日期", attrName="planDay", align= ExcelField.Align.CENTER, sort=86, dataFormat="yyyy-MM-dd"),
 			@ExcelField(title="备注", attrName="remark", align= ExcelField.Align.CENTER, sort=90),
 			@ExcelField(title="更新日期", attrName="updateDate", align= ExcelField.Align.CENTER, sort=150, dataFormat="yyyy-MM-dd hh:mm:ss"),
 	})
@@ -384,4 +400,37 @@ public class MesProductRecord extends DataEntity<MesProductRecord> {
 	public void setOprnoName(String oprnoName) {
 		this.oprnoName = oprnoName;
 	}
+
+	public String getPlanId() {
+		return planId;
+	}
+
+	public void setPlanId(String planId) {
+		this.planId = planId;
+	}
+
+	@JsonFormat(pattern = "yyyy-MM-dd")
+	public Date getPlanDay() {
+		return planDay;
+	}
+
+	public void setPlanDay(Date planDay) {
+		this.planDay = planDay;
+	}
+
+	public String getOrderSn() {
+		return orderSn;
+	}
+
+	public void setOrderSn(String orderSn) {
+		this.orderSn = orderSn;
+	}
+
+	public String getShiftTitle() {
+		return shiftTitle;
+	}
+
+	public void setShiftTitle(String shiftTitle) {
+		this.shiftTitle = shiftTitle;
+	}
 }

+ 111 - 0
src/main/java/com/jeesite/modules/mes/resp/MesCurrentPlanItemResp.java

@@ -0,0 +1,111 @@
+package com.jeesite.modules.mes.resp;
+
+/**
+ * 当前班次生产计划 明细
+ *
+ * @author hzd
+ * @version 2026-10-21
+ */
+public class MesCurrentPlanItemResp {
+
+	/** 生产计划id */
+	private String planId;
+	/** 生产订单id */
+	private String orderId;
+	/** 生产订单号 */
+	private String orderSn;
+	/** BOM名称 */
+	private String bomTitle;
+	/** 产品类型(工件类型编号) */
+	private String cateSn;
+	/** 产品类型名称 */
+	private String cateTitle;
+	/** 计划日期 */
+	private String day;
+	/** 班次名称 */
+	private String shiftTitle;
+	/** 计划数量 */
+	private Integer nums;
+	/** 已生产数量 */
+	private Integer produced;
+
+	public String getPlanId() {
+		return planId;
+	}
+
+	public void setPlanId(String planId) {
+		this.planId = planId;
+	}
+
+	public String getOrderId() {
+		return orderId;
+	}
+
+	public void setOrderId(String orderId) {
+		this.orderId = orderId;
+	}
+
+	public String getOrderSn() {
+		return orderSn;
+	}
+
+	public void setOrderSn(String orderSn) {
+		this.orderSn = orderSn;
+	}
+
+	public String getBomTitle() {
+		return bomTitle;
+	}
+
+	public void setBomTitle(String bomTitle) {
+		this.bomTitle = bomTitle;
+	}
+
+	public String getCateSn() {
+		return cateSn;
+	}
+
+	public void setCateSn(String cateSn) {
+		this.cateSn = cateSn;
+	}
+
+	public String getCateTitle() {
+		return cateTitle;
+	}
+
+	public void setCateTitle(String cateTitle) {
+		this.cateTitle = cateTitle;
+	}
+
+	public String getDay() {
+		return day;
+	}
+
+	public void setDay(String day) {
+		this.day = day;
+	}
+
+	public String getShiftTitle() {
+		return shiftTitle;
+	}
+
+	public void setShiftTitle(String shiftTitle) {
+		this.shiftTitle = shiftTitle;
+	}
+
+	public Integer getNums() {
+		return nums;
+	}
+
+	public void setNums(Integer nums) {
+		this.nums = nums;
+	}
+
+	public Integer getProduced() {
+		return produced;
+	}
+
+	public void setProduced(Integer produced) {
+		this.produced = produced;
+	}
+}

+ 115 - 0
src/main/java/com/jeesite/modules/mes/resp/MesCurrentPlanResp.java

@@ -0,0 +1,115 @@
+package com.jeesite.modules.mes.resp;
+
+import java.util.List;
+
+/**
+ * 当前班次生产计划/当前产品 返回数据
+ *
+ * 用于:客户端查询当前在生产什么产品;下订单时根据当前计划判断产品类型。
+ *
+ * @author hzd
+ * @version 2026-10-21
+ */
+public class MesCurrentPlanResp {
+
+	/** 当前班次id */
+	private String shiftId;
+	/** 当前班次名称 */
+	private String shiftTitle;
+	/** 当前班次开始时间 */
+	private String shiftStart;
+	/** 当前班次结束时间 */
+	private String shiftEnd;
+	/** 计划日期(班次开始日期) */
+	private String day;
+	/** 当前产品(工件类型编号,mes_product_cate.sn) */
+	private String cateSn;
+	/** 当前产品名称 */
+	private String cateTitle;
+	/** 当前计划数量合计 */
+	private Integer planNums;
+	/** 已生产数量合计 */
+	private Integer producedNums;
+	/** 当前班次计划明细 */
+	private List<MesCurrentPlanItemResp> plans;
+
+	public String getShiftId() {
+		return shiftId;
+	}
+
+	public void setShiftId(String shiftId) {
+		this.shiftId = shiftId;
+	}
+
+	public String getShiftTitle() {
+		return shiftTitle;
+	}
+
+	public void setShiftTitle(String shiftTitle) {
+		this.shiftTitle = shiftTitle;
+	}
+
+	public String getShiftStart() {
+		return shiftStart;
+	}
+
+	public void setShiftStart(String shiftStart) {
+		this.shiftStart = shiftStart;
+	}
+
+	public String getShiftEnd() {
+		return shiftEnd;
+	}
+
+	public void setShiftEnd(String shiftEnd) {
+		this.shiftEnd = shiftEnd;
+	}
+
+	public String getDay() {
+		return day;
+	}
+
+	public void setDay(String day) {
+		this.day = day;
+	}
+
+	public String getCateSn() {
+		return cateSn;
+	}
+
+	public void setCateSn(String cateSn) {
+		this.cateSn = cateSn;
+	}
+
+	public String getCateTitle() {
+		return cateTitle;
+	}
+
+	public void setCateTitle(String cateTitle) {
+		this.cateTitle = cateTitle;
+	}
+
+	public Integer getPlanNums() {
+		return planNums;
+	}
+
+	public void setPlanNums(Integer planNums) {
+		this.planNums = planNums;
+	}
+
+	public Integer getProducedNums() {
+		return producedNums;
+	}
+
+	public void setProducedNums(Integer producedNums) {
+		this.producedNums = producedNums;
+	}
+
+	public List<MesCurrentPlanItemResp> getPlans() {
+		return plans;
+	}
+
+	public void setPlans(List<MesCurrentPlanItemResp> plans) {
+		this.plans = plans;
+	}
+}

+ 45 - 1
src/main/java/com/jeesite/modules/mes/service/MesProductOrderService.java

@@ -4,9 +4,13 @@ import java.util.List;
 
 import cn.hutool.core.util.ObjectUtil;
 import com.jeesite.common.collect.ListUtils;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.service.ServiceException;
 import com.jeesite.modules.utils.HelperUtil;
+import com.jeesite.modules.wms.entity.WmsBom;
 import com.jeesite.modules.wms.entity.WmsOrder;
 import com.jeesite.modules.wms.entity.WmsSupplierOrder;
+import com.jeesite.modules.wms.service.WmsBomService;
 import com.jeesite.modules.wms.service.WmsOrderService;
 import com.twelvemonkeys.lang.StringUtil;
 import org.springframework.stereotype.Service;
@@ -14,6 +18,7 @@ import org.springframework.transaction.annotation.Transactional;
 
 import com.jeesite.common.entity.Page;
 import com.jeesite.common.service.CrudService;
+import com.jeesite.modules.mes.entity.MesProductCate;
 import com.jeesite.modules.mes.entity.MesProductOrder;
 import com.jeesite.modules.mes.dao.MesProductOrderDao;
 
@@ -26,9 +31,14 @@ import com.jeesite.modules.mes.dao.MesProductOrderDao;
 public class MesProductOrderService extends CrudService<MesProductOrderDao, MesProductOrder> {
 
 	private final WmsOrderService wmsOrderService;
+	private final WmsBomService wmsBomService;
+	private final MesProductCateService mesProductCateService;
 
-	public MesProductOrderService(WmsOrderService wmsOrderService) {
+	public MesProductOrderService(WmsOrderService wmsOrderService, WmsBomService wmsBomService,
+			MesProductCateService mesProductCateService) {
 		this.wmsOrderService = wmsOrderService;
+		this.wmsBomService = wmsBomService;
+		this.mesProductCateService = mesProductCateService;
 	}
 
 	/**
@@ -95,8 +105,42 @@ public class MesProductOrderService extends CrudService<MesProductOrderDao, MesP
 			}
 		}
 
+		// 产品类型(下单时选择):未选择时按 BOM 的工件类型补齐,与 BOM 不一致时报错
+		checkProductCate(mesProductOrder);
+
 		super.save(mesProductOrder);
 	}
+
+	/**
+	 * 校验并补齐订单的产品类型。
+	 *
+	 * 规则:
+	 *   1) 订单未选择产品类型时,按所选 BOM 的工件类型自动补齐;
+	 *   2) 订单选择的产品类型必须存在且启用;
+	 *   3) BOM 已维护工件类型时,订单产品类型必须与 BOM 一致,否则提示核对。
+	 * @param mesProductOrder 生产订单
+	 */
+	private void checkProductCate(MesProductOrder mesProductOrder) {
+		String bomCateSn = null;
+		if (StringUtils.isNotBlank(mesProductOrder.getBomId())) {
+			WmsBom wmsBom = wmsBomService.get(new WmsBom(mesProductOrder.getBomId()));
+			if (wmsBom != null) {
+				bomCateSn = wmsBom.getProductCateId();
+			}
+		}
+		if (StringUtils.isBlank(mesProductOrder.getProductCateId())) {
+			// 未选择时按 BOM 的工件类型补齐
+			mesProductOrder.setProductCateId(bomCateSn);
+			return;
+		}
+		MesProductCate mesProductCate = mesProductCateService.findBySn(mesProductOrder.getProductCateId());
+		if (mesProductCate == null) {
+			throw new ServiceException(text("所选产品类型不存在或已停用,请重新选择!"));
+		}
+		if (StringUtils.isNotBlank(bomCateSn) && !bomCateSn.equals(mesProductOrder.getProductCateId())) {
+			throw new ServiceException(text("所选产品类型与BOM的工件类型(" + bomCateSn + ")不一致,请核对后重试!"));
+		}
+	}
 	
 	/**
 	 * 更新状态

+ 248 - 11
src/main/java/com/jeesite/modules/mes/service/MesProductPlanService.java

@@ -1,5 +1,8 @@
 package com.jeesite.modules.mes.service;
 
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
 
 import com.jeesite.common.lang.StringUtils;
@@ -8,6 +11,8 @@ import com.jeesite.common.service.ServiceException;
 import com.jeesite.modules.mes.entity.MesProduct;
 import com.jeesite.modules.mes.entity.MesProductOrder;
 import com.jeesite.modules.mes.entity.MesShift;
+import com.jeesite.modules.mes.resp.MesCurrentPlanItemResp;
+import com.jeesite.modules.mes.resp.MesCurrentPlanResp;
 import com.jeesite.modules.mes.resp.MesShiftDataResp;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -101,16 +106,12 @@ public class MesProductPlanService extends CrudService<MesProductPlanDao, MesPro
 				throw new ServiceException(text("只有已下发的生产订单才能排产"));
 			}
 
-			String bomCateSn = null;
-			if(StringUtils.isNotBlank(mesProductOrder.getBomId())){
-				WmsBom wmsBom = wmsBomService.get(new WmsBom(mesProductOrder.getBomId()));
-				if(wmsBom != null){
-					bomCateSn = wmsBom.getProductCateId();
-				}
-			}
+			String bomCateSn = resolveProductCate(mesProductOrder);
 			if(StringUtils.isBlank(bomCateSn)){
-				throw new ServiceException(text("生产订单的BOM未维护工件类型,无法排产"));
+				throw new ServiceException(text("生产订单未维护产品类型(工件类型),无法排产"));
 			}
+			// 计划显式记录产品类型,保证 订单->计划->产品 链路一致
+			mesProductPlan.setProductCateId(bomCateSn);
 
 			MesProductPlan query = new MesProductPlan();
 			query.setOrderId(mesProductPlan.getOrderId());
@@ -125,7 +126,7 @@ public class MesProductPlanService extends CrudService<MesProductPlanDao, MesPro
 			MesProductPlan slotQuery = new MesProductPlan();
 			slotQuery.setDay(mesProductPlan.getDay());
 			slotQuery.setShiftId(mesProductPlan.getShiftId());
-			slotQuery.setBomProductCateId(bomCateSn);
+			slotQuery.setProductCateId(bomCateSn);
 			slotQuery.getSqlMap().getWhere().and("a.status", QueryType.EQ, "0");
 			if(!findList(slotQuery).isEmpty()){
 				throw new ServiceException(text("同一日期、班次、产品已存在生产计划,不能重复排产"));
@@ -146,9 +147,38 @@ public class MesProductPlanService extends CrudService<MesProductPlanDao, MesPro
 			}
 
 			mesProductPlan.setIsPick("0");
+		}else{
+			// 编辑计划时,产品类型跟随生产订单同步
+			MesProductOrder mesProductOrder = mesProductOrderService.get(new MesProductOrder(mesProductPlan.getOrderId()));
+			String productCateSn = resolveProductCate(mesProductOrder);
+			if(StringUtils.isNotBlank(productCateSn)){
+				mesProductPlan.setProductCateId(productCateSn);
+			}
 		}
 		super.save(mesProductPlan);
 	}
+
+	/**
+	 * 解析生产订单的产品类型:优先取订单选择的产品类型,订单未维护时用 BOM 的工件类型兜底。
+	 *
+	 * @param mesProductOrder 生产订单
+	 * @return 产品类型编号(mes_product_cate.sn)
+	 */
+	private String resolveProductCate(MesProductOrder mesProductOrder){
+		if(mesProductOrder == null){
+			return null;
+		}
+		if(StringUtils.isNotBlank(mesProductOrder.getProductCateId())){
+			return mesProductOrder.getProductCateId();
+		}
+		if(StringUtils.isNotBlank(mesProductOrder.getBomId())){
+			WmsBom wmsBom = wmsBomService.get(new WmsBom(mesProductOrder.getBomId()));
+			if(wmsBom != null){
+				return wmsBom.getProductCateId();
+			}
+		}
+		return null;
+	}
 	
 	/**
 	 * 更新状态
@@ -182,7 +212,7 @@ public class MesProductPlanService extends CrudService<MesProductPlanDao, MesPro
 	 *
 	 * 计划日期以“班次开始日期”为准,因此跨零点的夜班在次日凌晨仍然匹配前一天的计划。
 	 *
-	 * @param cateSn 产品工件类型(对应 wms_bom.product_cate_id)
+	 * @param cateSn 产品工件类型(对应 mes_product_cate.sn / 计划product_cate_id)
 	 * @return 唯一计划;没有计划时返回 null
 	 */
 	public MesProductPlan getCurrentPlan(String cateSn){
@@ -202,7 +232,7 @@ public class MesProductPlanService extends CrudService<MesProductPlanDao, MesPro
 		MesProductPlan query = new MesProductPlan();
 		query.setShiftId(mesShiftDataResp.getMesShift().getId());
 		query.setDay(java.sql.Date.valueOf(planDay));
-		query.setBomProductCateId(cateSn);
+		query.setProductCateId(cateSn);
 		query.getSqlMap().getWhere().and("a.status", QueryType.EQ, "0");
 		query.getSqlMap().getWhere().and("c.state", QueryType.EQ, "1");
 
@@ -233,5 +263,212 @@ public class MesProductPlanService extends CrudService<MesProductPlanDao, MesPro
 			throw new ServiceException(text("生产计划已完成,不能继续生产"));
 		}
 	}
+
+	/**
+	 * 获取有效的生产计划:计划存在、未删除停用、且关联的生产订单已下发。
+	 * 校验不通过直接抛出业务异常,阻断工位结果提交。
+	 *
+	 * @param planId 生产计划id
+	 * @return 有效的生产计划
+	 */
+	public MesProductPlan getValidPlan(String planId){
+		if(StringUtils.isBlank(planId)){
+			throw new ServiceException(text("该工件未关联生产计划(订单-计划-工件),不能作业!"));
+		}
+		MesProductPlan plan = get(new MesProductPlan(planId));
+		if(plan == null){
+			throw new ServiceException(text("该工件关联的生产计划不存在或已删除,不能作业!"));
+		}
+		if(!"0".equals(plan.getStatus())){
+			throw new ServiceException(text("该工件关联的生产计划已失效,不能作业!"));
+		}
+		MesProductOrder order = mesProductOrderService.get(new MesProductOrder(plan.getOrderId()));
+		if(order == null){
+			throw new ServiceException(text("该工件关联的生产订单不存在,不能作业!"));
+		}
+		if(!"1".equals(order.getState())){
+			throw new ServiceException(text("该工件关联的生产订单未下发,不能作业!"));
+		}
+		return plan;
+	}
+
+	/**
+	 * 查询工件(mes_product)已关联的生产计划id。
+	 *
+	 * @param sn 工件码
+	 * @return 生产计划id,未关联时返回 null
+	 */
+	public String getPlanIdByProductSn(String sn){
+		if(StringUtils.isBlank(sn)){
+			return null;
+		}
+		MesProduct query = new MesProduct();
+		query.setSn(sn);
+		MesProduct product = mesProductService.findInfo(query);
+		if(product == null){
+			return null;
+		}
+		return product.getPlanId();
+	}
+
+	/**
+	 * 把生产计划回写到工件上(只补空,不覆盖工件已有计划)。
+	 *
+	 * @param sn 工件码
+	 * @param planId 生产计划id
+	 */
+	public void bindPlanToProduct(String sn, String planId){
+		if(StringUtils.isBlank(sn) || StringUtils.isBlank(planId)){
+			return;
+		}
+		MesProduct query = new MesProduct();
+		query.setSn(sn);
+		MesProduct product = mesProductService.findInfo(query);
+		if(product == null || StringUtils.isNotBlank(product.getPlanId())){
+			return;
+		}
+		product.setPlanId(planId);
+		mesProductService.save(product);
+	}
+
+	/**
+	 * 获取当前班次的全部有效生产计划(订单已下发、计划未删除)。
+	 *
+	 * 计划日期以“班次开始日期”为准,跨零点的夜班在次日凌晨仍然匹配前一天的计划。
+	 *
+	 * @return 当前班次的计划列表;没有匹配到班次时返回空列表
+	 */
+	public List<MesProductPlan> getCurrentShiftPlans(){
+		List<MesProductPlan> plans = new ArrayList<>();
+		MesShiftDataResp mesShiftDataResp = mesShiftService.getCurShift();
+		if(mesShiftDataResp == null
+				|| mesShiftDataResp.getMesShift() == null
+				|| StringUtils.isBlank(mesShiftDataResp.getStart())
+				|| mesShiftDataResp.getStart().length() < 10){
+			return plans;
+		}
+		String planDay = mesShiftDataResp.getStart().substring(0, 10);
+		MesProductPlan query = new MesProductPlan();
+		query.setShiftId(mesShiftDataResp.getMesShift().getId());
+		query.setDay(java.sql.Date.valueOf(planDay));
+		query.getSqlMap().getWhere().and("a.status", QueryType.EQ, "0");
+		query.getSqlMap().getWhere().and("c.state", QueryType.EQ, "1");
+		List<MesProductPlan> list = findList(query);
+		if(list != null){
+			plans.addAll(list);
+		}
+		return plans;
+	}
+
+	/**
+	 * 获取当前班次正在生产的产品(工件类型)。
+	 * 当前班次只有一个产品类型时返回该类型;存在多个时返回 null。
+	 *
+	 * @return 产品类型编号(mes_product_cate.sn)
+	 */
+	public String getCurrentProductCate(){
+		List<MesProductPlan> plans = getCurrentShiftPlans();
+		if(plans.isEmpty()){
+			return null;
+		}
+		String cateSn = null;
+		for(MesProductPlan plan : plans){
+			if(StringUtils.isBlank(plan.getProductCateId())){
+				continue;
+			}
+			if(cateSn == null){
+				cateSn = plan.getProductCateId();
+			}else if(!cateSn.equals(plan.getProductCateId())){
+				// 当前班次存在多个产品类型,无法唯一判断
+				return null;
+			}
+		}
+		return cateSn;
+	}
+
+	/**
+	 * 获取当前班次的生产计划与当前产品(工件类型),供客户端/下单页面使用。
+	 *
+	 * @return 当前计划数据
+	 */
+	public MesCurrentPlanResp getCurrentPlanData(){
+		MesCurrentPlanResp resp = new MesCurrentPlanResp();
+		MesShiftDataResp shift = mesShiftService.getCurShift();
+		if(shift != null && shift.getMesShift() != null){
+			resp.setShiftId(shift.getMesShift().getId());
+			resp.setShiftTitle(shift.getMesShift().getTitle());
+			resp.setShiftStart(shift.getStart());
+			resp.setShiftEnd(shift.getEnd());
+			if(StringUtils.isNotBlank(shift.getStart()) && shift.getStart().length() >= 10){
+				resp.setDay(shift.getStart().substring(0, 10));
+			}
+		}
+
+		List<MesCurrentPlanItemResp> items = new ArrayList<>();
+		int planNums = 0;
+		int producedNums = 0;
+		for(MesProductPlan plan : getCurrentShiftPlans()){
+			MesCurrentPlanItemResp item = new MesCurrentPlanItemResp();
+			item.setPlanId(plan.getId());
+			item.setOrderId(plan.getOrderId());
+			item.setOrderSn(plan.getOrderSn());
+			item.setBomTitle(plan.getBomTitle());
+			item.setCateSn(plan.getProductCateId());
+			item.setCateTitle(plan.getProductCateTitle());
+			item.setDay(formatPlanDay(plan.getDay()));
+			item.setShiftTitle(plan.getShiftTitle());
+			item.setNums(plan.getNums());
+			int produced = countProduced(plan.getId());
+			item.setProduced(produced);
+			if(plan.getNums() != null){
+				planNums += plan.getNums();
+			}
+			producedNums += produced;
+			items.add(item);
+		}
+		resp.setPlans(items);
+		resp.setPlanNums(planNums);
+		resp.setProducedNums(producedNums);
+
+		String cateSn = getCurrentProductCate();
+		resp.setCateSn(cateSn);
+		if(cateSn != null){
+			for(MesCurrentPlanItemResp item : items){
+				if(cateSn.equals(item.getCateSn())){
+					resp.setCateTitle(item.getCateTitle());
+					break;
+				}
+			}
+		}
+		return resp;
+	}
+
+	/**
+	 * 统计计划下已生产的件数。
+	 *
+	 * @param planId 生产计划id
+	 * @return 已生产件数
+	 */
+	private int countProduced(String planId){
+		if(StringUtils.isBlank(planId)){
+			return 0;
+		}
+		MesProduct query = new MesProduct();
+		query.setPlanId(planId);
+		return (int) mesProductService.findCount(query);
+	}
+
+	/**
+	 * 格式化计划日期。
+	 *
+	 * @param day 计划日期
+	 * @return yyyy-MM-dd
+	 */
+	private String formatPlanDay(Date day){
+		if(day == null){
+			return null;
+		}
+		return new SimpleDateFormat("yyyy-MM-dd").format(day);
+	}
 	
 }

+ 132 - 1
src/main/java/com/jeesite/modules/mes/service/MesProductRecordService.java

@@ -46,6 +46,11 @@ import javax.servlet.http.HttpServletRequest;
 @Service
 public class MesProductRecordService extends CrudService<MesProductRecordDao, MesProductRecord> {
 
+	/**
+	 * 无计划拦截总开关配置key(js_sys_config.config_key)
+	 */
+	private static final String PLAN_INTERCEPT_CONFIG = "mes.plan.intercept";
+
 	@Autowired
 	private MesProductRecordDao mesProductRecordDao;
 
@@ -160,13 +165,38 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 		if (StringUtils.isEmpty(mesProductRecord.getSource())) {
 			mesProductRecord.setSource("0");
 		}
+		// 兜底补计划:记录未带计划时,按工件已关联的计划补齐(只补空,不覆盖已有计划)
+		if (StringUtils.isBlank(mesProductRecord.getPlanId())
+				&& StringUtils.isNotBlank(mesProductRecord.getSn())) {
+			String planId = mesProductPlanService.getPlanIdByProductSn(mesProductRecord.getSn());
+			if (StringUtils.isNotBlank(planId)) {
+				mesProductRecord.setPlanId(planId);
+			}
+		}
 		super.save(mesProductRecord);
 	}
 	public Long insertBatch(List<MesProductRecord> mesProductRecordList,int num) {
+		boolean needPlan = false;
+		String sn = null;
 		for (MesProductRecord record : mesProductRecordList) {
 			if (StringUtils.isEmpty(record.getSource())) {
 				record.setSource("0");
 			}
+			if (StringUtils.isBlank(record.getPlanId()) && StringUtils.isNotBlank(record.getSn())) {
+				needPlan = true;
+				sn = record.getSn();
+			}
+		}
+		// 兜底补计划:整批记录未带计划时,按工件已关联的计划一次性补齐(只补空,不覆盖已有计划)
+		if (needPlan) {
+			String planId = mesProductPlanService.getPlanIdByProductSn(sn);
+			if (StringUtils.isNotBlank(planId)) {
+				for (MesProductRecord record : mesProductRecordList) {
+					if (StringUtils.isBlank(record.getPlanId()) && sn.equals(record.getSn())) {
+						record.setPlanId(planId);
+					}
+				}
+			}
 		}
 		return mesProductRecordDao.insertBatch(mesProductRecordList,num);
 	}
@@ -274,6 +304,88 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 		return mesProductPlan;
 	}
 
+	/**
+	 * 无计划拦截总开关(js_sys_config:mes.plan.intercept)。
+	 * 0=关闭拦截(应急/灰度),其它值(含未配置)=开启拦截。
+	 */
+	private boolean isPlanInterceptEnabled() {
+		String cfg = Global.getConfig(PLAN_INTERCEPT_CONFIG);
+		if (cfg == null) {
+			return true;
+		}
+		return !"0".equals(cfg.trim());
+	}
+
+	/**
+	 * 提交工位结果前校验生产计划:工件必须关联有效的生产计划,否则拦截作业。
+	 *
+	 * 计划来源依次为:
+	 *   1) 当前记录上的 plan_id(新建工件时已写入)
+	 *   2) 工件级 mes_product.plan_id
+	 *   3) 该工件其它记录上的 plan_id
+	 *   4) 按当前班次(班次开始日期+班次+工件类型)自动匹配,匹配到即回写自愈
+	 * 都取不到则拦截。
+	 *
+	 * @param sn 工件码
+	 * @param cateSn 工件类型
+	 * @param isRepair 是否返修(1=返修,返修不拦截,沿用原记录计划)
+	 * @param recordPlanId 当前记录已带的生产计划id(可为空)
+	 * @return 命中的生产计划;拦截关闭或返修时返回 null
+	 */
+	private MesProductPlan requirePlan(String sn, String cateSn, String isRepair, String recordPlanId) {
+		if (!isPlanInterceptEnabled()) {
+			return null;
+		}
+		if ("1".equals(isRepair)) { // 返修不拦截,返修件不占用计划产量
+			return null;
+		}
+		String planId = recordPlanId;
+		if (StringUtils.isBlank(planId)) {
+			planId = mesProductPlanService.getPlanIdByProductSn(sn);
+		}
+		if (StringUtils.isBlank(planId)) {
+			planId = mesProductRecordDao.findPlanIdBySn(sn);
+		}
+		if (StringUtils.isNotBlank(planId)) {
+			// 已有计划:校验计划有效性(计划失效、订单未下发在此抛出)
+			return mesProductPlanService.getValidPlan(planId);
+		}
+		// 无计划:按当前班次自动匹配并回写(自愈)
+		MesProductPlan plan = autoBindCurrentPlan(sn, cateSn);
+		if (plan == null) {
+			throw new ServiceException(text("该工件未关联生产计划(订单-计划-工件),当前班次无有效生产计划,不能作业!"));
+		}
+		return plan;
+	}
+
+	/**
+	 * 无计划工件按当前班次自动匹配生产计划,并把计划回写到工件与生产记录上。
+	 *
+	 * @param sn 工件码
+	 * @param cateSn 工件类型
+	 * @return 匹配到的生产计划;无匹配时返回 null
+	 */
+	private MesProductPlan autoBindCurrentPlan(String sn, String cateSn) {
+		if (StringUtils.isBlank(cateSn)) {
+			return null;
+		}
+		MesProductPlan plan = null;
+		try {
+			plan = mesProductPlanService.getCurrentPlan(cateSn);
+		} catch (ServiceException e) {
+			// 未匹配到班次、计划重复等异常,交由上层给出明确提示
+			throw e;
+		} catch (RuntimeException e) {
+			return null;
+		}
+		if (plan == null) {
+			return null;
+		}
+		mesProductPlanService.bindPlanToProduct(sn, plan.getId());
+		mesProductRecordDao.updatePlanIdBySn(sn, plan.getId());
+		return plan;
+	}
+
 
 	/**
 	 * 保存数据(插入或更新)
@@ -327,6 +439,7 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 				mpr1.setTid(mesTemplateItems.getId());
 				mpr1.setCdate(cdate);
 				mpr1.setContent("UDF");
+				mpr1.setPlanId(mesProductPlan.getId());
 				if(mesTemplateItems.getOprno().equals(oprno)){
 					mpr1.setOprno(mesProductRecord.getOprno());
 					mpr1.setContent("OK");
@@ -387,6 +500,7 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 				mpr1.setCdate(cdate);
 				mpr1.setContent("UDF");
 				mpr1.setSource(source);
+				mpr1.setPlanId(mesProductPlan.getId());
 				if(mesTemplateItems.getOprno().equals(oprno)){
 					mpr1.setOprno(mesProductRecord.getOprno());
 					mpr1.setContent(mesProductRecord.getContent());
@@ -520,6 +634,7 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 				mpr1.setTreeLevel(0);
 				mpr1.setCdate(cdate);
 				mpr1.setContent("UDF");
+				mpr1.setPlanId(mesProductPlan.getId());
 				if(mesTemplateItems.getOprno().equals(oprno)){
 					mpr1.setOprno(mesProductRecord.getOprno());
 					mpr1.setContent(mesProductRecord.getContent());
@@ -616,11 +731,18 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 		content = content.equals("OK")?"OK":"NG";
 
 		String cateSn = "";
+		String recordPlanId = null;
 		for (MesProductRecord mesProductRecord : lists){
 			cateSn = mesProductRecord.getCateSn();
+			if (StringUtils.isBlank(recordPlanId)) {
+				recordPlanId = mesProductRecord.getPlanId();
+			}
 			break;
 		}
 
+		// 无计划拦截:提交工位结果前,工件必须关联有效的生产计划(订单-计划-工件)
+		requirePlan(sn, cateSn, isRepair, recordPlanId);
+
 		if(bmlists!=null&&bmlists.size() > 0){
 			for (BindMaterialResp bindMaterialResp:bmlists){
 				ParamsResp paramsResp = new ParamsResp();
@@ -719,7 +841,7 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 				mesProductModifyRecord1.setLineSn(mesProductRecord.getLineSn());
 				mesProductModifyRecord1.setContent(content);
 				mesProductModifyRecord1.setContentOld(oldcontent);
-				if(isRepair.equals("1")){
+				if("1".equals(isRepair)){
 					mesProductModifyRecord1.setSource("3"); // 来自返修
 				}else{
 					mesProductModifyRecord1.setSource("0");
@@ -1016,6 +1138,9 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 			throw new ServiceException(text("记录不存在!"));
 		}
 
+		// 无计划拦截:提交工位结果前,工件必须关联有效的生产计划(订单-计划-工件)
+		requirePlan(sn, mesProductRecord.getCateSn(), null, mesProductRecord.getPlanId());
+
 		if(oldOprno.length() == 6){
 			mesProductRecord.setOprno(oldOprno);
 		}
@@ -1065,6 +1190,9 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 			throw new ServiceException(text("记录不存在!"));
 		}
 
+		// 无计划拦截:提交工位结果前,工件必须关联有效的生产计划(订单-计划-工件)
+		requirePlan(sn, mesProductRecord.getCateSn(), null, mesProductRecord.getPlanId());
+
 		if(oldOprno.length() == 6){
 			mesProductRecord.setOprno(oldOprno);
 		}
@@ -1096,6 +1224,9 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 			throw new ServiceException(text("记录不存在!"));
 		}
 
+		// 无计划拦截:提交工位结果前,工件必须关联有效的生产计划(订单-计划-工件)
+		requirePlan(sn, mesProductRecord.getCateSn(), null, mesProductRecord.getPlanId());
+
 		if(oldOprno.length() == 6){
 			mesProductRecord.setOprno(oldOprno);
 		}

+ 46 - 0
src/main/java/com/jeesite/modules/mes/web/MesProductOrderController.java

@@ -9,6 +9,7 @@ import java.text.SimpleDateFormat;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONValidator;
 import com.jeesite.common.codec.EncodeUtils;
 import com.jeesite.modules.wms.entity.WmsBom;
@@ -36,8 +37,11 @@ import org.springframework.transaction.annotation.Transactional;
 
 import com.jeesite.common.config.Global;
 import com.jeesite.common.entity.Page;
+import com.jeesite.common.mybatis.mapper.query.QueryType;
 import com.jeesite.common.web.BaseController;
+import com.jeesite.modules.mes.entity.MesProductCate;
 import com.jeesite.modules.mes.entity.MesProductOrder;
+import com.jeesite.modules.mes.entity.MesProductPlan;
 import com.jeesite.modules.mes.service.MesProductOrderService;
 
 /**
@@ -63,6 +67,10 @@ public class MesProductOrderController extends BaseController {
     private WmsPickRecordService wmsPickRecordService;
     @Autowired private com.jeesite.modules.wms.service.WmsPickService wmsPickService;
     @Autowired private com.jeesite.modules.wms.service.WmsPickMaterialService wmsPickMaterialService;
+    @Autowired
+    private com.jeesite.modules.mes.service.MesProductPlanService mesProductPlanService;
+    @Autowired
+    private com.jeesite.modules.mes.service.MesProductCateService mesProductCateService;
 
 	/**
 	 * 获取数据
@@ -91,6 +99,20 @@ public class MesProductOrderController extends BaseController {
 	public Page<MesProductOrder> listData(MesProductOrder mesProductOrder, HttpServletRequest request, HttpServletResponse response) {
 		mesProductOrder.setPage(new Page<>(request, response));
 		Page<MesProductOrder> page = mesProductOrderService.findPage(mesProductOrder);
+		// 排产情况:已排产数量 / 未排产数量
+		for (MesProductOrder order : page.getList()) {
+			int planned = 0;
+			MesProductPlan planQuery = new MesProductPlan();
+			planQuery.setOrderId(order.getId());
+			planQuery.getSqlMap().getWhere().and("a.status", QueryType.EQ, "0");
+			for (MesProductPlan plan : mesProductPlanService.findList(planQuery)) {
+				if (plan.getNums() != null) {
+					planned += plan.getNums();
+				}
+			}
+			order.setPlanNums(planned);
+			order.setUnPlanNums(order.getNums() == null ? 0 : Math.max(0, order.getNums() - planned));
+		}
 		return page;
 	}
 
@@ -107,6 +129,30 @@ public class MesProductOrderController extends BaseController {
 		List<WmsBom> boms = wmsBomService.findList(wmsBom);
 		model.addAttribute("boms", boms);
 
+		// 产品类型(工件类型)下拉
+		MesProductCate cateQuery = new MesProductCate();
+		cateQuery.setStatus("0");
+		List<MesProductCate> cates = mesProductCateService.findList(cateQuery);
+		model.addAttribute("cates", cates);
+
+		// BOM -> 工件类型 映射,选择BOM时自动带出产品类型
+		Map<String, String> bomCateMap = new HashMap<>();
+		for (WmsBom bom : boms) {
+			if (bom.getProductCateId() != null) {
+				bomCateMap.put(bom.getId(), bom.getProductCateId());
+			}
+		}
+		model.addAttribute("bomCateMap", JSON.toJSONString(bomCateMap));
+
+		// 当前计划的产品类型:下单时据此判断产品类型(新建时默认带出)
+		String curCateSn = null;
+		try {
+			curCateSn = mesProductPlanService.getCurrentProductCate();
+		} catch (Exception e) {
+			logger.warn("获取当前计划产品类型失败:{}", e.getMessage());
+		}
+		model.addAttribute("curCateSn", curCateSn);
+
 		return "modules/mes/mesProductOrderForm";
 	}
 

+ 29 - 0
src/main/java/com/jeesite/modules/mes/web/MesProductPlanController.java

@@ -4,6 +4,7 @@ import java.util.List;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import com.jeesite.common.lang.StringUtils;
 import com.jeesite.common.mybatis.mapper.query.QueryType;
 import com.jeesite.modules.mes.entity.MesProduct;
 import com.jeesite.modules.mes.entity.MesProductOrder;
@@ -27,6 +28,7 @@ import com.jeesite.common.entity.Page;
 import com.jeesite.common.service.ServiceException;
 import com.jeesite.common.web.BaseController;
 import com.jeesite.modules.mes.entity.MesProductPlan;
+import com.jeesite.modules.mes.resp.CommonResp;
 import com.jeesite.modules.mes.service.MesProductPlanService;
 
 /**
@@ -76,6 +78,10 @@ public class MesProductPlanController extends BaseController {
 	@ResponseBody
 	public Page<MesProductPlan> listData(MesProductPlan mesProductPlan, HttpServletRequest request, HttpServletResponse response) {
 		mesProductPlan.getSqlMap().getWhere().and("a.status", QueryType.EQ, "0");
+		// 按生产订单号模糊查询(计划 -> 订单);关联字段默认已由框架自动绑定(c.sn LIKE)
+		if (StringUtils.isNotBlank(mesProductPlan.getOrderSn())) {
+			mesProductPlan.getSqlMap().getWhere().and("c.sn", QueryType.LIKE, mesProductPlan.getOrderSn());
+		}
 		mesProductPlan.setPage(new Page<>(request, response));
 		Page<MesProductPlan> page = mesProductPlanService.findPage(mesProductPlan);
 		for (MesProductPlan mesProductPlan1 : page.getList()) {
@@ -98,6 +104,29 @@ public class MesProductPlanController extends BaseController {
 	}
 
 	/**
+	 * 获取当前班次的生产计划与当前产品(工件类型)。
+	 *
+	 * 用途:客户端显示"当前在生产什么产品";下订单时根据当前计划判断产品类型。
+	 *
+	 * @return 当前班次计划、当前产品
+	 */
+	@PostMapping(value = "currentPlan")
+	@ResponseBody
+	public CommonResp currentPlan() {
+		CommonResp resp = new CommonResp();
+		try {
+			resp.setData(mesProductPlanService.getCurrentPlanData());
+			resp.setResult(Global.TRUE);
+			resp.setMessage(text("成功"));
+		} catch (Exception e) {
+			logger.error("获取当前生产计划失败", e);
+			resp.setResult(Global.FALSE);
+			resp.setMessage(StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : text("获取当前生产计划失败!"));
+		}
+		return resp;
+	}
+
+	/**
 	 * 查看编辑表单
 	 */
 	@RequiresPermissions("mes:mesProductPlan:view")

+ 31 - 1
src/main/java/com/jeesite/modules/mes/web/MesProductRecordController.java

@@ -312,6 +312,26 @@ public class MesProductRecordController extends BaseController {
 		return renderResult(Global.TRUE, text("操作成功!"));
 	}
 	/**
+	 * 生产记录按订单/计划维度过滤。
+	 * 关联关系:mes_product_record.plan_id -> mes_product_plan -> mes_product_order
+	 *
+	 * @param mesProductRecord 查询条件
+	 */
+	private void applyPlanFilter(MesProductRecord mesProductRecord) {
+		if (mesProductRecord == null || mesProductRecord.getSqlMap() == null) {
+			return;
+		}
+		String orderSn = mesProductRecord.getOrderSn();
+		if (StringUtils.isNotBlank(orderSn)) {
+			// 关联字段默认已由框架自动绑定(c.sn LIKE),此处兜底保证过滤生效
+			mesProductRecord.getSqlMap().getWhere().and("c.sn", QueryType.LIKE, orderSn);
+		}
+		if (mesProductRecord.getPlanDay() != null) {
+			mesProductRecord.getSqlMap().getWhere().and("b.day", QueryType.EQ, mesProductRecord.getPlanDay());
+		}
+	}
+
+	/**
 	 * 查询列表数据
 	 */
 	@RequiresPermissions("mes:mesProductRecord:view")
@@ -328,6 +348,9 @@ public class MesProductRecordController extends BaseController {
 			mesProductRecord.getSqlMap().getOrder().setOrderBy("a.id DESC");
 			mesProductRecord.getSqlMap().getWhere().and("content", QueryType.NE, "UDF");
 		}
+
+		// 订单/计划维度过滤:记录 -> 计划 -> 订单
+		applyPlanFilter(mesProductRecord);
 		String searchHmb = mesProductRecord.getHmb();
 
 		// 1. 判断前端是否传入了 "后面板码" 作为搜索条件
@@ -466,6 +489,9 @@ public class MesProductRecordController extends BaseController {
 		// 如果查询状态的值为 0, 表示用户选择查询 未加工
 		else if ("0".equals(mesProductRecord.getProcessState()) && "".equals(mesProductRecord.getContent()))
 			mesProductRecord.getSqlMap().getWhere().and("content",QueryType.EQ, "UDF");
+
+		// 订单/计划维度过滤:记录 -> 计划 -> 订单
+		applyPlanFilter(mesProductRecord);
 		Page<MesProductRecord> page = mesProductRecordService.findPage(mesProductRecord);
 		for (int i=0;i<page.getList().size();i++){
 			MesTemplateItems mesTemplateItems = new MesTemplateItems();
@@ -599,6 +625,9 @@ public class MesProductRecordController extends BaseController {
 
 		mesProductRecord.setCraft("100000");    // 只查工艺号10000的
 
+		// 订单/计划维度过滤:记录 -> 计划 -> 订单
+		applyPlanFilter(mesProductRecord);
+
 		// 确保 sqlMap 被初始化,并添加默认排序和过滤条件
 		if (mesProductRecord.getSqlMap() != null) {
 			mesProductRecord.getSqlMap().getOrder().setOrderBy("a.id DESC");
@@ -2680,7 +2709,7 @@ public class MesProductRecordController extends BaseController {
 		String result = request.getParameter("result");
 		String userCode = request.getParameter("ucode");
 		String isRepair = request.getParameter("isRepair");
-		isRepair = StringUtils.isEmpty(isRepair)?"0":"1";
+		isRepair = StringUtils.isEmpty(isRepair)?"0":isRepair;
 		String craft = "100000";
 		String remark = request.getParameter("remark");
 		String ps = request.getParameter("ps"); // 参数,多个以"|"分割
@@ -4156,6 +4185,7 @@ public class MesProductRecordController extends BaseController {
 				mesProductRecord4.setSource("0");
 				mesProductRecord4.setCdate(mesProductRecord1.getCdate());
 				mesProductRecord4.setCreateBy(mesProductRecord1.getCreateBy());
+				mesProductRecord4.setPlanId(mesProductRecord1.getPlanId());
 				mesProductRecordService.save(mesProductRecord4);
 			}else{
 				mesProductRecord3.setMaterielSn(batchSn);

+ 1 - 1
src/main/resources/mappings/modules/mes/MesProductDao.xml

@@ -2,7 +2,7 @@
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.jeesite.modules.mes.dao.MesProductDao">
 	<update id="updateDate">
-		update ${sqlMap.table.toSql()}
+		update mes_product
 		set update_date=#{updateDate}
 		WHERE id = #{id}
 	</update>

+ 26 - 3
src/main/resources/mappings/modules/mes/MesProductRecordDao.xml

@@ -42,8 +42,11 @@
     </select>
     <select id="findNgList" resultType="com.jeesite.modules.mes.entity.MesProductRecord">
         SELECT
-       a.*
+       a.*, a.plan_id AS planId, b.day AS planDay, c.sn AS orderSn, d.title AS shiftTitle
         FROM mes_product_record a
+        LEFT JOIN mes_product_plan b ON a.plan_id = b.id
+        LEFT JOIN mes_product_order c ON b.order_id = c.id
+        LEFT JOIN mes_shift d ON b.shift_id = d.id
         <where>
             a.content = 'NG'
             AND a.update_date &gt;= DATE_SUB(NOW(), INTERVAL 3 DAY)
@@ -52,7 +55,14 @@
             AND NOT EXISTS (
             SELECT 1 FROM mes_product_repair r
             WHERE r.sn = a.sn
-            ) </where>
+            )
+            <if test="orderSn != null and orderSn != ''">
+            AND c.sn LIKE CONCAT('%', #{orderSn}, '%')
+            </if>
+            <if test="planDay != null">
+            AND b.day = #{planDay}
+            </if>
+        </where>
         <choose>
             <when test="page !=null and page.orderBy != null and page.orderBy != ''">
                 ORDER BY ${page.orderBy}
@@ -74,8 +84,21 @@
         where sn = #{sn} and oprno = #{oprno} and craft = #{craft}
     </update>
     <update id="updateDate">
-        update ${sqlMap.table.toSql()}
+        update mes_product_record
         set update_date=#{updateDate}
         WHERE id = #{id}
     </update>
+    <!-- 查询工件已关联的生产计划(记录级),取第一条非空 -->
+    <select id="findPlanIdBySn" resultType="java.lang.String">
+        SELECT plan_id
+        FROM mes_product_record
+        WHERE sn = #{sn} AND plan_id IS NOT NULL AND plan_id &lt;&gt; ''
+        LIMIT 1
+    </select>
+    <!-- 回填工件所有记录的生产计划(只补空,不覆盖已有计划) -->
+    <update id="updatePlanIdBySn">
+        UPDATE mes_product_record
+        SET plan_id = #{planId}
+        WHERE sn = #{sn} AND (plan_id IS NULL OR plan_id = '')
+    </update>
 </mapper>

+ 39 - 0
src/main/resources/views/modules/mes/mesProductOrderForm.html

@@ -65,6 +65,18 @@
 					<div class="col-xs-12">
 						<div class="form-group">
 							<label class="control-label col-sm-2" title="">
+								<span class="required hide">*</span> ${text('产品类型')}:<i class="fa icon-question hide"></i></label>
+							<div class="col-sm-4">
+								<#form:select path="productCateId" items="${cates}" itemLabel="title" itemValue="sn" blankOption="true" class="form-control required" />
+								<span class="help-block" id="cateTip"></span>
+							</div>
+						</div>
+					</div>
+				</div>
+				<div class="row">
+					<div class="col-xs-12">
+						<div class="form-group">
+							<label class="control-label col-sm-2" title="">
 								<span class="required hide">*</span> ${text('备注')}:<i class="fa icon-question hide"></i></label>
 							<div class="col-sm-4">
 								<#form:input path="remark" maxlength="255" class="form-control"/>
@@ -111,4 +123,31 @@ $("#inputForm").validate({
 		}, "json");
     }
 });
+
+// BOM -> 工件类型 映射(选择BOM时自动带出产品类型)
+var bomCateMap = ${bomCateMap!'{}'};
+// 当前计划的产品类型(新建订单时默认带出)
+var curCateSn = '${curCateSn!}';
+
+$(function(){
+	// 新建订单且未选产品类型时,按当前计划的产品类型带出
+	if(!$('#productCateId').val() && curCateSn){
+		$('#productCateId').val(curCateSn);
+		showCateTip('已按当前计划的产品类型带出,可手动调整');
+	}
+	// 切换BOM时自动带出对应的工件类型
+	$('#bomId').on('change', function(){
+		var cate = bomCateMap[$('#bomId').val()];
+		if(cate){
+			$('#productCateId').val(cate);
+			showCateTip('已按BOM的工件类型自动带出');
+		}else{
+			showCateTip('该BOM未维护工件类型,请手动选择产品类型');
+		}
+	});
+});
+
+function showCateTip(msg){
+	$('#cateTip').text(msg || '');
+}
 </script>

+ 4 - 0
src/main/resources/views/modules/mes/mesProductOrderList.html

@@ -54,8 +54,12 @@ $('#dataGrid').dataGrid({
 		{header:'${text("订单编号")}', name:'sn', index:'a.sn', width:150, align:"center", frozen:true},
 		{header:'${text("销售订单")}', name:'wmsOrderSn', index:'c.sn', width:150, align:"center"},
 		{header:'${text("生产数量")}', name:'nums', index:'a.nums', width:150, align:"center"},
+		{header:'${text("已排产")}', name:'planNums', index:'a.nums', width:150, align:"center", sortable:false},
+		{header:'${text("未排产")}', name:'unPlanNums', index:'a.nums', width:150, align:"center", sortable:false},
 		{header:'${text("交货日期")}', name:'deliveryDate', index:'a.delivery_date', width:150, align:"center"},
 		{header:'${text("BOM")}', name:'bomTitle', index:'b.title', width:150, align:"center"},
+		{header:'${text("产品类型")}', name:'productCateTitle', index:'e.title', width:120, align:"center", sortable:false},
+		{header:'${text("产品类型编号")}', name:'productCateId', index:'a.product_cate_id', width:120, align:"center", sortable:false},
 		{header:'${text("备注")}', name:'remark', index:'a.remark', width:150, align:"center"},
 		{header:'${text("状态")}', name:'state', index:'a.state', width:150, align:"center", formatter: function(val, obj, row, act){
 			return js.getDictLabel(${@DictUtils.getDictListJson('wms_order_state')}, val, '${text("未知")}', true);

+ 16 - 1
src/main/resources/views/modules/mes/mesProductPlanList.html

@@ -23,6 +23,12 @@
 					</div>
 				</div>
 				<div class="form-group">
+					<label class="control-label">${text('订单号')}:</label>
+					<div class="control-inline">
+						<#form:input path="orderSn" maxlength="64" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
 					<label class="control-label">${text('日期')}:</label>
 					<div class="control-inline">
 						<#form:input path="day" readonly="true" maxlength="20" class="form-control laydate width-datetime"
@@ -52,11 +58,20 @@ $('#dataGrid').dataGrid({
 	searchForm: $("#searchForm"),
 	columnModel: [
 		{header:'${text("生产订单")}', name:'orderSn', index:'a.order_id', width:150, align:"center", frozen:true},
+		{header:'${text("产品类型")}', name:'productCateTitle', index:'e.title', width:120, align:"center", sortable:false},
 		{header:'${text("日期")}', name:'day', index:'a.day', width:150, align:"center"},
 		{header:'${text("BOM")}', name:'bomTitle', index:'d.title', width:150, align:"center"},
 		{header:'${text("班次")}', name:'shiftTitle', index:'a.shift_id', width:150, align:"center"},
 		{header:'${text("计划数量")}', name:'nums', index:'a.nums', width:150, align:"center"},
-		{header:'${text("生产数量")}', name:'nums2', index:'a.nums', width:150, align:"center"},
+		{header:'${text("生产数量")}', name:'nums2', index:'a.nums', width:150, align:"center", sortable:false},
+		{header:'${text("完成率")}', name:'nums', index:'a.nums', width:150, align:"center", sortable:false, formatter: function(val, obj, row, act){
+			var plan = parseInt(val) || 0;
+			var done = parseInt(row.nums2) || 0;
+			if (plan <= 0) {
+				return '';
+			}
+			return (done * 100 / plan).toFixed(1) + '%(' + done + '/' + plan + ')';
+		}},
 		{header:'${text("更新时间")}', name:'updateDate', index:'a.update_date', width:150, align:"center"},
 		{header:'${text("操作")}', name:'actions', width:120, formatter: function(val, obj, row, act){
 			var actions = [];

+ 24 - 0
src/main/resources/views/modules/mes/mesProductRecordList.html

@@ -45,6 +45,19 @@
 						<#form:input path="lineSn" maxlength="10" class="form-control width-120"/>
 					</div>
 				</div>
+				<div class="form-group">
+					<label class="control-label">${text('生产订单号')}:</label>
+					<div class="control-inline">
+						<#form:input path="orderSn" maxlength="64" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('计划日期')}:</label>
+					<div class="control-inline">
+						<#form:input path="planDay" readonly="true" maxlength="20" class="form-control laydate width-datetime"
+							dataFormat="date" data-type="date" data-format="yyyy-MM-dd"/>
+					</div>
+				</div>
 
 <!--				<div class="form-group">-->
 <!--					<label class="control-label">${text('工艺号')}:</label>-->
@@ -179,6 +192,17 @@ $('#dataGrid').dataGrid({
 		{header:'${text("绑定内容")}', name:'materielSn', index:'a.materiel_sn', width:180, align:"center"},
 		// {header:'${text("工件类型")}', name:'cateSn', index:'a.cate_sn', width:90, align:"center"},
 		{header:'${text("产线编号")}', name:'lineSn', index:'a.line_sn', width:90, align:"center"},
+		{header:'${text("生产订单号")}', name:'orderSn', index:'c.sn', width:150, align:"center", sortable:false},
+		{header:'${text("计划日期")}', name:'planDay', index:'b.day', width:110, align:"center", sortable:false, formatter: function(val){
+				return val || '';
+		}},
+		{header:'${text("班次")}', name:'shiftTitle', index:'d.title', width:90, align:"center", sortable:false},
+		{header:'${text("计划状态")}', name:'planId', index:'a.plan_id', width:90, align:"center", sortable:false, formatter: function(val, obj, row, act){
+				if (val) {
+					return '已关联';
+				}
+				return '<span style="color:#ff0000;">无计划</span>';
+		}},
 		// {header:'${text("工位结果序号")}', name:'sort', index:'b.sort', width:120, align:"center",sortable:true},
 		// {header:'${text("报文类型")}', name:'messageType', index:'b.cate', width:100, align:"center"},
 

+ 6 - 0
src/main/resources/views/modules/mes/mesProductRecordList2.html

@@ -47,6 +47,12 @@
 					</div>
 				</div>
 				<div class="form-group">
+					<label class="control-label">${text('生产订单号')}:</label>
+					<div class="control-inline">
+						<#form:input path="orderSn" maxlength="64" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
 					<label class="control-label">${text('绑定内容')}:</label>
 					<div class="control-inline">
 						<#form:input path="materielSn" maxlength="50" class="form-control width-120"/>

+ 7 - 0
src/main/resources/views/modules/mes/mesProductRecordListNg.html

@@ -58,6 +58,12 @@
 					</div>
 				</div>
 				<div class="form-group">
+					<label class="control-label">${text('生产订单号')}:</label>
+					<div class="control-inline">
+						<#form:input path="orderSn" maxlength="64" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
 					<label class="control-label">${text('绑定内容')}:</label>
 					<div class="control-inline">
 						<#form:input path="materielSn" maxlength="50" class="form-control width-120"/>
@@ -110,6 +116,7 @@ $('#dataGrid').dataGrid({
 		{header:'${text("绑定内容")}', name:'materielSn', index:'a.materiel_sn', width:180, align:"center"},
 		{header:'${text("工件类型")}', name:'cateSn', index:'a.cate_sn', width:90, align:"center"},
 		{header:'${text("产线编号")}', name:'lineSn', index:'a.line_sn', width:90, align:"center"},
+		{header:'${text("生产订单号")}', name:'orderSn', index:'c.sn', width:150, align:"center", sortable:false},
 		// {header:'${text("工位结果序号")}', name:'sort', index:'b.sort', width:120, align:"center",sortable:true},
 		// {header:'${text("报文类型")}', name:'messageType', index:'b.cate', width:100, align:"center"},
 		{header:'${text("备注")}', name:'remark', index:'a.remark', width:150, align:"center"},