feat: 添加会议转录文件服务

- 新增 `MeetingTranscriptFileServiceImpl` 实现会议转录文件的初始化和导出功能
- 定义 `MeetingTranscriptExportResult` 数据传输对象,用于封装导出结果
- 定义 `MeetingTranscriptFileService` 接口,提供初始化和导出会议转录文件的方法
dev_na
chenhao 2026-04-28 15:51:39 +08:00
parent 35698287de
commit a8b93a46f8
3 changed files with 203 additions and 0 deletions

View File

@ -0,0 +1,12 @@
package com.imeeting.dto.biz;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class MeetingTranscriptExportResult {
private byte[] content;
private String contentType;
private String fileName;
}

View File

@ -0,0 +1,11 @@
package com.imeeting.service.biz;
import com.imeeting.dto.biz.MeetingTranscriptExportResult;
import com.imeeting.dto.biz.MeetingVO;
import com.imeeting.entity.biz.Meeting;
public interface MeetingTranscriptFileService {
void initializeTranscriptFileIfAbsent(Long meetingId);
MeetingTranscriptExportResult exportTranscript(Meeting meeting, MeetingVO meetingDetail);
}

View File

@ -0,0 +1,180 @@
package com.imeeting.service.biz.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.imeeting.dto.biz.MeetingTranscriptExportResult;
import com.imeeting.dto.biz.MeetingVO;
import com.imeeting.entity.biz.Meeting;
import com.imeeting.entity.biz.MeetingTranscript;
import com.imeeting.mapper.biz.MeetingMapper;
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
import com.imeeting.service.biz.MeetingTranscriptFileService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Service
@RequiredArgsConstructor
public class MeetingTranscriptFileServiceImpl implements MeetingTranscriptFileService {
private static final String TRANSCRIPT_RELATIVE_PATH_TEMPLATE = "meetings/%s/transcripts/current.md";
private static final String CONTENT_TYPE = "text/markdown; charset=UTF-8";
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final MeetingMapper meetingMapper;
private final MeetingTranscriptMapper meetingTranscriptMapper;
@Value("${unisbase.app.upload-path}")
private String uploadPath;
@Override
public void initializeTranscriptFileIfAbsent(Long meetingId) {
if (meetingId == null) {
return;
}
Path transcriptPath = buildTranscriptPath(meetingId);
if (Files.exists(transcriptPath)) {
return;
}
Meeting meeting = meetingMapper.selectById(meetingId);
if (meeting == null) {
return;
}
writeTranscriptFile(meeting, null);
}
@Override
public MeetingTranscriptExportResult exportTranscript(Meeting meeting, MeetingVO meetingDetail) {
if (meeting == null || meeting.getId() == null) {
throw new RuntimeException("会议不存在");
}
byte[] content = writeTranscriptFile(meeting, meetingDetail);
String safeTitle = sanitizeFileName(
meetingDetail != null ? meetingDetail.getTitle() : meeting.getTitle(),
"meeting-transcript-" + meeting.getId()
);
return new MeetingTranscriptExportResult(content, CONTENT_TYPE, safeTitle + "-Transcript.md");
}
private byte[] writeTranscriptFile(Meeting meeting, MeetingVO meetingDetail) {
try {
Path transcriptPath = buildTranscriptPath(meeting.getId());
Path parent = transcriptPath.getParent();
if (parent != null) {
Files.createDirectories(parent);
}
String markdown = buildTranscriptMarkdown(meeting, meetingDetail);
Files.writeString(transcriptPath, markdown, StandardCharsets.UTF_8);
return markdown.getBytes(StandardCharsets.UTF_8);
} catch (IOException ex) {
throw new RuntimeException("写入会议转录文件失败", ex);
}
}
private String buildTranscriptMarkdown(Meeting meeting, MeetingVO meetingDetail) {
List<MeetingTranscript> transcripts = meetingTranscriptMapper.selectList(new LambdaQueryWrapper<MeetingTranscript>()
.eq(MeetingTranscript::getMeetingId, meeting.getId())
.orderByAsc(MeetingTranscript::getSortOrder)
.orderByAsc(MeetingTranscript::getStartTime)
.orderByAsc(MeetingTranscript::getId));
String title = firstNonBlank(meetingDetail != null ? meetingDetail.getTitle() : null, meeting.getTitle(), "未命名会议");
String hostName = firstNonBlank(meetingDetail != null ? meetingDetail.getHostName() : null, meeting.getHostName(), "未知");
String meetingTime = formatDateTime(meetingDetail != null ? meetingDetail.getMeetingTime() : meeting.getMeetingTime());
StringBuilder builder = new StringBuilder();
builder.append("# ").append(title).append(" 会议转录\n\n");
builder.append("- 会议时间:").append(meetingTime).append("\n");
builder.append("- 主持人:").append(hostName).append("\n");
builder.append("- 导出时间:").append(formatDateTime(LocalDateTime.now())).append("\n\n");
builder.append("## 转录正文\n\n");
if (transcripts.isEmpty()) {
builder.append("_当前暂无转录内容_\n");
return builder.toString();
}
for (MeetingTranscript transcript : transcripts) {
String speaker = firstNonBlank(transcript.getSpeakerName(), transcript.getSpeakerId(), "未知发言人");
builder.append("- ");
String timeRange = buildTimeRange(transcript.getStartTime(), transcript.getEndTime());
if (!timeRange.isBlank()) {
builder.append(timeRange).append(' ');
}
builder.append(speaker).append("").append(normalizeTranscriptContent(transcript.getContent())).append("\n");
}
return builder.toString();
}
private Path buildTranscriptPath(Long meetingId) {
String basePath = uploadPath.endsWith("/") || uploadPath.endsWith("\\") ? uploadPath : uploadPath + "/";
String relativePath = TRANSCRIPT_RELATIVE_PATH_TEMPLATE.formatted(meetingId);
return Paths.get(basePath, relativePath.replace("\\", "/"));
}
private String buildTimeRange(Integer startTime, Integer endTime) {
if (startTime == null && endTime == null) {
return "";
}
String start = formatMillis(startTime);
String end = formatMillis(endTime);
if (start.isBlank()) {
return "[" + end + "]";
}
if (end.isBlank()) {
return "[" + start + "]";
}
return "[" + start + " - " + end + "]";
}
private String formatMillis(Integer millis) {
if (millis == null || millis < 0) {
return "";
}
int totalSeconds = millis / 1000;
int hours = totalSeconds / 3600;
int minutes = (totalSeconds % 3600) / 60;
int seconds = totalSeconds % 60;
return String.format("%02d:%02d:%02d", hours, minutes, seconds);
}
private String normalizeTranscriptContent(String content) {
if (content == null || content.isBlank()) {
return "";
}
return content.replace("\r\n", " ").replace('\n', ' ').trim();
}
private String sanitizeFileName(String value, String fallback) {
String normalized = value == null ? "" : value.replaceAll("[\\\\/:*?\"<>|\\r\\n]", "_").trim();
return normalized.isEmpty() ? fallback : normalized;
}
private String formatDateTime(LocalDateTime value) {
if (value == null) {
return "未知";
}
return DATE_TIME_FORMATTER.format(value);
}
private String firstNonBlank(String... values) {
if (values == null) {
return "";
}
for (String value : values) {
if (value != null && !value.isBlank()) {
return value.trim();
}
}
return "";
}
}