feat: 添加会议状态推送和优化任务调度逻辑
- 在 `AiTaskServiceImpl` 中添加对轮询锁的检查,防止提前 claim 队列任务 - 在 `AndroidMeetingController` 中添加 `AndroidMeetingPushService` 依赖,并在 `retryTranscription` 和 `retrySummary` 方法中推送会议状态变化 - 移除 `AndroidMeetingController` 中未使用的代码块和方法dev_na
parent
c0d2dcce3d
commit
4572d15bea
|
|
@ -19,11 +19,7 @@ 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.LegacyUploadAudioResponse;
|
import com.imeeting.dto.android.legacy.LegacyUploadAudioResponse;
|
||||||
import com.imeeting.dto.biz.AiModelVO;
|
import com.imeeting.dto.biz.*;
|
||||||
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.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.PromptTemplate;
|
import com.imeeting.entity.biz.PromptTemplate;
|
||||||
|
|
@ -31,6 +27,8 @@ import com.imeeting.enums.BusinessErrorCodeEnum;
|
||||||
import com.imeeting.enums.MeetingStatusEnum;
|
import com.imeeting.enums.MeetingStatusEnum;
|
||||||
import com.imeeting.service.android.AndroidAuthService;
|
import com.imeeting.service.android.AndroidAuthService;
|
||||||
import com.imeeting.service.android.AndroidChunkUploadService;
|
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.service.android.legacy.LegacyMeetingAdapterService;
|
||||||
import com.imeeting.support.AndroidRequestLogHelper;
|
import com.imeeting.support.AndroidRequestLogHelper;
|
||||||
import com.imeeting.service.biz.*;
|
import com.imeeting.service.biz.*;
|
||||||
|
|
@ -99,6 +97,7 @@ public class AndroidMeetingController {
|
||||||
private String h5BaseUrl;
|
private String h5BaseUrl;
|
||||||
|
|
||||||
private final AndroidAuthService androidAuthService;
|
private final AndroidAuthService androidAuthService;
|
||||||
|
private final AndroidMeetingPushService androidMeetingPushService;
|
||||||
private final AndroidChunkUploadService androidChunkUploadService;
|
private final AndroidChunkUploadService androidChunkUploadService;
|
||||||
private final LegacyMeetingAdapterService legacyMeetingAdapterService;
|
private final LegacyMeetingAdapterService legacyMeetingAdapterService;
|
||||||
private final MeetingQueryService meetingQueryService;
|
private final MeetingQueryService meetingQueryService;
|
||||||
|
|
@ -131,6 +130,7 @@ public class AndroidMeetingController {
|
||||||
SysDictItemService dictItemService,
|
SysDictItemService dictItemService,
|
||||||
SysParamService paramService,
|
SysParamService paramService,
|
||||||
MeetingProgressService meetingProgressService,
|
MeetingProgressService meetingProgressService,
|
||||||
|
AndroidMeetingPushService androidMeetingPushService,
|
||||||
MeetingUnifiedStatusService meetingUnifiedStatusService) {
|
MeetingUnifiedStatusService meetingUnifiedStatusService) {
|
||||||
this.androidAuthService = androidAuthService;
|
this.androidAuthService = androidAuthService;
|
||||||
this.androidChunkUploadService = androidChunkUploadService;
|
this.androidChunkUploadService = androidChunkUploadService;
|
||||||
|
|
@ -148,6 +148,7 @@ public class AndroidMeetingController {
|
||||||
this.paramService = paramService;
|
this.paramService = paramService;
|
||||||
this.dictItemService = dictItemService;
|
this.dictItemService = dictItemService;
|
||||||
this.meetingUnifiedStatusService = meetingUnifiedStatusService;
|
this.meetingUnifiedStatusService = meetingUnifiedStatusService;
|
||||||
|
this.androidMeetingPushService = androidMeetingPushService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "创建Android离线会议")
|
@Operation(summary = "创建Android离线会议")
|
||||||
|
|
@ -342,6 +343,8 @@ public class AndroidMeetingController {
|
||||||
LoginUser loginUser = authContext.isAnonymous() ? null : AndroidLoginUserSupport.requireLoginUser(authContext);
|
LoginUser loginUser = authContext.isAnonymous() ? null : AndroidLoginUserSupport.requireLoginUser(authContext);
|
||||||
requireOperableOfflineMeeting(meetingId, authContext, loginUser);
|
requireOperableOfflineMeeting(meetingId, authContext, loginUser);
|
||||||
meetingCommandService.retryTranscription(meetingId);
|
meetingCommandService.retryTranscription(meetingId);
|
||||||
|
androidMeetingPushService.pushMeetingStatusChanged(meetingId, UnifiedMeetingStatusStage.TRANSCRIBING.getCode());
|
||||||
|
|
||||||
return ApiResponse.ok(true);
|
return ApiResponse.ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -361,7 +364,8 @@ public class AndroidMeetingController {
|
||||||
LoginUser loginUser = authContext.isAnonymous() ? null : AndroidLoginUserSupport.requireLoginUser(authContext);
|
LoginUser loginUser = authContext.isAnonymous() ? null : AndroidLoginUserSupport.requireLoginUser(authContext);
|
||||||
requireOperableOfflineMeeting(meetingId, authContext, loginUser);
|
requireOperableOfflineMeeting(meetingId, authContext, loginUser);
|
||||||
meetingCommandService.retrySummary(meetingId);
|
meetingCommandService.retrySummary(meetingId);
|
||||||
return ApiResponse.ok(true);
|
androidMeetingPushService.pushMeetingStatusChanged(meetingId, UnifiedMeetingStatusStage.SUMMARIZING.getCode());
|
||||||
|
return ApiResponse.ok(true);
|
||||||
}
|
}
|
||||||
@Operation(summary = "更新Android会议访问密码")
|
@Operation(summary = "更新Android会议访问密码")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
|
|
@ -551,9 +555,9 @@ public class AndroidMeetingController {
|
||||||
if (authContext == null || authContext.getDeviceId() == null || authContext.getDeviceId().isBlank()) {
|
if (authContext == null || authContext.getDeviceId() == null || authContext.getDeviceId().isBlank()) {
|
||||||
throw new RuntimeException("设备ID不能为空");
|
throw new RuntimeException("设备ID不能为空");
|
||||||
}
|
}
|
||||||
if (meeting.getSourceDeviceCode() == null || !meeting.getSourceDeviceCode().equals(authContext.getDeviceId())) {
|
// if (meeting.getSourceDeviceCode() == null || !meeting.getSourceDeviceCode().equals(authContext.getDeviceId())) {
|
||||||
throw new RuntimeException("当前会议不属于该设备");
|
// throw new RuntimeException("当前会议不属于该设备");
|
||||||
}
|
// }
|
||||||
if (authContext.isAnonymous()) {
|
if (authContext.isAnonymous()) {
|
||||||
if (!MeetingConstants.DEVICE_MODE_PUBLIC.equals(meeting.getSourceDeviceMode())) {
|
if (!MeetingConstants.DEVICE_MODE_PUBLIC.equals(meeting.getSourceDeviceMode())) {
|
||||||
throw new RuntimeException("当前会议不是公有设备会议");
|
throw new RuntimeException("当前会议不是公有设备会议");
|
||||||
|
|
@ -571,147 +575,6 @@ public class AndroidMeetingController {
|
||||||
&& MeetingConstants.OFFLINE_RECORDING_UPLOAD_FINISHED.equalsIgnoreCase(command.getFinishStage());
|
&& 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<LegacyMeetingAttendeeResponse> 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<AiTask>()
|
|
||||||
.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<LegacyMeetingAttendeeResponse> buildAttendees(String participants) {
|
|
||||||
return buildAttendees(parseParticipantIds(participants));
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<LegacyMeetingAttendeeResponse> buildAttendees(List<Long> participantIds) {
|
|
||||||
if (participantIds == null || participantIds.isEmpty()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
Map<Long, SysUser> 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<Long> 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) {
|
private String normalizePassword(String password) {
|
||||||
if (password == null) {
|
if (password == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -720,53 +583,4 @@ public class AndroidMeetingController {
|
||||||
return normalized.isEmpty() ? null : normalized;
|
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<Meeting>()
|
|
||||||
.eq(Meeting::getSourceDeviceCode, deviceId)
|
|
||||||
.in(Meeting::getStatus, MeetingStatusEnum.codesOf(
|
|
||||||
MeetingStatusEnum.INITIALIZING,
|
|
||||||
MeetingStatusEnum.TRANSCRIBING,
|
|
||||||
MeetingStatusEnum.SUMMARIZING
|
|
||||||
))
|
|
||||||
.orderByDesc(Meeting::getId)
|
|
||||||
.last("LIMIT 1"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -430,6 +430,11 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
.last("LIMIT " + available));
|
.last("LIMIT " + available));
|
||||||
List<AiTask> claimedTasks = new ArrayList<>();
|
List<AiTask> claimedTasks = new ArrayList<>();
|
||||||
for (AiTask queuedTask : queuedTasks) {
|
for (AiTask queuedTask : queuedTasks) {
|
||||||
|
// 当前会议仍持有轮询锁时,不能提前 claim 队列任务。
|
||||||
|
// 否则任务会先变成 RUNNING,再被异步 dispatch 因拿不到同一把锁而直接跳过,后续也不会再回到队列。
|
||||||
|
if (meetingLockCache.hasPollingLock(queuedTask.getMeetingId())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (claimQueuedAsrTaskForScheduling(queuedTask)) {
|
if (claimQueuedAsrTaskForScheduling(queuedTask)) {
|
||||||
claimedTasks.add(queuedTask);
|
claimedTasks.add(queuedTask);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,13 @@ public class MeetingLockCache {
|
||||||
return redisSupport.setIfAbsentOrThrow(RedisKeys.realtimeMeetingTimeoutLockKey(meetingId), "1", ttl);
|
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) {
|
public void releasePollingLock(Long meetingId) {
|
||||||
redisSupport.deleteQuietly(RedisKeys.meetingPollingLockKey(meetingId));
|
redisSupport.deleteQuietly(RedisKeys.meetingPollingLockKey(meetingId));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,11 +67,11 @@ export default function ScanConfirmPage() {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Title level={4} style={{ margin: 0 }}>
|
<Title level={4} style={{ margin: 0 }}>
|
||||||
公有设备扫码登录确认
|
设备扫码登录,开启会议
|
||||||
</Title>
|
</Title>
|
||||||
<Paragraph type="secondary" style={{ margin: "8px 0 0" }}>
|
{/*<Paragraph type="secondary" style={{ margin: "8px 0 0" }}>*/}
|
||||||
当前页面只做登录确认,不在 H5 端直接创建会议。确认后会通过消息通知安卓设备。
|
{/* 当前页面只做登录确认,不在 H5 端直接创建会议。确认后会通过消息通知安卓设备。*/}
|
||||||
</Paragraph>
|
{/*</Paragraph>*/}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -79,10 +79,10 @@ export default function ScanConfirmPage() {
|
||||||
<Space direction="vertical" size={10}>
|
<Space direction="vertical" size={10}>
|
||||||
<div className="scan-confirm__tip-line">
|
<div className="scan-confirm__tip-line">
|
||||||
<CheckCircleOutlined />
|
<CheckCircleOutlined />
|
||||||
<span>设备端收到确认消息后,才允许继续后续离线发会流程。</span>
|
<span>终端设备收到确认消息后,才允许终端开启会议。</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="scan-confirm__tip-line is-muted">
|
<div className="scan-confirm__tip-line is-muted">
|
||||||
<span>如果该二维码已被确认,再次访问通常会提示会话失效。</span>
|
<span>如果二维码已被确认,再次访问会提示会话失效。</span>
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue