订单-计划-产品-记录 全链路 & 无计划拦截 实施方案
关联提交: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
- 列定义新增:
java
@Column(name="plan_id", attrName="planId", label="生产计划"),
- 字段新增:
planId、planDay(计划日期)、planOrderId(计划所属订单)、orderSn(生产订单号)、shiftTitle(班次)
- 恢复并启用
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 之前被注释占用,现已释放)。
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)
执行顺序:
- 开关:
"0".equals(Global.getConfig("mes.plan.intercept")) → 直接返回 null(不拦截)
- 返修:
"1".equals(isRepair) → 跳过拦截(只做查询,不强制)
- 依次尝试 2.1 的三级来源;三级都取不到 → 抛
ServiceException
- 校验计划有效性(2.2 的 2、3 条)→ 不合法抛
ServiceException
- 命中第 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 已处理) |
八、待确认事项
- 拦截范围:只在"提交工位结果"拦截,还是"创建工件"也要再次强校验(现在创建已强校验)?建议两处都保留。
- 返修是否放行:建议放行(返修不占产量),若管理上要求返修也必须挂在有效计划下,去掉例外即可。
- 无计划件是否允许补录:建议提供"按 日期+班次+工件类型 批量补关联计划"的后台工具/SQL,用于历史数据归集。
- 计划超产是否在提交时拦截:建议否(数量在创建时已扣),如需则按
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 发布步骤
- 先执行
db/mes_product_record_plan.sql(必须先加字段,否则生产记录相关查询直接报错)
- 再发布应用
js_sys_config 直插后需重启服务,或在"系统配置"页面重新保存一次触发缓存刷新
- 灰度建议:先把
mes.plan.intercept 置为 0 上线,确认存量数据回填正常后再置 1 开启拦截
9.4 关键实现说明
- 热路径无额外查询:新建工件的记录在创建时就写入
plan_id,提交工位结果时直接从已加载的记录上取计划,不产生额外 SQL;只有历史无计划件才会走回退查询并自动回写(自愈)。
- 关联字段查询:JeeSite 会把 join 上的字段自动绑定为查询条件(
orderSn 声明为 LIKE、planDay 声明为 EQ),控制器里另加了兜底条件,值一致不会冲突。
- 拦截范围:返修(
isRepair=1)放行;开关关闭时全部放行;提交时不再校验超产数量(数量在创建工件时已扣)。