| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459 |
- 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();
|