🏗️ 总体架构
🏭 车间传感器
→
📡 Modbus RTU / DTL645
→
🗄️ 环思 ERP (HSdyeingERP)
👨💼 人工录入(产量/质量/出入库)
→
🗄️ 环思 ERP
🤖 面料小田
Python + pymssql
→
📋 LOOP 静态看板
🔄 BI 交互看板
📎 Excel 文件
📱 钉钉推送
恒田数据链路全景
| 数据来源 | 采集方式 | 入 ERP 方式 | 小田查询方式 |
| 产量(染色/定型/后整理) | 刷卡机报工 / 人工录入 | ppTrackOutput / rsRegistrationDtl | pymssql SQL |
| 品质(异常/批色/物测) | 品管员录入 | ycAbnormalQualityRHdr / qmColorMatchHdr / qmTesthdr | pymssql SQL |
| 库存(成品/坯布/染化料) | 扫码出入库 | vwmmSTInStore / mmFPTotalStock / vwmmDCTotalStock | pymssql SQL |
| 能耗(水电蒸汽天然气) | IoT 传感器 | mnEquipmentEnergyRealTime(30分钟级) kbEquipmentEnergyUsage(日汇总) | pymssql SQL |
| 水表 | IoT 传感器(Modbus RTU) | COMModbus_RTU_SB_52/53 → ERP | pymssql SQL |
| 人力/班组 | HR 排班 | smUser / ppWorkerGroup | pymssql SQL |
核心结论:后台数据是 真实传感器数据,不是 Mock。能耗数据通过 Modbus RTU / DTL645 等工业协议,每 30 分钟采集一次写入 ERP。
📡 第1章 · 数据接入:环思ERP查询
1.1 数据库连接
import pymssql
with open('/opt/hengtian/secrets/hsdyeing_readonly.password') as f:
pw = f.read().strip()
conn = pymssql.connect(
server='61.160.83.154',
port=2433,
user='readonly_user',
password=pw,
database='HSdyeingERP',
charset='utf8'
)
⚠️ Python 环境:系统 python3 没有 pymssql 模块,必须用虚拟环境:/opt/hengtian/.venv/bin/python3
1.2 核心表速查
| 业务域 | 表名 | 核心字段 |
| 订单 | vwsdOrder | sOrderNo, sCustomerName, sSourceName(涤/棉), dDeliveryDate |
| 工卡 | psWorkFlowCard | sCardNo, sOrderNo, sStatus, sRawFabricNo(缸号), nPlanOutputQty, nFactOutputQty |
| 染色产量 | ppTrackOutput | nTrackQty(Kg), nTrackQtyEx(匹), tTrackTime |
| 批色 | qmColorMatchHdr/Dtl | sResult('OK'=通过), sCardNo |
| 品质异常 | ycAbnormalQualityRHdr/Dtl | sDefect, nRepineQty, sDutyDept(责任部门, 分号分隔) |
| 成品入库 | vwmmSTInStore | nInQty(Kg), nInPkgQty(匹), sStoreNo='G02' |
| 成品出库 | vwmmSTOutStore | nStoreOutQty(Kg), sStoreOutNo |
| 能耗(日汇总) | fiProduceDayCost | nEL_Amount(电费), nVA_Amount(蒸汽费), nGas_Amount, nWater_Amount |
| 能耗(实时) | mnEquipmentEnergyRealTime | nRealValue, tRealTime(30分钟间隔) |
| 能耗(日读数) | kbEquipmentEnergyUsage | nStartQty, nEndQty, nQty(当日用量) |
| 成本 | pbCommonDataCostSTData | 150列, 含坯布/染料/助剂/工资/水电蒸汽/五金/委外等 |
⚠️ 关键陷阱:
fiProduceDayCost 中电费是 nEL_Amount,不是 nElec_Amount;蒸汽是 nVA_Amount,不是 nSteam_Amount
- 品质异常
sDutyDept 可能包含多个部门(分号分隔),计算时需均分
- 返修工卡判断:卡号后缀含
-A 或 -B 为返修
- 时间口径默认 早 07:30 ~ 次日 07:30
- 工卡表
psWorkFlowCard 没有 sColorName/sColorNo,需 JOIN tmColor
1.3 标准查询模板
查当日染色产量
SELECT e.sEquipmentNo AS 染缸编号,
ISNULL(SUM(p.nTrackQty), 0) AS 产量_Kg
FROM ppTrackOutput p
JOIN emEquipment e ON p.uemEquipmentGUID = e.uGUID
JOIN ppTrackJob j ON p.uppTrackJobGUID = j.uGUID
JOIN psWorkFlowCard w ON j.upsWorkFlowCardGUID = w.uGUID
WHERE e.sEquipmentGroup = 'HT'
AND e.sEquipmentNo NOT LIKE 'DX%'
AND p.tTrackTime >= '2026-07-21 07:30:00'
AND p.tTrackTime < '2026-07-22 07:30:00'
GROUP BY e.sEquipmentNo
ORDER BY 产量_Kg DESC
查当日能耗
SELECT
SUM(nWater_Amount) AS 水费,
SUM(nEL_Amount) AS 电费,
SUM(nVA_Amount) AS 蒸汽费,
SUM(nGas_Amount) AS 天然气费
FROM fiProduceDayCost
WHERE dBillDate = '2026-07-21'
1.4 染化料成本查询
SELECT
sMaterialTypeName,
SUM(nAmount) AS 金额
FROM vwmmDCOutStore
WHERE tStoreOutTime >= '2026-07-21 07:30:00'
AND tStoreOutTime < '2026-07-22 07:30:00'
GROUP BY sMaterialTypeName
💡 经验值:染助剂成本约占每日总能耗的 50-55%,不能遗漏。完整能耗公式 = 工业水 + 电 + 蒸汽 + 天然气 + 污水 + 染料 + 助剂。
🔌 第2章 · 在线监测:传感器 / IoT 数据
已接入传感器清单
| 类型 | 协议 | 点位数量 | 示例 |
| 电表 | COMModbus_RTU_DB / DTL645 / DTLONE / TCPModBus_MW | ~30+ 个 | 各染缸电表(H13~H39 / Q1~Q11)、定型机电表 1#~8# |
| 水表 | COMModbus_RTU_SB | ~8 个 | [52]棉总用水水表、[53]涤总用水水表、[91]/[92]回用水池 |
| 蒸汽表 | COMModbus_RTU_ZQ | ~5 个 | 南/北涤车间蒸汽表、棉车间蒸汽表 |
| 天然气表 | COMModbus_RTU_TRQ / TCPModBus_MW_TRQ | ~7 个 | 定型机 2#/5#/6#/7#/8# 天然气表、锅炉天然气表 |
2.1 实时数据表(mnEquipmentEnergyRealTime)
每 30 分钟采集一次,记录所有表计的读数。数据字段:
sModulCode = 'COMModbus_RTU_SB_52'
sModulName = '[52]棉总用水水表'
sEngClass = 'WA'
nRealValue = 42417.62
tRealTime = 2026-07-21 14:30:00
2.2 日汇总表(kbEquipmentEnergyUsage)
每日 08:00 自动生成一条汇总记录(按 07:30~07:30 会计口径):
sWorkCentreName = '染色车间'
sEquipmentNo = '002'
sEquipmentName = '涤车间总表'
sClass = '工业水(吨)'
sUserModuleName = '[53]涤总用水水表'
nStartQty = 158940.00
nEndQty = 159020.50
nQty = 80.50
tStartTime = 2026-07-20 07:30:00
tEndTime = 2026-07-21 07:30:00
2.3 判断数据真实性的方法
| 特征 | 真实数据表现 | 示例 |
| 读数递增 | 期初≠期末,数字连续增长 | 1#定型机电表 1219390.8 → 1220161.2 |
| 工业协议 | 使用标准协议名称 | COMModbus_RTU = RS485 Modbus RTU |
| 设备关联 | 表计编号与实际设备对应 | [68]1#定型机电表 → 设备 DX01 |
| 规律采样 | 固定的时间间隔 | 30分钟一次,持续数年 |
| 小幅波动 | 连续两次读数差反映真实消耗 | 棉总水表 42416 → 42417.62(半小时增1.62吨) |
2.4 水表查询(车间级)
SELECT dReportDate, sUserModuleName, nQty
FROM kbEquipmentEnergyUsage
WHERE sClass = '工业水(吨)'
AND sWorkCentreName = '染色车间'
AND (sUserModuleName LIKE '%涤总用水水表%'
OR sUserModuleName LIKE '%棉总用水水表%')
ORDER BY dReportDate DESC
🧠 记忆提示:染色车间只看「涤总用水水表」和「棉总用水水表」,不包含回用水池、染缸水表、水洗机水表等其他水表。
🎨 第3章 · 生成 HTML 看板
3.1 准备工作
mkdir -p /opt/hengtian/workspace/hengtian-fabric/outbox
3.2 三步生成法
1 查数据 — 用 pymssql 从 ERP 查数据,存为 JSON
import pymssql, json
data = {'total_kgs': 12345, 'orders': [...], 'defects': [...]}
with open('data.json', 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, default=str)
2 写 HTML — 单文件自包含,CSS/JS 内联,数据嵌入 <script>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>生产状态看板</title>
<style>
body { font-family: ...; }
</style>
</head>
<body>
<div id="summary"></div>
<div id="details"></div>
<script>
const DATA = JSON.parse('{"total_kgs": 12345, ...}');
function render(data) { ... }
render(DATA);
</script>
</body>
</html>
3.3 看板设计要点
| 要素 | 推荐做法 |
| 布局 | 顶部汇总卡片(4~6个KPI)+ 下方明细表格,有导航Tab |
| 配色 | 🔴 >90天超期 / 🟡 >30天超期 / 🔵 正常 / 🟢 完成 |
| 搜索 | 每个表格上方放搜索输入框,实时过滤 |
| 排序 | 点击表头排序 asc/desc |
| 响应式 | 用 CSS flex/grid,在手机端也能看 |
| 单文件 | 所有 CSS 和 JS 内联,不引用外部 CDN |
| 文件大小 | 控制在 2MB 以内 |
✅ 示例看板:需求表、生产进度、品质异常、成本分析 等看板模板可在面料小田工作区查找。
🌐 第4章 · 发布到 LOOP(静态报告)
4.1 什么是 LOOP
LOOP 是恒田内部的报告发布平台。HTML 看板上传到 LOOP 后,会获得一个固定链接,可供全员在公网访问。
| 地址 | 说明 |
https://www.handagroup.ai/loop/<slug> | 公共查看页(含 LOOP 包装) |
https://www.handagroup.ai/api/loop/reports/<slug>/html | 纯 HTML 直链(浏览器直接渲染) |
4.2 发布步骤
1 准备 HTML 文件 — 单文件自包含看板(见第3章)
2 调用发布 API
import os, json, requests
TOKEN = os.environ['HANDA_LOOP_UPLOAD_TOKEN']
SLUG = 'production-dashboard-20260721'
with open('/path/to/dashboard.html', 'r', encoding='utf-8') as f:
html_content = f.read()
resp = requests.post(
'https://www.handagroup.ai/api/loop/reports',
json={
'slug': SLUG,
'title': '生产进度看板 | 2026年7月21日',
'description': '纺织面料生产进度实时看板',
'owner': '恒田小兴',
'html': html_content,
'file_name': 'dashboard.html',
'tags': ['production', 'dashboard'],
'source_agent': 'hengtian-fabric',
'uploaded_by': '面料小田'
},
headers={'Authorization': f'Bearer {TOKEN}'}
)
print(resp.json())
curl -s -o /dev/null -w "%{http_code}" \
https://www.handagroup.ai/loop/production-dashboard-20260721
curl -s -o /dev/null -w "%{http_code}" \
https://www.handagroup.ai/api/loop/reports/production-dashboard-20260721/html
4.3 发布注意事项
🚫 重要限制:
- slug 不支持中文,只能用英文小写、数字和中划线
- token 从环境变量读取,不要硬编码
- API 只接受 JSON POST,不是 multipart/form-data
- HTML 内容字段是
html,不是 html_content
- 文件大小 不超过 2MB
- LOOP 没有 DELETE 接口,取消需用新内容覆盖
4.4 取消(下架)看板
cancel_html = '''<!DOCTYPE html>
<html><body>
<h1>已取消</h1>
<p>此报告已被取消。</p>
</body></html>'''
requests.post('https://www.handagroup.ai/api/loop/reports',
json={'slug': 'old-slug', 'html': cancel_html, 'owner': '恒田小兴'})
🔄 第5章 · BI 看板模式(交互刷新)
5.1 什么时候用 BI 看板
| 场景 | 推荐模式 | 说明 |
| 一次分享、固定报告 | LOOP 发布 | 静态 HTML,公网可访问 |
| 需要点按钮刷新数据 | BI 看板 | Python 服务 + API 查 ERP,前端 JSON 更新 |
| 局域网内频繁查看 | BI 看板 | 2000 端口起服务,省去每次重新发布 |
🚫 不要部署 BI 看板到 LOOP:LOOP 只存静态 HTML,刷新按钮会 404。BI 看板必须有后端 API 服务。
5.2 架构
📡 Python HTTP Server (port 19656)
├── GET / → 返回 index.html(含初始数据)
└── GET /api/refresh → 查ERP → 返回JSON → 前端更新DOM
5.3 后端启动
cd /opt/hengtian/workspace/hengtian-fabric
source .venv/bin/activate
python3 scripts/dashboard_server.py 19656
http://服务器IP:19656/
http://127.0.0.1:19656/
fuser -k 19656/tcp
5.4 前端刷新逻辑
function refreshData() {
const btn = document.getElementById('refreshBtn');
btn.disabled = true;
btn.textContent = '⏳ 刷新中...';
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/refresh', true);
xhr.timeout = 60000;
xhr.onload = function() {
try {
const res = JSON.parse(xhr.responseText);
if (res.ok) {
initDashboard(res.data);
} else {
alert('刷新失败: ' + res.error);
}
} catch(e) {
alert('解析失败');
}
btn.disabled = false;
btn.textContent = '🔄 刷新数据';
};
xhr.send();
}
5.5 后台运行(长期服务)
terminal(background=true, command='python3 scripts/dashboard_server.py 19656')
nohup .venv/bin/python3 scripts/dashboard_server.py 19656 > logs/dashboard.log 2>&1 &
⏰ 第6章 · 定时推送 / 日报
6.1 创建定时任务
cronjob(action='create',
schedule='0 2 * * *',
prompt='查昨日产量数据并生成日报推送',
skills=['erp-data-export', 'dingtalk-output-format'])
| 执行频率 | Cron 表达式 | 北京时间 |
| 每天 | 0 2 * * * | 10:00 |
| 每天 | 0 7 * * * | 15:00 |
| 每隔 30 分钟 | 30m | 每 30 分钟 |
| 每周一 | 0 1 * * 1 | 周一 09:00 |
| 每月1日 | 0 2 1 * * | 每月1日 10:00 |
6.2 两种推送模式
no_agent 模式(推荐)
脚本直接执行,不消耗 LLM token。适合固定格式的数据推送:
cronjob(action='create',
schedule='0 2 * * *',
script='scripts/daily_production_push.py',
no_agent=true)
LLM-agent 模式
Agent 阅读数据后汇总、分析、生成自然语言报告:
cronjob(action='create',
schedule='0 1 * * 1',
prompt='总结上周生产数据,分析异常趋势,生成周报摘要')
6.3 钉钉群推送
注意:cron job 的 deliver 参数对钉钉群聊不可靠。如需推到钉钉群,需用 群机器人 Webhook 主动 POST。
import requests
WEBHOOK = 'https://oapi.dingtalk.com/robot/send?access_token=xxx'
msg = {
'msgtype': 'markdown',
'markdown': {
'title': '📊 每日生产汇总',
'text': '''# 📊 每日生产汇总\n\n
| 项目 | 数值 |\n
|------|------|\n
| 入库产量 | 32.5 吨 |\n
| 出库产量 | 28.1 吨 |'''
}
}
requests.post(WEBHOOK, json=msg)
💡 推送格式:钉钉机器人不渲染表格行。如果数据多,推荐 列表+emoji 格式,然后把明细放 LOOP 看板链接。
6.4 查看与管理任务
cronjob(action='list')
cronjob(action='pause', job_id='xxx')
cronjob(action='remove', job_id='xxx')
cronjob(action='run', job_id='xxx')
📎 第7章 · Excel 文件交付
7.1 生成 Excel 并发送
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
wb = openpyxl.Workbook()
ws = wb.active
ws.title = '生产数据'
header_font = Font(name='微软雅黑', size=11, bold=true, color='FFFFFF')
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
thin_border = Border(
left=Side(style='thin'), right=Side(style='thin'),
top=Side(style='thin'), bottom=Side(style='thin'))
headers = ['客户', '订单号', '颜色', '数量(Kg)', '交期']
for col, h in enumerate(headers, 1):
cell = ws.cell(row=1, column=col, value=h)
cell.font = header_font
cell.fill = header_fill
cell.alignment = Alignment(horizontal='center')
cell.border = thin_border
for row_idx, row_data in enumerate(data_rows, 2):
for col_idx, val in enumerate(row_data, 1):
cell = ws.cell(row=row_idx, column=col_idx, value=val)
cell.border = thin_border
output = '/opt/hengtian/workspace/hengtian-fabric/outbox/生产数据_20260721.xlsx'
wb.save(output)
print(f'✅ 已保存: {output}')
7.2 发送文件到钉钉会话
MEDIA:/opt/hengtian/workspace/hengtian-fabric/outbox/生产数据_20260721.xlsx
面料小田会自动将 MEDIA 路径的文件作为附件发送到当前钉钉会话。
📁 文件类型支持:.xlsx .xls .csv .pdf .html .docx .pptx .txt .md .png .jpg
❓ 常见问题
Q: 为什么我查到的数据是 0 或 NULL?
常见原因及排查:
- 时间口径不对 — 默认 07:30~次日07:30,不是自然日 00:00~24:00
- 字段名拼错 — 如 `nEL_Amount` 不是 `nElec_Amount`,`nVA_Amount` 不是 `nSteam_Amount`
- 表不存在 — 先 `SELECT TOP 0 * FROM 表名` 确认表存在
- 权限不足 — readonly_user 不能执行存储过程,只能用直接 SQL
- 数据库掉线 — ERP 服务器偶尔超时,加 3 次重试机制
Q: 传感器数据是实时的吗?
mnEquipmentEnergyRealTime 表每 30 分钟采集一次(不是真正的实时)。
kbEquipmentEnergyUsage 表每天 08:00 生成日汇总(07:30~07:30 口径)。
Q: 如何判断我的数据是正确可信的?
做 sanity check(合理性校验):
- 单缸单班产量一般在 0.5~3.0 吨,如果某缸 >5 吨,可能单位输错了(公斤→吨)
- 能耗 2,300~2,800 元/吨 是正常范围
- 染助剂成本约占每日总能耗的 50~55%
- 固定成本(工资、辅料等)约 16 万/天
Q: BI 看板和 LOOP 发布有什么区别?
| LOOP 发布 | BI 看板 |
| 刷 新 | ❌ 不能,静态页面 | ✅ 点按钮刷新 |
| 访问 | 公网可访问 | 局域网内访问 |
| 数据更新 | 需重新生成→重新发布 | 调用 API 即可 |
| 适用 | 一次性报告、分享给外部 | 内部日常监控 |
Q: 我想定时把 Excel 报表发到钉钉群,怎么做?
两步:
- 写一个 Python 脚本,查 ERP → 生成 Excel → 通过钉钉群机器人 Webhook 发送链接
- 用
cronjob(action='create', no_agent=true, script='xxx.py') 设定时间
注意:钉钉机器人不能直接发文件附件,只能发消息+链接。