From c7a612fef158273147dd0c9c7a3000a1e359373d Mon Sep 17 00:00:00 2001 From: zhuxinru Date: Thu, 22 Jan 2026 19:29:23 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E7=94=9F=E6=88=90=E6=8A=A5=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/data/DataClientFeignClientImpl.java | 5 +- .../feign/inter/data/IDataFeignClient.java | 2 +- .../data/controller/DataFileController.java | 4 +- .../sdm/data/service/IDataFileService.java | 2 +- .../impl/MinioFileIDataFileServiceImpl.java | 4 +- .../impl/SystemFileIDataFileServiceImpl.java | 5 +- .../main/resources/application-dev-190.yml | 2 + .../src/main/resources/application-dev-65.yml | 2 + data/src/main/resources/application-local.yml | 2 + data/src/main/resources/application-lyric.yml | 2 + project/newExportWord.py | 934 ++++++++++++++++++ .../controller/SimulationRunController.java | 26 +- .../service/ISimulationRunService.java | 6 +- .../impl/SimulationRunServiceImpl.java | 244 ++++- .../main/resources/application-dev-190.yml | 1 + .../src/main/resources/application-dev-65.yml | 1 + .../src/main/resources/application-local.yml | 1 + .../src/main/resources/application-lyric.yml | 1 + 18 files changed, 1206 insertions(+), 38 deletions(-) create mode 100644 project/newExportWord.py diff --git a/common/src/main/java/com/sdm/common/feign/impl/data/DataClientFeignClientImpl.java b/common/src/main/java/com/sdm/common/feign/impl/data/DataClientFeignClientImpl.java index d90c0b71..173a2397 100644 --- a/common/src/main/java/com/sdm/common/feign/impl/data/DataClientFeignClientImpl.java +++ b/common/src/main/java/com/sdm/common/feign/impl/data/DataClientFeignClientImpl.java @@ -164,11 +164,12 @@ public class DataClientFeignClientImpl implements IDataFeignClient { } @Override - public void downloadFileToLocal(Long fileId,String path) { + public SdmResponse downloadFileToLocal(Long fileId,String path) { try { - dataClient.downloadFileToLocal(fileId,path); + return dataClient.downloadFileToLocal(fileId,path); } catch (Exception e) { log.error("下载文件响应", e); + return SdmResponse.failed("下载文件到本地失败"); } } diff --git a/common/src/main/java/com/sdm/common/feign/inter/data/IDataFeignClient.java b/common/src/main/java/com/sdm/common/feign/inter/data/IDataFeignClient.java index 528b73b4..78a0186d 100644 --- a/common/src/main/java/com/sdm/common/feign/inter/data/IDataFeignClient.java +++ b/common/src/main/java/com/sdm/common/feign/inter/data/IDataFeignClient.java @@ -62,7 +62,7 @@ public interface IDataFeignClient { SdmResponse queryFileMetadataInfo(@RequestParam(value = "uuid") String uuid, @RequestParam(value = "uuidOwnType") String uuidOwnType, @RequestParam(value = "dirId") Long dirId); @PostMapping("/data/downloadFileToLocal") - void downloadFileToLocal(@RequestParam(value = "fileId") @Validated Long fileId, @RequestParam(value = "path") @Validated String path); + SdmResponse downloadFileToLocal(@RequestParam(value = "fileId") @Validated Long fileId, @RequestParam(value = "path") @Validated String path); /** * 下载文件夹 diff --git a/data/src/main/java/com/sdm/data/controller/DataFileController.java b/data/src/main/java/com/sdm/data/controller/DataFileController.java index 5a875f4b..25398d3e 100644 --- a/data/src/main/java/com/sdm/data/controller/DataFileController.java +++ b/data/src/main/java/com/sdm/data/controller/DataFileController.java @@ -486,8 +486,8 @@ public class DataFileController implements IDataFeignClient { */ @PostMapping("/downloadFileToLocal") @Operation(summary = "下载文件到本地临时目录", description = "下载文件到本地临时目录") - public void downloadFileToLocal(@RequestParam(value = "fileId") @Validated Long fileId, @RequestParam(value = "path") @Validated String path) { - IDataFileService.downloadFileToLocal(fileId,path); + public SdmResponse downloadFileToLocal(@RequestParam(value = "fileId") @Validated Long fileId, @RequestParam(value = "path") @Validated String path) { + return IDataFileService.downloadFileToLocal(fileId,path); } @PostMapping("/downloadFolderToLocal") diff --git a/data/src/main/java/com/sdm/data/service/IDataFileService.java b/data/src/main/java/com/sdm/data/service/IDataFileService.java index 4c42dfda..f78f9c8e 100644 --- a/data/src/main/java/com/sdm/data/service/IDataFileService.java +++ b/data/src/main/java/com/sdm/data/service/IDataFileService.java @@ -366,7 +366,7 @@ public interface IDataFileService { * @param fileId 文件id * @param basePath 临时目录路径 */ - void downloadFileToLocal(Long fileId,String basePath); + SdmResponse downloadFileToLocal(Long fileId,String basePath); /** * 批量下载指定文件夹到本地目录 diff --git a/data/src/main/java/com/sdm/data/service/impl/MinioFileIDataFileServiceImpl.java b/data/src/main/java/com/sdm/data/service/impl/MinioFileIDataFileServiceImpl.java index 6435eb48..16ae71e4 100644 --- a/data/src/main/java/com/sdm/data/service/impl/MinioFileIDataFileServiceImpl.java +++ b/data/src/main/java/com/sdm/data/service/impl/MinioFileIDataFileServiceImpl.java @@ -3001,7 +3001,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { } @Override - public void downloadFileToLocal(Long fileId, String basePath) { + public SdmResponse downloadFileToLocal(Long fileId, String basePath) { if (fileId == null || basePath == null) { throw new RuntimeException("参数不能为空"); } @@ -3040,7 +3040,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { outputStream.write(buffer, 0, bytesRead); } } - + return SdmResponse.success(targetFile.toString()); } catch (Exception e) { log.error("下载文件失败,fileId={}, basePath={}, fileName={}", fileId, basePath, originalName, e); diff --git a/data/src/main/java/com/sdm/data/service/impl/SystemFileIDataFileServiceImpl.java b/data/src/main/java/com/sdm/data/service/impl/SystemFileIDataFileServiceImpl.java index ae8dcffc..fb8db273 100644 --- a/data/src/main/java/com/sdm/data/service/impl/SystemFileIDataFileServiceImpl.java +++ b/data/src/main/java/com/sdm/data/service/impl/SystemFileIDataFileServiceImpl.java @@ -1566,10 +1566,10 @@ public class SystemFileIDataFileServiceImpl implements IDataFileService { } @Override - public void downloadFileToLocal(Long fileId,String downloadPath) { + public SdmResponse downloadFileToLocal(Long fileId,String downloadPath) { if (StringUtils.isNotBlank(downloadPath) && downloadPath.contains("..")) { log.error(downloadPath + "非法文件路径!"); - return; + return SdmResponse.failed("非法文件路径"); } String path = Tools.getRootPath(rootPath, String.valueOf(ThreadLocalContext.getTenantId())) + File.separator + downloadPath; File file = new File(path); @@ -1591,6 +1591,7 @@ public class SystemFileIDataFileServiceImpl implements IDataFileService { } catch (IOException e) { log.error("下载文件失败:" + e); } + return SdmResponse.success(path); } @Override diff --git a/data/src/main/resources/application-dev-190.yml b/data/src/main/resources/application-dev-190.yml index 1b8b058b..742df38e 100644 --- a/data/src/main/resources/application-dev-190.yml +++ b/data/src/main/resources/application-dev-190.yml @@ -131,6 +131,8 @@ security: - /data/initNewTenant - /data/getMinioPresignedUrl - /data/getPublicDownloadUrl + - /data/updatePermission + - /data/createDir data: storage-monitor: diff --git a/data/src/main/resources/application-dev-65.yml b/data/src/main/resources/application-dev-65.yml index 5fa8ba55..b328e5df 100644 --- a/data/src/main/resources/application-dev-65.yml +++ b/data/src/main/resources/application-dev-65.yml @@ -131,6 +131,8 @@ security: - /data/initNewTenant - /data/getMinioPresignedUrl - /data/getPublicDownloadUrl + - /data/updatePermission + - /data/createDir data: storage-monitor: diff --git a/data/src/main/resources/application-local.yml b/data/src/main/resources/application-local.yml index 873dc98a..c0f1cf29 100644 --- a/data/src/main/resources/application-local.yml +++ b/data/src/main/resources/application-local.yml @@ -136,6 +136,8 @@ security: - /data/initNewTenant - /data/getMinioPresignedUrl - /data/getPublicDownloadUrl + - /data/updatePermission + - /data/createDir data: storage-monitor: diff --git a/data/src/main/resources/application-lyric.yml b/data/src/main/resources/application-lyric.yml index ef3fe1ad..5537e89b 100644 --- a/data/src/main/resources/application-lyric.yml +++ b/data/src/main/resources/application-lyric.yml @@ -131,6 +131,8 @@ security: - /data/initNewTenant - /data/getMinioPresignedUrl - /data/getPublicDownloadUrl + - /data/updatePermission + - /data/createDir data: storage-monitor: diff --git a/project/newExportWord.py b/project/newExportWord.py new file mode 100644 index 00000000..497886a8 --- /dev/null +++ b/project/newExportWord.py @@ -0,0 +1,934 @@ + +import os +import json +import shutil +import re +import traceback +from lxml import etree +from datetime import datetime +import sys +import base64 +from PIL import Image +import io +from docx.shared import Pt +from docx.oxml.ns import qn +from docx.shared import Emu, Inches +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.oxml import OxmlElement + +from docx import Document +from docx.shared import RGBColor +from Tool import getPfm, modCsv +import modifyReport + + +def getJsonData(jsonFilePath): + """获取JSON数据""" + if not os.path.exists(jsonFilePath): + print(f"Error:JSON文件路径 '{jsonFilePath}' 不存在,请确认") + return None + + with open(jsonFilePath, 'r', encoding='utf-8') as jsonFile: + data = json.load(jsonFile) + + return data + + +def getCsvData(csvFilePath): + """获取CSV数据""" + data_dict = {} + if not os.path.exists(csvFilePath): + print(f"Error:CSV文件路径 '{csvFilePath}' 不存在,请确认") + return data_dict + + with open(csvFilePath, 'r', encoding='utf-8') as csvFile: + lines = csvFile.readlines() + for line in lines: + if "," in line: + values = line.strip().split(',') + key = values[0] + data_dict[key] = values[1] + + return data_dict + + +def getTaskType(taskName, projectInfo_dict): + """根据工况名称获取分析项类型""" + taskType = projectInfo_dict.get("taskType", None) + if taskType is not None: + return taskType + else: + print( + f"Warning:projectInfo.json中不存在'taskType'字段,将根据工况名称'{taskName}'进行判断") + if "动力学" in taskName or taskName.startswith("dynamics"): + return "dynamics" + elif "静力学" in taskName or taskName.startswith("statics"): + return "statics" + elif "流体" in taskName or taskName.startswith("fluid"): + return "fluid" + elif "热分析" in taskName or taskName.startswith("thermal"): + return "thermal" + else: + print(f"Warning:未知的分析项类型 '{taskName}',默认返回 'statics'") + return "statics" + + +def wirteValueToPerformance(keyName, value, highValue, csvPath): + """把值写入到output_values.csv中 用于生成performance.json""" + lines = [] + if os.path.exists(csvPath): + with open(csvPath, 'r+', encoding='utf-8') as fr: + lines = fr.readlines() + found = False + for i in range(len(lines)): + if lines[i].startswith(keyName + ","): + lines[i] = f"{keyName},{value},{highValue}\n" + found = True + break + if not found: + lines.append(f"{keyName},{value},{highValue}\n") + with open(csvPath, 'w+', encoding='utf-8') as fw: + fw.writelines(lines) + + +def replace_text_in_paragraph(paragraph, data_dict, csvPath=None): + """替换段落中的文本""" + text = paragraph.text + # 居中显示 + # paragraph.alignment = 1 + ngFlag = False + if "$" in text and "<" not in text and ">" not in text: + keys_list = re.findall(r'\$([a-zA-Z0-9_]+)', text) + for keyName in keys_list: + value = data_dict.get(keyName, None) + if value is not None: + placeholder = f"${keyName}" + text = text.replace(placeholder, str(value)) + print(f"文本: {placeholder} -> {value}") + paragraph.text = text + elif "$" in text and "<" in text: + # 处理带有<>的占位符 + start_index = text.find("$") + end_index = text.find("<", start_index) + if end_index != -1: + placeholder = text[start_index:end_index + 1] + keyName = placeholder[1:-1] # 去掉$和<> + value = data_dict.get(keyName, None) + highValue = text.split("<")[1] + if value is not None: + # 把标准值写入到output_values.csv中 用于生成performance.json + wirteValueToPerformance(keyName, value, highValue, csvPath) + if float(value) > float(highValue): + result = "NG" + # 设置红色 + ngFlag = True + else: + result = "OK" + paragraph.text = result + if ngFlag: + for run in paragraph.runs: + run.font.color.rgb = RGBColor(255, 0, 0) + print(f"文本: {placeholder} -> {result}") + elif "$" in text and ">" in text: + # 处理带有<>的占位符 + start_index = text.find("$") + end_index = text.find(">", start_index) + if end_index != -1: + placeholder = text[start_index:end_index + 1] + keyName = placeholder[1:-1] # 去掉$和<> + value = data_dict.get(keyName, None) + highValue = text.split(">")[1] + if value is not None: + if float(value) < float(highValue): + result = "NG" + # 设置红色 + ngFlag = True + else: + result = "OK" + paragraph.text = result + if ngFlag: + for run in paragraph.runs: + run.font.color.rgb = RGBColor(255, 0, 0) + print(f"文本: {placeholder} -> {result}") + + +def replace_text_in_table(table, data_dict, csvPath): + """替换表格中的文本""" + for row in table.rows: + for cell in row.cells: + # 递归处理单元格中的嵌套表格 + if cell.tables: + for nested_table in cell.tables: + replace_text_in_table(nested_table, data_dict, csvPath) + + # 处理当前单元格的段落文本 + for paragraph in cell.paragraphs: + replace_text_in_paragraph(paragraph, data_dict, csvPath) + + +def replace_text_in_textbox(doc, data_dict): + """替换文本框中的文本""" + namespaces = { + 'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main', + 'wp': 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing', + 'a': 'http://schemas.openxmlformats.org/drawingml/2006/main', + 'wps': 'http://schemas.microsoft.com/office/word/2010/wordprocessingShape' + } + + doc_modified = False # 整个文档是否被修改 + for part_id, related_part in doc.part.related_parts.items(): + if hasattr(related_part, 'blob'): + xml_content = related_part.blob + # 检查是否是 XML 内容(以 {value}") + part_modified = True + doc_modified = True + + # 关键步骤:将修改后的 XML 写回到部件中 + if part_modified: + # 同时更新 _element 和 _blob + if hasattr(related_part, '_element'): + related_part._element = root + updated_xml = etree.tostring( + root, encoding='UTF-8', xml_declaration=True) + related_part._blob = updated_xml + + except etree.XMLSyntaxError as e: + print(f"Warning:部件 {part_id} 不是有效的 XML: {e}") + continue + except Exception as e: + print(f"Warning:处理部件 {part_id} 时出错: {e}") + continue + + +def replaceConclusion(doc, data_dict): + """替换结论中的文本""" + targeTable_list = doc.tables[0]._cells + try: + for targeTable in targeTable_list: + for paragraph in targeTable.paragraphs: + replace_text_in_paragraph(paragraph, data_dict) + except Exception as e: + pass + + +def getImageNameList(projectPath): + """获取项目根目录下所有图片名""" + imageName_list = [] + for file in os.listdir(projectPath): + if file.endswith(".png") or file.endswith(".jpg") or file.endswith(".jpeg"): + imageName_list.append(file) + print(f"{projectPath} 图片列表: {imageName_list}") + return imageName_list + + +def modifyImageName(imageName_list, targetImage_list): + """检查是否所有图片都存在 不存在则修改图片名""" + existImage_list = [] + for imageName in imageName_list: + if imageName in targetImage_list: + existImage_list.append(imageName) + else: + for targetImage in targetImage_list: + if targetImage in existImage_list: + continue + newImageName = targetImage + break + # 修改图片名 + newImage = f"{projectPath}/{newImageName}" + oldImage = f"{projectPath}/{imageName}" + print(f"Info:修改图片名: {imageName} -> {newImageName}") + existImage_list.append(newImageName) + if os.path.exists(oldImage): + os.rename(oldImage, newImage) + + +def getPefData(performance_dict, csvPath, templatePath, templateType): + """获取性能指标数据 写入csv文件""" + content_list = [] + for key, value in performance_dict.items(): + if key == "type": + continue + for k, item in value.items(): + if k == "value": + line = f"{key},{item}\n" + content_list.append(line) + break + + # 手动上传 根据ppt模板,修改key + # mapPptKeyPath = f"{templatePath}/mapPptKeyToPerfor.json" + # pptKeyData_dict = getJsonData(mapPptKeyPath)[templateType] + # targetKey_list = list(pptKeyData_dict.keys()) + # for index, line in enumerate(content_list): + # keyName = line.split(",")[0] + # if keyName not in targetKey_list and index < len(targetKey_list): + # newKeyName = targetKey_list[index] + # value = line.split(",")[1] + # newLine = f"{newKeyName},{value}" + # content_list[index] = newLine + + with open(csvPath, "w", encoding="utf-8") as f: + f.writelines(content_list) + print(f"写入csv文件: {csvPath}") + + +def inspect_document_structure(doc_path): + """ + 检查文档结构,找出所有形状的名称 + """ + doc = Document(doc_path) + + print("文档中的形状信息:") + for i, shape in enumerate(doc.inline_shapes): + element = shape._inline + doc_pr = element.find('.//wp:docPr', namespaces=element.nsmap) + + if doc_pr is not None: + shape_name = doc_pr.get('name', '') + shape_id = doc_pr.get('id', '') + desc = doc_pr.get('descr', '') + + print(f"形状 {i+1}:") + print(f" 名称: {shape_name}") + print(f" ID: {shape_id}") + print(f" 描述: {desc}") + print("-" * 30) + + +def generateReport(reportFilePath, dataPath, data_dict, csvPath, image_dict): + """生成Word报告""" + # inspect_document_structure(reportFilePath) + doc = Document(reportFilePath) + + # 处理页眉 + for section in doc.sections: + if section.header: + for paragraph in section.header.paragraphs: + replace_text_in_paragraph(paragraph, data_dict, csvPath) + # 处理页眉中的表格 + for table in section.header.tables: + replace_text_in_table(table, data_dict, csvPath) + + # 替换图片 + for rel in doc.part.rels.values(): + if "image" in rel.reltype: + # print(rel.target_ref, rel.reltype) + # media/image3.png + imageItem = rel.target_ref.split("/")[-1] + picName = image_dict.get(imageItem, None) + if picName is not None and picName != ".png": + replace_image_path = os.path.join(dataPath, picName) + if os.path.exists(replace_image_path): + print(f"图片: {imageItem} -> {picName}") + rel.target_part._blob = open( + replace_image_path, "rb").read() + + # 处理表格 + for table in doc.tables: + replace_text_in_table(table, data_dict, csvPath) + + # 处理结论 + replaceConclusion(doc, data_dict) + + # 处理文本框 + replace_text_in_textbox(doc, data_dict) + + # 保存文档 + doc.save(reportFilePath) + print(f"Info:Word文档已创建: {reportFilePath}") + + +def generatePerformance(projectPath, csvPath, key_dict, templateType): + """生成性能指标""" + performancePath = f"{projectPath}/performance.json" + + if templateType in ["statics", "dynamics"]: + performance_dict = getPfm.generalPerformance(csvPath, key_dict) + elif templateType == "modal": + performance_dict = getPfm.generalPerformanceModal(csvPath, key_dict) + else: + performance_dict = {} + + with open(performancePath, 'w', encoding='utf-8') as perfFile: + json.dump(performance_dict, perfFile, indent=4, ensure_ascii=False) + + print(f"Info:性能指标已生成: {performancePath}") + + +def exportWord(projectPath, templatePath, loacase=None): + """主函数""" + print(f"Info:开始生成工况: {loacase} 的Word报告...") + + # 根据工况名称获取docx模板类型和后处理图片和图片框对应关系 + templateType = getTaskType(loacase) + templateFilePath = f"{templatePath}/{templateType}_template.docx" + mapPicNamePath = f"{templatePath}/mapPicNameToPpt.json" + image_dict = getJsonData(mapPicNamePath).get(templateType, None) + print(f"Info:模板类型: {templateType}, 图片对应关系: {image_dict}") + if image_dict is None: + print(f"Warning:模板类型: {templateType} 不存在图片替换,请确认") + if not os.path.exists(templateFilePath): + print(f"Error:模板文件路径 '{templateFilePath}' 不存在,请确认") + return + + # 拷贝模板文件 + reportPath = f"{projectPath}/report" + if not os.path.exists(reportPath): + os.makedirs(reportPath) + reportFilePath = f"{reportPath}/report.docx" + shutil.copy(templateFilePath, reportFilePath) + + # 获取项目信息数据和结果数据 + projectInfoPath = f"{projectPath}/projectInfo.json" + projectInfo_dict = getJsonData(projectInfoPath) + + dataPath = f"{projectPath}/result/process_data" + csvPath = f"{dataPath}/output_values.csv" + # 个别工况需要先对output_values.csv进行处理 + if templateType == "thermal": + modCsv.modifyCsvForThermal(csvPath) + data_dict = getCsvData(csvPath) + data_dict.update(projectInfo_dict) + + # 生成报告 + generateReport(reportFilePath, dataPath, data_dict, csvPath, image_dict) + + # 生成性能指标 + mapPptKeyPath = f"{templatePath}/mapPptKeyToPerfor.json" + key_dict = getJsonData(mapPptKeyPath).get(templateType, None) + print(f"\nInfo:PPT键值中英文: {key_dict}") + generatePerformance(projectPath, csvPath, key_dict, templateType) + + +def exportWordByUser(projectPath, templatePath, loacase=None): + """用户手动导出Word报告 图片和性能指标都是手动上传到项目路径根目录下""" + print(f"Info:开始生成工况: {loacase} 的Word报告...") + projectInfoPath = f"{projectPath}/projectInfo.json" + projectInfo_dict = getJsonData(projectInfoPath) + + templateType = getTaskType(loacase, projectInfo_dict) + templateFilePath = f"{templatePath}/{templateType}_template.docx" + mapPicNamePath = f"{templatePath}/mapPicNameToPpt.json" + image_dict = getJsonData(mapPicNamePath).get(templateType, None) + targetImage_list = list(image_dict.values()) + print(f"Info:模板类型: {templateType}, 图片对应关系: {image_dict}") + if image_dict is None: + print(f"Warning:模板类型: {templateType} 不存在图片替换,请确认") + if not os.path.exists(templateFilePath): + print(f"Error:模板文件路径 '{templateFilePath}' 不存在,请确认") + return + + # 获取上传的图片名 + imageName_list = getImageNameList(projectPath) + # 检查是否所有图片都存在 不存在则修改图片名 + modifyImageName(imageName_list, targetImage_list) + + # 拷贝模板文件 + reportPath = f"{projectPath}/report" + if not os.path.exists(reportPath): + os.makedirs(reportPath) + reportFilePath = f"{reportPath}/report.docx" + shutil.copy(templateFilePath, reportFilePath) + + # 获取项目信息数据 + dataPath = projectPath + # 获取指标数据 + performancePath = f"{projectPath}/performance.json" + performance_dict = getJsonData(performancePath) + csvPath = f"{dataPath}/output_values.csv" + getPefData(performance_dict, csvPath, templatePath, templateType) + data_dict = getCsvData(csvPath) + data_dict.update(projectInfo_dict) + + # 生成报告 + generateReport(reportFilePath, dataPath, data_dict, csvPath, image_dict) + + +class Tee: + """自定义文件对象,同时写入文件和原控制台""" + + def __init__(self, *files): + self.files = files + + def write(self, obj): + for f in self.files: + f.write(obj) + f.flush() # 确保立即写入 + + def flush(self): + for f in self.files: + f.flush() + + +def base64ToImg(projectPath, reportContent_list): + """把base64字符串转成图片""" + img_num = 0 + imgName_dict = {} + for item_dict in reportContent_list: + if item_dict["type"] == "img": + key_name = item_dict.get("key", "") + if key_name not in imgName_dict: + imgName_dict[key_name] = [] + img_base64_list = item_dict.get("value", []) + + if img_base64_list: + for img_base64_dict in img_base64_list: + imgTemp_dict = {} + img_base64_str = img_base64_dict.get("src", "") + img_base64 = img_base64_str.split(",")[1] + img_data = base64.b64decode(img_base64) + img_num += 1 + img_path = f"{projectPath}/pic{img_num}.png" + picName = img_base64_dict.get("title", "") + imgTemp_dict[f"pic{img_num}"] = picName + imgName_dict[key_name].append(imgTemp_dict) + with open(img_path, 'wb') as img_file: + img_file.write(img_data) + # 新增字典中的picPath为图片路径 + # item_dict["picPath"] = img_path + return img_num, imgName_dict + + +def getDataDict(reportContent_list): + """获取数据字典 用于替换文本""" + data_dict = {} + table_num = 0 + for item_dict in reportContent_list: + if item_dict["type"] == "text": + data_dict[item_dict["key"]] = item_dict.get("value", "") + elif item_dict["type"] == "table": + tableContent = item_dict.get("value", []) + # 获取表头 + headers = item_dict.get("head", []) + tableName = item_dict.get("key", "") + if headers and tableName: + header_str = ",".join(headers) + # newKey = item_dict["key"] + "_" + header_str + newKey = tableName + "_" + header_str + data_dict[newKey] = tableContent + table_num += 1 + elif item_dict["type"] == "conclusion": + data_dict[item_dict["key"]] = item_dict["value"] + return data_dict, table_num + + +def set_table_border(table): + """ + 通过底层 XML 强制给表格添加全框线(细黑实线) + """ + tbl = table._tbl + tblPr = tbl.tblPr + + # 创建表格边框元素 + borders = OxmlElement('w:tblBorders') + + # 定义需要添加边框的方向:上下左右、内部水平、内部垂直 + for border_name in ['top', 'left', 'bottom', 'right', 'insideH', 'insideV']: + edge = OxmlElement(f'w:{border_name}') + edge.set(qn('w:val'), 'single') # 线型:单实线 + edge.set(qn('w:sz'), '4') # 线宽:1/8 磅的倍数,4 表示 0.5 磅 + edge.set(qn('w:space'), '0') # 间距 + edge.set(qn('w:color'), 'auto') # 颜色:自动(黑色) + borders.append(edge) + + tblPr.append(borders) + + +def replaceParagraph(doc, paragraph, data_dict): + """替换段落中的占位符:删除原段落并新增表格""" + text = paragraph.text + if "$table_" in text: + judgeFalg = False + targets = {"分析值", "达成状态", "目标值"} + parts = text.split("_") + if len(parts) < 2: + return + tableName = parts[1] + + for item_key, item_value in data_dict.items(): + if tableName in item_key: + head_str = item_key.split("_")[1] + head_list = head_str.split(",") + if all(item in head_list for item in targets): + judgeFalg = True + value_list = [list(item_dict.values()) + for item_dict in item_value] + table_content = [head_list] + value_list + # 创建新表格 + rows = len(table_content) + cols = len(head_list) + table = doc.add_table(rows=rows, cols=cols) + set_table_border(table) + for row_index, row_data in enumerate(table_content): + valueIndex = None + targetIndex = None + judgeIndex = None + if row_index == 0 and judgeFalg: + # 记录目标值、达成状态、分析值的列索引 + for col_index, cell_value in enumerate(row_data): + if cell_value == "目标值": + targetIndex = col_index + elif cell_value == "达成状态": + judgeIndex = col_index + elif cell_value == "分析值": + valueIndex = col_index + for col_index, cell_value in enumerate(row_data): + cell = table.cell(row_index, col_index) + cell.text = "" + p = cell.paragraphs[0] + p.alignment = WD_ALIGN_PARAGRAPH.CENTER + run = p.add_run(str(cell_value)) + run.font.name = '微软雅黑' + run._element.rPr.rFonts.set(qn('w:eastAsia'), '微软雅黑') + run.font.size = Pt(10.5) + if row_index == 0: + run.bold = True + # 如果是不合格变成红色字体 合格变成绿色 + if cell_value == "不合格": + run.font.color.rgb = RGBColor(0xFF, 0x00, 0x00) + if cell_value == "合格": + run.font.color.rgb = RGBColor(20, 139, 0) + + # 将表格的 XML 元素插入到当前段落元素的前面 + paragraph._element.addprevious(table._tbl) + + # 删除原来的占位段落 + p_element = paragraph._element + p_element.getparent().remove(p_element) + print(f"Info:新增表格{tableName}") + break + + else: + keys_list = re.findall(r'\$([a-zA-Z0-9_]+)', text) + if not keys_list: + return + for keyName in keys_list: + value = data_dict.get(keyName) + if value is None: + continue + # placeholder = f"${keyName}" + placeholder = keyName + + # 尝试在Run级别替换保留格式 + for run in paragraph.runs: + if "$" in run.text: + textKey = run.text.split("$")[1] + if textKey == "": + run.text = "" + elif keyName in textKey: + run.text = run.text.replace(f"${keyName}", str(value)) + print(f"Info: 文本替换: {placeholder} -> {str(value)}") + elif run.text == keyName: + run.text = run.text.replace(placeholder, str(value)) + print(f"Info: 文本替换: {placeholder} -> {str(value)}") + + +def clear_paragraph_indent(paragraph): + """强制清除段落的所有缩进""" + pPr = paragraph._element.get_or_add_pPr() + ind = pPr.get_or_add_ind() + ind.set(qn('w:firstLine'), '0') + ind.set(qn('w:firstLineChars'), '0') + ind.set(qn('w:left'), '0') + ind.set(qn('w:leftChars'), '0') + + +def crop_image_to_ratio(image_path, target_width_emu, target_height_emu): + """ + 根据目标宽高的比例,对图片进行【居中裁剪】 + 返回: BytesIO 对象 (可直接传给 add_picture) + """ + try: + with Image.open(image_path) as img: + # 1. 计算目标宽高比 + target_ratio = target_width_emu / target_height_emu + img_ratio = img.width / img.height + + # 2. 计算裁剪区域 (Box: left, upper, right, lower) + if img_ratio > target_ratio: + # 图片太宽,需要裁掉左右两边 + new_width = int(img.height * target_ratio) + offset = (img.width - new_width) // 2 + box = (offset, 0, img.width - offset, img.height) + else: + # 图片太高,需要裁掉上下两边 + new_height = int(img.width / target_ratio) + offset = (img.height - new_height) // 2 + box = (0, offset, img.width, img.height - offset) + + # 3. 执行裁剪 + cropped_img = img.crop(box) + + # 4. 保存到内存流 + img_byte_arr = io.BytesIO() + # 保持原格式,如果是PNG保持RGBA,否则转RGB + img_format = img.format if img.format else 'PNG' + cropped_img.save(img_byte_arr, format=img_format) + img_byte_arr.seek(0) + return img_byte_arr + except Exception as e: + print(f"裁剪图片出错: {e}") + return None + + +def process_images_in_paragraphs(doc, image_dict, paragraphs, imgName_dict, projectPath): + """ + 将所有图片(包含换行部分)全部放入一个统一的大表格中 + """ + section = doc.sections[0] + page_content_width = section.page_width - \ + section.left_margin - section.right_margin + gap_width_emu = 144000 + + for p in list(paragraphs): + blips = p._element.xpath('.//a:blip') + for blip in blips: + rId = blip.get(qn('r:embed')) + if rId in doc.part.rels: + rel = doc.part.rels[rId] + imageItem = rel.target_ref.split("/")[-1] + picName = image_dict.get(imageItem, None) + pic_value_list = imgName_dict.get(picName, []) + + if not pic_value_list: + continue + + # 1. 获取基准尺寸 + original_cx, original_cy = 2743200, 2743200 + extents = p._element.xpath('.//wp:extent') + if extents: + try: + original_cx = int(extents[0].get('cx')) + original_cy = int(extents[0].get('cy')) + except: + pass + + # 2. 计算分行布局 (例如: [[图1,图2,图3], [图4]]) + rows_data = [] + current_row = [] + current_w = 0 + for item in pic_value_list: + if not current_row: + current_row.append(item) + current_w = original_cx + elif current_w + gap_width_emu + original_cx <= page_content_width: + current_row.append(item) + current_w += (gap_width_emu + original_cx) + else: + rows_data.append(current_row) + current_row = [item] + current_w = original_cx + if current_row: + rows_data.append(current_row) + + # 3. 创建统一的大表格 + # 总行数 = 计算出的行数 * 2 (一行图,一行文) + # 总列数 = 所有行中图片最多的那一行的数量 + total_cols = max(len(r) for r in rows_data) + big_table = doc.add_table( + rows=len(rows_data) * 2, cols=total_cols) + p._element.addnext(big_table._element) + + # 移除大表边框 + tblPr = big_table._tbl.tblPr + borders = OxmlElement('w:tblBorders') + for b in ['top', 'left', 'bottom', 'right', 'insideH', 'insideV']: + node = OxmlElement(f'w:{b}') + node.set(qn('w:val'), 'nil') + borders.append(node) + tblPr.append(borders) + + # 4. 填充大表格内容 + for row_idx, row_content in enumerate(rows_data): + img_row_num = row_idx * 2 # 图片所在的行 + txt_row_num = row_idx * 2 + 1 # 文字所在的行 + + for col_idx, img_item in enumerate(row_content): + name = list(img_item.keys())[0] + title = img_item[name] + # path = os.path.join(projectPath, f"{name}.png") + path = name + + # A. 插入图片 + cell_img = big_table.cell(img_row_num, col_idx) + p_img = cell_img.paragraphs[0] + p_img.alignment = WD_ALIGN_PARAGRAPH.CENTER + clear_paragraph_indent(p_img) + + stream = crop_image_to_ratio( + path, original_cx, original_cy) + p_img.add_run().add_picture(stream, width=Emu(original_cx), height=Emu(original_cy)) + print(f"插入图片: {name} -> {title}") + + # B. 插入名称 + cell_txt = big_table.cell(txt_row_num, col_idx) + p_txt = cell_txt.paragraphs[0] + p_txt.alignment = WD_ALIGN_PARAGRAPH.CENTER + clear_paragraph_indent(p_txt) + + run_txt = p_txt.add_run(title) + run_txt.font.size = Pt(10.5) + run_txt.font.name = '宋体' + run_txt._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体') + + # 5. 删除占位段落并跳出 blip 循环 + p._element.getparent().remove(p._element) + break + + +def getImgNameDict(reportContent_list): + """获取图片名称字典""" + imgName_dict = {} + for item_dict in reportContent_list: + if item_dict["type"] == "img": + key_name = item_dict.get("key", "") + if key_name not in imgName_dict: + imgName_dict[key_name] = [] + img_base64_list = item_dict.get("value", []) + if img_base64_list: + for img_base64_dict in img_base64_list: + imgTemp_dict = {} + picName = img_base64_dict.get("title", "") + picPath = img_base64_dict.get("path", "") + imgTemp_dict[picPath] = picName + imgName_dict[key_name].append(imgTemp_dict) + return imgName_dict + + +def generateWord(projectPath, outputDocxPath, reportContent_list): + """根据JSON数据生成报告文件""" + print(f"Info:开始生成Word报告...") + doc = Document(outputDocxPath) + + # 把base64转成图片 + # img_num, imgName_dict = base64ToImg(projectPath, reportContent_list) + # print(f"Info:共转换{img_num}张图片") + + # 获取图片地址 + imgName_dict = getImgNameDict(reportContent_list) + img_keys = list(imgName_dict.keys()) + + data_dict, table_num = getDataDict(reportContent_list) + + # image_dict = {"image1.png": "pic1", + # "image2.png": "pic2" + # } + image_dict = { + f"image{i+1}.png": img_keys[i] for i in range(len(img_keys))} + + print(f"Info:图片分类:{img_keys}, 与模版的对应关系:{image_dict}") + + # 处理段落和表格 + for para in doc.paragraphs: + if "$" in para.text: + replaceParagraph(doc, para, data_dict) + # 处理图片 + process_images_in_paragraphs( + doc, image_dict, doc.paragraphs, imgName_dict, projectPath) + + # 保存文档 + doc.save(outputDocxPath) + print(f"Info:Word文档已创建: {outputDocxPath}") + + +if __name__ == "__main__": + + # currentPath = os.getcwd() + # projectPath = f"{currentPath}/project/generateReport" + # templatePath = f"{currentPath}/template/仿真报告标准模板_v1.0.docx" + # pythonLogPath = os.path.dirname(projectPath) + + projectPath = sys.argv[1] + templatePath = sys.argv[2] + pythonLogPath = "/opt/script" + + # 输出log日志 + # 日志文件路径 + log_fileDir = f"{pythonLogPath}/pythonLog" + if not os.path.exists(log_fileDir): + os.makedirs(log_fileDir) + # log文件用日期命名 + log_file = f"{log_fileDir}/{datetime.now().strftime('%Y%m%d')}.log" + # 使用上下文管理器重定向所有输出到文件 + with open(log_file, 'a', encoding='utf-8') as log_file_obj: + # 重定向标准输出和标准错误 + original_stdout = sys.stdout + original_stderr = sys.stderr + + # 创建Tee对象,同时输出到文件和原控制台 + tee = Tee(log_file_obj, original_stdout) + sys.stdout = tee + sys.stderr = tee + + # 在日志开头添加时间戳 + print("=" * 60) + print(f"生成报告 程序开始时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print(f"日志文件: {log_file}") + print(f"项目路径: {projectPath}") + print("=" * 60) + + try: + # 获取项目信息数据 + reportContentPath = f"{projectPath}/reportContent.json" + reportContent_list = getJsonData(reportContentPath) + # # 判断是否批量 + # isBatch = False + # if isBatch: + # projectPath = f"{projectPath}/{loacase}" + + # # 是否自动出报告 + # autoExport = projectInfo_dict.get("autoExport", False) + # if autoExport: + # exportWord(projectPath, templatePath, loacase=loacase) + # else: + # exportWordByUser(projectPath, templatePath, loacase=loacase) + + # 拷贝模板文件到项目目录下 + outputDocxPath = f"{projectPath}/report_{datetime.now().strftime('%Y%m%d')}.docx" + shutil.copy(templatePath, outputDocxPath) + generateWord(projectPath, outputDocxPath, reportContent_list) + + print("\n" + "=" * 60) + print(f"程序结束时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print("程序执行成功") + print("=" * 60) + + except Exception as e: + print(f"\n程序执行出错: {str(e)}", file=sys.stderr) + traceback.print_exc() + print("=" * 60) + print(f"程序异常结束时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print("=" * 60) + raise + finally: + # 恢复标准输出 + sys.stdout = original_stdout + sys.stderr = original_stderr diff --git a/project/src/main/java/com/sdm/project/controller/SimulationRunController.java b/project/src/main/java/com/sdm/project/controller/SimulationRunController.java index f7ec1e4c..b3ad8f29 100644 --- a/project/src/main/java/com/sdm/project/controller/SimulationRunController.java +++ b/project/src/main/java/com/sdm/project/controller/SimulationRunController.java @@ -222,6 +222,17 @@ public class SimulationRunController implements ISimulationRunFeignClient { runService.generateReport(req,response); } + /** + * 生成自动化报告(新) + * + * @return + */ + @PostMapping("/generateNewReport") + @Operation(summary = "生成自动化报告", description = "生成自动化报告") + public void generateNewReport(@RequestBody SpdmReportReq req, HttpServletResponse response) { + runService.generateNewReport(req,response); + } + /** * 编辑报告模板生成报告 * @@ -229,8 +240,19 @@ public class SimulationRunController implements ISimulationRunFeignClient { */ @PostMapping("/editReport") @Operation(summary = "编辑报告模板生成报告", description = "编辑报告模板生成报告") - public void editReport(@RequestBody EditReportReq req, HttpServletResponse response) { - runService.editReport(req,response); + public void editReport(@RequestBody EditReportReq req) { + runService.editReport(req); + } + + /** + * 编辑报告模板生成报告并下载 + * + * @return + */ + @PostMapping("/editReportAndDownload") + @Operation(summary = "编辑报告模板生成报告", description = "编辑报告模板生成报告") + public void editReportAndDownload(@RequestBody EditReportReq req, HttpServletResponse response) { + runService.editReportAndDownload(req,response); } /** diff --git a/project/src/main/java/com/sdm/project/service/ISimulationRunService.java b/project/src/main/java/com/sdm/project/service/ISimulationRunService.java index c96e5ee6..3aeaf650 100644 --- a/project/src/main/java/com/sdm/project/service/ISimulationRunService.java +++ b/project/src/main/java/com/sdm/project/service/ISimulationRunService.java @@ -69,7 +69,11 @@ public interface ISimulationRunService extends IService { void generateReport(SpdmReportReq req, HttpServletResponse response); - void editReport(EditReportReq req, HttpServletResponse response); + void generateNewReport(SpdmReportReq req, HttpServletResponse response); + + void editReport(EditReportReq req); + + void editReportAndDownload(EditReportReq req, HttpServletResponse response); /** diff --git a/project/src/main/java/com/sdm/project/service/impl/SimulationRunServiceImpl.java b/project/src/main/java/com/sdm/project/service/impl/SimulationRunServiceImpl.java index a7577ded..86b4729b 100644 --- a/project/src/main/java/com/sdm/project/service/impl/SimulationRunServiceImpl.java +++ b/project/src/main/java/com/sdm/project/service/impl/SimulationRunServiceImpl.java @@ -4,7 +4,10 @@ import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import com.sdm.common.common.SdmResponse; @@ -81,6 +84,7 @@ import java.nio.file.StandardCopyOption; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; +import java.util.function.UnaryOperator; import java.util.stream.Collectors; import static com.sdm.common.service.BaseService.generateUuid; @@ -158,8 +162,8 @@ public class SimulationRunServiceImpl extends ServiceImpl filePathMap = new HashMap<>(); + List imageFileIdList = extractImagePaths(reportContent); + if (CollectionUtils.isNotEmpty(imageFileIdList)) { + // 根据文件id下载文件到临时目录 + for (String fileId : imageFileIdList) { + SdmResponse sdmResponse = dataFeignClient.downloadFileToLocal(Long.valueOf(fileId), TEMP_REPORT_PATH + randomId); + if (sdmResponse.isSuccess()) { + filePathMap.put(fileId, (String) sdmResponse.getData()); + } + } + } + // 回设图片文件路径 + reportContent = replaceFileIdsWithPaths(reportContent, filePathMap); + // 前端参数写入临时目录 FileOutputStream projectInfoOutputStream = null; try { @@ -1392,23 +1410,25 @@ public class SimulationRunServiceImpl extends ServiceImpl command = new ArrayList<>(); + command.add("python"); + command.add("/opt/script/newExportWord.py"); + command.add(TEMP_REPORT_PATH + randomId); + command.add("/opt/script/template/仿真报告标准模板_v1.0.docx"); + String commands = String.join(" ", command); // 调用脚本 - log.info("调用脚本中。。。。。。"); - log.info("command:" + commands); - List result = new ArrayList<>(); + log.info("调用脚本中。。。。。。command:{}", commands); int runningStatus = -1; try { log.info("开始同步执行脚本"); Process process = Runtime.getRuntime().exec(commands); - log.info("准备获取脚本输出"); log.info("开始获取脚本输出"); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = reader.readLine()) != null) { log.info("executePython:" + line); - result.add(line); } log.info("脚本执行完成"); runningStatus = process.waitFor(); @@ -1427,8 +1447,10 @@ public class SimulationRunServiceImpl extends ServiceImpl extractImagePaths(String jsonData) { + List imagePaths = new ArrayList<>(); + ObjectMapper objectMapper = new ObjectMapper(); + + try { + JsonNode rootNode = objectMapper.readTree(jsonData); + for (JsonNode node : rootNode) { + // 检查是否为图片类型 + if (node.has("type") && "img".equals(node.get("type").asText())) { + JsonNode valueNode = node.get("value"); + if (valueNode != null && valueNode.isArray()) { + // 遍历数组获取所有图片路径 + for (JsonNode imageNode : valueNode) { + if (imageNode.has("path")) { + imagePaths.add(imageNode.get("path").asText()); + } + } + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return imagePaths; + } + + /** + * 处理所有图片路径,将下载到本地的路径更新到原JSON的path下 + */ + public String replaceFileIdsWithPaths(String jsonData, Map filePathMap) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + JsonNode root = objectMapper.readTree(jsonData); + for (JsonNode item : root) { + if (item.has("type") && "img".equals(item.get("type").asText())) { + replaceFileIdsInImageArray((ObjectNode) item, filePathMap); + } + } + return objectMapper.writeValueAsString(root); + } catch (Exception e) { + throw new RuntimeException("文件路径替换失败", e); + } + } + + private void replaceFileIdsInImageArray(ObjectNode itemNode, Map filePathMap) { + JsonNode value = itemNode.get("value"); + if (value != null && value.isArray()) { + ArrayNode images = (ArrayNode) value; + + for (int i = 0; i < images.size(); i++) { + ObjectNode image = (ObjectNode) images.get(i); + if (image.has("path")) { + String fileId = image.get("path").asText(); + + // 如果fileId在map中,则替换为新路径 + if (filePathMap.containsKey(fileId)) { + String newPath = filePathMap.get(fileId); + image.put("path", newPath); + } + } + } + } + } + @Override - public void editReport(EditReportReq req, HttpServletResponse response) { + public void editReport(EditReportReq req) { + log.info("编辑报告参数为:{}", req); + SimulationRun simulationRun = this.lambdaQuery().eq(SimulationRun::getUuid, req.getRunId()).one(); + if (simulationRun != null) { + String reportContent = req.getReportContent(); + // 算例绑定报告模板 + simulationRun.setReportTemplate(req.getReportTemplate()); + simulationRun.setReportContent(reportContent); + this.updateById(simulationRun); + + String randomId = RandomUtil.generateString(16); + // 创建临时文件夹 + Path folder = Paths.get(TEMP_REPORT_PATH + randomId); + if (!Files.exists(folder) || !Files.isDirectory(folder)) { + if (!new File(TEMP_REPORT_PATH + randomId).mkdir()) { + log.error("创建临时文件夹:{}失败",TEMP_REPORT_PATH + randomId); + throw new RuntimeException("生成报告失败,原因为:创建临时文件夹失败"); + } + } + log.info("临时路径为:{}", randomId); + + // 根据文件id下载文件到临时目录 + SdmResponse reportResponse = reportFeignClient.queryReportTemplateInfo(req.getReportTemplate()); + if (reportResponse.isSuccess()) { + Long reportTemplateFileId = reportResponse.getData().getFileId(); + // 获取报告模板名称 + GetFileBaseInfoReq getFileBaseInfoReq = new GetFileBaseInfoReq(); + getFileBaseInfoReq.setFileId(reportTemplateFileId); + SdmResponse fileBaseInfoResp = dataFeignClient.getFileBaseInfo(getFileBaseInfoReq); + String originalName = fileBaseInfoResp.getData().getOriginalName(); + // 下载到本地临时目录 + dataFeignClient.downloadFileToLocal(reportTemplateFileId, TEMP_REPORT_PATH + randomId); + + // 构建python命令 + List command = new ArrayList<>(); + command.add("python"); + command.add("/opt/script/modifyReport.py"); +// command.add(TEMP_REPORT_PATH + File.separator +"modifyReport.py"); + command.add(TEMP_REPORT_PATH + randomId); + command.add(TEMP_REPORT_PATH + randomId + File.separator + originalName); + String commands = String.join(" ", command); + + // 前端参数写入临时目录 + FileOutputStream projectInfoOutputStream = null; + try { + projectInfoOutputStream = new FileOutputStream(TEMP_REPORT_PATH + randomId + File.separator + "reportContent.json"); + projectInfoOutputStream.write(reportContent.getBytes(StandardCharsets.UTF_8)); + projectInfoOutputStream.flush(); + projectInfoOutputStream.close(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + // 调用脚本 + log.info("执行 Python 命令: {}", commands); + int runningStatus = -1; + try { + log.info("开始同步执行脚本"); + Process process = Runtime.getRuntime().exec(commands); + log.info("开始获取脚本输出"); + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + String line; + while ((line = reader.readLine()) != null) { + log.info("executePython:" + line); + } + log.info("脚本执行完成"); + runningStatus = process.waitFor(); + log.info("脚本运行状态:" + runningStatus); + } catch (IOException | InterruptedException e) { + log.error("执行脚本失败:" + e); + return; + } + if (runningStatus != 0) { + log.error("执行脚本失败"); + return; + } else { + log.info(commands + "执行脚本完成!"); + } + byte[] fileData = null; + try { + // 获取临时路径中脚本生成的报告 + String reportName = "report_" + + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + + ".docx"; + FileInputStream fileInputStream = new FileInputStream(TEMP_REPORT_PATH + randomId + File.separator + reportName); + fileData = fileInputStream.readAllBytes(); + + // 上传到算例下的报告文件夹下 + KeyResultReq resultReq = new KeyResultReq(); + resultReq.setKeyResultType(KeyResultTypeEnum.DOCUMENT.getKeyResultType()); + resultReq.setRunId(req.getRunId()); + resultReq.setName(reportName); + // 创建临时MultipartFile + MockMultipartFile multipartFile = new MockMultipartFile( + reportName, + reportName, + "application/json", + fileData); + resultReq.setFile(multipartFile); + resultReq.setFileName(reportName); + resultReq.setFileType(FileBizTypeEnum.REPORT_FILE.getValue()); + SdmResponse sdmResponse = addSimulationKeyResult(resultReq); + if (!sdmResponse.isSuccess()) { + throw new RuntimeException("生成自动化报告上传报告结果目录失败"); + } + fileInputStream.close(); + } catch (Exception ex) { + log.error("生成自动化报告失败:{}", ex.getMessage()); + throw new RuntimeException("生成自动化报告失败"); + } + // 删除临时路径 + log.info("删除临时路径:{},中。。。。。。", randomId); + deleteFolder(new File(TEMP_REPORT_PATH + randomId)); + } + } + } + + @Override + public void editReportAndDownload(EditReportReq req, HttpServletResponse response) { log.info("编辑报告参数为:{}", req); SimulationRun simulationRun = this.lambdaQuery().eq(SimulationRun::getUuid, req.getRunId()).one(); if (simulationRun != null) { diff --git a/project/src/main/resources/application-dev-190.yml b/project/src/main/resources/application-dev-190.yml index e023e67d..b0363964 100644 --- a/project/src/main/resources/application-dev-190.yml +++ b/project/src/main/resources/application-dev-190.yml @@ -124,6 +124,7 @@ security: - /run/getSimulationKeyResultFileIds - /run/generateReportInternal - /dataManager/tree/node/listUserByIds + - /demand/queryTodoList #logging: # config: ./config/logback.xml diff --git a/project/src/main/resources/application-dev-65.yml b/project/src/main/resources/application-dev-65.yml index 6361d18b..d434ef02 100644 --- a/project/src/main/resources/application-dev-65.yml +++ b/project/src/main/resources/application-dev-65.yml @@ -124,6 +124,7 @@ security: - /run/getSimulationKeyResultFileIds - /run/generateReportInternal - /dataManager/tree/node/listUserByIds + - /demand/queryTodoList #logging: # config: ./config/logback.xml diff --git a/project/src/main/resources/application-local.yml b/project/src/main/resources/application-local.yml index 1e740a14..fd2b7da9 100644 --- a/project/src/main/resources/application-local.yml +++ b/project/src/main/resources/application-local.yml @@ -130,6 +130,7 @@ security: - /run/getSimulationKeyResultFileIds - /run/generateReportInternal - /dataManager/tree/node/listUserByIds + - /demand/queryTodoList YA: backend: diff --git a/project/src/main/resources/application-lyric.yml b/project/src/main/resources/application-lyric.yml index 093b611d..37d7aaa1 100644 --- a/project/src/main/resources/application-lyric.yml +++ b/project/src/main/resources/application-lyric.yml @@ -125,6 +125,7 @@ security: - /run/generateReportInternal - /dataManager/tree/node/listUserByIds - /node/updateApprovalStatus + - /demand/queryTodoList #logging: # config: ./config/logback.xml