From e6580beaa8866bcf739201098bb7299ba0bf851b Mon Sep 17 00:00:00 2001 From: chenhao Date: Fri, 24 Apr 2026 15:47:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=9F=B3=E9=A2=91?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=A4=A7=E5=B0=8F=E9=AA=8C=E8=AF=81=E5=92=8C?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E5=8F=82=E6=95=B0=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 `MeetingAudioUploadSupport` 中添加 `validateFileSize` 方法,验证上传的音频文件大小 - 引入 `SysParamService` 以获取系统参数配置的最大上传大小 - 在前端 `MeetingCreateDrawer.tsx` 中添加文件大小验证逻辑,并显示系统配置的最大大小 --- .../com/imeeting/common/SysParamKeys.java | 1 + .../biz/impl/MeetingAudioUploadSupport.java | 32 +++++++++++++++ .../biz/impl/MeetingQueryServiceImpl.java | 3 +- .../business/MeetingCreateDrawer.tsx | 40 +++++++++++++++++-- 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/com/imeeting/common/SysParamKeys.java b/backend/src/main/java/com/imeeting/common/SysParamKeys.java index b38b01a..d016b75 100644 --- a/backend/src/main/java/com/imeeting/common/SysParamKeys.java +++ b/backend/src/main/java/com/imeeting/common/SysParamKeys.java @@ -5,4 +5,5 @@ public final class SysParamKeys { public static final String CAPTCHA_ENABLED = "security.captcha.enabled"; public static final String MEETING_SUMMARY_SYSTEM_PROMPT = "meeting.summary.system_prompt"; + public static final String MEETING_OFFLINE_AUDIO_MAX_SIZE_MB = "meeting.offline_audio.max_size_mb"; } diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingAudioUploadSupport.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingAudioUploadSupport.java index 926adb8..1796ac2 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingAudioUploadSupport.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingAudioUploadSupport.java @@ -1,5 +1,8 @@ package com.imeeting.service.biz.impl; +import com.imeeting.common.SysParamKeys; +import com.unisbase.service.SysParamService; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @@ -17,9 +20,11 @@ import java.util.Set; import java.util.UUID; @Component +@RequiredArgsConstructor public class MeetingAudioUploadSupport { public static final String STAGING_AUDIO_TOKEN_PREFIX = "staging:audio/"; + private static final long DEFAULT_MAX_UPLOAD_SIZE_MB = 1024L; private static final int HEADER_SIZE = 32; private static final Set SUPPORTED_EXTENSIONS = Set.of("mp3", "wav", "m4a"); @@ -30,11 +35,14 @@ public class MeetingAudioUploadSupport { @Value("${unisbase.app.upload-path}") private String uploadPath; + private final SysParamService sysParamService; + public String storeUploadedAudio(MultipartFile file) throws IOException { if (file == null || file.isEmpty()) { throw new RuntimeException("音频文件不能为空"); } + validateFileSize(file); String extension = resolveExtension(file.getOriginalFilename()); // validateContentType(file.getContentType(), extension); validateFileHeader(file, extension); @@ -106,6 +114,30 @@ public class MeetingAudioUploadSupport { return extension; } + private void validateFileSize(MultipartFile file) { + long maxUploadSizeMb = resolveMaxUploadSizeMb(); + long maxUploadSizeBytes = maxUploadSizeMb * 1024 * 1024; + if (file.getSize() > maxUploadSizeBytes) { + throw new RuntimeException("音频文件大小不能超过 " + maxUploadSizeMb + "MB"); + } + } + + private long resolveMaxUploadSizeMb() { + String configured = sysParamService.getCachedParamValue( + SysParamKeys.MEETING_OFFLINE_AUDIO_MAX_SIZE_MB, + String.valueOf(DEFAULT_MAX_UPLOAD_SIZE_MB) + ); + if (!StringUtils.hasText(configured)) { + return DEFAULT_MAX_UPLOAD_SIZE_MB; + } + try { + long parsed = Long.parseLong(configured.trim()); + return parsed > 0 ? parsed : DEFAULT_MAX_UPLOAD_SIZE_MB; + } catch (NumberFormatException ex) { + return DEFAULT_MAX_UPLOAD_SIZE_MB; + } + } + private void validateContentType(String contentType, String extension) { if (!StringUtils.hasText(contentType)) { return; diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingQueryServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingQueryServiceImpl.java index a1fc06f..b9a37e5 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingQueryServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingQueryServiceImpl.java @@ -32,8 +32,7 @@ public class MeetingQueryServiceImpl implements MeetingQueryService { @Override public PageResult> pageMeetings(Integer current, Integer size, String title, Long tenantId, Long userId, String userName, String viewType, boolean isAdmin) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper() - .eq(Meeting::getTenantId, tenantId); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper(); if (!isAdmin || !"all".equals(viewType)) { String userIdStr = String.valueOf(userId); diff --git a/frontend/src/components/business/MeetingCreateDrawer.tsx b/frontend/src/components/business/MeetingCreateDrawer.tsx index 365d635..b7bc526 100644 --- a/frontend/src/components/business/MeetingCreateDrawer.tsx +++ b/frontend/src/components/business/MeetingCreateDrawer.tsx @@ -7,7 +7,7 @@ import { useNavigate } from 'react-router-dom'; import { getAiModelPage, getAiModelDefault, AiModelVO } from '../../api/business/aimodel'; import { getPromptPage, PromptTemplateVO } from '../../api/business/prompt'; import { getHotWordPage, HotWordVO } from '../../api/business/hotword'; -import { listUsers } from '../../api'; +import { listUsers, pageParams } from '../../api'; import { createMeeting, createRealtimeMeeting, uploadAudio, CreateRealtimeMeetingCommand } from '../../api/business/meeting'; import { SysUser } from '../../types'; @@ -17,6 +17,9 @@ const { Text, Title } = Typography; export type MeetingCreateType = 'upload' | 'realtime'; +const DEFAULT_OFFLINE_AUDIO_MAX_SIZE_MB = 1024; +const OFFLINE_AUDIO_MAX_SIZE_PARAM_KEY = 'meeting.offline_audio.max_size_mb'; + interface MeetingCreateDrawerProps { open: boolean; initialType?: MeetingCreateType; @@ -73,6 +76,7 @@ export const MeetingCreateDrawer: React.FC = ({ open, const [audioUrl, setAudioUrl] = useState(''); const [uploadProgress, setUploadProgress] = useState(0); const [fileList, setFileList] = useState([]); + const [offlineAudioMaxSizeMb, setOfflineAudioMaxSizeMb] = useState(DEFAULT_OFFLINE_AUDIO_MAX_SIZE_MB); const watchedAsrModelId = Form.useWatch("asrModelId", form); const watchedPromptId = Form.useWatch("promptId", form); @@ -80,6 +84,7 @@ export const MeetingCreateDrawer: React.FC = ({ open, const selectedAsrModel = useMemo(() => asrModels.find((item) => item.id === watchedAsrModelId) || null, [asrModels, watchedAsrModelId]); const selectedSummaryModel = useMemo(() => llmModels.find((item) => item.id === watchedSummaryModelId) || null, [llmModels, watchedSummaryModelId]); + const offlineAudioMaxSizeBytes = useMemo(() => offlineAudioMaxSizeMb * 1024 * 1024, [offlineAudioMaxSizeMb]); useEffect(() => { if (open) { @@ -101,7 +106,7 @@ export const MeetingCreateDrawer: React.FC = ({ open, getHotWordPage({ current: 1, size: 1000 }), listUsers(), getAiModelDefault("ASR"), - getAiModelDefault("LLM") + getAiModelDefault("LLM"), ]); const activeAsrModels = asrRes.data.data.records.filter((m: AiModelVO) => m.status === 1); @@ -114,6 +119,7 @@ export const MeetingCreateDrawer: React.FC = ({ open, setPrompts(activePrompts); setHotwordList(activeHotwords); setUserList(users || []); + setOfflineAudioMaxSizeMb(await loadOfflineAudioMaxSizeMb()); form.setFieldsValue({ title: type === 'upload' ? `文件会议 ${dayjs().format("MM-DD HH:mm")}` : `实时会议 ${dayjs().format("MM-DD HH:mm")}`, @@ -168,7 +174,33 @@ export const MeetingCreateDrawer: React.FC = ({ open, message.success('录音上传成功'); } catch (err) { onError(err); - message.error('文件上传失败'); + if (!(err instanceof Error) || !err.message) { + message.error('文件上传失败'); + } + } + }; + + const beforeAudioUpload = (file: File) => { + if (file.size > offlineAudioMaxSizeBytes) { + message.error(`录音文件大小不能超过 ${offlineAudioMaxSizeMb}MB`); + setUploadProgress(0); + return Upload.LIST_IGNORE; + } + return true; + }; + + const loadOfflineAudioMaxSizeMb = async () => { + try { + const result = await pageParams({ + paramKey: OFFLINE_AUDIO_MAX_SIZE_PARAM_KEY, + pageNum: 1, + pageSize: 10, + }); + const matched = (result.records || []).find((item) => item.paramKey === OFFLINE_AUDIO_MAX_SIZE_PARAM_KEY); + const parsed = Number(matched?.paramValue); + return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_OFFLINE_AUDIO_MAX_SIZE_MB; + } catch { + return DEFAULT_OFFLINE_AUDIO_MAX_SIZE_MB; } }; @@ -472,6 +504,7 @@ export const MeetingCreateDrawer: React.FC = ({ open, setFileList(info.fileList.slice(-1))} @@ -482,6 +515,7 @@ export const MeetingCreateDrawer: React.FC = ({ open,

点击或拖拽录音文件到此处

支持高质量 .mp3, .wav, .m4a 格式音频

+

文件大小不超过 {offlineAudioMaxSizeMb}MB,取值来自系统参数配置

{uploadProgress > 0 && uploadProgress < 100 && (