Jelajahi Sumber

总图调试

liujiejie 3 hari lalu
induk
melakukan
c20be556ae

TEMPAT SAMPAH
downLoadServer/aa.zip


+ 41 - 0
downLoadServer/scripts/smoke-anomaly-report.mjs

@@ -22,6 +22,7 @@ import {
   summarizeTurbineAnomalyRate,
   buildAnomalySummaryRows,
   buildConclusionRows,
+  formatSensorTypeText,
 } from "../src/server/reportService/anomalyReportMapper.js";
 import {
   DETECTOR_TEMPLATE_CONFIG,
@@ -331,6 +332,10 @@ if (fs.existsSync(templateSource)) {
     "表10-1 检测结论应改为 conclusionRows 循环,表4-1 应闭合 detectorSummaryRows",
   );
   assert(
+    (patchedXml.match(/\{sensor_anomaly_type\}/g) || []).length >= 16,
+    "各检测器明细表应保留数据感知异常类型列",
+  );
+  assert(
     patchedXml.includes("{#show_module_wind}") &&
       patchedXml.includes("{/show_module_aero}"),
     "各检测模块章节应包一层 show_module 条件标签",
@@ -2614,6 +2619,18 @@ assert(
     majorTempRows[0].ratio === "30.00%",
   "数据感知表应支持大部件温度异常行",
 );
+assert(
+  formatSensorTypeText({ sensorAnomalyType: "9" }) === "大部件温度异常",
+  "数据感知类型 9 应翻译为大部件温度异常",
+);
+assert(
+  formatSensorTypeText({ sensorAnomalyType: "1,5" }) === "功率异常、扭矩异常",
+  "多个数据感知类型编码应按顿号拼接",
+);
+assert(
+  formatSensorTypeText({}) === "暂无异常",
+  "无数据感知异常时应写暂无异常",
+);
 
 const sensorHeat = buildSensorHeatmapData([
   {
@@ -3004,6 +3021,8 @@ const renderData = {
       engineName: "16",
       sensorAnomalyWind: 1,
       sensorAnomalyWindRatio: 0.2,
+      sensorAnomalyMajorTemp: 1,
+      sensorAnomalyMajorTempRatio: 0.3,
     },
   ]),
   detectorSummaryRows: [
@@ -3098,6 +3117,7 @@ DETECTOR_TEMPLATE_CONFIG.forEach((cfg) => {
             turbine_name: "111",
             anomaly_points: "3",
             anomaly_rate: "12.00%",
+            sensor_anomaly_type: "大部件温度异常",
             comment: "功能诊断异常",
           },
         ],
@@ -3140,4 +3160,25 @@ Object.keys(smokeZip.files)
     assert(!plain, `${name} 页脚应为空,实际为「${plain}」`);
   });
 
+const smokeDocText = smokeZip
+  .file("word/document.xml")
+  .asText()
+  .replace(/<[^>]+>/g, "");
+assert(
+  smokeDocText.includes("数据感知异常类型"),
+  "报告中检测器明细表应包含数据感知异常类型列",
+);
+assert(
+  smokeDocText.includes("大部件温度异常"),
+  "报告应输出大部件温度异常",
+);
+assert(
+  smokeDocText.includes("大部件温度(齿轮箱油温、发电机轴承"),
+  "表3-1 应包含大部件温度关联测点说明",
+);
+assert(
+  !/\{[A-Za-z_]/.test(smokeDocText),
+  "报告中不应残留模板占位符",
+);
+
 await shutdownChartService();

+ 38 - 10
downLoadServer/src/server/reportService/anomalyChartBuilder.js

@@ -1280,8 +1280,10 @@ function buildOptionFromPanel(panel, title, poKey, extra = {}) {
   } else if (opStateAdaptive) {
     if (!useTime) xAxis.scale = true;
   } else if (Array.isArray(panel.xrange) && panel.xrange.length >= 2) {
-    xAxis.min = useTime ? toAxisValue(panel.xrange[0], true) : panel.xrange[0];
-    xAxis.max = useTime ? toAxisValue(panel.xrange[1], true) : panel.xrange[1];
+    const xrange = normalizePanelXRange(panel.xrange, useTime);
+    if (xrange) {
+      [xAxis.min, xAxis.max] = xrange;
+    }
   }
   if (pitchCoord) {
     yRangeLocked = lockAxisToRange(yAxis, panel.yrange);
@@ -1289,14 +1291,10 @@ function buildOptionFromPanel(panel, title, poKey, extra = {}) {
   } else if (opStateAdaptive) {
     yAxis.scale = true;
   } else if (Array.isArray(panel.yrange) && panel.yrange.length >= 2) {
-    const yMin = Number(panel.yrange[0]);
-    const yMax = Number(panel.yrange[1]);
-    yAxis.min = Number.isFinite(yMin)
-      ? Number(yMin.toFixed(2))
-      : panel.yrange[0];
-    yAxis.max = Number.isFinite(yMax)
-      ? Number(yMax.toFixed(2))
-      : panel.yrange[1];
+    const yrange = normalizePanelYRange(panel.yrange);
+    if (yrange) {
+      [yAxis.min, yAxis.max] = yrange;
+    }
   }
   if (farmMode && templateKey === "aero_cp" && !useTime) {
     const extent = seriesAxisDataMin(series);
@@ -1398,6 +1396,36 @@ function isPlottableTimeX(x) {
   return year >= 2000 && year <= 2100;
 }
 
+/**
+ * 单机 JSON 的 xrange 有时不是「全场可用」的墙钟时间范围
+ * (秒级检测器会出现 [0,1440]、[null,null] 这类值)。
+ * 直接把这类范围锁到全场总图上会把所有点裁掉,画出来只有标题和图例。
+ * 这里只在能构成合法范围时才返回,否则交给坐标轴自适应。
+ */
+function normalizePanelXRange(range, useTime) {
+  if (!Array.isArray(range) || range.length < 2) return null;
+  if (useTime) {
+    const min = toAxisValue(range[0], true);
+    const max = toAxisValue(range[1], true);
+    if (!isPlottableTimeX(min) || !isPlottableTimeX(max)) return null;
+    if (Number(min) >= Number(max)) return null;
+    return [min, max];
+  }
+  const min = Number(range[0]);
+  const max = Number(range[1]);
+  if (!Number.isFinite(min) || !Number.isFinite(max) || min >= max) return null;
+  return [min, max];
+}
+
+/** 纵轴范围必须是有限数值且 min < max,否则同样按自适应处理 */
+function normalizePanelYRange(range) {
+  if (!Array.isArray(range) || range.length < 2) return null;
+  const min = Number(range[0]);
+  const max = Number(range[1]);
+  if (!Number.isFinite(min) || !Number.isFinite(max) || min >= max) return null;
+  return [Number(min.toFixed(2)), Number(max.toFixed(2))];
+}
+
 function seriesPointPlots(point, seriesType, timeAxis) {
   if (seriesType === "heatmap") {
     const value = Array.isArray(point)

+ 3 - 1
downLoadServer/src/server/reportService/anomalyReportMapper.js

@@ -450,7 +450,8 @@ function parseDateParts(raw) {
   };
 }
 
-function formatSensorTypeText(item = {}) {
+/** PO 的 sensorAnomalyType 编码 / 各传感器 flag → 「数据感知异常类型」文案 */
+export function formatSensorTypeText(item = {}) {
   const fromFlags = MODEL_SENSOR_FIELDS.filter(
     (cfg) => Number(item[cfg.field]) > 0,
   ).map((cfg) => cfg.label);
@@ -953,6 +954,7 @@ export function emptyDetectorRowsPlaceholder() {
       turbine_name: "暂无",
       anomaly_points: "—",
       anomaly_rate: "—",
+      sensor_anomaly_type: "—",
       comment: "暂无",
     },
   ];

+ 59 - 21
downLoadServer/src/server/reportService/anomalyReportService.js

@@ -44,6 +44,7 @@ import {
   emptyKeyTurbinePlaceholder,
   formatPercent,
   formatDetectorRowMetric,
+  formatSensorTypeText,
   formatSummaryMetricValue,
   isPoAnomaly,
   mapAnomalyCoverFields,
@@ -308,11 +309,10 @@ function sortTurbineDetailImages(images) {
   });
 }
 
-const FARM_OVERVIEW_TEMPLATE_KEYS = new Set([
-  "ctrl_power_quality",
-  "pitch_regulation",
-]);
-
+/**
+ * 所有检测项的全场总图都优先读 getAnomalyOverview 里的算法预聚合总图,
+ * 与页面总图 Tab 用同一份数据。读不到、不是 JSON、或出图为空时,再回退单机拼接。
+ */
 async function loadOverviewFarmJson(overview, templateKey) {
   const source = collectFarmGraphPaths(overview)[templateKey];
   const parsed = parseFarmGraphSource(source);
@@ -320,7 +320,15 @@ async function loadOverviewFarmJson(overview, templateKey) {
   if (parsed.kind !== "url" || !parsed.url) return null;
   try {
     const media = await fetchRemoteMedia(parsed.url);
-    return media?.kind === "json" ? media.data : null;
+    if (media?.kind !== "json") {
+      console.warn(
+        "[anomaly-report] 全场总图非 JSON,回退单机拼接:",
+        templateKey,
+        media?.kind || "empty",
+      );
+      return null;
+    }
+    return media.data;
   } catch (error) {
     console.warn(
       "[anomaly-report] 全场总图读取失败:",
@@ -346,6 +354,12 @@ function createDetectorState() {
       rateCount: 0,
       turbineNames: [],
       hasChartData: false,
+      // 诊断计数:定位「总图为什么没出来」
+      poMissing: 0,
+      graphPathMissing: 0,
+      mediaImage: 0,
+      mediaFailed: 0,
+      jsonNoData: 0,
     };
   });
   return state;
@@ -456,6 +470,7 @@ export async function generateAnomalyReport(req) {
           if (!cfg.poKey) continue;
           const po = pickPo(moduleBundle, cfg.poKey);
           const state = detectorState[cfg.templateKey];
+          if (!po) state.poMissing += 1;
           const points = poAnomalyPoints(po);
           const rate = poAnomalyRate(po);
           const abnormal = isPoAnomaly(po) || points > 0;
@@ -472,6 +487,7 @@ export async function generateAnomalyReport(req) {
               turbine_name: engineName,
               anomaly_points: String(points),
               anomaly_rate: formatDetectorRowMetric(cfg.templateKey, rate),
+              sensor_anomaly_type: formatSensorTypeText(po),
               comment: buildDetectorComment(po, cfg.templateKey),
             });
             perTurbine.anomalyDetectorCount += 1;
@@ -485,13 +501,20 @@ export async function generateAnomalyReport(req) {
           }
 
           const graphPath = po?.graphPath;
-          if (!graphPath) continue;
+          if (!graphPath) {
+            state.graphPathMissing += 1;
+            continue;
+          }
           const media = await loadMedia(graphPath);
           const imageKey = `anomaly_${String(engineId).replace(/[^\w-]/g, "_")}_${
             cfg.templateKey
           }`;
+          if (!media) {
+            state.mediaFailed += 1;
+          }
           if (media?.kind === "image" && media.buffer?.length) {
             registerImageBuffer(imageBufferMap, imageKey, media.buffer);
+            state.mediaImage += 1;
             state.turbineImages.push({
               engineName,
               image: imageKey,
@@ -506,6 +529,7 @@ export async function generateAnomalyReport(req) {
             });
           } else if (media?.kind === "json") {
             if (!plotJsonHasData(media.data)) {
+              state.jsonNoData += 1;
               continue;
             }
             state.plotJsons.push({
@@ -582,21 +606,21 @@ export async function generateAnomalyReport(req) {
     if (!cfg.poKey) continue;
     const farmTitle = `全场${cfg.title}检测汇总`;
     let farmOptions = [];
+    let farmSource = "none";
     try {
-      if (FARM_OVERVIEW_TEMPLATE_KEYS.has(cfg.templateKey)) {
-        const farmJson = await loadOverviewFarmJson(overview, cfg.templateKey);
-        if (farmJson) {
-          farmOptions = listFarmOverviewChartOptions(
-            farmJson,
-            farmTitle,
-            cfg.poKey,
-            {
-              templateKey: cfg.templateKey,
-              farmMode: true,
-              nameMap: engineNameMap,
-            },
-          );
-        }
+      const farmJson = await loadOverviewFarmJson(overview, cfg.templateKey);
+      if (farmJson) {
+        farmOptions = listFarmOverviewChartOptions(
+          farmJson,
+          farmTitle,
+          cfg.poKey,
+          {
+            templateKey: cfg.templateKey,
+            farmMode: true,
+            nameMap: engineNameMap,
+          },
+        );
+        if (farmOptions.length) farmSource = "overview";
       }
       if (!farmOptions.length) {
         farmOptions = listFarmChartOptions(
@@ -609,6 +633,7 @@ export async function generateAnomalyReport(req) {
             nameMap: engineNameMap,
           },
         );
+        if (farmOptions.length) farmSource = "stitch";
       }
     } catch (error) {
       console.warn(
@@ -619,6 +644,19 @@ export async function generateAnomalyReport(req) {
     }
     farmOptions = farmOptions.filter((option) => optionHasData(option));
     if (farmOptions.length) state.hasChartData = true;
+    if (farmOptions.length) {
+      console.log(
+        `[anomaly-report] 全场总图: ${cfg.title} 源=${farmSource} 张数=${farmOptions.length}`,
+      );
+    } else {
+      console.warn(
+        `[anomaly-report] 全场总图为空: ${cfg.title}(${cfg.templateKey}) ` +
+          `单机JSON=${state.plotJsons.length} 缺po=${state.poMissing} ` +
+          `缺graphPath=${state.graphPathMissing} 下载失败=${state.mediaFailed} ` +
+          `图片型=${state.mediaImage} JSON无数据=${state.jsonNoData} ` +
+          `分图=${state.turbineImages.length}`,
+      );
+    }
     if (state.hasChartData && farmOptions.length) {
       try {
         for (let i = 0; i < farmOptions.length; i += 1) {

TEMPAT SAMPAH
downLoadServer/ss.zip


TEMPAT SAMPAH
downLoadServer/templates/_smoke_anomaly_report.docx


TEMPAT SAMPAH
downLoadServer/templates/_smoke_health_report.docx


TEMPAT SAMPAH
downLoadServer/templates/anomaly-report-template.docx


TEMPAT SAMPAH
downLoadServer/templates/health-report-template.docx


TEMPAT SAMPAH
downLoadServer/templates/异常检测数据分析报告模板(大唐版)_修订版_人工对齐版.docx