生产计划-无计划拦截-实施方案.md 23 KB

订单-计划-产品-记录 全链路 & 无计划拦截 实施方案

关联提交: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

-- 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 新增方法

// 按 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 只做参数解析,所有通道自动覆盖):

/**
 * 提交工位结果前校验:工件必须关联有效生产计划,否则拦截。
 * @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

-- 无计划仍在生产的工件
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

返回示例:

{
  "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' 占位值), 产品类型无从推导,且匹配不到真实班次,不影响排产与拦截,建议人工清理。


十一、产品创建 195 / 196 / 198(已落地)

11.1 目标

在工件/产品类型主数据 mes_product_cate 中创建 195、196、198 三种产品, 使下单页「产品类型」下拉可以直接选择,订单-计划-工件链路按产品区分。

11.2 脚本

db/mes_product_create_195_196_198.sql(可重复执行):

  • 默认模板取 CZ060 的 template_id,取不到时用任一启用模板兜底;
  • 按 sn 幂等创建 195/196/198,title 与 sn 一致、status='0'、cate=0(产品);
  • 已存在时自动校正名称、模板、状态,不会产生重复数据。

11.3 验证

SELECT id, title, sn, status, template_id, cate
  FROM mes_product_cate
 WHERE sn IN ('195', '196', '198')
 ORDER BY sn;

本地库(localhost:3307 / mes_cloud_xz)执行结果:

sn title status template_id cate
195 195 0 2102266346164490240(CZ060 模板) 0
196 196 0 2102266346164490240(CZ060 模板) 0
198 198 0 2102266346164490240(CZ060 模板) 0

执行后刷新下单页即可看到三种产品;如需为三种产品配置独立生产模板,可在「工件类型管理」中编辑 template_id。