关联提交:
a7167fc fix: 打通生产订单-生产计划-工件关联链路(已完成 1/3:订单→计划→产品) 本次做 2/3:工位结果层面的无计划拦截 + 按订单/计划查询
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 | mes_product_record(生产记录/工位结果)没有计划字段 |
记录层面看不到属于哪个计划,履历无法按订单/计划追溯 |
| 2 | 提交工位结果不校验计划 | updateRecordResult() / updateRecord() 等直接写库;历史件、返修件、非正常创建路径产生的"无计划件"照样能过站 |
| 3 | 查询侧没有订单/计划维度 | 生产记录列表/导出只能按工件码、工位号查;看不出哪些件"无计划";计划页只能按 orderId 精确查 |
| 4 | 计划实际产出统计不完整 | MesProductPlanController.listData 只按 mes_product 计数,返修件/无计划件统计不到 |
拦截主体 = 工件(sn)+ 计划(plan),在 提交工位结果 时校验。
1) mes_product_record.plan_id ← 新增字段,本次加
2) mes_product.plan_id ← 已有字段
3) getCurrentPlan(cateSn) ← 按 当前班次开始日期+班次+工件类型 自动匹配(自愈),匹配到即回写 1)/2)
三级都取不到 → 拦截。
| 条件 | 提示语 |
|---|---|
| 找不到有效计划 | 该工件未关联生产计划(订单-计划-工件),当前班次无有效生产计划,不能作业! |
计划已删除/停用 (status != '0') |
该工件关联的生产计划已失效,不能作业! |
计划对应订单未下发 (order.state != '1') |
该工件关联的生产订单未下发,不能作业! |
| 例外 | 处理 |
|---|---|
返修提交 (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的做法)。
MesProductRecord.javajava
@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 增列:生产订单号、计划日期(导出用)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)
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) → 跳过拦截(只做查询,不强制)ServiceExceptionServiceExceptionmes_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 补齐MesProductRecordController.javalistData / listData2 / exportData:支持按 生产订单号、计划日期 过滤
java
if (StringUtils.isNotBlank(mesProductRecord.getOrderSn())) {
mesProductRecord.getSqlMap().getWhere().and("c.sn", QueryType.LIKE, mesProductRecord.getOrderSn());
}
listData:批量回填「无计划」标识(planId 为空的记录标红)| 文件 | 改动 |
|---|---|
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 已处理) |
mes_product.plan_id 计数再校验一次。| 文件 | 改动 |
|---|---|
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 |
列表加「已排产」「未排产」 |
| 问题 | 原因 | 处理 |
|---|---|---|
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 ... |
db/mes_product_record_plan.sql(必须先加字段,否则生产记录相关查询直接报错)js_sys_config 直插后需重启服务,或在"系统配置"页面重新保存一次触发缓存刷新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的说明人工清理。
plan_id,提交工位结果时直接从已加载的记录上取计划,不产生额外 SQL;只有历史无计划件才会走回退查询并自动回写(自愈)。orderSn 声明为 LIKE、planDay 声明为 EQ),控制器里另加了兜底条件,值一致不会冲突。isRepair=1)放行;开关关闭时全部放行;提交时不再校验超产数量(数量在创建工件时已扣)。把 订单 → 计划 → 产品 三段链路显式化:
生产订单.product_cate_id → 生产计划.product_cate_id → 工件(mes_product.cate)
(下单时选择) (排产时继承订单)
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 关联的那处是历史遗留,不影响本链路)。
| 位置 | 改动 |
|---|---|
MesProductOrderService.checkProductCate() |
保存时校验:未选择产品类型 → 按 BOM 的工件类型自动补齐;所选类型必须存在且启用;与 BOM 的工件类型不一致 → 报错「所选产品类型与BOM的工件类型(CZ060)不一致,请核对后重试!」 |
MesProductOrderController.form() |
下发产品类型下拉(mes_product_cate,status=0)、BOM→工件类型映射、当前计划的产品类型 |
mesProductOrderForm.html |
新增「产品类型」下拉(必填);选择 BOM 自动带出工件类型;新建订单按当前计划的产品类型默认带出 |
mesProductOrderList.html |
新增「产品类型」「产品类型编号」列 |
| 位置 | 改动 |
|---|---|
MesProductPlanService.save() |
产品类型优先取订单选择的产品类型,订单未维护时用 BOM 兜底;写入 plan.product_cate_id;"同日期+班次+产品唯一"校验改用 plan.product_cate_id;编辑计划时产品类型跟随订单同步 |
MesProductPlanService.getCurrentPlan(cateSn) |
匹配条件由 BOM 关联改为 plan.product_cate_id = cateSn(不再依赖 BOM 是否维护工件类型) |
mesProductPlanList.html |
新增「产品类型」列 |
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()| 检查项 | 结果 |
|---|---|
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'占位值), 产品类型无从推导,且匹配不到真实班次,不影响排产与拦截,建议人工清理。
在工件/产品类型主数据 mes_product_cate 中创建 195、196、198 三种产品,
使下单页「产品类型」下拉可以直接选择,订单-计划-工件链路按产品区分。
db/mes_product_create_195_196_198.sql(可重复执行):
CZ060 的 template_id,取不到时用任一启用模板兜底;sn 幂等创建 195/196/198,title 与 sn 一致、status='0'、cate=0(产品);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。