From 2660f65bd252a71cec9f565a19d381bc293e5464 Mon Sep 17 00:00:00 2001 From: chenhao Date: Fri, 7 Aug 2026 10:43:11 +0800 Subject: [PATCH] =?UTF-8?q?refactor(core):=20=E9=87=8D=E6=9E=84=E4=BC=9A?= =?UTF-8?q?=E8=AE=AE=E6=8E=A5=E5=8F=A3=E5=B9=B6=E7=BB=9F=E4=B8=80=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E6=8D=A2=E8=A1=8C=E7=AC=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 调整前端会议接口类型,新增 MeetingParticipant 定义 - 统一全项目文件换行符为 LF,清理 CRLF 格式 - 完善 .gitignore 规则与 H5 端基础类型定义 --- .../java/com/imeeting/dto/biz/MeetingVO.java | 3 ++ .../service/biz/impl/HotWordServiceImpl.java | 46 ++++++++++++++++--- .../biz/impl/MeetingDomainSupport.java | 27 ++++++++++- frontend/src/api/business/meeting.ts | 6 +++ .../business/MeetingCreateDrawer.tsx | 12 ++++- frontend/src/pages/business/HotWords.tsx | 17 +++++-- frontend/src/pages/business/MeetingDetail.tsx | 10 +++- .../src/pages/business/PromptTemplates.tsx | 7 ++- imeeting-h5/src/types/index.ts | 6 +++ 9 files changed, 117 insertions(+), 17 deletions(-) diff --git a/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java b/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java index da4de7a..0771f3b 100644 --- a/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java +++ b/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java @@ -42,6 +42,9 @@ public class MeetingVO { @Schema(description = "参会人ID列表") private List participantIds; + @Schema(description = "参会人列表,ID 与名称一一对应") + private List participantUsers; + @Schema(description = "标签串") private String tags; diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/HotWordServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/HotWordServiceImpl.java index 45952ed..c58763b 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/HotWordServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/HotWordServiceImpl.java @@ -13,6 +13,8 @@ import com.imeeting.mapper.biz.HotWordGroupMapper; import com.imeeting.mapper.biz.HotWordMapper; import com.imeeting.service.biz.HotWordService; import com.unisbase.common.exception.BusinessException; +import com.unisbase.dto.SysDictItemDTO; +import com.unisbase.service.SysDictItemService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import net.sourceforge.pinyin4j.PinyinHelper; @@ -35,12 +37,14 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class HotWordServiceImpl extends ServiceImpl implements HotWordService { - private static final int MAX_HOT_WORDS_PER_GROUP = 200; + private static final String HOT_WORD_GROUP_LIMIT_DICT_TYPE = "biz_hotword_group_limit"; + private static final int DEFAULT_MAX_HOT_WORDS_PER_GROUP = 200; private static final int DEFAULT_MATCH_STRATEGY = 1; private static final int DEFAULT_WEIGHT = 2; private static final int ENABLED_STATUS = 1; private final HotWordGroupMapper hotWordGroupMapper; + private final SysDictItemService sysDictItemService; @Override @Transactional(rollbackFor = Exception.class) @@ -201,8 +205,9 @@ public class HotWordServiceImpl extends ServiceImpl impl long currentCount = this.count(new LambdaQueryWrapper() .eq(HotWord::getHotWordGroupId, groupId) .ne(currentHotWordId != null, HotWord::getId, currentHotWordId)); - if (currentCount >= MAX_HOT_WORDS_PER_GROUP) { - throw new BusinessException("热词组最多只能包含 200 个热词"); + int maxHotWordsPerGroup = getMaxHotWordsPerGroup(); + if (currentCount >= maxHotWordsPerGroup) { + throwGroupCapacityExceeded(maxHotWordsPerGroup); } return group.getId(); } @@ -222,8 +227,9 @@ public class HotWordServiceImpl extends ServiceImpl impl long incomingCount = movingHotWords.stream() .filter(item -> !groupId.equals(item.getHotWordGroupId())) .count(); - if (currentCount + incomingCount > MAX_HOT_WORDS_PER_GROUP) { - throw new BusinessException("热词组最多只能包含 200 个热词"); + int maxHotWordsPerGroup = getMaxHotWordsPerGroup(); + if (currentCount + incomingCount > maxHotWordsPerGroup) { + throwGroupCapacityExceeded(maxHotWordsPerGroup); } } @@ -268,8 +274,9 @@ public class HotWordServiceImpl extends ServiceImpl impl } long currentCount = this.count(new LambdaQueryWrapper() .eq(HotWord::getHotWordGroupId, groupId)); - if (currentCount + incomingCount > MAX_HOT_WORDS_PER_GROUP) { - throw new BusinessException("热词组最多只能包含 200 个热词"); + int maxHotWordsPerGroup = getMaxHotWordsPerGroup(); + if (currentCount + incomingCount > maxHotWordsPerGroup) { + throwGroupCapacityExceeded(maxHotWordsPerGroup); } } @@ -288,6 +295,31 @@ public class HotWordServiceImpl extends ServiceImpl impl return hotWord; } + private int getMaxHotWordsPerGroup() { + List items = sysDictItemService.getItemsByTypeCode(HOT_WORD_GROUP_LIMIT_DICT_TYPE); + if (items == null || items.isEmpty()) { + return DEFAULT_MAX_HOT_WORDS_PER_GROUP; + } + for (SysDictItemDTO item : items) { + if (item == null || item.getItemValue() == null) { + continue; + } + try { + int configuredLimit = Integer.parseInt(item.getItemValue().trim()); + if (configuredLimit > 0) { + return configuredLimit; + } + } catch (NumberFormatException exception) { + log.warn("热词组上限字典配置值非法: {}", item.getItemValue()); + } + } + return DEFAULT_MAX_HOT_WORDS_PER_GROUP; + } + + private void throwGroupCapacityExceeded(int maxHotWordsPerGroup) { + throw new BusinessException("热词组最多只能包含 " + maxHotWordsPerGroup + " 个热词"); + } + private void generateCombinations(List> matrix, int index, String current, List result) { if (index == matrix.size()) { result.add(current.trim()); 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 6d2b05c..0105a2d 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 @@ -9,6 +9,7 @@ import com.imeeting.entity.biz.HotWordGroup; import com.imeeting.entity.biz.Meeting; import com.imeeting.entity.biz.MeetingTranscript; import com.imeeting.entity.biz.PromptTemplate; +import com.imeeting.dto.biz.MeetingParticipantVO; import com.imeeting.event.MeetingCreatedEvent; import com.imeeting.mapper.biz.MeetingTranscriptMapper; import com.imeeting.dto.biz.AiModelVO; @@ -23,6 +24,7 @@ import com.imeeting.service.realtime.RealtimeMeetingAudioStorageService; import com.unisbase.entity.SysUser; import com.unisbase.mapper.SysUserMapper; import com.unisbase.service.SysParamService; +import com.unisbase.service.SysTenantUserService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -47,6 +49,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.UUID; import java.util.stream.Collectors; @@ -61,6 +64,7 @@ public class MeetingDomainSupport { private final MeetingTranscriptMapper transcriptMapper; private final MeetingPointsService meetingPointsService; private final SysUserMapper sysUserMapper; + private final SysTenantUserService sysTenantUserService; private final ApplicationEventPublisher eventPublisher; private final MeetingSummaryFileService meetingSummaryFileService; private final MeetingPlaybackAudioResolver meetingPlaybackAudioResolver; @@ -460,19 +464,31 @@ public class MeetingDomainSupport { .map(Long::valueOf) .collect(Collectors.toList()); vo.setParticipantIds(userIds); + vo.setParticipantUsers(Collections.emptyList()); if (!userIds.isEmpty()) { List users = sysUserMapper.selectBatchIds(userIds); - String names = users.stream() - .map(u -> u.getDisplayName() != null ? u.getDisplayName() : u.getUsername()) + Map userNameMap = users.stream().collect(Collectors.toMap( + SysUser::getUserId, + user -> resolveParticipantName(user, meeting.getTenantId()) + )); + List participantUsers = userIds.stream() + .map(userId -> new MeetingParticipantVO(userId, userNameMap.get(userId))) + .collect(Collectors.toList()); + vo.setParticipantUsers(participantUsers); + String names = participantUsers.stream() + .map(MeetingParticipantVO::getName) + .filter(Objects::nonNull) .collect(Collectors.joining(", ")); vo.setParticipants(names); } } catch (Exception ex) { vo.setParticipantIds(Collections.emptyList()); + vo.setParticipantUsers(Collections.emptyList()); vo.setParticipants(meeting.getParticipants()); } } else { vo.setParticipantIds(Collections.emptyList()); + vo.setParticipantUsers(Collections.emptyList()); } fillLatestTaskAttemptInfo(meeting, vo); if (includeSummary) { @@ -482,6 +498,13 @@ public class MeetingDomainSupport { } } + private String resolveParticipantName(SysUser user, Long tenantId) { + if (user == null || user.getUserId() == null) { + return ""; + } + return user.getDisplayName() != null ? user.getDisplayName() : user.getUsername(); + } + private void fillSummaryConfigurationNames(Meeting meeting, com.imeeting.dto.biz.MeetingVO vo) { if (meeting.getSummaryModelId() != null) { AiModelVO summaryModel = aiModelService.getModelById(meeting.getSummaryModelId(), "LLM"); diff --git a/frontend/src/api/business/meeting.ts b/frontend/src/api/business/meeting.ts index ed19cd4..8fbf189 100644 --- a/frontend/src/api/business/meeting.ts +++ b/frontend/src/api/business/meeting.ts @@ -7,6 +7,11 @@ const MEETING_DETAIL_TIMEOUT = 120000; export type SummaryDetailLevel = "DETAILED" | "STANDARD" | "BRIEF"; export type MeetingSource = "WINDOWS" | "MACOS" | "KYLIN" | "UOS" | "HARMONYOS" | "WEB" | "CUSTOM_TERMINAL" | "ANDROID"; +export interface MeetingParticipant { + id: number; + name: string | null; +} + export interface MeetingCreateConfig { offlineEnabled: boolean; realtimeEnabled: boolean; @@ -27,6 +32,7 @@ export interface MeetingVO { meetingTime: string; participants: string; participantIds?: number[]; + participantUsers?: MeetingParticipant[]; tags: string; audioUrl: string; playbackAudioUrl?: string; diff --git a/frontend/src/components/business/MeetingCreateDrawer.tsx b/frontend/src/components/business/MeetingCreateDrawer.tsx index aa94a4f..bea65ae 100644 --- a/frontend/src/components/business/MeetingCreateDrawer.tsx +++ b/frontend/src/components/business/MeetingCreateDrawer.tsx @@ -46,6 +46,7 @@ import { uploadAudio, } from "../../api/business/meeting"; import { getPromptPage, type PromptTemplateVO } from "../../api/business/prompt"; +import {useHotWordGroupLimit} from "../../hooks/useHotWordGroupLimit"; import type { SysUser } from "../../types"; import "./MeetingCreateDrawer.css"; @@ -121,6 +122,7 @@ export const MeetingCreateDrawer: React.FC = ({ onSuccess, }) => { const { message } = App.useApp(); + const {limit: hotWordGroupLimit} = useHotWordGroupLimit(); const navigate = useNavigate(); const [form] = Form.useForm(); @@ -559,7 +561,15 @@ export const MeetingCreateDrawer: React.FC = ({ - ({ + label: `${item.groupName} (${item.hotWordCount}/${hotWordGroupLimit})`, + value: item.id + }))]}/> diff --git a/frontend/src/pages/business/HotWords.tsx b/frontend/src/pages/business/HotWords.tsx index 52fc565..0feca31 100644 --- a/frontend/src/pages/business/HotWords.tsx +++ b/frontend/src/pages/business/HotWords.tsx @@ -32,6 +32,7 @@ import { } from "@ant-design/icons"; import { useTranslation } from "react-i18next"; import { useDict } from "../../hooks/useDict"; +import {useHotWordGroupLimit} from "../../hooks/useHotWordGroupLimit"; import { deleteHotWord, getHotWordPage, @@ -97,6 +98,7 @@ const HotWords: React.FC = () => { const [groupForm] = Form.useForm(); const [bulkGroupForm] = Form.useForm(); const { items: categories } = useDict("biz_hotword_category"); + const {limit: hotWordGroupLimit} = useHotWordGroupLimit(); const userProfile = useMemo(() => { const profileStr = sessionStorage.getItem("userProfile"); return profileStr ? JSON.parse(profileStr) : {}; @@ -606,8 +608,9 @@ const HotWords: React.FC = () => { item.id ? ( - = 200 ? "red" : item.status === 1 ? "processing" : "default"}> - {item.hotWordCount}/200 + = hotWordGroupLimit ? "red" : item.status === 1 ? "processing" : "default"}> + {item.hotWordCount}/{hotWordGroupLimit} {item.remark || "暂无备注"} @@ -766,7 +769,10 @@ const HotWords: React.FC = () => { - ({ + label: `${item.groupName} (${item.hotWordCount}/${hotWordGroupLimit})`, + value: item.id + }))}/> @@ -843,7 +849,10 @@ const HotWords: React.FC = () => { placeholder="请选择热词组" options={[ { label: "未分组", value: 0 }, - ...groupOptions.map((item) => ({ label: `${item.groupName} (${item.hotWordCount}/200)`, value: item.id })), + ...groupOptions.map((item) => ({ + label: `${item.groupName} (${item.hotWordCount}/${hotWordGroupLimit})`, + value: item.id + })), ]} /> diff --git a/frontend/src/pages/business/MeetingDetail.tsx b/frontend/src/pages/business/MeetingDetail.tsx index 138f9b1..fd3302d 100644 --- a/frontend/src/pages/business/MeetingDetail.tsx +++ b/frontend/src/pages/business/MeetingDetail.tsx @@ -54,6 +54,7 @@ import {getHotWordGroupOptions, type HotWordGroupVO} from '../../api/business/ho import { getPromptPage, PromptTemplateVO } from '../../api/business/prompt'; import { listUsers } from '../../api'; import { useDict } from '../../hooks/useDict'; +import {useHotWordGroupLimit} from '../../hooks/useHotWordGroupLimit'; import { SysUser } from '../../types'; import PageContainer from "../../components/shared/PageContainer"; import SectionCard from "../../components/shared/SectionCard"; @@ -1214,6 +1215,7 @@ const ActiveTranscriptRow = React.memo(({ const MeetingDetail: React.FC = () => { const { message } = App.useApp(); + const {limit: hotWordGroupLimit, loading: hotWordGroupLimitLoading} = useHotWordGroupLimit(); const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const [form] = Form.useForm(); @@ -1886,11 +1888,15 @@ const MeetingDetail: React.FC = () => { message.warning('请先选择关键词'); return; } + if (hotWordGroupLimitLoading) { + message.info('热词组上限配置加载中,请稍后重试'); + return; + } setHotWordGroupLoading(true); try { const response = await getHotWordGroupOptions(); - const options = (response.data?.data || []).filter((item) => item.status === 1 && item.hotWordCount < 200); + const options = (response.data?.data || []).filter((item) => item.status === 1 && item.hotWordCount < hotWordGroupLimit); setHotWordGroupOptions(options); setSelectedHotWordGroupId(options.some((item) => item.id === meeting?.hotWordGroupId) ? meeting?.hotWordGroupId : options[0]?.id); setHotWordGroupModalOpen(true); @@ -4262,7 +4268,7 @@ const MeetingDetail: React.FC = () => { onChange={setSelectedHotWordGroupId} options={[ ...hotWordGroupOptions.map((item) => ({ - label: `${item.groupName} (${item.hotWordCount}/200)`, + label: `${item.groupName} (${item.hotWordCount}/${hotWordGroupLimit})`, value: item.id, })), ]} diff --git a/frontend/src/pages/business/PromptTemplates.tsx b/frontend/src/pages/business/PromptTemplates.tsx index 0bb36e8..5532849 100644 --- a/frontend/src/pages/business/PromptTemplates.tsx +++ b/frontend/src/pages/business/PromptTemplates.tsx @@ -35,6 +35,7 @@ import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { useTranslation } from 'react-i18next'; import { useDict } from '../../hooks/useDict'; +import {useHotWordGroupLimit} from '../../hooks/useHotWordGroupLimit'; import { deletePromptTemplate, clearPromptDefault, @@ -69,6 +70,7 @@ const PromptTemplates: React.FC = () => { const { items: categories, loading: dictLoading } = useDict('biz_prompt_category'); const { items: dictTags } = useDict('biz_prompt_tag'); const { items: promptLevels } = useDict('biz_prompt_level'); + const {limit: hotWordGroupLimit} = useHotWordGroupLimit(); const [loading, setLoading] = useState(false); const [data, setData] = useState([]); @@ -585,7 +587,10 @@ const PromptTemplates: React.FC = () => {