From b9593324a59bf1b136a0b9cb9b8f9f76fd1764e7 Mon Sep 17 00:00:00 2001 From: chenhao Date: Fri, 10 Apr 2026 09:14:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E4=BC=9A=E8=AE=AE?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E8=A7=A3=E6=9E=90=E5=92=8C=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E7=BA=A7=E5=88=AB=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 `MeetingCommandServiceImpl` 中引入 `MeetingRuntimeProfileResolver`,解析并应用运行时配置 - 更新 `application-dev.yml` 和 `application.yml`,添加日志级别配置和新的数据库表 --- .../imeeting/event/MeetingCreatedEvent.java | 14 +- .../listener/MeetingTaskDispatchListener.java | 2 +- .../listener/MeetingTaskRecoveryListener.java | 16 +- .../imeeting/service/biz/AiTaskService.java | 4 +- .../service/biz/impl/AiTaskServiceImpl.java | 23 ++- .../biz/impl/MeetingCommandServiceImpl.java | 87 ++++++--- .../biz/impl/MeetingDomainSupport.java | 4 +- .../src/main/resources/application-dev.yml | 12 +- .../src/main/resources/application-prod.yml | 8 +- backend/src/main/resources/application.yml | 8 + .../impl/MeetingCommandServiceImplTest.java | 173 +++++++++++++++++- components/PDFViewer/VirtualPDFViewer.jsx | 8 +- frontend/index.html | 3 - .../business/MeetingCreateDrawer.tsx | 5 +- .../shared/PDFViewer/VirtualPDFViewer.jsx | 8 +- frontend/src/pages/auth/login/index.tsx | 4 +- frontend/src/pages/business/HotWords.tsx | 2 +- frontend/src/pages/business/SpeakerReg.tsx | 2 +- frontend/src/pages/home/index.less | 50 ++--- frontend/src/pages/home/index.tsx | 4 +- 20 files changed, 342 insertions(+), 95 deletions(-) diff --git a/backend/src/main/java/com/imeeting/event/MeetingCreatedEvent.java b/backend/src/main/java/com/imeeting/event/MeetingCreatedEvent.java index 3da27bb..5bc8241 100644 --- a/backend/src/main/java/com/imeeting/event/MeetingCreatedEvent.java +++ b/backend/src/main/java/com/imeeting/event/MeetingCreatedEvent.java @@ -2,12 +2,24 @@ package com.imeeting.event; public class MeetingCreatedEvent { private final Long meetingId; + private final Long tenantId; + private final Long userId; - public MeetingCreatedEvent(Long meetingId) { + public MeetingCreatedEvent(Long meetingId, Long tenantId, Long userId) { this.meetingId = meetingId; + this.tenantId = tenantId; + this.userId = userId; } public Long getMeetingId() { return meetingId; } + + public Long getTenantId() { + return tenantId; + } + + public Long getUserId() { + return userId; + } } diff --git a/backend/src/main/java/com/imeeting/listener/MeetingTaskDispatchListener.java b/backend/src/main/java/com/imeeting/listener/MeetingTaskDispatchListener.java index 217e483..e2ae635 100644 --- a/backend/src/main/java/com/imeeting/listener/MeetingTaskDispatchListener.java +++ b/backend/src/main/java/com/imeeting/listener/MeetingTaskDispatchListener.java @@ -15,6 +15,6 @@ public class MeetingTaskDispatchListener { @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void onMeetingCreated(MeetingCreatedEvent event) { - aiTaskService.dispatchTasks(event.getMeetingId()); + aiTaskService.dispatchTasks(event.getMeetingId(), event.getTenantId(), event.getUserId()); } } diff --git a/backend/src/main/java/com/imeeting/listener/MeetingTaskRecoveryListener.java b/backend/src/main/java/com/imeeting/listener/MeetingTaskRecoveryListener.java index b706984..f485fb3 100644 --- a/backend/src/main/java/com/imeeting/listener/MeetingTaskRecoveryListener.java +++ b/backend/src/main/java/com/imeeting/listener/MeetingTaskRecoveryListener.java @@ -4,6 +4,7 @@ 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.support.TaskSecurityContextRunner; import com.imeeting.service.biz.AiTaskService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -27,15 +28,18 @@ public class MeetingTaskRecoveryListener implements ApplicationRunner { private final MeetingMapper meetingMapper; private final AiTaskService aiTaskService; private final StringRedisTemplate redisTemplate; + private final TaskSecurityContextRunner taskSecurityContextRunner; @Override public void run(ApplicationArguments args) { log.info("Starting meeting task self-healing check..."); // 1. 查询状态为 1(识别中) 或 2(总结中) 且未删除的会议 - List pendingMeetings = meetingMapper.selectList(new LambdaQueryWrapper() - .in(Meeting::getStatus, 1, 2) - .eq(Meeting::getIsDeleted, 0)); + List pendingMeetings = taskSecurityContextRunner.callAsPlatformAdmin(() -> + meetingMapper.selectList(new LambdaQueryWrapper() + .in(Meeting::getStatus, 1, 2) + .eq(Meeting::getIsDeleted, 0)) + ); if (pendingMeetings.isEmpty()) { log.info("No pending tasks found. Recovery check completed."); @@ -59,10 +63,10 @@ public class MeetingTaskRecoveryListener implements ApplicationRunner { // 3. 根据状态重新派发任务 (平滑拉起) if (meeting.getStatus() == 1) { log.info("Resuming ASR task for meeting {}", meeting.getId()); - aiTaskService.dispatchTasks(meeting.getId()); + aiTaskService.dispatchTasks(meeting.getId(), meeting.getTenantId(), meeting.getCreatorId()); } else if (meeting.getStatus() == 2) { log.info("Resuming Summary task for meeting {}", meeting.getId()); - aiTaskService.dispatchSummaryTask(meeting.getId()); + aiTaskService.dispatchSummaryTask(meeting.getId(), meeting.getTenantId(), meeting.getCreatorId()); } // 增加小延迟防止惊群效应 @@ -80,7 +84,7 @@ public class MeetingTaskRecoveryListener implements ApplicationRunner { Meeting update = new Meeting(); update.setId(m.getId()); update.setStatus(4); // 失败 - meetingMapper.updateById(update); + taskSecurityContextRunner.runAsTenantUser(m.getTenantId(), m.getCreatorId(), () -> meetingMapper.updateById(update)); // 同步 Redis 进度为失败 String progressKey = RedisKeys.meetingProgressKey(m.getId()); diff --git a/backend/src/main/java/com/imeeting/service/biz/AiTaskService.java b/backend/src/main/java/com/imeeting/service/biz/AiTaskService.java index 710c492..193613c 100644 --- a/backend/src/main/java/com/imeeting/service/biz/AiTaskService.java +++ b/backend/src/main/java/com/imeeting/service/biz/AiTaskService.java @@ -4,6 +4,6 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.imeeting.entity.biz.AiTask; public interface AiTaskService extends IService { - void dispatchTasks(Long meetingId); - void dispatchSummaryTask(Long meetingId); + void dispatchTasks(Long meetingId, Long tenantId, Long userId); + void dispatchSummaryTask(Long meetingId, Long tenantId, Long userId); } diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java index 4b3a85d..5945fa3 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java @@ -14,6 +14,7 @@ import com.imeeting.entity.biz.MeetingTranscript; import com.imeeting.mapper.biz.AiTaskMapper; import com.imeeting.mapper.biz.MeetingMapper; import com.imeeting.mapper.biz.MeetingTranscriptMapper; +import com.imeeting.support.TaskSecurityContextRunner; import com.imeeting.service.biz.AiModelService; import com.imeeting.service.biz.AiTaskService; import com.imeeting.service.biz.HotWordService; @@ -57,6 +58,7 @@ public class AiTaskServiceImpl extends ServiceImpl impleme private final HotWordService hotWordService; private final StringRedisTemplate redisTemplate; private final MeetingSummaryFileService meetingSummaryFileService; + private final TaskSecurityContextRunner taskSecurityContextRunner; @Value("${unisbase.app.server-base-url}") private String serverBaseUrl; @@ -71,7 +73,11 @@ public class AiTaskServiceImpl extends ServiceImpl impleme @Override @Async - public void dispatchTasks(Long meetingId) { + public void dispatchTasks(Long meetingId, Long tenantId, Long userId) { + taskSecurityContextRunner.runAsTenantUser(tenantId, userId, () -> doDispatchTasks(meetingId)); + } + + private void doDispatchTasks(Long meetingId) { String lockKey = RedisKeys.meetingPollingLockKey(meetingId); Boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 30, TimeUnit.MINUTES); if (Boolean.FALSE.equals(acquired)) { @@ -134,7 +140,11 @@ public class AiTaskServiceImpl extends ServiceImpl impleme @Override @Async - public void dispatchSummaryTask(Long meetingId) { + public void dispatchSummaryTask(Long meetingId, Long tenantId, Long userId) { + taskSecurityContextRunner.runAsTenantUser(tenantId, userId, () -> doDispatchSummaryTask(meetingId)); + } + + private void doDispatchSummaryTask(Long meetingId) { Meeting meeting = meetingMapper.selectById(meetingId); if (meeting == null) return; try { @@ -413,7 +423,14 @@ public class AiTaskServiceImpl extends ServiceImpl impleme Long summaryModelId = Long.valueOf(taskRecord.getTaskConfig().get("summaryModelId").toString()); AiModelVO llmModel = aiModelService.getModelById(summaryModelId, "LLM"); - if (llmModel == null) return; + if (llmModel == null) { + updateAiTaskFail(taskRecord, "LLM model not found: " + summaryModelId); + throw new RuntimeException("LLM模型配置不存在"); + } + if (!Integer.valueOf(1).equals(llmModel.getStatus())) { + updateAiTaskFail(taskRecord, "LLM model disabled: " + summaryModelId); + throw new RuntimeException("LLM模型未启用"); + } String promptContent = taskRecord.getTaskConfig().get("promptContent") != null ? taskRecord.getTaskConfig().get("promptContent").toString() : ""; diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java index 47b8243..45fabae 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java @@ -7,6 +7,7 @@ import com.imeeting.common.RedisKeys; import com.imeeting.dto.biz.CreateMeetingCommand; import com.imeeting.dto.biz.CreateRealtimeMeetingCommand; import com.imeeting.dto.biz.MeetingVO; +import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile; import com.imeeting.dto.biz.RealtimeMeetingResumeConfig; import com.imeeting.dto.biz.RealtimeMeetingSessionStatusVO; import com.imeeting.dto.biz.RealtimeTranscriptItemDTO; @@ -19,6 +20,7 @@ import com.imeeting.entity.biz.MeetingTranscript; import com.imeeting.service.biz.AiTaskService; import com.imeeting.service.biz.HotWordService; import com.imeeting.service.biz.MeetingCommandService; +import com.imeeting.service.biz.MeetingRuntimeProfileResolver; import com.imeeting.service.biz.MeetingService; import com.imeeting.service.biz.MeetingSummaryFileService; import com.imeeting.service.biz.RealtimeMeetingSessionStateService; @@ -49,6 +51,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { private final com.imeeting.mapper.biz.MeetingTranscriptMapper transcriptMapper; private final MeetingSummaryFileService meetingSummaryFileService; private final MeetingDomainSupport meetingDomainSupport; + private final MeetingRuntimeProfileResolver meetingRuntimeProfileResolver; private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService; private final RealtimeMeetingAudioStorageService realtimeMeetingAudioStorageService; private final StringRedisTemplate redisTemplate; @@ -57,6 +60,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { @Override @Transactional(rollbackFor = Exception.class) public MeetingVO createMeeting(CreateMeetingCommand command, Long tenantId, Long creatorId, String creatorName) { + RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId); Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId); String hostName = resolveHostName(command.getHostName(), creatorName, creatorId, hostUserId); Meeting meeting = meetingDomainSupport.initMeeting(command.getTitle(), command.getMeetingTime(), command.getParticipants(), command.getTags(), @@ -69,9 +73,9 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { asrTask.setStatus(0); Map asrConfig = new HashMap<>(); - asrConfig.put("asrModelId", command.getAsrModelId()); - asrConfig.put("useSpkId", command.getUseSpkId() != null ? command.getUseSpkId() : 1); - asrConfig.put("enableTextRefine", command.getEnableTextRefine() != null ? command.getEnableTextRefine() : false); + asrConfig.put("asrModelId", runtimeProfile.getResolvedAsrModelId()); + asrConfig.put("useSpkId", runtimeProfile.getResolvedUseSpkId()); + asrConfig.put("enableTextRefine", runtimeProfile.getResolvedEnableTextRefine()); List finalHotWords = command.getHotWords(); if (finalHotWords == null || finalHotWords.isEmpty()) { @@ -86,10 +90,10 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { asrTask.setTaskConfig(asrConfig); aiTaskService.save(asrTask); - meetingDomainSupport.createSummaryTask(meeting.getId(), command.getSummaryModelId(), command.getPromptId()); + meetingDomainSupport.createSummaryTask(meeting.getId(), runtimeProfile.getResolvedSummaryModelId(), runtimeProfile.getResolvedPromptId()); meeting.setAudioUrl(meetingDomainSupport.relocateAudioUrl(meeting.getId(), command.getAudioUrl())); meetingService.updateById(meeting); - meetingDomainSupport.publishMeetingCreated(meeting.getId()); + meetingDomainSupport.publishMeetingCreated(meeting.getId(), meeting.getTenantId(), meeting.getCreatorId()); MeetingVO vo = new MeetingVO(); meetingDomainSupport.fillMeetingVO(meeting, vo, false); @@ -99,14 +103,15 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { @Override @Transactional(rollbackFor = Exception.class) public MeetingVO createRealtimeMeeting(CreateRealtimeMeetingCommand command, Long tenantId, Long creatorId, String creatorName) { + RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId); Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId); String hostName = resolveHostName(command.getHostName(), creatorName, creatorId, hostUserId); Meeting meeting = meetingDomainSupport.initMeeting(command.getTitle(), command.getMeetingTime(), command.getParticipants(), command.getTags(), null, tenantId, creatorId, creatorName, hostUserId, hostName, 0); meetingService.save(meeting); - meetingDomainSupport.createSummaryTask(meeting.getId(), command.getSummaryModelId(), command.getPromptId()); + meetingDomainSupport.createSummaryTask(meeting.getId(), runtimeProfile.getResolvedSummaryModelId(), runtimeProfile.getResolvedPromptId()); realtimeMeetingSessionStateService.initSessionIfAbsent(meeting.getId(), tenantId, creatorId); - realtimeMeetingSessionStateService.rememberResumeConfig(meeting.getId(), buildRealtimeResumeConfig(command, tenantId)); + realtimeMeetingSessionStateService.rememberResumeConfig(meeting.getId(), buildRealtimeResumeConfig(command, tenantId, runtimeProfile)); MeetingVO vo = new MeetingVO(); meetingDomainSupport.fillMeetingVO(meeting, vo, false); @@ -239,7 +244,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { prepareOfflineReprocessTasks(meetingId, currentStatus); realtimeMeetingSessionStateService.clear(meetingId); updateMeetingProgress(meetingId, 0, "正在转入离线音频识别流程...", 0); - aiTaskService.dispatchTasks(meetingId); + aiTaskService.dispatchTasks(meetingId, meeting.getTenantId(), meeting.getCreatorId()); return; } @@ -266,7 +271,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { meeting.setStatus(2); meetingService.updateById(meeting); updateMeetingProgress(meetingId, 90, "正在生成会议总结...", 0); - aiTaskService.dispatchSummaryTask(meetingId); + aiTaskService.dispatchSummaryTask(meetingId, meeting.getTenantId(), meeting.getCreatorId()); } private void applyRealtimeAudioFinalizeResult(Meeting meeting, RealtimeMeetingAudioStorageService.FinalizeResult result) { @@ -450,18 +455,18 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { meetingDomainSupport.createSummaryTask(meetingId, summaryModelId, promptId); meeting.setStatus(2); meetingService.updateById(meeting); - dispatchSummaryTaskAfterCommit(meetingId); + dispatchSummaryTaskAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId()); } - private void dispatchSummaryTaskAfterCommit(Long meetingId) { + private void dispatchSummaryTaskAfterCommit(Long meetingId, Long tenantId, Long userId) { if (!TransactionSynchronizationManager.isSynchronizationActive()) { - aiTaskService.dispatchSummaryTask(meetingId); + aiTaskService.dispatchSummaryTask(meetingId, tenantId, userId); return; } TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @Override public void afterCommit() { - aiTaskService.dispatchSummaryTask(meetingId); + aiTaskService.dispatchSummaryTask(meetingId, tenantId, userId); } }); } @@ -484,20 +489,56 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { } } - private RealtimeMeetingResumeConfig buildRealtimeResumeConfig(CreateRealtimeMeetingCommand command, Long tenantId) { + private RealtimeMeetingResumeConfig buildRealtimeResumeConfig(CreateRealtimeMeetingCommand command, + Long tenantId, + RealtimeMeetingRuntimeProfile runtimeProfile) { RealtimeMeetingResumeConfig resumeConfig = new RealtimeMeetingResumeConfig(); - resumeConfig.setAsrModelId(command.getAsrModelId()); - resumeConfig.setMode(command.getMode() == null || command.getMode().isBlank() ? "2pass" : command.getMode().trim()); - resumeConfig.setLanguage(command.getLanguage() == null || command.getLanguage().isBlank() ? "auto" : command.getLanguage().trim()); - resumeConfig.setUseSpkId(command.getUseSpkId() != null ? command.getUseSpkId() : 0); - resumeConfig.setEnablePunctuation(command.getEnablePunctuation() != null ? command.getEnablePunctuation() : Boolean.TRUE); - resumeConfig.setEnableItn(command.getEnableItn() != null ? command.getEnableItn() : Boolean.TRUE); - resumeConfig.setEnableTextRefine(Boolean.TRUE.equals(command.getEnableTextRefine())); - resumeConfig.setSaveAudio(Boolean.TRUE.equals(command.getSaveAudio())); - resumeConfig.setHotwords(resolveRealtimeHotwords(command.getHotWords(), tenantId)); + resumeConfig.setAsrModelId(runtimeProfile.getResolvedAsrModelId()); + resumeConfig.setMode(runtimeProfile.getResolvedMode()); + resumeConfig.setLanguage(runtimeProfile.getResolvedLanguage()); + resumeConfig.setUseSpkId(runtimeProfile.getResolvedUseSpkId()); + resumeConfig.setEnablePunctuation(runtimeProfile.getResolvedEnablePunctuation()); + resumeConfig.setEnableItn(runtimeProfile.getResolvedEnableItn()); + resumeConfig.setEnableTextRefine(runtimeProfile.getResolvedEnableTextRefine()); + resumeConfig.setSaveAudio(runtimeProfile.getResolvedSaveAudio()); + resumeConfig.setHotwords(resolveRealtimeHotwords(runtimeProfile.getResolvedHotWords(), tenantId)); return resumeConfig; } + private RealtimeMeetingRuntimeProfile resolveCreateProfile(CreateMeetingCommand command, Long tenantId) { + return meetingRuntimeProfileResolver.resolve( + tenantId, + command.getAsrModelId(), + command.getSummaryModelId(), + command.getPromptId(), + null, + null, + command.getUseSpkId(), + null, + null, + command.getEnableTextRefine(), + null, + command.getHotWords() + ); + } + + private RealtimeMeetingRuntimeProfile resolveCreateProfile(CreateRealtimeMeetingCommand command, Long tenantId) { + return meetingRuntimeProfileResolver.resolve( + tenantId, + command.getAsrModelId(), + command.getSummaryModelId(), + command.getPromptId(), + command.getMode(), + command.getLanguage(), + command.getUseSpkId(), + command.getEnablePunctuation(), + command.getEnableItn(), + command.getEnableTextRefine(), + command.getSaveAudio(), + command.getHotWords() + ); + } + private List> resolveRealtimeHotwords(List selectedWords, Long tenantId) { List tenantHotwords = hotWordService.list(new LambdaQueryWrapper() .eq(HotWord::getTenantId, tenantId) diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingDomainSupport.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingDomainSupport.java index 0532805..9c03297 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingDomainSupport.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingDomainSupport.java @@ -86,8 +86,8 @@ public class MeetingDomainSupport { aiTaskService.save(sumTask); } - public void publishMeetingCreated(Long meetingId) { - eventPublisher.publishEvent(new MeetingCreatedEvent(meetingId)); + public void publishMeetingCreated(Long meetingId, Long tenantId, Long userId) { + eventPublisher.publishEvent(new MeetingCreatedEvent(meetingId, tenantId, userId)); } public String relocateAudioUrl(Long meetingId, String audioUrl) { diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml index 7dfbbba..b3c2821 100644 --- a/backend/src/main/resources/application-dev.yml +++ b/backend/src/main/resources/application-dev.yml @@ -1,6 +1,14 @@ server: port: ${SERVER_PORT:8081} - +logging: + level: + root: info + io.grpc: debug + io.grpc.netty.shaded.io.grpc.netty: debug + com.imeeting.config.grpc: debug + com.imeeting.grpc: debug + com.imeeting.service.realtime.impl.RealtimeMeetingGrpcSessionServiceImpl: debug + com.imeeting.service.realtime.impl.AsrUpstreamBridgeServiceImpl: debug spring: datasource: url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://127.0.0.1:5432/imeeting_db} @@ -24,4 +32,4 @@ unisbase: secret: ${INTERNAL_AUTH_SECRET:change-me-dev-internal-secret} app: server-base-url: ${APP_SERVER_BASE_URL:http://127.0.0.1:${server.port}} - upload-path: ${APP_UPLOAD_PATH:D:/data/imeeting/uploads/} \ No newline at end of file + upload-path: ${APP_UPLOAD_PATH:D:/data/imeeting/uploads/} diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml index 3d14489..a7442a3 100644 --- a/backend/src/main/resources/application-prod.yml +++ b/backend/src/main/resources/application-prod.yml @@ -15,9 +15,9 @@ spring: unisbase: security: - jwt-secret: ${SECURITY_JWT_SECRET} + jwt-secret: ${SECURITY_JWT_SECRET:change-me-dev-jwt-secret-32bytes} internal-auth: - secret: ${INTERNAL_AUTH_SECRET} + secret: ${INTERNAL_AUTH_SECRET:change-me-dev-internal-secret} app: - server-base-url: ${APP_SERVER_BASE_URL} - upload-path: ${APP_UPLOAD_PATH} \ No newline at end of file + server-base-url: ${APP_SERVER_BASE_URL:http://127.0.0.1:${server.port}} + upload-path: ${APP_UPLOAD_PATH:/data/imeeting/uploads/} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index fb6f827..f59e705 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -1,6 +1,10 @@ server: port: ${SERVER_PORT:8080} +logging: + file: + path: ${LOG_PATH:./logs} + spring: profiles: active: ${SPRING_PROFILES_ACTIVE:dev} @@ -34,6 +38,9 @@ unisbase: - biz_ai_tasks - biz_meeting_transcripts - biz_speakers + - biz_llm_models + - biz_asr_models + - biz_prompt_templates security: enabled: true mode: embedded @@ -43,6 +50,7 @@ unisbase: - /actuator/health - /api/static/** - /ws/** + - /api/android/** internal-auth: enabled: true header-name: X-Internal-Secret diff --git a/backend/src/test/java/com/imeeting/service/biz/impl/MeetingCommandServiceImplTest.java b/backend/src/test/java/com/imeeting/service/biz/impl/MeetingCommandServiceImplTest.java index 464f04a..b87b386 100644 --- a/backend/src/test/java/com/imeeting/service/biz/impl/MeetingCommandServiceImplTest.java +++ b/backend/src/test/java/com/imeeting/service/biz/impl/MeetingCommandServiceImplTest.java @@ -4,10 +4,14 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.imeeting.dto.biz.CreateMeetingCommand; import com.imeeting.dto.biz.CreateRealtimeMeetingCommand; import com.imeeting.dto.biz.MeetingVO; +import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile; +import com.imeeting.dto.biz.RealtimeMeetingResumeConfig; import com.imeeting.dto.biz.RealtimeTranscriptItemDTO; +import com.imeeting.entity.biz.AiTask; import com.imeeting.entity.biz.Meeting; import com.imeeting.service.biz.AiTaskService; import com.imeeting.service.biz.HotWordService; +import com.imeeting.service.biz.MeetingRuntimeProfileResolver; import com.imeeting.service.biz.MeetingService; import com.imeeting.service.biz.MeetingSummaryFileService; import com.imeeting.service.biz.RealtimeMeetingSessionStateService; @@ -22,6 +26,7 @@ import java.time.LocalDateTime; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; @@ -200,6 +205,7 @@ class MeetingCommandServiceImplTest { transcriptMapper, mock(MeetingSummaryFileService.class), meetingDomainSupport, + mockRuntimeProfileResolver(), sessionStateService, audioStorageService, mock(StringRedisTemplate.class), @@ -212,7 +218,7 @@ class MeetingCommandServiceImplTest { verify(meetingService).updateById(meetingCaptor.capture()); assertEquals("/api/static/meetings/202/source_audio.wav", meetingCaptor.getValue().getAudioUrl()); assertEquals(RealtimeMeetingAudioStorageService.STATUS_SUCCESS, meetingCaptor.getValue().getAudioSaveStatus()); - verify(aiTaskService).dispatchSummaryTask(202L); + verify(aiTaskService).dispatchSummaryTask(202L, null, null); } @Test @@ -237,6 +243,7 @@ class MeetingCommandServiceImplTest { transcriptMapper, mock(MeetingSummaryFileService.class), meetingDomainSupport, + mockRuntimeProfileResolver(), mock(RealtimeMeetingSessionStateService.class), audioStorageService, mock(StringRedisTemplate.class), @@ -262,6 +269,7 @@ class MeetingCommandServiceImplTest { transcriptMapper, mock(MeetingSummaryFileService.class), meetingDomainSupport, + mockRuntimeProfileResolver(), sessionStateService, mock(RealtimeMeetingAudioStorageService.class), mock(StringRedisTemplate.class), @@ -300,6 +308,7 @@ class MeetingCommandServiceImplTest { mock(com.imeeting.mapper.biz.MeetingTranscriptMapper.class), mock(MeetingSummaryFileService.class), meetingDomainSupport, + mockRuntimeProfileResolver(), mock(RealtimeMeetingSessionStateService.class), mock(RealtimeMeetingAudioStorageService.class), mock(StringRedisTemplate.class), @@ -313,16 +322,150 @@ class MeetingCommandServiceImplTest { verify(meetingDomainSupport).createSummaryTask(301L, 22L, 33L); assertEquals(2, meeting.getStatus()); verify(meetingService).updateById(meeting); - verify(aiTaskService, never()).dispatchSummaryTask(301L); + verify(aiTaskService, never()).dispatchSummaryTask(301L, null, null); TransactionSynchronizationUtils.triggerAfterCommit(); - verify(aiTaskService).dispatchSummaryTask(301L); + verify(aiTaskService).dispatchSummaryTask(301L, null, null); } finally { TransactionSynchronizationManager.clearSynchronization(); } } + @Test + void createMeetingShouldPersistResolvedRuntimeProfile() { + MeetingService meetingService = mock(MeetingService.class); + MeetingDomainSupport meetingDomainSupport = mock(MeetingDomainSupport.class); + AiTaskService aiTaskService = mock(AiTaskService.class); + MeetingRuntimeProfileResolver runtimeProfileResolver = mockRuntimeProfileResolver(101L, 202L, 303L); + Meeting meeting = new Meeting(); + meeting.setId(808L); + meeting.setTenantId(1L); + meeting.setHostUserId(7L); + meeting.setHostName("creator"); + + when(meetingDomainSupport.initMeeting( + eq("Resolved Meeting"), + any(LocalDateTime.class), + eq("1,2"), + eq("tagA"), + eq("/audio/demo.wav"), + eq(1L), + eq(7L), + eq("creator"), + eq(7L), + eq("creator"), + eq(0) + )).thenReturn(meeting); + when(meetingDomainSupport.relocateAudioUrl(808L, "/audio/demo.wav")).thenReturn("/audio/demo.wav"); + fillHostFieldsFromMeeting(meetingDomainSupport); + + MeetingCommandServiceImpl service = new MeetingCommandServiceImpl( + meetingService, + aiTaskService, + mock(HotWordService.class), + mock(com.imeeting.mapper.biz.MeetingTranscriptMapper.class), + mock(MeetingSummaryFileService.class), + meetingDomainSupport, + runtimeProfileResolver, + mock(RealtimeMeetingSessionStateService.class), + mock(RealtimeMeetingAudioStorageService.class), + mock(StringRedisTemplate.class), + new ObjectMapper() + ); + + CreateMeetingCommand command = new CreateMeetingCommand(); + command.setTitle("Resolved Meeting"); + command.setMeetingTime(LocalDateTime.of(2026, 4, 3, 19, 0)); + command.setParticipants("1,2"); + command.setTags("tagA"); + command.setAudioUrl("/audio/demo.wav"); + command.setAsrModelId(11L); + command.setSummaryModelId(22L); + command.setPromptId(33L); + + service.createMeeting(command, 1L, 7L, "creator"); + + verify(aiTaskService).save(argThat(task -> { + if (!"ASR".equals(task.getTaskType())) { + return false; + } + Object asrModelId = task.getTaskConfig().get("asrModelId"); + return Long.valueOf(101L).equals(asrModelId); + })); + verify(meetingDomainSupport).createSummaryTask(808L, 202L, 303L); + } + + @Test + void createRealtimeMeetingShouldPersistResolvedResumeProfile() { + MeetingService meetingService = mock(MeetingService.class); + MeetingDomainSupport meetingDomainSupport = mock(MeetingDomainSupport.class); + MeetingRuntimeProfileResolver runtimeProfileResolver = mockRuntimeProfileResolver(111L, 222L, 333L); + RealtimeMeetingSessionStateService sessionStateService = mock(RealtimeMeetingSessionStateService.class); + Meeting meeting = new Meeting(); + meeting.setId(909L); + meeting.setHostUserId(7L); + meeting.setHostName("creator"); + + when(meetingDomainSupport.initMeeting( + eq("Realtime Resolved"), + any(LocalDateTime.class), + eq("1,2"), + eq("tagB"), + isNull(), + eq(1L), + eq(7L), + eq("creator"), + eq(7L), + eq("creator"), + eq(0) + )).thenReturn(meeting); + fillHostFieldsFromMeeting(meetingDomainSupport); + + MeetingCommandServiceImpl service = new MeetingCommandServiceImpl( + meetingService, + mock(AiTaskService.class), + mock(HotWordService.class), + mock(com.imeeting.mapper.biz.MeetingTranscriptMapper.class), + mock(MeetingSummaryFileService.class), + meetingDomainSupport, + runtimeProfileResolver, + sessionStateService, + mock(RealtimeMeetingAudioStorageService.class), + mock(StringRedisTemplate.class), + new ObjectMapper() + ); + + CreateRealtimeMeetingCommand command = new CreateRealtimeMeetingCommand(); + command.setTitle("Realtime Resolved"); + command.setMeetingTime(LocalDateTime.of(2026, 4, 3, 19, 0)); + command.setParticipants("1,2"); + command.setTags("tagB"); + command.setAsrModelId(11L); + command.setSummaryModelId(22L); + command.setPromptId(33L); + command.setMode("online"); + command.setLanguage("zh"); + command.setEnablePunctuation(false); + command.setEnableItn(false); + command.setEnableTextRefine(true); + command.setSaveAudio(true); + + service.createRealtimeMeeting(command, 1L, 7L, "creator"); + + verify(meetingDomainSupport).createSummaryTask(909L, 222L, 333L); + verify(sessionStateService).rememberResumeConfig(eq(909L), argThat(config -> + Long.valueOf(111L).equals(config.getAsrModelId()) + && "online".equals(config.getMode()) + && "zh".equals(config.getLanguage()) + && Integer.valueOf(1).equals(config.getUseSpkId()) + && Boolean.FALSE.equals(config.getEnablePunctuation()) + && Boolean.FALSE.equals(config.getEnableItn()) + && Boolean.TRUE.equals(config.getEnableTextRefine()) + && Boolean.TRUE.equals(config.getSaveAudio()) + )); + } + private MeetingCommandServiceImpl newService(MeetingService meetingService, MeetingDomainSupport meetingDomainSupport) { return new MeetingCommandServiceImpl( meetingService, @@ -331,6 +474,7 @@ class MeetingCommandServiceImplTest { mock(com.imeeting.mapper.biz.MeetingTranscriptMapper.class), mock(MeetingSummaryFileService.class), meetingDomainSupport, + mockRuntimeProfileResolver(), mock(RealtimeMeetingSessionStateService.class), mock(RealtimeMeetingAudioStorageService.class), mock(StringRedisTemplate.class), @@ -338,6 +482,29 @@ class MeetingCommandServiceImplTest { ); } + private MeetingRuntimeProfileResolver mockRuntimeProfileResolver() { + return mockRuntimeProfileResolver(11L, 22L, 33L); + } + + private MeetingRuntimeProfileResolver mockRuntimeProfileResolver(Long asrModelId, Long summaryModelId, Long promptId) { + MeetingRuntimeProfileResolver resolver = mock(MeetingRuntimeProfileResolver.class); + RealtimeMeetingRuntimeProfile profile = new RealtimeMeetingRuntimeProfile(); + profile.setResolvedAsrModelId(asrModelId); + profile.setResolvedSummaryModelId(summaryModelId); + profile.setResolvedPromptId(promptId); + profile.setResolvedMode("online"); + profile.setResolvedLanguage("zh"); + profile.setResolvedUseSpkId(1); + profile.setResolvedEnablePunctuation(Boolean.FALSE); + profile.setResolvedEnableItn(Boolean.FALSE); + profile.setResolvedEnableTextRefine(Boolean.TRUE); + profile.setResolvedSaveAudio(Boolean.TRUE); + profile.setResolvedHotWords(java.util.List.of()); + when(resolver.resolve(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenReturn(profile); + return resolver; + } + private void fillHostFieldsFromMeeting(MeetingDomainSupport meetingDomainSupport) { doAnswer(invocation -> { MeetingVO vo = invocation.getArgument(1); diff --git a/components/PDFViewer/VirtualPDFViewer.jsx b/components/PDFViewer/VirtualPDFViewer.jsx index 4978844..4fffa3b 100644 --- a/components/PDFViewer/VirtualPDFViewer.jsx +++ b/components/PDFViewer/VirtualPDFViewer.jsx @@ -27,12 +27,8 @@ function VirtualPDFViewer({ url, filename }) { // 使用 useMemo 避免不必要的重新加载 const fileConfig = useMemo(() => ({ url }), [url]) - // Memoize PDF.js options to prevent unnecessary reloads - const pdfOptions = useMemo(() => ({ - cMapUrl: 'https://unpkg.com/pdfjs-dist@5.4.296/cmaps/', - cMapPacked: true, - standardFontDataUrl: 'https://unpkg.com/pdfjs-dist@5.4.296/standard_fonts/', - }), []) + // 离线部署环境不依赖 unpkg 等外部静态资源。 + const pdfOptions = useMemo(() => undefined, []) // 根据 PDF 实际宽高和缩放比例计算页面高度 const pageHeight = useMemo(() => { diff --git a/frontend/index.html b/frontend/index.html index d758788..5a7946d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,9 +4,6 @@ - - - MeetingAI - 智能会议系统 diff --git a/frontend/src/components/business/MeetingCreateDrawer.tsx b/frontend/src/components/business/MeetingCreateDrawer.tsx index 8598e4a..35ceb51 100644 --- a/frontend/src/components/business/MeetingCreateDrawer.tsx +++ b/frontend/src/components/business/MeetingCreateDrawer.tsx @@ -211,7 +211,7 @@ export const MeetingCreateDrawer: React.FC = ({ open, tags: meetingValues.tags?.join(",") || "", mode: meetingValues.mode || "2pass", language: meetingValues.language || "auto", - useSpkId: meetingValues.useSpkId ? 1 : 0, + useSpkId: meetingValues.useSpkId == null ? 1 : (meetingValues.useSpkId ? 1 : 0), enablePunctuation: meetingValues.enablePunctuation !== false, enableItn: meetingValues.enableItn !== false, enableTextRefine: !!meetingValues.enableTextRefine, @@ -229,7 +229,7 @@ export const MeetingCreateDrawer: React.FC = ({ open, asrModelId: selectedAsrModel?.id || values.asrModelId, mode: values.mode || "2pass", language: values.language || "auto", - useSpkId: values.useSpkId ? 1 : 0, + useSpkId: values.useSpkId == null ? 1 : (values.useSpkId ? 1 : 0), enablePunctuation: values.enablePunctuation !== false, enableItn: values.enableItn !== false, enableTextRefine: !!values.enableTextRefine, @@ -394,6 +394,7 @@ export const MeetingCreateDrawer: React.FC = ({ open, items={[ { key: 'advanced', + forceRender: true, label: (
diff --git a/frontend/src/components/shared/PDFViewer/VirtualPDFViewer.jsx b/frontend/src/components/shared/PDFViewer/VirtualPDFViewer.jsx index 830e9cd..51df365 100644 --- a/frontend/src/components/shared/PDFViewer/VirtualPDFViewer.jsx +++ b/frontend/src/components/shared/PDFViewer/VirtualPDFViewer.jsx @@ -28,12 +28,8 @@ function VirtualPDFViewer({ url, filename }) { // 使用 useMemo 避免不必要的重新加载 const fileConfig = useMemo(() => ({ url }), [url]) - // Memoize PDF.js options to prevent unnecessary reloads - const pdfOptions = useMemo(() => ({ - cMapUrl: 'https://unpkg.com/pdfjs-dist@5.4.296/cmaps/', - cMapPacked: true, - standardFontDataUrl: 'https://unpkg.com/pdfjs-dist@5.4.296/standard_fonts/', - }), []) + // 离线部署环境不依赖 unpkg 等外部静态资源。 + const pdfOptions = useMemo(() => undefined, []) // 根据 PDF 实际宽高和缩放比例计算页面高度 const pageHeight = useMemo(() => { diff --git a/frontend/src/pages/auth/login/index.tsx b/frontend/src/pages/auth/login/index.tsx index 35faa76..3bf2510 100644 --- a/frontend/src/pages/auth/login/index.tsx +++ b/frontend/src/pages/auth/login/index.tsx @@ -228,8 +228,8 @@ export default function Login() {
- {t("login.demoAccount")} admin / {t("login.password")}{" "} - 123456 + {/*{t("login.demoAccount")} admin / {t("login.password")}{" "}*/} + {/*123456*/}
diff --git a/frontend/src/pages/business/HotWords.tsx b/frontend/src/pages/business/HotWords.tsx index d0479c5..2ed5883 100644 --- a/frontend/src/pages/business/HotWords.tsx +++ b/frontend/src/pages/business/HotWords.tsx @@ -302,7 +302,7 @@ const HotWords: React.FC = () => { dataSource={data} rowKey="id" loading={loading} - scroll={{ y: "calc(100vh - 340px)" }} + scroll={{ y: "calc(100vh - 440px)" }} pagination={{ current, pageSize: size, diff --git a/frontend/src/pages/business/SpeakerReg.tsx b/frontend/src/pages/business/SpeakerReg.tsx index fddaeed..0d66b98 100644 --- a/frontend/src/pages/business/SpeakerReg.tsx +++ b/frontend/src/pages/business/SpeakerReg.tsx @@ -27,7 +27,7 @@ const { Search } = Input; const REG_CONTENT = 'iMeeting 智能会议系统,助力高效办公,让每一场讨论都有据可查。我正在进行声纹注册,以确保会议识别的准确性。'; -const DEFAULT_DURATION = 10; +const DEFAULT_DURATION = 15; const DEFAULT_PAGE_SIZE = 8; const SpeakerReg: React.FC = () => { diff --git a/frontend/src/pages/home/index.less b/frontend/src/pages/home/index.less index 03f461e..55e88fa 100644 --- a/frontend/src/pages/home/index.less +++ b/frontend/src/pages/home/index.less @@ -6,13 +6,12 @@ .home-container { position: relative; flex: 1; - height: 100%; - padding: clamp(24px, 4vw, 40px) clamp(24px, 5vw, 60px); + min-height: 100%; // Changed from height: 100% to avoid height conflicts + padding: clamp(40px, 8vh, 80px) clamp(24px, 5vw, 60px) 0; // Top shift down, ZERO bottom padding background: linear-gradient(135deg, #ffffff 0%, #f1f5f9 100%); color: @home-text-main; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; overflow-x: hidden; - overflow-y: auto; z-index: 1; display: flex; flex-direction: column; @@ -60,23 +59,24 @@ max-width: 1400px; width: 100%; margin: 0 auto; - flex: 1; // 占满剩余高度 + flex: 1; display: flex; flex-direction: column; justify-content: flex-start; + gap: clamp(24px, 5vh, 48px); } .home-hero { - margin-bottom: 56px; - padding-top: 32px; + margin-bottom: 0; + padding-top: 0; } .home-title { - font-size: clamp(40px, 5vw, 64px) !important; + font-size: clamp(32px, 4.5vw, 56px) !important; // Restored size font-weight: 800 !important; - margin-bottom: 56px !important; + margin-bottom: clamp(32px, 6vh, 64px) !important; // Restored margin color: @home-text-main !important; - letter-spacing: -0.02em; // Tighter tracking + letter-spacing: -0.02em; display: flex; align-items: center; flex-wrap: wrap; @@ -85,7 +85,7 @@ position: relative; display: inline-flex; flex-direction: column; - height: 1.2em; /* fixed height for scroller */ + height: 1.2em; overflow: hidden; vertical-align: bottom; } @@ -99,10 +99,10 @@ .home-title-accent { display: flex; align-items: center; - height: 1.2em; /* strictly matched to wrapper height */ + height: 1.2em; line-height: 1.2em; white-space: nowrap; - background: linear-gradient(90deg, #4f46e5, #7c3aed, #2563eb); // Deeper, more elegant gradient + background: linear-gradient(90deg, #4f46e5, #7c3aed, #2563eb); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; @@ -112,11 +112,11 @@ .home-quick-actions { display: flex; - gap: 64px; + gap: clamp(24px, 4vw, 48px); flex-wrap: wrap; @media (max-width: 768px) { - gap: 24px; + gap: 16px; flex-direction: column; align-items: stretch; @@ -136,7 +136,7 @@ max-width: 280px; flex: 1; min-width: 240px; - padding: 32px 24px; + padding: 32px 24px; // Restored original padding border-radius: 20px; background: var(--action-bg-gradient); transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.4s ease; @@ -195,9 +195,9 @@ .home-action-icon-wrapper { position: relative; - width: 72px; - height: 72px; - margin-bottom: 32px; + width: 72px; // Restored original size + height: 72px; + margin-bottom: 32px; // Restored margin transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); } @@ -259,7 +259,7 @@ } .home-action-title { - font-size: 24px !important; + font-size: 24px !important; // Restored original size font-weight: 700 !important; margin-bottom: 16px !important; color: #1e1e38 !important; @@ -273,14 +273,14 @@ } .home-action-line { - font-size: 15px; + font-size: 15px; // Restored original size color: #5a5a72; line-height: 1.5; } } .home-recent-section { - margin-top: auto; + margin-top: 0; } .home-section-header { @@ -325,8 +325,8 @@ position: relative; display: flex; flex-direction: column; - min-height: 140px; - padding: 20px 24px 20px; + min-height: 140px; // Restored original height + padding: 20px 24px; // Restored original padding cursor: pointer; overflow: hidden; border-radius: 20px; @@ -421,12 +421,12 @@ .home-recent-card-title { margin: 0 !important; color: #2d2c59 !important; - font-size: 17px !important; + font-size: 17px !important; // Restored original size line-height: 1.4 !important; font-weight: 700 !important; display: -webkit-box; overflow: hidden; - -webkit-line-clamp: 2; + -webkit-line-clamp: 2; // Restored original lines -webkit-box-orient: vertical; } diff --git a/frontend/src/pages/home/index.tsx b/frontend/src/pages/home/index.tsx index 3320e8a..9d504b2 100644 --- a/frontend/src/pages/home/index.tsx +++ b/frontend/src/pages/home/index.tsx @@ -62,7 +62,7 @@ function buildRecentCards(tasks: MeetingVO[]): RecentCard[] { return fallbackRecentCards; } - return tasks.slice(0, 3).map((task, index) => ({ + return tasks.slice(0, 4).map((task, index) => ({ id: task.id, title: task.title, duration: `0${index + 1}:${10 + index * 12}`, @@ -142,7 +142,7 @@ export default function HomePage() { description: ["音视频转文字", "区分发言人,一键导出"], accent: "cyan", badge: "iMeeting", - onClick: () => navigate("/meetings?create=true") + onClick: () => navigate("/meetings?action=create&type=upload") } ], [navigate]