smoke-health-report.mjs 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. import fs from "fs";
  2. import PizZip from "pizzip";
  3. import {
  4. registerImageBuffer,
  5. renderDocxReport,
  6. } from "../src/server/reportService/docxReportBuilder.js";
  7. import { mapHealthReportContext } from "../src/server/reportService/healthReportMapper.js";
  8. import {
  9. buildFarmCategoryBarOption,
  10. buildFarmLevelPieOption,
  11. buildTurbineTrendChartOptions,
  12. } from "../src/server/reportService/healthChartBuilder.js";
  13. import { renderEchartsOption } from "../src/server/reportService/echartsRenderer.js";
  14. import {
  15. initChartService,
  16. shutdownChartService,
  17. } from "../src/server/utils/chartService/index.js";
  18. function assert(condition, message) {
  19. if (!condition) throw new Error(message);
  20. }
  21. const mapped = mapHealthReportContext(
  22. {
  23. healthscoresWindVO: {
  24. fieldName: "鄂能风电场",
  25. fieldCode: "SVLgVZfi",
  26. sourceDatetime: "2026-09-01",
  27. totalCount: 1,
  28. overallScore: 80,
  29. },
  30. healthOverviewListVOList: [],
  31. },
  32. {
  33. fieldName: "新疆十三师风电场",
  34. fieldMeta: {
  35. fieldName: "新疆十三师风电场",
  36. provinceName: "大唐新疆能源有限公司",
  37. machineTypeCode: "GW",
  38. },
  39. },
  40. );
  41. assert(
  42. mapped.Province === "新疆" &&
  43. mapped.Wind_farm === "十三师风电场" &&
  44. mapped.Company === "大唐新疆能源有限公司",
  45. `健康报告公司和风场应取选中风场,实际 Province=${mapped.Province} Wind_farm=${mapped.Wind_farm} Company=${mapped.Company}`,
  46. );
  47. assert(
  48. buildFarmLevelPieOption({
  49. overallScore: -1,
  50. excellentCount: 0,
  51. goodCount: 0,
  52. fairCount: 0,
  53. poorCount: 0,
  54. }) === null,
  55. "风场未评估时不应生成综合评分饼图",
  56. );
  57. assert(
  58. buildFarmCategoryBarOption({
  59. overallScore: -1,
  60. systemScore: -1,
  61. componentScore: -1,
  62. structureScore: -1,
  63. }) === null,
  64. "风场分类评分全未评估时不应生成柱状图",
  65. );
  66. const partialBar = buildFarmCategoryBarOption({
  67. systemScore: 75,
  68. componentScore: 70,
  69. structureScore: -1,
  70. });
  71. assert(partialBar, "有有效分类评分时应生成柱状图");
  72. assert(
  73. JSON.stringify(partialBar.xAxis.data) === JSON.stringify(["系统健康", "部件健康"]),
  74. `未评估分类不应出现在柱状图中,实际为 ${JSON.stringify(partialBar.xAxis.data)}`,
  75. );
  76. const emptyStructuralPoints = [
  77. {
  78. birthday: "2026-08-01",
  79. overallScore: 80,
  80. rotorScore: -1,
  81. towerScore: -1,
  82. yawSystemScore: 72,
  83. pitchSystemScore: 71,
  84. hydraulicSystemScore: 70,
  85. controlSystemScore: 69,
  86. generatorScore: 68,
  87. gearboxScore: 67,
  88. mainShaftScore: 66,
  89. converterScore: 65,
  90. },
  91. ];
  92. const emptyStructuralCharts = buildTurbineTrendChartOptions(
  93. emptyStructuralPoints,
  94. emptyStructuralPoints[0],
  95. );
  96. assert(
  97. !emptyStructuralCharts.some((item) => item.key === "structural"),
  98. "结构未评估时不应生成结构健康趋势图",
  99. );
  100. assert(
  101. emptyStructuralCharts.some((item) => item.key === "system"),
  102. "系统有数据时应保留系统健康趋势图",
  103. );
  104. await initChartService();
  105. const map = {};
  106. registerImageBuffer(
  107. map,
  108. "health_fig1",
  109. await renderEchartsOption(
  110. buildFarmLevelPieOption({
  111. overallScore: 80,
  112. excellentCount: 2,
  113. goodCount: 1,
  114. fairCount: 1,
  115. poorCount: 0,
  116. }),
  117. { width: 760, height: 420 },
  118. ),
  119. );
  120. registerImageBuffer(
  121. map,
  122. "health_fig2",
  123. await renderEchartsOption(
  124. buildFarmCategoryBarOption({
  125. overallScore: 80,
  126. systemScore: 75,
  127. componentScore: 70,
  128. structureScore: 72,
  129. }),
  130. { width: 760, height: 360 },
  131. ),
  132. );
  133. const trendPoints = [
  134. {
  135. birthday: "2026-08-01",
  136. overallScore: 80,
  137. rotorScore: 70,
  138. towerScore: 75,
  139. yawSystemScore: 72,
  140. pitchSystemScore: 71,
  141. hydraulicSystemScore: 70,
  142. controlSystemScore: 69,
  143. generatorScore: 68,
  144. gearboxScore: 67,
  145. mainShaftScore: 66,
  146. converterScore: 65,
  147. },
  148. {
  149. birthday: "2026-08-02",
  150. overallScore: 82,
  151. rotorScore: 71,
  152. towerScore: 75,
  153. yawSystemScore: 73,
  154. pitchSystemScore: 72,
  155. hydraulicSystemScore: 71,
  156. controlSystemScore: 70,
  157. generatorScore: 69,
  158. gearboxScore: 68,
  159. mainShaftScore: 67,
  160. converterScore: 66,
  161. },
  162. ];
  163. const trendCharts = buildTurbineTrendChartOptions(trendPoints, trendPoints[1]);
  164. assert(trendCharts.length === 4, "完整趋势数据应生成 4 张趋势图");
  165. const renderedCharts = [];
  166. for (const chart of trendCharts) {
  167. const imageKey = `health_trend_WT01_${chart.key}`;
  168. registerImageBuffer(
  169. map,
  170. imageKey,
  171. await renderEchartsOption(chart.option, {
  172. width: 760,
  173. height: chart.key === "overall" ? 260 : 300,
  174. }),
  175. );
  176. renderedCharts.push({
  177. chart_title: chart.title,
  178. trend_image: imageKey,
  179. });
  180. }
  181. const out = await renderDocxReport({
  182. templateName: "health-report-template.docx",
  183. skipPatch: false,
  184. imageBufferMap: map,
  185. renderData: {
  186. 风场名称: "测试风场",
  187. farm_name: "十三师风电场",
  188. Province: "新疆",
  189. Wind_farm: "十三师风电场",
  190. Year_now: "2026",
  191. Month_now: "08",
  192. machineTypeCode: "TEST",
  193. Company: "大唐新疆能源有限公司",
  194. field_id: "F1",
  195. create_time: "2026-08-13",
  196. source_datetime: "2026-08-12",
  197. eval_start_date: "2026-07-14",
  198. eval_end_date: "2026-08-12",
  199. eval_date_range: "2026-07-14至2026-08-12(滚动30天)",
  200. turbine_count: "1",
  201. valid_turbine_count: "1",
  202. turbine_types: "TEST",
  203. overall_score: "80",
  204. system_score: "75",
  205. component_score: "70",
  206. "structure_score/未评估": "72",
  207. excellent_count: "2",
  208. good_count: "1",
  209. fair_count: "1",
  210. poor_count: "0",
  211. overall_level_text: "良",
  212. main_level: "良",
  213. level_distribution_text: "优2台",
  214. overall_summary_text: "测试",
  215. focus_engine_list: "WT01",
  216. chart_fig1: "health_fig1",
  217. chart_fig2: "health_fig2",
  218. show_chart_fig1: [{}],
  219. show_chart_fig2: [{}],
  220. turbine_trends: [
  221. { engine_title: "WT01 风机健康趋势图", charts: renderedCharts },
  222. ],
  223. },
  224. });
  225. fs.writeFileSync("templates/_smoke_health_report.docx", out);
  226. const zip = new PizZip(out);
  227. const headerTexts = Object.keys(zip.files)
  228. .filter((name) => /^word\/header\d+\.xml$/.test(name))
  229. .sort()
  230. .map((name) => {
  231. const xml = zip.file(name).asText();
  232. const texts = [];
  233. xml.replace(/<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>/g, (_, text) => {
  234. texts.push(text);
  235. return _;
  236. });
  237. return { name, plain: texts.join("").trim() };
  238. });
  239. const filledHeaders = headerTexts.filter((item) => item.plain);
  240. assert(filledHeaders.length > 0, "健康报告应有非空页眉");
  241. filledHeaders.forEach((item) => {
  242. assert(
  243. item.plain === "大唐新疆能源有限公司十三师风电场风机健康评估报告",
  244. `${item.name} 页眉应为完整公司+风场标题,实际为「${item.plain}」`,
  245. );
  246. });
  247. const footerTexts = Object.keys(zip.files)
  248. .filter((name) => /^word\/footer\d+\.xml$/.test(name))
  249. .sort()
  250. .map((name) => {
  251. const xml = zip.file(name).asText();
  252. const texts = [];
  253. xml.replace(/<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>/g, (_, text) => {
  254. texts.push(text);
  255. return _;
  256. });
  257. return { name, plain: texts.join(""), hasPage: /PAGE/.test(xml), xml };
  258. });
  259. footerTexts.forEach((item) => {
  260. assert(!item.hasPage, `${item.name} 不应含页码域`);
  261. assert(!item.plain.trim(), `${item.name} 页脚应为空,实际为「${item.plain}」`);
  262. });
  263. const coverXml = zip.file("word/document.xml").asText();
  264. assert(
  265. coverXml.includes("新疆十三师风电场") &&
  266. !coverXml.includes("新疆十三师风电场风电场"),
  267. "封面风场名不应重复「风电场」",
  268. );
  269. console.log("smoke ok, size=", out.length);
  270. await shutdownChartService();
  271. process.exit(0);