import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import { registerImageBuffer, renderDocxReport, } from "../src/server/reportService/docxReportBuilder.js"; import { DETECTOR_TEMPLATE_CONFIG, buildDetectorPlotOption, buildFarmPlotJsons, listDetectorPlotOptions, } from "../src/server/reportService/anomalyChartBuilder.js"; import { buildDetectorSectionPayload } from "../src/server/reportService/anomalyReportMapper.js"; import { renderEchartsOption } from "../src/server/reportService/echartsRenderer.js"; import { initChartService, shutdownChartService, } from "../src/server/utils/chartService/index.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const EMPTY_PNG = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mP8z8BQz0AEYBxVSF+FABJADveWkH6oAAAAAElFTkSuQmCC", "base64", ); function samplePlot(labelPrefix = "") { return { title: `${labelPrefix}偏航`, xaxis: "时间", yaxis: "偏移 (°)", data: [ { label: "正常偏航角", mode: "markers", timeData: [ "2026-07-14 04:00:00", "2026-07-14 08:00:00", "2026-07-14 12:00:00", ], yData: [180, 200, 190], }, { label: "12h均值", mode: "lines", timeData: [ "2026-07-14 04:00:00", "2026-07-14 08:00:00", "2026-07-14 12:00:00", ], yData: [185, 198, 120], }, ], }; } function assert(condition, message) { if (!condition) throw new Error(message); } const scatterFarm = buildFarmPlotJsons( [ { engineName: "A", plotJson: { xaxis: "风速", yaxis: "功率", data: [ { label: "散点", mode: "markers", xData: [1, 2], yData: [3, 4] }, { label: "上限", mode: "lines", xData: [1, 2], yData: [8, 9] }, { label: "下限", mode: "lines", xData: [1, 2], yData: [1, 2] }, { label: "参考功率曲线", mode: "lines", xData: [1, 2], yData: [5, 6] }, ], }, }, { engineName: "B", plotJson: { xaxis: "风速", yaxis: "功率", data: [ { label: "散点", mode: "markers", xData: [2, 3], yData: [4, 5] }, { label: "参考功率曲线", mode: "lines", xData: [1, 2], yData: [5, 6] }, ], }, }, ], "wind_power_scatter", "全场散点", ); assert(scatterFarm[0].data.filter((row) => row.__shared).length === 1, "参考功率曲线应只保留一条"); assert(!scatterFarm[0].data.some((row) => /上限|下限/.test(row.label)), "散点总图不应含上下限"); assert( scatterFarm[0].data.some((row) => row.label === "A") && scatterFarm[0].data.some((row) => row.label === "B"), "散点总图应按风机分系列", ); const farmScatterOption = buildDetectorPlotOption( scatterFarm[0], scatterFarm[0].title, "anomalyScatterPO", ); const farmScatterSeries = (farmScatterOption.series || []).filter( (item) => item.type === "scatter", ); assert(farmScatterSeries.length > 0, "全场散点应输出 scatter 系列"); assert( farmScatterSeries.every((item) => Number(item.symbolSize) >= 10), "全场散点 symbolSize 过小", ); assert( farmScatterSeries.every((item) => item.large === false), "报告散点不应开启 large 模式", ); const qualityPanels = listDetectorPlotOptions( { panels: [ { panelTitle: "功率因数 | 异常点: 0", xaxis: "时间", yaxis: "功率因数", data: [ { label: "功率因数", mode: "markers", timeData: ["2026-08-01 00:00:00", "2026-08-01 01:00:00"], yData: [0.98, 0.97], }, ], }, { panelTitle: "电流不平衡度", xaxis: "时间", yaxis: "电流不平衡度", data: [ { label: "电流不平衡度", mode: "markers", timeData: ["2026-08-01 00:00:00", "2026-08-01 01:00:00"], yData: [0.12, 0.15], }, ], }, ], }, "DT01 电能质量分析", "anomalyPowerqualityPO", ); assert(qualityPanels.length === 2, "电能质量单机图应按 panel 全部出图"); assert( qualityPanels[1].yAxis.name === "电流不平衡度", "第二张单机图应使用对应 panel 的 Y 轴", ); assert( (qualityPanels[1].series || []).some( (item) => Array.isArray(item.data) && item.data.length > 0, ), "单机图系列不能为空", ); const pitchFarm = buildFarmPlotJsons( [ { engineName: "101", plotJson: { panels: [ { panelTitle: "桨距角时序", xaxis: "时间", yaxis: "桨距角", data: [ { label: "桨叶 1", mode: "markers", timeData: ["2026-01-01 00:00"], yData: [1] }, { label: "桨叶 2", mode: "lines", timeData: ["2026-01-01 00:00"], yData: [2] }, { label: "桨叶 3", mode: "lines", timeData: ["2026-01-01 00:00"], yData: [3] }, { label: "阈值参考", mode: "lines", timeData: ["2026-01-01 00:00"], yData: [4] }, ], }, { panelTitle: "桨距角差", xaxis: "时间", yaxis: "差值", data: [ { label: "桨叶 1", mode: "markers", timeData: ["2026-01-01 00:00"], yData: [1] }, ], }, ], }, }, ], "pitch_regulation", "全场变桨一致性", ); assert(pitchFarm.length === 2, "变桨一致性总图应为 2 张"); assert( !pitchFarm[0].data.some((row) => /阈值/.test(row.originalLabel || "")), "变桨一致性不应展示阈值参考", ); const deloadFarm = buildFarmPlotJsons( [ { engineName: "101", plotJson: { panels: [ { panelTitle: "风速", xaxis: "时间", yaxis: "风速", data: [{ label: "风速", timeData: ["t"], yData: [1] }], }, { panelTitle: "有功功率时序", xaxis: "时间", yaxis: "有功功率", data: [ { label: "正常", mode: "markers", timeData: ["t"], yData: [1] }, { label: "异常", mode: "markers", timeData: ["t"], yData: [2] }, ], }, ], }, }, ], "ctrl_deload", "全场降载", ); assert(deloadFarm.length === 1, "降载总图只出有功功率时序"); assert(/有功功率/.test(deloadFarm[0].title + deloadFarm[0].yaxis), "降载总图应为有功功率"); const opFarm = buildFarmPlotJsons( [ { engineName: "101", plotJson: { panels: [ { panelTitle: "投影", xaxis: "x", yaxis: "y", data: [{ label: "簇", xData: [1], yData: [1] }], }, { panelTitle: "转速-功率散点", xaxis: "转速", yaxis: "功率", data: [{ label: "散点", xData: [1], yData: [2] }], }, ], }, }, ], "ctrl_op_state", "全场运行状态", ); assert(opFarm.length === 1, "运行状态总图只出一张"); assert(/转速/.test(opFarm[0].title + opFarm[0].xaxis), "运行状态总图应为转速-功率"); const pqFarm = buildFarmPlotJsons( [ { engineName: "101", plotJson: { panels: [ { panelTitle: "功率因数", xaxis: "x", yaxis: "y", data: [{ label: "点", xData: [1], yData: [1] }] }, { panelTitle: "不平衡度", xaxis: "x", yaxis: "y", data: [{ label: "点", xData: [1], yData: [1] }] }, { panelTitle: "频率", xaxis: "x", yaxis: "y", data: [{ label: "点", xData: [1], yData: [1] }] }, ], }, }, ], "ctrl_power_quality", "全场电能质量", ); assert(pqFarm.length === 3, "电能质量总图应为 3 张"); console.log("farm chart rules ok"); await initChartService(); const imageBufferMap = {}; registerImageBuffer(imageBufferMap, "anomaly_sensor_dist", EMPTY_PNG); const farmPlots = buildFarmPlotJsons( [ { engineName: "111", plotJson: samplePlot("111") }, { engineName: "112", plotJson: samplePlot("112") }, ], "yaw_static", "全场静态偏航分析检测汇总", ); const farmLabels = (farmPlots[0]?.data || []).map((row) => row.label); if (farmLabels.join(",") !== "111,112") { throw new Error(`全场偏航分色失败: ${farmLabels.join(",")}`); } if ((farmPlots[0]?.data || []).some((row) => /均值/.test(row.originalLabel || ""))) { throw new Error("全场偏航仍包含 2h/12h 均值"); } const farmBuf = await renderEchartsOption( buildDetectorPlotOption(farmPlots[0], farmPlots[0].title, "anomalyStaticyawPO"), { width: 820, height: 420 }, ); registerImageBuffer(imageBufferMap, "anomaly_farm_yaw_static", farmBuf); const turbineBuf = await renderEchartsOption( buildDetectorPlotOption(samplePlot("111"), "111 静态偏航分析"), { width: 760, height: 400 }, ); registerImageBuffer(imageBufferMap, "anomaly_111_yaw_static", turbineBuf); const denseCount = 4500; const denseTimes = Array.from({ length: denseCount }, (_, i) => { const t = new Date("2026-08-01T00:00:00").getTime() + i * 60000; const pad = (n) => String(n).padStart(2, "0"); const d = new Date(t); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad( d.getHours(), )}:${pad(d.getMinutes())}:00`; }); const denseOption = buildDetectorPlotOption( { title: "DT01 电能质量分析", xaxis: "时间", yaxis: "电流不平衡度", data: [ { label: "电流不平衡度", mode: "markers", timeData: denseTimes, yData: Array.from({ length: denseCount }, (_, i) => 0.08 + (i % 20) / 200), }, ], }, "DT01 电能质量分析", "anomalyPowerqualityPO", ); assert(denseOption.series[0].large === false, "密点单机图不应使用 large"); assert(Number(denseOption.series[0].symbolSize) >= 8, "密点单机图 symbolSize 过小"); const denseBuf = await renderEchartsOption(denseOption, { width: 760, height: 420 }); assert(denseBuf.length > 20000, "密点单机图截图过小,可能未画出数据"); const renderData = { reportNo: "AD-SMOKE-20260824", Province: "测试省", Wind_farm: "烟雾风场", Year_now: "2026", Month_now: "08", machineTypeCode: "WD", turbine_count: "2", anomaly_turbine_count: "1", total_anomaly_points: "3", anomaly_rate: "50.00", Overview_of_the_Wind_Farm: "烟雾风场用于模板渲染校验。", target_date: "2026-08-24", anomaly_module_count: "1", main_problem_modules: "偏航与扭缆", top_problem_description: "静态偏航分析异常点数最多(3)", sensorAnomalyRows: [ { turbine_name: "111", sensor_anomaly_type: "风速异常", anomaly_points: "1", ratio: "—", }, ], detectorSummaryRows: [ { detector_name: "静态偏航分析", module_name: "偏航与扭缆", data_granularity: "秒级", anomaly_turbines: "1", anomaly_points: "3", avg_anomaly_rate: "12.00%", }, ], anomalySummaryRows: [ { turbine_name: "111", anomaly_detector_count: "1", anomaly_points: "3", anomaly_rate: "12.00%", main_anomaly_type: "静态偏航分析", }, ], priorityList: "111", keyTurbineLoop: [ { turbine_name: "111", turbineDetailRows: [ { detector_name: "静态偏航分析", anomaly_points: "3", anomaly_rate: "12.00%", conclusion: "建议结合现场复核", }, ], "zn-techcn-replace-tags-key_turbine-generalFiles": [ { image: "anomaly_111_yaw_static" }, ], }, ], conclusionRows: [ { index: "1", problem_type: "静态偏航分析", turbine_names: "111", anomaly_points: "3", risk_level: "P2", suggestion: "核对偏航角越限情况。", }, ], "zn-techcn-replace-tags-data_sensor_anomaly-generalFiles": [ { image: "anomaly_sensor_dist" }, ], "zn-techcn-replace-tags-key_turbine-generalFiles": [], }; DETECTOR_TEMPLATE_CONFIG.forEach((cfg) => { if (cfg.templateKey === "yaw_static") { Object.assign( renderData, buildDetectorSectionPayload(cfg, { farmImages: [{ image: "anomaly_farm_yaw_static" }], turbineImages: [{ image: "anomaly_111_yaw_static" }], rows: [ { turbine_name: "111", anomaly_points: "3", anomaly_rate: "12.00%", comment: "功能诊断异常", }, ], anomalyPoints: 3, anomalyRate: 0.12, anomalyTurbines: 1, }), ); return; } const farmTag = `zn-techcn-replace-tags-${cfg.templateKey}-farmSummary`; const fileTag = `zn-techcn-replace-tags-${cfg.templateKey}-generalFiles`; renderData[`show-${fileTag}`] = []; renderData[farmTag] = []; renderData[fileTag] = []; renderData[`${cfg.templateKey}Rows`] = []; }); const buffer = await renderDocxReport({ templateName: "异常检测数据分析报告模板(大唐版).docx", renderData, imageBufferMap, }); const outDir = path.join(__dirname, "../templates"); fs.mkdirSync(outDir, { recursive: true }); const outPath = path.join(outDir, "_smoke_anomaly_report.docx"); fs.writeFileSync(outPath, buffer); console.log("smoke anomaly report written:", outPath, "bytes=", buffer.length); await shutdownChartService();