feat: 添加音频文件大小验证和系统参数配置
- 在 `MeetingAudioUploadSupport` 中添加 `validateFileSize` 方法,验证上传的音频文件大小 - 引入 `SysParamService` 以获取系统参数配置的最大上传大小 - 在前端 `MeetingCreateDrawer.tsx` 中添加文件大小验证逻辑,并显示系统配置的最大大小dev_na
parent
2f80c6c55e
commit
e6580beaa8
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String> 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;
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ public class MeetingQueryServiceImpl implements MeetingQueryService {
|
|||
@Override
|
||||
public PageResult<List<MeetingVO>> pageMeetings(Integer current, Integer size, String title, Long tenantId,
|
||||
Long userId, String userName, String viewType, boolean isAdmin) {
|
||||
LambdaQueryWrapper<Meeting> wrapper = new LambdaQueryWrapper<Meeting>()
|
||||
.eq(Meeting::getTenantId, tenantId);
|
||||
LambdaQueryWrapper<Meeting> wrapper = new LambdaQueryWrapper<Meeting>();
|
||||
|
||||
if (!isAdmin || !"all".equals(viewType)) {
|
||||
String userIdStr = String.valueOf(userId);
|
||||
|
|
|
|||
|
|
@ -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<MeetingCreateDrawerProps> = ({ open,
|
|||
const [audioUrl, setAudioUrl] = useState('');
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [fileList, setFileList] = useState<any[]>([]);
|
||||
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<MeetingCreateDrawerProps> = ({ 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<MeetingCreateDrawerProps> = ({ 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<MeetingCreateDrawerProps> = ({ 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<MeetingCreateDrawerProps> = ({ 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<MeetingCreateDrawerProps> = ({ open,
|
|||
</div>
|
||||
<Dragger
|
||||
accept=".mp3,.wav,.m4a"
|
||||
beforeUpload={beforeAudioUpload}
|
||||
fileList={fileList}
|
||||
customRequest={customUpload}
|
||||
onChange={info => setFileList(info.fileList.slice(-1))}
|
||||
|
|
@ -482,6 +515,7 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
|
|||
<p className="ant-upload-drag-icon" style={{ marginBottom: 16 }}><CloudUploadOutlined style={{ fontSize: 56, color: '#1890ff' }} /></p>
|
||||
<p className="ant-upload-text" style={{ fontSize: 18, fontWeight: 500, color: 'var(--app-text-main)' }}>点击或拖拽录音文件到此处</p>
|
||||
<p className="ant-upload-hint" style={{ fontSize: 14, marginTop: 12, color: 'var(--app-text-secondary)' }}>支持高质量 .mp3, .wav, .m4a 格式音频</p>
|
||||
<p className="ant-upload-hint" style={{ fontSize: 13, marginTop: 8, color: 'var(--app-text-secondary)' }}>文件大小不超过 {offlineAudioMaxSizeMb}MB,取值来自系统参数配置</p>
|
||||
{uploadProgress > 0 && uploadProgress < 100 && (
|
||||
<div style={{ width: '60%', margin: '32px auto 0' }}>
|
||||
<Progress percent={uploadProgress} size="small" />
|
||||
|
|
|
|||
Loading…
Reference in New Issue