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;
|
||||
|
||||
public final class MeetingConstants {
|
||||
|
||||
public static final String TYPE_OFFLINE = "OFFLINE";
|
||||
public static final String TYPE_REALTIME = "REALTIME";
|
||||
|
||||
public static final String SOURCE_WEB = "WEB";
|
||||
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_STANDARD = "STANDARD";
|
||||
public static final String SUMMARY_DETAIL_BRIEF = "BRIEF";
|
||||
|
|
|
|||
|
|
@ -111,6 +111,18 @@ public final class RedisKeys {
|
|||
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 SYS_PARAM_FIELD_VALUE = "value";
|
||||
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_MEETING_DURATION = "meeting.max_meeting_duration";
|
||||
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;
|
||||
|
||||
import com.imeeting.service.android.AndroidDeviceBindingService;
|
||||
import com.imeeting.support.AndroidRequestLogHelper;
|
||||
import com.unisbase.auth.JwtTokenProvider;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.dto.LoginRequest;
|
||||
import com.unisbase.dto.RefreshRequest;
|
||||
import com.unisbase.dto.TokenResponse;
|
||||
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.Schema;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
|
@ -27,8 +30,9 @@ import org.springframework.web.bind.annotation.RestController;
|
|||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AndroidAuthController {
|
||||
|
||||
private final AuthService authService;
|
||||
private final AndroidDeviceBindingService androidDeviceBindingService;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
@Operation(summary = "Android登录")
|
||||
@ApiResponses({
|
||||
|
|
@ -39,9 +43,26 @@ public class AndroidAuthController {
|
|||
)
|
||||
})
|
||||
@PostMapping("/login")
|
||||
public ApiResponse<TokenResponse> login(@Valid @RequestBody LoginRequest request) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android认证", "登录接口", "request", request);
|
||||
return ApiResponse.ok(authService.login(request, true));
|
||||
public ApiResponse<TokenResponse> login(@Valid @RequestBody LoginRequest request,
|
||||
@RequestHeader(value = "X-Android-Device-Id", required = false) String deviceId,
|
||||
@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刷新令牌")
|
||||
|
|
@ -63,6 +84,26 @@ public class AndroidAuthController {
|
|||
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) {
|
||||
if (request != null && StringUtils.hasText(request.getRefreshToken())) {
|
||||
return request.getRefreshToken().trim();
|
||||
|
|
@ -79,4 +120,12 @@ public class AndroidAuthController {
|
|||
}
|
||||
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.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.dto.android.AndroidAuthContext;
|
||||
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.support.AndroidRequestLogHelper;
|
||||
import com.imeeting.service.biz.*;
|
||||
import com.imeeting.service.biz.impl.RedisOnlyMeetingProgressServiceAdapter;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.common.annotation.Log;
|
||||
import com.unisbase.dto.PageResult;
|
||||
|
|
@ -44,7 +40,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
|
|
@ -91,7 +86,6 @@ public class AndroidMeetingController {
|
|||
private final SysDictItemService dictItemService;
|
||||
private final SysParamService paramService;
|
||||
private final MeetingProgressService meetingProgressService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
public AndroidMeetingController(AndroidAuthService androidAuthService,
|
||||
|
|
@ -106,8 +100,7 @@ public class AndroidMeetingController {
|
|||
AiModelService aiModelService,
|
||||
SysDictItemService dictItemService,
|
||||
SysParamService paramService,
|
||||
MeetingProgressService meetingProgressService,
|
||||
ObjectMapper objectMapper) {
|
||||
MeetingProgressService meetingProgressService) {
|
||||
this.androidAuthService = androidAuthService;
|
||||
this.legacyMeetingAdapterService = legacyMeetingAdapterService;
|
||||
this.meetingQueryService = meetingQueryService;
|
||||
|
|
@ -118,44 +111,11 @@ public class AndroidMeetingController {
|
|||
this.promptTemplateService = promptTemplateService;
|
||||
this.sysUserMapper = sysUserMapper;
|
||||
this.meetingProgressService = meetingProgressService;
|
||||
this.objectMapper = objectMapper;
|
||||
this.aiModelService = aiModelService;
|
||||
this.paramService = paramService;
|
||||
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离线会议")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
|
|
@ -170,7 +130,11 @@ public class AndroidMeetingController {
|
|||
AndroidRequestLogHelper.logRequest(log, "Android会议", "创建离线会议接口", "request", command);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
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会议音频")
|
||||
|
|
@ -195,6 +159,14 @@ public class AndroidMeetingController {
|
|||
"forceReplace", forceReplace,
|
||||
"audioFile", audioFile);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
if (authContext.isAnonymous()) {
|
||||
return ApiResponse.ok(legacyMeetingAdapterService.uploadAndTriggerOfflineProcessForPublicDevice(
|
||||
meetingId,
|
||||
forceReplace,
|
||||
audioFile,
|
||||
authContext
|
||||
));
|
||||
}
|
||||
LoginUser loginUser = AndroidLoginUserSupport.requireLoginUser(authContext);
|
||||
return ApiResponse.ok(legacyMeetingAdapterService.uploadAndTriggerOfflineProcess(
|
||||
meetingId,
|
||||
|
|
@ -202,6 +174,7 @@ public class AndroidMeetingController {
|
|||
modelCode,
|
||||
forceReplace,
|
||||
audioFile,
|
||||
authContext,
|
||||
loginUser
|
||||
));
|
||||
}
|
||||
|
|
@ -341,6 +314,8 @@ public class AndroidMeetingController {
|
|||
BigDecimal bigDecimal = new BigDecimal(paramService.getParamValue(SysParamKeys.MEETING_MAX_PAUSE_DURATION, "99"));
|
||||
bigDecimal = bigDecimal.setScale(2, RoundingMode.HALF_UP);
|
||||
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);
|
||||
}
|
||||
|
|
@ -629,4 +604,15 @@ public class AndroidMeetingController {
|
|||
private String formatDateTime(LocalDateTime value) {
|
||||
return value == null ? null : value.toString();
|
||||
}
|
||||
|
||||
private Meeting findLatestUnfinishedMeetingByDevice(String deviceId) {
|
||||
if (deviceId == null || deviceId.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return meetingService.getOne(new LambdaQueryWrapper<Meeting>()
|
||||
.eq(Meeting::getSourceDeviceCode, deviceId)
|
||||
.in(Meeting::getStatus, 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.update.LambdaUpdateWrapper;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.imeeting.common.RedisKeys;
|
||||
import com.imeeting.dto.android.legacy.LegacyApiResponse;
|
||||
import com.imeeting.dto.android.AndroidAuthContext;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingAccessPasswordRequest;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingAccessPasswordResponse;
|
||||
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.MeetingService;
|
||||
import com.imeeting.service.biz.PromptTemplateService;
|
||||
import com.imeeting.service.biz.impl.RedisOnlyMeetingProgressServiceAdapter;
|
||||
import com.unisbase.common.annotation.Log;
|
||||
import com.unisbase.dto.PageResult;
|
||||
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 lombok.extern.slf4j.Slf4j;
|
||||
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.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
|
|
@ -109,32 +106,6 @@ public class LegacyMeetingController {
|
|||
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
|
||||
public LegacyMeetingController(LegacyMeetingAdapterService legacyMeetingAdapterService,
|
||||
MeetingQueryService meetingQueryService,
|
||||
|
|
@ -166,7 +137,7 @@ public class LegacyMeetingController {
|
|||
@Log(value = "新增兼容会议", type = "兼容会议管理")
|
||||
public LegacyApiResponse<LegacyMeetingCreateResponse> create(@RequestBody LegacyMeetingCreateRequest 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()));
|
||||
}
|
||||
|
||||
|
|
@ -190,6 +161,7 @@ public class LegacyMeetingController {
|
|||
modelCode,
|
||||
forceReplace,
|
||||
audioFile,
|
||||
buildLegacyAuthContext(),
|
||||
currentLoginUser()
|
||||
);
|
||||
return LegacyApiResponse.ok("上传成功", null);
|
||||
|
|
@ -665,4 +637,7 @@ public class LegacyMeetingController {
|
|||
private String resolveCreatorName(LoginUser loginUser) {
|
||||
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.imeeting.common.MeetingConstants;
|
||||
import com.imeeting.common.RedisKeys;
|
||||
import com.imeeting.common.SysParamKeys;
|
||||
import com.imeeting.dto.biz.CreateMeetingCommand;
|
||||
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.RealtimeMeetingSocketSessionService;
|
||||
import com.imeeting.service.biz.impl.MeetingAudioUploadSupport;
|
||||
import com.imeeting.service.biz.impl.RedisOnlyMeetingProgressServiceAdapter;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.common.annotation.Log;
|
||||
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 jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
|
@ -88,7 +85,6 @@ public class MeetingController {
|
|||
private final MeetingProgressService meetingProgressService;
|
||||
private final SysParamService sysParamService;
|
||||
private final AiTaskService aiTaskService;
|
||||
private AiTaskService compatibilityAiTaskService;
|
||||
|
||||
@Autowired
|
||||
public MeetingController(MeetingQueryService meetingQueryService,
|
||||
|
|
@ -117,35 +113,6 @@ public class MeetingController {
|
|||
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 = "查询会议处理进度")
|
||||
@GetMapping("/{id}/progress")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
|
|
@ -154,8 +121,8 @@ public class MeetingController {
|
|||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanViewMeeting(meeting, loginUser);
|
||||
Map<String, Object> progress = meetingProgressService.getProgressMap(id);
|
||||
if (compatibilityAiTaskService != null && "Waiting...".equals(progress.get("message"))) {
|
||||
AiTask asrTask = compatibilityAiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
||||
if ("Waiting...".equals(progress.get("message"))) {
|
||||
AiTask asrTask = aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
||||
.eq(AiTask::getMeetingId, id)
|
||||
.eq(AiTask::getTaskType, "ASR")
|
||||
.orderByDesc(AiTask::getId)
|
||||
|
|
@ -188,8 +155,8 @@ public class MeetingController {
|
|||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanViewMeeting(meeting, loginUser);
|
||||
Map<String, Object> progress = meetingProgressService.getProgressMap(id);
|
||||
if (compatibilityAiTaskService != null && "Waiting...".equals(progress.get("message"))) {
|
||||
AiTask asrTask = compatibilityAiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
||||
if ("Waiting...".equals(progress.get("message"))) {
|
||||
AiTask asrTask = aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
||||
.eq(AiTask::getMeetingId, id)
|
||||
.eq(AiTask::getTaskType, "ASR")
|
||||
.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
|
||||
public class AndroidMeetingConfigVo {
|
||||
@io.swagger.v3.oas.annotations.media.Schema(description = "可用模型列表")
|
||||
private List<AiModelVO> modelsList;
|
||||
@io.swagger.v3.oas.annotations.media.Schema(description = "可用模板列表")
|
||||
private List<PromptTemplateVO> templateList;
|
||||
@io.swagger.v3.oas.annotations.media.Schema(description = "总结详细程度字典项")
|
||||
private List<SysDictItemDTO> summaryDegreeOfDetail;
|
||||
@io.swagger.v3.oas.annotations.media.Schema(description = "允许暂停最大时长,单位秒")
|
||||
private Integer maxPauseDuration;
|
||||
@io.swagger.v3.oas.annotations.media.Schema(description = "最大会议时长,单位分钟")
|
||||
private Integer maxMeetingDuration;
|
||||
@io.swagger.v3.oas.annotations.media.Schema(description = "允许的最大丢包率")
|
||||
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;
|
||||
@Schema(description = "会议来源")
|
||||
private String meetingSource;
|
||||
@Schema(description = "来源设备编码")
|
||||
private String sourceDeviceCode;
|
||||
@Schema(description = "来源设备模式")
|
||||
private String sourceDeviceMode;
|
||||
@Schema(description = "总结详细程度")
|
||||
private String summaryDetailLevel;
|
||||
@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 = "会议来源")
|
||||
private String meetingSource;
|
||||
|
||||
@Schema(description = "来源设备编码")
|
||||
private String sourceDeviceCode;
|
||||
|
||||
@Schema(description = "来源设备模式")
|
||||
private String sourceDeviceMode;
|
||||
|
||||
@Schema(description = "总结详细程度")
|
||||
private String summaryDetailLevel;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,16 +4,14 @@ import lombok.Getter;
|
|||
|
||||
@Getter
|
||||
public enum MeetingPushTypeEnum {
|
||||
MEETING_COMPLETED("MEETING_COMPLETED","会议完成通知"),;
|
||||
MEETING_PENDING("MEETING_PENDING", "待开始会议通知"),
|
||||
MEETING_COMPLETED("MEETING_COMPLETED", "会议完成通知");
|
||||
|
||||
private final String code;
|
||||
private final String 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.AndroidDeviceSessionState;
|
||||
import com.imeeting.service.android.AndroidAuthService;
|
||||
import com.imeeting.service.android.AndroidPushMessageService;
|
||||
import com.imeeting.service.android.AndroidDeviceSessionService;
|
||||
import com.imeeting.service.android.AndroidGatewayPushService;
|
||||
import com.imeeting.service.biz.DeviceOnlineManagementService;
|
||||
|
|
@ -26,6 +27,7 @@ public class AndroidPushGrpcService extends PushServiceGrpc.PushServiceImplBase
|
|||
private final AndroidAuthService androidAuthService;
|
||||
private final AndroidDeviceSessionService androidDeviceSessionService;
|
||||
private final AndroidGatewayPushService androidGatewayPushService;
|
||||
private final AndroidPushMessageService androidPushMessageService;
|
||||
private final DeviceOnlineManagementService deviceOnlineManagementService;
|
||||
|
||||
@Override
|
||||
|
|
@ -203,6 +205,7 @@ public class AndroidPushGrpcService extends PushServiceGrpc.PushServiceImplBase
|
|||
deviceId,
|
||||
appVersion,
|
||||
platform));
|
||||
androidPushMessageService.ack(request.getMessageId(), deviceId);
|
||||
}
|
||||
|
||||
private boolean validateConnected() {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
package com.imeeting.listener;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.imeeting.common.RedisKeys;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.mapper.biz.MeetingMapper;
|
||||
import com.imeeting.service.biz.AiTaskService;
|
||||
import com.imeeting.support.RedisValueSupport;
|
||||
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 org.springframework.boot.ApplicationArguments;
|
||||
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 java.util.List;
|
||||
|
|
@ -19,32 +18,14 @@ import java.util.concurrent.TimeUnit;
|
|||
|
||||
@Component
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class MeetingTaskRecoveryListener implements ApplicationRunner {
|
||||
|
||||
private final MeetingMapper meetingMapper;
|
||||
private final AiTaskService aiTaskService;
|
||||
private final RedisValueSupport redisValueSupport;
|
||||
private final MeetingLockCache meetingLockCache;
|
||||
private final MeetingAsrPermitCache meetingAsrPermitCache;
|
||||
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
|
||||
public void run(ApplicationArguments args) {
|
||||
|
|
@ -62,9 +43,8 @@ public class MeetingTaskRecoveryListener implements ApplicationRunner {
|
|||
|
||||
for (Meeting meeting : pendingMeetings) {
|
||||
try {
|
||||
redisValueSupport.delete(RedisKeys.meetingPollingLockKey(meeting.getId()));
|
||||
redisValueSupport.delete(RedisKeys.meetingSummaryLockKey(meeting.getId()));
|
||||
clearLegacyRedisState(meeting.getId());
|
||||
meetingLockCache.clearDispatchLocks(meeting.getId());
|
||||
meetingAsrPermitCache.clearRecoveryState(meeting.getId());
|
||||
|
||||
if (Integer.valueOf(1).equals(meeting.getStatus())) {
|
||||
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());
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
public interface AndroidMeetingPushService {
|
||||
void pushPendingMeetingToDevice(Long meetingId, String deviceId);
|
||||
|
||||
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.mapper.DeviceInfoMapper;
|
||||
import com.imeeting.service.android.AndroidAuthService;
|
||||
import com.imeeting.service.android.AndroidDeviceBindingService;
|
||||
import com.unisbase.common.exception.BusinessException;
|
||||
import com.unisbase.dto.InternalAuthCheckResponse;
|
||||
import com.unisbase.security.LoginUser;
|
||||
|
|
@ -32,6 +33,7 @@ public class AndroidAuthServiceImpl implements AndroidAuthService {
|
|||
private final AndroidGrpcAuthProperties properties;
|
||||
private final TokenValidationService tokenValidationService;
|
||||
private final DeviceInfoMapper deviceInfoMapper;
|
||||
private final AndroidDeviceBindingService androidDeviceBindingService;
|
||||
|
||||
@Override
|
||||
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");
|
||||
}
|
||||
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
|
||||
|
|
@ -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);
|
||||
assertDeviceEnabled(deviceId);
|
||||
if (loginUser != null) {
|
||||
androidDeviceBindingService.validatePrivateDeviceAccess(deviceId, loginUser.getTenantId(), loginUser.getUserId());
|
||||
return buildContext("USER_JWT", false,
|
||||
deviceId,
|
||||
appId,
|
||||
|
|
@ -68,6 +77,7 @@ public class AndroidAuthServiceImpl implements AndroidAuthService {
|
|||
|
||||
if (StringUtils.hasText(resolvedToken)) {
|
||||
InternalAuthCheckResponse authResult = validateToken(resolvedToken);
|
||||
androidDeviceBindingService.validatePrivateDeviceAccess(deviceId, authResult.getTenantId(), authResult.getUserId());
|
||||
return buildContext("USER_JWT", false,
|
||||
deviceId,
|
||||
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;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.imeeting.common.RedisKeys;
|
||||
import com.imeeting.config.grpc.GrpcServerProperties;
|
||||
import com.imeeting.dto.android.AndroidAuthContext;
|
||||
import com.imeeting.dto.android.AndroidDeviceSessionState;
|
||||
import com.imeeting.service.android.AndroidDeviceSessionService;
|
||||
import com.imeeting.support.redis.AndroidDeviceSessionCache;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
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 final StringRedisTemplate redisTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final AndroidDeviceSessionCache sessionCache;
|
||||
private final GrpcServerProperties grpcServerProperties;
|
||||
|
||||
@Override
|
||||
|
|
@ -61,63 +58,37 @@ public class AndroidDeviceSessionServiceImpl implements AndroidDeviceSessionServ
|
|||
|
||||
@Override
|
||||
public AndroidDeviceSessionState getByConnectionId(String connectionId) {
|
||||
String raw = redisTemplate.opsForValue().get(RedisKeys.androidDeviceConnectionKey(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;
|
||||
}
|
||||
return sessionCache.getByConnectionId(connectionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AndroidDeviceSessionState getByDeviceId(String deviceId) {
|
||||
String raw = redisTemplate.opsForValue().get(RedisKeys.androidDeviceOnlineKey(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;
|
||||
}
|
||||
return sessionCache.getByDeviceId(deviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getActiveConnectionId(String deviceId) {
|
||||
String value = redisTemplate.opsForValue().get(RedisKeys.androidDeviceActiveConnectionKey(deviceId));
|
||||
return value == null || value.isBlank() ? null : value;
|
||||
return sessionCache.getActiveConnectionId(deviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateTopics(String deviceId, List<String> topics) {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
sessionCache.saveTopics(deviceId, topics);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closeSession(String connectionId) {
|
||||
AndroidDeviceSessionState state = getByConnectionId(connectionId);
|
||||
if (state == null) {
|
||||
redisTemplate.delete(RedisKeys.androidDeviceConnectionKey(connectionId));
|
||||
sessionCache.deleteConnection(connectionId);
|
||||
return;
|
||||
}
|
||||
String activeConn = getActiveConnectionId(state.getDeviceId());
|
||||
if (connectionId.equals(activeConn)) {
|
||||
redisTemplate.delete(RedisKeys.androidDeviceActiveConnectionKey(state.getDeviceId()));
|
||||
redisTemplate.delete(RedisKeys.androidDeviceOnlineKey(state.getDeviceId()));
|
||||
sessionCache.deleteActiveConnection(state.getDeviceId());
|
||||
sessionCache.deleteOnlineState(state.getDeviceId());
|
||||
}
|
||||
redisTemplate.delete(RedisKeys.androidDeviceConnectionKey(connectionId));
|
||||
sessionCache.deleteConnection(connectionId);
|
||||
log.info(buildLog("gRPC会话关闭",
|
||||
"关闭Android设备会话,连接ID=" + connectionId,
|
||||
state.getDeviceId(),
|
||||
|
|
@ -127,14 +98,7 @@ public class AndroidDeviceSessionServiceImpl implements AndroidDeviceSessionServ
|
|||
|
||||
private void writeState(AndroidDeviceSessionState state) {
|
||||
Duration ttl = Duration.ofSeconds(grpcServerProperties.getGateway().getHeartbeatTimeoutSeconds());
|
||||
try {
|
||||
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);
|
||||
}
|
||||
sessionCache.saveState(state, ttl);
|
||||
}
|
||||
|
||||
private String nonBlank(String value, String defaultValue) {
|
||||
|
|
|
|||
|
|
@ -1,19 +1,17 @@
|
|||
package com.imeeting.service.android.impl;
|
||||
|
||||
import cn.hutool.json.JSON;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.imeeting.dto.biz.MeetingVO;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.enums.MeetingPushTypeEnum;
|
||||
import com.imeeting.grpc.push.PushMessage;
|
||||
|
||||
import com.imeeting.service.android.AndroidGatewayPushService;
|
||||
import com.imeeting.service.android.AndroidMeetingPushService;
|
||||
import com.imeeting.service.android.AndroidPushMessageService;
|
||||
import com.imeeting.service.biz.MeetingQueryService;
|
||||
import com.imeeting.service.biz.MeetingService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
|
|
@ -25,23 +23,50 @@ import java.util.UUID;
|
|||
|
||||
@Slf4j
|
||||
@Service
|
||||
|
||||
public class AndroidMeetingPushServiceImpl implements AndroidMeetingPushService {
|
||||
|
||||
private static final DateTimeFormatter TITLE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||
|
||||
@Autowired
|
||||
@Lazy
|
||||
private MeetingQueryService meetingService;
|
||||
private MeetingQueryService meetingQueryService;
|
||||
@Autowired
|
||||
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
|
||||
public void pushMeetingCompleted(Long meetingId) {
|
||||
if (meetingId == null) {
|
||||
return;
|
||||
}
|
||||
MeetingVO meeting = meetingService.getDetailIgnoreTenant(meetingId);
|
||||
MeetingVO meeting = meetingQueryService.getDetailIgnoreTenant(meetingId);
|
||||
if (meeting == null || meeting.getTenantId() == null || meeting.getCreatorId() == null) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -49,8 +74,8 @@ public class AndroidMeetingPushServiceImpl implements AndroidMeetingPushService
|
|||
.setMessageId("meeting_completed:" + meetingId + ":" + UUID.randomUUID())
|
||||
.setTimestamp(System.currentTimeMillis())
|
||||
.setType(MeetingPushTypeEnum.MEETING_COMPLETED.getCode())
|
||||
.setTitle(resolveTitle(meeting))
|
||||
.setContent(resolveContent(meeting))
|
||||
.setTitle(resolveCompletedTitle(meeting))
|
||||
.setContent(buildCompletedContent(meeting))
|
||||
.setNeedAck(false)
|
||||
.build();
|
||||
int pushed = androidGatewayPushService.pushToUser(meeting.getTenantId(), meeting.getCreatorId(), message);
|
||||
|
|
@ -58,8 +83,18 @@ public class AndroidMeetingPushServiceImpl implements AndroidMeetingPushService
|
|||
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();
|
||||
if (title != null && !title.isBlank()) {
|
||||
return "会议已完成: " + title.trim();
|
||||
|
|
@ -70,7 +105,18 @@ public class AndroidMeetingPushServiceImpl implements AndroidMeetingPushService
|
|||
: "会议已完成: " + TITLE_TIME_FORMATTER.format(meetingTime);
|
||||
}
|
||||
|
||||
private String resolveContent(MeetingVO meeting) {
|
||||
private String buildPendingContent(MeetingVO meeting) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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.LegacyUploadAudioResponse;
|
||||
import com.imeeting.dto.android.AndroidAuthContext;
|
||||
import com.imeeting.dto.biz.MeetingVO;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
|
@ -9,12 +10,18 @@ import org.springframework.web.multipart.MultipartFile;
|
|||
import java.io.IOException;
|
||||
|
||||
public interface LegacyMeetingAdapterService {
|
||||
MeetingVO createMeeting(LegacyMeetingCreateRequest request, LoginUser loginUser);
|
||||
MeetingVO createMeeting(LegacyMeetingCreateRequest request, AndroidAuthContext authContext, LoginUser loginUser);
|
||||
|
||||
LegacyUploadAudioResponse uploadAndTriggerOfflineProcess(Long meetingId,
|
||||
Long promptId,
|
||||
String modelCode,
|
||||
boolean forceReplace,
|
||||
MultipartFile audioFile,
|
||||
AndroidAuthContext authContext,
|
||||
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.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.LegacyUploadAudioResponse;
|
||||
import com.imeeting.dto.biz.MeetingVO;
|
||||
import com.imeeting.dto.biz.PublicDeviceMeetingCreateCommand;
|
||||
import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile;
|
||||
import com.imeeting.entity.biz.AiTask;
|
||||
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.MeetingTranscriptMapper;
|
||||
import com.imeeting.service.android.legacy.LegacyMeetingAdapterService;
|
||||
import com.imeeting.service.android.AndroidPendingMeetingDraftService;
|
||||
import com.imeeting.service.biz.AiTaskService;
|
||||
import com.imeeting.service.biz.MeetingAccessService;
|
||||
import com.imeeting.service.biz.MeetingRuntimeProfileResolver;
|
||||
|
|
@ -56,10 +60,11 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
|
|||
private final MeetingTranscriptMapper transcriptMapper;
|
||||
private final LlmModelMapper llmModelMapper;
|
||||
private final MeetingAudioUploadSupport meetingAudioUploadSupport;
|
||||
private final AndroidPendingMeetingDraftService androidPendingMeetingDraftService;
|
||||
|
||||
@Override
|
||||
@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()) {
|
||||
throw new RuntimeException("会议标题不能为空");
|
||||
}
|
||||
|
|
@ -82,7 +87,9 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
|
|||
loginUser.getUserId(),
|
||||
resolveCreatorName(loginUser),
|
||||
MeetingConstants.SUMMARY_DETAIL_STANDARD,
|
||||
0
|
||||
0,
|
||||
authContext == null ? null : authContext.getDeviceId(),
|
||||
MeetingConstants.DEVICE_MODE_PRIVATE
|
||||
);
|
||||
meetingService.save(meeting);
|
||||
|
||||
|
|
@ -98,6 +105,7 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
|
|||
String modelCode,
|
||||
boolean forceReplace,
|
||||
MultipartFile audioFile,
|
||||
AndroidAuthContext authContext,
|
||||
LoginUser loginUser) throws IOException {
|
||||
if (meetingId == null) {
|
||||
throw new RuntimeException("meeting_id 不能为空");
|
||||
|
|
@ -108,6 +116,7 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
|
|||
|
||||
Meeting meeting = meetingAccessService.requireMeeting(meetingId);
|
||||
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
||||
assertDeviceOwnsMeeting(meeting, authContext);
|
||||
|
||||
if (!forceReplace && meeting.getAudioUrl() != null && !meeting.getAudioUrl().isBlank()) {
|
||||
throw new RuntimeException("当前会议已存在音频,如需替换请设置 force_replace=true");
|
||||
|
|
@ -158,6 +167,69 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
|
|||
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) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return "";
|
||||
|
|
@ -249,22 +321,65 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
|
|||
}
|
||||
|
||||
private void resetOrCreateSummaryTask(Long meetingId, RealtimeMeetingRuntimeProfile profile) {
|
||||
AiTask task = findLatestTask(meetingId, "SUMMARY");
|
||||
Map<String, Object> taskConfig = meetingSummaryPromptAssembler.buildTaskConfig(
|
||||
resetOrCreateSummaryTask(meetingId, profile, null, null);
|
||||
}
|
||||
|
||||
private void resetOrCreateSummaryTask(Long meetingId, RealtimeMeetingRuntimeProfile profile, String userPrompt, String summaryDetailLevel) {
|
||||
resetOrCreateSummaryTask(
|
||||
meetingId,
|
||||
profile.getResolvedSummaryModelId(),
|
||||
profile.getResolvedSummaryModelId(),
|
||||
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);
|
||||
}
|
||||
|
||||
private void resetOrCreateChapterTask(Long meetingId, RealtimeMeetingRuntimeProfile profile) {
|
||||
AiTask task = findLatestTask(meetingId, "CHAPTER");
|
||||
Map<String, Object> taskConfig = meetingSummaryPromptAssembler.buildTaskConfig(
|
||||
resetOrCreateChapterTask(meetingId, profile, null, null);
|
||||
}
|
||||
|
||||
private void resetOrCreateChapterTask(Long meetingId, RealtimeMeetingRuntimeProfile profile, String userPrompt, String summaryDetailLevel) {
|
||||
resetOrCreateChapterTask(
|
||||
meetingId,
|
||||
profile.getResolvedSummaryModelId(),
|
||||
profile.getResolvedSummaryModelId(),
|
||||
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);
|
||||
}
|
||||
|
|
@ -315,4 +430,14 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
|
|||
private String resolveCreatorName(LoginUser loginUser) {
|
||||
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.MeetingTranscriptChapterImportResultVO;
|
||||
import com.imeeting.dto.biz.MeetingVO;
|
||||
import com.imeeting.dto.biz.PublicDeviceMeetingCreateCommand;
|
||||
import com.imeeting.dto.biz.RealtimeTranscriptItemDTO;
|
||||
import com.imeeting.dto.biz.UpdateMeetingBasicCommand;
|
||||
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 createPublicDeviceMeeting(PublicDeviceMeetingCreateCommand command,
|
||||
Long tenantId,
|
||||
Long creatorId,
|
||||
String creatorName,
|
||||
String deviceCode);
|
||||
|
||||
void deleteMeeting(Long id);
|
||||
|
||||
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.imeeting.common.MeetingProgressStage;
|
||||
import com.imeeting.common.RedisKeys;
|
||||
import com.imeeting.dto.biz.AiModelVO;
|
||||
import com.imeeting.dto.biz.MeetingSummarySource;
|
||||
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.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.mapper.SysUserMapper;
|
||||
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.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
|
@ -59,7 +57,6 @@ import java.time.Duration;
|
|||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
|
|
@ -72,7 +69,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
private final ObjectMapper objectMapper;
|
||||
private final SysUserMapper sysUserMapper;
|
||||
private final HotWordService hotWordService;
|
||||
private final RedisValueSupport redisValueSupport;
|
||||
private final MeetingLockCache meetingLockCache;
|
||||
private final MeetingProgressService meetingProgressService;
|
||||
private final MeetingSummaryFileService meetingSummaryFileService;
|
||||
private final MeetingTranscriptFileService meetingTranscriptFileService;
|
||||
|
|
@ -117,7 +114,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
ObjectMapper objectMapper,
|
||||
SysUserMapper sysUserMapper,
|
||||
HotWordService hotWordService,
|
||||
RedisValueSupport redisValueSupport,
|
||||
MeetingLockCache meetingLockCache,
|
||||
MeetingProgressService meetingProgressService,
|
||||
MeetingSummaryFileService meetingSummaryFileService,
|
||||
MeetingTranscriptFileService meetingTranscriptFileService,
|
||||
|
|
@ -133,7 +130,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
this.objectMapper = objectMapper;
|
||||
this.sysUserMapper = sysUserMapper;
|
||||
this.hotWordService = hotWordService;
|
||||
this.redisValueSupport = redisValueSupport;
|
||||
this.meetingLockCache = meetingLockCache;
|
||||
this.meetingProgressService = meetingProgressService;
|
||||
this.meetingSummaryFileService = meetingSummaryFileService;
|
||||
this.meetingTranscriptFileService = meetingTranscriptFileService;
|
||||
|
|
@ -145,41 +142,6 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
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
|
||||
@Async("asrDispatchExecutor")
|
||||
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) {
|
||||
String lockKey = RedisKeys.meetingPollingLockKey(meetingId);
|
||||
Boolean acquired = redisValueSupport.setIfAbsent(lockKey, "locked", 30, TimeUnit.MINUTES);
|
||||
if (Boolean.FALSE.equals(acquired)) {
|
||||
boolean acquired = meetingLockCache.tryAcquirePollingLock(meetingId, Duration.ofMinutes(30));
|
||||
if (!acquired) {
|
||||
log.warn("Meeting {} is already being processed", meetingId);
|
||||
return;
|
||||
}
|
||||
|
|
@ -293,7 +254,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
updateMeetingStatus(meetingId, 4);
|
||||
updateProgress(meetingId, -1, "分析失败: " + e.getMessage(), 0);
|
||||
} finally {
|
||||
redisValueSupport.delete(lockKey);
|
||||
meetingLockCache.releasePollingLock(meetingId);
|
||||
scheduleQueuedAsrTasks();
|
||||
}
|
||||
}
|
||||
|
|
@ -390,9 +351,8 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
if (queuedCount <= 0) {
|
||||
return;
|
||||
}
|
||||
String scheduleLockKey = RedisKeys.meetingAsrScheduleLockKey();
|
||||
Boolean acquired = redisValueSupport.setIfAbsent(scheduleLockKey, "locked", 30, TimeUnit.SECONDS);
|
||||
if (Boolean.FALSE.equals(acquired)) {
|
||||
boolean acquired = meetingLockCache.tryAcquireAsrScheduleLock(Duration.ofSeconds(30));
|
||||
if (!acquired) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
|
@ -425,7 +385,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
self.dispatchTasks(queuedMeeting.getId(), queuedMeeting.getTenantId(), queuedMeeting.getCreatorId());
|
||||
}
|
||||
} 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);
|
||||
return;
|
||||
}
|
||||
String summaryLockKey = RedisKeys.meetingSummaryLockKey(meeting.getId());
|
||||
Boolean acquired = redisValueSupport.setIfAbsent(summaryLockKey, "locked", 30, TimeUnit.MINUTES);
|
||||
if (Boolean.FALSE.equals(acquired)) {
|
||||
boolean acquired = meetingLockCache.tryAcquireSummaryLock(meeting.getId(), Duration.ofMinutes(30));
|
||||
if (!acquired) {
|
||||
log.warn("Meeting {} summary is already being processed", meeting.getId());
|
||||
return;
|
||||
}
|
||||
|
|
@ -1086,7 +1045,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
processSummaryTask(meeting, summarySource, sumTask);
|
||||
reconcileMeetingStatus(meeting.getId());
|
||||
} 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.imeeting.common.MeetingConstants;
|
||||
import com.imeeting.common.RedisKeys;
|
||||
import com.imeeting.dto.android.AndroidPendingMeetingDraft;
|
||||
import com.imeeting.dto.biz.CreateMeetingCommand;
|
||||
import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
|
||||
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.MeetingTranscriptSourceVO;
|
||||
import com.imeeting.dto.biz.MeetingVO;
|
||||
import com.imeeting.dto.biz.PublicDeviceMeetingCreateCommand;
|
||||
import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile;
|
||||
import com.imeeting.dto.biz.RealtimeMeetingResumeConfig;
|
||||
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.MeetingTranscriptChapterVersion;
|
||||
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.HotWordService;
|
||||
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.MeetingTranscriptRevisionService;
|
||||
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
||||
import com.imeeting.service.biz.impl.RedisOnlyMeetingProgressServiceAdapter;
|
||||
import com.imeeting.service.realtime.RealtimeMeetingAudioStorageService;
|
||||
import com.imeeting.support.redis.MeetingAsrPermitCache;
|
||||
import com.imeeting.support.redis.MeetingLockCache;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
|
|
@ -77,7 +81,10 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
private final ObjectMapper objectMapper;
|
||||
private final MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger;
|
||||
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}")
|
||||
private String summaryOrchestrationMode;
|
||||
|
|
@ -98,7 +105,11 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
MeetingProgressService meetingProgressService,
|
||||
ObjectMapper objectMapper,
|
||||
MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger,
|
||||
AndroidMeetingPushService androidMeetingPushService) {
|
||||
AndroidMeetingPushService androidMeetingPushService,
|
||||
AndroidPushMessageService androidPushMessageService,
|
||||
AndroidPendingMeetingDraftService androidPendingMeetingDraftService,
|
||||
MeetingLockCache meetingLockCache,
|
||||
MeetingAsrPermitCache meetingAsrPermitCache) {
|
||||
this.meetingService = meetingService;
|
||||
this.aiTaskService = aiTaskService;
|
||||
this.hotWordService = hotWordService;
|
||||
|
|
@ -115,43 +126,10 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
this.objectMapper = objectMapper;
|
||||
this.meetingExternalSummaryWebhookTrigger = meetingExternalSummaryWebhookTrigger;
|
||||
this.androidMeetingPushService = androidMeetingPushService;
|
||||
}
|
||||
|
||||
public MeetingCommandServiceImpl(MeetingService meetingService,
|
||||
AiTaskService aiTaskService,
|
||||
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;
|
||||
this.androidPushMessageService = androidPushMessageService;
|
||||
this.androidPendingMeetingDraftService = androidPendingMeetingDraftService;
|
||||
this.meetingLockCache = meetingLockCache;
|
||||
this.meetingAsrPermitCache = meetingAsrPermitCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -273,6 +251,65 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
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
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteMeeting(Long id) {
|
||||
|
|
@ -283,6 +320,8 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
meetingService.removeById(id);
|
||||
realtimeMeetingSessionStateService.clear(id);
|
||||
meetingProgressService.clear(id);
|
||||
androidPushMessageService.markCancelledByMeeting(id);
|
||||
androidPendingMeetingDraftService.clear(id);
|
||||
deleteMeetingArtifactsAfterCommit(id);
|
||||
}
|
||||
|
||||
|
|
@ -1073,12 +1112,11 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
}
|
||||
|
||||
private void clearLegacyDispatchState(Long meetingId) {
|
||||
if (compatibilityRedisTemplate == null || meetingId == null) {
|
||||
if (meetingId == null) {
|
||||
return;
|
||||
}
|
||||
compatibilityRedisTemplate.delete(RedisKeys.meetingPollingLockKey(meetingId));
|
||||
compatibilityRedisTemplate.delete(RedisKeys.meetingSummaryLockKey(meetingId));
|
||||
compatibilityRedisTemplate.opsForSet().remove(RedisKeys.meetingAsrPermitSetKey(), String.valueOf(meetingId));
|
||||
meetingLockCache.clearDispatchLocks(meetingId);
|
||||
meetingAsrPermitCache.removePermit(meetingId);
|
||||
}
|
||||
|
||||
private void ensureExternalSummaryModeEnabled() {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,16 @@ public class MeetingDomainSupport {
|
|||
String audioUrl, String meetingType, String meetingSource,
|
||||
Long tenantId, Long creatorId, String creatorName,
|
||||
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.setTitle(title);
|
||||
meeting.setMeetingTime(meetingTime);
|
||||
|
|
@ -67,6 +77,8 @@ public class MeetingDomainSupport {
|
|||
meeting.setHostName(hostName);
|
||||
meeting.setTenantId(tenantId != null ? tenantId : 0L);
|
||||
meeting.setAudioUrl(audioUrl);
|
||||
meeting.setSourceDeviceCode(sourceDeviceCode);
|
||||
meeting.setSourceDeviceMode(sourceDeviceMode);
|
||||
meeting.setSummaryDetailLevel(normalizeSummaryDetailLevel(summaryDetailLevel));
|
||||
meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_NONE);
|
||||
meeting.setStatus(status);
|
||||
|
|
@ -365,6 +377,8 @@ public class MeetingDomainSupport {
|
|||
}
|
||||
vo.setMeetingType(meeting.getMeetingType());
|
||||
vo.setMeetingSource(meeting.getMeetingSource());
|
||||
vo.setSourceDeviceCode(meeting.getSourceDeviceCode());
|
||||
vo.setSourceDeviceMode(meeting.getSourceDeviceMode());
|
||||
vo.setSummaryDetailLevel(normalizeSummaryDetailLevel(meeting.getSummaryDetailLevel()));
|
||||
vo.setAudioSaveStatus(meeting.getAudioSaveStatus());
|
||||
vo.setAudioSaveMessage(meeting.getAudioSaveMessage());
|
||||
|
|
|
|||
|
|
@ -3,14 +3,13 @@ package com.imeeting.service.biz.impl;
|
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
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.entity.biz.Meeting;
|
||||
import com.imeeting.mapper.biz.AiTaskMapper;
|
||||
import com.imeeting.mapper.biz.MeetingMapper;
|
||||
import com.imeeting.service.biz.MeetingProgressService;
|
||||
import com.imeeting.support.RedisValueSupport;
|
||||
import com.imeeting.support.redis.MeetingProgressCache;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
|
|
@ -21,17 +20,14 @@ import java.util.HashMap;
|
|||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MeetingProgressServiceImpl implements MeetingProgressService {
|
||||
|
||||
private static final long PROGRESS_TTL_HOURS = 1L;
|
||||
|
||||
private final MeetingMapper meetingMapper;
|
||||
private final AiTaskMapper aiTaskMapper;
|
||||
private final RedisValueSupport redisValueSupport;
|
||||
private final MeetingProgressCache meetingProgressCache;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
|
|
@ -39,12 +35,12 @@ public class MeetingProgressServiceImpl implements MeetingProgressService {
|
|||
if (meetingId == null) {
|
||||
return;
|
||||
}
|
||||
redisValueSupport.delete(RedisKeys.meetingProgressKey(meetingId));
|
||||
meetingProgressCache.clear(meetingId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getProgressMap(Long meetingId) {
|
||||
MeetingProgressSnapshot snapshot = redisValueSupport.getJson(RedisKeys.meetingProgressKey(meetingId), MeetingProgressSnapshot.class);
|
||||
MeetingProgressSnapshot snapshot = meetingProgressCache.getSnapshot(meetingId);
|
||||
if (snapshot == null) {
|
||||
snapshot = buildFallbackSnapshot(meetingId);
|
||||
if (snapshot != null) {
|
||||
|
|
@ -74,7 +70,7 @@ public class MeetingProgressServiceImpl implements MeetingProgressService {
|
|||
|
||||
@Override
|
||||
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) {
|
||||
return snapshot.getPercent();
|
||||
}
|
||||
|
|
@ -115,11 +111,11 @@ public class MeetingProgressServiceImpl implements MeetingProgressService {
|
|||
if (snapshot == null || snapshot.getMeetingId() == null) {
|
||||
return;
|
||||
}
|
||||
MeetingProgressSnapshot existing = redisValueSupport.getJson(RedisKeys.meetingProgressKey(snapshot.getMeetingId()), MeetingProgressSnapshot.class);
|
||||
MeetingProgressSnapshot existing = meetingProgressCache.getSnapshot(snapshot.getMeetingId());
|
||||
if (!shouldReplace(existing, snapshot)) {
|
||||
return;
|
||||
}
|
||||
redisValueSupport.setJson(RedisKeys.meetingProgressKey(snapshot.getMeetingId()), snapshot, PROGRESS_TTL_HOURS, TimeUnit.HOURS);
|
||||
meetingProgressCache.saveSnapshot(snapshot);
|
||||
}
|
||||
|
||||
private MeetingProgressSnapshot buildSnapshot(Long meetingId,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package com.imeeting.service.biz.impl;
|
||||
|
||||
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.RealtimeMeetingSessionState;
|
||||
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.MeetingTranscriptMapper;
|
||||
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
||||
import com.imeeting.support.redis.MeetingLockCache;
|
||||
import com.imeeting.support.redis.RealtimeMeetingSessionCache;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
|
|
@ -27,8 +26,8 @@ import java.util.Map;
|
|||
@RequiredArgsConstructor
|
||||
public class RealtimeMeetingSessionStateServiceImpl implements RealtimeMeetingSessionStateService {
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final RealtimeMeetingSessionCache sessionCache;
|
||||
private final MeetingLockCache meetingLockCache;
|
||||
private final MeetingTranscriptMapper transcriptMapper;
|
||||
private final MeetingMapper meetingMapper;
|
||||
|
||||
|
|
@ -113,8 +112,8 @@ public class RealtimeMeetingSessionStateServiceImpl implements RealtimeMeetingSe
|
|||
state.setUpdatedAt(now);
|
||||
writeState(state);
|
||||
|
||||
redisTemplate.delete(RedisKeys.realtimeMeetingResumeTimeoutKey(meetingId));
|
||||
redisTemplate.delete(RedisKeys.realtimeMeetingEmptyTimeoutKey(meetingId));
|
||||
sessionCache.clearResumeTimeout(meetingId);
|
||||
sessionCache.clearEmptyTimeout(meetingId);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -194,8 +193,8 @@ public class RealtimeMeetingSessionStateServiceImpl implements RealtimeMeetingSe
|
|||
if ("PAUSED_EMPTY".equals(state.getStatus()) || "PAUSED_RESUMABLE".equals(state.getStatus())) {
|
||||
state.setStatus("PAUSED_RESUMABLE");
|
||||
state.setResumeExpireAt(now + Duration.ofMinutes(getResumeWindowMinutes()).toMillis());
|
||||
ensureTimeoutKey(meetingId, RedisKeys.realtimeMeetingResumeTimeoutKey(meetingId), Duration.ofMinutes(getResumeWindowMinutes()));
|
||||
redisTemplate.delete(RedisKeys.realtimeMeetingEmptyTimeoutKey(meetingId));
|
||||
sessionCache.saveResumeTimeout(meetingId, Duration.ofMinutes(getResumeWindowMinutes()));
|
||||
sessionCache.clearEmptyTimeout(meetingId);
|
||||
}
|
||||
|
||||
writeState(state);
|
||||
|
|
@ -203,9 +202,8 @@ public class RealtimeMeetingSessionStateServiceImpl implements RealtimeMeetingSe
|
|||
|
||||
@Override
|
||||
public boolean markCompletingIfResumeExpired(Long meetingId) {
|
||||
String lockKey = RedisKeys.realtimeMeetingTimeoutLockKey(meetingId);
|
||||
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", Duration.ofMinutes(1));
|
||||
if (Boolean.FALSE.equals(locked)) {
|
||||
boolean locked = meetingLockCache.tryAcquireRealtimeTimeoutLock(meetingId, Duration.ofMinutes(1));
|
||||
if (!locked) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -227,7 +225,7 @@ public class RealtimeMeetingSessionStateServiceImpl implements RealtimeMeetingSe
|
|||
state.setLastTranscriptAt(now);
|
||||
state.setResumeExpireAt(now + Duration.ofMinutes(getResumeWindowMinutes()).toMillis());
|
||||
writeState(state);
|
||||
ensureTimeoutKey(meetingId, RedisKeys.realtimeMeetingResumeTimeoutKey(meetingId), Duration.ofMinutes(getResumeWindowMinutes()));
|
||||
sessionCache.saveResumeTimeout(meetingId, Duration.ofMinutes(getResumeWindowMinutes()));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -236,7 +234,7 @@ public class RealtimeMeetingSessionStateServiceImpl implements RealtimeMeetingSe
|
|||
writeState(state);
|
||||
return true;
|
||||
} finally {
|
||||
redisTemplate.delete(lockKey);
|
||||
meetingLockCache.releaseRealtimeTimeoutLock(meetingId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -253,10 +251,7 @@ public class RealtimeMeetingSessionStateServiceImpl implements RealtimeMeetingSe
|
|||
|
||||
@Override
|
||||
public void clear(Long meetingId) {
|
||||
redisTemplate.delete(RedisKeys.realtimeMeetingSessionStateKey(meetingId));
|
||||
redisTemplate.delete(RedisKeys.realtimeMeetingResumeTimeoutKey(meetingId));
|
||||
redisTemplate.delete(RedisKeys.realtimeMeetingEmptyTimeoutKey(meetingId));
|
||||
redisTemplate.delete(RedisKeys.realtimeMeetingEventSeqKey(meetingId));
|
||||
sessionCache.clearAll(meetingId);
|
||||
}
|
||||
|
||||
private RealtimeMeetingSessionStatusVO pauseState(Long meetingId, RealtimeMeetingSessionState state) {
|
||||
|
|
@ -272,16 +267,16 @@ public class RealtimeMeetingSessionStateServiceImpl implements RealtimeMeetingSe
|
|||
if (transcriptCount > 0) {
|
||||
state.setStatus("PAUSED_RESUMABLE");
|
||||
state.setResumeExpireAt(now + Duration.ofMinutes(getResumeWindowMinutes()).toMillis());
|
||||
ensureTimeoutKey(meetingId, RedisKeys.realtimeMeetingResumeTimeoutKey(meetingId), Duration.ofMinutes(getResumeWindowMinutes()));
|
||||
redisTemplate.delete(RedisKeys.realtimeMeetingEmptyTimeoutKey(meetingId));
|
||||
sessionCache.saveResumeTimeout(meetingId, Duration.ofMinutes(getResumeWindowMinutes()));
|
||||
sessionCache.clearEmptyTimeout(meetingId);
|
||||
if (state.getLastTranscriptAt() == null) {
|
||||
state.setLastTranscriptAt(now);
|
||||
}
|
||||
} else {
|
||||
state.setStatus("PAUSED_EMPTY");
|
||||
state.setResumeExpireAt(null);
|
||||
redisTemplate.delete(RedisKeys.realtimeMeetingResumeTimeoutKey(meetingId));
|
||||
ensureTimeoutKey(meetingId, RedisKeys.realtimeMeetingEmptyTimeoutKey(meetingId), Duration.ofMinutes(getEmptySessionRetentionMinutes()));
|
||||
sessionCache.clearResumeTimeout(meetingId);
|
||||
sessionCache.saveEmptyTimeout(meetingId, Duration.ofMinutes(getEmptySessionRetentionMinutes()));
|
||||
}
|
||||
|
||||
writeState(state);
|
||||
|
|
@ -372,31 +367,11 @@ public class RealtimeMeetingSessionStateServiceImpl implements RealtimeMeetingSe
|
|||
}
|
||||
|
||||
private RealtimeMeetingSessionState readState(Long meetingId) {
|
||||
String raw = redisTemplate.opsForValue().get(RedisKeys.realtimeMeetingSessionStateKey(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;
|
||||
}
|
||||
return sessionCache.getState(meetingId);
|
||||
}
|
||||
|
||||
private void writeState(RealtimeMeetingSessionState state) {
|
||||
try {
|
||||
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);
|
||||
sessionCache.saveState(state);
|
||||
}
|
||||
|
||||
private long getResumeWindowMinutes() {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package com.imeeting.service.biz.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.imeeting.common.MeetingConstants;
|
||||
import com.imeeting.common.RedisKeys;
|
||||
import com.imeeting.dto.biz.AiModelVO;
|
||||
import com.imeeting.dto.biz.RealtimeMeetingResumeConfig;
|
||||
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.RealtimeMeetingSessionStateService;
|
||||
import com.imeeting.service.biz.RealtimeMeetingSocketSessionService;
|
||||
import com.imeeting.support.redis.RealtimeMeetingSocketSessionCache;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
|
@ -27,11 +24,9 @@ import java.util.UUID;
|
|||
@RequiredArgsConstructor
|
||||
public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingSocketSessionService {
|
||||
|
||||
private static final Duration SESSION_TTL = Duration.ofMinutes(10);
|
||||
private static final String WS_PATH = "/ws/meeting/realtime";
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
private final RealtimeMeetingSocketSessionCache socketSessionCache;
|
||||
private final MeetingAccessService meetingAccessService;
|
||||
private final AiModelService aiModelService;
|
||||
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
|
||||
|
|
@ -90,20 +85,12 @@ public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingS
|
|||
sessionData.setTargetWsUrl(targetWsUrl);
|
||||
|
||||
String sessionToken = UUID.randomUUID().toString().replace("-", "");
|
||||
try {
|
||||
redisTemplate.opsForValue().set(
|
||||
RedisKeys.realtimeMeetingSocketSessionKey(sessionToken),
|
||||
objectMapper.writeValueAsString(sessionData),
|
||||
SESSION_TTL
|
||||
);
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException("创建实时 Socket 会话失败", ex);
|
||||
}
|
||||
socketSessionCache.save(sessionToken, sessionData);
|
||||
|
||||
RealtimeSocketSessionVO vo = new RealtimeSocketSessionVO();
|
||||
vo.setSessionToken(sessionToken);
|
||||
vo.setPath(WS_PATH);
|
||||
vo.setExpiresInSeconds(SESSION_TTL.toSeconds());
|
||||
vo.setExpiresInSeconds(socketSessionCache.getSessionTtlSeconds());
|
||||
vo.setStartMessage(buildStartMessage(
|
||||
asrModel,
|
||||
meetingId,
|
||||
|
|
@ -121,18 +108,7 @@ public class RealtimeMeetingSocketSessionServiceImpl implements RealtimeMeetingS
|
|||
|
||||
@Override
|
||||
public RealtimeSocketSessionData getSessionData(String sessionToken) {
|
||||
if (sessionToken == null || sessionToken.isBlank()) {
|
||||
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);
|
||||
}
|
||||
return socketSessionCache.get(sessionToken);
|
||||
}
|
||||
|
||||
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.MeetingTranscriptFileService;
|
||||
import com.imeeting.service.biz.PromptTemplateService;
|
||||
import com.imeeting.service.biz.impl.RedisOnlyMeetingProgressServiceAdapter;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.entity.SysUser;
|
||||
import com.unisbase.mapper.SysUserMapper;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
|
@ -87,30 +85,6 @@ public class MeetingMcpToolService {
|
|||
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:}")
|
||||
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;
|
||||
realtimeEnabled: boolean;
|
||||
offlineAudioMaxSizeMb: number;
|
||||
chunkUploadEnabled?: boolean;
|
||||
chunkDurationSeconds?: number;
|
||||
}
|
||||
|
||||
export interface MeetingVO {
|
||||
|
|
@ -28,6 +30,8 @@ export interface MeetingVO {
|
|||
playbackAudioUrl?: string;
|
||||
meetingType?: "OFFLINE" | "REALTIME";
|
||||
meetingSource?: "WEB" | "ANDROID";
|
||||
sourceDeviceCode?: string;
|
||||
sourceDeviceMode?: "PUBLIC" | "PRIVATE";
|
||||
summaryDetailLevel?: SummaryDetailLevel;
|
||||
audioSaveStatus?: "NONE" | "SUCCESS" | "FAILED";
|
||||
audioSaveMessage?: string;
|
||||
|
|
@ -95,6 +99,26 @@ export interface CreateMeetingCommand {
|
|||
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 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 {
|
||||
speakerId?: string;
|
||||
speakerName?: string;
|
||||
|
|
|
|||
|
|
@ -101,7 +101,9 @@ export default function Login() {
|
|||
}
|
||||
|
||||
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 {
|
||||
if (captchaEnabled) {
|
||||
await loadCaptcha();
|
||||
|
|
|
|||
|
|
@ -239,8 +239,9 @@ const MeetingCardItem: React.FC<{
|
|||
progress: MeetingProgress | null;
|
||||
onOpenMeeting: (meeting: MeetingVO) => void;
|
||||
onRetrySchedule: (meeting: MeetingVO) => void;
|
||||
onDelete: (id: number) => void;
|
||||
retrying: boolean;
|
||||
}> = ({ item, config, progress, onOpenMeeting, onRetrySchedule, retrying }) => {
|
||||
}> = ({ item, config, progress, onOpenMeeting, onRetrySchedule, onDelete, retrying }) => {
|
||||
const effectiveStatus = getEffectiveStatus(item, progress);
|
||||
const isProcessing = shouldTrackGenerationProgress(item);
|
||||
const isPaused = effectiveStatus === PAUSED_DISPLAY_STATUS;
|
||||
|
|
@ -310,7 +311,7 @@ const MeetingCardItem: React.FC<{
|
|||
<Popconfirm
|
||||
title="确定删除会议吗?"
|
||||
description="删除后将无法找回该会议记录。"
|
||||
onConfirm={() => deleteMeeting(item.id).then(fetchData)}
|
||||
onConfirm={() => onDelete(item.id)}
|
||||
okText="删除"
|
||||
cancelText="取消"
|
||||
okButtonProps={{ danger: true }}
|
||||
|
|
@ -905,6 +906,7 @@ const Meetings: React.FC = () => {
|
|||
progress={progress}
|
||||
onOpenMeeting={handleOpenMeeting}
|
||||
onRetrySchedule={(meeting) => { void handleRetrySchedule(meeting); }}
|
||||
onDelete={(id) => { deleteMeeting(id).then(() => { message.success('删除成功'); fetchData(); }) }}
|
||||
retrying={!!retryingMeetingIds[item.id]}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 { useAuth } from "@/hooks/useAuth";
|
||||
import { menuRoutes,extraRoutes } from "./routes";
|
||||
|
|
@ -7,6 +7,7 @@ import { menuRoutes,extraRoutes } from "./routes";
|
|||
const Login = lazy(() => import("@/pages/auth/login"));
|
||||
const ResetPassword = lazy(() => import("@/pages/auth/reset-password"));
|
||||
const MeetingPreview = lazy(() => import("@/pages/business/MeetingPreview"));
|
||||
const PublicDeviceMeetingCreate = lazy(() => import("@/pages/business/PublicDeviceMeetingCreate"));
|
||||
|
||||
function RouteFallback() {
|
||||
let platformName = "iMeeting";
|
||||
|
|
@ -50,9 +51,11 @@ function RouteFallback() {
|
|||
|
||||
function RequireAuth({ children }: { children: JSX.Element }) {
|
||||
const { isAuthed, profile } = useAuth();
|
||||
const location = useLocation();
|
||||
|
||||
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) {
|
||||
|
|
@ -72,6 +75,14 @@ export default function AppRoutes() {
|
|||
path="/meetings/:id/preview"
|
||||
element={<MeetingPreview />}
|
||||
/>
|
||||
<Route
|
||||
path="/public-device-meetings/:sessionId/create"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<PublicDeviceMeetingCreate />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
|
|
|
|||
Loading…
Reference in New Issue