feat: 增强旧版会议控制器功能和相关测试

- 添加 `StringRedisTemplate` 和 `ObjectMapper` 依赖
- 更新 `buildListItem` 方法以构建会议列表项
- 增加实时进度解析和处理逻辑
- 优化会议预览数据响应构建
- 添加并更新相关单元测试以验证新功能的正确性
dev_na
chenhao 2026-04-15 15:21:00 +08:00
parent 658b7e6b59
commit 6d46998abe
7 changed files with 862 additions and 40 deletions

View File

@ -1,6 +1,9 @@
package com.imeeting.controller.android.legacy; package com.imeeting.controller.android.legacy;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imeeting.common.RedisKeys;
import com.imeeting.dto.android.legacy.LegacyApiResponse; import com.imeeting.dto.android.legacy.LegacyApiResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingAccessPasswordRequest; import com.imeeting.dto.android.legacy.LegacyMeetingAccessPasswordRequest;
import com.imeeting.dto.android.legacy.LegacyMeetingAccessPasswordResponse; import com.imeeting.dto.android.legacy.LegacyMeetingAccessPasswordResponse;
@ -12,10 +15,11 @@ import com.imeeting.dto.android.legacy.LegacyMeetingListResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingPreviewDataResponse; import com.imeeting.dto.android.legacy.LegacyMeetingPreviewDataResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingPreviewResult; import com.imeeting.dto.android.legacy.LegacyMeetingPreviewResult;
import com.imeeting.dto.android.legacy.LegacyMeetingProcessingStatusResponse; import com.imeeting.dto.android.legacy.LegacyMeetingProcessingStatusResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingTagResponse;
import com.imeeting.dto.biz.MeetingVO; import com.imeeting.dto.biz.MeetingVO;
import com.imeeting.entity.biz.AiTask; import com.imeeting.entity.biz.AiTask;
import com.imeeting.entity.biz.Meeting; import com.imeeting.entity.biz.Meeting;
import com.imeeting.entity.biz.MeetingTranscript; import com.imeeting.mapper.biz.MeetingTranscriptMapper;
import com.imeeting.entity.biz.PromptTemplate; import com.imeeting.entity.biz.PromptTemplate;
import com.imeeting.service.android.legacy.LegacyMeetingAdapterService; import com.imeeting.service.android.legacy.LegacyMeetingAdapterService;
import com.imeeting.service.biz.AiTaskService; import com.imeeting.service.biz.AiTaskService;
@ -24,12 +28,12 @@ import com.imeeting.service.biz.MeetingCommandService;
import com.imeeting.service.biz.MeetingQueryService; import com.imeeting.service.biz.MeetingQueryService;
import com.imeeting.service.biz.MeetingService; import com.imeeting.service.biz.MeetingService;
import com.imeeting.service.biz.PromptTemplateService; import com.imeeting.service.biz.PromptTemplateService;
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
import com.unisbase.dto.PageResult; import com.unisbase.dto.PageResult;
import com.unisbase.entity.SysUser; import com.unisbase.entity.SysUser;
import com.unisbase.mapper.SysUserMapper; import com.unisbase.mapper.SysUserMapper;
import com.unisbase.security.LoginUser; import com.unisbase.security.LoginUser;
import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
@ -44,6 +48,7 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.IOException; import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Arrays; import java.util.Arrays;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@ -53,7 +58,7 @@ import java.util.stream.Collectors;
@RestController @RestController
@RequestMapping("/api/meetings") @RequestMapping("/api/meetings")
@RequiredArgsConstructor
public class LegacyMeetingController { public class LegacyMeetingController {
private static final String STAGE_DATA_INITIALIZATION = "data_initialization"; private static final String STAGE_DATA_INITIALIZATION = "data_initialization";
@ -70,6 +75,55 @@ public class LegacyMeetingController {
private final PromptTemplateService promptTemplateService; private final PromptTemplateService promptTemplateService;
private final MeetingTranscriptMapper meetingTranscriptMapper; private final MeetingTranscriptMapper meetingTranscriptMapper;
private final SysUserMapper sysUserMapper; private final SysUserMapper sysUserMapper;
private final StringRedisTemplate redisTemplate;
private final ObjectMapper objectMapper;
public LegacyMeetingController(LegacyMeetingAdapterService legacyMeetingAdapterService,
MeetingQueryService meetingQueryService,
MeetingAccessService meetingAccessService,
MeetingCommandService meetingCommandService,
MeetingService meetingService,
AiTaskService aiTaskService,
PromptTemplateService promptTemplateService,
MeetingTranscriptMapper meetingTranscriptMapper,
SysUserMapper sysUserMapper) {
this(legacyMeetingAdapterService,
meetingQueryService,
meetingAccessService,
meetingCommandService,
meetingService,
aiTaskService,
promptTemplateService,
meetingTranscriptMapper,
sysUserMapper,
null,
new ObjectMapper());
}
@Autowired
public LegacyMeetingController(LegacyMeetingAdapterService legacyMeetingAdapterService,
MeetingQueryService meetingQueryService,
MeetingAccessService meetingAccessService,
MeetingCommandService meetingCommandService,
MeetingService meetingService,
AiTaskService aiTaskService,
PromptTemplateService promptTemplateService,
MeetingTranscriptMapper meetingTranscriptMapper,
SysUserMapper sysUserMapper,
StringRedisTemplate redisTemplate,
ObjectMapper objectMapper) {
this.legacyMeetingAdapterService = legacyMeetingAdapterService;
this.meetingQueryService = meetingQueryService;
this.meetingAccessService = meetingAccessService;
this.meetingCommandService = meetingCommandService;
this.meetingService = meetingService;
this.aiTaskService = aiTaskService;
this.promptTemplateService = promptTemplateService;
this.meetingTranscriptMapper = meetingTranscriptMapper;
this.sysUserMapper = sysUserMapper;
this.redisTemplate = redisTemplate;
this.objectMapper = objectMapper;
}
@PostMapping @PostMapping
@PreAuthorize("isAuthenticated()") @PreAuthorize("isAuthenticated()")
@ -123,7 +177,7 @@ public class LegacyMeetingController {
data.setHasMore(page != null && page < data.getTotalPages()); data.setHasMore(page != null && page < data.getTotalPages());
data.setMeetings(result.getRecords() == null data.setMeetings(result.getRecords() == null
? List.of() ? List.of()
: result.getRecords().stream().map(LegacyMeetingItemResponse::from).toList()); : result.getRecords().stream().map(this::buildListItem).toList());
return LegacyApiResponse.ok(data); return LegacyApiResponse.ok(data);
} }
@ -182,6 +236,13 @@ public class LegacyMeetingController {
buildProcessingPreview(meeting, summaryTask, processingStatus("摘要已生成,可扫码查看", 100, STAGE_COMPLETED)) buildProcessingPreview(meeting, summaryTask, processingStatus("摘要已生成,可扫码查看", 100, STAGE_COMPLETED))
); );
} }
if (isFailed(asrTask) || Integer.valueOf(4).equals(meeting.getStatus())) {
return new LegacyMeetingPreviewResult(
"503",
buildFailureMessage(asrTask, "转译"),
buildProcessingPreview(meeting, summaryTask, processingStatus("转译或总结失败", 50, STAGE_AUDIO_TRANSCRIPTION))
);
}
if (isFailed(summaryTask)) { if (isFailed(summaryTask)) {
return new LegacyMeetingPreviewResult( return new LegacyMeetingPreviewResult(
"503", "503",
@ -189,24 +250,36 @@ public class LegacyMeetingController {
buildProcessingPreview(meeting, summaryTask, processingStatus("转译或总结失败", 75, STAGE_SUMMARY_GENERATION)) buildProcessingPreview(meeting, summaryTask, processingStatus("转译或总结失败", 75, STAGE_SUMMARY_GENERATION))
); );
} }
if (isFailed(asrTask) || Integer.valueOf(4).equals(meeting.getStatus())) {
return new LegacyMeetingPreviewResult( Integer realtimeProgress = resolveRealtimeProgress(meetingId);
"503", if (realtimeProgress != null) {
buildFailureMessage(asrTask, "转译"), if (realtimeProgress < 90) {
buildProcessingPreview(meeting, summaryTask, processingStatus("转译或总结失败", 45, STAGE_AUDIO_TRANSCRIPTION)) return new LegacyMeetingPreviewResult(
); "400",
"浼氳姝e湪澶勭悊涓?",
buildProcessingPreview(meeting, summaryTask, processingStatus("姝e湪杞瘧闊抽", 50, STAGE_AUDIO_TRANSCRIPTION))
);
}
if (realtimeProgress == 90) {
return new LegacyMeetingPreviewResult(
"400",
"浼氳姝e湪澶勭悊涓?",
buildProcessingPreview(meeting, summaryTask, processingStatus("姝e湪鐢熸垚鎬荤粨", 75, STAGE_SUMMARY_GENERATION))
);
}
} }
long transcriptCount = meetingTranscriptMapper.selectCount(new LambdaQueryWrapper<MeetingTranscript>() boolean isSummaryStage = isSummaryStage(meeting.getStatus(), summaryTask);
.eq(MeetingTranscript::getMeetingId, meetingId)); boolean isAsrStage = isAsrStage(meeting.getStatus(), asrTask, hasAudio(meeting), isSummaryStage);
if (isRunningSummary(summaryTask) || transcriptCount > 0 || Integer.valueOf(2).equals(meeting.getStatus())) {
if (!isAsrStage && !isSummaryStage) {
return new LegacyMeetingPreviewResult( return new LegacyMeetingPreviewResult(
"400", "400",
"会议正在处理中", "会议正在处理中",
buildProcessingPreview(meeting, summaryTask, processingStatus("正在生成总结", 75, STAGE_SUMMARY_GENERATION)) buildProcessingPreview(meeting, summaryTask, processingStatus("会议数据准备中", 25, STAGE_DATA_INITIALIZATION))
); );
} }
if (isRunningAsr(asrTask) || (meeting.getAudioUrl() != null && !meeting.getAudioUrl().isBlank()) || Integer.valueOf(1).equals(meeting.getStatus())) { if (!isSummaryStage) {
return new LegacyMeetingPreviewResult( return new LegacyMeetingPreviewResult(
"400", "400",
"会议正在处理中", "会议正在处理中",
@ -216,7 +289,7 @@ public class LegacyMeetingController {
return new LegacyMeetingPreviewResult( return new LegacyMeetingPreviewResult(
"400", "400",
"会议正在处理中", "会议正在处理中",
buildProcessingPreview(meeting, summaryTask, processingStatus("会议数据准备中", 25, STAGE_DATA_INITIALIZATION)) buildProcessingPreview(meeting, summaryTask, processingStatus("正在生成总结", 75, STAGE_SUMMARY_GENERATION))
); );
} }
@ -224,9 +297,9 @@ public class LegacyMeetingController {
LegacyMeetingPreviewDataResponse data = new LegacyMeetingPreviewDataResponse(); LegacyMeetingPreviewDataResponse data = new LegacyMeetingPreviewDataResponse();
data.setMeetingId(meeting.getId()); data.setMeetingId(meeting.getId());
data.setTitle(meeting.getTitle()); data.setTitle(meeting.getTitle());
data.setMeetingTime(meeting.getMeetingTime() == null ? null : meeting.getMeetingTime().toString()); data.setMeetingTime(formatDateTime(meeting.getMeetingTime()));
data.setSummary(detail.getSummaryContent()); data.setSummary(detail.getSummaryContent());
data.setCreatorUsername(meeting.getCreatorName()); data.setCreatorUsername(resolveCreatorDisplayName(meeting.getCreatorId(), meeting.getCreatorName()));
Long promptId = resolvePromptId(summaryTask); Long promptId = resolvePromptId(summaryTask);
data.setPromptId(promptId); data.setPromptId(promptId);
data.setPromptName(resolvePromptName(promptId)); data.setPromptName(resolvePromptName(promptId));
@ -238,14 +311,39 @@ public class LegacyMeetingController {
return data; return data;
} }
private LegacyMeetingItemResponse buildListItem(MeetingVO meeting) {
LegacyMeetingItemResponse item = new LegacyMeetingItemResponse();
item.setMeetingId(meeting.getId());
item.setTitle(meeting.getTitle());
item.setMeetingTime(formatDateTime(meeting.getMeetingTime()));
item.setCreatedAt(formatDateTime(meeting.getCreatedAt()));
item.setCreatorId(meeting.getCreatorId());
item.setCreatorUsername(resolveCreatorDisplayName(meeting.getCreatorId(), meeting.getCreatorName()));
item.setAudioFilePath(meeting.getAudioUrl());
item.setAudioDuration(meeting.getDuration());
item.setAccessPassword(resolveAccessPassword(meeting.getId()));
List<Long> attendeeIds = meeting.getParticipantIds() == null ? List.of() : meeting.getParticipantIds();
item.setAttendeeIds(attendeeIds);
item.setAttendees(buildAttendees(attendeeIds));
item.setTags(buildTags(meeting.getTags()));
item.setSummary(resolveListSummary(meeting.getId()));
LegacyMeetingProcessingStatusResponse status = buildListStatus(meeting);
item.setOverallStatus(status.getOverallStatus());
item.setOverallProgress(status.getOverallProgress());
item.setCurrentStage(translateListStage(status.getCurrentStage()));
return item;
}
private LegacyMeetingPreviewDataResponse buildProcessingPreview(Meeting meeting, private LegacyMeetingPreviewDataResponse buildProcessingPreview(Meeting meeting,
AiTask summaryTask, AiTask summaryTask,
LegacyMeetingProcessingStatusResponse status) { LegacyMeetingProcessingStatusResponse status) {
LegacyMeetingPreviewDataResponse data = new LegacyMeetingPreviewDataResponse(); LegacyMeetingPreviewDataResponse data = new LegacyMeetingPreviewDataResponse();
data.setMeetingId(meeting.getId()); data.setMeetingId(meeting.getId());
data.setTitle(meeting.getTitle()); data.setTitle(meeting.getTitle());
data.setMeetingTime(meeting.getMeetingTime() == null ? null : meeting.getMeetingTime().toString()); data.setMeetingTime(formatDateTime(meeting.getMeetingTime()));
data.setCreatorUsername(meeting.getCreatorName()); data.setCreatorUsername(resolveCreatorDisplayName(meeting.getCreatorId(), meeting.getCreatorName()));
Long promptId = resolvePromptId(summaryTask); Long promptId = resolvePromptId(summaryTask);
data.setPromptId(promptId); data.setPromptId(promptId);
data.setPromptName(resolvePromptName(promptId)); data.setPromptName(resolvePromptName(promptId));
@ -258,6 +356,50 @@ public class LegacyMeetingController {
return new LegacyMeetingProcessingStatusResponse(overallStatus, overallProgress, currentStage); return new LegacyMeetingProcessingStatusResponse(overallStatus, overallProgress, currentStage);
} }
private Integer resolveRealtimeProgress(Long meetingId) {
if (redisTemplate == null) {
return null;
}
String rawProgress = redisTemplate.opsForValue().get(RedisKeys.meetingProgressKey(meetingId));
if (rawProgress == null || rawProgress.isBlank()) {
return null;
}
try {
JsonNode progress = objectMapper.readTree(rawProgress);
return progress.hasNonNull("percent") ? progress.path("percent").asInt() : null;
} catch (Exception ignored) {
return null;
}
}
private LegacyMeetingProcessingStatusResponse buildListStatus(MeetingVO meeting) {
Long meetingId = meeting.getId();
AiTask asrTask = findLatestTask(meetingId, "ASR");
AiTask summaryTask = findLatestTask(meetingId, "SUMMARY");
boolean summaryCompleted = summaryTask != null && Integer.valueOf(2).equals(summaryTask.getStatus());
if (Integer.valueOf(3).equals(meeting.getStatus()) || summaryCompleted) {
return new LegacyMeetingProcessingStatusResponse("completed", 100, STAGE_COMPLETED);
}
if (isFailed(asrTask) || Integer.valueOf(4).equals(meeting.getStatus())) {
return new LegacyMeetingProcessingStatusResponse("failed", 50, STAGE_AUDIO_TRANSCRIPTION);
}
if (isFailed(summaryTask)) {
return new LegacyMeetingProcessingStatusResponse("failed", 75, STAGE_SUMMARY_GENERATION);
}
boolean isSummaryStage = isSummaryStage(meeting.getStatus(), summaryTask);
boolean isAsrStage = isAsrStage(meeting.getStatus(), asrTask, hasAudio(meeting), isSummaryStage);
if (!isAsrStage && !isSummaryStage) {
return new LegacyMeetingProcessingStatusResponse("pending", 0, STAGE_DATA_INITIALIZATION);
}
if (isSummaryStage) {
return new LegacyMeetingProcessingStatusResponse("summarizing", 75, STAGE_SUMMARY_GENERATION);
}
return new LegacyMeetingProcessingStatusResponse("transcribing", 50, STAGE_AUDIO_TRANSCRIPTION);
}
private String buildFailureMessage(AiTask failedTask, String stageName) { private String buildFailureMessage(AiTask failedTask, String stageName) {
String error = failedTask == null || failedTask.getErrorMsg() == null || failedTask.getErrorMsg().isBlank() String error = failedTask == null || failedTask.getErrorMsg() == null || failedTask.getErrorMsg().isBlank()
? "处理失败" ? "处理失败"
@ -316,8 +458,11 @@ public class LegacyMeetingController {
} }
private List<LegacyMeetingAttendeeResponse> buildAttendees(String participants) { private List<LegacyMeetingAttendeeResponse> buildAttendees(String participants) {
List<Long> participantIds = parseParticipantIds(participants); return buildAttendees(parseParticipantIds(participants));
if (participantIds.isEmpty()) { }
private List<LegacyMeetingAttendeeResponse> buildAttendees(List<Long> participantIds) {
if (participantIds == null || participantIds.isEmpty()) {
return List.of(); return List.of();
} }
Map<Long, SysUser> userMap = sysUserMapper.selectBatchIds(participantIds).stream() Map<Long, SysUser> userMap = sysUserMapper.selectBatchIds(participantIds).stream()
@ -329,11 +474,23 @@ public class LegacyMeetingController {
String caption = user == null String caption = user == null
? String.valueOf(userId) ? String.valueOf(userId)
: (user.getDisplayName() != null ? user.getDisplayName() : user.getUsername()); : (user.getDisplayName() != null ? user.getDisplayName() : user.getUsername());
return new LegacyMeetingAttendeeResponse(userId, caption); String username = user == null ? null : user.getUsername();
return new LegacyMeetingAttendeeResponse(userId, username, caption);
}) })
.toList(); .toList();
} }
private List<LegacyMeetingTagResponse> buildTags(String rawTags) {
if (rawTags == null || rawTags.isBlank()) {
return List.of();
}
return Arrays.stream(rawTags.split(","))
.map(String::trim)
.filter(value -> !value.isEmpty())
.map(value -> new LegacyMeetingTagResponse(null, value))
.toList();
}
private List<Long> parseParticipantIds(String participants) { private List<Long> parseParticipantIds(String participants) {
if (participants == null || participants.isBlank()) { if (participants == null || participants.isBlank()) {
return List.of(); return List.of();
@ -360,6 +517,69 @@ public class LegacyMeetingController {
return normalized.isEmpty() ? null : normalized; return normalized.isEmpty() ? null : normalized;
} }
private String resolveListSummary(Long meetingId) {
MeetingVO detail = meetingQueryService.getDetail(meetingId);
if (detail == null || detail.getSummaryContent() == null || detail.getSummaryContent().isBlank()) {
return null;
}
String summary = detail.getSummaryContent().trim();
return summary.length() <= 240 ? summary : summary.substring(0, 240);
}
private String resolveAccessPassword(Long meetingId) {
Meeting meeting = meetingService.getById(meetingId);
return meeting == null ? null : normalizePassword(meeting.getAccessPassword());
}
private String resolveCreatorDisplayName(Long creatorId, String fallbackName) {
if (creatorId == null) {
return fallbackName;
}
SysUser creator = sysUserMapper.selectById(creatorId);
if (creator == null) {
return fallbackName;
}
if (creator.getDisplayName() != null && !creator.getDisplayName().isBlank()) {
return creator.getDisplayName();
}
if (creator.getUsername() != null && !creator.getUsername().isBlank()) {
return creator.getUsername();
}
return fallbackName;
}
private boolean hasAudio(Meeting meeting) {
return meeting.getAudioUrl() != null && !meeting.getAudioUrl().isBlank();
}
private boolean hasAudio(MeetingVO meeting) {
return meeting.getAudioUrl() != null && !meeting.getAudioUrl().isBlank();
}
private boolean isSummaryStage(Integer meetingStatus, AiTask summaryTask) {
return Integer.valueOf(2).equals(meetingStatus) || isRunningSummary(summaryTask);
}
private boolean isAsrStage(Integer meetingStatus, AiTask asrTask, boolean hasAudio, boolean isSummaryStage) {
return Integer.valueOf(1).equals(meetingStatus)
|| isRunningAsr(asrTask)
|| (hasAudio && !isSummaryStage);
}
private String formatDateTime(LocalDateTime value) {
return value == null ? null : value.toString();
}
private String translateListStage(String stage) {
if (STAGE_SUMMARY_GENERATION.equals(stage)) {
return "llm";
}
if (STAGE_COMPLETED.equals(stage)) {
return "completed";
}
return "transcription";
}
private LoginUser currentLoginUser() { private LoginUser currentLoginUser() {
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
} }

View File

@ -4,7 +4,11 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.imeeting.entity.biz.ExternalApp; import com.imeeting.entity.biz.ExternalApp;
import lombok.Data; import lombok.Data;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Locale;
@Data @Data
public class LegacyExternalAppItemResponse { public class LegacyExternalAppItemResponse {
@ -47,7 +51,7 @@ public class LegacyExternalAppItemResponse {
response.setId(source.getId()); response.setId(source.getId());
response.setAppName(source.getAppName()); response.setAppName(source.getAppName());
response.setAppType(source.getAppType()); response.setAppType(source.getAppType());
response.setAppInfo(source.getAppInfo()); response.setAppInfo(normalizeAppInfo(source.getAppInfo()));
response.setIconUrl(source.getIconUrl()); response.setIconUrl(source.getIconUrl());
response.setDescription(source.getDescription()); response.setDescription(source.getDescription());
response.setSortOrder(source.getSortOrder()); response.setSortOrder(source.getSortOrder());
@ -58,4 +62,40 @@ public class LegacyExternalAppItemResponse {
response.setCreatorUsername(creatorUsername); response.setCreatorUsername(creatorUsername);
return response; return response;
} }
private static Map<String, Object> normalizeAppInfo(Map<String, Object> appInfo) {
if (appInfo == null || appInfo.isEmpty()) {
return appInfo;
}
Map<String, Object> normalized = new LinkedHashMap<>();
appInfo.forEach((key, value) -> normalized.put(toSnakeCase(key), normalizeValue(value)));
return normalized;
}
private static Object normalizeValue(Object value) {
if (value instanceof Map<?, ?> nestedMap) {
Map<String, Object> normalized = new LinkedHashMap<>();
nestedMap.forEach((key, nestedValue) -> normalized.put(toSnakeCase(String.valueOf(key)), normalizeValue(nestedValue)));
return normalized;
}
if (value instanceof List<?> list) {
List<Object> normalized = new ArrayList<>(list.size());
list.forEach(item -> normalized.add(normalizeValue(item)));
return normalized;
}
return value;
}
private static String toSnakeCase(String value) {
if (value == null || value.isBlank()) {
return value;
}
return value
.replace('-', '_')
.replace(' ', '_')
.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2")
.replaceAll("([a-z0-9])([A-Z])", "$1_$2")
.replaceAll("_+", "_")
.toLowerCase(Locale.ROOT);
}
} }

View File

@ -12,5 +12,7 @@ public class LegacyMeetingAttendeeResponse {
@JsonProperty("user_id") @JsonProperty("user_id")
private Long userId; private Long userId;
private String username;
private String caption; private String caption;
} }

View File

@ -1,9 +1,10 @@
package com.imeeting.dto.android.legacy; package com.imeeting.dto.android.legacy;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.imeeting.dto.biz.MeetingVO;
import lombok.Data; import lombok.Data;
import java.util.List;
@Data @Data
public class LegacyMeetingItemResponse { public class LegacyMeetingItemResponse {
@JsonProperty("meeting_id") @JsonProperty("meeting_id")
@ -14,11 +15,39 @@ public class LegacyMeetingItemResponse {
@JsonProperty("meeting_time") @JsonProperty("meeting_time")
private String meetingTime; private String meetingTime;
public static LegacyMeetingItemResponse from(MeetingVO source) { private String summary;
LegacyMeetingItemResponse response = new LegacyMeetingItemResponse();
response.setMeetingId(source.getId()); @JsonProperty("created_at")
response.setTitle(source.getTitle()); private String createdAt;
response.setMeetingTime(source.getMeetingTime() == null ? null : source.getMeetingTime().toString());
return response; @JsonProperty("creator_id")
} private Long creatorId;
@JsonProperty("creator_username")
private String creatorUsername;
private List<LegacyMeetingAttendeeResponse> attendees;
@JsonProperty("attendee_ids")
private List<Long> attendeeIds;
private List<LegacyMeetingTagResponse> tags;
@JsonProperty("audio_file_path")
private String audioFilePath;
@JsonProperty("audio_duration")
private Integer audioDuration;
@JsonProperty("overall_status")
private String overallStatus;
@JsonProperty("overall_progress")
private Integer overallProgress;
@JsonProperty("current_stage")
private String currentStage;
@JsonProperty("access_password")
private String accessPassword;
} }

View File

@ -1,8 +1,12 @@
package com.imeeting.controller.android.legacy; package com.imeeting.controller.android.legacy;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imeeting.dto.android.legacy.LegacyApiResponse; import com.imeeting.dto.android.legacy.LegacyApiResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingAccessPasswordRequest; import com.imeeting.dto.android.legacy.LegacyMeetingAccessPasswordRequest;
import com.imeeting.dto.android.legacy.LegacyMeetingAccessPasswordResponse; import com.imeeting.dto.android.legacy.LegacyMeetingAccessPasswordResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingItemResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingListResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingPreviewDataResponse;
import com.imeeting.dto.biz.MeetingVO; import com.imeeting.dto.biz.MeetingVO;
import com.imeeting.entity.biz.AiTask; import com.imeeting.entity.biz.AiTask;
import com.imeeting.entity.biz.Meeting; import com.imeeting.entity.biz.Meeting;
@ -15,11 +19,14 @@ import com.imeeting.service.biz.MeetingCommandService;
import com.imeeting.service.biz.MeetingQueryService; import com.imeeting.service.biz.MeetingQueryService;
import com.imeeting.service.biz.MeetingService; import com.imeeting.service.biz.MeetingService;
import com.imeeting.service.biz.PromptTemplateService; import com.imeeting.service.biz.PromptTemplateService;
import com.unisbase.dto.PageResult;
import com.unisbase.entity.SysUser; import com.unisbase.entity.SysUser;
import com.unisbase.mapper.SysUserMapper; import com.unisbase.mapper.SysUserMapper;
import com.unisbase.security.LoginUser; import com.unisbase.security.LoginUser;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
@ -53,9 +60,10 @@ class LegacyMeetingControllerTest {
Meeting meeting = new Meeting(); Meeting meeting = new Meeting();
meeting.setId(8L); meeting.setId(8L);
meeting.setTitle("项目复盘"); meeting.setTitle("retro");
meeting.setMeetingTime(LocalDateTime.of(2026, 4, 13, 10, 0)); meeting.setMeetingTime(LocalDateTime.of(2026, 4, 13, 10, 0));
meeting.setCreatorName("发起人"); meeting.setCreatorId(7L);
meeting.setCreatorName("owner");
meeting.setParticipants("2,3"); meeting.setParticipants("2,3");
meeting.setAccessPassword("123456"); meeting.setAccessPassword("123456");
meeting.setStatus(3); meeting.setStatus(3);
@ -63,23 +71,30 @@ class LegacyMeetingControllerTest {
AiTask summaryTask = new AiTask(); AiTask summaryTask = new AiTask();
summaryTask.setTaskConfig(Map.of("promptId", 5L)); summaryTask.setTaskConfig(Map.of("promptId", 5L));
when(aiTaskService.getOne(any())).thenReturn(null, summaryTask); when(aiTaskService.getOne(any())).thenReturn((AiTask) null, summaryTask);
MeetingVO detail = new MeetingVO(); MeetingVO detail = new MeetingVO();
detail.setSummaryContent("## 总结\n已完成"); detail.setSummaryContent("done");
when(meetingQueryService.getDetail(8L)).thenReturn(detail); when(meetingQueryService.getDetail(8L)).thenReturn(detail);
PromptTemplate template = new PromptTemplate(); PromptTemplate template = new PromptTemplate();
template.setTemplateName("标准纪要"); template.setTemplateName("standard");
when(promptTemplateService.getById(5L)).thenReturn(template); when(promptTemplateService.getById(5L)).thenReturn(template);
SysUser user2 = new SysUser(); SysUser user2 = new SysUser();
user2.setUserId(2L); user2.setUserId(2L);
user2.setDisplayName("张三"); user2.setUsername("alice");
user2.setDisplayName("Alice");
SysUser user3 = new SysUser(); SysUser user3 = new SysUser();
user3.setUserId(3L); user3.setUserId(3L);
user3.setDisplayName("李四"); user3.setUsername("bob");
user3.setDisplayName("Bob");
when(sysUserMapper.selectBatchIds(List.of(2L, 3L))).thenReturn(List.of(user2, user3)); when(sysUserMapper.selectBatchIds(List.of(2L, 3L))).thenReturn(List.of(user2, user3));
SysUser creator = new SysUser();
creator.setUserId(7L);
creator.setUsername("owner-login");
creator.setDisplayName("Owner Display");
when(sysUserMapper.selectById(7L)).thenReturn(creator);
LegacyMeetingController controller = new LegacyMeetingController( LegacyMeetingController controller = new LegacyMeetingController(
mock(LegacyMeetingAdapterService.class), mock(LegacyMeetingAdapterService.class),
@ -99,6 +114,388 @@ class LegacyMeetingControllerTest {
assertNotNull(response.getData()); assertNotNull(response.getData());
} }
@Test
void listShouldReturnLegacyMeetingPayloadAlignedWithPythonResponse() {
MeetingQueryService meetingQueryService = mock(MeetingQueryService.class);
MeetingService meetingService = mock(MeetingService.class);
AiTaskService aiTaskService = mock(AiTaskService.class);
MeetingTranscriptMapper transcriptMapper = mock(MeetingTranscriptMapper.class);
SysUserMapper sysUserMapper = mock(SysUserMapper.class);
MeetingVO meeting = new MeetingVO();
meeting.setId(18L);
meeting.setTitle("weekly");
meeting.setMeetingTime(LocalDateTime.of(2026, 4, 14, 9, 30));
meeting.setCreatedAt(LocalDateTime.of(2026, 4, 14, 10, 0));
meeting.setCreatorId(7L);
meeting.setCreatorName("owner");
meeting.setParticipantIds(List.of(2L, 3L));
meeting.setTags("dev,weekly");
meeting.setAudioUrl("/api/static/meetings/18/source_audio.wav");
meeting.setDuration(366);
meeting.setStatus(3);
PageResult<List<MeetingVO>> pageResult = new PageResult<>();
pageResult.setTotal(1L);
pageResult.setRecords(List.of(meeting));
when(meetingQueryService.pageMeetings(any(), any(), any(), any(), any(), any(), any(), org.mockito.ArgumentMatchers.anyBoolean())).thenReturn(pageResult);
MeetingVO detail = new MeetingVO();
detail.setSummaryContent("summary");
when(meetingQueryService.getDetail(18L)).thenReturn(detail);
Meeting entity = new Meeting();
entity.setId(18L);
entity.setAccessPassword("123456");
when(meetingService.getById(18L)).thenReturn(entity);
SysUser user2 = new SysUser();
user2.setUserId(2L);
user2.setUsername("alice");
user2.setDisplayName("Alice");
SysUser user3 = new SysUser();
user3.setUserId(3L);
user3.setUsername("bob");
user3.setDisplayName("Bob");
when(sysUserMapper.selectBatchIds(List.of(2L, 3L))).thenReturn(List.of(user2, user3));
SysUser creator = new SysUser();
creator.setUserId(7L);
creator.setUsername("owner-login");
creator.setDisplayName("Owner Display");
when(sysUserMapper.selectById(7L)).thenReturn(creator);
when(aiTaskService.getOne(any())).thenReturn((AiTask) null, (AiTask) null);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(new LoginUser(7L, 1L, "creator", false, false, Set.of()), null)
);
LegacyMeetingController controller = new LegacyMeetingController(
mock(LegacyMeetingAdapterService.class),
meetingQueryService,
mock(MeetingAccessService.class),
mock(MeetingCommandService.class),
meetingService,
aiTaskService,
mock(PromptTemplateService.class),
transcriptMapper,
sysUserMapper
);
LegacyApiResponse<LegacyMeetingListResponse> response = controller.list(null, 1, 10, null);
assertEquals("200", response.getCode());
assertNotNull(response.getData());
assertEquals(1L, response.getData().getTotal());
assertEquals(1, response.getData().getMeetings().size());
LegacyMeetingItemResponse item = response.getData().getMeetings().get(0);
assertEquals(18L, item.getMeetingId());
assertEquals("weekly", item.getTitle());
assertEquals("summary", item.getSummary());
assertEquals(7L, item.getCreatorId());
assertEquals("Owner Display", item.getCreatorUsername());
assertEquals("/api/static/meetings/18/source_audio.wav", item.getAudioFilePath());
assertEquals(366, item.getAudioDuration());
assertEquals("123456", item.getAccessPassword());
assertEquals("completed", item.getOverallStatus());
assertEquals(100, item.getOverallProgress());
assertEquals("completed", item.getCurrentStage());
assertEquals(List.of(2L, 3L), item.getAttendeeIds());
assertEquals(2, item.getAttendees().size());
assertEquals("alice", item.getAttendees().get(0).getUsername());
assertEquals("Alice", item.getAttendees().get(0).getCaption());
assertEquals(2, item.getTags().size());
assertEquals("dev", item.getTags().get(0).getName());
}
@Test
void listShouldPreferTranscriptionStageBeforeSummaryStage() {
MeetingQueryService meetingQueryService = mock(MeetingQueryService.class);
MeetingService meetingService = mock(MeetingService.class);
AiTaskService aiTaskService = mock(AiTaskService.class);
MeetingTranscriptMapper transcriptMapper = mock(MeetingTranscriptMapper.class);
MeetingVO meeting = new MeetingVO();
meeting.setId(19L);
meeting.setTitle("status ordering");
meeting.setAudioUrl("/tmp/audio.wav");
meeting.setStatus(0);
PageResult<List<MeetingVO>> pageResult = new PageResult<>();
pageResult.setTotal(1L);
pageResult.setRecords(List.of(meeting));
when(meetingQueryService.pageMeetings(any(), any(), any(), any(), any(), any(), any(), org.mockito.ArgumentMatchers.anyBoolean())).thenReturn(pageResult);
when(meetingQueryService.getDetail(19L)).thenReturn(new MeetingVO());
when(meetingService.getById(19L)).thenReturn(new Meeting());
when(aiTaskService.getOne(any())).thenReturn((AiTask) null, (AiTask) null);
when(transcriptMapper.selectCount(any())).thenReturn(2L);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(new LoginUser(7L, 1L, "creator", false, false, Set.of()), null)
);
LegacyMeetingController controller = new LegacyMeetingController(
mock(LegacyMeetingAdapterService.class),
meetingQueryService,
mock(MeetingAccessService.class),
mock(MeetingCommandService.class),
meetingService,
aiTaskService,
mock(PromptTemplateService.class),
transcriptMapper,
mock(SysUserMapper.class)
);
LegacyApiResponse<LegacyMeetingListResponse> response = controller.list(null, 1, 10, null);
assertEquals("200", response.getCode());
assertNotNull(response.getData());
assertEquals(1, response.getData().getMeetings().size());
LegacyMeetingItemResponse item = response.getData().getMeetings().get(0);
assertEquals("transcribing", item.getOverallStatus());
assertEquals(50, item.getOverallProgress());
assertEquals("transcription", item.getCurrentStage());
}
@Test
void previewDataShouldReportAsrFailureAtFiftyPercent() {
MeetingService meetingService = mock(MeetingService.class);
AiTaskService aiTaskService = mock(AiTaskService.class);
MeetingQueryService meetingQueryService = mock(MeetingQueryService.class);
MeetingTranscriptMapper transcriptMapper = mock(MeetingTranscriptMapper.class);
Meeting meeting = new Meeting();
meeting.setId(20L);
meeting.setTitle("asr failed");
meeting.setStatus(4);
when(meetingService.getById(20L)).thenReturn(meeting);
AiTask asrTask = new AiTask();
asrTask.setStatus(3);
asrTask.setErrorMsg("asr failed");
when(aiTaskService.getOne(any())).thenReturn(asrTask, (AiTask) null);
LegacyMeetingController controller = new LegacyMeetingController(
mock(LegacyMeetingAdapterService.class),
meetingQueryService,
mock(MeetingAccessService.class),
mock(MeetingCommandService.class),
meetingService,
aiTaskService,
mock(PromptTemplateService.class),
transcriptMapper,
mock(SysUserMapper.class)
);
LegacyApiResponse<?> response = controller.previewData(20L);
assertEquals("503", response.getCode());
LegacyMeetingPreviewDataResponse data = (LegacyMeetingPreviewDataResponse) response.getData();
assertNotNull(data);
assertEquals(50, data.getProcessingStatus().getOverallProgress());
}
@Test
void previewDataShouldPrioritizeAsrFailureBeforeSummaryFailure() {
MeetingService meetingService = mock(MeetingService.class);
AiTaskService aiTaskService = mock(AiTaskService.class);
Meeting meeting = new Meeting();
meeting.setId(23L);
meeting.setTitle("both failed");
when(meetingService.getById(23L)).thenReturn(meeting);
AiTask asrTask = new AiTask();
asrTask.setStatus(3);
asrTask.setErrorMsg("asr failed");
AiTask summaryTask = new AiTask();
summaryTask.setStatus(3);
summaryTask.setErrorMsg("summary failed");
when(aiTaskService.getOne(any())).thenReturn(asrTask, summaryTask);
LegacyMeetingController controller = new LegacyMeetingController(
mock(LegacyMeetingAdapterService.class),
mock(MeetingQueryService.class),
mock(MeetingAccessService.class),
mock(MeetingCommandService.class),
meetingService,
aiTaskService,
mock(PromptTemplateService.class),
mock(MeetingTranscriptMapper.class),
mock(SysUserMapper.class)
);
LegacyApiResponse<?> response = controller.previewData(23L);
assertEquals("503", response.getCode());
LegacyMeetingPreviewDataResponse data = (LegacyMeetingPreviewDataResponse) response.getData();
assertNotNull(data);
assertEquals(50, data.getProcessingStatus().getOverallProgress());
assertEquals("audio_transcription", data.getProcessingStatus().getCurrentStage());
}
@Test
void previewDataShouldPreferTranscriptionStageBeforeSummaryStage() {
MeetingService meetingService = mock(MeetingService.class);
AiTaskService aiTaskService = mock(AiTaskService.class);
MeetingTranscriptMapper transcriptMapper = mock(MeetingTranscriptMapper.class);
Meeting meeting = new Meeting();
meeting.setId(22L);
meeting.setTitle("transcribing first");
meeting.setStatus(0);
meeting.setAudioUrl("/tmp/audio.wav");
when(meetingService.getById(22L)).thenReturn(meeting);
when(aiTaskService.getOne(any())).thenReturn((AiTask) null, (AiTask) null);
when(transcriptMapper.selectCount(any())).thenReturn(3L);
LegacyMeetingController controller = new LegacyMeetingController(
mock(LegacyMeetingAdapterService.class),
mock(MeetingQueryService.class),
mock(MeetingAccessService.class),
mock(MeetingCommandService.class),
meetingService,
aiTaskService,
mock(PromptTemplateService.class),
transcriptMapper,
mock(SysUserMapper.class)
);
LegacyApiResponse<?> response = controller.previewData(22L);
assertEquals("400", response.getCode());
LegacyMeetingPreviewDataResponse data = (LegacyMeetingPreviewDataResponse) response.getData();
assertNotNull(data);
assertEquals(50, data.getProcessingStatus().getOverallProgress());
assertEquals("audio_transcription", data.getProcessingStatus().getCurrentStage());
}
@Test
void previewDataShouldReportSummaryStageAtSeventyFivePercent() {
MeetingService meetingService = mock(MeetingService.class);
AiTaskService aiTaskService = mock(AiTaskService.class);
MeetingTranscriptMapper transcriptMapper = mock(MeetingTranscriptMapper.class);
Meeting meeting = new Meeting();
meeting.setId(21L);
meeting.setTitle("summary running");
meeting.setStatus(2);
when(meetingService.getById(21L)).thenReturn(meeting);
AiTask summaryTask = new AiTask();
summaryTask.setStatus(1);
when(aiTaskService.getOne(any())).thenReturn((AiTask) null, summaryTask);
when(transcriptMapper.selectCount(any())).thenReturn(0L);
LegacyMeetingController controller = new LegacyMeetingController(
mock(LegacyMeetingAdapterService.class),
mock(MeetingQueryService.class),
mock(MeetingAccessService.class),
mock(MeetingCommandService.class),
meetingService,
aiTaskService,
mock(PromptTemplateService.class),
transcriptMapper,
mock(SysUserMapper.class)
);
LegacyApiResponse<?> response = controller.previewData(21L);
assertEquals("400", response.getCode());
LegacyMeetingPreviewDataResponse data = (LegacyMeetingPreviewDataResponse) response.getData();
assertNotNull(data);
assertEquals(75, data.getProcessingStatus().getOverallProgress());
}
@Test
void previewDataShouldReportSummaryStageAtSeventyFivePercentWhenAudioExists() {
MeetingService meetingService = mock(MeetingService.class);
AiTaskService aiTaskService = mock(AiTaskService.class);
MeetingTranscriptMapper transcriptMapper = mock(MeetingTranscriptMapper.class);
Meeting meeting = new Meeting();
meeting.setId(24L);
meeting.setTitle("summary running with audio");
meeting.setStatus(2);
meeting.setAudioUrl("/tmp/audio.wav");
when(meetingService.getById(24L)).thenReturn(meeting);
AiTask summaryTask = new AiTask();
summaryTask.setStatus(1);
when(aiTaskService.getOne(any())).thenReturn((AiTask) null, summaryTask);
when(transcriptMapper.selectCount(any())).thenReturn(3L);
LegacyMeetingController controller = new LegacyMeetingController(
mock(LegacyMeetingAdapterService.class),
mock(MeetingQueryService.class),
mock(MeetingAccessService.class),
mock(MeetingCommandService.class),
meetingService,
aiTaskService,
mock(PromptTemplateService.class),
transcriptMapper,
mock(SysUserMapper.class)
);
LegacyApiResponse<?> response = controller.previewData(24L);
assertEquals("400", response.getCode());
LegacyMeetingPreviewDataResponse data = (LegacyMeetingPreviewDataResponse) response.getData();
assertNotNull(data);
assertEquals(75, data.getProcessingStatus().getOverallProgress());
assertEquals("summary_generation", data.getProcessingStatus().getCurrentStage());
}
@Test
void listShouldReportSummaryStageAtSeventyFivePercentWhenAudioExists() {
MeetingQueryService meetingQueryService = mock(MeetingQueryService.class);
MeetingService meetingService = mock(MeetingService.class);
AiTaskService aiTaskService = mock(AiTaskService.class);
MeetingTranscriptMapper transcriptMapper = mock(MeetingTranscriptMapper.class);
MeetingVO meeting = new MeetingVO();
meeting.setId(25L);
meeting.setTitle("summary stage with audio");
meeting.setAudioUrl("/tmp/audio.wav");
meeting.setStatus(2);
PageResult<List<MeetingVO>> pageResult = new PageResult<>();
pageResult.setTotal(1L);
pageResult.setRecords(List.of(meeting));
when(meetingQueryService.pageMeetings(any(), any(), any(), any(), any(), any(), any(), org.mockito.ArgumentMatchers.anyBoolean())).thenReturn(pageResult);
AiTask summaryTask = new AiTask();
summaryTask.setStatus(1);
when(aiTaskService.getOne(any())).thenReturn((AiTask) null, summaryTask);
when(transcriptMapper.selectCount(any())).thenReturn(3L);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(new LoginUser(7L, 1L, "creator", false, false, Set.of()), null)
);
LegacyMeetingController controller = new LegacyMeetingController(
mock(LegacyMeetingAdapterService.class),
meetingQueryService,
mock(MeetingAccessService.class),
mock(MeetingCommandService.class),
meetingService,
aiTaskService,
mock(PromptTemplateService.class),
transcriptMapper,
mock(SysUserMapper.class)
);
LegacyApiResponse<LegacyMeetingListResponse> response = controller.list(null, 1, 10, null);
assertEquals("200", response.getCode());
assertNotNull(response.getData());
assertEquals(1, response.getData().getMeetings().size());
LegacyMeetingItemResponse item = response.getData().getMeetings().get(0);
assertEquals("summarizing", item.getOverallStatus());
assertEquals(75, item.getOverallProgress());
assertEquals("llm", item.getCurrentStage());
}
@Test @Test
void updateAccessPasswordShouldOnlyAllowCreator() { void updateAccessPasswordShouldOnlyAllowCreator() {
MeetingAccessService meetingAccessService = mock(MeetingAccessService.class); MeetingAccessService meetingAccessService = mock(MeetingAccessService.class);
@ -135,4 +532,86 @@ class LegacyMeetingControllerTest {
assertEquals(null, meeting.getAccessPassword()); assertEquals(null, meeting.getAccessPassword());
verify(meetingService).updateById(meeting); verify(meetingService).updateById(meeting);
} }
@Test
void previewDataShouldTranslateRealtimeProgressBelowNinetyToTranscription() {
MeetingService meetingService = mock(MeetingService.class);
AiTaskService aiTaskService = mock(AiTaskService.class);
MeetingTranscriptMapper transcriptMapper = mock(MeetingTranscriptMapper.class);
StringRedisTemplate redisTemplate = mock(StringRedisTemplate.class);
@SuppressWarnings("unchecked")
ValueOperations<String, String> valueOperations = mock(ValueOperations.class);
Meeting meeting = new Meeting();
meeting.setId(26L);
meeting.setTitle("progress translating");
meeting.setStatus(0);
when(meetingService.getById(26L)).thenReturn(meeting);
when(aiTaskService.getOne(any())).thenReturn((AiTask) null, (AiTask) null);
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(valueOperations.get("biz:meeting:progress:26")).thenReturn("{\"percent\":45}");
LegacyMeetingController controller = new LegacyMeetingController(
mock(LegacyMeetingAdapterService.class),
mock(MeetingQueryService.class),
mock(MeetingAccessService.class),
mock(MeetingCommandService.class),
meetingService,
aiTaskService,
mock(PromptTemplateService.class),
transcriptMapper,
mock(SysUserMapper.class),
redisTemplate,
new ObjectMapper()
);
LegacyApiResponse<?> response = controller.previewData(26L);
assertEquals("400", response.getCode());
LegacyMeetingPreviewDataResponse data = (LegacyMeetingPreviewDataResponse) response.getData();
assertNotNull(data);
assertEquals(50, data.getProcessingStatus().getOverallProgress());
assertEquals("audio_transcription", data.getProcessingStatus().getCurrentStage());
}
@Test
void previewDataShouldTranslateRealtimeProgressAtNinetyToSummaryStage() {
MeetingService meetingService = mock(MeetingService.class);
AiTaskService aiTaskService = mock(AiTaskService.class);
MeetingTranscriptMapper transcriptMapper = mock(MeetingTranscriptMapper.class);
StringRedisTemplate redisTemplate = mock(StringRedisTemplate.class);
@SuppressWarnings("unchecked")
ValueOperations<String, String> valueOperations = mock(ValueOperations.class);
Meeting meeting = new Meeting();
meeting.setId(27L);
meeting.setTitle("summary by progress");
meeting.setStatus(0);
when(meetingService.getById(27L)).thenReturn(meeting);
when(aiTaskService.getOne(any())).thenReturn((AiTask) null, (AiTask) null);
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(valueOperations.get("biz:meeting:progress:27")).thenReturn("{\"percent\":90}");
LegacyMeetingController controller = new LegacyMeetingController(
mock(LegacyMeetingAdapterService.class),
mock(MeetingQueryService.class),
mock(MeetingAccessService.class),
mock(MeetingCommandService.class),
meetingService,
aiTaskService,
mock(PromptTemplateService.class),
transcriptMapper,
mock(SysUserMapper.class),
redisTemplate,
new ObjectMapper()
);
LegacyApiResponse<?> response = controller.previewData(27L);
assertEquals("400", response.getCode());
LegacyMeetingPreviewDataResponse data = (LegacyMeetingPreviewDataResponse) response.getData();
assertNotNull(data);
assertEquals(75, data.getProcessingStatus().getOverallProgress());
assertEquals("summary_generation", data.getProcessingStatus().getCurrentStage());
}
} }

View File

@ -1,5 +1,7 @@
package com.imeeting.service.android.legacy; package com.imeeting.service.android.legacy;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imeeting.dto.android.legacy.LegacyClientDownloadResponse; import com.imeeting.dto.android.legacy.LegacyClientDownloadResponse;
import com.imeeting.dto.android.legacy.LegacyExternalAppItemResponse; import com.imeeting.dto.android.legacy.LegacyExternalAppItemResponse;
import com.imeeting.entity.biz.ClientDownload; import com.imeeting.entity.biz.ClientDownload;
@ -16,6 +18,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -93,4 +96,44 @@ class LegacyCatalogAdapterServiceImplTest {
assertEquals("管理员", responses.get(0).getCreatorUsername()); assertEquals("管理员", responses.get(0).getCreatorUsername());
assertEquals(1, responses.get(0).getIsActive()); assertEquals(1, responses.get(0).getIsActive());
} }
@Test
void listActiveExternalAppsShouldSerializeAppInfoKeysAsSnakeCase() throws Exception {
ExternalAppMapper externalAppMapper = mock(ExternalAppMapper.class);
SysUserMapper sysUserMapper = mock(SysUserMapper.class);
ExternalApp app = new ExternalApp();
app.setId(102L);
app.setAppName("鎵撳崱宸ュ叿");
app.setAppType("native");
app.setAppInfo(Map.of(
"versionName", "2.1.0",
"webUrl", "https://board.example.com",
"nestedConfig", Map.of("packageName", "com.example.clockin"),
"launchTargets", List.of(Map.of("apkUrl", "https://dl.example.com/app.apk"))
));
app.setStatus(1);
app.setCreatedBy(1L);
when(externalAppMapper.selectList(any())).thenReturn(List.of(app));
SysUser creator = new SysUser();
creator.setUserId(1L);
creator.setDisplayName("admin");
when(sysUserMapper.selectBatchIds(List.of(1L))).thenReturn(List.of(creator));
LegacyCatalogAdapterServiceImpl service = new LegacyCatalogAdapterServiceImpl(
mock(ClientDownloadMapper.class),
externalAppMapper,
sysUserMapper
);
List<LegacyExternalAppItemResponse> responses = service.listActiveExternalApps();
JsonNode json = new ObjectMapper().readTree(new ObjectMapper().writeValueAsBytes(responses.get(0)));
assertTrue(json.has("app_info"));
assertEquals("2.1.0", json.path("app_info").path("version_name").asText());
assertEquals("https://board.example.com", json.path("app_info").path("web_url").asText());
assertEquals("com.example.clockin", json.path("app_info").path("nested_config").path("package_name").asText());
assertEquals("https://dl.example.com/app.apk", json.path("app_info").path("launch_targets").get(0).path("apk_url").asText());
}
} }

View File

@ -6,6 +6,7 @@ import { menuRoutes,extraRoutes } from "./routes";
const Login = lazy(() => import("@/pages/auth/login")); const Login = lazy(() => import("@/pages/auth/login"));
const ResetPassword = lazy(() => import("@/pages/auth/reset-password")); const ResetPassword = lazy(() => import("@/pages/auth/reset-password"));
const MeetingPreview = lazy(() => import("@/pages/business/MeetingPreview"));
function RouteFallback() { function RouteFallback() {
return <div className="app-page__empty-state" style={{ minHeight: 320 }}>Loading...</div>; return <div className="app-page__empty-state" style={{ minHeight: 320 }}>Loading...</div>;
@ -31,6 +32,14 @@ export default function AppRoutes() {
<Routes> <Routes>
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/reset-password" element={<ResetPassword />} /> <Route path="/reset-password" element={<ResetPassword />} />
<Route
path="/meetings/:id/preview"
element={
<RequireAuth>
<MeetingPreview />
</RequireAuth>
}
/>
<Route <Route
path="/" path="/"
element={ element={