feat: word文档调整暂存2

dev_na
puz 2026-07-14 11:24:15 +08:00
parent 58674642e7
commit cd4b5365d6
2 changed files with 173 additions and 19 deletions

View File

@ -116,7 +116,7 @@ public class MeetingExportServiceImpl implements MeetingExportService {
? applyPdfWatermark(baseBytes, resolveWatermarkText(loginUser))
: baseBytes;
return new MeetingSummaryExportResult(bytes, contentType, safeTitle + "-AI-Summary." + ext);
return new MeetingSummaryExportResult(bytes, contentType, safeTitle + "." + ext);
} catch (IOException ex) {
throw new RuntimeException("导出失败:" + ex.getMessage(), ex);
}

View File

@ -1,12 +1,21 @@
package com.imeeting.service.biz.impl;
import com.imeeting.dto.biz.MeetingVO;
import org.apache.poi.xwpf.usermodel.LineSpacingRule;
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageSz;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSectPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STPageOrientation;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
@ -15,7 +24,20 @@ import java.util.regex.Pattern;
final class MeetingWordDocumentBuilder {
static final String EXPORT_VERSION_PROPERTY = "iMeetingWordExportVersion";
static final String EXPORT_VERSION = "3";
static final String EXPORT_VERSION = "12";
private static final String DOCUMENT_FONT = "仿宋_GB2312";
private static final int BODY_FONT_SIZE = 14;
private static final int BODY_FIRST_LINE_INDENT = 640;
private static final int BODY_LINE_SPACING = 29;
private static final DateTimeFormatter MEETING_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
private static final Pattern BODY_STYLE_LIST_PREFIX = Pattern.compile(
"^\\*\\*([^*\\r\\n:]+?)(?:\\*\\*[\\h\\u200B]*([:])|([:])[\\h\\u200B]*\\*\\*)"
);
private static final Pattern FULLY_BOLD_LIST_ITEM = Pattern.compile(
"^\\*\\*([^*\\r\\n]+)\\*\\*$"
);
private static final Pattern QUOTE_PREFIX = Pattern.compile("^[>]+[\\h\\u200B]*");
private static final Pattern HORIZONTAL_RULE = Pattern.compile("^[-*_](?:[\\h\\u200B]*[-*_]){2,}$");
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})"
@ -26,46 +48,55 @@ final class MeetingWordDocumentBuilder {
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
document.getProperties().getCustomProperties()
.addProperty(EXPORT_VERSION_PROPERTY, EXPORT_VERSION);
applyPageLayout(document);
XWPFParagraph title = document.createParagraph();
title.setAlignment(ParagraphAlignment.CENTER);
title.setSpacingAfter(240);
XWPFRun titleRun = title.createRun();
titleRun.setBold(true);
titleRun.setFontSize(16);
titleRun.setText((meeting.getTitle() == null ? "Meeting" : meeting.getTitle()) + " - AI Summary");
titleRun.setFontSize(22);
titleRun.setText(meeting.getTitle() == null ? "Meeting" : meeting.getTitle());
XWPFParagraph timeP = document.createParagraph();
timeP.createRun().setText("Meeting Time: " + String.valueOf(meeting.getMeetingTime()));
appendMetadata(timeP, "会议时间:" + formatMeetingTime(meeting));
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();
XWPFParagraph separator = document.createParagraph();
separator.setSpacingAfter(120);
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);
applyHeadingParagraphStyle(paragraph);
int size = Math.max(12, 16 - (block.level - 1) * 2);
appendMarkdownRuns(paragraph, block.text, true, size);
} else if (block.type == MdType.QUOTE) {
applyBodyParagraphStyle(paragraph);
appendPlainText(paragraph, block.text, BODY_FONT_SIZE);
} else if (block.type == MdType.LIST && isBodyStyleListItem(block.text)) {
applyBodyParagraphStyle(paragraph);
appendMarkdownRuns(paragraph, demoteBodyStyleListPrefix(block.text), false, BODY_FONT_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);
applyBodyParagraphStyle(paragraph);
appendMarkdownRuns(paragraph, block.text, false, BODY_FONT_SIZE);
} else {
appendMarkdownRuns(paragraph, block.text, false, 12);
applyBodyParagraphStyle(paragraph);
appendMarkdownRuns(paragraph, block.text, false, BODY_FONT_SIZE);
}
}
appendGeneratedByFooter(document);
applyDocumentFont(document);
document.write(out);
return out.toByteArray();
}
}
private String formatMeetingTime(MeetingVO meeting) {
return meeting.getMeetingTime() == null ? "" : meeting.getMeetingTime().format(MEETING_TIME_FORMATTER);
}
private String normalizeMixedContentSpacing(String markdown) {
if (markdown == null) {
return null;
@ -73,6 +104,119 @@ final class MeetingWordDocumentBuilder {
return MIXED_CONTENT_SPACING.matcher(markdown).replaceAll("");
}
private boolean isBodyStyleListItem(String text) {
if (text == null) {
return false;
}
Matcher prefixMatcher = BODY_STYLE_LIST_PREFIX.matcher(text);
if (prefixMatcher.find() && !text.substring(prefixMatcher.end()).isBlank()) {
return true;
}
Matcher fullBoldMatcher = FULLY_BOLD_LIST_ITEM.matcher(text);
if (!fullBoldMatcher.matches()) {
return false;
}
String plainText = fullBoldMatcher.group(1).trim();
int colonIndex = firstTitleColonIndex(plainText);
return colonIndex > 0 && colonIndex < plainText.length() - 1;
}
private String demoteBodyStyleListPrefix(String text) {
return toPlainInline(text).trim()
.replaceFirst("[\\h\\u200B]*([:])[\\h\\u200B]*", "$1");
}
private int firstTitleColonIndex(String text) {
for (int index = 0; index < text.length(); index++) {
char current = text.charAt(index);
if (current == '') {
return index;
}
if (current == ':'
&& (index == 0 || !Character.isDigit(text.charAt(index - 1)))
&& (index == text.length() - 1 || !Character.isDigit(text.charAt(index + 1)))) {
return index;
}
}
return -1;
}
private void applyBodyParagraphStyle(XWPFParagraph paragraph) {
paragraph.setAlignment(ParagraphAlignment.BOTH);
paragraph.setFirstLineIndent(BODY_FIRST_LINE_INDENT);
paragraph.setSpacingBetween(BODY_LINE_SPACING, LineSpacingRule.EXACT);
paragraph.setSpacingBefore(0);
paragraph.setSpacingAfter(0);
}
private void applyHeadingParagraphStyle(XWPFParagraph paragraph) {
if (!paragraph.getCTP().isSetPPr()) {
paragraph.getCTP().addNewPPr();
}
paragraph.setKeepNext(true);
paragraph.setSpacingBefore(240);
paragraph.setSpacingAfter(120);
}
private void appendMetadata(XWPFParagraph paragraph, String text) {
paragraph.setAlignment(ParagraphAlignment.RIGHT);
paragraph.setSpacingAfter(80);
XWPFRun run = paragraph.createRun();
run.setFontSize(12);
run.setText(text);
}
private void appendPlainText(XWPFParagraph paragraph, String text, int size) {
String plainText = toPlainInline(text);
if (plainText.isEmpty()) {
return;
}
XWPFRun run = paragraph.createRun();
run.setFontSize(size);
run.setText(plainText);
}
private void appendGeneratedByFooter(XWPFDocument document) {
XWPFParagraph paragraph = document.createParagraph();
paragraph.setAlignment(ParagraphAlignment.CENTER);
paragraph.setSpacingBefore(480);
paragraph.setSpacingAfter(0);
XWPFRun run = paragraph.createRun();
run.setItalic(true);
run.setFontSize(10);
run.setText("—— 内容由智听云AI生成 ——");
}
private void applyPageLayout(XWPFDocument document) {
CTBody body = document.getDocument().getBody();
CTSectPr section = body.isSetSectPr() ? body.getSectPr() : body.addNewSectPr();
CTPageSz pageSize = section.isSetPgSz() ? section.getPgSz() : section.addNewPgSz();
pageSize.setW(BigInteger.valueOf(11906));
pageSize.setH(BigInteger.valueOf(16838));
pageSize.setOrient(STPageOrientation.PORTRAIT);
CTPageMar margins = section.isSetPgMar() ? section.getPgMar() : section.addNewPgMar();
margins.setTop(BigInteger.valueOf(2098));
margins.setRight(BigInteger.valueOf(1531));
margins.setBottom(BigInteger.valueOf(1871));
margins.setLeft(BigInteger.valueOf(1531));
margins.setHeader(BigInteger.valueOf(708));
margins.setFooter(BigInteger.valueOf(708));
margins.setGutter(BigInteger.ZERO);
}
private void applyDocumentFont(XWPFDocument document) {
for (XWPFParagraph paragraph : document.getParagraphs()) {
for (XWPFRun run : paragraph.getRuns()) {
run.setFontFamily(DOCUMENT_FONT, XWPFRun.FontCharRange.eastAsia);
run.setFontFamily(DOCUMENT_FONT, XWPFRun.FontCharRange.ascii);
run.setFontFamily(DOCUMENT_FONT, XWPFRun.FontCharRange.hAnsi);
run.setFontFamily(DOCUMENT_FONT, XWPFRun.FontCharRange.cs);
}
}
}
private void appendMarkdownRuns(XWPFParagraph paragraph, String text, boolean defaultBold, int size) {
String input = text == null ? "" : text;
Matcher matcher = Pattern.compile("\\*\\*(.+?)\\*\\*").matcher(input);
@ -118,6 +262,10 @@ final class MeetingWordDocumentBuilder {
flushParagraph(blocks, paragraph);
continue;
}
if (HORIZONTAL_RULE.matcher(line).matches()) {
flushParagraph(blocks, paragraph);
continue;
}
if (line.startsWith("#")) {
flushParagraph(blocks, paragraph);
int level = 0;
@ -128,6 +276,11 @@ final class MeetingWordDocumentBuilder {
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.QUOTE, 0, QUOTE_PREFIX.matcher(line).replaceFirst("")));
continue;
}
if (line.startsWith("- ") || line.startsWith("* ")) {
flushParagraph(blocks, paragraph);
blocks.add(new MdBlock(MdType.LIST, 0, line.substring(2).trim()));
@ -170,6 +323,7 @@ final class MeetingWordDocumentBuilder {
private enum MdType {
HEADING,
QUOTE,
LIST,
PARAGRAPH
}