fix:生成报告
This commit is contained in:
@@ -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("下载文件到本地失败");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ public interface IDataFeignClient {
|
||||
SdmResponse<FileMetadataInfoResp> 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);
|
||||
|
||||
/**
|
||||
* 下载文件夹
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -366,7 +366,7 @@ public interface IDataFileService {
|
||||
* @param fileId 文件id
|
||||
* @param basePath 临时目录路径
|
||||
*/
|
||||
void downloadFileToLocal(Long fileId,String basePath);
|
||||
SdmResponse downloadFileToLocal(Long fileId,String basePath);
|
||||
|
||||
/**
|
||||
* 批量下载指定文件夹到本地目录
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -131,6 +131,8 @@ security:
|
||||
- /data/initNewTenant
|
||||
- /data/getMinioPresignedUrl
|
||||
- /data/getPublicDownloadUrl
|
||||
- /data/updatePermission
|
||||
- /data/createDir
|
||||
|
||||
data:
|
||||
storage-monitor:
|
||||
|
||||
@@ -131,6 +131,8 @@ security:
|
||||
- /data/initNewTenant
|
||||
- /data/getMinioPresignedUrl
|
||||
- /data/getPublicDownloadUrl
|
||||
- /data/updatePermission
|
||||
- /data/createDir
|
||||
|
||||
data:
|
||||
storage-monitor:
|
||||
|
||||
@@ -136,6 +136,8 @@ security:
|
||||
- /data/initNewTenant
|
||||
- /data/getMinioPresignedUrl
|
||||
- /data/getPublicDownloadUrl
|
||||
- /data/updatePermission
|
||||
- /data/createDir
|
||||
|
||||
data:
|
||||
storage-monitor:
|
||||
|
||||
@@ -131,6 +131,8 @@ security:
|
||||
- /data/initNewTenant
|
||||
- /data/getMinioPresignedUrl
|
||||
- /data/getPublicDownloadUrl
|
||||
- /data/updatePermission
|
||||
- /data/createDir
|
||||
|
||||
data:
|
||||
storage-monitor:
|
||||
|
||||
934
project/newExportWord.py
Normal file
934
project/newExportWord.py
Normal file
@@ -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 内容(以 <?xml 或 < 开头)
|
||||
if not xml_content.strip().startswith(b'<?xml') and not xml_content.strip().startswith(b'<'):
|
||||
continue
|
||||
try:
|
||||
try:
|
||||
content_str = xml_content.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
# 检查是否包含 XML 声明或根元素
|
||||
if not content_str.strip().startswith('<?xml') and not content_str.strip().startswith('<'):
|
||||
continue
|
||||
|
||||
root = etree.fromstring(xml_content)
|
||||
# 查找文本框
|
||||
textboxes = root.xpath('//wps:txbx', namespaces=namespaces)
|
||||
part_modified = False # 当前部件是否被修改
|
||||
for textbox in textboxes:
|
||||
# 提取文本内容
|
||||
text_elements = textbox.xpath(
|
||||
'.//w:t', namespaces=namespaces)
|
||||
for elem in text_elements:
|
||||
# print(f"原始文本: {elem.text}")
|
||||
if elem.text and "$" in elem.text:
|
||||
keyName = elem.text.split("$")[1]
|
||||
value = data_dict.get(keyName, None)
|
||||
if value is not None:
|
||||
elem.text = elem.text.replace(
|
||||
f"${keyName}", str(value))
|
||||
print(f"文本框替换: ${keyName} -> {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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -69,7 +69,11 @@ public interface ISimulationRunService extends IService<SimulationRun> {
|
||||
|
||||
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);
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<SimulationRunMapper, S
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
private static final String TEMP_REPORT_PATH = "/opt/report/";
|
||||
// private static final String TEMP_REPORT_PATH = System.getProperty("user.dir") + File.separator + "project";
|
||||
// private static final String TEMP_REPORT_PATH = "/opt/report/";
|
||||
private static final String TEMP_REPORT_PATH = System.getProperty("user.dir") + File.separator + "project";
|
||||
|
||||
|
||||
private static final String TEMPLATE_PATH = " /opt/script/template ";
|
||||
@@ -1368,7 +1372,7 @@ public class SimulationRunServiceImpl extends ServiceImpl<SimulationRunMapper, S
|
||||
deleteFolder(new File(TEMP_REPORT_PATH + randomId));
|
||||
}
|
||||
|
||||
public void generateReport2(SpdmReportReq req, HttpServletResponse response) {
|
||||
public void generateNewReport(SpdmReportReq req, HttpServletResponse response) {
|
||||
log.info("生成自动化报告参数为:{}", req);
|
||||
String randomId = RandomUtil.generateString(16);
|
||||
Path folder = Paths.get(TEMP_REPORT_PATH + randomId);
|
||||
@@ -1381,6 +1385,20 @@ public class SimulationRunServiceImpl extends ServiceImpl<SimulationRunMapper, S
|
||||
log.info("临时路径为:{}", randomId);
|
||||
|
||||
String reportContent = req.getReportContent();
|
||||
Map<String, String> filePathMap = new HashMap<>();
|
||||
List<String> 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<SimulationRunMapper, S
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
String commands = "python /opt/script/exportWord.py " + TEMP_REPORT_PATH + randomId + File.separator + TEMPLATE_PATH + "Analyse";
|
||||
// 构建python命令
|
||||
List<String> 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<String> 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<SimulationRunMapper, S
|
||||
if (response != null) {
|
||||
try {
|
||||
// 获取临时路径中脚本生成的报告
|
||||
String reportName = "report.docx";
|
||||
FileInputStream fileInputStream = new FileInputStream(TEMP_REPORT_PATH + randomId + File.separator + "report" + File.separator + "report.docx");
|
||||
String reportName = "report_" +
|
||||
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) +
|
||||
".docx";
|
||||
FileInputStream fileInputStream = new FileInputStream(TEMP_REPORT_PATH + randomId + File.separator + "report.docx");
|
||||
fileData = fileInputStream.readAllBytes();
|
||||
|
||||
// 上传到算例下的报告文件夹下
|
||||
@@ -1465,26 +1487,198 @@ public class SimulationRunServiceImpl extends ServiceImpl<SimulationRunMapper, S
|
||||
throw new RuntimeException("生成自动化报告失败");
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotBlank(req.getFlowPath()) && fileData != null) {
|
||||
// 将生成的报告上传到flowPath路径下
|
||||
// 写入响应流
|
||||
FileOutputStream outputStream = null;
|
||||
try {
|
||||
outputStream = new FileOutputStream(req.getFlowPath());
|
||||
outputStream.write(fileData);
|
||||
outputStream.flush();
|
||||
outputStream.close();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
// 删除临时路径
|
||||
log.info("删除临时路径:{},中。。。。。。", randomId);
|
||||
deleteFolder(new File(TEMP_REPORT_PATH + randomId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取前端传入的JSON参数,获得图片文件id,下载到本地
|
||||
*/
|
||||
public List<String> extractImagePaths(String jsonData) {
|
||||
List<String> 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<String, String> 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<String, String> 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<ReportTemplateResp> reportResponse = reportFeignClient.queryReportTemplateInfo(req.getReportTemplate());
|
||||
if (reportResponse.isSuccess()) {
|
||||
Long reportTemplateFileId = reportResponse.getData().getFileId();
|
||||
// 获取报告模板名称
|
||||
GetFileBaseInfoReq getFileBaseInfoReq = new GetFileBaseInfoReq();
|
||||
getFileBaseInfoReq.setFileId(reportTemplateFileId);
|
||||
SdmResponse<FileMetadataInfoResp> fileBaseInfoResp = dataFeignClient.getFileBaseInfo(getFileBaseInfoReq);
|
||||
String originalName = fileBaseInfoResp.getData().getOriginalName();
|
||||
// 下载到本地临时目录
|
||||
dataFeignClient.downloadFileToLocal(reportTemplateFileId, TEMP_REPORT_PATH + randomId);
|
||||
|
||||
// 构建python命令
|
||||
List<String> 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) {
|
||||
|
||||
@@ -124,6 +124,7 @@ security:
|
||||
- /run/getSimulationKeyResultFileIds
|
||||
- /run/generateReportInternal
|
||||
- /dataManager/tree/node/listUserByIds
|
||||
- /demand/queryTodoList
|
||||
#logging:
|
||||
# config: ./config/logback.xml
|
||||
|
||||
|
||||
@@ -124,6 +124,7 @@ security:
|
||||
- /run/getSimulationKeyResultFileIds
|
||||
- /run/generateReportInternal
|
||||
- /dataManager/tree/node/listUserByIds
|
||||
- /demand/queryTodoList
|
||||
#logging:
|
||||
# config: ./config/logback.xml
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@ security:
|
||||
- /run/getSimulationKeyResultFileIds
|
||||
- /run/generateReportInternal
|
||||
- /dataManager/tree/node/listUserByIds
|
||||
- /demand/queryTodoList
|
||||
|
||||
YA:
|
||||
backend:
|
||||
|
||||
@@ -125,6 +125,7 @@ security:
|
||||
- /run/generateReportInternal
|
||||
- /dataManager/tree/node/listUserByIds
|
||||
- /node/updateApprovalStatus
|
||||
- /demand/queryTodoList
|
||||
#logging:
|
||||
# config: ./config/logback.xml
|
||||
|
||||
|
||||
Reference in New Issue
Block a user