refactor: 移除 `ExistingOfflineMeetingException` 并更新相关逻辑

- 移除 `ExistingOfflineMeetingException` 类
- 更新 `AndroidMeetingController` 中的异常处理,使用 `BusinessException` 和 `BusinessErrorCodeEnum`
- 优化 `AndroidDeviceHomeServiceImpl`,添加 `TenantMeetingPointsSettingService` 依赖并更新积分校验逻辑
- 将 `AboutPage` 页面移至 `ProfilePage` 的模态框中
- 更新 `MeetingUnifiedStatusServiceImpl` 的代码格式和逻辑顺序
dev_na
chenhao 2026-06-12 14:00:45 +08:00
parent fd9ef5c885
commit c64c8b5690
11 changed files with 301 additions and 408 deletions

View File

@ -1,13 +0,0 @@
package com.imeeting.common.exception;
import lombok.Getter;
@Getter
public class ExistingOfflineMeetingException extends RuntimeException {
private final Long meetingId;
public ExistingOfflineMeetingException(Long meetingId) {
super("有未结束会议");
this.meetingId = meetingId;
}
}

View File

@ -4,7 +4,6 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.imeeting.common.MeetingConstants;
import com.imeeting.common.SysParamKeys;
import com.imeeting.common.exception.ExistingOfflineMeetingException;
import com.imeeting.dto.android.AndroidAuthContext;
import com.imeeting.dto.android.AndroidOfflineMeetingCreateCommand;
import com.imeeting.dto.android.AndroidMeetingCreateResponse;
@ -28,6 +27,7 @@ import com.imeeting.dto.biz.UnifiedMeetingStatusVO;
import com.imeeting.entity.biz.AiTask;
import com.imeeting.entity.biz.Meeting;
import com.imeeting.entity.biz.PromptTemplate;
import com.imeeting.enums.BusinessErrorCodeEnum;
import com.imeeting.enums.MeetingStatusEnum;
import com.imeeting.service.android.AndroidAuthService;
import com.imeeting.service.android.AndroidChunkUploadService;
@ -37,6 +37,7 @@ import com.imeeting.service.biz.*;
import com.unisbase.annotation.Anonymous;
import com.unisbase.common.ApiResponse;
import com.unisbase.common.annotation.Log;
import com.unisbase.common.exception.BusinessException;
import com.unisbase.dto.PageResult;
import com.unisbase.entity.SysTenant;
import com.unisbase.entity.SysUser;
@ -166,16 +167,13 @@ public class AndroidMeetingController {
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
resolvePublicDeviceTenantId(request, command, authContext);
LoginUser loginUser = authContext.isAnonymous() ? null : AndroidLoginUserSupport.requireLoginUser(authContext);
try {
// Meeting existingMeeting = findLatestUnfinishedMeetingByDevice(authContext.getDeviceId());
// if (existingMeeting != null) {
// return new ApiResponse<>("409", "设备端已有会议", meetingQueryService.getDetailIgnoreTenant(existingMeeting.getId()));
// }
MeetingVO meeting = legacyMeetingAdapterService.createMeeting(command, authContext, loginUser);
return ApiResponse.ok(buildAndroidMeetingCreateResponse(meeting));
} catch (ExistingOfflineMeetingException ex) {
return new ApiResponse<>("409", "有未结束会议", new AndroidOfflineMeetingConflictVO(ex.getMeetingId()));
}
}
@Operation(summary = "上传Android会议音频")
@ -292,23 +290,8 @@ public class AndroidMeetingController {
return ApiResponse.ok(buildAndroidMeetingListPage(result));
}
@Operation(summary = "查询Android会议预览数据")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "返回会议预览结果,包含已完成摘要或处理中状态信息",
content = @Content(schema = @Schema(implementation = LegacyMeetingPreviewDataResponse.class))
)
})
@GetMapping("/{meetingId}/preview-data")
public ApiResponse<LegacyMeetingPreviewDataResponse> previewData(HttpServletRequest request, @PathVariable Long meetingId) {
AndroidRequestLogHelper.logRequest(log, "Android会议", "查询会议预览数据接口", "meetingId", meetingId);
androidAuthService.authenticateHttp(request);
LegacyMeetingPreviewResult result = buildPreviewResult(meetingId);
return new ApiResponse<>(result.getCode(), result.getMessage(), result.getData());
}
@Operation(summary = "查询Android会议统一状态")
@Operation(summary = "查询Android会议统一状态")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
@ -560,7 +543,7 @@ public class AndroidMeetingController {
private MeetingVO requireOperableOfflineMeeting(Long meetingId, AndroidAuthContext authContext, LoginUser loginUser) {
MeetingVO meeting = meetingQueryService.getDetailIgnoreTenant(meetingId);
if (meeting == null) {
throw new RuntimeException("会议不存在");
throw new BusinessException(BusinessErrorCodeEnum.MEETING_NOT_FOUND.getCode(), "会议不存在");
}
if (!MeetingConstants.TYPE_OFFLINE.equals(meeting.getMeetingType())) {
throw new RuntimeException("当前会议不是离线会议");
@ -588,107 +571,6 @@ public class AndroidMeetingController {
&& MeetingConstants.OFFLINE_RECORDING_UPLOAD_FINISHED.equalsIgnoreCase(command.getFinishStage());
}
private LegacyMeetingPreviewResult buildPreviewResult(Long meetingId) {
Meeting meeting = meetingService.getById(meetingId);
if (meeting == null) {
return new LegacyMeetingPreviewResult("404", "会议不存在", null);
}
AiTask asrTask = findLatestTask(meetingId, "ASR");
AiTask summaryTask = findLatestTask(meetingId, "SUMMARY");
boolean summaryCompleted = summaryTask != null && Integer.valueOf(2).equals(summaryTask.getStatus());
MeetingVO detail = (MeetingStatusEnum.isCode(meeting.getStatus(), MeetingStatusEnum.COMPLETED) || summaryCompleted)
? meetingQueryService.getDetail(meetingId)
: null;
boolean hasSummary = detail != null && detail.getSummaryContent() != null && !detail.getSummaryContent().isBlank();
if (hasSummary) {
return new LegacyMeetingPreviewResult("200", "success", buildCompletedPreview(meeting, detail, summaryTask));
}
if (summaryCompleted) {
return new LegacyMeetingPreviewResult(
"504",
"处理已完成,但摘要尚未同步,请稍后重试",
buildProcessingPreview(meeting, summaryTask, processingStatus("摘要已生成,可查看详情", 100, STAGE_COMPLETED))
);
}
if (isFailed(asrTask)) {
return new LegacyMeetingPreviewResult(
"503",
buildFailureMessage(asrTask, "转写"),
buildProcessingPreview(meeting, summaryTask, processingStatus("转写或总结失败", 50, STAGE_AUDIO_TRANSCRIPTION))
);
}
if (isFailed(summaryTask)) {
return new LegacyMeetingPreviewResult(
"503",
buildFailureMessage(summaryTask, "总结"),
buildProcessingPreview(meeting, summaryTask, processingStatus("转写或总结失败", 75, STAGE_SUMMARY_GENERATION))
);
}
Integer realtimeProgress = resolveRealtimeProgress(meetingId);
if (asrTask != null && Integer.valueOf(0).equals(asrTask.getStatus()) && realtimeProgress != null && realtimeProgress <= 0) {
return new LegacyMeetingPreviewResult(
"400",
"会议正在处理中",
buildProcessingPreview(meeting, summaryTask, processingStatus("会议数据准备中", 25, STAGE_DATA_INITIALIZATION))
);
}
if (realtimeProgress != null) {
if (realtimeProgress >= 100) {
MeetingVO completedDetail = detail != null ? detail : meetingQueryService.getDetail(meetingId);
boolean completedHasSummary = completedDetail != null
&& completedDetail.getSummaryContent() != null
&& !completedDetail.getSummaryContent().isBlank();
if (completedHasSummary) {
return new LegacyMeetingPreviewResult("200", "success", buildCompletedPreview(meeting, completedDetail, summaryTask));
}
return new LegacyMeetingPreviewResult(
"504",
"处理已完成,但摘要尚未同步,请稍后重试",
buildProcessingPreview(meeting, summaryTask, processingStatus("摘要已生成,可查看详情", 100, STAGE_COMPLETED))
);
}
if (realtimeProgress < 90) {
return new LegacyMeetingPreviewResult(
"400",
"会议正在处理中",
buildProcessingPreview(meeting, summaryTask, processingStatus("正在转写音频", 50, STAGE_AUDIO_TRANSCRIPTION))
);
}
if (realtimeProgress >= 90) {
return new LegacyMeetingPreviewResult(
"400",
"会议正在处理中",
buildProcessingPreview(meeting, summaryTask, processingStatus("正在生成总结", 75, STAGE_SUMMARY_GENERATION))
);
}
}
boolean isSummaryStage = isSummaryStage(meeting.getStatus(), summaryTask);
boolean isAsrStage = isAsrStage(meeting.getStatus(), asrTask, hasAudio(meeting), isSummaryStage);
if (!isAsrStage && !isSummaryStage) {
return new LegacyMeetingPreviewResult(
"400",
"会议正在处理中",
buildProcessingPreview(meeting, summaryTask, processingStatus("会议数据准备中", 25, STAGE_DATA_INITIALIZATION))
);
}
if (!isSummaryStage) {
return new LegacyMeetingPreviewResult(
"400",
"会议正在处理中",
buildProcessingPreview(meeting, summaryTask, processingStatus("正在转写音频", 50, STAGE_AUDIO_TRANSCRIPTION))
);
}
return new LegacyMeetingPreviewResult(
"400",
"会议正在处理中",
buildProcessingPreview(meeting, summaryTask, processingStatus("正在生成总结", 75, STAGE_SUMMARY_GENERATION))
);
}
private LegacyMeetingPreviewDataResponse buildCompletedPreview(Meeting meeting, MeetingVO detail, AiTask summaryTask) {
LegacyMeetingPreviewDataResponse data = new LegacyMeetingPreviewDataResponse();

View File

@ -33,4 +33,6 @@ public class AndroidDeviceHomeStatsVO {
@Schema(description = "是否已登录")
private Boolean loggedIn;
@Schema(description = "是否开启余额校验")
private Boolean balanceCheckEnabled;
}

View File

@ -0,0 +1,18 @@
package com.imeeting.enums;
import lombok.Getter;
@Getter
public enum BusinessErrorCodeEnum {
MEETING_NOT_FOUND("40001", "会议不存在"),
;
private final String code;
private final String desc;
BusinessErrorCodeEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
}

View File

@ -28,6 +28,7 @@ public class AndroidAuthServiceImpl implements AndroidAuthService {
private static final String HEADER_DEVICE_ID = "X-Android-Device-Id";
private static final String HEADER_APP_ID = "X-Android-App-Id";
private static final String HEADER_TENANT_CODE = "X-Tenant-Code";
private static final String HEADER_APP_VERSION = "X-Android-App-Version";
private static final String HEADER_PLATFORM = "X-Android-Platform";
private static final String HEADER_AUTHORIZATION = "Authorization";

View File

@ -11,12 +11,14 @@ import com.imeeting.dto.android.AndroidDeviceWeatherCacheValue;
import com.imeeting.dto.biz.MeetingPointsBalanceVO;
import com.imeeting.entity.biz.DeviceInfoEntity;
import com.imeeting.entity.biz.LicenseEntity;
import com.imeeting.entity.biz.TenantMeetingPointsSetting;
import com.imeeting.mapper.DeviceInfoMapper;
import com.imeeting.mapper.DeviceLoginLogMapper;
import com.imeeting.mapper.LicenseMapper;
import com.imeeting.mapper.biz.MeetingMapper;
import com.imeeting.service.android.AndroidDeviceHomeService;
import com.imeeting.service.biz.MeetingPointsService;
import com.imeeting.service.biz.TenantMeetingPointsSettingService;
import com.imeeting.support.RedisSupport;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -55,8 +57,9 @@ public class AndroidDeviceHomeServiceImpl implements AndroidDeviceHomeService {
private final RedisSupport redisSupport;
private final com.unisbase.service.SysParamService sysParamService;
private final ObjectMapper objectMapper;
private final TenantMeetingPointsSettingService tenantMeetingPointsSettingService;
@Value("${imeeting.h5.base-url:}")
@Value("${imeeting.h5.base-url:}")
private String h5BaseUrl;
private final HttpClient httpClient = HttpClient.newBuilder()
@ -95,7 +98,11 @@ public class AndroidDeviceHomeServiceImpl implements AndroidDeviceHomeService {
vo.setRemainingMinutes(calculateRemainingMinutes(tenantId, authContext.getUserId(), authContext.isAnonymous()));
vo.setWeather(resolveWeather(device.getWeatherCityName()));
vo.setLoggedIn(!authContext.isAnonymous() && authContext.getUserId() != null);
return vo;
TenantMeetingPointsSetting byTenantId = tenantMeetingPointsSettingService.getByTenantId(authContext.getTenantId());
vo.setBalanceCheckEnabled(byTenantId == null || byTenantId.getBalanceCheckEnabled() == 1);
return vo;
}
private Long calculateRemainingMinutes(Long tenantId, Long userId, boolean anonymous) {

View File

@ -97,7 +97,8 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
Meeting existingMeeting = findLatestBlockingOfflineMeeting(authContext == null ? null : authContext.getDeviceId(), creatorUserId);
if (existingMeeting != null) {
throw new ExistingOfflineMeetingException(existingMeeting.getId());
existingMeeting.setOfflineRecordingStatus(MeetingConstants.OFFLINE_RECORDING_PRE_END);
meetingService.updateById(existingMeeting);
}
Long requestedSummaryModelId = request instanceof AndroidOfflineMeetingCreateCommand androidCommand

View File

@ -26,260 +26,260 @@ import java.util.Objects;
@RequiredArgsConstructor
public class MeetingUnifiedStatusServiceImpl implements MeetingUnifiedStatusService {
private final MeetingMapper meetingMapper;
private final AiTaskMapper aiTaskMapper;
private final MeetingTranscriptMapper meetingTranscriptMapper;
private final MeetingTranscriptChapterVersionMapper chapterVersionMapper;
private final MeetingProgressCache meetingProgressCache;
private final MeetingMapper meetingMapper;
private final AiTaskMapper aiTaskMapper;
private final MeetingTranscriptMapper meetingTranscriptMapper;
private final MeetingTranscriptChapterVersionMapper chapterVersionMapper;
private final MeetingProgressCache meetingProgressCache;
@Override
public UnifiedMeetingStatusVO resolve(MeetingVO meeting, MeetingProgressSnapshot snapshot) {
if (meeting == null || meeting.getId() == null) {
return null;
}
UnifiedMeetingStatusStage stage = resolveStage(meeting, snapshot);
UnifiedMeetingStatusStage failedStage = resolveFailedStage(meeting);
boolean failed = failedStage != null;
UnifiedMeetingStatusStage effectiveStage = failed ? failedStage : stage;
return UnifiedMeetingStatusVO.builder()
.meetingId(meeting.getId())
.statusCode(effectiveStage.getCode())
.statusText(effectiveStage.getText())
.percent(resolvePercent(snapshot, effectiveStage))
.message(resolveMessage(meeting, snapshot, effectiveStage))
.eta(snapshot == null ? null : snapshot.getEta())
.failedStageCode(failedStage == null ? null : failedStage.getCode())
.failedStageText(failedStage == null ? null : failedStage.getText())
.canViewTranscript(canViewTranscript(meeting.getId()))
.canViewAiChapters(canViewAiChapters(meeting.getId()))
.canViewSummary(canViewSummary(meeting))
.build();
@Override
public UnifiedMeetingStatusVO resolve(MeetingVO meeting, MeetingProgressSnapshot snapshot) {
if (meeting == null || meeting.getId() == null) {
return null;
}
@Override
public UnifiedMeetingStatusVO resolve(Long meetingId) {
if (meetingId == null) {
return null;
}
Meeting meeting = meetingMapper.selectByIdIgnoreTenant(meetingId);
if (meeting == null) {
return null;
}
return resolve(toMeetingVO(meeting), meetingProgressCache.getSnapshot(meetingId));
UnifiedMeetingStatusStage stage = resolveStage(meeting, snapshot);
UnifiedMeetingStatusStage failedStage = resolveFailedStage(meeting);
boolean failed = failedStage != null;
UnifiedMeetingStatusStage effectiveStage = failed ? failedStage : stage;
return UnifiedMeetingStatusVO.builder()
.meetingId(meeting.getId())
.statusCode(effectiveStage.getCode())
.statusText(effectiveStage.getText())
.percent(resolvePercent(snapshot, effectiveStage))
.message(resolveMessage(meeting, snapshot, effectiveStage))
.eta(snapshot == null ? null : snapshot.getEta())
.failedStageCode(failedStage == null ? null : failedStage.getCode())
.failedStageText(failedStage == null ? null : failedStage.getText())
.canViewTranscript(canViewTranscript(meeting.getId()))
.canViewAiChapters(canViewAiChapters(meeting.getId()))
.canViewSummary(canViewSummary(meeting))
.build();
}
@Override
public UnifiedMeetingStatusVO resolve(Long meetingId) {
if (meetingId == null) {
return null;
}
Meeting meeting = meetingMapper.selectByIdIgnoreTenant(meetingId);
if (meeting == null) {
return null;
}
return resolve(toMeetingVO(meeting), meetingProgressCache.getSnapshot(meetingId));
}
private MeetingUnifiedStageContext buildStageContext(Long meetingId, MeetingProgressSnapshot snapshot) {
AiTask latestAsr = findLatestTask(meetingId, "ASR");
AiTask latestChapter = findLatestTask(meetingId, "CHAPTER");
AiTask latestSummary = findLatestTask(meetingId, "SUMMARY");
return new MeetingUnifiedStageContext(latestAsr, latestChapter, latestSummary, snapshot);
}
private UnifiedMeetingStatusStage resolveStage(MeetingVO meeting, MeetingProgressSnapshot snapshot) {
if (meeting == null) {
return UnifiedMeetingStatusStage.INITIALIZING;
}
if (MeetingStatusEnum.isCode(meeting.getStatus(), MeetingStatusEnum.COMPLETED)) {
return UnifiedMeetingStatusStage.COMPLETED;
}
if (isAndroidOfflineMeetingWaitingUpload(meeting)) {
return UnifiedMeetingStatusStage.WAITING_UPLOAD;
}
UnifiedMeetingStatusStage stageFromSnapshot = resolveStageFromSnapshot(snapshot);
if (stageFromSnapshot != null) {
return stageFromSnapshot;
}
private MeetingUnifiedStageContext buildStageContext(Long meetingId, MeetingProgressSnapshot snapshot) {
AiTask latestAsr = findLatestTask(meetingId, "ASR");
AiTask latestChapter = findLatestTask(meetingId, "CHAPTER");
AiTask latestSummary = findLatestTask(meetingId, "SUMMARY");
return new MeetingUnifiedStageContext(latestAsr, latestChapter, latestSummary, snapshot);
MeetingUnifiedStageContext context = buildStageContext(meeting.getId(), snapshot);
if (isTranscribing(context)) {
return UnifiedMeetingStatusStage.TRANSCRIBING;
}
if (isSummarizing(context)) {
return UnifiedMeetingStatusStage.SUMMARIZING;
}
private UnifiedMeetingStatusStage resolveStage(MeetingVO meeting, MeetingProgressSnapshot snapshot) {
if (meeting == null) {
return UnifiedMeetingStatusStage.INITIALIZING;
}
if (MeetingStatusEnum.isCode(meeting.getStatus(), MeetingStatusEnum.COMPLETED)) {
return UnifiedMeetingStatusStage.COMPLETED;
}
if (isAndroidOfflineMeetingWaitingUpload(meeting)) {
return UnifiedMeetingStatusStage.WAITING_UPLOAD;
}
UnifiedMeetingStatusStage stageFromSnapshot = resolveStageFromSnapshot(snapshot);
if (stageFromSnapshot != null) {
return stageFromSnapshot;
}
return UnifiedMeetingStatusStage.INITIALIZING;
}
MeetingUnifiedStageContext context = buildStageContext(meeting.getId(), snapshot);
if (isTranscribing(context)) {
return UnifiedMeetingStatusStage.TRANSCRIBING;
}
if (isSummarizing(context)) {
return UnifiedMeetingStatusStage.SUMMARIZING;
}
private UnifiedMeetingStatusStage resolveStageFromSnapshot(MeetingProgressSnapshot snapshot) {
if (snapshot == null || snapshot.getStage() == null || snapshot.getStage().isBlank()) {
return null;
}
return switch (snapshot.getStage()) {
case "failed" -> null;
case "completed" -> UnifiedMeetingStatusStage.COMPLETED;
case "summary_running", "chapter_running" -> UnifiedMeetingStatusStage.SUMMARIZING;
case "asr_running", "asr_completed", "asr_submitted" -> UnifiedMeetingStatusStage.TRANSCRIBING;
case "queued" -> UnifiedMeetingStatusStage.INITIALIZING;
default -> null;
};
}
return UnifiedMeetingStatusStage.INITIALIZING;
private UnifiedMeetingStatusStage resolveFailedStage(MeetingVO meeting) {
if (meeting == null || !MeetingStatusEnum.isCode(meeting.getStatus(), MeetingStatusEnum.FAILED)) {
return null;
}
AiTask asrTask = findLatestTask(meeting.getId(), "ASR");
if (isTaskFailed(asrTask)) {
return UnifiedMeetingStatusStage.FAILED_TRANSCRIBING;
}
AiTask summaryTask = findLatestTask(meeting.getId(), "SUMMARY");
if (isTaskFailed(summaryTask)) {
return UnifiedMeetingStatusStage.FAILED_SUMMARIZING;
}
AiTask chapterTask = findLatestTask(meeting.getId(), "CHAPTER");
if (isTaskFailed(chapterTask)) {
return UnifiedMeetingStatusStage.FAILED_SUMMARIZING;
}
private UnifiedMeetingStatusStage resolveStageFromSnapshot(MeetingProgressSnapshot snapshot) {
if (snapshot == null || snapshot.getStage() == null || snapshot.getStage().isBlank()) {
return null;
}
return switch (snapshot.getStage()) {
case "failed" -> null;
case "completed" -> UnifiedMeetingStatusStage.COMPLETED;
case "summary_running", "chapter_running" -> UnifiedMeetingStatusStage.SUMMARIZING;
case "asr_running", "asr_completed", "asr_submitted" -> UnifiedMeetingStatusStage.TRANSCRIBING;
case "queued" -> UnifiedMeetingStatusStage.INITIALIZING;
default -> null;
};
}
return UnifiedMeetingStatusStage.FAILED_INITIALIZING;
}
private UnifiedMeetingStatusStage resolveFailedStage(MeetingVO meeting) {
if (meeting == null || !MeetingStatusEnum.isCode(meeting.getStatus(), MeetingStatusEnum.FAILED)) {
return null;
}
private boolean isAndroidOfflineMeetingWaitingUpload(MeetingVO meeting) {
return meeting != null
&& MeetingConstants.TYPE_OFFLINE.equalsIgnoreCase(meeting.getMeetingType())
&& MeetingConstants.SOURCE_ANDROID.equalsIgnoreCase(meeting.getMeetingSource())
&& !MeetingConstants.OFFLINE_RECORDING_UPLOAD_FINISHED.equalsIgnoreCase(meeting.getOfflineRecordingStatus());
}
AiTask summaryTask = findLatestTask(meeting.getId(), "SUMMARY");
if (isTaskFailed(summaryTask)) {
return UnifiedMeetingStatusStage.FAILED_SUMMARIZING;
}
AiTask chapterTask = findLatestTask(meeting.getId(), "CHAPTER");
if (isTaskFailed(chapterTask)) {
return UnifiedMeetingStatusStage.FAILED_SUMMARIZING;
}
AiTask asrTask = findLatestTask(meeting.getId(), "ASR");
if (isTaskFailed(asrTask)) {
return UnifiedMeetingStatusStage.FAILED_TRANSCRIBING;
}
return UnifiedMeetingStatusStage.FAILED_INITIALIZING;
}
private boolean isSummarizing(MeetingUnifiedStageContext context) {
return isTaskRunning(context.summaryTask())
|| isTaskRunning(context.chapterTask())
|| isTaskCompleted(context.chapterTask())
|| isTaskCompleted(context.summaryTask());
}
private boolean isAndroidOfflineMeetingWaitingUpload(MeetingVO meeting) {
return meeting != null
&& MeetingConstants.TYPE_OFFLINE.equalsIgnoreCase(meeting.getMeetingType())
&& MeetingConstants.SOURCE_ANDROID.equalsIgnoreCase(meeting.getMeetingSource())
&& !MeetingConstants.OFFLINE_RECORDING_UPLOAD_FINISHED.equalsIgnoreCase(meeting.getOfflineRecordingStatus());
private boolean isTranscribing(MeetingUnifiedStageContext context) {
if (isTaskRunningOrQueued(context.summaryTask()) || isTaskRunningOrQueued(context.chapterTask())) {
return false;
}
return isTaskRunningOrQueued(context.asrTask()) || isTaskCompleted(context.asrTask());
}
private boolean isSummarizing(MeetingUnifiedStageContext context) {
return isTaskRunning(context.summaryTask())
|| isTaskRunning(context.chapterTask())
|| isTaskCompleted(context.chapterTask())
|| isTaskCompleted(context.summaryTask());
private Integer resolvePercent(MeetingProgressSnapshot snapshot, UnifiedMeetingStatusStage stage) {
if (snapshot != null && snapshot.getPercent() != null) {
return snapshot.getPercent();
}
return switch (stage) {
case WAITING_UPLOAD -> 0;
case INITIALIZING -> 5;
case TRANSCRIBING -> 50;
case SUMMARIZING -> 90;
case COMPLETED -> 100;
case FAILED_INITIALIZING, FAILED_TRANSCRIBING, FAILED_SUMMARIZING -> -1;
};
}
private boolean isTranscribing(MeetingUnifiedStageContext context) {
if (isTaskRunningOrQueued(context.summaryTask()) || isTaskRunningOrQueued(context.chapterTask())) {
return false;
}
return isTaskRunningOrQueued(context.asrTask()) || isTaskCompleted(context.asrTask());
private String resolveMessage(MeetingVO meeting, MeetingProgressSnapshot snapshot, UnifiedMeetingStatusStage stage) {
if (stage == UnifiedMeetingStatusStage.WAITING_UPLOAD) {
return "待上传录音文件";
}
if (snapshot != null && snapshot.getMessage() != null && !snapshot.getMessage().isBlank() && !Objects.equals(snapshot.getMessage(), "Waiting...")) {
return snapshot.getMessage();
}
if (stage == UnifiedMeetingStatusStage.FAILED_INITIALIZING
|| stage == UnifiedMeetingStatusStage.FAILED_TRANSCRIBING
|| stage == UnifiedMeetingStatusStage.FAILED_SUMMARIZING) {
return resolveFailureMessage(meeting);
}
return switch (stage) {
case WAITING_UPLOAD -> "待上传录音文件";
case INITIALIZING -> "数据初始化";
case TRANSCRIBING -> "转译音频";
case SUMMARIZING -> "生成总结";
case COMPLETED -> "处理完成";
case FAILED_INITIALIZING -> "数据初始化失败";
case FAILED_TRANSCRIBING -> "转译音频失败";
case FAILED_SUMMARIZING -> "生成总结失败";
};
}
private Integer resolvePercent(MeetingProgressSnapshot snapshot, UnifiedMeetingStatusStage stage) {
if (snapshot != null && snapshot.getPercent() != null) {
return snapshot.getPercent();
}
return switch (stage) {
case WAITING_UPLOAD -> 0;
case INITIALIZING -> 5;
case TRANSCRIBING -> 50;
case SUMMARIZING -> 90;
case COMPLETED -> 100;
case FAILED_INITIALIZING, FAILED_TRANSCRIBING, FAILED_SUMMARIZING -> -1;
};
private String resolveFailureMessage(MeetingVO meeting) {
if (meeting == null) {
return "处理失败";
}
if (meeting.getLatestSummaryAttemptErrorMsg() != null && !meeting.getLatestSummaryAttemptErrorMsg().isBlank()) {
return meeting.getLatestSummaryAttemptErrorMsg();
}
if (meeting.getLatestChapterAttemptErrorMsg() != null && !meeting.getLatestChapterAttemptErrorMsg().isBlank()) {
return meeting.getLatestChapterAttemptErrorMsg();
}
return "处理失败";
}
private String resolveMessage(MeetingVO meeting, MeetingProgressSnapshot snapshot, UnifiedMeetingStatusStage stage) {
if (stage == UnifiedMeetingStatusStage.WAITING_UPLOAD) {
return "待上传录音文件";
}
if (snapshot != null && snapshot.getMessage() != null && !snapshot.getMessage().isBlank() && !Objects.equals(snapshot.getMessage(), "Waiting...")) {
return snapshot.getMessage();
}
if (stage == UnifiedMeetingStatusStage.FAILED_INITIALIZING
|| stage == UnifiedMeetingStatusStage.FAILED_TRANSCRIBING
|| stage == UnifiedMeetingStatusStage.FAILED_SUMMARIZING) {
return resolveFailureMessage(meeting);
}
return switch (stage) {
case WAITING_UPLOAD -> "待上传录音文件";
case INITIALIZING -> "数据初始化";
case TRANSCRIBING -> "转译音频";
case SUMMARIZING -> "生成总结";
case COMPLETED -> "处理完成";
case FAILED_INITIALIZING -> "数据初始化失败";
case FAILED_TRANSCRIBING -> "转译音频失败";
case FAILED_SUMMARIZING -> "生成总结失败";
};
}
private boolean canViewTranscript(Long meetingId) {
return meetingId != null && meetingTranscriptMapper.selectCount(new LambdaQueryWrapper<MeetingTranscript>()
.eq(MeetingTranscript::getMeetingId, meetingId)) > 0;
}
private String resolveFailureMessage(MeetingVO meeting) {
if (meeting == null) {
return "处理失败";
}
if (meeting.getLatestSummaryAttemptErrorMsg() != null && !meeting.getLatestSummaryAttemptErrorMsg().isBlank()) {
return meeting.getLatestSummaryAttemptErrorMsg();
}
if (meeting.getLatestChapterAttemptErrorMsg() != null && !meeting.getLatestChapterAttemptErrorMsg().isBlank()) {
return meeting.getLatestChapterAttemptErrorMsg();
}
return "处理失败";
}
private boolean canViewAiChapters(Long meetingId) {
return meetingId != null && chapterVersionMapper.selectCount(new LambdaQueryWrapper<MeetingTranscriptChapterVersion>()
.eq(MeetingTranscriptChapterVersion::getMeetingId, meetingId)
.eq(MeetingTranscriptChapterVersion::getIsCurrent, 1)
.eq(MeetingTranscriptChapterVersion::getStatus, 2)) > 0;
}
private boolean canViewTranscript(Long meetingId) {
return meetingId != null && meetingTranscriptMapper.selectCount(new LambdaQueryWrapper<MeetingTranscript>()
.eq(MeetingTranscript::getMeetingId, meetingId)) > 0;
}
private boolean canViewSummary(MeetingVO meeting) {
return meeting != null && meeting.getSummaryContent() != null && !meeting.getSummaryContent().isBlank();
}
private boolean canViewAiChapters(Long meetingId) {
return meetingId != null && chapterVersionMapper.selectCount(new LambdaQueryWrapper<MeetingTranscriptChapterVersion>()
.eq(MeetingTranscriptChapterVersion::getMeetingId, meetingId)
.eq(MeetingTranscriptChapterVersion::getIsCurrent, 1)
.eq(MeetingTranscriptChapterVersion::getStatus, 2)) > 0;
}
private AiTask findLatestTask(Long meetingId, String taskType) {
return aiTaskMapper.selectOne(new LambdaQueryWrapper<AiTask>()
.eq(AiTask::getMeetingId, meetingId)
.eq(AiTask::getTaskType, taskType)
.orderByDesc(AiTask::getId)
.last("LIMIT 1"));
}
private boolean canViewSummary(MeetingVO meeting) {
return meeting != null && meeting.getSummaryContent() != null && !meeting.getSummaryContent().isBlank();
}
private boolean isTaskRunningOrQueued(AiTask task) {
return task != null && (Integer.valueOf(0).equals(task.getStatus()) || Integer.valueOf(1).equals(task.getStatus()));
}
private AiTask findLatestTask(Long meetingId, String taskType) {
return aiTaskMapper.selectOne(new LambdaQueryWrapper<AiTask>()
.eq(AiTask::getMeetingId, meetingId)
.eq(AiTask::getTaskType, taskType)
.orderByDesc(AiTask::getId)
.last("LIMIT 1"));
}
private boolean isTaskRunning(AiTask task) {
return task != null && Integer.valueOf(1).equals(task.getStatus());
}
private boolean isTaskRunningOrQueued(AiTask task) {
return task != null && (Integer.valueOf(0).equals(task.getStatus()) || Integer.valueOf(1).equals(task.getStatus()));
}
private boolean isTaskCompleted(AiTask task) {
return task != null && Integer.valueOf(2).equals(task.getStatus());
}
private boolean isTaskRunning(AiTask task) {
return task != null && Integer.valueOf(1).equals(task.getStatus());
}
private boolean isTaskFailed(AiTask task) {
return task != null && Integer.valueOf(3).equals(task.getStatus());
}
private boolean isTaskCompleted(AiTask task) {
return task != null && Integer.valueOf(2).equals(task.getStatus());
}
private MeetingVO toMeetingVO(Meeting meeting) {
MeetingVO vo = new MeetingVO();
vo.setId(meeting.getId());
vo.setTenantId(meeting.getTenantId());
vo.setCreatorId(meeting.getCreatorId());
vo.setCreatorName(meeting.getCreatorName());
vo.setHostUserId(meeting.getHostUserId());
vo.setHostName(meeting.getHostName());
vo.setTitle(meeting.getTitle());
vo.setMeetingTime(meeting.getMeetingTime());
vo.setParticipants(meeting.getParticipants());
vo.setTags(meeting.getTags());
vo.setAudioUrl(meeting.getAudioUrl());
vo.setMeetingType(meeting.getMeetingType());
vo.setMeetingSource(meeting.getMeetingSource());
vo.setSourceDeviceCode(meeting.getSourceDeviceCode());
vo.setSourceDeviceMode(meeting.getSourceDeviceMode());
vo.setOfflineRecordingStatus(meeting.getOfflineRecordingStatus());
vo.setSummaryDetailLevel(meeting.getSummaryDetailLevel());
vo.setAudioSaveStatus(meeting.getAudioSaveStatus());
vo.setAudioSaveMessage(meeting.getAudioSaveMessage());
vo.setAccessPassword(meeting.getAccessPassword());
vo.setEffectiveAudioDurationSeconds(meeting.getEffectiveAudioDurationSeconds());
vo.setStatus(meeting.getStatus());
vo.setCreatedAt(meeting.getCreatedAt());
return vo;
}
private boolean isTaskFailed(AiTask task) {
return task != null && Integer.valueOf(3).equals(task.getStatus());
}
private MeetingVO toMeetingVO(Meeting meeting) {
MeetingVO vo = new MeetingVO();
vo.setId(meeting.getId());
vo.setTenantId(meeting.getTenantId());
vo.setCreatorId(meeting.getCreatorId());
vo.setCreatorName(meeting.getCreatorName());
vo.setHostUserId(meeting.getHostUserId());
vo.setHostName(meeting.getHostName());
vo.setTitle(meeting.getTitle());
vo.setMeetingTime(meeting.getMeetingTime());
vo.setParticipants(meeting.getParticipants());
vo.setTags(meeting.getTags());
vo.setAudioUrl(meeting.getAudioUrl());
vo.setMeetingType(meeting.getMeetingType());
vo.setMeetingSource(meeting.getMeetingSource());
vo.setSourceDeviceCode(meeting.getSourceDeviceCode());
vo.setSourceDeviceMode(meeting.getSourceDeviceMode());
vo.setOfflineRecordingStatus(meeting.getOfflineRecordingStatus());
vo.setSummaryDetailLevel(meeting.getSummaryDetailLevel());
vo.setAudioSaveStatus(meeting.getAudioSaveStatus());
vo.setAudioSaveMessage(meeting.getAudioSaveMessage());
vo.setAccessPassword(meeting.getAccessPassword());
vo.setEffectiveAudioDurationSeconds(meeting.getEffectiveAudioDurationSeconds());
vo.setStatus(meeting.getStatus());
vo.setCreatedAt(meeting.getCreatedAt());
return vo;
}
private record MeetingUnifiedStageContext(AiTask asrTask,
AiTask chapterTask,
AiTask summaryTask,
MeetingProgressSnapshot snapshot) {
}
private record MeetingUnifiedStageContext(AiTask asrTask,
AiTask chapterTask,
AiTask summaryTask,
MeetingProgressSnapshot snapshot) {
}
}

View File

@ -12,7 +12,6 @@ const MeetingDetailPage = lazy(() => import("@/pages/meeting-detail"));
const MeetingPreviewPage = lazy(() => import("@/pages/meeting-preview"));
const ProfilePage = lazy(() => import("@/pages/profile"));
const PasswordPage = lazy(() => import("@/pages/password"));
const AboutPage = lazy(() => import("@/pages/about"));
const ScanConfirmPage = lazy(() => import("@/pages/scan-confirm"));
function HomeRedirect() {
@ -67,16 +66,6 @@ export default function App() {
</ProtectedRoute>
}
/>
<Route
path="/about"
element={
<ProtectedRoute>
<MainLayout>
<AboutPage />
</MainLayout>
</ProtectedRoute>
}
/>
<Route
path="/scan-confirm/:sessionId"
element={

View File

@ -1,31 +0,0 @@
import { Card, Typography } from "antd";
import { usePlatformConfig } from "@/components/PlatformConfigProvider";
import usePageTitle from "@/hooks/usePageTitle";
import PageHeader from "@/components/PageHeader";
const { Paragraph, Title } = Typography;
export default function AboutPage() {
const { platformConfig } = usePlatformConfig();
usePageTitle("关于我们");
return (
<div className="page-stack">
<PageHeader title="关于我们" back />
<Card className="surface-card">
<Title level={4}>{platformConfig?.projectName || "iMeeting H5"}</Title>
<Paragraph>
iMeeting H5
</Paragraph>
<Paragraph>
访
</Paragraph>
<Paragraph>
support@imeeting.example.com
</Paragraph>
<Paragraph type="secondary">{platformConfig?.copyrightInfo || "Copyright © iMeeting"}</Paragraph>
</Card>
</div>
);
}

View File

@ -1,4 +1,4 @@
import { App, Avatar, Button, Card, Space, Typography } from "antd";
import {App, Avatar, Button, Card, Modal, Space, Typography} from "antd";
import { InfoCircleOutlined, LockOutlined, LogoutOutlined, RightOutlined } from "@ant-design/icons";
import { useEffect, useState, type ReactNode } from "react";
import { useNavigate } from "react-router-dom";
@ -6,6 +6,7 @@ import { useNavigate } from "react-router-dom";
import { getCurrentUser } from "@/api/user";
import LoadingScreen from "@/components/LoadingScreen";
import PageHeader from "@/components/PageHeader";
import {usePlatformConfig} from "@/components/PlatformConfigProvider";
import usePageTitle from "@/hooks/usePageTitle";
import type { UserProfile } from "@/types";
import { clearAuth, saveProfile } from "@/utils/auth";
@ -38,9 +39,11 @@ function ProfileAction({
export default function ProfilePage() {
const { message } = App.useApp();
const navigate = useNavigate();
const {platformConfig} = usePlatformConfig();
usePageTitle("个人中心");
const [loading, setLoading] = useState(true);
const [profile, setProfile] = useState<UserProfile | null>(null);
const [aboutVisible, setAboutVisible] = useState(false);
useEffect(() => {
const loadProfile = async () => {
@ -95,7 +98,7 @@ export default function ProfilePage() {
<Card className="surface-card">
<Space direction="vertical" size={12} style={{ width: "100%" }}>
<ProfileAction icon={<LockOutlined />} label="个人设置" onClick={() => navigate("/profile/password")} />
<ProfileAction icon={<InfoCircleOutlined />} label="关于我们" onClick={() => navigate("/about")} />
<ProfileAction icon={<InfoCircleOutlined/>} label="关于我们" onClick={() => setAboutVisible(true)}/>
<ProfileAction icon={<LogoutOutlined />} label="退出当前账号" onClick={handleLogout} danger />
</Space>
</Card>
@ -103,6 +106,40 @@ export default function ProfilePage() {
<Paragraph type="secondary" className="profile-footer">
访访
</Paragraph>
<Modal
title="关于我们"
open={aboutVisible}
onCancel={() => setAboutVisible(false)}
footer={null}
centered
width="90%"
styles={{body: {textAlign: "center", padding: "24px 16px"}}}
>
{/*<img src={platformConfig?.logoUrl || "/logo.svg"} alt="logo" style={{ width: 64, height: 64, marginBottom: 16 }} />*/}
<Title level={4} style={{marginBottom: 20}}>
{platformConfig?.projectName || "iMeeting H5"}
</Title>
{/*<Paragraph type="secondary" style={{ marginBottom: 16 }}>*/}
{/* 版本号v0.1.0*/}
{/*</Paragraph>*/}
<Paragraph style={{textAlign: "left"}}>
AI
</Paragraph>
{/*<Paragraph style={{ textAlign: "left" }}>*/}
{/* 第一版页面以快速访问和移动端阅读体验为优先,聚焦“查看”和“确认”两类核心动作,不混入后台管理功能。*/}
{/*</Paragraph>*/}
{/*<Paragraph style={{ textAlign: "left", marginBottom: 24 }}>*/}
{/* 联系邮箱support@imeeting.example.com*/}
{/*</Paragraph>*/}
<Paragraph type="secondary" style={{fontSize: "12px"}}>
{platformConfig?.copyrightInfo || "Copyright © iMeeting"}
</Paragraph>
</Modal>
</div>
);
}