diff --git a/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java b/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java index 509ea14..6d0d565 100644 --- a/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java +++ b/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java @@ -19,11 +19,7 @@ import com.imeeting.dto.android.legacy.LegacyMeetingPreviewDataResponse; import com.imeeting.dto.android.legacy.LegacyMeetingPreviewResult; import com.imeeting.dto.android.legacy.LegacyMeetingProcessingStatusResponse; import com.imeeting.dto.android.legacy.LegacyUploadAudioResponse; -import com.imeeting.dto.biz.AiModelVO; -import com.imeeting.dto.biz.MeetingTranscriptVO; -import com.imeeting.dto.biz.MeetingVO; -import com.imeeting.dto.biz.PromptTemplateVO; -import com.imeeting.dto.biz.UnifiedMeetingStatusVO; +import com.imeeting.dto.biz.*; import com.imeeting.entity.biz.AiTask; import com.imeeting.entity.biz.Meeting; import com.imeeting.entity.biz.PromptTemplate; @@ -31,6 +27,8 @@ import com.imeeting.enums.BusinessErrorCodeEnum; import com.imeeting.enums.MeetingStatusEnum; import com.imeeting.service.android.AndroidAuthService; import com.imeeting.service.android.AndroidChunkUploadService; +import com.imeeting.service.android.AndroidGatewayPushService; +import com.imeeting.service.android.AndroidMeetingPushService; import com.imeeting.service.android.legacy.LegacyMeetingAdapterService; import com.imeeting.support.AndroidRequestLogHelper; import com.imeeting.service.biz.*; @@ -99,6 +97,7 @@ public class AndroidMeetingController { private String h5BaseUrl; private final AndroidAuthService androidAuthService; + private final AndroidMeetingPushService androidMeetingPushService; private final AndroidChunkUploadService androidChunkUploadService; private final LegacyMeetingAdapterService legacyMeetingAdapterService; private final MeetingQueryService meetingQueryService; @@ -131,6 +130,7 @@ public class AndroidMeetingController { SysDictItemService dictItemService, SysParamService paramService, MeetingProgressService meetingProgressService, + AndroidMeetingPushService androidMeetingPushService, MeetingUnifiedStatusService meetingUnifiedStatusService) { this.androidAuthService = androidAuthService; this.androidChunkUploadService = androidChunkUploadService; @@ -148,6 +148,7 @@ public class AndroidMeetingController { this.paramService = paramService; this.dictItemService = dictItemService; this.meetingUnifiedStatusService = meetingUnifiedStatusService; + this.androidMeetingPushService = androidMeetingPushService; } @Operation(summary = "创建Android离线会议") @@ -342,6 +343,8 @@ public class AndroidMeetingController { LoginUser loginUser = authContext.isAnonymous() ? null : AndroidLoginUserSupport.requireLoginUser(authContext); requireOperableOfflineMeeting(meetingId, authContext, loginUser); meetingCommandService.retryTranscription(meetingId); + androidMeetingPushService.pushMeetingStatusChanged(meetingId, UnifiedMeetingStatusStage.TRANSCRIBING.getCode()); + return ApiResponse.ok(true); } @@ -361,7 +364,8 @@ public class AndroidMeetingController { LoginUser loginUser = authContext.isAnonymous() ? null : AndroidLoginUserSupport.requireLoginUser(authContext); requireOperableOfflineMeeting(meetingId, authContext, loginUser); meetingCommandService.retrySummary(meetingId); - return ApiResponse.ok(true); + androidMeetingPushService.pushMeetingStatusChanged(meetingId, UnifiedMeetingStatusStage.SUMMARIZING.getCode()); + return ApiResponse.ok(true); } @Operation(summary = "更新Android会议访问密码") @ApiResponses({ @@ -551,9 +555,9 @@ public class AndroidMeetingController { if (authContext == null || authContext.getDeviceId() == null || authContext.getDeviceId().isBlank()) { throw new RuntimeException("设备ID不能为空"); } - if (meeting.getSourceDeviceCode() == null || !meeting.getSourceDeviceCode().equals(authContext.getDeviceId())) { - throw new RuntimeException("当前会议不属于该设备"); - } +// if (meeting.getSourceDeviceCode() == null || !meeting.getSourceDeviceCode().equals(authContext.getDeviceId())) { +// throw new RuntimeException("当前会议不属于该设备"); +// } if (authContext.isAnonymous()) { if (!MeetingConstants.DEVICE_MODE_PUBLIC.equals(meeting.getSourceDeviceMode())) { throw new RuntimeException("当前会议不是公有设备会议"); @@ -571,147 +575,6 @@ public class AndroidMeetingController { && MeetingConstants.OFFLINE_RECORDING_UPLOAD_FINISHED.equalsIgnoreCase(command.getFinishStage()); } - - private LegacyMeetingPreviewDataResponse buildCompletedPreview(Meeting meeting, MeetingVO detail, AiTask summaryTask) { - LegacyMeetingPreviewDataResponse data = new LegacyMeetingPreviewDataResponse(); - data.setMeetingId(meeting.getId()); - data.setTitle(meeting.getTitle()); - data.setMeetingTime(formatDateTime(meeting.getMeetingTime())); - data.setSummary(detail.getSummaryContent()); - data.setCreatorUsername(resolveCreatorDisplayName(meeting.getCreatorId(), meeting.getCreatorName())); - Long promptId = resolvePromptId(summaryTask); - data.setPromptId(promptId); - data.setPromptName(resolvePromptName(promptId)); - List attendees = buildAttendees(meeting.getParticipants()); - data.setAttendees(attendees); - data.setAttendeesCount(attendees.size()); - data.setHasPassword(meeting.getAccessPassword() != null && !meeting.getAccessPassword().isBlank()); - data.setProcessingStatus(processingStatus("摘要已生成,可查看详情", 100, STAGE_COMPLETED)); - return data; - } - - private LegacyMeetingPreviewDataResponse buildProcessingPreview(Meeting meeting, - AiTask summaryTask, - LegacyMeetingProcessingStatusResponse status) { - LegacyMeetingPreviewDataResponse data = new LegacyMeetingPreviewDataResponse(); - data.setMeetingId(meeting.getId()); - data.setTitle(meeting.getTitle()); - data.setMeetingTime(formatDateTime(meeting.getMeetingTime())); - data.setCreatorUsername(resolveCreatorDisplayName(meeting.getCreatorId(), meeting.getCreatorName())); - Long promptId = resolvePromptId(summaryTask); - data.setPromptId(promptId); - data.setPromptName(resolvePromptName(promptId)); - data.setHasPassword(meeting.getAccessPassword() != null && !meeting.getAccessPassword().isBlank()); - data.setProcessingStatus(status); - return data; - } - - private LegacyMeetingProcessingStatusResponse processingStatus(String overallStatus, int overallProgress, String currentStage) { - return new LegacyMeetingProcessingStatusResponse(overallStatus, overallProgress, currentStage); - } - - private Integer resolveRealtimeProgress(Long meetingId) { - return meetingProgressService.resolvePercent(meetingId); - } - - private String buildFailureMessage(AiTask failedTask, String stageName) { - String error = failedTask == null || failedTask.getErrorMsg() == null || failedTask.getErrorMsg().isBlank() - ? "处理失败" - : failedTask.getErrorMsg(); - return "会议" + stageName + "失败: " + error; - } - - private boolean isRunningAsr(AiTask task) { - return task != null && Integer.valueOf(1).equals(task.getStatus()); - } - - private boolean isRunningSummary(AiTask task) { - return task != null && Integer.valueOf(1).equals(task.getStatus()); - } - - private boolean isFailed(AiTask task) { - return task != null && Integer.valueOf(3).equals(task.getStatus()); - } - - private AiTask findLatestTask(Long meetingId, String taskType) { - return aiTaskService.getOne(new LambdaQueryWrapper() - .eq(AiTask::getMeetingId, meetingId) - .eq(AiTask::getTaskType, taskType) - .orderByDesc(AiTask::getId) - .last("LIMIT 1")); - } - - private Long resolvePromptId(AiTask summaryTask) { - if (summaryTask == null || summaryTask.getTaskConfig() == null) { - return null; - } - Object rawPromptId = summaryTask.getTaskConfig().get("promptId"); - if (rawPromptId == null) { - return null; - } - if (rawPromptId instanceof Number number) { - return number.longValue(); - } - String value = String.valueOf(rawPromptId).trim(); - if (value.isEmpty()) { - return null; - } - try { - return Long.parseLong(value); - } catch (NumberFormatException ignored) { - return null; - } - } - - private String resolvePromptName(Long promptId) { - if (promptId == null) { - return null; - } - PromptTemplate template = promptTemplateService.getById(promptId); - return template == null ? null : template.getTemplateName(); - } - - private List buildAttendees(String participants) { - return buildAttendees(parseParticipantIds(participants)); - } - - private List buildAttendees(List participantIds) { - if (participantIds == null || participantIds.isEmpty()) { - return List.of(); - } - Map userMap = sysUserMapper.selectBatchIds(participantIds).stream() - .collect(Collectors.toMap(SysUser::getUserId, user -> user, (left, right) -> left, LinkedHashMap::new)); - - return participantIds.stream() - .map(userId -> { - SysUser user = userMap.get(userId); - String caption = user == null - ? String.valueOf(userId) - : (user.getDisplayName() != null ? user.getDisplayName() : user.getUsername()); - String username = user == null ? null : user.getUsername(); - return new LegacyMeetingAttendeeResponse(userId, username, caption); - }) - .toList(); - } - - private List parseParticipantIds(String participants) { - if (participants == null || participants.isBlank()) { - return List.of(); - } - return Arrays.stream(participants.split(",")) - .map(String::trim) - .filter(value -> !value.isEmpty()) - .map(value -> { - try { - return Long.parseLong(value); - } catch (NumberFormatException ignored) { - return null; - } - }) - .filter(Objects::nonNull) - .toList(); - } - private String normalizePassword(String password) { if (password == null) { return null; @@ -720,53 +583,4 @@ public class AndroidMeetingController { return normalized.isEmpty() ? null : normalized; } - 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 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) && (asrTask == null || !Integer.valueOf(0).equals(asrTask.getStatus()))) - || isRunningAsr(asrTask) - || (asrTask == null && hasAudio && !isSummaryStage); - } - - private String formatDateTime(LocalDateTime value) { - return value == null ? null : value.toString(); - } - - private Meeting findLatestUnfinishedMeetingByDevice(String deviceId) { - if (deviceId == null || deviceId.isBlank()) { - return null; - } - return meetingService.getOne(new LambdaQueryWrapper() - .eq(Meeting::getSourceDeviceCode, deviceId) - .in(Meeting::getStatus, MeetingStatusEnum.codesOf( - MeetingStatusEnum.INITIALIZING, - MeetingStatusEnum.TRANSCRIBING, - MeetingStatusEnum.SUMMARIZING - )) - .orderByDesc(Meeting::getId) - .last("LIMIT 1")); - } } diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java index c182f94..0df4634 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java @@ -430,6 +430,11 @@ public class AiTaskServiceImpl extends ServiceImpl impleme .last("LIMIT " + available)); List claimedTasks = new ArrayList<>(); for (AiTask queuedTask : queuedTasks) { + // 当前会议仍持有轮询锁时,不能提前 claim 队列任务。 + // 否则任务会先变成 RUNNING,再被异步 dispatch 因拿不到同一把锁而直接跳过,后续也不会再回到队列。 + if (meetingLockCache.hasPollingLock(queuedTask.getMeetingId())) { + continue; + } if (claimQueuedAsrTaskForScheduling(queuedTask)) { claimedTasks.add(queuedTask); } diff --git a/backend/src/main/java/com/imeeting/support/redis/MeetingLockCache.java b/backend/src/main/java/com/imeeting/support/redis/MeetingLockCache.java index 1bd7ca3..4a5a4ea 100644 --- a/backend/src/main/java/com/imeeting/support/redis/MeetingLockCache.java +++ b/backend/src/main/java/com/imeeting/support/redis/MeetingLockCache.java @@ -29,6 +29,13 @@ public class MeetingLockCache { return redisSupport.setIfAbsentOrThrow(RedisKeys.realtimeMeetingTimeoutLockKey(meetingId), "1", ttl); } + public boolean hasPollingLock(Long meetingId) { + if (meetingId == null) { + return false; + } + return redisSupport.getStringQuietly(RedisKeys.meetingPollingLockKey(meetingId)) != null; + } + public void releasePollingLock(Long meetingId) { redisSupport.deleteQuietly(RedisKeys.meetingPollingLockKey(meetingId)); } diff --git a/imeeting-h5/src/pages/scan-confirm/index.tsx b/imeeting-h5/src/pages/scan-confirm/index.tsx index ff2a27a..cb977ed 100644 --- a/imeeting-h5/src/pages/scan-confirm/index.tsx +++ b/imeeting-h5/src/pages/scan-confirm/index.tsx @@ -67,11 +67,11 @@ export default function ScanConfirmPage() {
- 公有设备扫码登录确认 + 设备扫码登录,开启会议 - - 当前页面只做登录确认,不在 H5 端直接创建会议。确认后会通过消息通知安卓设备。 - + {/**/} + {/* 当前页面只做登录确认,不在 H5 端直接创建会议。确认后会通过消息通知安卓设备。*/} + {/**/}
@@ -79,10 +79,10 @@ export default function ScanConfirmPage() {
- 设备端收到确认消息后,才允许继续后续离线发会流程。 + 终端设备收到确认消息后,才允许终端开启会议。
- 如果该二维码已被确认,再次访问通常会提示会话失效。 + 如果二维码已被确认,再次访问会提示会话失效。