refactor: 重构会议进度管理和Android设备绑定服务
- 移除 `RedisOnlyMeetingProgressServiceAdapter` 和 `RedisValueSupport` 类 - 更新 `MeetingProgressServiceImpl` 使用新的 `MeetingProgressCache` - 重构 `MeetingTaskRecoveryListener` 使用 `MeetingLockCache` 和 `MeetingAsrPermitCache` - 添加 `AndroidDeviceBindingService` 和 `AndroidPushMessageService` 接口及其实现类 - 新增 `AndroidPublicMeetingSessionRequest` 和 `AndroidPublicMeetingSessionVO` DTO 类 - 更新 `AndroidMeetingPushService` 及其实现类,添加推送待处理会议功能dev_na
parent
fb5c4b545e
commit
7c3b65624e
|
|
@ -1,12 +1,21 @@
|
||||||
package com.imeeting.common;
|
package com.imeeting.common;
|
||||||
|
|
||||||
public final class MeetingConstants {
|
public final class MeetingConstants {
|
||||||
|
|
||||||
public static final String TYPE_OFFLINE = "OFFLINE";
|
public static final String TYPE_OFFLINE = "OFFLINE";
|
||||||
public static final String TYPE_REALTIME = "REALTIME";
|
public static final String TYPE_REALTIME = "REALTIME";
|
||||||
|
|
||||||
public static final String SOURCE_WEB = "WEB";
|
public static final String SOURCE_WEB = "WEB";
|
||||||
public static final String SOURCE_ANDROID = "ANDROID";
|
public static final String SOURCE_ANDROID = "ANDROID";
|
||||||
|
|
||||||
|
public static final String DEVICE_MODE_PUBLIC = "PUBLIC";
|
||||||
|
public static final String DEVICE_MODE_PRIVATE = "PRIVATE";
|
||||||
|
|
||||||
|
public static final String DEVICE_DELIVERY_NONE = "NONE";
|
||||||
|
public static final String DEVICE_DELIVERY_PENDING = "PENDING";
|
||||||
|
public static final String DEVICE_DELIVERY_ACKED = "ACKED";
|
||||||
|
public static final String DEVICE_DELIVERY_EXPIRED = "EXPIRED";
|
||||||
|
public static final String DEVICE_DELIVERY_CANCELLED = "CANCELLED";
|
||||||
|
|
||||||
public static final String SUMMARY_DETAIL_DETAILED = "DETAILED";
|
public static final String SUMMARY_DETAIL_DETAILED = "DETAILED";
|
||||||
public static final String SUMMARY_DETAIL_STANDARD = "STANDARD";
|
public static final String SUMMARY_DETAIL_STANDARD = "STANDARD";
|
||||||
public static final String SUMMARY_DETAIL_BRIEF = "BRIEF";
|
public static final String SUMMARY_DETAIL_BRIEF = "BRIEF";
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,18 @@ public final class RedisKeys {
|
||||||
return "biz:meeting:realtime:event-seq:" + meetingId;
|
return "biz:meeting:realtime:event-seq:" + meetingId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String publicMeetingSessionKey(String sessionId) {
|
||||||
|
return "biz:meeting:public-session:" + sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String androidChunkUploadSessionKey(String uploadSessionId) {
|
||||||
|
return "biz:meeting:android:chunk-upload:" + uploadSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String androidPendingMeetingDraftKey(Long meetingId) {
|
||||||
|
return "biz:meeting:android:draft:" + meetingId;
|
||||||
|
}
|
||||||
|
|
||||||
public static final String CACHE_EMPTY_MARKER = "EMPTY_MARKER";
|
public static final String CACHE_EMPTY_MARKER = "EMPTY_MARKER";
|
||||||
public static final String SYS_PARAM_FIELD_VALUE = "value";
|
public static final String SYS_PARAM_FIELD_VALUE = "value";
|
||||||
public static final String SYS_PARAM_FIELD_TYPE = "type";
|
public static final String SYS_PARAM_FIELD_TYPE = "type";
|
||||||
|
|
|
||||||
|
|
@ -12,4 +12,6 @@ public final class SysParamKeys {
|
||||||
public static final String MEETING_MAX_PAUSE_DURATION = "meeting.max_pause_duration";
|
public static final String MEETING_MAX_PAUSE_DURATION = "meeting.max_pause_duration";
|
||||||
public static final String MEETING_MAX_MEETING_DURATION = "meeting.max_meeting_duration";
|
public static final String MEETING_MAX_MEETING_DURATION = "meeting.max_meeting_duration";
|
||||||
public static final String MEETING_PACKET_LOSS_RATE = "meeting.packet_loss_rate";
|
public static final String MEETING_PACKET_LOSS_RATE = "meeting.packet_loss_rate";
|
||||||
|
public static final String MEETING_ANDROID_AUDIO_CHUNK_UPLOAD_ENABLED = "meeting.android.audio.chunk_upload_enabled";
|
||||||
|
public static final String MEETING_ANDROID_AUDIO_CHUNK_DURATION_SECONDS = "meeting.android.audio.chunk_duration_seconds";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
package com.imeeting.controller.android;
|
package com.imeeting.controller.android;
|
||||||
|
|
||||||
|
import com.imeeting.service.android.AndroidDeviceBindingService;
|
||||||
|
import com.imeeting.support.AndroidRequestLogHelper;
|
||||||
|
import com.unisbase.auth.JwtTokenProvider;
|
||||||
import com.unisbase.common.ApiResponse;
|
import com.unisbase.common.ApiResponse;
|
||||||
import com.unisbase.dto.LoginRequest;
|
import com.unisbase.dto.LoginRequest;
|
||||||
import com.unisbase.dto.RefreshRequest;
|
import com.unisbase.dto.RefreshRequest;
|
||||||
import com.unisbase.dto.TokenResponse;
|
import com.unisbase.dto.TokenResponse;
|
||||||
import com.unisbase.service.AuthService;
|
import com.unisbase.service.AuthService;
|
||||||
import com.imeeting.support.AndroidRequestLogHelper;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
@ -27,8 +30,9 @@ import org.springframework.web.bind.annotation.RestController;
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class AndroidAuthController {
|
public class AndroidAuthController {
|
||||||
|
|
||||||
private final AuthService authService;
|
private final AuthService authService;
|
||||||
|
private final AndroidDeviceBindingService androidDeviceBindingService;
|
||||||
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
|
|
||||||
@Operation(summary = "Android登录")
|
@Operation(summary = "Android登录")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
|
|
@ -39,9 +43,26 @@ public class AndroidAuthController {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public ApiResponse<TokenResponse> login(@Valid @RequestBody LoginRequest request) {
|
public ApiResponse<TokenResponse> login(@Valid @RequestBody LoginRequest request,
|
||||||
AndroidRequestLogHelper.logRequest(log, "Android认证", "登录接口", "request", request);
|
@RequestHeader(value = "X-Android-Device-Id", required = false) String deviceId,
|
||||||
return ApiResponse.ok(authService.login(request, true));
|
@RequestHeader(value = "X-Android-App-Version", required = false) String appVersion,
|
||||||
|
@RequestHeader(value = "X-Android-Platform", required = false) String platform) {
|
||||||
|
AndroidRequestLogHelper.logRequest(log, "Android认证", "登录接口",
|
||||||
|
"request", request,
|
||||||
|
"deviceId", deviceId,
|
||||||
|
"appVersion", appVersion,
|
||||||
|
"platform", platform);
|
||||||
|
TokenResponse response = authService.login(request, true);
|
||||||
|
if (response != null && response.getUser() != null && response.getCurrentTenantId() != null && StringUtils.hasText(deviceId)) {
|
||||||
|
androidDeviceBindingService.bindPrivateDevice(
|
||||||
|
deviceId.trim(),
|
||||||
|
response.getCurrentTenantId(),
|
||||||
|
response.getUser().getUserId(),
|
||||||
|
appVersion,
|
||||||
|
platform
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ApiResponse.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "Android刷新令牌")
|
@Operation(summary = "Android刷新令牌")
|
||||||
|
|
@ -63,6 +84,26 @@ public class AndroidAuthController {
|
||||||
return ApiResponse.ok(authService.refresh(resolveRefreshToken(request, authorization, androidAccessToken)));
|
return ApiResponse.ok(authService.refresh(resolveRefreshToken(request, authorization, androidAccessToken)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "Android退出登录")
|
||||||
|
@PostMapping("/logout")
|
||||||
|
public ApiResponse<Void> logout(HttpServletRequest request,
|
||||||
|
@RequestHeader(value = "Authorization", required = false) String authorization,
|
||||||
|
@RequestHeader(value = "X-Android-Device-Id", required = false) String deviceId) {
|
||||||
|
AndroidRequestLogHelper.logRequest(log, "Android认证", "退出登录接口",
|
||||||
|
"authorization", authorization,
|
||||||
|
"deviceId", deviceId);
|
||||||
|
String token = extractToken(authorization);
|
||||||
|
var claims = jwtTokenProvider.parseToken(token);
|
||||||
|
Long userId = claims.get("userId", Long.class);
|
||||||
|
Long tenantId = claims.get("tenantId", Long.class);
|
||||||
|
String sessionId = claims.get("sessionId", String.class);
|
||||||
|
authService.logout(userId, tenantId, sessionId);
|
||||||
|
if (StringUtils.hasText(deviceId)) {
|
||||||
|
androidDeviceBindingService.unbindPrivateDevice(deviceId.trim());
|
||||||
|
}
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
private String resolveRefreshToken(RefreshRequest request, String authorization, String androidAccessToken) {
|
private String resolveRefreshToken(RefreshRequest request, String authorization, String androidAccessToken) {
|
||||||
if (request != null && StringUtils.hasText(request.getRefreshToken())) {
|
if (request != null && StringUtils.hasText(request.getRefreshToken())) {
|
||||||
return request.getRefreshToken().trim();
|
return request.getRefreshToken().trim();
|
||||||
|
|
@ -79,4 +120,12 @@ public class AndroidAuthController {
|
||||||
}
|
}
|
||||||
throw new IllegalArgumentException("refreshToken不能为空");
|
throw new IllegalArgumentException("refreshToken不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String extractToken(String authorization) {
|
||||||
|
if (!StringUtils.hasText(authorization)) {
|
||||||
|
throw new IllegalArgumentException("Authorization不能为空");
|
||||||
|
}
|
||||||
|
String value = authorization.trim();
|
||||||
|
return value.startsWith("Bearer ") ? value.substring(7).trim() : value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
package com.imeeting.controller.android;
|
||||||
|
|
||||||
|
import com.imeeting.dto.android.AndroidAuthContext;
|
||||||
|
import com.imeeting.dto.android.legacy.LegacyUploadAudioResponse;
|
||||||
|
import com.imeeting.service.android.AndroidAuthService;
|
||||||
|
import com.imeeting.service.android.AndroidChunkUploadService;
|
||||||
|
import com.imeeting.support.AndroidRequestLogHelper;
|
||||||
|
import com.unisbase.annotation.Anonymous;
|
||||||
|
import com.unisbase.common.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Tag(name = "Android会议分片上传接口")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/android/meetings/upload-audio")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class AndroidMeetingChunkUploadController {
|
||||||
|
private final AndroidAuthService androidAuthService;
|
||||||
|
private final AndroidChunkUploadService androidChunkUploadService;
|
||||||
|
|
||||||
|
@Operation(summary = "上传会议音频分片")
|
||||||
|
@io.swagger.v3.oas.annotations.responses.ApiResponses({
|
||||||
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "分片上传成功返回 true",
|
||||||
|
content = @Content(schema = @Schema(implementation = Boolean.class))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
@PostMapping("/chunk")
|
||||||
|
@Anonymous
|
||||||
|
public ApiResponse<Boolean> uploadChunk(HttpServletRequest request,
|
||||||
|
@RequestParam("meeting_id") Long meetingId,
|
||||||
|
@RequestParam("upload_session_id") String uploadSessionId,
|
||||||
|
@RequestParam("chunk_index") Integer chunkIndex,
|
||||||
|
@RequestParam("total_chunks") Integer totalChunks,
|
||||||
|
@RequestParam("chunk_file") MultipartFile chunkFile) throws IOException {
|
||||||
|
AndroidRequestLogHelper.logRequest(log, "Android会议", "上传会议音频分片",
|
||||||
|
"meetingId", meetingId,
|
||||||
|
"uploadSessionId", uploadSessionId,
|
||||||
|
"chunkIndex", chunkIndex,
|
||||||
|
"totalChunks", totalChunks,
|
||||||
|
"chunkFile", chunkFile);
|
||||||
|
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||||
|
androidChunkUploadService.saveChunk(meetingId, uploadSessionId, chunkIndex, totalChunks, chunkFile, authContext);
|
||||||
|
return ApiResponse.ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "完成分片上传并触发会议音频处理")
|
||||||
|
@io.swagger.v3.oas.annotations.responses.ApiResponses({
|
||||||
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "返回上传后的会议 ID 和音频地址",
|
||||||
|
content = @Content(schema = @Schema(implementation = LegacyUploadAudioResponse.class))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
@PostMapping("/complete")
|
||||||
|
@Anonymous
|
||||||
|
public ApiResponse<LegacyUploadAudioResponse> completeUpload(HttpServletRequest request,
|
||||||
|
@RequestParam("meeting_id") Long meetingId,
|
||||||
|
@RequestParam("upload_session_id") String uploadSessionId,
|
||||||
|
@RequestParam(value = "force_replace", defaultValue = "false") boolean forceReplace,
|
||||||
|
@RequestParam(value = "prompt_id", required = false) Long promptId,
|
||||||
|
@RequestParam(value = "model_code", required = false) String modelCode) throws IOException {
|
||||||
|
AndroidRequestLogHelper.logRequest(log, "Android会议", "完成分片上传",
|
||||||
|
"meetingId", meetingId,
|
||||||
|
"uploadSessionId", uploadSessionId,
|
||||||
|
"forceReplace", forceReplace,
|
||||||
|
"promptId", promptId,
|
||||||
|
"modelCode", modelCode);
|
||||||
|
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||||
|
return ApiResponse.ok(androidChunkUploadService.completeUpload(
|
||||||
|
meetingId,
|
||||||
|
uploadSessionId,
|
||||||
|
forceReplace,
|
||||||
|
promptId,
|
||||||
|
modelCode,
|
||||||
|
authContext
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,9 +2,6 @@ package com.imeeting.controller.android;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.imeeting.common.RedisKeys;
|
|
||||||
import com.imeeting.common.SysParamKeys;
|
import com.imeeting.common.SysParamKeys;
|
||||||
import com.imeeting.dto.android.AndroidAuthContext;
|
import com.imeeting.dto.android.AndroidAuthContext;
|
||||||
import com.imeeting.dto.android.AndroidMeetingConfigVo;
|
import com.imeeting.dto.android.AndroidMeetingConfigVo;
|
||||||
|
|
@ -25,7 +22,6 @@ import com.imeeting.service.android.AndroidAuthService;
|
||||||
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.*;
|
||||||
import com.imeeting.service.biz.impl.RedisOnlyMeetingProgressServiceAdapter;
|
|
||||||
import com.unisbase.common.ApiResponse;
|
import com.unisbase.common.ApiResponse;
|
||||||
import com.unisbase.common.annotation.Log;
|
import com.unisbase.common.annotation.Log;
|
||||||
import com.unisbase.dto.PageResult;
|
import com.unisbase.dto.PageResult;
|
||||||
|
|
@ -44,7 +40,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
|
@ -91,7 +86,6 @@ public class AndroidMeetingController {
|
||||||
private final SysDictItemService dictItemService;
|
private final SysDictItemService dictItemService;
|
||||||
private final SysParamService paramService;
|
private final SysParamService paramService;
|
||||||
private final MeetingProgressService meetingProgressService;
|
private final MeetingProgressService meetingProgressService;
|
||||||
private final ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public AndroidMeetingController(AndroidAuthService androidAuthService,
|
public AndroidMeetingController(AndroidAuthService androidAuthService,
|
||||||
|
|
@ -106,8 +100,7 @@ public class AndroidMeetingController {
|
||||||
AiModelService aiModelService,
|
AiModelService aiModelService,
|
||||||
SysDictItemService dictItemService,
|
SysDictItemService dictItemService,
|
||||||
SysParamService paramService,
|
SysParamService paramService,
|
||||||
MeetingProgressService meetingProgressService,
|
MeetingProgressService meetingProgressService) {
|
||||||
ObjectMapper objectMapper) {
|
|
||||||
this.androidAuthService = androidAuthService;
|
this.androidAuthService = androidAuthService;
|
||||||
this.legacyMeetingAdapterService = legacyMeetingAdapterService;
|
this.legacyMeetingAdapterService = legacyMeetingAdapterService;
|
||||||
this.meetingQueryService = meetingQueryService;
|
this.meetingQueryService = meetingQueryService;
|
||||||
|
|
@ -118,44 +111,11 @@ public class AndroidMeetingController {
|
||||||
this.promptTemplateService = promptTemplateService;
|
this.promptTemplateService = promptTemplateService;
|
||||||
this.sysUserMapper = sysUserMapper;
|
this.sysUserMapper = sysUserMapper;
|
||||||
this.meetingProgressService = meetingProgressService;
|
this.meetingProgressService = meetingProgressService;
|
||||||
this.objectMapper = objectMapper;
|
|
||||||
this.aiModelService = aiModelService;
|
this.aiModelService = aiModelService;
|
||||||
this.paramService = paramService;
|
this.paramService = paramService;
|
||||||
this.dictItemService = dictItemService;
|
this.dictItemService = dictItemService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AndroidMeetingController(AndroidAuthService androidAuthService,
|
|
||||||
LegacyMeetingAdapterService legacyMeetingAdapterService,
|
|
||||||
MeetingQueryService meetingQueryService,
|
|
||||||
MeetingAccessService meetingAccessService,
|
|
||||||
MeetingCommandService meetingCommandService,
|
|
||||||
MeetingService meetingService,
|
|
||||||
AiTaskService aiTaskService,
|
|
||||||
PromptTemplateService promptTemplateService,
|
|
||||||
SysUserMapper sysUserMapper,
|
|
||||||
AiModelService aiModelService,
|
|
||||||
SysDictItemService dictItemService,
|
|
||||||
SysParamService paramService,
|
|
||||||
StringRedisTemplate redisTemplate,
|
|
||||||
ObjectMapper objectMapper) {
|
|
||||||
this(
|
|
||||||
androidAuthService,
|
|
||||||
legacyMeetingAdapterService,
|
|
||||||
meetingQueryService,
|
|
||||||
meetingAccessService,
|
|
||||||
meetingCommandService,
|
|
||||||
meetingService,
|
|
||||||
aiTaskService,
|
|
||||||
promptTemplateService,
|
|
||||||
sysUserMapper,
|
|
||||||
aiModelService,
|
|
||||||
dictItemService,
|
|
||||||
paramService,
|
|
||||||
new RedisOnlyMeetingProgressServiceAdapter(redisTemplate, objectMapper),
|
|
||||||
objectMapper
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "创建Android离线会议")
|
@Operation(summary = "创建Android离线会议")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||||
|
|
@ -170,7 +130,11 @@ public class AndroidMeetingController {
|
||||||
AndroidRequestLogHelper.logRequest(log, "Android会议", "创建离线会议接口", "request", command);
|
AndroidRequestLogHelper.logRequest(log, "Android会议", "创建离线会议接口", "request", command);
|
||||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||||
LoginUser loginUser = AndroidLoginUserSupport.requireLoginUser(authContext);
|
LoginUser loginUser = AndroidLoginUserSupport.requireLoginUser(authContext);
|
||||||
return ApiResponse.ok(legacyMeetingAdapterService.createMeeting(command, loginUser));
|
Meeting existingMeeting = findLatestUnfinishedMeetingByDevice(authContext.getDeviceId());
|
||||||
|
if (existingMeeting != null) {
|
||||||
|
return new ApiResponse<>("409", "设备端已有会议", meetingQueryService.getDetailIgnoreTenant(existingMeeting.getId()));
|
||||||
|
}
|
||||||
|
return ApiResponse.ok(legacyMeetingAdapterService.createMeeting(command, authContext, loginUser));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "上传Android会议音频")
|
@Operation(summary = "上传Android会议音频")
|
||||||
|
|
@ -195,6 +159,14 @@ public class AndroidMeetingController {
|
||||||
"forceReplace", forceReplace,
|
"forceReplace", forceReplace,
|
||||||
"audioFile", audioFile);
|
"audioFile", audioFile);
|
||||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||||
|
if (authContext.isAnonymous()) {
|
||||||
|
return ApiResponse.ok(legacyMeetingAdapterService.uploadAndTriggerOfflineProcessForPublicDevice(
|
||||||
|
meetingId,
|
||||||
|
forceReplace,
|
||||||
|
audioFile,
|
||||||
|
authContext
|
||||||
|
));
|
||||||
|
}
|
||||||
LoginUser loginUser = AndroidLoginUserSupport.requireLoginUser(authContext);
|
LoginUser loginUser = AndroidLoginUserSupport.requireLoginUser(authContext);
|
||||||
return ApiResponse.ok(legacyMeetingAdapterService.uploadAndTriggerOfflineProcess(
|
return ApiResponse.ok(legacyMeetingAdapterService.uploadAndTriggerOfflineProcess(
|
||||||
meetingId,
|
meetingId,
|
||||||
|
|
@ -202,6 +174,7 @@ public class AndroidMeetingController {
|
||||||
modelCode,
|
modelCode,
|
||||||
forceReplace,
|
forceReplace,
|
||||||
audioFile,
|
audioFile,
|
||||||
|
authContext,
|
||||||
loginUser
|
loginUser
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -341,6 +314,8 @@ public class AndroidMeetingController {
|
||||||
BigDecimal bigDecimal = new BigDecimal(paramService.getParamValue(SysParamKeys.MEETING_MAX_PAUSE_DURATION, "99"));
|
BigDecimal bigDecimal = new BigDecimal(paramService.getParamValue(SysParamKeys.MEETING_MAX_PAUSE_DURATION, "99"));
|
||||||
bigDecimal = bigDecimal.setScale(2, RoundingMode.HALF_UP);
|
bigDecimal = bigDecimal.setScale(2, RoundingMode.HALF_UP);
|
||||||
resultVo.setPacketLossRate(bigDecimal );
|
resultVo.setPacketLossRate(bigDecimal );
|
||||||
|
resultVo.setChunkUploadEnabled(Boolean.parseBoolean(paramService.getParamValue(SysParamKeys.MEETING_ANDROID_AUDIO_CHUNK_UPLOAD_ENABLED, "false")));
|
||||||
|
resultVo.setChunkDurationSeconds(Integer.valueOf(paramService.getParamValue(SysParamKeys.MEETING_ANDROID_AUDIO_CHUNK_DURATION_SECONDS, "60")));
|
||||||
|
|
||||||
return ApiResponse.ok(resultVo);
|
return ApiResponse.ok(resultVo);
|
||||||
}
|
}
|
||||||
|
|
@ -629,4 +604,15 @@ public class AndroidMeetingController {
|
||||||
private String formatDateTime(LocalDateTime value) {
|
private String formatDateTime(LocalDateTime value) {
|
||||||
return value == null ? null : value.toString();
|
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, 0, 1, 2)
|
||||||
|
.orderByDesc(Meeting::getId)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
package com.imeeting.controller.android;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.imeeting.dto.android.AndroidAuthContext;
|
||||||
|
import com.imeeting.dto.android.AndroidPublicMeetingSessionRequest;
|
||||||
|
import com.imeeting.dto.android.AndroidPublicMeetingSessionVO;
|
||||||
|
import com.imeeting.dto.biz.MeetingVO;
|
||||||
|
import com.imeeting.entity.biz.Meeting;
|
||||||
|
import com.imeeting.mapper.DeviceInfoMapper;
|
||||||
|
import com.imeeting.service.android.AndroidAuthService;
|
||||||
|
import com.imeeting.service.android.AndroidPublicMeetingSessionService;
|
||||||
|
import com.imeeting.service.biz.MeetingCommandService;
|
||||||
|
import com.imeeting.service.biz.MeetingQueryService;
|
||||||
|
import com.imeeting.service.biz.MeetingService;
|
||||||
|
import com.imeeting.support.AndroidRequestLogHelper;
|
||||||
|
import com.unisbase.annotation.Anonymous;
|
||||||
|
import com.unisbase.common.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@Tag(name = "Android公有设备会议接口")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/android/public-meetings")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class AndroidPublicMeetingController {
|
||||||
|
private final AndroidAuthService androidAuthService;
|
||||||
|
private final AndroidPublicMeetingSessionService androidPublicMeetingSessionService;
|
||||||
|
private final MeetingQueryService meetingQueryService;
|
||||||
|
private final MeetingCommandService meetingCommandService;
|
||||||
|
private final MeetingService meetingService;
|
||||||
|
private final DeviceInfoMapper deviceInfoMapper;
|
||||||
|
|
||||||
|
@Operation(summary = "创建公有设备扫码发会会话")
|
||||||
|
@io.swagger.v3.oas.annotations.responses.ApiResponses({
|
||||||
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "返回扫码会话信息,供设备展示二维码",
|
||||||
|
content = @Content(schema = @Schema(implementation = AndroidPublicMeetingSessionVO.class))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
@PostMapping("/session")
|
||||||
|
@Anonymous
|
||||||
|
public ApiResponse<Object> createSession(HttpServletRequest request,
|
||||||
|
@RequestBody(required = false) AndroidPublicMeetingSessionRequest command) {
|
||||||
|
AndroidRequestLogHelper.logRequest(log, "Android公有会议", "创建扫码发会会话",
|
||||||
|
"request", command);
|
||||||
|
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||||
|
assertPublicDevice(authContext.getDeviceId());
|
||||||
|
Meeting existingMeeting = findLatestUnfinishedMeetingByDevice(authContext.getDeviceId());
|
||||||
|
if (existingMeeting != null) {
|
||||||
|
return new ApiResponse<>("409", "设备端已有会议", meetingQueryService.getDetailIgnoreTenant(existingMeeting.getId()));
|
||||||
|
}
|
||||||
|
AndroidPublicMeetingSessionVO vo = androidPublicMeetingSessionService.create(
|
||||||
|
authContext.getDeviceId(),
|
||||||
|
command == null ? null : command.getTitle()
|
||||||
|
);
|
||||||
|
return ApiResponse.ok(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "公有设备删除未开始会议")
|
||||||
|
@io.swagger.v3.oas.annotations.responses.ApiResponses({
|
||||||
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "删除成功返回 true",
|
||||||
|
content = @Content(schema = @Schema(implementation = Boolean.class))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
@DeleteMapping("/{meetingId}")
|
||||||
|
@Anonymous
|
||||||
|
public ApiResponse<Boolean> deletePendingMeeting(HttpServletRequest request, @PathVariable Long meetingId) {
|
||||||
|
AndroidRequestLogHelper.logRequest(log, "Android公有会议", "删除未开始会议", "meetingId", meetingId);
|
||||||
|
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||||
|
assertPublicDevice(authContext.getDeviceId());
|
||||||
|
Meeting meeting = meetingService.getById(meetingId);
|
||||||
|
if (meeting == null) {
|
||||||
|
throw new RuntimeException("会议不存在");
|
||||||
|
}
|
||||||
|
if (meeting.getSourceDeviceCode() == null || !meeting.getSourceDeviceCode().equals(authContext.getDeviceId())) {
|
||||||
|
throw new RuntimeException("当前会议不属于该设备");
|
||||||
|
}
|
||||||
|
if (meeting.getStatus() != null && meeting.getStatus() > 2) {
|
||||||
|
throw new RuntimeException("当前会议状态不允许删除");
|
||||||
|
}
|
||||||
|
meetingCommandService.deleteMeeting(meetingId);
|
||||||
|
return ApiResponse.ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 0, 1, 2)
|
||||||
|
.orderByDesc(Meeting::getId)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertPublicDevice(String deviceId) {
|
||||||
|
if (deviceId == null || deviceId.isBlank()) {
|
||||||
|
throw new RuntimeException("设备ID不能为空");
|
||||||
|
}
|
||||||
|
var device = deviceInfoMapper.selectByDeviceCodeIgnoreTenant(deviceId);
|
||||||
|
if (device != null && device.getUserId() != null) {
|
||||||
|
throw new RuntimeException("当前设备为私有设备,请走私有设备发会流程");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,10 +2,9 @@ package com.imeeting.controller.android.legacy;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
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.AndroidAuthContext;
|
||||||
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.LegacyMeetingAttendeeResponse;
|
import com.imeeting.dto.android.legacy.LegacyMeetingAttendeeResponse;
|
||||||
|
|
@ -31,7 +30,6 @@ import com.imeeting.service.biz.MeetingProgressService;
|
||||||
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.service.biz.impl.RedisOnlyMeetingProgressServiceAdapter;
|
|
||||||
import com.unisbase.common.annotation.Log;
|
import com.unisbase.common.annotation.Log;
|
||||||
import com.unisbase.dto.PageResult;
|
import com.unisbase.dto.PageResult;
|
||||||
import com.unisbase.entity.SysUser;
|
import com.unisbase.entity.SysUser;
|
||||||
|
|
@ -41,7 +39,6 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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;
|
||||||
|
|
@ -109,32 +106,6 @@ public class LegacyMeetingController {
|
||||||
new ObjectMapper());
|
new ObjectMapper());
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
|
||||||
meetingQueryService,
|
|
||||||
meetingAccessService,
|
|
||||||
meetingCommandService,
|
|
||||||
meetingService,
|
|
||||||
aiTaskService,
|
|
||||||
promptTemplateService,
|
|
||||||
meetingTranscriptMapper,
|
|
||||||
sysUserMapper,
|
|
||||||
new RedisOnlyMeetingProgressServiceAdapter(redisTemplate, objectMapper),
|
|
||||||
objectMapper
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public LegacyMeetingController(LegacyMeetingAdapterService legacyMeetingAdapterService,
|
public LegacyMeetingController(LegacyMeetingAdapterService legacyMeetingAdapterService,
|
||||||
MeetingQueryService meetingQueryService,
|
MeetingQueryService meetingQueryService,
|
||||||
|
|
@ -166,7 +137,7 @@ public class LegacyMeetingController {
|
||||||
@Log(value = "新增兼容会议", type = "兼容会议管理")
|
@Log(value = "新增兼容会议", type = "兼容会议管理")
|
||||||
public LegacyApiResponse<LegacyMeetingCreateResponse> create(@RequestBody LegacyMeetingCreateRequest request) {
|
public LegacyApiResponse<LegacyMeetingCreateResponse> create(@RequestBody LegacyMeetingCreateRequest request) {
|
||||||
AndroidRequestLogHelper.logRequest(log, "兼容会议", "创建会议接口", "request", request);
|
AndroidRequestLogHelper.logRequest(log, "兼容会议", "创建会议接口", "request", request);
|
||||||
MeetingVO meeting = legacyMeetingAdapterService.createMeeting(request, currentLoginUser());
|
MeetingVO meeting = legacyMeetingAdapterService.createMeeting(request, buildLegacyAuthContext(), currentLoginUser());
|
||||||
return LegacyApiResponse.ok(new LegacyMeetingCreateResponse(meeting.getId()));
|
return LegacyApiResponse.ok(new LegacyMeetingCreateResponse(meeting.getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -190,6 +161,7 @@ public class LegacyMeetingController {
|
||||||
modelCode,
|
modelCode,
|
||||||
forceReplace,
|
forceReplace,
|
||||||
audioFile,
|
audioFile,
|
||||||
|
buildLegacyAuthContext(),
|
||||||
currentLoginUser()
|
currentLoginUser()
|
||||||
);
|
);
|
||||||
return LegacyApiResponse.ok("上传成功", null);
|
return LegacyApiResponse.ok("上传成功", null);
|
||||||
|
|
@ -665,4 +637,7 @@ public class LegacyMeetingController {
|
||||||
private String resolveCreatorName(LoginUser loginUser) {
|
private String resolveCreatorName(LoginUser loginUser) {
|
||||||
return loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername();
|
return loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername();
|
||||||
}
|
}
|
||||||
|
private AndroidAuthContext buildLegacyAuthContext() {
|
||||||
|
return new AndroidAuthContext();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package com.imeeting.controller.biz;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.imeeting.common.MeetingConstants;
|
import com.imeeting.common.MeetingConstants;
|
||||||
import com.imeeting.common.RedisKeys;
|
|
||||||
import com.imeeting.common.SysParamKeys;
|
import com.imeeting.common.SysParamKeys;
|
||||||
import com.imeeting.dto.biz.CreateMeetingCommand;
|
import com.imeeting.dto.biz.CreateMeetingCommand;
|
||||||
import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
|
import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
|
||||||
|
|
@ -36,7 +35,6 @@ import com.imeeting.service.biz.PromptTemplateService;
|
||||||
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
||||||
import com.imeeting.service.biz.RealtimeMeetingSocketSessionService;
|
import com.imeeting.service.biz.RealtimeMeetingSocketSessionService;
|
||||||
import com.imeeting.service.biz.impl.MeetingAudioUploadSupport;
|
import com.imeeting.service.biz.impl.MeetingAudioUploadSupport;
|
||||||
import com.imeeting.service.biz.impl.RedisOnlyMeetingProgressServiceAdapter;
|
|
||||||
import com.unisbase.common.ApiResponse;
|
import com.unisbase.common.ApiResponse;
|
||||||
import com.unisbase.common.annotation.Log;
|
import com.unisbase.common.annotation.Log;
|
||||||
import com.unisbase.dto.PageResult;
|
import com.unisbase.dto.PageResult;
|
||||||
|
|
@ -46,7 +44,6 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
|
@ -88,7 +85,6 @@ public class MeetingController {
|
||||||
private final MeetingProgressService meetingProgressService;
|
private final MeetingProgressService meetingProgressService;
|
||||||
private final SysParamService sysParamService;
|
private final SysParamService sysParamService;
|
||||||
private final AiTaskService aiTaskService;
|
private final AiTaskService aiTaskService;
|
||||||
private AiTaskService compatibilityAiTaskService;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public MeetingController(MeetingQueryService meetingQueryService,
|
public MeetingController(MeetingQueryService meetingQueryService,
|
||||||
|
|
@ -117,35 +113,6 @@ public class MeetingController {
|
||||||
this.aiTaskService = aiTaskService;
|
this.aiTaskService = aiTaskService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MeetingController(MeetingQueryService meetingQueryService,
|
|
||||||
MeetingCommandService meetingCommandService,
|
|
||||||
MeetingAccessService meetingAccessService,
|
|
||||||
MeetingExportService meetingExportService,
|
|
||||||
MeetingTranscriptFileService meetingTranscriptFileService,
|
|
||||||
PromptTemplateService promptTemplateService,
|
|
||||||
RealtimeMeetingSocketSessionService realtimeMeetingSocketSessionService,
|
|
||||||
RealtimeMeetingSessionStateService realtimeMeetingSessionStateService,
|
|
||||||
AiTaskService unusedAiTaskService,
|
|
||||||
MeetingAudioUploadSupport meetingAudioUploadSupport,
|
|
||||||
StringRedisTemplate redisTemplate,
|
|
||||||
SysParamService sysParamService) {
|
|
||||||
this(
|
|
||||||
meetingQueryService,
|
|
||||||
meetingCommandService,
|
|
||||||
meetingAccessService,
|
|
||||||
meetingExportService,
|
|
||||||
meetingTranscriptFileService,
|
|
||||||
promptTemplateService,
|
|
||||||
realtimeMeetingSocketSessionService,
|
|
||||||
realtimeMeetingSessionStateService,
|
|
||||||
meetingAudioUploadSupport,
|
|
||||||
new RedisOnlyMeetingProgressServiceAdapter(redisTemplate, new com.fasterxml.jackson.databind.ObjectMapper()),
|
|
||||||
sysParamService,
|
|
||||||
unusedAiTaskService
|
|
||||||
);
|
|
||||||
this.compatibilityAiTaskService = unusedAiTaskService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "查询会议处理进度")
|
@Operation(summary = "查询会议处理进度")
|
||||||
@GetMapping("/{id}/progress")
|
@GetMapping("/{id}/progress")
|
||||||
@PreAuthorize("isAuthenticated()")
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
|
@ -154,8 +121,8 @@ public class MeetingController {
|
||||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||||
meetingAccessService.assertCanViewMeeting(meeting, loginUser);
|
meetingAccessService.assertCanViewMeeting(meeting, loginUser);
|
||||||
Map<String, Object> progress = meetingProgressService.getProgressMap(id);
|
Map<String, Object> progress = meetingProgressService.getProgressMap(id);
|
||||||
if (compatibilityAiTaskService != null && "Waiting...".equals(progress.get("message"))) {
|
if ("Waiting...".equals(progress.get("message"))) {
|
||||||
AiTask asrTask = compatibilityAiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
AiTask asrTask = aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
||||||
.eq(AiTask::getMeetingId, id)
|
.eq(AiTask::getMeetingId, id)
|
||||||
.eq(AiTask::getTaskType, "ASR")
|
.eq(AiTask::getTaskType, "ASR")
|
||||||
.orderByDesc(AiTask::getId)
|
.orderByDesc(AiTask::getId)
|
||||||
|
|
@ -188,8 +155,8 @@ public class MeetingController {
|
||||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||||
meetingAccessService.assertCanViewMeeting(meeting, loginUser);
|
meetingAccessService.assertCanViewMeeting(meeting, loginUser);
|
||||||
Map<String, Object> progress = meetingProgressService.getProgressMap(id);
|
Map<String, Object> progress = meetingProgressService.getProgressMap(id);
|
||||||
if (compatibilityAiTaskService != null && "Waiting...".equals(progress.get("message"))) {
|
if ("Waiting...".equals(progress.get("message"))) {
|
||||||
AiTask asrTask = compatibilityAiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
AiTask asrTask = aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
||||||
.eq(AiTask::getMeetingId, id)
|
.eq(AiTask::getMeetingId, id)
|
||||||
.eq(AiTask::getTaskType, "ASR")
|
.eq(AiTask::getTaskType, "ASR")
|
||||||
.orderByDesc(AiTask::getId)
|
.orderByDesc(AiTask::getId)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
package com.imeeting.controller.biz;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.imeeting.dto.android.AndroidPublicMeetingSessionState;
|
||||||
|
import com.imeeting.dto.biz.MeetingVO;
|
||||||
|
import com.imeeting.dto.biz.PublicDeviceMeetingCreateCommand;
|
||||||
|
import com.imeeting.entity.biz.Meeting;
|
||||||
|
import com.imeeting.service.android.AndroidMeetingPushService;
|
||||||
|
import com.imeeting.service.android.AndroidPublicMeetingSessionService;
|
||||||
|
import com.imeeting.service.biz.MeetingCommandService;
|
||||||
|
import com.imeeting.service.biz.MeetingQueryService;
|
||||||
|
import com.imeeting.service.biz.MeetingService;
|
||||||
|
import com.unisbase.common.ApiResponse;
|
||||||
|
import com.unisbase.security.LoginUser;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@Tag(name = "公有设备扫码建会接口")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/biz/public-device-meetings")
|
||||||
|
public class PublicDeviceMeetingController {
|
||||||
|
private final AndroidPublicMeetingSessionService androidPublicMeetingSessionService;
|
||||||
|
private final MeetingCommandService meetingCommandService;
|
||||||
|
private final MeetingQueryService meetingQueryService;
|
||||||
|
private final AndroidMeetingPushService androidMeetingPushService;
|
||||||
|
private final MeetingService meetingService;
|
||||||
|
|
||||||
|
public PublicDeviceMeetingController(AndroidPublicMeetingSessionService androidPublicMeetingSessionService,
|
||||||
|
MeetingCommandService meetingCommandService,
|
||||||
|
MeetingQueryService meetingQueryService,
|
||||||
|
AndroidMeetingPushService androidMeetingPushService,
|
||||||
|
MeetingService meetingService) {
|
||||||
|
this.androidPublicMeetingSessionService = androidPublicMeetingSessionService;
|
||||||
|
this.meetingCommandService = meetingCommandService;
|
||||||
|
this.meetingQueryService = meetingQueryService;
|
||||||
|
this.androidMeetingPushService = androidMeetingPushService;
|
||||||
|
this.meetingService = meetingService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "H5扫码为公有设备创建会议")
|
||||||
|
@io.swagger.v3.oas.annotations.responses.ApiResponses({
|
||||||
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "返回已创建的会议详情;若设备已有未结束会议,则返回已有会议",
|
||||||
|
content = @Content(schema = @Schema(implementation = MeetingVO.class))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
@PostMapping("/sessions/{sessionId}/create")
|
||||||
|
public ApiResponse<Object> createBySession(@PathVariable String sessionId,
|
||||||
|
@Valid @RequestBody PublicDeviceMeetingCreateCommand command) {
|
||||||
|
LoginUser loginUser = currentLoginUser();
|
||||||
|
AndroidPublicMeetingSessionState session = androidPublicMeetingSessionService.require(sessionId);
|
||||||
|
Meeting existingMeeting = findLatestUnfinishedMeetingByDevice(session.getDeviceId());
|
||||||
|
if (existingMeeting != null) {
|
||||||
|
return new ApiResponse<>("409", "设备端已有会议", meetingQueryService.getDetailIgnoreTenant(existingMeeting.getId()));
|
||||||
|
}
|
||||||
|
String creatorName = loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername();
|
||||||
|
MeetingVO vo = meetingCommandService.createPublicDeviceMeeting(
|
||||||
|
command,
|
||||||
|
loginUser.getTenantId(),
|
||||||
|
loginUser.getUserId(),
|
||||||
|
creatorName,
|
||||||
|
session.getDeviceId()
|
||||||
|
);
|
||||||
|
androidMeetingPushService.pushPendingMeetingToDevice(vo.getId(), session.getDeviceId());
|
||||||
|
androidPublicMeetingSessionService.clear(sessionId);
|
||||||
|
return ApiResponse.ok(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 0, 1, 2)
|
||||||
|
.orderByDesc(Meeting::getId)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoginUser currentLoginUser() {
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
if (authentication == null || !(authentication.getPrincipal() instanceof LoginUser loginUser)) {
|
||||||
|
throw new RuntimeException("未获取到登录用户");
|
||||||
|
}
|
||||||
|
return loginUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.imeeting.dto.android;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class AndroidChunkUploadSessionState {
|
||||||
|
private String uploadSessionId;
|
||||||
|
private Long meetingId;
|
||||||
|
private String deviceId;
|
||||||
|
private Integer totalChunks;
|
||||||
|
private String fileName;
|
||||||
|
private String contentType;
|
||||||
|
private Set<Integer> receivedChunks = new TreeSet<>();
|
||||||
|
}
|
||||||
|
|
@ -25,10 +25,20 @@ import java.util.List;
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class AndroidMeetingConfigVo {
|
public class AndroidMeetingConfigVo {
|
||||||
|
@io.swagger.v3.oas.annotations.media.Schema(description = "可用模型列表")
|
||||||
private List<AiModelVO> modelsList;
|
private List<AiModelVO> modelsList;
|
||||||
|
@io.swagger.v3.oas.annotations.media.Schema(description = "可用模板列表")
|
||||||
private List<PromptTemplateVO> templateList;
|
private List<PromptTemplateVO> templateList;
|
||||||
|
@io.swagger.v3.oas.annotations.media.Schema(description = "总结详细程度字典项")
|
||||||
private List<SysDictItemDTO> summaryDegreeOfDetail;
|
private List<SysDictItemDTO> summaryDegreeOfDetail;
|
||||||
|
@io.swagger.v3.oas.annotations.media.Schema(description = "允许暂停最大时长,单位秒")
|
||||||
private Integer maxPauseDuration;
|
private Integer maxPauseDuration;
|
||||||
|
@io.swagger.v3.oas.annotations.media.Schema(description = "最大会议时长,单位分钟")
|
||||||
private Integer maxMeetingDuration;
|
private Integer maxMeetingDuration;
|
||||||
|
@io.swagger.v3.oas.annotations.media.Schema(description = "允许的最大丢包率")
|
||||||
private BigDecimal packetLossRate;
|
private BigDecimal packetLossRate;
|
||||||
|
@io.swagger.v3.oas.annotations.media.Schema(description = "是否启用音频分片上传")
|
||||||
|
private Boolean chunkUploadEnabled;
|
||||||
|
@io.swagger.v3.oas.annotations.media.Schema(description = "分片时长,单位秒")
|
||||||
|
private Integer chunkDurationSeconds;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.imeeting.dto.android;
|
||||||
|
|
||||||
|
import com.imeeting.dto.biz.PublicDeviceMeetingCreateCommand;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class AndroidPendingMeetingDraft {
|
||||||
|
private Long meetingId;
|
||||||
|
private String deviceId;
|
||||||
|
private Long tenantId;
|
||||||
|
private Long creatorId;
|
||||||
|
private PublicDeviceMeetingCreateCommand command;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.imeeting.dto.android;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "公有设备发会准备请求")
|
||||||
|
public class AndroidPublicMeetingSessionRequest {
|
||||||
|
@Schema(description = "设备端展示用途的会话标题,可为空")
|
||||||
|
private String title;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.imeeting.dto.android;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class AndroidPublicMeetingSessionState {
|
||||||
|
private String sessionId;
|
||||||
|
private String sessionToken;
|
||||||
|
private String deviceId;
|
||||||
|
private String title;
|
||||||
|
private LocalDateTime expireAt;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.imeeting.dto.android;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "公有设备发会会话")
|
||||||
|
public class AndroidPublicMeetingSessionVO {
|
||||||
|
@Schema(description = "发会会话ID")
|
||||||
|
private String sessionId;
|
||||||
|
|
||||||
|
@Schema(description = "用于H5扫码建会的token")
|
||||||
|
private String sessionToken;
|
||||||
|
|
||||||
|
@Schema(description = "设备ID")
|
||||||
|
private String deviceId;
|
||||||
|
|
||||||
|
@Schema(description = "会话过期时间")
|
||||||
|
private LocalDateTime expireAt;
|
||||||
|
}
|
||||||
|
|
@ -43,6 +43,10 @@ public class MeetingVO {
|
||||||
private String meetingType;
|
private String meetingType;
|
||||||
@Schema(description = "会议来源")
|
@Schema(description = "会议来源")
|
||||||
private String meetingSource;
|
private String meetingSource;
|
||||||
|
@Schema(description = "来源设备编码")
|
||||||
|
private String sourceDeviceCode;
|
||||||
|
@Schema(description = "来源设备模式")
|
||||||
|
private String sourceDeviceMode;
|
||||||
@Schema(description = "总结详细程度")
|
@Schema(description = "总结详细程度")
|
||||||
private String summaryDetailLevel;
|
private String summaryDetailLevel;
|
||||||
@Schema(description = "音频保存状态")
|
@Schema(description = "音频保存状态")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.imeeting.common.MeetingConstants;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "公有设备扫码创建会议请求")
|
||||||
|
public class PublicDeviceMeetingCreateCommand {
|
||||||
|
@NotBlank(message = "标题不能为空")
|
||||||
|
@Schema(description = "会议标题")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@NotNull(message = "meetingTime不能为空")
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@Schema(description = "会议时间")
|
||||||
|
private LocalDateTime meetingTime;
|
||||||
|
|
||||||
|
@Schema(description = "参会人ID串,逗号分隔")
|
||||||
|
private String participants;
|
||||||
|
@Schema(description = "会议标签")
|
||||||
|
private String tags;
|
||||||
|
@Schema(description = "主持人用户ID")
|
||||||
|
private Long hostUserId;
|
||||||
|
@Schema(description = "主持人名称")
|
||||||
|
private String hostName;
|
||||||
|
|
||||||
|
@NotNull(message = "asrModelId不能为空")
|
||||||
|
@Schema(description = "ASR模型ID")
|
||||||
|
private Long asrModelId;
|
||||||
|
|
||||||
|
@NotNull(message = "summaryModelId不能为空")
|
||||||
|
@Schema(description = "总结模型ID")
|
||||||
|
private Long summaryModelId;
|
||||||
|
|
||||||
|
@Schema(description = "章节模型ID,可为空,默认复用总结模型")
|
||||||
|
private Long chapterModelId;
|
||||||
|
|
||||||
|
@NotNull(message = "promptId不能为空")
|
||||||
|
@Schema(description = "模板ID")
|
||||||
|
private Long promptId;
|
||||||
|
|
||||||
|
@Schema(description = "热词组ID")
|
||||||
|
private Long hotWordGroupId;
|
||||||
|
|
||||||
|
@Size(max = 2000, message = "userPrompt length must be <= 2000")
|
||||||
|
@Schema(description = "用户补充提示词")
|
||||||
|
private String userPrompt;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "总结详细程度:DETAILED=详细,STANDARD=标准,BRIEF=简洁",
|
||||||
|
allowableValues = {
|
||||||
|
MeetingConstants.SUMMARY_DETAIL_DETAILED,
|
||||||
|
MeetingConstants.SUMMARY_DETAIL_STANDARD,
|
||||||
|
MeetingConstants.SUMMARY_DETAIL_BRIEF
|
||||||
|
}
|
||||||
|
)
|
||||||
|
private String summaryDetailLevel;
|
||||||
|
|
||||||
|
@Schema(description = "是否启用说话人分离")
|
||||||
|
private Integer useSpkId;
|
||||||
|
@Schema(description = "是否启用文本规整")
|
||||||
|
private Boolean enableTextRefine;
|
||||||
|
@Schema(description = "热词列表")
|
||||||
|
private List<String> hotWords;
|
||||||
|
@Schema(description = "会议访问密码")
|
||||||
|
private String accessPassword;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package com.imeeting.entity.biz;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.unisbase.entity.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("biz_android_push_message")
|
||||||
|
public class AndroidPushMessage extends BaseEntity {
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private Long meetingId;
|
||||||
|
|
||||||
|
private String deviceCode;
|
||||||
|
|
||||||
|
private String messageId;
|
||||||
|
|
||||||
|
private String messageType;
|
||||||
|
|
||||||
|
private String payload;
|
||||||
|
|
||||||
|
private Integer needAck;
|
||||||
|
|
||||||
|
private Integer acked;
|
||||||
|
|
||||||
|
private String pushStatus;
|
||||||
|
|
||||||
|
private Integer pushCount;
|
||||||
|
|
||||||
|
private LocalDateTime lastPushAt;
|
||||||
|
|
||||||
|
private LocalDateTime ackAt;
|
||||||
|
|
||||||
|
private LocalDateTime expireAt;
|
||||||
|
}
|
||||||
|
|
@ -40,6 +40,13 @@ public class Meeting extends BaseEntity {
|
||||||
|
|
||||||
@Schema(description = "会议来源")
|
@Schema(description = "会议来源")
|
||||||
private String meetingSource;
|
private String meetingSource;
|
||||||
|
|
||||||
|
@Schema(description = "来源设备编码")
|
||||||
|
private String sourceDeviceCode;
|
||||||
|
|
||||||
|
@Schema(description = "来源设备模式")
|
||||||
|
private String sourceDeviceMode;
|
||||||
|
|
||||||
@Schema(description = "总结详细程度")
|
@Schema(description = "总结详细程度")
|
||||||
private String summaryDetailLevel;
|
private String summaryDetailLevel;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,14 @@ import lombok.Getter;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
public enum MeetingPushTypeEnum {
|
public enum MeetingPushTypeEnum {
|
||||||
MEETING_COMPLETED("MEETING_COMPLETED","会议完成通知"),;
|
MEETING_PENDING("MEETING_PENDING", "待开始会议通知"),
|
||||||
|
MEETING_COMPLETED("MEETING_COMPLETED", "会议完成通知");
|
||||||
|
|
||||||
private final String code;
|
private final String code;
|
||||||
private final String desc;
|
private final String desc;
|
||||||
MeetingPushTypeEnum(String code,String desc){
|
|
||||||
this.code=code;
|
|
||||||
this.desc=desc;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
MeetingPushTypeEnum(String code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.imeeting.grpc.push;
|
||||||
import com.imeeting.dto.android.AndroidAuthContext;
|
import com.imeeting.dto.android.AndroidAuthContext;
|
||||||
import com.imeeting.dto.android.AndroidDeviceSessionState;
|
import com.imeeting.dto.android.AndroidDeviceSessionState;
|
||||||
import com.imeeting.service.android.AndroidAuthService;
|
import com.imeeting.service.android.AndroidAuthService;
|
||||||
|
import com.imeeting.service.android.AndroidPushMessageService;
|
||||||
import com.imeeting.service.android.AndroidDeviceSessionService;
|
import com.imeeting.service.android.AndroidDeviceSessionService;
|
||||||
import com.imeeting.service.android.AndroidGatewayPushService;
|
import com.imeeting.service.android.AndroidGatewayPushService;
|
||||||
import com.imeeting.service.biz.DeviceOnlineManagementService;
|
import com.imeeting.service.biz.DeviceOnlineManagementService;
|
||||||
|
|
@ -26,6 +27,7 @@ public class AndroidPushGrpcService extends PushServiceGrpc.PushServiceImplBase
|
||||||
private final AndroidAuthService androidAuthService;
|
private final AndroidAuthService androidAuthService;
|
||||||
private final AndroidDeviceSessionService androidDeviceSessionService;
|
private final AndroidDeviceSessionService androidDeviceSessionService;
|
||||||
private final AndroidGatewayPushService androidGatewayPushService;
|
private final AndroidGatewayPushService androidGatewayPushService;
|
||||||
|
private final AndroidPushMessageService androidPushMessageService;
|
||||||
private final DeviceOnlineManagementService deviceOnlineManagementService;
|
private final DeviceOnlineManagementService deviceOnlineManagementService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -203,6 +205,7 @@ public class AndroidPushGrpcService extends PushServiceGrpc.PushServiceImplBase
|
||||||
deviceId,
|
deviceId,
|
||||||
appVersion,
|
appVersion,
|
||||||
platform));
|
platform));
|
||||||
|
androidPushMessageService.ack(request.getMessageId(), deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean validateConnected() {
|
private boolean validateConnected() {
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
package com.imeeting.listener;
|
package com.imeeting.listener;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.imeeting.common.RedisKeys;
|
|
||||||
import com.imeeting.entity.biz.Meeting;
|
import com.imeeting.entity.biz.Meeting;
|
||||||
import com.imeeting.mapper.biz.MeetingMapper;
|
import com.imeeting.mapper.biz.MeetingMapper;
|
||||||
import com.imeeting.service.biz.AiTaskService;
|
import com.imeeting.service.biz.AiTaskService;
|
||||||
import com.imeeting.support.RedisValueSupport;
|
|
||||||
import com.imeeting.support.TaskSecurityContextRunner;
|
import com.imeeting.support.TaskSecurityContextRunner;
|
||||||
|
import com.imeeting.support.redis.MeetingAsrPermitCache;
|
||||||
|
import com.imeeting.support.redis.MeetingLockCache;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.boot.ApplicationArguments;
|
import org.springframework.boot.ApplicationArguments;
|
||||||
import org.springframework.boot.ApplicationRunner;
|
import org.springframework.boot.ApplicationRunner;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -19,32 +18,14 @@ import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class MeetingTaskRecoveryListener implements ApplicationRunner {
|
public class MeetingTaskRecoveryListener implements ApplicationRunner {
|
||||||
|
|
||||||
private final MeetingMapper meetingMapper;
|
private final MeetingMapper meetingMapper;
|
||||||
private final AiTaskService aiTaskService;
|
private final AiTaskService aiTaskService;
|
||||||
private final RedisValueSupport redisValueSupport;
|
private final MeetingLockCache meetingLockCache;
|
||||||
|
private final MeetingAsrPermitCache meetingAsrPermitCache;
|
||||||
private final TaskSecurityContextRunner taskSecurityContextRunner;
|
private final TaskSecurityContextRunner taskSecurityContextRunner;
|
||||||
private StringRedisTemplate compatibilityRedisTemplate;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public MeetingTaskRecoveryListener(MeetingMapper meetingMapper,
|
|
||||||
AiTaskService aiTaskService,
|
|
||||||
RedisValueSupport redisValueSupport,
|
|
||||||
TaskSecurityContextRunner taskSecurityContextRunner) {
|
|
||||||
this.meetingMapper = meetingMapper;
|
|
||||||
this.aiTaskService = aiTaskService;
|
|
||||||
this.redisValueSupport = redisValueSupport;
|
|
||||||
this.taskSecurityContextRunner = taskSecurityContextRunner;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MeetingTaskRecoveryListener(MeetingMapper meetingMapper,
|
|
||||||
AiTaskService aiTaskService,
|
|
||||||
StringRedisTemplate redisTemplate,
|
|
||||||
TaskSecurityContextRunner taskSecurityContextRunner) {
|
|
||||||
this(meetingMapper, aiTaskService, new RedisValueSupport(redisTemplate, new com.fasterxml.jackson.databind.ObjectMapper()), taskSecurityContextRunner);
|
|
||||||
this.compatibilityRedisTemplate = redisTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run(ApplicationArguments args) {
|
public void run(ApplicationArguments args) {
|
||||||
|
|
@ -62,9 +43,8 @@ public class MeetingTaskRecoveryListener implements ApplicationRunner {
|
||||||
|
|
||||||
for (Meeting meeting : pendingMeetings) {
|
for (Meeting meeting : pendingMeetings) {
|
||||||
try {
|
try {
|
||||||
redisValueSupport.delete(RedisKeys.meetingPollingLockKey(meeting.getId()));
|
meetingLockCache.clearDispatchLocks(meeting.getId());
|
||||||
redisValueSupport.delete(RedisKeys.meetingSummaryLockKey(meeting.getId()));
|
meetingAsrPermitCache.clearRecoveryState(meeting.getId());
|
||||||
clearLegacyRedisState(meeting.getId());
|
|
||||||
|
|
||||||
if (Integer.valueOf(1).equals(meeting.getStatus())) {
|
if (Integer.valueOf(1).equals(meeting.getStatus())) {
|
||||||
log.info("Recovering ASR task for meeting {}", meeting.getId());
|
log.info("Recovering ASR task for meeting {}", meeting.getId());
|
||||||
|
|
@ -82,15 +62,4 @@ public class MeetingTaskRecoveryListener implements ApplicationRunner {
|
||||||
|
|
||||||
log.info("Meeting task recovery processed {} meetings.", pendingMeetings.size());
|
log.info("Meeting task recovery processed {} meetings.", pendingMeetings.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearLegacyRedisState(Long meetingId) {
|
|
||||||
if (compatibilityRedisTemplate == null || meetingId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
compatibilityRedisTemplate.delete(List.of(
|
|
||||||
RedisKeys.meetingAsrPermitSyncLockKey(),
|
|
||||||
RedisKeys.meetingAsrRefillLockKey()
|
|
||||||
));
|
|
||||||
compatibilityRedisTemplate.opsForSet().remove(RedisKeys.meetingAsrPermitSetKey(), String.valueOf(meetingId));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.imeeting.mapper.biz;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.imeeting.entity.biz.AndroidPushMessage;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface AndroidPushMessageMapper extends BaseMapper<AndroidPushMessage> {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package com.imeeting.service.android;
|
||||||
|
|
||||||
|
import com.imeeting.dto.android.AndroidAuthContext;
|
||||||
|
import com.imeeting.dto.android.legacy.LegacyUploadAudioResponse;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public interface AndroidChunkUploadService {
|
||||||
|
void saveChunk(Long meetingId,
|
||||||
|
String uploadSessionId,
|
||||||
|
Integer chunkIndex,
|
||||||
|
Integer totalChunks,
|
||||||
|
MultipartFile chunkFile,
|
||||||
|
AndroidAuthContext authContext) throws IOException;
|
||||||
|
|
||||||
|
LegacyUploadAudioResponse completeUpload(Long meetingId,
|
||||||
|
String uploadSessionId,
|
||||||
|
boolean forceReplace,
|
||||||
|
Long promptId,
|
||||||
|
String modelCode,
|
||||||
|
AndroidAuthContext authContext) throws IOException;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.imeeting.service.android;
|
||||||
|
|
||||||
|
public interface AndroidDeviceBindingService {
|
||||||
|
void bindPrivateDevice(String deviceCode, Long tenantId, Long userId, String appVersion, String platform);
|
||||||
|
|
||||||
|
void validatePrivateDeviceAccess(String deviceCode, Long tenantId, Long userId);
|
||||||
|
|
||||||
|
void unbindPrivateDevice(String deviceCode);
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
package com.imeeting.service.android;
|
package com.imeeting.service.android;
|
||||||
|
|
||||||
public interface AndroidMeetingPushService {
|
public interface AndroidMeetingPushService {
|
||||||
|
void pushPendingMeetingToDevice(Long meetingId, String deviceId);
|
||||||
|
|
||||||
void pushMeetingCompleted(Long meetingId);
|
void pushMeetingCompleted(Long meetingId);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.imeeting.service.android;
|
||||||
|
|
||||||
|
import com.imeeting.dto.android.AndroidPendingMeetingDraft;
|
||||||
|
|
||||||
|
public interface AndroidPendingMeetingDraftService {
|
||||||
|
void save(AndroidPendingMeetingDraft draft);
|
||||||
|
|
||||||
|
AndroidPendingMeetingDraft get(Long meetingId);
|
||||||
|
|
||||||
|
void clear(Long meetingId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.imeeting.service.android;
|
||||||
|
|
||||||
|
import com.imeeting.dto.android.AndroidPublicMeetingSessionState;
|
||||||
|
import com.imeeting.dto.android.AndroidPublicMeetingSessionVO;
|
||||||
|
|
||||||
|
public interface AndroidPublicMeetingSessionService {
|
||||||
|
AndroidPublicMeetingSessionVO create(String deviceId, String title);
|
||||||
|
|
||||||
|
AndroidPublicMeetingSessionState require(String sessionId);
|
||||||
|
|
||||||
|
void clear(String sessionId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.imeeting.service.android;
|
||||||
|
|
||||||
|
import com.imeeting.entity.biz.AndroidPushMessage;
|
||||||
|
import com.imeeting.grpc.push.PushMessage;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface AndroidPushMessageService {
|
||||||
|
AndroidPushMessage saveMeetingPushMessage(Long tenantId, Long meetingId, String deviceCode, PushMessage pushMessage, long expireAfterMinutes);
|
||||||
|
|
||||||
|
boolean ack(String messageId, String deviceCode);
|
||||||
|
|
||||||
|
List<AndroidPushMessage> listPendingMeetingPushMessages();
|
||||||
|
|
||||||
|
void markPushed(Long id);
|
||||||
|
|
||||||
|
void markExpired(Long id);
|
||||||
|
|
||||||
|
void markCancelledByMeeting(Long meetingId);
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import com.imeeting.dto.android.AndroidAuthContext;
|
||||||
import com.imeeting.entity.biz.DeviceInfoEntity;
|
import com.imeeting.entity.biz.DeviceInfoEntity;
|
||||||
import com.imeeting.mapper.DeviceInfoMapper;
|
import com.imeeting.mapper.DeviceInfoMapper;
|
||||||
import com.imeeting.service.android.AndroidAuthService;
|
import com.imeeting.service.android.AndroidAuthService;
|
||||||
|
import com.imeeting.service.android.AndroidDeviceBindingService;
|
||||||
import com.unisbase.common.exception.BusinessException;
|
import com.unisbase.common.exception.BusinessException;
|
||||||
import com.unisbase.dto.InternalAuthCheckResponse;
|
import com.unisbase.dto.InternalAuthCheckResponse;
|
||||||
import com.unisbase.security.LoginUser;
|
import com.unisbase.security.LoginUser;
|
||||||
|
|
@ -32,6 +33,7 @@ public class AndroidAuthServiceImpl implements AndroidAuthService {
|
||||||
private final AndroidGrpcAuthProperties properties;
|
private final AndroidGrpcAuthProperties properties;
|
||||||
private final TokenValidationService tokenValidationService;
|
private final TokenValidationService tokenValidationService;
|
||||||
private final DeviceInfoMapper deviceInfoMapper;
|
private final DeviceInfoMapper deviceInfoMapper;
|
||||||
|
private final AndroidDeviceBindingService androidDeviceBindingService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AndroidAuthContext authenticateGrpc(String deviceId, String appVersion, String platform) {
|
public AndroidAuthContext authenticateGrpc(String deviceId, String appVersion, String platform) {
|
||||||
|
|
@ -39,7 +41,13 @@ public class AndroidAuthServiceImpl implements AndroidAuthService {
|
||||||
throw new RuntimeException("Android gRPC push does not allow anonymous access");
|
throw new RuntimeException("Android gRPC push does not allow anonymous access");
|
||||||
}
|
}
|
||||||
assertDeviceEnabled(deviceId);
|
assertDeviceEnabled(deviceId);
|
||||||
return buildContext("NONE", true, deviceId, null, appVersion, platform, null, null, null, null);
|
AndroidAuthContext context = buildContext("NONE", true, deviceId, null, appVersion, platform, null, null, null, null);
|
||||||
|
DeviceInfoEntity device = deviceInfoMapper.selectByDeviceCodeIgnoreTenant(deviceId.trim());
|
||||||
|
if (device != null) {
|
||||||
|
context.setUserId(device.getUserId());
|
||||||
|
context.setTenantId(device.getTenantId());
|
||||||
|
}
|
||||||
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -55,6 +63,7 @@ public class AndroidAuthServiceImpl implements AndroidAuthService {
|
||||||
log.info("[安卓接口访问]X-Android-Device-Id={},X-Android-App-Version={},X-Android-Platform={}",deviceId,appVersion,platform);
|
log.info("[安卓接口访问]X-Android-Device-Id={},X-Android-App-Version={},X-Android-Platform={}",deviceId,appVersion,platform);
|
||||||
assertDeviceEnabled(deviceId);
|
assertDeviceEnabled(deviceId);
|
||||||
if (loginUser != null) {
|
if (loginUser != null) {
|
||||||
|
androidDeviceBindingService.validatePrivateDeviceAccess(deviceId, loginUser.getTenantId(), loginUser.getUserId());
|
||||||
return buildContext("USER_JWT", false,
|
return buildContext("USER_JWT", false,
|
||||||
deviceId,
|
deviceId,
|
||||||
appId,
|
appId,
|
||||||
|
|
@ -68,6 +77,7 @@ public class AndroidAuthServiceImpl implements AndroidAuthService {
|
||||||
|
|
||||||
if (StringUtils.hasText(resolvedToken)) {
|
if (StringUtils.hasText(resolvedToken)) {
|
||||||
InternalAuthCheckResponse authResult = validateToken(resolvedToken);
|
InternalAuthCheckResponse authResult = validateToken(resolvedToken);
|
||||||
|
androidDeviceBindingService.validatePrivateDeviceAccess(deviceId, authResult.getTenantId(), authResult.getUserId());
|
||||||
return buildContext("USER_JWT", false,
|
return buildContext("USER_JWT", false,
|
||||||
deviceId,
|
deviceId,
|
||||||
appId,
|
appId,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,243 @@
|
||||||
|
package com.imeeting.service.android.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.imeeting.dto.android.AndroidAuthContext;
|
||||||
|
import com.imeeting.dto.android.AndroidChunkUploadSessionState;
|
||||||
|
import com.imeeting.dto.android.legacy.LegacyUploadAudioResponse;
|
||||||
|
import com.imeeting.service.android.AndroidChunkUploadService;
|
||||||
|
import com.imeeting.service.android.legacy.LegacyMeetingAdapterService;
|
||||||
|
import com.imeeting.support.redis.AndroidChunkUploadSessionCache;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import com.unisbase.security.LoginUser;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AndroidChunkUploadServiceImpl implements AndroidChunkUploadService {
|
||||||
|
private final AndroidChunkUploadSessionCache sessionCache;
|
||||||
|
private final LegacyMeetingAdapterService legacyMeetingAdapterService;
|
||||||
|
|
||||||
|
@Value("${unisbase.app.upload-path}")
|
||||||
|
private String uploadPath;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveChunk(Long meetingId,
|
||||||
|
String uploadSessionId,
|
||||||
|
Integer chunkIndex,
|
||||||
|
Integer totalChunks,
|
||||||
|
MultipartFile chunkFile,
|
||||||
|
AndroidAuthContext authContext) throws IOException {
|
||||||
|
if (meetingId == null || uploadSessionId == null || uploadSessionId.isBlank()) {
|
||||||
|
throw new RuntimeException("uploadSessionId不能为空");
|
||||||
|
}
|
||||||
|
if (chunkIndex == null || totalChunks == null || chunkIndex < 0 || totalChunks <= 0) {
|
||||||
|
throw new RuntimeException("分片参数无效");
|
||||||
|
}
|
||||||
|
if (chunkFile == null || chunkFile.isEmpty()) {
|
||||||
|
throw new RuntimeException("chunk_file不能为空");
|
||||||
|
}
|
||||||
|
AndroidChunkUploadSessionState state = getOrCreateState(meetingId, uploadSessionId, totalChunks, chunkFile, authContext);
|
||||||
|
if (!Objects.equals(state.getMeetingId(), meetingId) || !Objects.equals(state.getDeviceId(), authContext.getDeviceId())) {
|
||||||
|
throw new RuntimeException("分片上传会话与当前设备或会议不匹配");
|
||||||
|
}
|
||||||
|
Path sessionDir = sessionDir(uploadSessionId);
|
||||||
|
Files.createDirectories(sessionDir);
|
||||||
|
Files.write(sessionDir.resolve(chunkIndex + ".part"), chunkFile.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||||
|
state.getReceivedChunks().add(chunkIndex);
|
||||||
|
saveState(uploadSessionId, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LegacyUploadAudioResponse completeUpload(Long meetingId,
|
||||||
|
String uploadSessionId,
|
||||||
|
boolean forceReplace,
|
||||||
|
Long promptId,
|
||||||
|
String modelCode,
|
||||||
|
AndroidAuthContext authContext) throws IOException {
|
||||||
|
AndroidChunkUploadSessionState state = requireState(uploadSessionId);
|
||||||
|
if (!Objects.equals(state.getMeetingId(), meetingId) || !Objects.equals(state.getDeviceId(), authContext.getDeviceId())) {
|
||||||
|
throw new RuntimeException("分片上传会话与当前设备或会议不匹配");
|
||||||
|
}
|
||||||
|
for (int i = 0; i < state.getTotalChunks(); i++) {
|
||||||
|
if (!state.getReceivedChunks().contains(i)) {
|
||||||
|
throw new RuntimeException("分片未上传完整");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Path mergedFile = mergeChunks(state);
|
||||||
|
try {
|
||||||
|
MultipartFile mergedMultipart = new LocalMultipartFile(
|
||||||
|
state.getFileName() == null ? "meeting-audio.bin" : state.getFileName(),
|
||||||
|
state.getContentType(),
|
||||||
|
Files.readAllBytes(mergedFile)
|
||||||
|
);
|
||||||
|
if (authContext.isAnonymous()) {
|
||||||
|
return legacyMeetingAdapterService.uploadAndTriggerOfflineProcessForPublicDevice(
|
||||||
|
meetingId,
|
||||||
|
forceReplace,
|
||||||
|
mergedMultipart,
|
||||||
|
authContext
|
||||||
|
);
|
||||||
|
}
|
||||||
|
LoginUser loginUser = toLoginUser(authContext);
|
||||||
|
return legacyMeetingAdapterService.uploadAndTriggerOfflineProcess(
|
||||||
|
meetingId,
|
||||||
|
promptId,
|
||||||
|
modelCode,
|
||||||
|
forceReplace,
|
||||||
|
mergedMultipart,
|
||||||
|
authContext,
|
||||||
|
loginUser
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
cleanup(uploadSessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AndroidChunkUploadSessionState getOrCreateState(Long meetingId,
|
||||||
|
String uploadSessionId,
|
||||||
|
Integer totalChunks,
|
||||||
|
MultipartFile chunkFile,
|
||||||
|
AndroidAuthContext authContext) throws IOException {
|
||||||
|
AndroidChunkUploadSessionState existing = getState(uploadSessionId);
|
||||||
|
if (existing != null) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
AndroidChunkUploadSessionState state = new AndroidChunkUploadSessionState();
|
||||||
|
state.setUploadSessionId(uploadSessionId);
|
||||||
|
state.setMeetingId(meetingId);
|
||||||
|
state.setDeviceId(authContext.getDeviceId());
|
||||||
|
state.setTotalChunks(totalChunks);
|
||||||
|
state.setFileName(chunkFile.getOriginalFilename());
|
||||||
|
state.setContentType(chunkFile.getContentType());
|
||||||
|
saveState(uploadSessionId, state);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path mergeChunks(AndroidChunkUploadSessionState state) throws IOException {
|
||||||
|
Path sessionDir = sessionDir(state.getUploadSessionId());
|
||||||
|
Path merged = sessionDir.resolve("merged.bin");
|
||||||
|
Files.deleteIfExists(merged);
|
||||||
|
Files.createFile(merged);
|
||||||
|
for (int i = 0; i < state.getTotalChunks(); i++) {
|
||||||
|
Files.write(merged, Files.readAllBytes(sessionDir.resolve(i + ".part")), StandardOpenOption.APPEND);
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AndroidChunkUploadSessionState requireState(String uploadSessionId) {
|
||||||
|
AndroidChunkUploadSessionState state = getState(uploadSessionId);
|
||||||
|
if (state == null) {
|
||||||
|
throw new RuntimeException("分片上传会话不存在或已过期");
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AndroidChunkUploadSessionState getState(String uploadSessionId) {
|
||||||
|
return sessionCache.get(uploadSessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveState(String uploadSessionId, AndroidChunkUploadSessionState state) {
|
||||||
|
sessionCache.save(uploadSessionId, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanup(String uploadSessionId) throws IOException {
|
||||||
|
sessionCache.clear(uploadSessionId);
|
||||||
|
Path sessionDir = sessionDir(uploadSessionId);
|
||||||
|
if (Files.exists(sessionDir)) {
|
||||||
|
try (var paths = Files.walk(sessionDir)) {
|
||||||
|
paths.sorted((left, right) -> right.compareTo(left)).forEach(path -> {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(path);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path sessionDir(String uploadSessionId) {
|
||||||
|
String normalizedBasePath = uploadPath.endsWith("/") || uploadPath.endsWith("\\") ? uploadPath : uploadPath + "/";
|
||||||
|
return Paths.get(normalizedBasePath, "android-chunks", uploadSessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoginUser toLoginUser(AndroidAuthContext authContext) {
|
||||||
|
if (authContext == null || authContext.isAnonymous() || authContext.getUserId() == null || authContext.getTenantId() == null) {
|
||||||
|
throw new RuntimeException("安卓用户未登录或认证无效");
|
||||||
|
}
|
||||||
|
LoginUser loginUser = new LoginUser(
|
||||||
|
authContext.getUserId(),
|
||||||
|
authContext.getTenantId(),
|
||||||
|
authContext.getUsername(),
|
||||||
|
authContext.getPlatformAdmin(),
|
||||||
|
authContext.getTenantAdmin(),
|
||||||
|
authContext.getPermissions()
|
||||||
|
);
|
||||||
|
loginUser.setDisplayName(authContext.getDisplayName());
|
||||||
|
return loginUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class LocalMultipartFile implements MultipartFile {
|
||||||
|
private final String originalFilename;
|
||||||
|
private final String contentType;
|
||||||
|
private final byte[] bytes;
|
||||||
|
|
||||||
|
private LocalMultipartFile(String originalFilename, String contentType, byte[] bytes) {
|
||||||
|
this.originalFilename = originalFilename;
|
||||||
|
this.contentType = contentType;
|
||||||
|
this.bytes = bytes == null ? new byte[0] : bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return originalFilename;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getOriginalFilename() {
|
||||||
|
return originalFilename;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getContentType() {
|
||||||
|
return contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return bytes.length == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getSize() {
|
||||||
|
return bytes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getBytes() {
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() {
|
||||||
|
return new ByteArrayInputStream(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void transferTo(java.io.File dest) throws IOException, IllegalStateException {
|
||||||
|
Files.write(dest.toPath(), bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
package com.imeeting.service.android.impl;
|
||||||
|
|
||||||
|
import com.imeeting.mapper.DeviceInfoMapper;
|
||||||
|
import com.imeeting.entity.biz.DeviceInfoEntity;
|
||||||
|
import com.imeeting.service.android.AndroidDeviceBindingService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AndroidDeviceBindingServiceImpl implements AndroidDeviceBindingService {
|
||||||
|
private final DeviceInfoMapper deviceInfoMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void bindPrivateDevice(String deviceCode, Long tenantId, Long userId, String appVersion, String platform) {
|
||||||
|
if (!StringUtils.hasText(deviceCode) || userId == null || tenantId == null) {
|
||||||
|
throw new RuntimeException("设备登录缺少绑定上下文");
|
||||||
|
}
|
||||||
|
DeviceInfoEntity existing = deviceInfoMapper.selectByDeviceCodeIgnoreTenant(deviceCode.trim());
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
if (existing == null) {
|
||||||
|
DeviceInfoEntity created = new DeviceInfoEntity();
|
||||||
|
created.setTenantId(tenantId);
|
||||||
|
created.setUserId(userId);
|
||||||
|
created.setDeviceCode(deviceCode.trim());
|
||||||
|
created.setTerminalType(normalize(platform));
|
||||||
|
created.setTerminalVersion(normalize(appVersion));
|
||||||
|
created.setLastOnlineAt(now);
|
||||||
|
created.setStatus(1);
|
||||||
|
deviceInfoMapper.insert(created);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
existing.setTenantId(tenantId);
|
||||||
|
existing.setUserId(userId);
|
||||||
|
existing.setTerminalType(normalize(platform));
|
||||||
|
existing.setTerminalVersion(normalize(appVersion));
|
||||||
|
existing.setLastOnlineAt(now);
|
||||||
|
deviceInfoMapper.updateConnectionInfoByIdIgnoreTenant(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validatePrivateDeviceAccess(String deviceCode, Long tenantId, Long userId) {
|
||||||
|
if (!StringUtils.hasText(deviceCode) || userId == null || tenantId == null) {
|
||||||
|
throw new RuntimeException("设备登录态无效");
|
||||||
|
}
|
||||||
|
DeviceInfoEntity existing = deviceInfoMapper.selectByDeviceCodeIgnoreTenant(deviceCode.trim());
|
||||||
|
if (existing == null || existing.getUserId() == null || existing.getTenantId() == null) {
|
||||||
|
throw new RuntimeException("设备未登录,请先完成设备登录");
|
||||||
|
}
|
||||||
|
if (!Objects.equals(existing.getUserId(), userId) || !Objects.equals(existing.getTenantId(), tenantId)) {
|
||||||
|
throw new RuntimeException("当前设备已被其他用户占用,请使用当前登录用户或重新登录占用设备");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unbindPrivateDevice(String deviceCode) {
|
||||||
|
if (!StringUtils.hasText(deviceCode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DeviceInfoEntity existing = deviceInfoMapper.selectByDeviceCodeIgnoreTenant(deviceCode.trim());
|
||||||
|
if (existing == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
existing.setUserId(null);
|
||||||
|
existing.setTenantId(null);
|
||||||
|
deviceInfoMapper.updateConnectionInfoByIdIgnoreTenant(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalize(String value) {
|
||||||
|
if (!StringUtils.hasText(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
package com.imeeting.service.android.impl;
|
package com.imeeting.service.android.impl;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.imeeting.common.RedisKeys;
|
|
||||||
import com.imeeting.config.grpc.GrpcServerProperties;
|
import com.imeeting.config.grpc.GrpcServerProperties;
|
||||||
import com.imeeting.dto.android.AndroidAuthContext;
|
import com.imeeting.dto.android.AndroidAuthContext;
|
||||||
import com.imeeting.dto.android.AndroidDeviceSessionState;
|
import com.imeeting.dto.android.AndroidDeviceSessionState;
|
||||||
import com.imeeting.service.android.AndroidDeviceSessionService;
|
import com.imeeting.service.android.AndroidDeviceSessionService;
|
||||||
|
import com.imeeting.support.redis.AndroidDeviceSessionCache;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
@ -24,8 +22,7 @@ public class AndroidDeviceSessionServiceImpl implements AndroidDeviceSessionServ
|
||||||
|
|
||||||
private static final DateTimeFormatter LOG_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
private static final DateTimeFormatter LOG_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
private final StringRedisTemplate redisTemplate;
|
private final AndroidDeviceSessionCache sessionCache;
|
||||||
private final ObjectMapper objectMapper;
|
|
||||||
private final GrpcServerProperties grpcServerProperties;
|
private final GrpcServerProperties grpcServerProperties;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -61,63 +58,37 @@ public class AndroidDeviceSessionServiceImpl implements AndroidDeviceSessionServ
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AndroidDeviceSessionState getByConnectionId(String connectionId) {
|
public AndroidDeviceSessionState getByConnectionId(String connectionId) {
|
||||||
String raw = redisTemplate.opsForValue().get(RedisKeys.androidDeviceConnectionKey(connectionId));
|
return sessionCache.getByConnectionId(connectionId);
|
||||||
if (raw == null || raw.isBlank()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return objectMapper.readValue(raw, AndroidDeviceSessionState.class);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.warn("Failed to read android device session, connectionId={}", connectionId, ex);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AndroidDeviceSessionState getByDeviceId(String deviceId) {
|
public AndroidDeviceSessionState getByDeviceId(String deviceId) {
|
||||||
String raw = redisTemplate.opsForValue().get(RedisKeys.androidDeviceOnlineKey(deviceId));
|
return sessionCache.getByDeviceId(deviceId);
|
||||||
if (raw == null || raw.isBlank()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return objectMapper.readValue(raw, AndroidDeviceSessionState.class);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.warn("Failed to read android device online state, deviceId={}", deviceId, ex);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getActiveConnectionId(String deviceId) {
|
public String getActiveConnectionId(String deviceId) {
|
||||||
String value = redisTemplate.opsForValue().get(RedisKeys.androidDeviceActiveConnectionKey(deviceId));
|
return sessionCache.getActiveConnectionId(deviceId);
|
||||||
return value == null || value.isBlank() ? null : value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateTopics(String deviceId, List<String> topics) {
|
public void updateTopics(String deviceId, List<String> topics) {
|
||||||
try {
|
sessionCache.saveTopics(deviceId, topics);
|
||||||
redisTemplate.opsForValue().set(
|
|
||||||
RedisKeys.androidDeviceTopicsKey(deviceId),
|
|
||||||
objectMapper.writeValueAsString(topics == null ? List.of() : topics)
|
|
||||||
);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
throw new RuntimeException("Failed to update android device topics", ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void closeSession(String connectionId) {
|
public void closeSession(String connectionId) {
|
||||||
AndroidDeviceSessionState state = getByConnectionId(connectionId);
|
AndroidDeviceSessionState state = getByConnectionId(connectionId);
|
||||||
if (state == null) {
|
if (state == null) {
|
||||||
redisTemplate.delete(RedisKeys.androidDeviceConnectionKey(connectionId));
|
sessionCache.deleteConnection(connectionId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String activeConn = getActiveConnectionId(state.getDeviceId());
|
String activeConn = getActiveConnectionId(state.getDeviceId());
|
||||||
if (connectionId.equals(activeConn)) {
|
if (connectionId.equals(activeConn)) {
|
||||||
redisTemplate.delete(RedisKeys.androidDeviceActiveConnectionKey(state.getDeviceId()));
|
sessionCache.deleteActiveConnection(state.getDeviceId());
|
||||||
redisTemplate.delete(RedisKeys.androidDeviceOnlineKey(state.getDeviceId()));
|
sessionCache.deleteOnlineState(state.getDeviceId());
|
||||||
}
|
}
|
||||||
redisTemplate.delete(RedisKeys.androidDeviceConnectionKey(connectionId));
|
sessionCache.deleteConnection(connectionId);
|
||||||
log.info(buildLog("gRPC会话关闭",
|
log.info(buildLog("gRPC会话关闭",
|
||||||
"关闭Android设备会话,连接ID=" + connectionId,
|
"关闭Android设备会话,连接ID=" + connectionId,
|
||||||
state.getDeviceId(),
|
state.getDeviceId(),
|
||||||
|
|
@ -127,14 +98,7 @@ public class AndroidDeviceSessionServiceImpl implements AndroidDeviceSessionServ
|
||||||
|
|
||||||
private void writeState(AndroidDeviceSessionState state) {
|
private void writeState(AndroidDeviceSessionState state) {
|
||||||
Duration ttl = Duration.ofSeconds(grpcServerProperties.getGateway().getHeartbeatTimeoutSeconds());
|
Duration ttl = Duration.ofSeconds(grpcServerProperties.getGateway().getHeartbeatTimeoutSeconds());
|
||||||
try {
|
sessionCache.saveState(state, ttl);
|
||||||
String json = objectMapper.writeValueAsString(state);
|
|
||||||
redisTemplate.opsForValue().set(RedisKeys.androidDeviceOnlineKey(state.getDeviceId()), json, ttl);
|
|
||||||
redisTemplate.opsForValue().set(RedisKeys.androidDeviceActiveConnectionKey(state.getDeviceId()), state.getConnectionId(), ttl);
|
|
||||||
redisTemplate.opsForValue().set(RedisKeys.androidDeviceConnectionKey(state.getConnectionId()), json, ttl);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
throw new RuntimeException("Failed to store android device session", ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String nonBlank(String value, String defaultValue) {
|
private String nonBlank(String value, String defaultValue) {
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,17 @@
|
||||||
package com.imeeting.service.android.impl;
|
package com.imeeting.service.android.impl;
|
||||||
|
|
||||||
import cn.hutool.json.JSON;
|
|
||||||
import cn.hutool.json.JSONUtil;
|
import cn.hutool.json.JSONUtil;
|
||||||
import com.imeeting.dto.biz.MeetingVO;
|
import com.imeeting.dto.biz.MeetingVO;
|
||||||
import com.imeeting.entity.biz.Meeting;
|
|
||||||
import com.imeeting.enums.MeetingPushTypeEnum;
|
import com.imeeting.enums.MeetingPushTypeEnum;
|
||||||
import com.imeeting.grpc.push.PushMessage;
|
import com.imeeting.grpc.push.PushMessage;
|
||||||
|
|
||||||
import com.imeeting.service.android.AndroidGatewayPushService;
|
import com.imeeting.service.android.AndroidGatewayPushService;
|
||||||
import com.imeeting.service.android.AndroidMeetingPushService;
|
import com.imeeting.service.android.AndroidMeetingPushService;
|
||||||
|
import com.imeeting.service.android.AndroidPushMessageService;
|
||||||
import com.imeeting.service.biz.MeetingQueryService;
|
import com.imeeting.service.biz.MeetingQueryService;
|
||||||
import com.imeeting.service.biz.MeetingService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
|
@ -25,23 +23,50 @@ import java.util.UUID;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
|
|
||||||
public class AndroidMeetingPushServiceImpl implements AndroidMeetingPushService {
|
public class AndroidMeetingPushServiceImpl implements AndroidMeetingPushService {
|
||||||
|
|
||||||
private static final DateTimeFormatter TITLE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
private static final DateTimeFormatter TITLE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Lazy
|
@Lazy
|
||||||
private MeetingQueryService meetingService;
|
private MeetingQueryService meetingQueryService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private AndroidGatewayPushService androidGatewayPushService;
|
private AndroidGatewayPushService androidGatewayPushService;
|
||||||
|
@Autowired
|
||||||
|
private AndroidPushMessageService androidPushMessageService;
|
||||||
|
|
||||||
|
@Value("${imeeting.android.push.pending-expire-minutes:30}")
|
||||||
|
private long pendingExpireMinutes;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void pushPendingMeetingToDevice(Long meetingId, String deviceId) {
|
||||||
|
if (meetingId == null || deviceId == null || deviceId.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MeetingVO meeting = meetingQueryService.getDetailIgnoreTenant(meetingId);
|
||||||
|
if (meeting == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PushMessage message = PushMessage.newBuilder()
|
||||||
|
.setMessageId("meeting_pending:" + meetingId + ":" + UUID.randomUUID())
|
||||||
|
.setTimestamp(System.currentTimeMillis())
|
||||||
|
.setType(MeetingPushTypeEnum.MEETING_PENDING.getCode())
|
||||||
|
.setTitle(resolvePendingTitle(meeting))
|
||||||
|
.setContent(buildPendingContent(meeting))
|
||||||
|
.setNeedAck(true)
|
||||||
|
.build();
|
||||||
|
var pushEntity = androidPushMessageService.saveMeetingPushMessage(meeting.getTenantId(), meetingId, deviceId, message, pendingExpireMinutes);
|
||||||
|
int pushed = androidGatewayPushService.pushToDevice(deviceId, message);
|
||||||
|
if (pushEntity.getId() != null) {
|
||||||
|
androidPushMessageService.markPushed(pushEntity.getId());
|
||||||
|
}
|
||||||
|
log.info("Android pending meeting push finished, meetingId={}, deviceId={}, pushedConnections={}", meetingId, deviceId, pushed);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void pushMeetingCompleted(Long meetingId) {
|
public void pushMeetingCompleted(Long meetingId) {
|
||||||
if (meetingId == null) {
|
if (meetingId == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
MeetingVO meeting = meetingService.getDetailIgnoreTenant(meetingId);
|
MeetingVO meeting = meetingQueryService.getDetailIgnoreTenant(meetingId);
|
||||||
if (meeting == null || meeting.getTenantId() == null || meeting.getCreatorId() == null) {
|
if (meeting == null || meeting.getTenantId() == null || meeting.getCreatorId() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -49,8 +74,8 @@ public class AndroidMeetingPushServiceImpl implements AndroidMeetingPushService
|
||||||
.setMessageId("meeting_completed:" + meetingId + ":" + UUID.randomUUID())
|
.setMessageId("meeting_completed:" + meetingId + ":" + UUID.randomUUID())
|
||||||
.setTimestamp(System.currentTimeMillis())
|
.setTimestamp(System.currentTimeMillis())
|
||||||
.setType(MeetingPushTypeEnum.MEETING_COMPLETED.getCode())
|
.setType(MeetingPushTypeEnum.MEETING_COMPLETED.getCode())
|
||||||
.setTitle(resolveTitle(meeting))
|
.setTitle(resolveCompletedTitle(meeting))
|
||||||
.setContent(resolveContent(meeting))
|
.setContent(buildCompletedContent(meeting))
|
||||||
.setNeedAck(false)
|
.setNeedAck(false)
|
||||||
.build();
|
.build();
|
||||||
int pushed = androidGatewayPushService.pushToUser(meeting.getTenantId(), meeting.getCreatorId(), message);
|
int pushed = androidGatewayPushService.pushToUser(meeting.getTenantId(), meeting.getCreatorId(), message);
|
||||||
|
|
@ -58,8 +83,18 @@ public class AndroidMeetingPushServiceImpl implements AndroidMeetingPushService
|
||||||
meetingId, meeting.getTenantId(), meeting.getCreatorId(), pushed);
|
meetingId, meeting.getTenantId(), meeting.getCreatorId(), pushed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String resolvePendingTitle(MeetingVO meeting) {
|
||||||
|
String title = meeting.getTitle();
|
||||||
|
if (title != null && !title.isBlank()) {
|
||||||
|
return "待开始会议: " + title.trim();
|
||||||
|
}
|
||||||
|
LocalDateTime meetingTime = meeting.getMeetingTime();
|
||||||
|
return meetingTime == null
|
||||||
|
? "待开始会议"
|
||||||
|
: "待开始会议: " + TITLE_TIME_FORMATTER.format(meetingTime);
|
||||||
|
}
|
||||||
|
|
||||||
private String resolveTitle(MeetingVO meeting) {
|
private String resolveCompletedTitle(MeetingVO meeting) {
|
||||||
String title = meeting.getTitle();
|
String title = meeting.getTitle();
|
||||||
if (title != null && !title.isBlank()) {
|
if (title != null && !title.isBlank()) {
|
||||||
return "会议已完成: " + title.trim();
|
return "会议已完成: " + title.trim();
|
||||||
|
|
@ -70,9 +105,20 @@ public class AndroidMeetingPushServiceImpl implements AndroidMeetingPushService
|
||||||
: "会议已完成: " + TITLE_TIME_FORMATTER.format(meetingTime);
|
: "会议已完成: " + TITLE_TIME_FORMATTER.format(meetingTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolveContent(MeetingVO meeting) {
|
private String buildPendingContent(MeetingVO meeting) {
|
||||||
Map<String,Object> result=new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
result.put("meetingId",meeting.getId());
|
result.put("meetingId", meeting.getId());
|
||||||
|
result.put("title", meeting.getTitle());
|
||||||
|
result.put("meetingTime", meeting.getMeetingTime());
|
||||||
|
result.put("sourceDeviceCode", meeting.getSourceDeviceCode());
|
||||||
|
result.put("sourceDeviceMode", meeting.getSourceDeviceMode());
|
||||||
|
result.put("status", meeting.getStatus());
|
||||||
|
return JSONUtil.toJsonStr(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildCompletedContent(MeetingVO meeting) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("meetingId", meeting.getId());
|
||||||
return JSONUtil.toJsonStr(result);
|
return JSONUtil.toJsonStr(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package com.imeeting.service.android.impl;
|
||||||
|
|
||||||
|
import com.imeeting.dto.android.AndroidPendingMeetingDraft;
|
||||||
|
import com.imeeting.service.android.AndroidPendingMeetingDraftService;
|
||||||
|
import com.imeeting.support.redis.AndroidPendingMeetingDraftCache;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AndroidPendingMeetingDraftServiceImpl implements AndroidPendingMeetingDraftService {
|
||||||
|
|
||||||
|
private final AndroidPendingMeetingDraftCache draftCache;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save(AndroidPendingMeetingDraft draft) {
|
||||||
|
draftCache.save(draft);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AndroidPendingMeetingDraft get(Long meetingId) {
|
||||||
|
return draftCache.get(meetingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clear(Long meetingId) {
|
||||||
|
draftCache.clear(meetingId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package com.imeeting.service.android.impl;
|
||||||
|
|
||||||
|
import com.imeeting.dto.android.AndroidPublicMeetingSessionState;
|
||||||
|
import com.imeeting.dto.android.AndroidPublicMeetingSessionVO;
|
||||||
|
import com.imeeting.service.android.AndroidPublicMeetingSessionService;
|
||||||
|
import com.imeeting.support.redis.AndroidPublicMeetingSessionCache;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AndroidPublicMeetingSessionServiceImpl implements AndroidPublicMeetingSessionService {
|
||||||
|
private final AndroidPublicMeetingSessionCache sessionCache;
|
||||||
|
|
||||||
|
@Value("${imeeting.public-device.session-ttl-minutes:30}")
|
||||||
|
private long sessionTtlMinutes;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AndroidPublicMeetingSessionVO create(String deviceId, String title) {
|
||||||
|
String sessionId = UUID.randomUUID().toString().replace("-", "");
|
||||||
|
LocalDateTime expireAt = LocalDateTime.now().plusMinutes(Math.max(sessionTtlMinutes, 1));
|
||||||
|
AndroidPublicMeetingSessionState state = new AndroidPublicMeetingSessionState();
|
||||||
|
state.setSessionId(sessionId);
|
||||||
|
state.setSessionToken(sessionId);
|
||||||
|
state.setDeviceId(deviceId);
|
||||||
|
state.setTitle(title);
|
||||||
|
state.setExpireAt(expireAt);
|
||||||
|
sessionCache.save(sessionId, state, Duration.ofMinutes(Math.max(sessionTtlMinutes, 1)));
|
||||||
|
AndroidPublicMeetingSessionVO vo = new AndroidPublicMeetingSessionVO();
|
||||||
|
vo.setSessionId(sessionId);
|
||||||
|
vo.setSessionToken(sessionId);
|
||||||
|
vo.setDeviceId(deviceId);
|
||||||
|
vo.setExpireAt(expireAt);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AndroidPublicMeetingSessionState require(String sessionId) {
|
||||||
|
AndroidPublicMeetingSessionState state = sessionCache.get(sessionId);
|
||||||
|
if (state == null) {
|
||||||
|
throw new RuntimeException("公有设备发会会话不存在或已过期");
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clear(String sessionId) {
|
||||||
|
sessionCache.clear(sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
package com.imeeting.service.android.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
|
import com.imeeting.common.MeetingConstants;
|
||||||
|
import com.imeeting.entity.biz.AndroidPushMessage;
|
||||||
|
import com.imeeting.grpc.push.PushMessage;
|
||||||
|
import com.imeeting.mapper.biz.AndroidPushMessageMapper;
|
||||||
|
import com.imeeting.service.android.AndroidPushMessageService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AndroidPushMessageServiceImpl implements AndroidPushMessageService {
|
||||||
|
private final AndroidPushMessageMapper androidPushMessageMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AndroidPushMessage saveMeetingPushMessage(Long tenantId,
|
||||||
|
Long meetingId,
|
||||||
|
String deviceCode,
|
||||||
|
PushMessage pushMessage,
|
||||||
|
long expireAfterMinutes) {
|
||||||
|
AndroidPushMessage entity = new AndroidPushMessage();
|
||||||
|
entity.setTenantId(tenantId);
|
||||||
|
entity.setMeetingId(meetingId);
|
||||||
|
entity.setDeviceCode(deviceCode);
|
||||||
|
entity.setMessageId(pushMessage.getMessageId());
|
||||||
|
entity.setMessageType(pushMessage.getType());
|
||||||
|
entity.setPayload(pushMessage.getContent());
|
||||||
|
entity.setNeedAck(pushMessage.getNeedAck() ? 1 : 0);
|
||||||
|
entity.setAcked(0);
|
||||||
|
entity.setPushStatus(MeetingConstants.DEVICE_DELIVERY_PENDING);
|
||||||
|
entity.setPushCount(0);
|
||||||
|
entity.setExpireAt(LocalDateTime.now().plusMinutes(Math.max(expireAfterMinutes, 1)));
|
||||||
|
androidPushMessageMapper.insert(entity);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean ack(String messageId, String deviceCode) {
|
||||||
|
if (messageId == null || messageId.isBlank() || deviceCode == null || deviceCode.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int updated = androidPushMessageMapper.update(null, new LambdaUpdateWrapper<AndroidPushMessage>()
|
||||||
|
.eq(AndroidPushMessage::getMessageId, messageId)
|
||||||
|
.eq(AndroidPushMessage::getDeviceCode, deviceCode)
|
||||||
|
.eq(AndroidPushMessage::getIsDeleted, 0)
|
||||||
|
.set(AndroidPushMessage::getAcked, 1)
|
||||||
|
.set(AndroidPushMessage::getPushStatus, MeetingConstants.DEVICE_DELIVERY_ACKED)
|
||||||
|
.set(AndroidPushMessage::getAckAt, LocalDateTime.now()));
|
||||||
|
return updated > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AndroidPushMessage> listPendingMeetingPushMessages() {
|
||||||
|
return androidPushMessageMapper.selectList(new LambdaQueryWrapper<AndroidPushMessage>()
|
||||||
|
.eq(AndroidPushMessage::getNeedAck, 1)
|
||||||
|
.eq(AndroidPushMessage::getAcked, 0)
|
||||||
|
.eq(AndroidPushMessage::getPushStatus, MeetingConstants.DEVICE_DELIVERY_PENDING)
|
||||||
|
.eq(AndroidPushMessage::getIsDeleted, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void markPushed(Long id) {
|
||||||
|
androidPushMessageMapper.update(null, new LambdaUpdateWrapper<AndroidPushMessage>()
|
||||||
|
.eq(AndroidPushMessage::getId, id)
|
||||||
|
.eq(AndroidPushMessage::getIsDeleted, 0)
|
||||||
|
.setSql("push_count = COALESCE(push_count, 0) + 1")
|
||||||
|
.set(AndroidPushMessage::getLastPushAt, LocalDateTime.now()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void markExpired(Long id) {
|
||||||
|
androidPushMessageMapper.update(null, new LambdaUpdateWrapper<AndroidPushMessage>()
|
||||||
|
.eq(AndroidPushMessage::getId, id)
|
||||||
|
.eq(AndroidPushMessage::getIsDeleted, 0)
|
||||||
|
.set(AndroidPushMessage::getPushStatus, MeetingConstants.DEVICE_DELIVERY_EXPIRED));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void markCancelledByMeeting(Long meetingId) {
|
||||||
|
if (meetingId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
androidPushMessageMapper.update(null, new LambdaUpdateWrapper<AndroidPushMessage>()
|
||||||
|
.eq(AndroidPushMessage::getMeetingId, meetingId)
|
||||||
|
.eq(AndroidPushMessage::getAcked, 0)
|
||||||
|
.eq(AndroidPushMessage::getIsDeleted, 0)
|
||||||
|
.set(AndroidPushMessage::getPushStatus, MeetingConstants.DEVICE_DELIVERY_CANCELLED));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package com.imeeting.service.android.legacy;
|
||||||
|
|
||||||
import com.imeeting.dto.android.legacy.LegacyMeetingCreateRequest;
|
import com.imeeting.dto.android.legacy.LegacyMeetingCreateRequest;
|
||||||
import com.imeeting.dto.android.legacy.LegacyUploadAudioResponse;
|
import com.imeeting.dto.android.legacy.LegacyUploadAudioResponse;
|
||||||
|
import com.imeeting.dto.android.AndroidAuthContext;
|
||||||
import com.imeeting.dto.biz.MeetingVO;
|
import com.imeeting.dto.biz.MeetingVO;
|
||||||
import com.unisbase.security.LoginUser;
|
import com.unisbase.security.LoginUser;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
@ -9,12 +10,18 @@ import org.springframework.web.multipart.MultipartFile;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public interface LegacyMeetingAdapterService {
|
public interface LegacyMeetingAdapterService {
|
||||||
MeetingVO createMeeting(LegacyMeetingCreateRequest request, LoginUser loginUser);
|
MeetingVO createMeeting(LegacyMeetingCreateRequest request, AndroidAuthContext authContext, LoginUser loginUser);
|
||||||
|
|
||||||
LegacyUploadAudioResponse uploadAndTriggerOfflineProcess(Long meetingId,
|
LegacyUploadAudioResponse uploadAndTriggerOfflineProcess(Long meetingId,
|
||||||
Long promptId,
|
Long promptId,
|
||||||
String modelCode,
|
String modelCode,
|
||||||
boolean forceReplace,
|
boolean forceReplace,
|
||||||
MultipartFile audioFile,
|
MultipartFile audioFile,
|
||||||
|
AndroidAuthContext authContext,
|
||||||
LoginUser loginUser) throws IOException;
|
LoginUser loginUser) throws IOException;
|
||||||
|
|
||||||
|
LegacyUploadAudioResponse uploadAndTriggerOfflineProcessForPublicDevice(Long meetingId,
|
||||||
|
boolean forceReplace,
|
||||||
|
MultipartFile audioFile,
|
||||||
|
AndroidAuthContext authContext) throws IOException;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,12 @@ package com.imeeting.service.android.legacy.impl;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.imeeting.common.MeetingConstants;
|
import com.imeeting.common.MeetingConstants;
|
||||||
|
import com.imeeting.dto.android.AndroidAuthContext;
|
||||||
|
import com.imeeting.dto.android.AndroidPendingMeetingDraft;
|
||||||
import com.imeeting.dto.android.legacy.LegacyMeetingCreateRequest;
|
import com.imeeting.dto.android.legacy.LegacyMeetingCreateRequest;
|
||||||
import com.imeeting.dto.android.legacy.LegacyUploadAudioResponse;
|
import com.imeeting.dto.android.legacy.LegacyUploadAudioResponse;
|
||||||
import com.imeeting.dto.biz.MeetingVO;
|
import com.imeeting.dto.biz.MeetingVO;
|
||||||
|
import com.imeeting.dto.biz.PublicDeviceMeetingCreateCommand;
|
||||||
import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile;
|
import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile;
|
||||||
import com.imeeting.entity.biz.AiTask;
|
import com.imeeting.entity.biz.AiTask;
|
||||||
import com.imeeting.entity.biz.LlmModel;
|
import com.imeeting.entity.biz.LlmModel;
|
||||||
|
|
@ -14,6 +17,7 @@ import com.imeeting.entity.biz.PromptTemplate;
|
||||||
import com.imeeting.mapper.biz.LlmModelMapper;
|
import com.imeeting.mapper.biz.LlmModelMapper;
|
||||||
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
||||||
import com.imeeting.service.android.legacy.LegacyMeetingAdapterService;
|
import com.imeeting.service.android.legacy.LegacyMeetingAdapterService;
|
||||||
|
import com.imeeting.service.android.AndroidPendingMeetingDraftService;
|
||||||
import com.imeeting.service.biz.AiTaskService;
|
import com.imeeting.service.biz.AiTaskService;
|
||||||
import com.imeeting.service.biz.MeetingAccessService;
|
import com.imeeting.service.biz.MeetingAccessService;
|
||||||
import com.imeeting.service.biz.MeetingRuntimeProfileResolver;
|
import com.imeeting.service.biz.MeetingRuntimeProfileResolver;
|
||||||
|
|
@ -56,10 +60,11 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
|
||||||
private final MeetingTranscriptMapper transcriptMapper;
|
private final MeetingTranscriptMapper transcriptMapper;
|
||||||
private final LlmModelMapper llmModelMapper;
|
private final LlmModelMapper llmModelMapper;
|
||||||
private final MeetingAudioUploadSupport meetingAudioUploadSupport;
|
private final MeetingAudioUploadSupport meetingAudioUploadSupport;
|
||||||
|
private final AndroidPendingMeetingDraftService androidPendingMeetingDraftService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public MeetingVO createMeeting(LegacyMeetingCreateRequest request, LoginUser loginUser) {
|
public MeetingVO createMeeting(LegacyMeetingCreateRequest request, AndroidAuthContext authContext, LoginUser loginUser) {
|
||||||
if (request == null || request.getTitle() == null || request.getTitle().isBlank()) {
|
if (request == null || request.getTitle() == null || request.getTitle().isBlank()) {
|
||||||
throw new RuntimeException("会议标题不能为空");
|
throw new RuntimeException("会议标题不能为空");
|
||||||
}
|
}
|
||||||
|
|
@ -82,7 +87,9 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
|
||||||
loginUser.getUserId(),
|
loginUser.getUserId(),
|
||||||
resolveCreatorName(loginUser),
|
resolveCreatorName(loginUser),
|
||||||
MeetingConstants.SUMMARY_DETAIL_STANDARD,
|
MeetingConstants.SUMMARY_DETAIL_STANDARD,
|
||||||
0
|
0,
|
||||||
|
authContext == null ? null : authContext.getDeviceId(),
|
||||||
|
MeetingConstants.DEVICE_MODE_PRIVATE
|
||||||
);
|
);
|
||||||
meetingService.save(meeting);
|
meetingService.save(meeting);
|
||||||
|
|
||||||
|
|
@ -98,6 +105,7 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
|
||||||
String modelCode,
|
String modelCode,
|
||||||
boolean forceReplace,
|
boolean forceReplace,
|
||||||
MultipartFile audioFile,
|
MultipartFile audioFile,
|
||||||
|
AndroidAuthContext authContext,
|
||||||
LoginUser loginUser) throws IOException {
|
LoginUser loginUser) throws IOException {
|
||||||
if (meetingId == null) {
|
if (meetingId == null) {
|
||||||
throw new RuntimeException("meeting_id 不能为空");
|
throw new RuntimeException("meeting_id 不能为空");
|
||||||
|
|
@ -108,6 +116,7 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
|
||||||
|
|
||||||
Meeting meeting = meetingAccessService.requireMeeting(meetingId);
|
Meeting meeting = meetingAccessService.requireMeeting(meetingId);
|
||||||
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
||||||
|
assertDeviceOwnsMeeting(meeting, authContext);
|
||||||
|
|
||||||
if (!forceReplace && meeting.getAudioUrl() != null && !meeting.getAudioUrl().isBlank()) {
|
if (!forceReplace && meeting.getAudioUrl() != null && !meeting.getAudioUrl().isBlank()) {
|
||||||
throw new RuntimeException("当前会议已存在音频,如需替换请设置 force_replace=true");
|
throw new RuntimeException("当前会议已存在音频,如需替换请设置 force_replace=true");
|
||||||
|
|
@ -158,6 +167,69 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
|
||||||
return new LegacyUploadAudioResponse(meetingId, relocatedUrl);
|
return new LegacyUploadAudioResponse(meetingId, relocatedUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public LegacyUploadAudioResponse uploadAndTriggerOfflineProcessForPublicDevice(Long meetingId,
|
||||||
|
boolean forceReplace,
|
||||||
|
MultipartFile audioFile,
|
||||||
|
AndroidAuthContext authContext) throws IOException {
|
||||||
|
if (meetingId == null) {
|
||||||
|
throw new RuntimeException("meeting_id 不能为空");
|
||||||
|
}
|
||||||
|
if (audioFile == null || audioFile.isEmpty()) {
|
||||||
|
throw new RuntimeException("audio_file 不能为空");
|
||||||
|
}
|
||||||
|
Meeting meeting = meetingAccessService.requireMeeting(meetingId);
|
||||||
|
assertDeviceOwnsMeeting(meeting, authContext);
|
||||||
|
if (!MeetingConstants.DEVICE_MODE_PUBLIC.equals(meeting.getSourceDeviceMode())) {
|
||||||
|
throw new RuntimeException("当前会议不是公有设备会议");
|
||||||
|
}
|
||||||
|
if (!forceReplace && meeting.getAudioUrl() != null && !meeting.getAudioUrl().isBlank()) {
|
||||||
|
throw new RuntimeException("当前会议已存在音频,如需替换请设置 force_replace=true");
|
||||||
|
}
|
||||||
|
AndroidPendingMeetingDraft draft = androidPendingMeetingDraftService.get(meetingId);
|
||||||
|
if (draft == null || draft.getCommand() == null) {
|
||||||
|
throw new RuntimeException("未找到公有设备会议配置草稿");
|
||||||
|
}
|
||||||
|
long transcriptCount = transcriptMapper.selectCount(new LambdaQueryWrapper<MeetingTranscript>()
|
||||||
|
.eq(MeetingTranscript::getMeetingId, meetingId));
|
||||||
|
if (transcriptCount > 0) {
|
||||||
|
throw new RuntimeException("当前会议已存在转录内容,不支持替换已生成的转录");
|
||||||
|
}
|
||||||
|
PublicDeviceMeetingCreateCommand command = draft.getCommand();
|
||||||
|
RealtimeMeetingRuntimeProfile profile = runtimeProfileResolver.resolve(
|
||||||
|
meeting.getTenantId(),
|
||||||
|
command.getAsrModelId(),
|
||||||
|
command.getSummaryModelId(),
|
||||||
|
command.getPromptId(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
command.getUseSpkId(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
command.getEnableTextRefine(),
|
||||||
|
null,
|
||||||
|
command.getHotWordGroupId(),
|
||||||
|
command.getHotWords()
|
||||||
|
);
|
||||||
|
String stagingUrl = storeStagingAudio(audioFile);
|
||||||
|
String relocatedUrl = meetingDomainSupport.relocateAudioUrl(meetingId, stagingUrl);
|
||||||
|
meeting.setAudioUrl(relocatedUrl);
|
||||||
|
meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_SUCCESS);
|
||||||
|
meeting.setAudioSaveMessage(null);
|
||||||
|
meeting.setStatus(1);
|
||||||
|
meetingService.updateById(meeting);
|
||||||
|
|
||||||
|
resetOrCreateAsrTask(meetingId, profile);
|
||||||
|
Long summaryModelId = profile.getResolvedSummaryModelId();
|
||||||
|
Long chapterModelId = command.getChapterModelId() != null ? command.getChapterModelId() : summaryModelId;
|
||||||
|
resetOrCreateChapterTask(meetingId, summaryModelId, chapterModelId, profile.getResolvedPromptId(), command.getUserPrompt(), command.getSummaryDetailLevel());
|
||||||
|
resetOrCreateSummaryTask(meetingId, summaryModelId, chapterModelId, profile.getResolvedPromptId(), command.getUserPrompt(), command.getSummaryDetailLevel());
|
||||||
|
dispatchTasksAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId());
|
||||||
|
androidPendingMeetingDraftService.clear(meetingId);
|
||||||
|
return new LegacyUploadAudioResponse(meetingId, relocatedUrl);
|
||||||
|
}
|
||||||
|
|
||||||
private String joinIds(List<Long> ids) {
|
private String joinIds(List<Long> ids) {
|
||||||
if (ids == null || ids.isEmpty()) {
|
if (ids == null || ids.isEmpty()) {
|
||||||
return "";
|
return "";
|
||||||
|
|
@ -249,22 +321,65 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetOrCreateSummaryTask(Long meetingId, RealtimeMeetingRuntimeProfile profile) {
|
private void resetOrCreateSummaryTask(Long meetingId, RealtimeMeetingRuntimeProfile profile) {
|
||||||
AiTask task = findLatestTask(meetingId, "SUMMARY");
|
resetOrCreateSummaryTask(meetingId, profile, null, null);
|
||||||
Map<String, Object> taskConfig = meetingSummaryPromptAssembler.buildTaskConfig(
|
}
|
||||||
|
|
||||||
|
private void resetOrCreateSummaryTask(Long meetingId, RealtimeMeetingRuntimeProfile profile, String userPrompt, String summaryDetailLevel) {
|
||||||
|
resetOrCreateSummaryTask(
|
||||||
|
meetingId,
|
||||||
|
profile.getResolvedSummaryModelId(),
|
||||||
profile.getResolvedSummaryModelId(),
|
profile.getResolvedSummaryModelId(),
|
||||||
profile.getResolvedPromptId(),
|
profile.getResolvedPromptId(),
|
||||||
null
|
userPrompt,
|
||||||
|
summaryDetailLevel
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetOrCreateSummaryTask(Long meetingId,
|
||||||
|
Long summaryModelId,
|
||||||
|
Long chapterModelId,
|
||||||
|
Long promptId,
|
||||||
|
String userPrompt,
|
||||||
|
String summaryDetailLevel) {
|
||||||
|
AiTask task = findLatestTask(meetingId, "SUMMARY");
|
||||||
|
Map<String, Object> taskConfig = meetingSummaryPromptAssembler.buildTaskConfig(
|
||||||
|
summaryModelId,
|
||||||
|
chapterModelId,
|
||||||
|
promptId,
|
||||||
|
userPrompt,
|
||||||
|
summaryDetailLevel
|
||||||
);
|
);
|
||||||
resetOrCreateTask(task, meetingId, "SUMMARY", taskConfig);
|
resetOrCreateTask(task, meetingId, "SUMMARY", taskConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetOrCreateChapterTask(Long meetingId, RealtimeMeetingRuntimeProfile profile) {
|
private void resetOrCreateChapterTask(Long meetingId, RealtimeMeetingRuntimeProfile profile) {
|
||||||
AiTask task = findLatestTask(meetingId, "CHAPTER");
|
resetOrCreateChapterTask(meetingId, profile, null, null);
|
||||||
Map<String, Object> taskConfig = meetingSummaryPromptAssembler.buildTaskConfig(
|
}
|
||||||
|
|
||||||
|
private void resetOrCreateChapterTask(Long meetingId, RealtimeMeetingRuntimeProfile profile, String userPrompt, String summaryDetailLevel) {
|
||||||
|
resetOrCreateChapterTask(
|
||||||
|
meetingId,
|
||||||
profile.getResolvedSummaryModelId(),
|
profile.getResolvedSummaryModelId(),
|
||||||
profile.getResolvedSummaryModelId(),
|
profile.getResolvedSummaryModelId(),
|
||||||
profile.getResolvedPromptId(),
|
profile.getResolvedPromptId(),
|
||||||
null
|
userPrompt,
|
||||||
|
summaryDetailLevel
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetOrCreateChapterTask(Long meetingId,
|
||||||
|
Long summaryModelId,
|
||||||
|
Long chapterModelId,
|
||||||
|
Long promptId,
|
||||||
|
String userPrompt,
|
||||||
|
String summaryDetailLevel) {
|
||||||
|
AiTask task = findLatestTask(meetingId, "CHAPTER");
|
||||||
|
Map<String, Object> taskConfig = meetingSummaryPromptAssembler.buildTaskConfig(
|
||||||
|
summaryModelId,
|
||||||
|
chapterModelId,
|
||||||
|
promptId,
|
||||||
|
userPrompt,
|
||||||
|
summaryDetailLevel
|
||||||
);
|
);
|
||||||
resetOrCreateTask(task, meetingId, "CHAPTER", taskConfig);
|
resetOrCreateTask(task, meetingId, "CHAPTER", taskConfig);
|
||||||
}
|
}
|
||||||
|
|
@ -315,4 +430,14 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
|
||||||
private String resolveCreatorName(LoginUser loginUser) {
|
private String resolveCreatorName(LoginUser loginUser) {
|
||||||
return loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername();
|
return loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void assertDeviceOwnsMeeting(Meeting meeting, AndroidAuthContext authContext) {
|
||||||
|
if (meeting == null || authContext == null || authContext.getDeviceId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (meeting.getSourceDeviceCode() != null && !meeting.getSourceDeviceCode().isBlank()
|
||||||
|
&& !meeting.getSourceDeviceCode().equals(authContext.getDeviceId())) {
|
||||||
|
throw new RuntimeException("当前会议不属于该设备");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import com.imeeting.dto.biz.MeetingSummaryFinalizeDTO;
|
||||||
import com.imeeting.dto.biz.MeetingTranscriptChapterImportDTO;
|
import com.imeeting.dto.biz.MeetingTranscriptChapterImportDTO;
|
||||||
import com.imeeting.dto.biz.MeetingTranscriptChapterImportResultVO;
|
import com.imeeting.dto.biz.MeetingTranscriptChapterImportResultVO;
|
||||||
import com.imeeting.dto.biz.MeetingVO;
|
import com.imeeting.dto.biz.MeetingVO;
|
||||||
|
import com.imeeting.dto.biz.PublicDeviceMeetingCreateCommand;
|
||||||
import com.imeeting.dto.biz.RealtimeTranscriptItemDTO;
|
import com.imeeting.dto.biz.RealtimeTranscriptItemDTO;
|
||||||
import com.imeeting.dto.biz.UpdateMeetingBasicCommand;
|
import com.imeeting.dto.biz.UpdateMeetingBasicCommand;
|
||||||
import com.imeeting.dto.biz.UpdateMeetingTranscriptCommand;
|
import com.imeeting.dto.biz.UpdateMeetingTranscriptCommand;
|
||||||
|
|
@ -19,6 +20,12 @@ public interface MeetingCommandService {
|
||||||
|
|
||||||
MeetingVO createRealtimeMeeting(CreateRealtimeMeetingCommand command, Long tenantId, Long creatorId, String creatorName, String meetingSource);
|
MeetingVO createRealtimeMeeting(CreateRealtimeMeetingCommand command, Long tenantId, Long creatorId, String creatorName, String meetingSource);
|
||||||
|
|
||||||
|
MeetingVO createPublicDeviceMeeting(PublicDeviceMeetingCreateCommand command,
|
||||||
|
Long tenantId,
|
||||||
|
Long creatorId,
|
||||||
|
String creatorName,
|
||||||
|
String deviceCode);
|
||||||
|
|
||||||
void deleteMeeting(Long id);
|
void deleteMeeting(Long id);
|
||||||
|
|
||||||
void appendRealtimeTranscripts(Long meetingId, List<RealtimeTranscriptItemDTO> items);
|
void appendRealtimeTranscripts(Long meetingId, List<RealtimeTranscriptItemDTO> items);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import com.imeeting.common.MeetingProgressStage;
|
import com.imeeting.common.MeetingProgressStage;
|
||||||
import com.imeeting.common.RedisKeys;
|
|
||||||
import com.imeeting.dto.biz.AiModelVO;
|
import com.imeeting.dto.biz.AiModelVO;
|
||||||
import com.imeeting.dto.biz.MeetingSummarySource;
|
import com.imeeting.dto.biz.MeetingSummarySource;
|
||||||
import com.imeeting.dto.biz.MeetingTranscriptSourceVO;
|
import com.imeeting.dto.biz.MeetingTranscriptSourceVO;
|
||||||
|
|
@ -28,8 +27,8 @@ import com.imeeting.service.biz.MeetingSummaryFileService;
|
||||||
import com.imeeting.service.biz.MeetingTranscriptChapterService;
|
import com.imeeting.service.biz.MeetingTranscriptChapterService;
|
||||||
import com.imeeting.service.biz.MeetingTranscriptFileService;
|
import com.imeeting.service.biz.MeetingTranscriptFileService;
|
||||||
|
|
||||||
import com.imeeting.support.RedisValueSupport;
|
import com.imeeting.support.TaskSecurityContextRunner;
|
||||||
|
import com.imeeting.support.redis.MeetingLockCache;
|
||||||
import com.unisbase.entity.SysUser;
|
import com.unisbase.entity.SysUser;
|
||||||
import com.unisbase.mapper.SysUserMapper;
|
import com.unisbase.mapper.SysUserMapper;
|
||||||
import com.unisbase.service.SysParamService;
|
import com.unisbase.service.SysParamService;
|
||||||
|
|
@ -38,7 +37,6 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
|
||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
@ -59,7 +57,6 @@ import java.time.Duration;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
|
@ -72,7 +69,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final SysUserMapper sysUserMapper;
|
private final SysUserMapper sysUserMapper;
|
||||||
private final HotWordService hotWordService;
|
private final HotWordService hotWordService;
|
||||||
private final RedisValueSupport redisValueSupport;
|
private final MeetingLockCache meetingLockCache;
|
||||||
private final MeetingProgressService meetingProgressService;
|
private final MeetingProgressService meetingProgressService;
|
||||||
private final MeetingSummaryFileService meetingSummaryFileService;
|
private final MeetingSummaryFileService meetingSummaryFileService;
|
||||||
private final MeetingTranscriptFileService meetingTranscriptFileService;
|
private final MeetingTranscriptFileService meetingTranscriptFileService;
|
||||||
|
|
@ -117,7 +114,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
ObjectMapper objectMapper,
|
ObjectMapper objectMapper,
|
||||||
SysUserMapper sysUserMapper,
|
SysUserMapper sysUserMapper,
|
||||||
HotWordService hotWordService,
|
HotWordService hotWordService,
|
||||||
RedisValueSupport redisValueSupport,
|
MeetingLockCache meetingLockCache,
|
||||||
MeetingProgressService meetingProgressService,
|
MeetingProgressService meetingProgressService,
|
||||||
MeetingSummaryFileService meetingSummaryFileService,
|
MeetingSummaryFileService meetingSummaryFileService,
|
||||||
MeetingTranscriptFileService meetingTranscriptFileService,
|
MeetingTranscriptFileService meetingTranscriptFileService,
|
||||||
|
|
@ -133,7 +130,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
this.sysUserMapper = sysUserMapper;
|
this.sysUserMapper = sysUserMapper;
|
||||||
this.hotWordService = hotWordService;
|
this.hotWordService = hotWordService;
|
||||||
this.redisValueSupport = redisValueSupport;
|
this.meetingLockCache = meetingLockCache;
|
||||||
this.meetingProgressService = meetingProgressService;
|
this.meetingProgressService = meetingProgressService;
|
||||||
this.meetingSummaryFileService = meetingSummaryFileService;
|
this.meetingSummaryFileService = meetingSummaryFileService;
|
||||||
this.meetingTranscriptFileService = meetingTranscriptFileService;
|
this.meetingTranscriptFileService = meetingTranscriptFileService;
|
||||||
|
|
@ -145,41 +142,6 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
this.androidMeetingPushService = androidMeetingPushService;
|
this.androidMeetingPushService = androidMeetingPushService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AiTaskServiceImpl(MeetingMapper meetingMapper,
|
|
||||||
MeetingTranscriptMapper transcriptMapper,
|
|
||||||
AiModelService aiModelService,
|
|
||||||
ObjectMapper objectMapper,
|
|
||||||
SysUserMapper sysUserMapper,
|
|
||||||
HotWordService hotWordService,
|
|
||||||
StringRedisTemplate redisTemplate,
|
|
||||||
MeetingSummaryFileService meetingSummaryFileService,
|
|
||||||
MeetingTranscriptFileService meetingTranscriptFileService,
|
|
||||||
|
|
||||||
MeetingTranscriptChapterService meetingTranscriptChapterService,
|
|
||||||
MeetingSummaryPromptAssembler meetingSummaryPromptAssembler,
|
|
||||||
TaskSecurityContextRunner taskSecurityContextRunner,
|
|
||||||
MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger,
|
|
||||||
AndroidMeetingPushService androidMeetingPushService) {
|
|
||||||
this(
|
|
||||||
meetingMapper,
|
|
||||||
transcriptMapper,
|
|
||||||
aiModelService,
|
|
||||||
objectMapper,
|
|
||||||
sysUserMapper,
|
|
||||||
hotWordService,
|
|
||||||
new RedisValueSupport(redisTemplate, objectMapper),
|
|
||||||
new RedisOnlyMeetingProgressServiceAdapter(redisTemplate, objectMapper),
|
|
||||||
meetingSummaryFileService,
|
|
||||||
meetingTranscriptFileService,
|
|
||||||
meetingTranscriptChapterService,
|
|
||||||
meetingSummaryPromptAssembler,
|
|
||||||
taskSecurityContextRunner,
|
|
||||||
meetingExternalSummaryWebhookTrigger,
|
|
||||||
null,
|
|
||||||
androidMeetingPushService
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Async("asrDispatchExecutor")
|
@Async("asrDispatchExecutor")
|
||||||
public void dispatchTasks(Long meetingId, Long tenantId, Long userId) {
|
public void dispatchTasks(Long meetingId, Long tenantId, Long userId) {
|
||||||
|
|
@ -216,9 +178,8 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
|
|
||||||
|
|
||||||
private void doDispatchTasks(Long meetingId) {
|
private void doDispatchTasks(Long meetingId) {
|
||||||
String lockKey = RedisKeys.meetingPollingLockKey(meetingId);
|
boolean acquired = meetingLockCache.tryAcquirePollingLock(meetingId, Duration.ofMinutes(30));
|
||||||
Boolean acquired = redisValueSupport.setIfAbsent(lockKey, "locked", 30, TimeUnit.MINUTES);
|
if (!acquired) {
|
||||||
if (Boolean.FALSE.equals(acquired)) {
|
|
||||||
log.warn("Meeting {} is already being processed", meetingId);
|
log.warn("Meeting {} is already being processed", meetingId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -293,7 +254,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
updateMeetingStatus(meetingId, 4);
|
updateMeetingStatus(meetingId, 4);
|
||||||
updateProgress(meetingId, -1, "分析失败: " + e.getMessage(), 0);
|
updateProgress(meetingId, -1, "分析失败: " + e.getMessage(), 0);
|
||||||
} finally {
|
} finally {
|
||||||
redisValueSupport.delete(lockKey);
|
meetingLockCache.releasePollingLock(meetingId);
|
||||||
scheduleQueuedAsrTasks();
|
scheduleQueuedAsrTasks();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -390,9 +351,8 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
if (queuedCount <= 0) {
|
if (queuedCount <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String scheduleLockKey = RedisKeys.meetingAsrScheduleLockKey();
|
boolean acquired = meetingLockCache.tryAcquireAsrScheduleLock(Duration.ofSeconds(30));
|
||||||
Boolean acquired = redisValueSupport.setIfAbsent(scheduleLockKey, "locked", 30, TimeUnit.SECONDS);
|
if (!acquired) {
|
||||||
if (Boolean.FALSE.equals(acquired)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|
@ -425,7 +385,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
self.dispatchTasks(queuedMeeting.getId(), queuedMeeting.getTenantId(), queuedMeeting.getCreatorId());
|
self.dispatchTasks(queuedMeeting.getId(), queuedMeeting.getTenantId(), queuedMeeting.getCreatorId());
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
redisValueSupport.delete(scheduleLockKey);
|
meetingLockCache.releaseAsrScheduleLock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1070,9 +1030,8 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
triggerExternalSummaryWebhook(meeting, sumTask, chapterTask, "AUTO_AFTER_TRANSCRIPT_READY", false);
|
triggerExternalSummaryWebhook(meeting, sumTask, chapterTask, "AUTO_AFTER_TRANSCRIPT_READY", false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String summaryLockKey = RedisKeys.meetingSummaryLockKey(meeting.getId());
|
boolean acquired = meetingLockCache.tryAcquireSummaryLock(meeting.getId(), Duration.ofMinutes(30));
|
||||||
Boolean acquired = redisValueSupport.setIfAbsent(summaryLockKey, "locked", 30, TimeUnit.MINUTES);
|
if (!acquired) {
|
||||||
if (Boolean.FALSE.equals(acquired)) {
|
|
||||||
log.warn("Meeting {} summary is already being processed", meeting.getId());
|
log.warn("Meeting {} summary is already being processed", meeting.getId());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1086,7 +1045,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
processSummaryTask(meeting, summarySource, sumTask);
|
processSummaryTask(meeting, summarySource, sumTask);
|
||||||
reconcileMeetingStatus(meeting.getId());
|
reconcileMeetingStatus(meeting.getId());
|
||||||
} finally {
|
} finally {
|
||||||
redisValueSupport.delete(summaryLockKey);
|
meetingLockCache.releaseSummaryLock(meeting.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.imeeting.common.MeetingConstants;
|
import com.imeeting.common.MeetingConstants;
|
||||||
import com.imeeting.common.RedisKeys;
|
import com.imeeting.common.RedisKeys;
|
||||||
|
import com.imeeting.dto.android.AndroidPendingMeetingDraft;
|
||||||
import com.imeeting.dto.biz.CreateMeetingCommand;
|
import com.imeeting.dto.biz.CreateMeetingCommand;
|
||||||
import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
|
import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
|
||||||
import com.imeeting.dto.biz.MeetingExternalWorkflowFailureDTO;
|
import com.imeeting.dto.biz.MeetingExternalWorkflowFailureDTO;
|
||||||
|
|
@ -14,6 +15,7 @@ import com.imeeting.dto.biz.MeetingTranscriptChapterImportDTO;
|
||||||
import com.imeeting.dto.biz.MeetingTranscriptChapterImportResultVO;
|
import com.imeeting.dto.biz.MeetingTranscriptChapterImportResultVO;
|
||||||
import com.imeeting.dto.biz.MeetingTranscriptSourceVO;
|
import com.imeeting.dto.biz.MeetingTranscriptSourceVO;
|
||||||
import com.imeeting.dto.biz.MeetingVO;
|
import com.imeeting.dto.biz.MeetingVO;
|
||||||
|
import com.imeeting.dto.biz.PublicDeviceMeetingCreateCommand;
|
||||||
import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile;
|
import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile;
|
||||||
import com.imeeting.dto.biz.RealtimeMeetingResumeConfig;
|
import com.imeeting.dto.biz.RealtimeMeetingResumeConfig;
|
||||||
import com.imeeting.dto.biz.RealtimeMeetingSessionStatusVO;
|
import com.imeeting.dto.biz.RealtimeMeetingSessionStatusVO;
|
||||||
|
|
@ -26,6 +28,8 @@ import com.imeeting.entity.biz.Meeting;
|
||||||
import com.imeeting.entity.biz.MeetingTranscript;
|
import com.imeeting.entity.biz.MeetingTranscript;
|
||||||
import com.imeeting.entity.biz.MeetingTranscriptChapterVersion;
|
import com.imeeting.entity.biz.MeetingTranscriptChapterVersion;
|
||||||
import com.imeeting.service.android.AndroidMeetingPushService;
|
import com.imeeting.service.android.AndroidMeetingPushService;
|
||||||
|
import com.imeeting.service.android.AndroidPendingMeetingDraftService;
|
||||||
|
import com.imeeting.service.android.AndroidPushMessageService;
|
||||||
import com.imeeting.service.biz.AiTaskService;
|
import com.imeeting.service.biz.AiTaskService;
|
||||||
import com.imeeting.service.biz.HotWordService;
|
import com.imeeting.service.biz.HotWordService;
|
||||||
import com.imeeting.service.biz.MeetingCommandService;
|
import com.imeeting.service.biz.MeetingCommandService;
|
||||||
|
|
@ -37,12 +41,12 @@ import com.imeeting.service.biz.MeetingTranscriptChapterService;
|
||||||
import com.imeeting.service.biz.MeetingTranscriptFileService;
|
import com.imeeting.service.biz.MeetingTranscriptFileService;
|
||||||
import com.imeeting.service.biz.MeetingTranscriptRevisionService;
|
import com.imeeting.service.biz.MeetingTranscriptRevisionService;
|
||||||
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
||||||
import com.imeeting.service.biz.impl.RedisOnlyMeetingProgressServiceAdapter;
|
|
||||||
import com.imeeting.service.realtime.RealtimeMeetingAudioStorageService;
|
import com.imeeting.service.realtime.RealtimeMeetingAudioStorageService;
|
||||||
|
import com.imeeting.support.redis.MeetingAsrPermitCache;
|
||||||
|
import com.imeeting.support.redis.MeetingLockCache;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.transaction.support.TransactionSynchronization;
|
import org.springframework.transaction.support.TransactionSynchronization;
|
||||||
|
|
@ -77,7 +81,10 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger;
|
private final MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger;
|
||||||
private final AndroidMeetingPushService androidMeetingPushService;
|
private final AndroidMeetingPushService androidMeetingPushService;
|
||||||
private StringRedisTemplate compatibilityRedisTemplate;
|
private final AndroidPushMessageService androidPushMessageService;
|
||||||
|
private final AndroidPendingMeetingDraftService androidPendingMeetingDraftService;
|
||||||
|
private final MeetingLockCache meetingLockCache;
|
||||||
|
private final MeetingAsrPermitCache meetingAsrPermitCache;
|
||||||
|
|
||||||
@Value("${imeeting.summary-orchestration.mode:INTERNAL_BUILTIN}")
|
@Value("${imeeting.summary-orchestration.mode:INTERNAL_BUILTIN}")
|
||||||
private String summaryOrchestrationMode;
|
private String summaryOrchestrationMode;
|
||||||
|
|
@ -98,7 +105,11 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
MeetingProgressService meetingProgressService,
|
MeetingProgressService meetingProgressService,
|
||||||
ObjectMapper objectMapper,
|
ObjectMapper objectMapper,
|
||||||
MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger,
|
MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger,
|
||||||
AndroidMeetingPushService androidMeetingPushService) {
|
AndroidMeetingPushService androidMeetingPushService,
|
||||||
|
AndroidPushMessageService androidPushMessageService,
|
||||||
|
AndroidPendingMeetingDraftService androidPendingMeetingDraftService,
|
||||||
|
MeetingLockCache meetingLockCache,
|
||||||
|
MeetingAsrPermitCache meetingAsrPermitCache) {
|
||||||
this.meetingService = meetingService;
|
this.meetingService = meetingService;
|
||||||
this.aiTaskService = aiTaskService;
|
this.aiTaskService = aiTaskService;
|
||||||
this.hotWordService = hotWordService;
|
this.hotWordService = hotWordService;
|
||||||
|
|
@ -115,43 +126,10 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
this.meetingExternalSummaryWebhookTrigger = meetingExternalSummaryWebhookTrigger;
|
this.meetingExternalSummaryWebhookTrigger = meetingExternalSummaryWebhookTrigger;
|
||||||
this.androidMeetingPushService = androidMeetingPushService;
|
this.androidMeetingPushService = androidMeetingPushService;
|
||||||
}
|
this.androidPushMessageService = androidPushMessageService;
|
||||||
|
this.androidPendingMeetingDraftService = androidPendingMeetingDraftService;
|
||||||
public MeetingCommandServiceImpl(MeetingService meetingService,
|
this.meetingLockCache = meetingLockCache;
|
||||||
AiTaskService aiTaskService,
|
this.meetingAsrPermitCache = meetingAsrPermitCache;
|
||||||
HotWordService hotWordService,
|
|
||||||
com.imeeting.mapper.biz.MeetingTranscriptMapper transcriptMapper,
|
|
||||||
MeetingSummaryFileService meetingSummaryFileService,
|
|
||||||
MeetingTranscriptFileService meetingTranscriptFileService,
|
|
||||||
|
|
||||||
MeetingTranscriptChapterService meetingTranscriptChapterService,
|
|
||||||
MeetingDomainSupport meetingDomainSupport,
|
|
||||||
MeetingRuntimeProfileResolver meetingRuntimeProfileResolver,
|
|
||||||
RealtimeMeetingSessionStateService realtimeMeetingSessionStateService,
|
|
||||||
RealtimeMeetingAudioStorageService realtimeMeetingAudioStorageService,
|
|
||||||
StringRedisTemplate redisTemplate,
|
|
||||||
ObjectMapper objectMapper,
|
|
||||||
MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger,
|
|
||||||
AndroidMeetingPushService androidMeetingPushService) {
|
|
||||||
this(
|
|
||||||
meetingService,
|
|
||||||
aiTaskService,
|
|
||||||
hotWordService,
|
|
||||||
transcriptMapper,
|
|
||||||
meetingSummaryFileService,
|
|
||||||
meetingTranscriptFileService,
|
|
||||||
|
|
||||||
meetingTranscriptChapterService,
|
|
||||||
meetingDomainSupport,
|
|
||||||
meetingRuntimeProfileResolver,
|
|
||||||
realtimeMeetingSessionStateService,
|
|
||||||
realtimeMeetingAudioStorageService,
|
|
||||||
new RedisOnlyMeetingProgressServiceAdapter(redisTemplate, objectMapper),
|
|
||||||
objectMapper,
|
|
||||||
meetingExternalSummaryWebhookTrigger,
|
|
||||||
androidMeetingPushService
|
|
||||||
);
|
|
||||||
this.compatibilityRedisTemplate = redisTemplate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -273,6 +251,65 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public MeetingVO createPublicDeviceMeeting(PublicDeviceMeetingCreateCommand command,
|
||||||
|
Long tenantId,
|
||||||
|
Long creatorId,
|
||||||
|
String creatorName,
|
||||||
|
String deviceCode) {
|
||||||
|
RealtimeMeetingRuntimeProfile runtimeProfile = meetingRuntimeProfileResolver.resolve(
|
||||||
|
tenantId,
|
||||||
|
command.getAsrModelId(),
|
||||||
|
command.getSummaryModelId(),
|
||||||
|
command.getPromptId(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
command.getUseSpkId(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
command.getEnableTextRefine(),
|
||||||
|
null,
|
||||||
|
command.getHotWordGroupId(),
|
||||||
|
command.getHotWords()
|
||||||
|
);
|
||||||
|
Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId);
|
||||||
|
String hostName = resolveHostName(command.getHostName(), creatorName, creatorId, hostUserId);
|
||||||
|
String summaryDetailLevel = resolveSummaryDetailLevel(command.getSummaryDetailLevel());
|
||||||
|
Meeting meeting = meetingDomainSupport.initMeeting(
|
||||||
|
command.getTitle(),
|
||||||
|
command.getMeetingTime(),
|
||||||
|
command.getParticipants(),
|
||||||
|
command.getTags(),
|
||||||
|
null,
|
||||||
|
MeetingConstants.TYPE_OFFLINE,
|
||||||
|
MeetingConstants.SOURCE_ANDROID,
|
||||||
|
tenantId,
|
||||||
|
creatorId,
|
||||||
|
creatorName,
|
||||||
|
hostUserId,
|
||||||
|
hostName,
|
||||||
|
summaryDetailLevel,
|
||||||
|
0,
|
||||||
|
deviceCode,
|
||||||
|
MeetingConstants.DEVICE_MODE_PUBLIC
|
||||||
|
);
|
||||||
|
meeting.setAccessPassword(command.getAccessPassword());
|
||||||
|
meetingService.save(meeting);
|
||||||
|
|
||||||
|
AndroidPendingMeetingDraft draft = new AndroidPendingMeetingDraft();
|
||||||
|
draft.setMeetingId(meeting.getId());
|
||||||
|
draft.setDeviceId(deviceCode);
|
||||||
|
draft.setTenantId(tenantId);
|
||||||
|
draft.setCreatorId(creatorId);
|
||||||
|
draft.setCommand(command);
|
||||||
|
androidPendingMeetingDraftService.save(draft);
|
||||||
|
|
||||||
|
MeetingVO vo = new MeetingVO();
|
||||||
|
meetingDomainSupport.fillMeetingVO(meeting, vo, false, false);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void deleteMeeting(Long id) {
|
public void deleteMeeting(Long id) {
|
||||||
|
|
@ -283,6 +320,8 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
meetingService.removeById(id);
|
meetingService.removeById(id);
|
||||||
realtimeMeetingSessionStateService.clear(id);
|
realtimeMeetingSessionStateService.clear(id);
|
||||||
meetingProgressService.clear(id);
|
meetingProgressService.clear(id);
|
||||||
|
androidPushMessageService.markCancelledByMeeting(id);
|
||||||
|
androidPendingMeetingDraftService.clear(id);
|
||||||
deleteMeetingArtifactsAfterCommit(id);
|
deleteMeetingArtifactsAfterCommit(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1073,12 +1112,11 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearLegacyDispatchState(Long meetingId) {
|
private void clearLegacyDispatchState(Long meetingId) {
|
||||||
if (compatibilityRedisTemplate == null || meetingId == null) {
|
if (meetingId == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
compatibilityRedisTemplate.delete(RedisKeys.meetingPollingLockKey(meetingId));
|
meetingLockCache.clearDispatchLocks(meetingId);
|
||||||
compatibilityRedisTemplate.delete(RedisKeys.meetingSummaryLockKey(meetingId));
|
meetingAsrPermitCache.removePermit(meetingId);
|
||||||
compatibilityRedisTemplate.opsForSet().remove(RedisKeys.meetingAsrPermitSetKey(), String.valueOf(meetingId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ensureExternalSummaryModeEnabled() {
|
private void ensureExternalSummaryModeEnabled() {
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,16 @@ public class MeetingDomainSupport {
|
||||||
String audioUrl, String meetingType, String meetingSource,
|
String audioUrl, String meetingType, String meetingSource,
|
||||||
Long tenantId, Long creatorId, String creatorName,
|
Long tenantId, Long creatorId, String creatorName,
|
||||||
Long hostUserId, String hostName, String summaryDetailLevel, int status) {
|
Long hostUserId, String hostName, String summaryDetailLevel, int status) {
|
||||||
|
return initMeeting(title, meetingTime, participants, tags, audioUrl, meetingType, meetingSource,
|
||||||
|
tenantId, creatorId, creatorName, hostUserId, hostName, summaryDetailLevel, status,
|
||||||
|
null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Meeting initMeeting(String title, LocalDateTime meetingTime, String participants, String tags,
|
||||||
|
String audioUrl, String meetingType, String meetingSource,
|
||||||
|
Long tenantId, Long creatorId, String creatorName,
|
||||||
|
Long hostUserId, String hostName, String summaryDetailLevel, int status,
|
||||||
|
String sourceDeviceCode, String sourceDeviceMode) {
|
||||||
Meeting meeting = new Meeting();
|
Meeting meeting = new Meeting();
|
||||||
meeting.setTitle(title);
|
meeting.setTitle(title);
|
||||||
meeting.setMeetingTime(meetingTime);
|
meeting.setMeetingTime(meetingTime);
|
||||||
|
|
@ -67,6 +77,8 @@ public class MeetingDomainSupport {
|
||||||
meeting.setHostName(hostName);
|
meeting.setHostName(hostName);
|
||||||
meeting.setTenantId(tenantId != null ? tenantId : 0L);
|
meeting.setTenantId(tenantId != null ? tenantId : 0L);
|
||||||
meeting.setAudioUrl(audioUrl);
|
meeting.setAudioUrl(audioUrl);
|
||||||
|
meeting.setSourceDeviceCode(sourceDeviceCode);
|
||||||
|
meeting.setSourceDeviceMode(sourceDeviceMode);
|
||||||
meeting.setSummaryDetailLevel(normalizeSummaryDetailLevel(summaryDetailLevel));
|
meeting.setSummaryDetailLevel(normalizeSummaryDetailLevel(summaryDetailLevel));
|
||||||
meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_NONE);
|
meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_NONE);
|
||||||
meeting.setStatus(status);
|
meeting.setStatus(status);
|
||||||
|
|
@ -365,6 +377,8 @@ public class MeetingDomainSupport {
|
||||||
}
|
}
|
||||||
vo.setMeetingType(meeting.getMeetingType());
|
vo.setMeetingType(meeting.getMeetingType());
|
||||||
vo.setMeetingSource(meeting.getMeetingSource());
|
vo.setMeetingSource(meeting.getMeetingSource());
|
||||||
|
vo.setSourceDeviceCode(meeting.getSourceDeviceCode());
|
||||||
|
vo.setSourceDeviceMode(meeting.getSourceDeviceMode());
|
||||||
vo.setSummaryDetailLevel(normalizeSummaryDetailLevel(meeting.getSummaryDetailLevel()));
|
vo.setSummaryDetailLevel(normalizeSummaryDetailLevel(meeting.getSummaryDetailLevel()));
|
||||||
vo.setAudioSaveStatus(meeting.getAudioSaveStatus());
|
vo.setAudioSaveStatus(meeting.getAudioSaveStatus());
|
||||||
vo.setAudioSaveMessage(meeting.getAudioSaveMessage());
|
vo.setAudioSaveMessage(meeting.getAudioSaveMessage());
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,13 @@ package com.imeeting.service.biz.impl;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.imeeting.common.MeetingProgressStage;
|
import com.imeeting.common.MeetingProgressStage;
|
||||||
import com.imeeting.common.RedisKeys;
|
|
||||||
import com.imeeting.dto.biz.MeetingProgressSnapshot;
|
import com.imeeting.dto.biz.MeetingProgressSnapshot;
|
||||||
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.mapper.biz.AiTaskMapper;
|
import com.imeeting.mapper.biz.AiTaskMapper;
|
||||||
import com.imeeting.mapper.biz.MeetingMapper;
|
import com.imeeting.mapper.biz.MeetingMapper;
|
||||||
import com.imeeting.service.biz.MeetingProgressService;
|
import com.imeeting.service.biz.MeetingProgressService;
|
||||||
import com.imeeting.support.RedisValueSupport;
|
import com.imeeting.support.redis.MeetingProgressCache;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.support.TransactionSynchronization;
|
import org.springframework.transaction.support.TransactionSynchronization;
|
||||||
|
|
@ -21,17 +20,14 @@ import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class MeetingProgressServiceImpl implements MeetingProgressService {
|
public class MeetingProgressServiceImpl implements MeetingProgressService {
|
||||||
|
|
||||||
private static final long PROGRESS_TTL_HOURS = 1L;
|
|
||||||
|
|
||||||
private final MeetingMapper meetingMapper;
|
private final MeetingMapper meetingMapper;
|
||||||
private final AiTaskMapper aiTaskMapper;
|
private final AiTaskMapper aiTaskMapper;
|
||||||
private final RedisValueSupport redisValueSupport;
|
private final MeetingProgressCache meetingProgressCache;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -39,12 +35,12 @@ public class MeetingProgressServiceImpl implements MeetingProgressService {
|
||||||
if (meetingId == null) {
|
if (meetingId == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
redisValueSupport.delete(RedisKeys.meetingProgressKey(meetingId));
|
meetingProgressCache.clear(meetingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> getProgressMap(Long meetingId) {
|
public Map<String, Object> getProgressMap(Long meetingId) {
|
||||||
MeetingProgressSnapshot snapshot = redisValueSupport.getJson(RedisKeys.meetingProgressKey(meetingId), MeetingProgressSnapshot.class);
|
MeetingProgressSnapshot snapshot = meetingProgressCache.getSnapshot(meetingId);
|
||||||
if (snapshot == null) {
|
if (snapshot == null) {
|
||||||
snapshot = buildFallbackSnapshot(meetingId);
|
snapshot = buildFallbackSnapshot(meetingId);
|
||||||
if (snapshot != null) {
|
if (snapshot != null) {
|
||||||
|
|
@ -74,7 +70,7 @@ public class MeetingProgressServiceImpl implements MeetingProgressService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Integer resolvePercent(Long meetingId) {
|
public Integer resolvePercent(Long meetingId) {
|
||||||
MeetingProgressSnapshot snapshot = redisValueSupport.getJson(RedisKeys.meetingProgressKey(meetingId), MeetingProgressSnapshot.class);
|
MeetingProgressSnapshot snapshot = meetingProgressCache.getSnapshot(meetingId);
|
||||||
if (snapshot != null && snapshot.getPercent() != null) {
|
if (snapshot != null && snapshot.getPercent() != null) {
|
||||||
return snapshot.getPercent();
|
return snapshot.getPercent();
|
||||||
}
|
}
|
||||||
|
|
@ -115,11 +111,11 @@ public class MeetingProgressServiceImpl implements MeetingProgressService {
|
||||||
if (snapshot == null || snapshot.getMeetingId() == null) {
|
if (snapshot == null || snapshot.getMeetingId() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
MeetingProgressSnapshot existing = redisValueSupport.getJson(RedisKeys.meetingProgressKey(snapshot.getMeetingId()), MeetingProgressSnapshot.class);
|
MeetingProgressSnapshot existing = meetingProgressCache.getSnapshot(snapshot.getMeetingId());
|
||||||
if (!shouldReplace(existing, snapshot)) {
|
if (!shouldReplace(existing, snapshot)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
redisValueSupport.setJson(RedisKeys.meetingProgressKey(snapshot.getMeetingId()), snapshot, PROGRESS_TTL_HOURS, TimeUnit.HOURS);
|
meetingProgressCache.saveSnapshot(snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
private MeetingProgressSnapshot buildSnapshot(Long meetingId,
|
private MeetingProgressSnapshot buildSnapshot(Long meetingId,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
package com.imeeting.service.biz.impl;
|
package com.imeeting.service.biz.impl;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.imeeting.common.RedisKeys;
|
|
||||||
import com.imeeting.dto.biz.RealtimeMeetingResumeConfig;
|
import com.imeeting.dto.biz.RealtimeMeetingResumeConfig;
|
||||||
import com.imeeting.dto.biz.RealtimeMeetingSessionState;
|
import com.imeeting.dto.biz.RealtimeMeetingSessionState;
|
||||||
import com.imeeting.dto.biz.RealtimeMeetingSessionStatusVO;
|
import com.imeeting.dto.biz.RealtimeMeetingSessionStatusVO;
|
||||||
|
|
@ -11,10 +9,11 @@ import com.imeeting.entity.biz.MeetingTranscript;
|
||||||
import com.imeeting.mapper.biz.MeetingMapper;
|
import com.imeeting.mapper.biz.MeetingMapper;
|
||||||
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
||||||
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
||||||
|
import com.imeeting.support.redis.MeetingLockCache;
|
||||||
|
import com.imeeting.support.redis.RealtimeMeetingSessionCache;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
@ -27,8 +26,8 @@ import java.util.Map;
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class RealtimeMeetingSessionStateServiceImpl implements RealtimeMeetingSessionStateService {
|
public class RealtimeMeetingSessionStateServiceImpl implements RealtimeMeetingSessionStateService {
|
||||||
|
|
||||||
private final StringRedisTemplate redisTemplate;
|
private final RealtimeMeetingSessionCache sessionCache;
|
||||||
private final ObjectMapper objectMapper;
|
private final MeetingLockCache meetingLockCache;
|
||||||
private final MeetingTranscriptMapper transcriptMapper;
|
private final MeetingTranscriptMapper transcriptMapper;
|
||||||
private final MeetingMapper meetingMapper;
|
private final MeetingMapper meetingMapper;
|
||||||
|
|
||||||
|
|
@ -113,8 +112,8 @@ public class RealtimeMeetingSessionStateServiceImpl implements RealtimeMeetingSe
|
||||||
state.setUpdatedAt(now);
|
state.setUpdatedAt(now);
|
||||||
writeState(state);
|
writeState(state);
|
||||||
|
|
||||||
redisTemplate.delete(RedisKeys.realtimeMeetingResumeTimeoutKey(meetingId));
|
sessionCache.clearResumeTimeout(meetingId);
|
||||||
redisTemplate.delete(RedisKeys.realtimeMeetingEmptyTimeoutKey(meetingId));
|
sessionCache.clearEmptyTimeout(meetingId);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -194,8 +193,8 @@ public class RealtimeMeetingSessionStateServiceImpl implements RealtimeMeetingSe
|
||||||
if ("PAUSED_EMPTY".equals(state.getStatus()) || "PAUSED_RESUMABLE".equals(state.getStatus())) {
|
if ("PAUSED_EMPTY".equals(state.getStatus()) || "PAUSED_RESUMABLE".equals(state.getStatus())) {
|
||||||
state.setStatus("PAUSED_RESUMABLE");
|
state.setStatus("PAUSED_RESUMABLE");
|
||||||
state.setResumeExpireAt(now + Duration.ofMinutes(getResumeWindowMinutes()).toMillis());
|
state.setResumeExpireAt(now + Duration.ofMinutes(getResumeWindowMinutes()).toMillis());
|
||||||
ensureTimeoutKey(meetingId, RedisKeys.realtimeMeetingResumeTimeoutKey(meetingId), Duration.ofMinutes(getResumeWindowMinutes()));
|
sessionCache.saveResumeTimeout(meetingId, Duration.ofMinutes(getResumeWindowMinutes()));
|
||||||
redisTemplate.delete(RedisKeys.realtimeMeetingEmptyTimeoutKey(meetingId));
|
sessionCache.clearEmptyTimeout(meetingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
writeState(state);
|
writeState(state);
|
||||||
|
|
@ -203,9 +202,8 @@ public class RealtimeMeetingSessionStateServiceImpl implements RealtimeMeetingSe
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean markCompletingIfResumeExpired(Long meetingId) {
|
public boolean markCompletingIfResumeExpired(Long meetingId) {
|
||||||
String lockKey = RedisKeys.realtimeMeetingTimeoutLockKey(meetingId);
|
boolean locked = meetingLockCache.tryAcquireRealtimeTimeoutLock(meetingId, Duration.ofMinutes(1));
|
||||||
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", Duration.ofMinutes(1));
|
if (!locked) {
|
||||||
if (Boolean.FALSE.equals(locked)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -227,7 +225,7 @@ public class RealtimeMeetingSessionStateServiceImpl implements RealtimeMeetingSe
|
||||||
state.setLastTranscriptAt(now);
|
state.setLastTranscriptAt(now);
|
||||||
state.setResumeExpireAt(now + Duration.ofMinutes(getResumeWindowMinutes()).toMillis());
|
state.setResumeExpireAt(now + Duration.ofMinutes(getResumeWindowMinutes()).toMillis());
|
||||||
writeState(state);
|
writeState(state);
|
||||||
ensureTimeoutKey(meetingId, RedisKeys.realtimeMeetingResumeTimeoutKey(meetingId), Duration.ofMinutes(getResumeWindowMinutes()));
|
sessionCache.saveResumeTimeout(meetingId, Duration.ofMinutes(getResumeWindowMinutes()));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -236,7 +234,7 @@ public class RealtimeMeetingSessionStateServiceImpl implements RealtimeMeetingSe
|
||||||
writeState(state);
|
writeState(state);
|
||||||
return true;
|
return true;
|
||||||
} finally {
|
} finally {
|
||||||
redisTemplate.delete(lockKey);
|
meetingLockCache.releaseRealtimeTimeoutLock(meetingId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,10 +251,7 @@ public class RealtimeMeetingSessionStateServiceImpl implements RealtimeMeetingSe
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clear(Long meetingId) {
|
public void clear(Long meetingId) {
|
||||||
redisTemplate.delete(RedisKeys.realtimeMeetingSessionStateKey(meetingId));
|
sessionCache.clearAll(meetingId);
|
||||||
redisTemplate.delete(RedisKeys.realtimeMeetingResumeTimeoutKey(meetingId));
|
|
||||||
redisTemplate.delete(RedisKeys.realtimeMeetingEmptyTimeoutKey(meetingId));
|
|
||||||
redisTemplate.delete(RedisKeys.realtimeMeetingEventSeqKey(meetingId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private RealtimeMeetingSessionStatusVO pauseState(Long meetingId, RealtimeMeetingSessionState state) {
|
private RealtimeMeetingSessionStatusVO pauseState(Long meetingId, RealtimeMeetingSessionState state) {
|
||||||
|
|
@ -272,16 +267,16 @@ public class RealtimeMeetingSessionStateServiceImpl implements RealtimeMeetingSe
|
||||||
if (transcriptCount > 0) {
|
if (transcriptCount > 0) {
|
||||||
state.setStatus("PAUSED_RESUMABLE");
|
state.setStatus("PAUSED_RESUMABLE");
|
||||||
state.setResumeExpireAt(now + Duration.ofMinutes(getResumeWindowMinutes()).toMillis());
|
state.setResumeExpireAt(now + Duration.ofMinutes(getResumeWindowMinutes()).toMillis());
|
||||||
ensureTimeoutKey(meetingId, RedisKeys.realtimeMeetingResumeTimeoutKey(meetingId), Duration.ofMinutes(getResumeWindowMinutes()));
|
sessionCache.saveResumeTimeout(meetingId, Duration.ofMinutes(getResumeWindowMinutes()));
|
||||||
redisTemplate.delete(RedisKeys.realtimeMeetingEmptyTimeoutKey(meetingId));
|
sessionCache.clearEmptyTimeout(meetingId);
|
||||||
if (state.getLastTranscriptAt() == null) {
|
if (state.getLastTranscriptAt() == null) {
|
||||||
state.setLastTranscriptAt(now);
|
state.setLastTranscriptAt(now);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
state.setStatus("PAUSED_EMPTY");
|
state.setStatus("PAUSED_EMPTY");
|
||||||
state.setResumeExpireAt(null);
|
state.setResumeExpireAt(null);
|
||||||
redisTemplate.delete(RedisKeys.realtimeMeetingResumeTimeoutKey(meetingId));
|
sessionCache.clearResumeTimeout(meetingId);
|
||||||
ensureTimeoutKey(meetingId, RedisKeys.realtimeMeetingEmptyTimeoutKey(meetingId), Duration.ofMinutes(getEmptySessionRetentionMinutes()));
|
sessionCache.saveEmptyTimeout(meetingId, Duration.ofMinutes(getEmptySessionRetentionMinutes()));
|
||||||
}
|
}
|
||||||
|
|
||||||
writeState(state);
|
writeState(state);
|
||||||
|
|
@ -372,31 +367,11 @@ public class RealtimeMeetingSessionStateServiceImpl implements RealtimeMeetingSe
|
||||||
}
|
}
|
||||||
|
|
||||||
private RealtimeMeetingSessionState readState(Long meetingId) {
|
private RealtimeMeetingSessionState readState(Long meetingId) {
|
||||||
String raw = redisTemplate.opsForValue().get(RedisKeys.realtimeMeetingSessionStateKey(meetingId));
|
return sessionCache.getState(meetingId);
|
||||||
if (raw == null || raw.isBlank()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return objectMapper.readValue(raw, RealtimeMeetingSessionState.class);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.warn("Failed to read realtime meeting session state, meetingId={}", meetingId, ex);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeState(RealtimeMeetingSessionState state) {
|
private void writeState(RealtimeMeetingSessionState state) {
|
||||||
try {
|
sessionCache.saveState(state);
|
||||||
redisTemplate.opsForValue().set(
|
|
||||||
RedisKeys.realtimeMeetingSessionStateKey(state.getMeetingId()),
|
|
||||||
objectMapper.writeValueAsString(state)
|
|
||||||
);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
throw new RuntimeException("写入实时会议会话状态失败", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ensureTimeoutKey(Long meetingId, String key, Duration ttl) {
|
|
||||||
redisTemplate.opsForValue().set(key, String.valueOf(meetingId), ttl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private long getResumeWindowMinutes() {
|
private long getResumeWindowMinutes() {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
package com.imeeting.service.biz.impl;
|
package com.imeeting.service.biz.impl;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.imeeting.common.MeetingConstants;
|
import com.imeeting.common.MeetingConstants;
|
||||||
import com.imeeting.common.RedisKeys;
|
|
||||||
import com.imeeting.dto.biz.AiModelVO;
|
import com.imeeting.dto.biz.AiModelVO;
|
||||||
import com.imeeting.dto.biz.RealtimeMeetingResumeConfig;
|
import com.imeeting.dto.biz.RealtimeMeetingResumeConfig;
|
||||||
import com.imeeting.dto.biz.RealtimeSocketSessionData;
|
import com.imeeting.dto.biz.RealtimeSocketSessionData;
|
||||||
|
|
@ -12,12 +10,11 @@ import com.imeeting.service.biz.AiModelService;
|
||||||
import com.imeeting.service.biz.MeetingAccessService;
|
import com.imeeting.service.biz.MeetingAccessService;
|
||||||
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
||||||
import com.imeeting.service.biz.RealtimeMeetingSocketSessionService;
|
import com.imeeting.service.biz.RealtimeMeetingSocketSessionService;
|
||||||
|
import com.imeeting.support.redis.RealtimeMeetingSocketSessionCache;
|
||||||
import com.unisbase.security.LoginUser;
|
import com.unisbase.security.LoginUser;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
@ -27,11 +24,9 @@ import java.util.UUID;
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingSocketSessionService {
|
public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingSocketSessionService {
|
||||||
|
|
||||||
private static final Duration SESSION_TTL = Duration.ofMinutes(10);
|
|
||||||
private static final String WS_PATH = "/ws/meeting/realtime";
|
private static final String WS_PATH = "/ws/meeting/realtime";
|
||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
private final RealtimeMeetingSocketSessionCache socketSessionCache;
|
||||||
private final StringRedisTemplate redisTemplate;
|
|
||||||
private final MeetingAccessService meetingAccessService;
|
private final MeetingAccessService meetingAccessService;
|
||||||
private final AiModelService aiModelService;
|
private final AiModelService aiModelService;
|
||||||
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
|
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
|
||||||
|
|
@ -90,20 +85,12 @@ public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingS
|
||||||
sessionData.setTargetWsUrl(targetWsUrl);
|
sessionData.setTargetWsUrl(targetWsUrl);
|
||||||
|
|
||||||
String sessionToken = UUID.randomUUID().toString().replace("-", "");
|
String sessionToken = UUID.randomUUID().toString().replace("-", "");
|
||||||
try {
|
socketSessionCache.save(sessionToken, sessionData);
|
||||||
redisTemplate.opsForValue().set(
|
|
||||||
RedisKeys.realtimeMeetingSocketSessionKey(sessionToken),
|
|
||||||
objectMapper.writeValueAsString(sessionData),
|
|
||||||
SESSION_TTL
|
|
||||||
);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
throw new RuntimeException("创建实时 Socket 会话失败", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
RealtimeSocketSessionVO vo = new RealtimeSocketSessionVO();
|
RealtimeSocketSessionVO vo = new RealtimeSocketSessionVO();
|
||||||
vo.setSessionToken(sessionToken);
|
vo.setSessionToken(sessionToken);
|
||||||
vo.setPath(WS_PATH);
|
vo.setPath(WS_PATH);
|
||||||
vo.setExpiresInSeconds(SESSION_TTL.toSeconds());
|
vo.setExpiresInSeconds(socketSessionCache.getSessionTtlSeconds());
|
||||||
vo.setStartMessage(buildStartMessage(
|
vo.setStartMessage(buildStartMessage(
|
||||||
asrModel,
|
asrModel,
|
||||||
meetingId,
|
meetingId,
|
||||||
|
|
@ -121,18 +108,7 @@ public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingS
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RealtimeSocketSessionData getSessionData(String sessionToken) {
|
public RealtimeSocketSessionData getSessionData(String sessionToken) {
|
||||||
if (sessionToken == null || sessionToken.isBlank()) {
|
return socketSessionCache.get(sessionToken);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String raw = redisTemplate.opsForValue().get(RedisKeys.realtimeMeetingSocketSessionKey(sessionToken));
|
|
||||||
if (raw == null || raw.isBlank()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return objectMapper.readValue(raw, RealtimeSocketSessionData.class);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
throw new RuntimeException("读取实时 Socket 会话失败", ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolveWsUrl(AiModelVO model) {
|
private String resolveWsUrl(AiModelVO model) {
|
||||||
|
|
|
||||||
|
|
@ -1,149 +0,0 @@
|
||||||
package com.imeeting.service.biz.impl;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.imeeting.common.MeetingProgressStage;
|
|
||||||
import com.imeeting.common.RedisKeys;
|
|
||||||
import com.imeeting.dto.biz.MeetingProgressSnapshot;
|
|
||||||
import com.imeeting.entity.biz.AiTask;
|
|
||||||
import com.imeeting.service.biz.MeetingProgressService;
|
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public class RedisOnlyMeetingProgressServiceAdapter implements MeetingProgressService {
|
|
||||||
|
|
||||||
private final StringRedisTemplate redisTemplate;
|
|
||||||
private final ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
public RedisOnlyMeetingProgressServiceAdapter(StringRedisTemplate redisTemplate, ObjectMapper objectMapper) {
|
|
||||||
this.redisTemplate = redisTemplate;
|
|
||||||
this.objectMapper = objectMapper == null ? new ObjectMapper() : objectMapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clear(Long meetingId) {
|
|
||||||
if (redisTemplate == null || meetingId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
redisTemplate.delete(RedisKeys.meetingProgressKey(meetingId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Map<String, Object> getProgressMap(Long meetingId) {
|
|
||||||
MeetingProgressSnapshot snapshot = readSnapshot(meetingId);
|
|
||||||
if (snapshot == null) {
|
|
||||||
return Map.of("percent", 0, "message", "Waiting...");
|
|
||||||
}
|
|
||||||
return objectMapper.convertValue(snapshot, Map.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Map<Long, Map<String, Object>> getProgressMaps(List<Long> meetingIds) {
|
|
||||||
Map<Long, Map<String, Object>> result = new LinkedHashMap<>();
|
|
||||||
if (meetingIds == null || meetingIds.isEmpty()) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
for (Long meetingId : meetingIds) {
|
|
||||||
if (meetingId == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
result.put(meetingId, getProgressMap(meetingId));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Integer resolvePercent(Long meetingId) {
|
|
||||||
MeetingProgressSnapshot snapshot = readSnapshot(meetingId);
|
|
||||||
return snapshot == null ? null : snapshot.getPercent();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void markQueued(Long meetingId, AiTask task, Integer meetingStatus, String message) {
|
|
||||||
writeSnapshot(buildSnapshot(meetingId, task, meetingStatus, MeetingProgressStage.QUEUED, 0, message, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void markQueuedAfterCommitOrNow(Long meetingId, AiTask task, Integer meetingStatus, String message) {
|
|
||||||
markQueued(meetingId, task, meetingStatus, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void markStage(Long meetingId, AiTask task, Integer meetingStatus, MeetingProgressStage stage, int percent, String message, int eta) {
|
|
||||||
writeSnapshot(buildSnapshot(meetingId, task, meetingStatus, stage, percent, message, eta));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void markStageAfterCommitOrNow(Long meetingId, AiTask task, Integer meetingStatus, MeetingProgressStage stage, int percent, String message, int eta) {
|
|
||||||
markStage(meetingId, task, meetingStatus, stage, percent, message, eta);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void syncFromDatabase(Long meetingId) {
|
|
||||||
// No-op for constructor compatibility in tests.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeSnapshot(MeetingProgressSnapshot snapshot) {
|
|
||||||
if (redisTemplate == null || snapshot == null || snapshot.getMeetingId() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
redisTemplate.opsForValue().set(
|
|
||||||
RedisKeys.meetingProgressKey(snapshot.getMeetingId()),
|
|
||||||
objectMapper.writeValueAsString(snapshot),
|
|
||||||
1,
|
|
||||||
TimeUnit.HOURS
|
|
||||||
);
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
// Compatibility adapter keeps test setup lightweight.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private MeetingProgressSnapshot readSnapshot(Long meetingId) {
|
|
||||||
if (redisTemplate == null || meetingId == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
String raw = redisTemplate.opsForValue().get(RedisKeys.meetingProgressKey(meetingId));
|
|
||||||
if (raw == null || raw.isBlank()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return objectMapper.readValue(raw, MeetingProgressSnapshot.class);
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private MeetingProgressSnapshot buildSnapshot(Long meetingId,
|
|
||||||
AiTask task,
|
|
||||||
Integer meetingStatus,
|
|
||||||
MeetingProgressStage stage,
|
|
||||||
int percent,
|
|
||||||
String message,
|
|
||||||
int eta) {
|
|
||||||
String resolvedMessage = message;
|
|
||||||
if (stage == MeetingProgressStage.QUEUED && (resolvedMessage == null || resolvedMessage.isBlank())) {
|
|
||||||
resolvedMessage = "已进入 ASR 队列,等待执行";
|
|
||||||
}
|
|
||||||
return MeetingProgressSnapshot.builder()
|
|
||||||
.meetingId(meetingId)
|
|
||||||
.taskId(task == null ? null : task.getId())
|
|
||||||
.taskType(task == null ? null : task.getTaskType())
|
|
||||||
.taskStatus(task == null ? null : task.getStatus())
|
|
||||||
.meetingStatus(meetingStatus)
|
|
||||||
.stage(stage.getCode())
|
|
||||||
.stageOrder(stage.getOrder())
|
|
||||||
.percent(percent)
|
|
||||||
.message(resolvedMessage)
|
|
||||||
.eta(eta)
|
|
||||||
.queuedAt(task == null ? null : task.getQueuedAt())
|
|
||||||
.startedAt(task == null ? null : task.getStartedAt())
|
|
||||||
.completedAt(task == null ? null : task.getCompletedAt())
|
|
||||||
.updateAt(System.currentTimeMillis())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -25,14 +25,12 @@ import com.imeeting.service.biz.MeetingService;
|
||||||
import com.imeeting.service.biz.MeetingTranscriptChapterService;
|
import com.imeeting.service.biz.MeetingTranscriptChapterService;
|
||||||
import com.imeeting.service.biz.MeetingTranscriptFileService;
|
import com.imeeting.service.biz.MeetingTranscriptFileService;
|
||||||
import com.imeeting.service.biz.PromptTemplateService;
|
import com.imeeting.service.biz.PromptTemplateService;
|
||||||
import com.imeeting.service.biz.impl.RedisOnlyMeetingProgressServiceAdapter;
|
|
||||||
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 org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
@ -87,30 +85,6 @@ public class MeetingMcpToolService {
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MeetingMcpToolService(MeetingQueryService meetingQueryService,
|
|
||||||
MeetingAccessService meetingAccessService,
|
|
||||||
MeetingService meetingService,
|
|
||||||
AiTaskService aiTaskService,
|
|
||||||
PromptTemplateService promptTemplateService,
|
|
||||||
MeetingTranscriptFileService meetingTranscriptFileService,
|
|
||||||
MeetingTranscriptChapterService meetingTranscriptChapterService,
|
|
||||||
SysUserMapper sysUserMapper,
|
|
||||||
StringRedisTemplate redisTemplate,
|
|
||||||
ObjectMapper objectMapper) {
|
|
||||||
this(
|
|
||||||
meetingQueryService,
|
|
||||||
meetingAccessService,
|
|
||||||
meetingService,
|
|
||||||
aiTaskService,
|
|
||||||
promptTemplateService,
|
|
||||||
meetingTranscriptFileService,
|
|
||||||
meetingTranscriptChapterService,
|
|
||||||
sysUserMapper,
|
|
||||||
new RedisOnlyMeetingProgressServiceAdapter(redisTemplate, objectMapper),
|
|
||||||
objectMapper
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Value("${unisbase.app.server-base-url:}")
|
@Value("${unisbase.app.server-base-url:}")
|
||||||
private String serverBaseUrl;
|
private String serverBaseUrl;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
package com.imeeting.support;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RedisSupport {
|
||||||
|
|
||||||
|
private final StringRedisTemplate redisTemplate;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public String getStringQuietly(String key) {
|
||||||
|
try {
|
||||||
|
return redisTemplate.opsForValue().get(key);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("读取 Redis 字符串失败, key={}", key, ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T getJsonQuietly(String key, Class<T> type) {
|
||||||
|
String raw = getStringQuietly(key);
|
||||||
|
if (raw == null || raw.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(raw, type);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("读取 Redis JSON 失败, key={}, type={}", key, type == null ? null : type.getSimpleName(), ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setString(String key, String value) {
|
||||||
|
try {
|
||||||
|
redisTemplate.opsForValue().set(key, value);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new RuntimeException("写入 Redis 字符串失败, key=" + key, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setString(String key, String value, Duration ttl) {
|
||||||
|
try {
|
||||||
|
redisTemplate.opsForValue().set(key, value, ttl);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new RuntimeException("写入 Redis 字符串失败, key=" + key, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setJson(String key, Object value) {
|
||||||
|
setString(key, writeJson(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setJson(String key, Object value, Duration ttl) {
|
||||||
|
setString(key, writeJson(value), ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean setIfAbsentQuietly(String key, String value, Duration ttl) {
|
||||||
|
try {
|
||||||
|
Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value, ttl);
|
||||||
|
return Boolean.TRUE.equals(success);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("写入 Redis 锁失败, key={}", key, ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean setIfAbsentOrThrow(String key, String value, Duration ttl) {
|
||||||
|
try {
|
||||||
|
Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value, ttl);
|
||||||
|
return Boolean.TRUE.equals(success);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new RuntimeException("写入 Redis 锁失败, key=" + key, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteQuietly(String key) {
|
||||||
|
try {
|
||||||
|
redisTemplate.delete(key);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("删除 Redis Key 失败, key={}", key, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteQuietly(Collection<String> keys) {
|
||||||
|
try {
|
||||||
|
redisTemplate.delete(keys);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("批量删除 Redis Key 失败, keys={}", keys, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeFromSetQuietly(String key, String... members) {
|
||||||
|
if (members == null || members.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
redisTemplate.opsForSet().remove(key, (Object[]) members);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("从 Redis Set 删除成员失败, key={}", key, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String writeJson(Object value) {
|
||||||
|
try {
|
||||||
|
return objectMapper.writeValueAsString(value);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new RuntimeException("序列化 Redis JSON 失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
package com.imeeting.support;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
@Slf4j
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class RedisValueSupport {
|
|
||||||
|
|
||||||
private final StringRedisTemplate redisTemplate;
|
|
||||||
private final ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
public <T> T getJson(String key, Class<T> type) {
|
|
||||||
String raw = getString(key);
|
|
||||||
if (raw == null || raw.isBlank()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return objectMapper.readValue(raw, type);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.warn("Failed to parse redis json, key={}", key, ex);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getString(String key) {
|
|
||||||
try {
|
|
||||||
return redisTemplate.opsForValue().get(key);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.warn("Failed to get redis value, key={}", key, ex);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setJson(String key, Object value, long ttl, TimeUnit unit) {
|
|
||||||
try {
|
|
||||||
redisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(value), ttl, unit);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.warn("Failed to write redis json, key={}", key, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean setIfAbsent(String key, String value, long ttl, TimeUnit unit) {
|
|
||||||
try {
|
|
||||||
return redisTemplate.opsForValue().setIfAbsent(key, value, ttl, unit);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.warn("Failed to acquire redis lock, key={}", key, ex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void delete(String key) {
|
|
||||||
try {
|
|
||||||
redisTemplate.delete(key);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.warn("Failed to delete redis key, key={}", key, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void delete(Collection<String> keys) {
|
|
||||||
try {
|
|
||||||
redisTemplate.delete(keys);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.warn("Failed to delete redis keys, keys={}", keys, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package com.imeeting.support.redis;
|
||||||
|
|
||||||
|
import com.imeeting.common.RedisKeys;
|
||||||
|
import com.imeeting.dto.android.AndroidChunkUploadSessionState;
|
||||||
|
import com.imeeting.support.RedisSupport;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AndroidChunkUploadSessionCache {
|
||||||
|
|
||||||
|
private static final Duration SESSION_TTL = Duration.ofHours(6);
|
||||||
|
|
||||||
|
private final RedisSupport redisSupport;
|
||||||
|
|
||||||
|
public AndroidChunkUploadSessionState get(String uploadSessionId) {
|
||||||
|
if (uploadSessionId == null || uploadSessionId.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return redisSupport.getJsonQuietly(RedisKeys.androidChunkUploadSessionKey(uploadSessionId), AndroidChunkUploadSessionState.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save(String uploadSessionId, AndroidChunkUploadSessionState state) {
|
||||||
|
redisSupport.setJson(RedisKeys.androidChunkUploadSessionKey(uploadSessionId), state, SESSION_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear(String uploadSessionId) {
|
||||||
|
if (uploadSessionId == null || uploadSessionId.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
redisSupport.deleteQuietly(RedisKeys.androidChunkUploadSessionKey(uploadSessionId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
package com.imeeting.support.redis;
|
||||||
|
|
||||||
|
import com.imeeting.common.RedisKeys;
|
||||||
|
import com.imeeting.dto.android.AndroidDeviceSessionState;
|
||||||
|
import com.imeeting.support.RedisSupport;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AndroidDeviceSessionCache {
|
||||||
|
|
||||||
|
private final RedisSupport redisSupport;
|
||||||
|
|
||||||
|
public AndroidDeviceSessionState getByConnectionId(String connectionId) {
|
||||||
|
if (connectionId == null || connectionId.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return redisSupport.getJsonQuietly(RedisKeys.androidDeviceConnectionKey(connectionId), AndroidDeviceSessionState.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AndroidDeviceSessionState getByDeviceId(String deviceId) {
|
||||||
|
if (deviceId == null || deviceId.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return redisSupport.getJsonQuietly(RedisKeys.androidDeviceOnlineKey(deviceId), AndroidDeviceSessionState.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getActiveConnectionId(String deviceId) {
|
||||||
|
if (deviceId == null || deviceId.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String value = redisSupport.getStringQuietly(RedisKeys.androidDeviceActiveConnectionKey(deviceId));
|
||||||
|
return value == null || value.isBlank() ? null : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveTopics(String deviceId, List<String> topics) {
|
||||||
|
redisSupport.setJson(RedisKeys.androidDeviceTopicsKey(deviceId), topics == null ? List.of() : topics);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveState(AndroidDeviceSessionState state, Duration ttl) {
|
||||||
|
redisSupport.setJson(RedisKeys.androidDeviceOnlineKey(state.getDeviceId()), state, ttl);
|
||||||
|
redisSupport.setString(RedisKeys.androidDeviceActiveConnectionKey(state.getDeviceId()), state.getConnectionId(), ttl);
|
||||||
|
redisSupport.setJson(RedisKeys.androidDeviceConnectionKey(state.getConnectionId()), state, ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteConnection(String connectionId) {
|
||||||
|
redisSupport.deleteQuietly(RedisKeys.androidDeviceConnectionKey(connectionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteActiveConnection(String deviceId) {
|
||||||
|
redisSupport.deleteQuietly(RedisKeys.androidDeviceActiveConnectionKey(deviceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteOnlineState(String deviceId) {
|
||||||
|
redisSupport.deleteQuietly(RedisKeys.androidDeviceOnlineKey(deviceId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.imeeting.support.redis;
|
||||||
|
|
||||||
|
import com.imeeting.common.RedisKeys;
|
||||||
|
import com.imeeting.dto.android.AndroidPendingMeetingDraft;
|
||||||
|
import com.imeeting.support.RedisSupport;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AndroidPendingMeetingDraftCache {
|
||||||
|
|
||||||
|
private static final Duration DRAFT_TTL = Duration.ofHours(24);
|
||||||
|
|
||||||
|
private final RedisSupport redisSupport;
|
||||||
|
|
||||||
|
public void save(AndroidPendingMeetingDraft draft) {
|
||||||
|
if (draft == null || draft.getMeetingId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
redisSupport.setJson(RedisKeys.androidPendingMeetingDraftKey(draft.getMeetingId()), draft, DRAFT_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AndroidPendingMeetingDraft get(Long meetingId) {
|
||||||
|
if (meetingId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return redisSupport.getJsonQuietly(RedisKeys.androidPendingMeetingDraftKey(meetingId), AndroidPendingMeetingDraft.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear(Long meetingId) {
|
||||||
|
if (meetingId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
redisSupport.deleteQuietly(RedisKeys.androidPendingMeetingDraftKey(meetingId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.imeeting.support.redis;
|
||||||
|
|
||||||
|
import com.imeeting.common.RedisKeys;
|
||||||
|
import com.imeeting.dto.android.AndroidPublicMeetingSessionState;
|
||||||
|
import com.imeeting.support.RedisSupport;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AndroidPublicMeetingSessionCache {
|
||||||
|
|
||||||
|
private final RedisSupport redisSupport;
|
||||||
|
|
||||||
|
public void save(String sessionId, AndroidPublicMeetingSessionState state, Duration ttl) {
|
||||||
|
redisSupport.setJson(RedisKeys.publicMeetingSessionKey(sessionId), state, ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AndroidPublicMeetingSessionState get(String sessionId) {
|
||||||
|
if (sessionId == null || sessionId.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return redisSupport.getJsonQuietly(RedisKeys.publicMeetingSessionKey(sessionId), AndroidPublicMeetingSessionState.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear(String sessionId) {
|
||||||
|
if (sessionId == null || sessionId.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
redisSupport.deleteQuietly(RedisKeys.publicMeetingSessionKey(sessionId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package com.imeeting.support.redis;
|
||||||
|
|
||||||
|
import com.imeeting.common.RedisKeys;
|
||||||
|
import com.imeeting.support.RedisSupport;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MeetingAsrPermitCache {
|
||||||
|
|
||||||
|
private final RedisSupport redisSupport;
|
||||||
|
|
||||||
|
public void clearRecoveryState(Long meetingId) {
|
||||||
|
redisSupport.deleteQuietly(List.of(
|
||||||
|
RedisKeys.meetingAsrPermitSyncLockKey(),
|
||||||
|
RedisKeys.meetingAsrRefillLockKey()
|
||||||
|
));
|
||||||
|
if (meetingId != null) {
|
||||||
|
redisSupport.removeFromSetQuietly(RedisKeys.meetingAsrPermitSetKey(), String.valueOf(meetingId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removePermit(Long meetingId) {
|
||||||
|
if (meetingId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
redisSupport.removeFromSetQuietly(RedisKeys.meetingAsrPermitSetKey(), String.valueOf(meetingId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package com.imeeting.support.redis;
|
||||||
|
|
||||||
|
import com.imeeting.common.RedisKeys;
|
||||||
|
import com.imeeting.support.RedisSupport;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MeetingLockCache {
|
||||||
|
|
||||||
|
private final RedisSupport redisSupport;
|
||||||
|
|
||||||
|
public boolean tryAcquirePollingLock(Long meetingId, Duration ttl) {
|
||||||
|
return redisSupport.setIfAbsentOrThrow(RedisKeys.meetingPollingLockKey(meetingId), "locked", ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean tryAcquireSummaryLock(Long meetingId, Duration ttl) {
|
||||||
|
return redisSupport.setIfAbsentOrThrow(RedisKeys.meetingSummaryLockKey(meetingId), "locked", ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean tryAcquireAsrScheduleLock(Duration ttl) {
|
||||||
|
return redisSupport.setIfAbsentOrThrow(RedisKeys.meetingAsrScheduleLockKey(), "locked", ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean tryAcquireRealtimeTimeoutLock(Long meetingId, Duration ttl) {
|
||||||
|
return redisSupport.setIfAbsentOrThrow(RedisKeys.realtimeMeetingTimeoutLockKey(meetingId), "1", ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void releasePollingLock(Long meetingId) {
|
||||||
|
redisSupport.deleteQuietly(RedisKeys.meetingPollingLockKey(meetingId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void releaseSummaryLock(Long meetingId) {
|
||||||
|
redisSupport.deleteQuietly(RedisKeys.meetingSummaryLockKey(meetingId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void releaseAsrScheduleLock() {
|
||||||
|
redisSupport.deleteQuietly(RedisKeys.meetingAsrScheduleLockKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void releaseRealtimeTimeoutLock(Long meetingId) {
|
||||||
|
redisSupport.deleteQuietly(RedisKeys.realtimeMeetingTimeoutLockKey(meetingId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearDispatchLocks(Long meetingId) {
|
||||||
|
if (meetingId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
redisSupport.deleteQuietly(RedisKeys.meetingPollingLockKey(meetingId));
|
||||||
|
redisSupport.deleteQuietly(RedisKeys.meetingSummaryLockKey(meetingId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package com.imeeting.support.redis;
|
||||||
|
|
||||||
|
import com.imeeting.common.RedisKeys;
|
||||||
|
import com.imeeting.dto.biz.MeetingProgressSnapshot;
|
||||||
|
import com.imeeting.support.RedisSupport;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MeetingProgressCache {
|
||||||
|
|
||||||
|
private static final Duration PROGRESS_TTL = Duration.ofHours(1);
|
||||||
|
|
||||||
|
private final RedisSupport redisSupport;
|
||||||
|
|
||||||
|
public MeetingProgressSnapshot getSnapshot(Long meetingId) {
|
||||||
|
if (meetingId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return redisSupport.getJsonQuietly(RedisKeys.meetingProgressKey(meetingId), MeetingProgressSnapshot.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveSnapshot(MeetingProgressSnapshot snapshot) {
|
||||||
|
if (snapshot == null || snapshot.getMeetingId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
redisSupport.setJson(RedisKeys.meetingProgressKey(snapshot.getMeetingId()), snapshot, PROGRESS_TTL);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("写入会议进度缓存失败, meetingId={}", snapshot.getMeetingId(), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear(Long meetingId) {
|
||||||
|
if (meetingId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
redisSupport.deleteQuietly(RedisKeys.meetingProgressKey(meetingId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
package com.imeeting.support.redis;
|
||||||
|
|
||||||
|
import com.imeeting.common.RedisKeys;
|
||||||
|
import com.imeeting.dto.biz.RealtimeMeetingSessionState;
|
||||||
|
import com.imeeting.support.RedisSupport;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RealtimeMeetingSessionCache {
|
||||||
|
|
||||||
|
private final RedisSupport redisSupport;
|
||||||
|
|
||||||
|
public RealtimeMeetingSessionState getState(Long meetingId) {
|
||||||
|
if (meetingId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return redisSupport.getJsonQuietly(RedisKeys.realtimeMeetingSessionStateKey(meetingId), RealtimeMeetingSessionState.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveState(RealtimeMeetingSessionState state) {
|
||||||
|
if (state == null || state.getMeetingId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
redisSupport.setJson(RedisKeys.realtimeMeetingSessionStateKey(state.getMeetingId()), state);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveResumeTimeout(Long meetingId, Duration ttl) {
|
||||||
|
redisSupport.setString(RedisKeys.realtimeMeetingResumeTimeoutKey(meetingId), String.valueOf(meetingId), ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveEmptyTimeout(Long meetingId, Duration ttl) {
|
||||||
|
redisSupport.setString(RedisKeys.realtimeMeetingEmptyTimeoutKey(meetingId), String.valueOf(meetingId), ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearResumeTimeout(Long meetingId) {
|
||||||
|
redisSupport.deleteQuietly(RedisKeys.realtimeMeetingResumeTimeoutKey(meetingId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearEmptyTimeout(Long meetingId) {
|
||||||
|
redisSupport.deleteQuietly(RedisKeys.realtimeMeetingEmptyTimeoutKey(meetingId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearAll(Long meetingId) {
|
||||||
|
if (meetingId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
redisSupport.deleteQuietly(RedisKeys.realtimeMeetingSessionStateKey(meetingId));
|
||||||
|
redisSupport.deleteQuietly(RedisKeys.realtimeMeetingResumeTimeoutKey(meetingId));
|
||||||
|
redisSupport.deleteQuietly(RedisKeys.realtimeMeetingEmptyTimeoutKey(meetingId));
|
||||||
|
redisSupport.deleteQuietly(RedisKeys.realtimeMeetingEventSeqKey(meetingId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.imeeting.support.redis;
|
||||||
|
|
||||||
|
import com.imeeting.common.RedisKeys;
|
||||||
|
import com.imeeting.dto.biz.RealtimeSocketSessionData;
|
||||||
|
import com.imeeting.support.RedisSupport;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RealtimeMeetingSocketSessionCache {
|
||||||
|
|
||||||
|
private static final Duration SESSION_TTL = Duration.ofMinutes(10);
|
||||||
|
|
||||||
|
private final RedisSupport redisSupport;
|
||||||
|
|
||||||
|
public void save(String sessionToken, RealtimeSocketSessionData sessionData) {
|
||||||
|
redisSupport.setJson(RedisKeys.realtimeMeetingSocketSessionKey(sessionToken), sessionData, SESSION_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealtimeSocketSessionData get(String sessionToken) {
|
||||||
|
if (sessionToken == null || sessionToken.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return redisSupport.getJsonQuietly(RedisKeys.realtimeMeetingSocketSessionKey(sessionToken), RealtimeSocketSessionData.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getSessionTtlSeconds() {
|
||||||
|
return SESSION_TTL.toSeconds();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package com.imeeting.task;
|
||||||
|
|
||||||
|
import com.imeeting.entity.biz.AndroidPushMessage;
|
||||||
|
import com.imeeting.grpc.push.PushMessage;
|
||||||
|
import com.imeeting.service.android.AndroidGatewayPushService;
|
||||||
|
import com.imeeting.service.android.AndroidPushMessageService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AndroidPushMessageRetryTask {
|
||||||
|
private final AndroidPushMessageService androidPushMessageService;
|
||||||
|
private final AndroidGatewayPushService androidGatewayPushService;
|
||||||
|
|
||||||
|
@Scheduled(fixedDelayString = "${imeeting.android.push.retry-interval-ms:15000}")
|
||||||
|
public void retryPendingMessages() {
|
||||||
|
List<AndroidPushMessage> pendingMessages = androidPushMessageService.listPendingMeetingPushMessages();
|
||||||
|
for (AndroidPushMessage message : pendingMessages) {
|
||||||
|
if (message.getExpireAt() != null && message.getExpireAt().isBefore(LocalDateTime.now())) {
|
||||||
|
androidPushMessageService.markExpired(message.getId());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
PushMessage pushMessage = PushMessage.newBuilder()
|
||||||
|
.setMessageId(message.getMessageId())
|
||||||
|
.setTimestamp(System.currentTimeMillis())
|
||||||
|
.setType(message.getMessageType())
|
||||||
|
.setTitle("待开始会议")
|
||||||
|
.setContent(message.getPayload() == null ? "" : message.getPayload())
|
||||||
|
.setNeedAck(true)
|
||||||
|
.build();
|
||||||
|
int pushed = androidGatewayPushService.pushToDevice(message.getDeviceCode(), pushMessage);
|
||||||
|
if (pushed > 0) {
|
||||||
|
androidPushMessageService.markPushed(message.getId());
|
||||||
|
log.info("Retried android push message, messageId={}, deviceCode={}, pushCountIncreased=true", message.getMessageId(), message.getDeviceCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,8 @@ export interface MeetingCreateConfig {
|
||||||
offlineEnabled: boolean;
|
offlineEnabled: boolean;
|
||||||
realtimeEnabled: boolean;
|
realtimeEnabled: boolean;
|
||||||
offlineAudioMaxSizeMb: number;
|
offlineAudioMaxSizeMb: number;
|
||||||
|
chunkUploadEnabled?: boolean;
|
||||||
|
chunkDurationSeconds?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MeetingVO {
|
export interface MeetingVO {
|
||||||
|
|
@ -28,6 +30,8 @@ export interface MeetingVO {
|
||||||
playbackAudioUrl?: string;
|
playbackAudioUrl?: string;
|
||||||
meetingType?: "OFFLINE" | "REALTIME";
|
meetingType?: "OFFLINE" | "REALTIME";
|
||||||
meetingSource?: "WEB" | "ANDROID";
|
meetingSource?: "WEB" | "ANDROID";
|
||||||
|
sourceDeviceCode?: string;
|
||||||
|
sourceDeviceMode?: "PUBLIC" | "PRIVATE";
|
||||||
summaryDetailLevel?: SummaryDetailLevel;
|
summaryDetailLevel?: SummaryDetailLevel;
|
||||||
audioSaveStatus?: "NONE" | "SUCCESS" | "FAILED";
|
audioSaveStatus?: "NONE" | "SUCCESS" | "FAILED";
|
||||||
audioSaveMessage?: string;
|
audioSaveMessage?: string;
|
||||||
|
|
@ -95,6 +99,26 @@ export interface CreateMeetingCommand {
|
||||||
hotWords?: string[];
|
hotWords?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PublicDeviceMeetingCreateCommand {
|
||||||
|
title: string;
|
||||||
|
meetingTime: string;
|
||||||
|
participants: string;
|
||||||
|
tags: string;
|
||||||
|
hostUserId?: number;
|
||||||
|
hostName?: string;
|
||||||
|
asrModelId: number;
|
||||||
|
summaryModelId: number;
|
||||||
|
chapterModelId?: number;
|
||||||
|
promptId: number;
|
||||||
|
hotWordGroupId?: number;
|
||||||
|
userPrompt?: string;
|
||||||
|
summaryDetailLevel?: SummaryDetailLevel;
|
||||||
|
useSpkId?: number;
|
||||||
|
enableTextRefine?: boolean;
|
||||||
|
hotWords?: string[];
|
||||||
|
accessPassword?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type MeetingDTO = CreateMeetingCommand;
|
export type MeetingDTO = CreateMeetingCommand;
|
||||||
|
|
||||||
export interface CreateRealtimeMeetingCommand {
|
export interface CreateRealtimeMeetingCommand {
|
||||||
|
|
@ -160,6 +184,13 @@ export const createMeeting = (data: CreateMeetingCommand) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createPublicDeviceMeetingBySession = (sessionId: string, data: PublicDeviceMeetingCreateCommand) => {
|
||||||
|
return http.post<{ code: string; data: MeetingVO; msg: string }>(
|
||||||
|
`/api/biz/public-device-meetings/sessions/${sessionId}/create`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export interface RealtimeTranscriptItemDTO {
|
export interface RealtimeTranscriptItemDTO {
|
||||||
speakerId?: string;
|
speakerId?: string;
|
||||||
speakerName?: string;
|
speakerName?: string;
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,9 @@ export default function Login() {
|
||||||
}
|
}
|
||||||
|
|
||||||
message.success(t("common.success"));
|
message.success(t("common.success"));
|
||||||
window.location.href = "/";
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
|
const redirect = searchParams.get("redirect");
|
||||||
|
window.location.href = redirect || "/";
|
||||||
} catch {
|
} catch {
|
||||||
if (captchaEnabled) {
|
if (captchaEnabled) {
|
||||||
await loadCaptcha();
|
await loadCaptcha();
|
||||||
|
|
|
||||||
|
|
@ -239,8 +239,9 @@ const MeetingCardItem: React.FC<{
|
||||||
progress: MeetingProgress | null;
|
progress: MeetingProgress | null;
|
||||||
onOpenMeeting: (meeting: MeetingVO) => void;
|
onOpenMeeting: (meeting: MeetingVO) => void;
|
||||||
onRetrySchedule: (meeting: MeetingVO) => void;
|
onRetrySchedule: (meeting: MeetingVO) => void;
|
||||||
|
onDelete: (id: number) => void;
|
||||||
retrying: boolean;
|
retrying: boolean;
|
||||||
}> = ({ item, config, progress, onOpenMeeting, onRetrySchedule, retrying }) => {
|
}> = ({ item, config, progress, onOpenMeeting, onRetrySchedule, onDelete, retrying }) => {
|
||||||
const effectiveStatus = getEffectiveStatus(item, progress);
|
const effectiveStatus = getEffectiveStatus(item, progress);
|
||||||
const isProcessing = shouldTrackGenerationProgress(item);
|
const isProcessing = shouldTrackGenerationProgress(item);
|
||||||
const isPaused = effectiveStatus === PAUSED_DISPLAY_STATUS;
|
const isPaused = effectiveStatus === PAUSED_DISPLAY_STATUS;
|
||||||
|
|
@ -310,7 +311,7 @@ const MeetingCardItem: React.FC<{
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="确定删除会议吗?"
|
title="确定删除会议吗?"
|
||||||
description="删除后将无法找回该会议记录。"
|
description="删除后将无法找回该会议记录。"
|
||||||
onConfirm={() => deleteMeeting(item.id).then(fetchData)}
|
onConfirm={() => onDelete(item.id)}
|
||||||
okText="删除"
|
okText="删除"
|
||||||
cancelText="取消"
|
cancelText="取消"
|
||||||
okButtonProps={{ danger: true }}
|
okButtonProps={{ danger: true }}
|
||||||
|
|
@ -905,6 +906,7 @@ const Meetings: React.FC = () => {
|
||||||
progress={progress}
|
progress={progress}
|
||||||
onOpenMeeting={handleOpenMeeting}
|
onOpenMeeting={handleOpenMeeting}
|
||||||
onRetrySchedule={(meeting) => { void handleRetrySchedule(meeting); }}
|
onRetrySchedule={(meeting) => { void handleRetrySchedule(meeting); }}
|
||||||
|
onDelete={(id) => { deleteMeeting(id).then(() => { message.success('删除成功'); fetchData(); }) }}
|
||||||
retrying={!!retryingMeetingIds[item.id]}
|
retrying={!!retryingMeetingIds[item.id]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Suspense, lazy } from "react";
|
import { Suspense, lazy } from "react";
|
||||||
import { Navigate, Route, Routes } from "react-router-dom";
|
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
|
||||||
import AppLayout from "@/layouts/AppLayout";
|
import AppLayout from "@/layouts/AppLayout";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { menuRoutes,extraRoutes } from "./routes";
|
import { menuRoutes,extraRoutes } from "./routes";
|
||||||
|
|
@ -7,6 +7,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"));
|
const MeetingPreview = lazy(() => import("@/pages/business/MeetingPreview"));
|
||||||
|
const PublicDeviceMeetingCreate = lazy(() => import("@/pages/business/PublicDeviceMeetingCreate"));
|
||||||
|
|
||||||
function RouteFallback() {
|
function RouteFallback() {
|
||||||
let platformName = "iMeeting";
|
let platformName = "iMeeting";
|
||||||
|
|
@ -50,9 +51,11 @@ function RouteFallback() {
|
||||||
|
|
||||||
function RequireAuth({ children }: { children: JSX.Element }) {
|
function RequireAuth({ children }: { children: JSX.Element }) {
|
||||||
const { isAuthed, profile } = useAuth();
|
const { isAuthed, profile } = useAuth();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
if (!isAuthed) {
|
if (!isAuthed) {
|
||||||
return <Navigate to="/login" replace />;
|
const redirect = encodeURIComponent(`${location.pathname}${location.search}`);
|
||||||
|
return <Navigate to={`/login?redirect=${redirect}`} replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile?.pwdResetRequired === 1) {
|
if (profile?.pwdResetRequired === 1) {
|
||||||
|
|
@ -72,6 +75,14 @@ export default function AppRoutes() {
|
||||||
path="/meetings/:id/preview"
|
path="/meetings/:id/preview"
|
||||||
element={<MeetingPreview />}
|
element={<MeetingPreview />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/public-device-meetings/:sessionId/create"
|
||||||
|
element={
|
||||||
|
<RequireAuth>
|
||||||
|
<PublicDeviceMeetingCreate />
|
||||||
|
</RequireAuth>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
element={
|
element={
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue