feat: word文档调整暂存

dev_na
puz 2026-07-13 18:07:57 +08:00
parent 0aaa3a1cde
commit 58674642e7
2 changed files with 210 additions and 159 deletions

View File

@ -16,8 +16,6 @@ import com.unisbase.service.TenantManagementService;
import lombok.RequiredArgsConstructor;
import org.apache.pdfbox.pdmodel.graphics.blend.BlendMode;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
@ -38,6 +36,7 @@ import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@ -48,8 +47,6 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
@RequiredArgsConstructor
@ -96,18 +93,23 @@ public class MeetingExportServiceImpl implements MeetingExportService {
Files.createDirectories(exportDir);
Path exportPath = exportDir.resolve("summary." + ext);
boolean isPdf = "pdf".equals(ext);
boolean isPdf = "pdf".equals(ext);
boolean needRegenerate = !Files.exists(exportPath)
|| Files.getLastModifiedTime(exportPath).toMillis() < Files.getLastModifiedTime(summarySourcePath).toMillis();
if (!needRegenerate && "docx".equals(ext) && !isCurrentWordExport(exportPath)) {
needRegenerate = true;
}
byte[] baseBytes;
byte[] baseBytes;
if (needRegenerate) {
String markdown = Files.readString(summarySourcePath, StandardCharsets.UTF_8);
meetingDetail.setSummaryContent(meetingSummaryFileService.stripFrontMatter(markdown));
baseBytes = "docx".equals(ext) ? buildWordBytes(meetingDetail) : buildPdfBytes(meetingDetail);
Files.write(exportPath, baseBytes);
baseBytes = "docx".equals(ext)
? new MeetingWordDocumentBuilder().build(meetingDetail)
: buildPdfBytes(meetingDetail);
Files.write(exportPath, baseBytes);
} else {
baseBytes = Files.readAllBytes(exportPath);
baseBytes = Files.readAllBytes(exportPath);
}
byte[] bytes = isPdf
@ -120,44 +122,17 @@ public class MeetingExportServiceImpl implements MeetingExportService {
}
}
private byte[] buildWordBytes(MeetingVO meeting) throws IOException {
try (XWPFDocument document = new XWPFDocument();
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
XWPFParagraph title = document.createParagraph();
XWPFRun titleRun = title.createRun();
titleRun.setBold(true);
titleRun.setFontSize(16);
titleRun.setText((meeting.getTitle() == null ? "Meeting" : meeting.getTitle()) + " - AI Summary");
XWPFParagraph timeP = document.createParagraph();
timeP.createRun().setText("Meeting Time: " + String.valueOf(meeting.getMeetingTime()));
XWPFParagraph hostP = document.createParagraph();
hostP.createRun().setText("Host: " + (meeting.getHostName() == null ? "" : meeting.getHostName()));
XWPFParagraph participantsP = document.createParagraph();
participantsP.createRun().setText("Participants: " + (meeting.getParticipants() == null ? "" : meeting.getParticipants()));
document.createParagraph();
for (MdBlock block : parseMarkdownBlocks(meeting.getSummaryContent())) {
XWPFParagraph p = document.createParagraph();
if (block.type == MdType.HEADING) {
int size = Math.max(12, 18 - (block.level - 1) * 2);
appendMarkdownRuns(p, block.text, true, size);
} else if (block.type == MdType.LIST) {
p.setIndentationLeft(360);
XWPFRun bullet = p.createRun();
bullet.setFontSize(12);
bullet.setText("- ");
appendMarkdownRuns(p, block.text, false, 12);
} else {
appendMarkdownRuns(p, block.text, false, 12);
}
}
document.write(out);
return out.toByteArray();
private boolean isCurrentWordExport(Path exportPath) {
try (InputStream input = Files.newInputStream(exportPath);
XWPFDocument document = new XWPFDocument(input)) {
var property = document.getProperties()
.getCustomProperties()
.getProperty(MeetingWordDocumentBuilder.EXPORT_VERSION_PROPERTY);
return property != null
&& property.isSetLpwstr()
&& MeetingWordDocumentBuilder.EXPORT_VERSION.equals(property.getLpwstr());
} catch (Exception ignored) {
return false;
}
}
@ -419,116 +394,4 @@ public class MeetingExportServiceImpl implements MeetingExportService {
}
}
private void appendMarkdownRuns(XWPFParagraph paragraph, String text, boolean defaultBold, int size) {
String input = text == null ? "" : text;
Matcher matcher = Pattern.compile("\\*\\*(.+?)\\*\\*").matcher(input);
int start = 0;
while (matcher.find()) {
String normal = toPlainInline(input.substring(start, matcher.start()));
if (!normal.isEmpty()) {
XWPFRun run = paragraph.createRun();
run.setBold(defaultBold);
run.setFontSize(size);
run.setText(normal);
}
String boldText = toPlainInline(matcher.group(1));
if (!boldText.isEmpty()) {
XWPFRun run = paragraph.createRun();
run.setBold(true);
run.setFontSize(size);
run.setText(boldText);
}
start = matcher.end();
}
String tail = toPlainInline(input.substring(start));
if (!tail.isEmpty()) {
XWPFRun run = paragraph.createRun();
run.setBold(defaultBold);
run.setFontSize(size);
run.setText(tail);
}
}
private List<MdBlock> parseMarkdownBlocks(String markdown) {
List<MdBlock> blocks = new ArrayList<>();
if (markdown == null || markdown.trim().isEmpty()) {
return blocks;
}
String[] lines = markdown.replace("\r\n", "\n").split("\n");
StringBuilder paragraph = new StringBuilder();
for (String raw : lines) {
String line = raw == null ? "" : raw.trim();
if (line.isEmpty()) {
flushParagraph(blocks, paragraph);
continue;
}
if (line.startsWith("#")) {
flushParagraph(blocks, paragraph);
int level = 0;
while (level < line.length() && line.charAt(level) == '#') {
level++;
}
level = Math.min(level, 6);
blocks.add(new MdBlock(MdType.HEADING, level, line.substring(level).trim()));
continue;
}
if (line.startsWith("- ") || line.startsWith("* ")) {
flushParagraph(blocks, paragraph);
blocks.add(new MdBlock(MdType.LIST, 0, line.substring(2).trim()));
continue;
}
Matcher ordered = Pattern.compile("^\\d+\\.\\s+(.*)$").matcher(line);
if (ordered.find()) {
flushParagraph(blocks, paragraph);
blocks.add(new MdBlock(MdType.LIST, 0, ordered.group(1).trim()));
continue;
}
if (paragraph.length() > 0) {
paragraph.append(' ');
}
paragraph.append(line);
}
flushParagraph(blocks, paragraph);
return blocks;
}
private void flushParagraph(List<MdBlock> blocks, StringBuilder paragraph) {
if (paragraph.length() > 0) {
blocks.add(new MdBlock(MdType.PARAGRAPH, 0, paragraph.toString()));
paragraph.setLength(0);
}
}
private String toPlainInline(String input) {
if (input == null) {
return "";
}
return input
.replaceAll("`([^`]+)`", "$1")
.replaceAll("\\*\\*(.*?)\\*\\*", "$1")
.replaceAll("\\*(.*?)\\*", "$1")
.replaceAll("\\[(.*?)]\\((.*?)\\)", "$1");
}
private enum MdType {
HEADING,
LIST,
PARAGRAPH
}
private static class MdBlock {
private final MdType type;
private final int level;
private final String text;
private MdBlock(MdType type, int level, String text) {
this.type = type;
this.level = level;
this.text = text;
}
}
}

View File

@ -0,0 +1,188 @@
package com.imeeting.service.biz.impl;
import com.imeeting.dto.biz.MeetingVO;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
final class MeetingWordDocumentBuilder {
static final String EXPORT_VERSION_PROPERTY = "iMeetingWordExportVersion";
static final String EXPORT_VERSION = "3";
private static final Pattern MIXED_CONTENT_SPACING = Pattern.compile(
"(?<=\\p{IsHan})[\\h\\u200B]+(?=[\\p{IsLatin}\\p{N}])"
+ "|(?<=[\\p{IsLatin}\\p{N}])[\\h\\u200B]+(?=\\p{IsHan})"
);
byte[] build(MeetingVO meeting) throws IOException {
try (XWPFDocument document = new XWPFDocument();
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
document.getProperties().getCustomProperties()
.addProperty(EXPORT_VERSION_PROPERTY, EXPORT_VERSION);
XWPFParagraph title = document.createParagraph();
XWPFRun titleRun = title.createRun();
titleRun.setBold(true);
titleRun.setFontSize(16);
titleRun.setText((meeting.getTitle() == null ? "Meeting" : meeting.getTitle()) + " - AI Summary");
XWPFParagraph timeP = document.createParagraph();
timeP.createRun().setText("Meeting Time: " + String.valueOf(meeting.getMeetingTime()));
XWPFParagraph hostP = document.createParagraph();
hostP.createRun().setText("Host: " + (meeting.getHostName() == null ? "" : meeting.getHostName()));
XWPFParagraph participantsP = document.createParagraph();
participantsP.createRun().setText("Participants: " + (meeting.getParticipants() == null ? "" : meeting.getParticipants()));
document.createParagraph();
String summaryContent = normalizeMixedContentSpacing(meeting.getSummaryContent());
for (MdBlock block : parseMarkdownBlocks(summaryContent)) {
XWPFParagraph paragraph = document.createParagraph();
if (block.type == MdType.HEADING) {
int size = Math.max(12, 18 - (block.level - 1) * 2);
appendMarkdownRuns(paragraph, block.text, true, size);
} else if (block.type == MdType.LIST) {
paragraph.setIndentationLeft(360);
XWPFRun bullet = paragraph.createRun();
bullet.setFontSize(12);
bullet.setText("- ");
appendMarkdownRuns(paragraph, block.text, false, 12);
} else {
appendMarkdownRuns(paragraph, block.text, false, 12);
}
}
document.write(out);
return out.toByteArray();
}
}
private String normalizeMixedContentSpacing(String markdown) {
if (markdown == null) {
return null;
}
return MIXED_CONTENT_SPACING.matcher(markdown).replaceAll("");
}
private void appendMarkdownRuns(XWPFParagraph paragraph, String text, boolean defaultBold, int size) {
String input = text == null ? "" : text;
Matcher matcher = Pattern.compile("\\*\\*(.+?)\\*\\*").matcher(input);
int start = 0;
while (matcher.find()) {
String normal = toPlainInline(input.substring(start, matcher.start()));
if (!normal.isEmpty()) {
XWPFRun run = paragraph.createRun();
run.setBold(defaultBold);
run.setFontSize(size);
run.setText(normal);
}
String boldText = toPlainInline(matcher.group(1));
if (!boldText.isEmpty()) {
XWPFRun run = paragraph.createRun();
run.setBold(true);
run.setFontSize(size);
run.setText(boldText);
}
start = matcher.end();
}
String tail = toPlainInline(input.substring(start));
if (!tail.isEmpty()) {
XWPFRun run = paragraph.createRun();
run.setBold(defaultBold);
run.setFontSize(size);
run.setText(tail);
}
}
private List<MdBlock> parseMarkdownBlocks(String markdown) {
List<MdBlock> blocks = new ArrayList<>();
if (markdown == null || markdown.trim().isEmpty()) {
return blocks;
}
String[] lines = markdown.replace("\r\n", "\n").split("\n");
StringBuilder paragraph = new StringBuilder();
for (String raw : lines) {
String line = raw == null ? "" : raw.trim();
if (line.isEmpty()) {
flushParagraph(blocks, paragraph);
continue;
}
if (line.startsWith("#")) {
flushParagraph(blocks, paragraph);
int level = 0;
while (level < line.length() && line.charAt(level) == '#') {
level++;
}
level = Math.min(level, 6);
blocks.add(new MdBlock(MdType.HEADING, level, line.substring(level).trim()));
continue;
}
if (line.startsWith("- ") || line.startsWith("* ")) {
flushParagraph(blocks, paragraph);
blocks.add(new MdBlock(MdType.LIST, 0, line.substring(2).trim()));
continue;
}
Matcher ordered = Pattern.compile("^\\d+\\.\\s+(.*)$").matcher(line);
if (ordered.find()) {
flushParagraph(blocks, paragraph);
blocks.add(new MdBlock(MdType.LIST, 0, ordered.group(1).trim()));
continue;
}
if (paragraph.length() > 0) {
paragraph.append(' ');
}
paragraph.append(line);
}
flushParagraph(blocks, paragraph);
return blocks;
}
private void flushParagraph(List<MdBlock> blocks, StringBuilder paragraph) {
if (paragraph.length() > 0) {
blocks.add(new MdBlock(MdType.PARAGRAPH, 0, paragraph.toString()));
paragraph.setLength(0);
}
}
private String toPlainInline(String input) {
if (input == null) {
return "";
}
return input
.replaceAll("`([^`]+)`", "$1")
.replaceAll("\\*\\*(.*?)\\*\\*", "$1")
.replaceAll("\\*(.*?)\\*", "$1")
.replaceAll("\\[(.*?)]\\((.*?)\\)", "$1");
}
private enum MdType {
HEADING,
LIST,
PARAGRAPH
}
private static class MdBlock {
private final MdType type;
private final int level;
private final String text;
private MdBlock(MdType type, int level, String text) {
this.type = type;
this.level = level;
this.text = text;
}
}
}