feat: 添加会议转录文件服务
- 新增 `MeetingTranscriptFileServiceImpl` 实现会议转录文件的初始化和导出功能 - 定义 `MeetingTranscriptExportResult` 数据传输对象,用于封装导出结果 - 定义 `MeetingTranscriptFileService` 接口,提供初始化和导出会议转录文件的方法dev_na
parent
35698287de
commit
a8b93a46f8
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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 "";
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue