本文基于真实项目------一个智能生产线MES系统的驾驶舱开发过程整理,记录了从表结构分析、业务逻辑梳理到前后端代码实现的全过程。
一、背景
近期在做一个MES项目,驾驶舱界面需要展示工厂的产能信息,包含3张ECharts图表:
| 图表 | 类型 | 展示内容 |
|---|---|---|
| 年度产量趋势 | 折线图 | 1~12月成品产量变化 |
| 近7日产量趋势 | 折线图 | 最近7天每日成品产量 |
| 年度产量达成 | 柱状图 | 计划产量 vs 实际产量 |
平台是KingFusion(亚控科技的MES低代码平台),采用前后端分离架构:
-
前端:KingFusion页面设计器 + ECharts图表控件
-
后端:请求式计算脚本(类似API接口)

二、数据库表结构分析
最开始我以为只需要两张表,但随着分析的深入,最终涉及4张表。
2.1 主计划列表(PM_MaiorPlanData)
这张表记录的是生产计划,每个订单对应一条主计划记录。
| 字段 | 说明 |
|---|---|
c_MasterPlanOrder |
主计划编号(主键) |
i_Num |
计划数量 |
d_DeliverDate |
交货日期 |
i_ProductState |
生产状态(0执行中/1待执行/2已完成) |
用途:提供年度计划产量数据,用于图表3的"计划产量"柱状图。
2.2 工单列表(PM_WorkOrderData)
主计划拆分为多个工单,每个工序对应一个工单。
| 字段 | 说明 |
|---|---|
c_WorkOrder |
工单号(主键) |
c_MasterPlanOrder |
关联主计划编号 |
c_MaterialCode |
物料编码(关键关联字段) |
c_WorkOrderType |
工单类型(0半成品/1成品/2外协) |
用途:桥梁表,将报工记录与物料档案关联起来。
2.3 报工记录表(PM_WorkReportRecord)
这是产能信息的核心数据表,每个工序完工后都会产生一条报工记录。
| 字段 | 说明 |
|---|---|
c_WorkReportCode |
报工单号(主键) |
c_WorkOrder |
工单号(关联PM_WorkOrderData) |
d_Date |
报工日期 |
i_Num |
报工数量(实际产量) |
c_Line |
设备/线体 |
2.4 物料档案信息表(AA_Basic_MaterialFileTable)
这张表定义了每个物料的分类属性,是最关键的过滤依据。
| 字段 | 说明 |
|---|---|
c_MaterialCode |
物料编码(主键) |
c_MaterialClassification |
物料分类(原料/半成品/成品) |
c_MaterialName |
物料名称 |
三、核心业务逻辑------为什么要加"成品过滤"?
这是开发过程中最关键的发现,也是这篇文章最有价值的部分。
问题场景
一开始我直接统计所有报工记录的 i_Num,结果数据明显偏大。经过分析发现:
一个完整的防护门生产,需要经过多个工序:下料→门框焊接→门扇焊接→组装→喷涂→检验。
每个工序都要报工,但只有最后一个"组装"工序的报工才代表成品产出。
门框、门扇等中间工序的报工,对应的是半成品,不是成品。
如果不做过滤,驾驶舱显示的"产量"会把门框、门扇、面板等所有工序的报工都算进去,数据就失真了。
解决方案
通过三表JOIN + 物料分类过滤,只统计物料分类为"成品"的报工记录:
bash
PM_WorkReportRecord (报工记录)
↓ c_WorkOrder JOIN
PM_WorkOrderData (工单列表)
↓ c_MaterialCode JOIN
AA_Basic_MaterialFileTable (物料档案)
↓ WHERE c_MaterialClassification = '成品'
四、后端实现(请求式计算脚本)
在后端计算工程中,我创建了一个请求式计算脚本 PM_cockpit,用 switch-case 结构区分不同接口。
case 1:年度产量趋势
sql
SELECT
EXTRACT(MONTH FROM wrr."d_Date") AS month,
COALESCE(SUM(wrr."i_Num"), 0) AS actual_qty
FROM "PM_WorkReportRecord" wrr
JOIN "PM_WorkOrderData" wod ON wrr."c_WorkOrder" = wod."c_WorkOrder"
JOIN "AA_Basic_MaterialFileTable" mft ON wod."c_MaterialCode" = mft."c_MaterialCode"
WHERE mft."c_MaterialClassification" = '成品'
AND wrr."d_Date" >= '2026-01-01' AND wrr."d_Date" < '2027-01-01'
AND wrr."c_WorkOrder" IS NOT NULL AND wrr."c_WorkOrder" != 'null'
GROUP BY month ORDER BY month;
case 2:近7日产量趋势
sql
SELECT
wrr."d_Date" AS date,
COALESCE(SUM(wrr."i_Num"), 0) AS actual_qty
FROM "PM_WorkReportRecord" wrr
JOIN "PM_WorkOrderData" wod ON wrr."c_WorkOrder" = wod."c_WorkOrder"
JOIN "AA_Basic_MaterialFileTable" mft ON wod."c_MaterialCode" = mft."c_MaterialCode"
WHERE mft."c_MaterialClassification" = '成品'
AND wrr."d_Date" >= CURRENT_DATE - 7
AND wrr."c_WorkOrder" IS NOT NULL AND wrr."c_WorkOrder" != 'null'
GROUP BY wrr."d_Date" ORDER BY wrr."d_Date";
case 3:年度产量达成(计划 vs 实际)
这个case需要双路查询,分别返回计划产量和实际产量:
sql
case 3://驾驶舱-产能信息-年度产量达成
// 计划产量:从主计划表按月份汇总
var sqlPlan =`SELECT
EXTRACT(MONTH FROM "d_DeliverDate") AS month,
COALESCE(SUM("i_Num"), 0) AS plan_qty
FROM "PM_MaiorPlanData"
WHERE "d_DeliverDate" >= '2026-01-01' AND "d_DeliverDate" < '2027-01-01'
GROUP BY month ORDER BY month`;
// 实际产量:三表JOIN + 成品过滤
var sqlActual =`SELECT
EXTRACT(MONTH FROM wrr."d_Date") AS month,
COALESCE(SUM(wrr."i_Num"), 0) AS actual_qty
FROM "PM_WorkReportRecord" wrr
JOIN "PM_WorkOrderData" wod ON wrr."c_WorkOrder" = wod."c_WorkOrder"
JOIN "AA_Basic_MaterialFileTable" mft ON wod."c_MaterialCode" = mft."c_MaterialCode"
WHERE mft."c_MaterialClassification" = '成品'
AND wrr."d_Date" >= '2026-01-01' AND wrr."d_Date" < '2027-01-01'
AND wrr."c_WorkOrder" IS NOT NULL AND wrr."c_WorkOrder" != 'null'
GROUP BY month ORDER BY month`;
var resPlan=[], resActual=[];
var retPlan = await SQLQuery('RFM_PM', sqlPlan, resPlan);
var retActual = await SQLQuery('RFM_PM', sqlActual, resActual);
OutParam.records = resPlan; // 前端取 records
OutParam.records1 = resActual; // 前端取 records1
OutParam.errorCode = retPlan;
OutParam.errorCode1 = retActual;
break;
注意:OutParam.records 和 OutParam.records1 分别返回两个数据集,前端通过 res.records 和 res.records1 分别接收。
五、前端实现(页面事件 + ECharts)
5.1 界面创建时------初始化变量
在驾驶舱页面加载时,通过翻转变量值来触发变量改变事件,实现图表数据的自动加载:
javascript
//=============产能信息==============//
//近7日产量趋势图
$Variable.Day7Output=!$Variable.Day7Output;
//年度产量趋势
$Variable.AnnualOutput=!$Variable.AnnualOutput;
//年度产量达成情况
$Variable.AnnualOutputAchieve=!$Variable.AnnualOutputAchieve;
5.2 变量事件------调用后端 + 更新图表
以年度产量趋势为例,变量改变事件中通过 requestCalculationPost 调用后端,返回数据后更新 ECharts:
javascript
//年度产量趋势
var M={ 'type': 1 };
requestCalculationPost($System.RFM_PM, 'PM_cockpit', M, function (res) {
if(res.Code===0){
if (res.errorCode === 0) {
var resdata = res.records;
var monthData = ['1','2','3','4','5','6','7','8','9','10','11','12'];
var actualData = [0,0,0,0,0,0,0,0,0,0,0,0];
for (var i = 0; i < resdata.length; i++) {
var idx = parseInt(resdata[i].month) - 1;
actualData[idx] = resdata[i].actual_qty;
}
option = {
title: {
text: "年度产量趋势",
left: "center",
textStyle: { color: 'white', fontSize: 30 },
top: 11
},
xAxis: {
type: 'category',
boundaryGap: false,
data: monthData,
axisLine: { lineStyle: { color: 'white' } },
axisLabel: { textStyle: { color: 'white', fontSize: 20 } },
name: '月份',
nameTextStyle: { fontSize: 20 }
},
yAxis: {
axisLine: { lineStyle: { color: 'white' } },
axisLabel: { textStyle: { color: 'white', fontSize: 20 } },
splitLine: { show: false },
type: 'value',
name: '单位:(樘)'
},
series: [{
data: actualData,
type: 'line',
lineStyle: { color: 'green', width: 5 },
areaStyle: {},
label: { show: true, color: 'white', fontSize: 20 }
}]
};
CustomCharts42.$Children.echart.setOption(option, true);
} else {
$Function.tip('error', '年度产量趋势查询失败!');
}
}
});
5.3 近7日产量趋势------动态日期
前端用 getLastSevenDays() 函数动态生成近7天的日期数组,与后端返回的数据做映射:
javascript
var dates = getLastSevenDays();
function getLastSevenDays() {
const dates = [];
for (let i = 6; i >= 0; i--) {
const date = new Date();
date.setDate(date.getDate() - i);
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
dates.push(`${month}-${day}`);
}
return dates;
}
5.4 年度产量达成------双柱对比
这个图表有两个柱子系列,用不同颜色区分:
javascript
series: [
{
name: '计划产量',
type: 'bar',
data: planQty,
itemStyle: { color: '#FFD400' }, // 黄色
barWidth: '20'
},
{
name: '实际产量',
type: 'bar',
data: actualQty,
itemStyle: { color: '#2b80ff' }, // 蓝色
barWidth: '16'
}
]
六、踩坑总结
坑1:成品过滤------最大的坑
一开始只统计了报工记录表,发现数据偏大。后来才意识到报工记录包含了所有工序的数据,必须通过物料档案表过滤出"成品"。
坑2:数据源名称
一开始以为不同数据库的数据源名称不同,实际上所有表在同一个数据库,不需要跨库查询,一个 JOIN 就能解决。
坑3:ECharts组件ID
驾驶舱JSON中图表的 name 属性(如 CustomCharts42)就是前端JS中引用的组件ID,代码中
CustomCharts42.$Children.echart.setOption() 必须与设计器中的名称一致。
七、完整数据流图
javascript
PM_WorkReportRecord (报工记录)
│ c_WorkOrder
▼
PM_WorkOrderData (工单列表)
│ c_MaterialCode
▼
AA_Basic_MaterialFileTable (物料档案)
│ WHERE c_MaterialClassification = '成品'
▼
GROUP BY month/date → SUM(i_Num) → actual_qty
│
├─→ 年度产量趋势 (CustomCharts42)
├─→ 近7日产量趋势 (CustomCharts43)
└─→ 年度产量达成 (CustomCharts41)
八、总结
KingFusion驾驶舱开发的核心思路:
-
数据源分析:先理清业务数据在哪些表中,字段含义是什么
-
业务逻辑梳理:产量统计的关键不是"怎么统计",而是"统计什么"------成品/半成品/原料的区分是MES系统的核心业务逻辑
-
前后端分离:后端请求式计算负责SQL查询,前端变量事件负责数据展示,通过
requestCalculationPost通信 -
ECharts渲染:用
setOption动态更新图表,数据从后端返回后填充到option中
本文基于某智能生产线MES项目实战经验整理,所用平台为亚控KingFusion。
如果你也在做KingFusion相关的MES项目,欢迎交流讨论。