2026-08-07 | 雷工笔记 | MES系列合集
一、背景
驾驶舱是MES系统的"仪表盘",也是工厂管理层最关注的功能模块。之前我分享了产能信息和设备信息模块,今天继续完善转运信息区域。
转运信息模块的核心是AGV小车的运行状态和任务数据展示。现场有5台AGV小车,由AGV调度系统统一管理,通过API接口将AGV状态实时上报给MES系统,MES收到后更新到数据库,驾驶舱再从数据库读取数据展示给用户。
整体数据链路:
AGV调度系统 → API上报 → MES接口 → PM_AGVStatusDetail表 → 计算工程 → 驾驶舱前端
PM_AGVTaskDetail表 → (case 8~11) → 图表/变量
本文聚焦计算工程后端脚本(SQL查询)和前端客户端页面变量绑定的具体实现,不带任何概念包装,全是代码和踩坑记录。

二、涉及的表结构
2.1 PM_AGVStatusDetail(AGV状态详情)
这个表由AGV调度系统通过API实时更新写入,5台AGV各有一条记录,字段如下:
javascript
CREATE TABLE "public"."PM_AGVStatusDetail" (
"id" int4 NOT NULL,
"c_AGVCode" int4 NOT NULL, -- AGV设备编码(1~5)
"c_AGVStatus" int4 NOT NULL, -- 状态:0未定义,1停止,2运行,3空闲,4读卡停止,5管制
"c_AGVBattery" int4, -- 电池电量
"c_AGVAlarm" int4, -- 报警状态
"c_AGVPosition" varchar(50), -- 当前位置
"c_FinchTime" timestamp -- 更新时间
);
ALTER TABLE "PM_AGVStatusDetail" ADD CONSTRAINT "AGV" UNIQUE ("c_AGVCode");
2.2 PM_AGVTaskDetail(AGV任务详情)
每一条记录代表一次AGV转运任务,字段如下:
javascript
CREATE TABLE "public"."PM_AGVTaskDetail" (
"ID" int4 NOT NULL,
"c_TaskID" varchar(100), -- 调度AGV任务编码
"c_AGVCode" int4, -- AGV设备编码
"c_LocationCode" varchar(50), -- 当前位置
"c_AGVTaskStatus" varchar(20), -- 任务状态:创建/执行中/已完成
"c_CreateTime" timestamp, -- 任务创建时间
"c_FinchTime" timestamp, -- 完成时间
"c_UpdateTime" timestamp -- 更新时间
);
三、功能实现的四个步骤
3.1 AGV状态更新(case 8)
需求:从PM_AGVStatusDetail表读取5台AGV的状态码,赋值给5个工程变量 System.状态_AGV01 \~ System.状态_AGV05,图形模型根据变量值显示不同颜色的AGV图标。
后端计算工程脚本(PM_cockpit请求式计算):
javascript
case 8://驾驶舱-转运信息-AGV状态
var sql = `SELECT "c_AGVCode", "c_AGVStatus"
FROM "PM_AGVStatusDetail"
ORDER BY "c_AGVCode"`;
var res=[];
var ret = await SQLQuery('RFM_PM', sql, res);
OutParam.records=res;
OutParam.errorCode=ret;
break;
前端客户端(驾驶舱页面,变量触发事件):
javascript
var M = { 'type': 8 };
requestCalculationPost($System.RFM_PM, 'PM_cockpit', M, function (res) {
if (res.Code === 0 && res.errorCode === 0) {
var resdata = res.records;
for (var i = 0; i < resdata.length; i++) {
var code = resdata[i].c_AGVCode;
var status = resdata[i].c_AGVStatus;
if (code === 1) $System.状态_AGV01 = status;
else if (code === 2) $System.状态_AGV02 = status;
else if (code === 3) $System.状态_AGV03 = status;
else if (code === 4) $System.状态_AGV04 = status;
else if (code === 5) $System.状态_AGV05 = status;
}
}
});
状态码 → 颜色映射:
| 状态码 | 含义 | 图标颜色 |
|---|---|---|
| 0 | 未定义 | 灰色 |
| 1 | 停止 | 红色 |
| 2 | 运行 | 绿色 |
| 3 | 空闲 | 黄色 |
| 4 | 读卡停止 | 橙色 |
| 5 | 管制 | 紫色 |
3.2 AGV任务列表(case 9)
需求:在Datagrid中展示最近10条AGV任务记录,5个字段:任务编码、AGV编号、当前位置、任务状态、创建时间。
后端:
javascript
case 9://驾驶舱-转运信息-AGV任务列表
var sql = `SELECT "c_TaskID", "c_AGVCode", "c_LocationCode",
"c_AGVTaskStatus", "c_CreateTime"
FROM "PM_AGVTaskDetail"
ORDER BY "c_UpdateTime" DESC NULLS LAST
LIMIT 10`;
var res=[];
var ret = await SQLQuery('RFM_PM', sql, res);
OutParam.records=res;
OutParam.errorCode=ret;
break;
关键细节:ORDER BY "c_UpdateTime" DESC NULLS LAST。为什么加 NULLS LAST?因为未完成的任务 c_UpdateTime 为NULL,不处理的话NULL值会排在最前面,导致取到的10条记录全是NULL。加上 NULLS LAST 确保真正有更新时间的记录排前面。
前端绑定Datagrid:
javascript
var M = { 'type': 9 };
requestCalculationPost($System.RFM_PM, 'PM_cockpit', M, function (res) {
if (res.Code === 0 && res.errorCode === 0) {
var resdata = res.records;
if (resdata.length === 0) {
$Function.tip('info', '无AGV任务数据!');
} else {
var datagrid1 = [];
for (var i = 0; i < resdata.length; i++) {
var data2 = {};
data2.number = i + 1;
data2.c_TaskID = resdata[i].c_TaskID;
data2.c_AGVCode = resdata[i].c_AGVCode;
data2.c_LocationCode = resdata[i].c_LocationCode || '';
data2.c_AGVTaskStatus = resdata[i].c_AGVTaskStatus;
data2.c_CreateTime = resdata[i].c_CreateTime;
datagrid1.push(data2);
}
$(DatagridX.id).datagrid({
data: datagrid1
});
}
}
});
3.3 近7日转运完成率(case 10)
需求:折线图展示近7天每天AGV转运任务的完成率(已完成任务数 / 总任务数 × 100%)。
后端:
javascript
case 10://驾驶舱-转运信息-近七日转运完成率
var sql = `SELECT
"c_CreateTime"::date AS "date",
ROUND(COUNT(*) FILTER (WHERE "c_AGVTaskStatus" = '已完成')
* 100.0 / NULLIF(COUNT(*), 0), 1) AS "rate"
FROM "PM_AGVTaskDetail"
WHERE "c_CreateTime" >= CURRENT_DATE - 7
GROUP BY "c_CreateTime"::date
ORDER BY "c_CreateTime"::date`;
var res=[];
var ret = await SQLQuery('RFM_PM', sql, res);
OutParam.records=res;
OutParam.errorCode=ret;
break;
技术要点:
-
NULLIF(COUNT(*), 0)防止当天没有任务时除零报错
-
FILTER (WHERE ...)是PostgreSQL的聚合过滤语法,比
CASE WHEN更简洁 -
::date是PostgreSQL的类型转换语法,将timestamp转为date以按天分组
前端(ECharts折线图,CustomCharts52):
javascript
var dates = getLastSevenDays(); // 生成最近7天日期数组
var M = { 'type': 10 };
requestCalculationPost($System.RFM_PM, 'PM_cockpit', M, function (res) {
if (res.Code === 0 && res.errorCode === 0) {
var resdata = res.records;
// 构建日期→完成率映射
var rateMap = {};
for (var i = 0; i < resdata.length; i++) {
var d = resdata[i].date;
var parts = d.split('-');
rateMap[parts[1] + '-' + parts[2]] = Number(resdata[i].rate);
}
// 按7天顺序填充数据,无数据的天填0
var chartData = [];
for (var j = 0; j < dates.length; j++) {
chartData.push(rateMap[dates[j]] || 0);
}
option = {
title: { text: "近七日转运完成率", ... },
xAxis: { data: dates, ... },
yAxis: { name: '单位:(100%)', ... },
series: [{
data: chartData,
type: 'line',
lineStyle: { color: 'DeepSkyBlue', width: 5 },
...
}]
};
CustomCharts52.$Children.echart.setOption(option, true);
}
});
3.4 近7日转运次数(case 11)
需求:柱状图展示近7天每天AGV转运任务的总数量。
后端:
javascript
case 11://驾驶舱-转运信息-近七日转运次数
var sql = `SELECT
"c_CreateTime"::date AS "date",
COUNT(*) AS "cnt"
FROM "PM_AGVTaskDetail"
WHERE "c_CreateTime" >= CURRENT_DATE - 7
GROUP BY "c_CreateTime"::date
ORDER BY "c_CreateTime"::date`;
var res=[];
var ret = await SQLQuery('RFM_PM', sql, res);
OutParam.records=res;
OutParam.errorCode=ret;
break;
前端的绑定逻辑与完成率类似,只是后端返回的是 cnt(数量)而非 rate(百分比),图表改为柱状图(CustomCharts53),使用渐变色柱状条。
四、遇到的坑和解决
坑1:PostgreSQL 的列名必须加双引号
KingFusion 连接的是 PostgreSQL 数据库,PG 对大小写敏感,KingFusion 建立表时字段名带了双引号(如 "c_AGVCode"),后续查询必须原样加上双引号,否则报错"字段不存在"。
错误写法:
var sql = `SELECT c_AGVCode, c_AGVStatus FROM PM_AGVStatusDetail`;
正确写法:
var sql = `SELECT "c_AGVCode", "c_AGVStatus" FROM "PM_AGVStatusDetail"`;
坑2:NULL值排序问题
ORDER BY "c_UpdateTime" DESC 时,NULL值在PostgreSQL中默认排在最前面(DESC排序时),导致 LIMIT 10 取到的都是没有更新时间的记录,真正需要的最新记录反而取不到。
解决:加 NULLS LAST,让NULL值排到最后。
坑3:静态数据到动态数据的过渡
之前驾驶舱的图表数据是静态写死的,比如完成率 95, 96, 95, 97, 98, 95, 97,转运次数 50, 45, 42, 36, 38, 31, 50。改动态数据后,需要前端构建 日期→数据 映射,然后按7天固定顺序填充,某天无数据时填0,避免图表数据错位。
坑4:刷新机制设计
AGV状态需要实时更新,但又不希望频繁调用后端。最终采用"页面变量取反触发"的方式:
-
页面创建时,取反变量 → 触发一次查询
-
如需定时刷新,在定时器位置取反同一变量即可
-
这样刷新的频率和触发时机完全可配置,不用修改查询逻辑
五、总结
转运信息模块的开发,核心就两件事:后端写SQL、前端绑定变量。但细节决定成败:
-
数据库设计决定了能做什么功能,PM_AGVStatusDetail 和 PM_AGVTaskDetail 两张表分别支撑了状态展示和任务统计
-
SQL查询要考虑PG的特殊语法和NULL值处理
-
前端架构用页面变量触发取代直接调用,刷新机制更灵活
-
代码管理:每次测试通过后,代码备份到知识库,并在驾驶舱页面创建Excalidraw逻辑流程图,避免时间久了忘记
相关文章:
代码备份:本文所有代码已归档到个人知识库,持续更新。