自研實(shí)踐:突破組態(tài)軟件限制的架構(gòu)設(shè)計與實(shí)現(xiàn))
1. 這套報表系統(tǒng)到底解決什么問題1.1 組態(tài)軟件自帶的報表為什么越用越難受在工控這個圈子里待久了我相信不少同行都有過類似的體驗(yàn)項(xiàng)目要上線了甲方提了一堆報表需求——周期報表、趨勢報表、能耗報表、交接班報表每一樣都不一樣。組態(tài)軟件自帶的報表功能呢也能做但總感覺是在將就著用。單元格合并、跨表聯(lián)查、按工單維度匯總這些稍微復(fù)雜一點(diǎn)的需求在組態(tài)軟件里做起來特別費(fèi)勁改一次要重新編譯下載一次版本管理也是一團(tuán)亂麻。我做了幾年上位機(jī)項(xiàng)目最初也老老實(shí)實(shí)依托組態(tài)軟件做報表后來發(fā)現(xiàn)這條路越走越窄。歸納了一下組態(tài)軟件自帶報表的限制主要卡在四個地方。第一模板和工程深度耦合。報表模板是嵌在組態(tài)工程里的意味著你要加一個字段、改一個表頭、調(diào)整一次統(tǒng)計口徑就得進(jìn)組態(tài)開發(fā)環(huán)境改工程文件然后再編譯、下載、運(yùn)行。在項(xiàng)目調(diào)試期這種改動幾乎天天都有每次都走這套流程非常消耗精力而且容易把人搞崩潰。第二數(shù)據(jù)源非常封閉。組態(tài)軟件自帶的報表一般只能引用自己內(nèi)部定義的變量點(diǎn)比如壓力、溫度、流量這些實(shí)時值。可實(shí)際的報表需求往往會涉及外部數(shù)據(jù)比如MES下發(fā)過來的工單號、ERP里的物料編碼、人工錄入的質(zhì)檢結(jié)果這些數(shù)據(jù)組態(tài)軟件根本拿不到。數(shù)據(jù)源不打通報表就只能局限在設(shè)備實(shí)時數(shù)據(jù)這個圈子里做一個孤島。第三打印和導(dǎo)出能力弱。組態(tài)軟件的報表模塊普遍重顯示、輕排版做一張符合甲方格式規(guī)范的驗(yàn)收單需要花費(fèi)大量時間調(diào)格式。很多組態(tài)自帶的表格控件連精確控制行高、合并單元格、分頁打印都做不到更不用說批量導(dǎo)出成Excel、PDF交給第三方系統(tǒng)歸檔了。第四升級與維護(hù)坑多。工程一旦換了版本或者換了電腦報表要跟著重新配一遍。項(xiàng)目多了以后各個工程里的報表邏輯互相沒有復(fù)用性同樣的改動要在不同項(xiàng)目里重復(fù)操作維護(hù)成本極高。坦白講這些痛點(diǎn)單拎出來哪一條都不致命但疊加在一起就讓人非常難受了。所以我后來做了一個決定干脆自己寫一套獨(dú)立的PC端報表系統(tǒng)把數(shù)據(jù)呈現(xiàn)徹底從組態(tài)軟件里解放出來。1.2 項(xiàng)目啟動從三條現(xiàn)場需求說起這個自研項(xiàng)目不是憑空拍腦袋的起因是某條產(chǎn)線項(xiàng)目提出了三個比較典型的需求讓我意識到繼續(xù)依賴組態(tài)報表不是長久之計。第一個需求是跨系統(tǒng)聯(lián)查??蛻粝M請蟊砝锊粌H顯示每臺設(shè)備的運(yùn)行時長、報警條數(shù)還要把當(dāng)天對應(yīng)工單的批次號、良品率、操作班組一起列出來。這些數(shù)據(jù)一部分來自PLC采集一部分來自MES數(shù)據(jù)庫還有一部分是人工錄入的Excel臺賬。組態(tài)軟件要做這種跨系統(tǒng)關(guān)聯(lián)基本只能靠腳本硬湊而且維護(hù)邏輯非常脆弱。第二個需求是按班組維度的能耗統(tǒng)計??蛻粢竺總€班次單獨(dú)統(tǒng)計水、電、氣的累計用量需要按照早班、中班、夜班的班次時間切分而不是簡單的按自然天切分。組態(tài)軟件的報表只能按固定時間窗口出數(shù)不支持自定義班次時間表做出來的報表口徑對不上現(xiàn)場就會扯皮。第三個需求是歷史審計追溯。客戶要求任意時間點(diǎn)、任意設(shè)備的歷史數(shù)據(jù)快照必須能精確回溯并且導(dǎo)出的報表要帶查詢條件、生成時間、操作人等信息。這已經(jīng)超出了組態(tài)軟件報表模塊的能力范圍更像是一個獨(dú)立的數(shù)據(jù)服務(wù)系統(tǒng)。接到這三個需求之后我把方案擺到桌面上評估繼續(xù)在組態(tài)軟件里做報表開發(fā)周期長、維護(hù)成本高而且大概率不能滿足甲方格式上的要求買一套現(xiàn)成的商業(yè)化報表工具又面臨授權(quán)費(fèi)高、二次開發(fā)和組態(tài)數(shù)據(jù)對接的復(fù)雜問題。反復(fù)比較之后我決定走自研這條路基于現(xiàn)有數(shù)據(jù)采集架構(gòu)開發(fā)一套以PC客戶端為主、服務(wù)端為輔的獨(dú)立通用報表系統(tǒng)。1.3 方案選型為什么不用花架子只選能落地的技術(shù)組合自研報表系統(tǒng)第一件事不是寫代碼而是把技術(shù)棧和部署形態(tài)定下來。我當(dāng)時的選型原則很樸素不追求新潮只求穩(wěn)定、可控、現(xiàn)場好維護(hù)。開發(fā)框架選了C# WinForms。雖然業(yè)界有更時髦的WPF、Electron但WinForms開發(fā)效率高部署簡單在工控機(jī)的低配置環(huán)境下表現(xiàn)也穩(wěn)定而且做表格類界面沒有太多復(fù)雜渲染需求。我們團(tuán)隊(duì)對C#最熟后期維護(hù)不會有上手成本。數(shù)據(jù)存儲分兩層。實(shí)時數(shù)據(jù)入歷史庫我先用SQLite做本地歸檔同時保留一個MySQL實(shí)例用于跨系統(tǒng)的業(yè)務(wù)數(shù)據(jù)關(guān)聯(lián)。SQLite適合單機(jī)部署、免安裝、性能足夠MySQL用于項(xiàng)目里已經(jīng)存在的業(yè)務(wù)系統(tǒng)對接。后面如果遇到海量時序數(shù)據(jù)場景可以再引入專門的時序數(shù)據(jù)庫但核心的報表查詢邏輯要封裝在獨(dú)立的數(shù)據(jù)訪問層里避免和具體的存儲引擎綁定死。報表引擎沒有用商用的報表組件也刻意避開了復(fù)雜的報表設(shè)計器而是自己封裝了一套基于DataGridView二次開發(fā)的網(wǎng)格渲染控件同時結(jié)合輕量級的模板定義文件來描述報表布局。這樣做的原因很簡單工控報表的結(jié)構(gòu)相對規(guī)整行數(shù)、列數(shù)、合并需求遠(yuǎn)遠(yuǎn)沒有財務(wù)系統(tǒng)的復(fù)雜報表那么夸張自研控件的靈活性反而更高想怎么改都是自己說了算。導(dǎo)出方案用了開源庫NPOI專門處理Excel文件讀寫。選擇NPOI而不是讓客戶端依賴Office COM組件是為了避免現(xiàn)場機(jī)器沒裝Office時程序直接崩掉。NPOI純托管代碼不依賴Office環(huán)境導(dǎo)出大文件也夠穩(wěn)定。這套組合看起來不fancy但它有一個很關(guān)鍵的優(yōu)勢——每一層我都能控制出了問題能自己動手解決。設(shè)備和組態(tài)、組態(tài)和報表數(shù)據(jù)庫之間的數(shù)據(jù)鏈路全部掌握在手里甲方提需求的時候我們改的是自己的代碼而不是想方設(shè)法去遷就組態(tài)軟件的限制。2. 系統(tǒng)架構(gòu)四層數(shù)據(jù)鏈路與核心抽象2.1 數(shù)據(jù)采集層先把數(shù)據(jù)從組態(tài)軟件手里拿回來傳統(tǒng)思路是讓組態(tài)軟件既做采集又做存儲又做展示什么都干。自研報表系統(tǒng)要做的第一件事就是打破這種大一統(tǒng)架構(gòu)把數(shù)據(jù)從組態(tài)軟件手里解放出來。我在項(xiàng)目里的做法是在采集層面增加一個獨(dú)立的采集網(wǎng)關(guān)通過OPC UA或者M(jìn)odbus TCP協(xié)議直接讀設(shè)備點(diǎn)位設(shè)備點(diǎn)位、采集周期、報警規(guī)則都配置在網(wǎng)關(guān)側(cè)。數(shù)據(jù)采集到之后網(wǎng)關(guān)同時做兩件事一份實(shí)時推送給組態(tài)軟件做畫面監(jiān)控一份寫入歷史數(shù)據(jù)庫供報表系統(tǒng)使用。這樣做的好處非常明顯組態(tài)軟件哪怕崩潰了、重啟了報表系統(tǒng)照樣能從歷史庫里取數(shù)互不牽連。關(guān)于采集網(wǎng)關(guān)和歷史庫的數(shù)據(jù)交互我踩過一個坑一開始直接用組態(tài)軟件的ODBC接口往數(shù)據(jù)庫寫歷史數(shù)據(jù)導(dǎo)致組態(tài)和報表強(qiáng)綁定組態(tài)有故障數(shù)據(jù)就不完整了。后來改成獨(dú)立網(wǎng)關(guān)直接采集、獨(dú)立鏈路寫庫報表數(shù)據(jù)才真正做到獨(dú)立可靠?,F(xiàn)場遇到通信斷線的情況網(wǎng)關(guān)側(cè)還做了本地緩存和斷點(diǎn)續(xù)傳歷史數(shù)據(jù)一條都不會缺。2.2 存儲層歷史歸檔與統(tǒng)計查詢的分工協(xié)作存儲層的核心準(zhǔn)則是不要把實(shí)時明細(xì)數(shù)據(jù)和統(tǒng)計報表數(shù)據(jù)混在一個表里硬扛。實(shí)時歷史數(shù)據(jù)的特點(diǎn)是寫入頻繁、單條數(shù)據(jù)量小、量大查詢時往往需要做時間范圍的聚合而報表數(shù)據(jù)的特點(diǎn)是查詢頻率高、要求響應(yīng)快、格式相對固定。我采用的方案是雙區(qū)存儲明細(xì)區(qū)放原始采樣數(shù)據(jù)表結(jié)構(gòu)非常精簡一般只有時間戳、點(diǎn)位ID、數(shù)值、質(zhì)量戳幾個字段統(tǒng)計區(qū)放定時任務(wù)預(yù)計算出來的日/周/月匯總數(shù)據(jù)比如設(shè)備日運(yùn)行時長、日均溫度、累計流量等。報表查詢時優(yōu)先走統(tǒng)計區(qū)只有做明細(xì)穿透時才會去查原始數(shù)據(jù)區(qū)。對于存儲引擎的選擇我整理了一個對比思路單機(jī)小項(xiàng)目用SQLite起步零配置、復(fù)制即用有并發(fā)寫入需求、又希望和現(xiàn)有業(yè)務(wù)系統(tǒng)共享數(shù)據(jù)時切到MySQL如果單日采集點(diǎn)位數(shù)超過幾百萬條而且需要長時間保留就要考慮時序數(shù)據(jù)庫。下面這個表是我在不同項(xiàng)目里總結(jié)出來的選型參考場景推薦存儲原因單機(jī)部署、點(diǎn)數(shù)少于500、保留時間短SQLite免安裝、備份簡單、性能足夠多客戶端并發(fā)訪問、需要對接業(yè)務(wù)系統(tǒng)MySQL并發(fā)好、生態(tài)成熟、權(quán)限方便海量采樣點(diǎn)、秒級采集、長期歸檔時序數(shù)據(jù)庫壓縮率高、聚合查詢快不過無論選哪種報表系統(tǒng)的查詢邏輯都要封裝在數(shù)據(jù)訪問層里不要直接散落SQL。這樣才能做到存儲層隨時可替換不影響上層的報表模板。2.3 報表服務(wù)層時間軸是核心抽象報表系統(tǒng)的服務(wù)層說白了就是一件核心的事把業(yè)務(wù)人員想要的指標(biāo)以正確的時間維度算出來。工控報表跟普通管理報表最大的差異在于所有數(shù)據(jù)都是依附時間軸存在的。沒有時間概念的報表是沒有意義的。所以我做了一件事把時間軸抽象成了一個獨(dú)立的維度對象它不只是一個開始時間和結(jié)束時間而是一套可以按規(guī)則拆分的區(qū)間集合。比如按自然日拆分按小時拆分按班次拆分按周拆分甚至按自定義日期范圍拆分。每個時間區(qū)間配合聚合函數(shù)平均值、累計值、最大值、最小值、首末值、計數(shù)等就可以生成一組行列數(shù)據(jù)。這塊抽象是自研報表系統(tǒng)超越組態(tài)軟件的關(guān)鍵。組態(tài)軟件做報表的時候時間窗口是死的要么只能選某個固定時間段要么只能按固定的日歷周期出數(shù)。而自研系統(tǒng)把時間區(qū)間拆成了可配置的模板參數(shù)你可以在報表模板里定義行表頭是設(shè)備列表頭是班次單元格數(shù)據(jù)是班次內(nèi)的累計流量這樣一張班組能耗表就出來了。再換一個模板行表頭是日期列表頭是設(shè)備單元格是日均溫度一張溫度趨勢表又出來了。配置化設(shè)計靈活性完全掌握在自己手里。2.4 客戶端展示層自研網(wǎng)格控件和圖表聯(lián)動展示層需要在用戶界面上同時呈現(xiàn)表格數(shù)據(jù)和圖表趨勢。表格部分我沒有直接拿原生的DataGridView裸用而是做了一層封裝實(shí)現(xiàn)了幾個關(guān)鍵能力按行按列合并單元格、自定義表頭多層結(jié)構(gòu)、單元格背景色按數(shù)值范圍自動著色、行列凍結(jié)。圖表部分選擇了輕量級的開源圖表庫按時間序列顯示趨勢曲線。更重要的是圖表的查詢參數(shù)和表格共用同一個查詢上下文。用戶在界面上選定時間段、勾選設(shè)備后表格和曲線同時刷新。點(diǎn)擊表格的某一行曲線圖上自動高亮對應(yīng)時段洞察異常點(diǎn)的效率明顯提升。這些交互體驗(yàn)組態(tài)軟件自帶的報表模塊基本做不出來恰恰也是甲方看到實(shí)物后最認(rèn)可的部分。3. 報表生成核心機(jī)制模板化與動態(tài)查詢3.1 模板-區(qū)塊-字段三層模型自研通用報表系統(tǒng)的一個核心設(shè)計思路就是讓報表的定義和代碼完全分離。我把一張報表拆分成了三個層次模板、區(qū)塊、字段。模板是報表的頂層容器定義了報表的名稱、紙張大小、頁邊距、數(shù)據(jù)源連接信息、關(guān)聯(lián)的時間軸規(guī)則。一個模板對應(yīng)一個表格式的XML或JSON配置文件新增一張報表本質(zhì)上就是新增一個配置文件而不需要改代碼。區(qū)塊是模板內(nèi)部的組成單元。一張報表可以包含多個區(qū)塊常見的有表格區(qū)塊、圖表區(qū)塊、文本區(qū)塊和簽名區(qū)塊。比如一張日常生產(chǎn)報表最上方是文本區(qū)塊顯示報表名稱、生成日期、制表人中間是表格區(qū)塊列出各設(shè)備的運(yùn)行參數(shù)下方可以是圖表區(qū)塊展示趨勢曲線最后是簽名區(qū)塊給現(xiàn)場人員簽字用。區(qū)塊之間可以縱向排列也可以左右排列。字段是最細(xì)粒度的綁定單元。表格區(qū)塊的每一行、每一列、每個單元格都綁定到具體的查詢結(jié)果字段或者聚合函數(shù)。例如“設(shè)備名稱”綁定點(diǎn)位名稱字段“班次累計流量”綁定流量點(diǎn)位的SUM聚合結(jié)果“運(yùn)行時長”綁定設(shè)備運(yùn)行狀態(tài)的計時字段。這里的字段綁定完全通過配置完成業(yè)務(wù)人員經(jīng)過簡單培訓(xùn)后也能自己拖一個字段出來看數(shù)據(jù)。3.2 動態(tài)查詢參數(shù)化拼接別讓SQL裸奔報表系統(tǒng)的核心能力在于查詢不是固定的同一個模板換了參數(shù)就能出完全不同的報表。為了實(shí)現(xiàn)這一點(diǎn)我把查詢語句做了參數(shù)化處理。模板里定義的不是寫死的SQL而是一組查詢片段每個片段對應(yīng)一個查詢語義系統(tǒng)在運(yùn)行時根據(jù)報表參數(shù)將片段拼接成完整的查詢命令。比如能耗報表的模板里定義了一個查詢片段“{時間區(qū)間} 內(nèi)設(shè)備 {設(shè)備列表} 的 {點(diǎn)位列表} 按 {聚合粒度} 計算 {聚合函數(shù)}”。點(diǎn)擊查詢按鈕之后系統(tǒng)把時間段、設(shè)備清單、點(diǎn)位清單、聚合粒度、聚合函數(shù)這些參數(shù)替換進(jìn)去生成一條帶參數(shù)化占位符的SQL再交給數(shù)據(jù)庫執(zhí)行。這里有個很重要的經(jīng)驗(yàn)動態(tài)拼接SQL時無論模板怎么動態(tài)用戶輸入條件必須用參數(shù)化方式傳值絕不能把界面輸入的文本直接拼接進(jìn)SQL字符串。有一次我為了省事直接把前端傳入的日期范圍格式化后拼進(jìn)SQL結(jié)果有人故意傳了一段特殊字符直接把整張報表的表結(jié)構(gòu)暴露出來了。從那以后所有前端傳參一律走參數(shù)化寧可多寫幾行代碼也不能留這種安全漏洞。支持的聚合函數(shù)也要封裝成一個白名單比如AVG、SUM、MIN、MAX、COUNT、FIRST、LAST和RATE。RATE這個函數(shù)是自定義的用來計算變化率比如溫度從T1時刻到T2時刻的升溫速率這在工藝分析報表里非常常用。這樣就算現(xiàn)場提出一些特殊統(tǒng)計口徑也不需要改代碼只要在函數(shù)庫新增一個函數(shù)即可。3.3 數(shù)據(jù)對齊與插值解決采樣周期不一致的難題做過工控項(xiàng)目的人都知道設(shè)備采樣周期往往五花八門。有些溫度點(diǎn)1秒采一次有些壓力點(diǎn)2秒采一次有些能耗表可能是5分鐘跳一次字。要做多點(diǎn)的聯(lián)合報表第一個難題就是數(shù)據(jù)對齊。如果不做任何處理直接按原始時間戳拼接出來的報表就是一堆稀疏數(shù)據(jù)根本無法分析。我在系統(tǒng)里內(nèi)置了三種對齊策略根據(jù)場景自動選擇。精確對齊策略適用于所有點(diǎn)位采樣周期一致的情況直接按時間戳取同一時刻的記錄。這種最簡單但現(xiàn)場很少見。前值填充策略適用于慢變點(diǎn)位比如液位、溫度。如果某個時刻點(diǎn)沒有采樣記錄就沿用最近一條數(shù)據(jù)直到出現(xiàn)新數(shù)據(jù)。用生活類比這就像等公交車你看了一眼站牌車還沒來等了兩分鐘車還是沒來這段時間的站牌信息就等同于剛才那一眼看到的信息。線性插值策略適用于快速變化的物理量比如流量、壓力波動。如果某個點(diǎn)位缺少中間采樣點(diǎn)就按照前后兩點(diǎn)做線性插值補(bǔ)齊。這種策略在繪制趨勢曲線時最常用曲線更平滑也更能反映真實(shí)過程。比如說一個流量點(diǎn)在10:00:00記錄為10立方米/小時在10:00:06記錄為16立方米/小時系統(tǒng)要把10:00:03的數(shù)值補(bǔ)出來就按線性關(guān)系算出來約13立方米/小時。三種策略的選擇不是靠感覺而是可以在點(diǎn)位配置里單獨(dú)指定。報表引擎會先檢查點(diǎn)位配置的對齊策略再決定對缺失數(shù)據(jù)采取哪種方式處理。3.4 快照、累計量與越限時長的計算細(xì)節(jié)工控報表和一般業(yè)務(wù)報表還有一個重要的不同很多數(shù)據(jù)不是普通的數(shù)值而是有特殊語義的量。比如液位計當(dāng)前值這是一個快照量報表里展示的應(yīng)該是某個時刻點(diǎn)的瞬時值電表讀數(shù)、流量計累計值這是累計量報表里需要展示的是兩個時刻之間的差值還有設(shè)備運(yùn)行狀態(tài)這是一個布爾量報表里往往需要統(tǒng)計的是異常持續(xù)時間。快照量的處理比較簡單就是取時間區(qū)間內(nèi)的最后一個有效值。但要特別注意質(zhì)量戳如果最后一條數(shù)據(jù)質(zhì)量標(biāo)志為壞值系統(tǒng)要自動追溯往前找最近的有效值否則報表會莫名其妙出現(xiàn)一個巨大的異常值。累計量的處理要復(fù)雜得多。累計量讀數(shù)一般是單調(diào)遞增的但現(xiàn)場經(jīng)常會發(fā)生換表、清零、撥碼溢位的情況。處理邏輯是先把原始讀數(shù)轉(zhuǎn)成差分序列同時檢測序列中的非正常回退如果讀數(shù)相比上一條變小了說明可能換表或者溢出了此時不是把差值算成負(fù)數(shù)而是根據(jù)量程范圍做正向補(bǔ)償。舉個例子一塊電表量程0到10000度本次讀數(shù)800上次讀數(shù)9800正確的消耗值應(yīng)該是(10000 - 9800) 800 1000度而不是等于負(fù)的9000度。類似的邊界處理如果不提前做報表出來的累計量會完全沒法看。越限時長的統(tǒng)計也比較巧妙。比如要統(tǒng)計一個溫度點(diǎn)在一天內(nèi)超過80℃的累計時長系統(tǒng)會把溫度采樣序列按照時間區(qū)間切片然后數(shù)一下哪些區(qū)間段的數(shù)值超過了閾值把這些區(qū)間的時長加總。這里要區(qū)分瞬時越限和持續(xù)越限持續(xù)越限一般要求連續(xù)超過某個時間后才開始計時比如持續(xù)超過5分鐘才算報警。這個規(guī)則也可以在模板里配置非常靈活。4. 跨系統(tǒng)接口與部署形態(tài)4.1 三種對接方式讓報表系統(tǒng)融入既有的系統(tǒng)環(huán)境自研報表系統(tǒng)最大的好處之一就是沒有歷史包袱對外接口可以按需設(shè)計。我在實(shí)際項(xiàng)目里總結(jié)了三種常見的對接方式。數(shù)據(jù)庫直連是最直接的方式也是最常用的方式。報表系統(tǒng)直接訪問業(yè)務(wù)系統(tǒng)的數(shù)據(jù)庫表比如MES的工單表、ERP的物料表、質(zhì)檢系統(tǒng)的檢驗(yàn)記錄表。這種方式實(shí)現(xiàn)簡單查詢性能也好適合系統(tǒng)之間網(wǎng)絡(luò)互通、數(shù)據(jù)庫權(quán)限可控的場景。我通常會單獨(dú)建立一個只讀賬號給報表系統(tǒng)使用從權(quán)限上約束住避免誤操作影響業(yè)務(wù)庫。OPC UA Server方式適用于需要從設(shè)備層實(shí)時取數(shù)的場景。我們的報表系統(tǒng)內(nèi)置了一個輕量級的OPC UA客戶端可以直接訂閱現(xiàn)場設(shè)備的實(shí)時點(diǎn)位用于生成實(shí)時快照報表或者觸發(fā)采集。這種方式不依賴組態(tài)軟件即使組態(tài)軟件停機(jī)維護(hù)報表系統(tǒng)仍然能從設(shè)備端獨(dú)立取得數(shù)據(jù)。REST API方式適用于跨部門、跨網(wǎng)絡(luò)的集成。我會把報表系統(tǒng)的一些核心查詢能力封裝成HTTP接口比如“按時間范圍查詢某設(shè)備的匯總數(shù)據(jù)”、“按工單號查詢生產(chǎn)記錄”。這樣其他系統(tǒng)通過簡單的HTTP請求就能拿到結(jié)構(gòu)化的JSON數(shù)據(jù)而不需要直接訪問數(shù)據(jù)庫。第三種方式在公司內(nèi)部的綜合看板項(xiàng)目里非常好用大屏展示系統(tǒng)只需要定時調(diào)用接口刷新數(shù)據(jù)完全不關(guān)心底層數(shù)據(jù)存哪里。4.2 部署形態(tài)免安裝綠色版和三級權(quán)限工控現(xiàn)場最怕的是什么是需要裝一堆運(yùn)行庫、框架還得配環(huán)境變量。為了最大限度減少現(xiàn)場部署的阻力我采用了綠色免安裝的部署方案編譯好的程序、配置文件、數(shù)據(jù)庫文件放在同一個目錄下拷貝到任意一臺工控機(jī)上修改一下數(shù)據(jù)庫連接串雙擊exe就能運(yùn)行。升級的時候直接替換exe和依賴文件數(shù)據(jù)庫結(jié)構(gòu)不變十幾秒搞定完全不影響正在運(yùn)行的生產(chǎn)。系統(tǒng)內(nèi)置了三類角色權(quán)限。查看角色只能瀏覽報表數(shù)據(jù)不能導(dǎo)出、不能修改模板操作角色可以導(dǎo)出報表、調(diào)整查詢參數(shù)但不能修改模板結(jié)構(gòu)管理員角色擁有全部權(quán)限包括模板維護(hù)、點(diǎn)位配置、用戶管理。權(quán)限體系直接保存在數(shù)據(jù)庫里登錄時校驗(yàn)越權(quán)操作會被拒絕。這里有一個小細(xì)節(jié)值得分享權(quán)限控制不只在前端隱藏按鈕更要在后端服務(wù)層做二次校驗(yàn)。前端界面可以騙人但每個報表查詢、導(dǎo)出請求到了服務(wù)端都會再次校驗(yàn)當(dāng)前用戶的角色這樣就算有人繞過了界面直接發(fā)請求也拿不到他沒權(quán)限的數(shù)據(jù)。4.3 定時任務(wù)讓報表自己跑起來如果把報表系統(tǒng)做成一個純工具軟件那些每天都要生成的日報、周報、月報就會變成人工負(fù)擔(dān)。所以我把定時任務(wù)功能也內(nèi)置到了系統(tǒng)里。定時任務(wù)模塊的核心是一個調(diào)度器支持每天、每小時、每周固定時間觸發(fā)報表生成。觸發(fā)之后系統(tǒng)讀取模板配置、執(zhí)行查詢、渲染報表、導(dǎo)出文件最后按設(shè)定的分發(fā)方式處理結(jié)果。分發(fā)方式有三種保存到本地目錄、發(fā)送郵件給指定列表、通過FTP上傳到服務(wù)器。實(shí)際應(yīng)用中很多項(xiàng)目都要求報表在每天早上7點(diǎn)前自動生成并推送給管理層。有了定時任務(wù)系統(tǒng)在前一天夜里自動執(zhí)行查詢和導(dǎo)出早上6點(diǎn)55分生成文件7點(diǎn)整郵件推送全程不需要人工介入。配置界面上可以快速看到所有定時任務(wù)的最近執(zhí)行狀態(tài)、失敗原因便于排查。如果定時任務(wù)執(zhí)行失敗系統(tǒng)會自動重試兩次仍然失敗就發(fā)告警通知。這塊要特別強(qiáng)調(diào)告警通知必須獨(dú)立于報表系統(tǒng)本身郵件接口掛了不會導(dǎo)致告警發(fā)不出去我一般直接把告警也接入到現(xiàn)場的聲光報警系統(tǒng)里故障出現(xiàn)了能第一時間被值班人員發(fā)現(xiàn)。5. 實(shí)施中的踩坑記錄與排查速查5.1 半小時聚合數(shù)值對不上的謎案項(xiàng)目上線不久甲方反映一個很奇怪的問題某設(shè)備的半小時電耗報表上午的數(shù)據(jù)是對的下午的偶爾對不上同一時間段的匯總有時多有時少完全沒有規(guī)律。我排查了很久才定位到問題根子出在時間戳的邊界處理上。采集網(wǎng)關(guān)在跨整點(diǎn)的時候會出現(xiàn)時間戳不穩(wěn)定的情況比如本應(yīng)在14:00:00整點(diǎn)寫入的記錄實(shí)際時間戳變成了前一天23:59:59或者是14:00:00.000但用了本地時區(qū)偏移計算后被劃分到了上一個半小時區(qū)間里。修復(fù)方式并不復(fù)雜但必須做兩件事一是聚合查詢時所有時間邊界的比較統(tǒng)一使用數(shù)據(jù)時間戳的整點(diǎn)對齊邏輯不允許依賴數(shù)據(jù)庫的時區(qū)隱式轉(zhuǎn)換二是對邊緣時間戳做哨兵處理比如統(tǒng)計14:00到14:30這個區(qū)間查詢條件寫成時間戳大于等于14:00:00且小于14:30:00而不是簡單截斷字符串。這個坑給了我一個深刻教訓(xùn)在做組態(tài)監(jiān)控時時間偏差一秒可能無所謂但在報表統(tǒng)計里邊界錯一條記錄整段數(shù)據(jù)的匯總就失真了。時間處理必須處處小心。5.2 導(dǎo)出大報表把界面卡死的尷尬時刻有一回甲方導(dǎo)一張全月所有設(shè)備的分鐘級數(shù)據(jù)匯總表表里有幾萬行數(shù)據(jù)客戶端界面直接卡死。原因很簡單我在UI線程里同步執(zhí)行了查詢和Excel生成的邏輯數(shù)據(jù)一多界面就不響應(yīng)了。后來我把整個報表生成流程改成了異步模式用戶點(diǎn)擊導(dǎo)出后界面立即切換到進(jìn)度條頁面后臺線程負(fù)責(zé)查詢、聚合、渲染、導(dǎo)出完成后彈窗提示文件保存路徑。同時數(shù)據(jù)查詢也做了分批處理不再一次性把幾萬行全部load進(jìn)內(nèi)存而是每次查詢5000行邊查邊處理邊處理邊釋放內(nèi)存占用大大降低。如果是導(dǎo)出ExcelNPOI在生成大數(shù)據(jù)量文件時也要注意按Sheet分批寫入不能一次性構(gòu)造完所有單元格再保存。我寫了一個通用的分批寫入方法實(shí)測下來20萬行數(shù)據(jù)的導(dǎo)出可以穩(wěn)定跑完內(nèi)存占用控制在500MB以內(nèi)。5.3 流量計累計量回退是怎么被搞定的某次做水流量統(tǒng)計報表甲方說周六的用水量數(shù)據(jù)明顯異常算出來是負(fù)數(shù)。我查了原始數(shù)據(jù)發(fā)現(xiàn)現(xiàn)場那塊流量計在周六早上由于通信故障被運(yùn)維人員更換了新表累計值從0開始累計舊表累計值停在了原來的數(shù)字上。數(shù)據(jù)本身沒有錯但差分算法把它當(dāng)成了一次負(fù)值。這個case促使我在累計量處理模塊里加了異?;赝藱z測當(dāng)差分序列出現(xiàn)負(fù)值時自動啟動換表監(jiān)測邏輯如果負(fù)值的絕對值接近上一時刻的讀數(shù)基本判定是發(fā)生了換表清零此時把這一段數(shù)據(jù)的差值按新的累計起點(diǎn)計算而不是按負(fù)值處理。后來這塊邏輯還擴(kuò)展成了可以配置的換表事件表運(yùn)維人員在界面上登記一次換表事件報表系統(tǒng)自動把換表前后的數(shù)據(jù)銜接正確財務(wù)報表再沒出現(xiàn)過這種問題。5.4 班次報表和交接班時間對不上怎么辦甲方要求按早班、中班、夜班出報表早班是08:00到16:00中班是16:00到00:00夜班是00:00到08:00這個需求在組態(tài)軟件里非常難做因?yàn)榻M態(tài)的時間軸只認(rèn)自然日。自研系統(tǒng)有了自定義時間軸支持實(shí)現(xiàn)起來一陣操作就搞定。我先在系統(tǒng)里建了一張班次定義表字段包括班次編號、開始時間、結(jié)束時間、班次名稱。查詢時報模板指定按班次時間軸切分系統(tǒng)先根據(jù)起始日期生成該日期的三個班次區(qū)間再對每個區(qū)間執(zhí)行聚合查詢最后把三個班次的數(shù)據(jù)分行展示。真正崩潰的是遇到跨月班次。比如6月30日的夜班是00:00到08:00但這個夜班其實(shí)屬于6月29日的晚班。如果不注意月底報表會把6月30日凌晨的數(shù)據(jù)算到6月30日的夜班里而按考勤口徑它應(yīng)該歸屬6月29日的夜班。處理方案是班次區(qū)間查詢時統(tǒng)一按照班次開始時刻所在日期來歸屬統(tǒng)計。有了這個規(guī)則月底數(shù)據(jù)才對得上。這個問題排查起來特別隱蔽不仔細(xì)看只會覺得數(shù)據(jù)差一點(diǎn)點(diǎn)但對不上就是口徑問題現(xiàn)場扯皮很頭疼。建議每個自研報表系統(tǒng)都提前把班次歸屬規(guī)則定義清楚別等到月底對賬了再抓瞎。5.5 實(shí)用問題排查速查表現(xiàn)象可能原因處理方案報表數(shù)據(jù)比儀表實(shí)際值偏大時間戳跨整點(diǎn)重復(fù)計數(shù)檢查時間邊界比較邏輯是否統(tǒng)一為左閉右開累計量出現(xiàn)負(fù)數(shù)換表、清零、溢位啟用累計量差分異常檢測和換表補(bǔ)償多設(shè)備報表同一時刻點(diǎn)數(shù)不一致采樣周期不同配置點(diǎn)位對齊策略前值填充或線性插值導(dǎo)出大文件時內(nèi)存暴漲一次加載全量數(shù)據(jù)分批查詢、分批寫Excel、后臺異步執(zhí)行報表偶爾出現(xiàn)巨大瞬時值質(zhì)量戳為壞值快照查詢增加質(zhì)量戳過濾和回退查找班次報表月底對上不賬跨月班次歸屬錯誤按班次開始時刻歸屬統(tǒng)計日期一個報表模板改完其他報表也被影響模板間共用了一個配置對象配置保存為值拷貝每次點(diǎn)擊生成獨(dú)立實(shí)例上面這些坑大部分是我在不同項(xiàng)目現(xiàn)場踩完之后才總結(jié)出來的。實(shí)際上報表系統(tǒng)做到后面最大的收獲不是那幾個功能而是掌握了一套面對各種數(shù)據(jù)場景都能兜底的思路。每次現(xiàn)場人員提需求我的第一反應(yīng)不再是“組態(tài)里能不能做”而是“這個數(shù)據(jù)從哪來、怎么對齊、怎么聚合成一張表”。如果你也在做類似的系統(tǒng)建議你也把這些問題沉淀成基礎(chǔ)工具和檢查清單日后再接手新項(xiàng)目會發(fā)現(xiàn)這套獨(dú)立報表系統(tǒng)真正能幫你省下大量重復(fù)勞動。