imeeting/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java

586 lines
30 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package com.imeeting.controller.android;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.imeeting.common.MeetingConstants;
import com.imeeting.common.SysParamKeys;
import com.imeeting.dto.android.AndroidAuthContext;
import com.imeeting.dto.android.AndroidOfflineMeetingCreateCommand;
import com.imeeting.dto.android.AndroidMeetingCreateResponse;
import com.imeeting.dto.android.AndroidMeetingConfigVo;
import com.imeeting.dto.android.AndroidMeetingListItemVO;
import com.imeeting.dto.android.AndroidOfflineMeetingConflictVO;
import com.imeeting.dto.android.AndroidOfflineMeetingFinishRequest;
import com.imeeting.dto.android.AndroidUnifiedMeetingStatusRequest;
import com.imeeting.dto.android.AndroidUnifiedMeetingStatusResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingAccessPasswordRequest;
import com.imeeting.dto.android.legacy.LegacyMeetingAttendeeResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingPreviewDataResponse;
import com.imeeting.dto.android.legacy.LegacyMeetingPreviewResult;
import com.imeeting.dto.android.legacy.LegacyMeetingProcessingStatusResponse;
import com.imeeting.dto.android.legacy.LegacyUploadAudioResponse;
import com.imeeting.dto.biz.*;
import com.imeeting.entity.biz.AiTask;
import com.imeeting.entity.biz.Meeting;
import com.imeeting.entity.biz.PromptTemplate;
import com.imeeting.enums.BusinessErrorCodeEnum;
import com.imeeting.enums.MeetingStatusEnum;
import com.imeeting.service.android.AndroidAuthService;
import com.imeeting.service.android.AndroidChunkUploadService;
import com.imeeting.service.android.AndroidGatewayPushService;
import com.imeeting.service.android.AndroidMeetingPushService;
import com.imeeting.service.android.legacy.LegacyMeetingAdapterService;
import com.imeeting.support.AndroidRequestLogHelper;
import com.imeeting.service.biz.*;
import com.unisbase.annotation.Anonymous;
import com.unisbase.common.ApiResponse;
import com.unisbase.common.annotation.Log;
import com.unisbase.common.exception.BusinessException;
import com.unisbase.dto.PageResult;
import com.unisbase.entity.SysTenant;
import com.unisbase.entity.SysUser;
import com.unisbase.mapper.SysTenantMapper;
import com.unisbase.mapper.SysUserMapper;
import com.unisbase.security.LoginUser;
import com.unisbase.service.SysDictItemService;
import com.unisbase.service.SysParamService;
import com.unisbase.service.impl.SysDictItemServiceImpl;
import io.swagger.v3.oas.annotations.media.ArraySchema;
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 lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.StopWatch;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
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;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@Tag(name = "Android会议接口")
@RestController
@RequestMapping("/api/android/meetings")
@Slf4j
public class AndroidMeetingController {
private static final String STAGE_DATA_INITIALIZATION = "data_initialization";
private static final String STAGE_AUDIO_TRANSCRIPTION = "audio_transcription";
private static final String STAGE_SUMMARY_GENERATION = "summary_generation";
private static final String STAGE_COMPLETED = "completed";
private static final String TENANT_CODE_HEADER = "X-Tenant-Code";
@Value("${imeeting.h5.base-url:}")
private String h5BaseUrl;
private final AndroidAuthService androidAuthService;
private final AndroidMeetingPushService androidMeetingPushService;
private final AndroidChunkUploadService androidChunkUploadService;
private final LegacyMeetingAdapterService legacyMeetingAdapterService;
private final MeetingQueryService meetingQueryService;
private final MeetingAccessService meetingAccessService;
private final MeetingCommandService meetingCommandService;
private final MeetingService meetingService;
private final AiTaskService aiTaskService;
private final PromptTemplateService promptTemplateService;
private final SysTenantMapper sysTenantMapper;
private final SysUserMapper sysUserMapper;
private final AiModelService aiModelService;
private final HotWordGroupService hotWordGroupService;
private final SysDictItemService dictItemService;
private final SysParamService paramService;
private final MeetingProgressService meetingProgressService;
private final MeetingUnifiedStatusService meetingUnifiedStatusService;
@Autowired
public AndroidMeetingController(AndroidAuthService androidAuthService,
AndroidChunkUploadService androidChunkUploadService,
LegacyMeetingAdapterService legacyMeetingAdapterService,
MeetingQueryService meetingQueryService,
MeetingAccessService meetingAccessService,
MeetingCommandService meetingCommandService,
MeetingService meetingService,
AiTaskService aiTaskService,
PromptTemplateService promptTemplateService,
SysTenantMapper sysTenantMapper,
SysUserMapper sysUserMapper,
AiModelService aiModelService,
HotWordGroupService hotWordGroupService,
SysDictItemService dictItemService,
SysParamService paramService,
MeetingProgressService meetingProgressService,
AndroidMeetingPushService androidMeetingPushService,
MeetingUnifiedStatusService meetingUnifiedStatusService) {
this.androidAuthService = androidAuthService;
this.androidChunkUploadService = androidChunkUploadService;
this.legacyMeetingAdapterService = legacyMeetingAdapterService;
this.meetingQueryService = meetingQueryService;
this.meetingAccessService = meetingAccessService;
this.meetingCommandService = meetingCommandService;
this.meetingService = meetingService;
this.aiTaskService = aiTaskService;
this.promptTemplateService = promptTemplateService;
this.sysTenantMapper = sysTenantMapper;
this.sysUserMapper = sysUserMapper;
this.meetingProgressService = meetingProgressService;
this.aiModelService = aiModelService;
this.hotWordGroupService = hotWordGroupService;
this.paramService = paramService;
this.dictItemService = dictItemService;
this.meetingUnifiedStatusService = meetingUnifiedStatusService;
this.androidMeetingPushService = androidMeetingPushService;
}
@Operation(summary = "创建Android离线会议")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "返回新创建的会议详情",
content = @Content(schema = @Schema(implementation = AndroidMeetingCreateResponse.class))
)
})
@PostMapping("/create")
@Anonymous
@Log(value = "新增Android会议", type = "Android会议管理")
public ApiResponse<Object> create(HttpServletRequest request,
@RequestBody AndroidOfflineMeetingCreateCommand command) {
AndroidRequestLogHelper.logRequest(log, "Android会议", "创建离线会议接口", "request", command);
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
resolvePublicDeviceTenantId(request, command, authContext);
LoginUser loginUser = authContext.isAnonymous() ? null : AndroidLoginUserSupport.requireLoginUser(authContext);
// Meeting existingMeeting = findLatestUnfinishedMeetingByDevice(authContext.getDeviceId());
// if (existingMeeting != null) {
// return new ApiResponse<>("409", "设备端已有会议", meetingQueryService.getDetailIgnoreTenant(existingMeeting.getId()));
// }
MeetingVO meeting = legacyMeetingAdapterService.createMeeting(command, authContext, loginUser);
return ApiResponse.ok(buildAndroidMeetingCreateResponse(meeting));
}
@Operation(summary = "上传Android会议音频")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "返回上传后的会议 ID 和音频地址",
content = @Content(schema = @Schema(implementation = LegacyUploadAudioResponse.class))
)
})
@PostMapping("/upload-audio")
@Anonymous
public ApiResponse<LegacyUploadAudioResponse> uploadAudio(HttpServletRequest request,
@RequestParam("id") Long meetingId,
@RequestParam(value = "prompt_id", required = false) Long promptId,
@RequestParam(value = "model_code", required = false) String modelCode,
@RequestParam(value = "force_replace", defaultValue = "false") boolean forceReplace,
@RequestParam("audio_file") MultipartFile audioFile) throws IOException {
AndroidRequestLogHelper.logRequest(log, "Android会议", "上传会议音频接口",
"meetingId", meetingId,
"promptId", promptId,
"modelCode", modelCode,
"forceReplace", forceReplace,
"audioFile", audioFile);
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
if (authContext.isAnonymous()) {
return ApiResponse.ok(legacyMeetingAdapterService.uploadAndTriggerOfflineProcessForPublicDevice(
meetingId,
promptId,
modelCode,
forceReplace,
audioFile,
authContext
));
}
LoginUser loginUser = AndroidLoginUserSupport.requireLoginUser(authContext);
return ApiResponse.ok(legacyMeetingAdapterService.uploadAndTriggerOfflineProcess(
meetingId,
promptId,
modelCode,
forceReplace,
audioFile,
authContext,
loginUser
));
}
@Operation(summary = "结束 Android 离线会议录音阶段")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "结束成功返回 true",
content = @Content(schema = @Schema(implementation = Boolean.class))
)
})
@PostMapping("/{meetingId}/finish")
@Anonymous
public ApiResponse<Object> finishOfflineMeeting(HttpServletRequest request,
@PathVariable Long meetingId,
@RequestBody(required = false) AndroidOfflineMeetingFinishRequest command) throws IOException {
AndroidRequestLogHelper.logRequest(log, "Android会议", "结束离线会议录音阶段",
"meetingId", meetingId,
"request", command);
AndroidAuthContext authContext = androidAuthService.authenticateHttpIgnoreToken(request, true);
LoginUser loginUser = authContext.isAnonymous() ? null : AndroidLoginUserSupport.requireLoginUser(authContext);
requireOperableOfflineMeeting(meetingId, authContext, loginUser);
MeetingVO meeting = meetingQueryService.getDetailIgnoreTenant(meetingId);
LegacyUploadAudioResponse uploadResult = new LegacyUploadAudioResponse();
if (isUploadFinishedStage(command)) {
androidChunkUploadService.completeUploadAsync(
meeting.getId(),
command.getTotalChunks(),
authContext
);
// if (uploadResult == null) {
// throw new RuntimeException("分片上传完成后未生成结果");
// }
uploadResult.setMeetingId(meetingId);
}
meetingCommandService.finishOfflineMeeting(meeting.getId(), command == null ? null : command.getFinishStage());
return ApiResponse.ok(uploadResult);
}
@Operation(summary = "分页查询Android会议")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "返回当前用户可见的会议分页结果",
content = @Content(schema = @Schema(implementation = PageResult.class))
)
})
@GetMapping
public ApiResponse<PageResult<List<AndroidMeetingListItemVO>>> list(HttpServletRequest request,
@RequestParam(value = "user_id", required = false) Long ignoredUserId,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(value = "page_size", defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String title) {
AndroidRequestLogHelper.logRequest(log, "Android会议", "分页查询会议接口",
"userId", ignoredUserId,
"page", page,
"pageSize", pageSize,
"title", title);
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
LoginUser loginUser = AndroidLoginUserSupport.requireLoginUser(authContext);
PageResult<List<MeetingVO>> result = meetingQueryService.pageMeetings(
page,
pageSize,
title,
loginUser.getTenantId(),
loginUser.getUserId(),
AndroidLoginUserSupport.resolveDisplayName(authContext),
"all",
null,
AndroidLoginUserSupport.isAdmin(authContext)
);
return ApiResponse.ok(buildAndroidMeetingListPage(result));
}
@Operation(summary = "查询Android会议统一状态")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "返回统一状态及可选内容",
content = @Content(schema = @Schema(implementation = AndroidUnifiedMeetingStatusResponse.class))
)
})
@PostMapping("/{meetingId}/status")
@Anonymous
public ApiResponse<AndroidUnifiedMeetingStatusResponse> getUnifiedStatus(HttpServletRequest request,
@PathVariable Long meetingId,
@RequestBody(required = false) AndroidUnifiedMeetingStatusRequest command) {
AndroidRequestLogHelper.logRequest(log, "Android会议", "查询会议统一状态",
"meetingId", meetingId,
"request", command);
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
LoginUser loginUser = authContext.isAnonymous() ? null : AndroidLoginUserSupport.requireLoginUser(authContext);
requireOperableOfflineMeeting(meetingId, authContext, loginUser);
MeetingVO meeting = meetingQueryService.getDetailIgnoreTenant(meetingId,false);
UnifiedMeetingStatusVO status = meetingUnifiedStatusService.resolve(meetingId);
boolean includeTranscript = Boolean.TRUE.equals(command == null ? null : command.getIncludeTranscript());
boolean includeSummary = Boolean.TRUE.equals(command == null ? null : command.getIncludeSummary());
List<MeetingTranscriptVO> transcripts = includeTranscript ? meetingQueryService.getTranscripts(meetingId) : null;
String summaryContent = includeSummary ? meeting.getSummaryContent() : null;
AndroidUnifiedMeetingStatusResponse build = AndroidUnifiedMeetingStatusResponse.builder()
.meetingId(meetingId)
.status(status)
.meeting(meeting)
.includesTranscript(includeTranscript)
.transcripts(transcripts)
.includesSummary(includeSummary)
.summaryContent(summaryContent)
.build();
log.info("[{}]{}.返回数据:[{}]", "Android会议", "查询会议统一状态", build);
return ApiResponse.ok(build);
}
@Operation(summary = "重试 Android 会议 ASR 识别")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "重试成功返回 true",
content = @Content(schema = @Schema(implementation = Boolean.class))
)
})
@PostMapping("/{meetingId}/transcripts/retry")
@Anonymous
public ApiResponse<Boolean> retryTranscription(HttpServletRequest request, @PathVariable Long meetingId) {
AndroidRequestLogHelper.logRequest(log, "Android会议", "重试会议 ASR 识别接口", "meetingId", meetingId);
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
LoginUser loginUser = authContext.isAnonymous() ? null : AndroidLoginUserSupport.requireLoginUser(authContext);
requireOperableOfflineMeeting(meetingId, authContext, loginUser);
meetingCommandService.retryTranscription(meetingId);
androidMeetingPushService.pushMeetingStatusChanged(meetingId, UnifiedMeetingStatusStage.TRANSCRIBING.getCode());
return ApiResponse.ok(true);
}
@Operation(summary = "重试 Android 会议总结")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "重试成功返回 true",
content = @Content(schema = @Schema(implementation = Boolean.class))
)
})
@PostMapping("/{meetingId}/summary/retry")
@Anonymous
public ApiResponse<Boolean> retrySummary(HttpServletRequest request, @PathVariable Long meetingId) {
AndroidRequestLogHelper.logRequest(log, "Android会议", "重试会议总结接口", "meetingId", meetingId);
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
LoginUser loginUser = authContext.isAnonymous() ? null : AndroidLoginUserSupport.requireLoginUser(authContext);
requireOperableOfflineMeeting(meetingId, authContext, loginUser);
meetingCommandService.retrySummary(meetingId);
androidMeetingPushService.pushMeetingStatusChanged(meetingId, UnifiedMeetingStatusStage.SUMMARIZING.getCode());
return ApiResponse.ok(true);
}
@Operation(summary = "更新Android会议访问密码")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "返回更新后的会议访问密码,传空时表示清空访问密码",
content = @Content(schema = @Schema(implementation = String.class))
)
})
@PutMapping("/{meetingId}/access-password")
@Log(value = "修改Android会议访问密码", type = "Android会议管理")
public ApiResponse<String> updateAccessPassword(HttpServletRequest request,
@PathVariable Long meetingId,
@RequestBody(required = false) LegacyMeetingAccessPasswordRequest command) {
AndroidRequestLogHelper.logRequest(log, "Android会议", "更新会议访问密码接口",
"meetingId", meetingId,
"request", command);
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
LoginUser loginUser = AndroidLoginUserSupport.requireLoginUser(authContext);
Meeting meeting = meetingAccessService.requireMeeting(meetingId);
if (!Objects.equals(meeting.getCreatorId(), loginUser.getUserId())) {
return ApiResponse.error("仅会议创建人可设置访问密码");
}
String password = normalizePassword(command == null ? null : command.getPassword());
meetingService.update(new LambdaUpdateWrapper<Meeting>()
.eq(Meeting::getId, meeting.getId())
.set(Meeting::getAccessPassword, password));
return ApiResponse.ok(password);
}
@Operation(summary = "删除Android会议")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "返回是否删除成功",
content = @Content(schema = @Schema(implementation = Boolean.class))
)
})
@DeleteMapping("/{meetingId}")
@Log(value = "删除Android会议", type = "Android会议管理")
public ApiResponse<Boolean> delete(HttpServletRequest request, @PathVariable Long meetingId) {
AndroidRequestLogHelper.logRequest(log, "Android会议", "删除会议接口", "meetingId", meetingId);
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
LoginUser loginUser = AndroidLoginUserSupport.requireLoginUser(authContext);
Meeting meeting = meetingAccessService.requireMeeting(meetingId);
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
meetingCommandService.deleteMeeting(meetingId);
return ApiResponse.ok(true);
}
@GetMapping("/config")
@Log(value = "获取会议配置", type = "Android会议管理")
@Operation(summary = "获取会议配置")
@Anonymous
public ApiResponse<AndroidMeetingConfigVo> config(HttpServletRequest request) {
AndroidRequestLogHelper.logRequest(log, "Android会议", "获取会议配置接口");
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
LoginUser loginUser = AndroidLoginUserSupport.toLoginUser(authContext);
Long tenantId = loginUser != null ? loginUser.getTenantId() : authContext.getTenantId();
Long userId = loginUser != null ? loginUser.getUserId() : null;
boolean isPlatformAdmin = loginUser != null && Boolean.TRUE.equals(loginUser.getIsPlatformAdmin());
boolean isTenantAdmin = loginUser != null && Boolean.TRUE.equals(loginUser.getIsTenantAdmin());
AndroidMeetingConfigVo resultVo = new AndroidMeetingConfigVo();
PageResult<List<PromptTemplateVO>> promptTemplateList = promptTemplateService.pageTemplates(
1,
1000,
null,
null,
tenantId,
userId,
isPlatformAdmin,
isTenantAdmin
);
List<PromptTemplateVO> enabledTemplates = promptTemplateList.getRecords() == null
? List.of()
: promptTemplateList.getRecords().stream()
.filter(item -> Integer.valueOf(1).equals(item.getStatus()))
.toList();
resultVo.setTemplateList(enabledTemplates);
PageResult<List<AiModelVO>> modelList = aiModelService.pageModels(1, 1000, null, "LLM", tenantId, false);
List<AiModelVO> enabledModels = modelList.getRecords() == null
? List.of()
: modelList.getRecords().stream()
.filter(item -> Integer.valueOf(1).equals(item.getStatus()))
.toList();
resultVo.setModelsList(enabledModels);
resultVo.setHotWordGroupList(hotWordGroupService.listVisibleOptions(tenantId));
resultVo.setSummaryDegreeOfDetail(dictItemService.getItemsByTypeCode("summary_degree_detail"));
resultVo.setMaxMeetingDuration(Integer.valueOf(paramService.getParamValue(SysParamKeys.MEETING_MAX_MEETING_DURATION, "30")));
resultVo.setMinMeetingDuration(Integer.valueOf(paramService.getParamValue(SysParamKeys.MEETING_MIN_MEETING_DURATION, "10")));
resultVo.setMaxPauseDuration(Integer.valueOf(paramService.getParamValue(SysParamKeys.MEETING_MAX_PAUSE_DURATION, String.valueOf(60 * 4))));
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);
}
private void resolvePublicDeviceTenantId(HttpServletRequest request,
AndroidOfflineMeetingCreateCommand command,
AndroidAuthContext authContext) {
if (command == null || command.getTenantId() != null || authContext == null || !authContext.isAnonymous()) {
return;
}
String tenantCode = request == null ? null : request.getHeader(TENANT_CODE_HEADER);
if (!StringUtils.hasText(tenantCode)) {
throw new IllegalArgumentException("tenantCode不能为空");
}
SysTenant tenant = sysTenantMapper.selectOne(new LambdaQueryWrapper<SysTenant>()
.eq(SysTenant::getTenantCode, tenantCode.trim())
.eq(SysTenant::getIsDeleted, 0)
.last("LIMIT 1"));
if (tenant == null || tenant.getId() == null) {
throw new IllegalArgumentException("tenantCode无效无法获取tenantId");
}
command.setTenantId(tenant.getId());
}
private AndroidMeetingCreateResponse buildAndroidMeetingCreateResponse(MeetingVO meeting) {
AndroidMeetingCreateResponse response = new AndroidMeetingCreateResponse();
if (meeting == null) {
return response;
}
BeanUtils.copyProperties(meeting, response);
response.setPreviewUrl(buildPreviewUrl(meeting.getId()));
return response;
}
private PageResult<List<AndroidMeetingListItemVO>> buildAndroidMeetingListPage(PageResult<List<MeetingVO>> source) {
PageResult<List<AndroidMeetingListItemVO>> result = new PageResult<>();
if (source == null) {
result.setTotal(0L);
result.setRecords(List.of());
return result;
}
result.setTotal(source.getTotal());
result.setRecords(source.getRecords() == null
? List.of()
: source.getRecords().stream().map(this::buildAndroidMeetingListItem).toList());
return result;
}
private AndroidMeetingListItemVO buildAndroidMeetingListItem(MeetingVO meeting) {
AndroidMeetingListItemVO item = new AndroidMeetingListItemVO();
if (meeting == null) {
return item;
}
BeanUtils.copyProperties(meeting, item);
item.setPreviewUrl(buildPreviewUrl(meeting.getId()));
item.setDayOffset(resolveDayOffset(meeting.getMeetingTime()));
return item;
}
private String buildPreviewUrl(Long meetingId) {
if (meetingId == null) {
return null;
}
String baseUrl = normalizeH5BaseUrl();
if (!StringUtils.hasText(baseUrl)) {
return null;
}
return baseUrl + "/meetings/" + meetingId + "/preview";
}
private String normalizeH5BaseUrl() {
String baseUrl = StringUtils.hasText(h5BaseUrl) ? h5BaseUrl.trim() : "";
if (!StringUtils.hasText(baseUrl)) {
return "";
}
return baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
}
private Long resolveDayOffset(LocalDateTime meetingTime) {
if (meetingTime == null) {
return null;
}
return ChronoUnit.DAYS.between(LocalDate.now(), meetingTime.toLocalDate());
}
private Meeting requireOperableOfflineMeeting(Long meetingId, AndroidAuthContext authContext, LoginUser loginUser) {
Meeting meeting = meetingAccessService.requireMeetingIgnoreTenant(meetingId);
if (!MeetingConstants.TYPE_OFFLINE.equals(meeting.getMeetingType())) {
throw new RuntimeException("当前会议不是离线会议");
}
if (authContext == null || authContext.getDeviceId() == null || authContext.getDeviceId().isBlank()) {
throw new RuntimeException("设备ID不能为空");
}
return meeting;
}
private boolean isUploadFinishedStage(AndroidOfflineMeetingFinishRequest command) {
return command != null
&& MeetingConstants.OFFLINE_RECORDING_UPLOAD_FINISHED.equalsIgnoreCase(command.getFinishStage());
}
private String normalizePassword(String password) {
if (password == null) {
return null;
}
String normalized = password.trim();
return normalized.isEmpty() ? null : normalized;
}
}