refactor(core): 重构会议接口并统一代码换行符

- 调整前端会议接口类型,新增 MeetingParticipant 定义
- 统一全项目文件换行符为 LF,清理 CRLF 格式
- 完善 .gitignore 规则与 H5 端基础类型定义
dev_na
chenhao 2026-08-07 10:43:11 +08:00
parent e3a37757ba
commit 2660f65bd2
9 changed files with 117 additions and 17 deletions

View File

@ -42,6 +42,9 @@ public class MeetingVO {
@Schema(description = "参会人ID列表")
private List<Long> participantIds;
@Schema(description = "参会人列表ID 与名称一一对应")
private List<MeetingParticipantVO> participantUsers;
@Schema(description = "标签串")
private String tags;

View File

@ -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<HotWordMapper, HotWord> 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<HotWordMapper, HotWord> impl
long currentCount = this.count(new LambdaQueryWrapper<HotWord>()
.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<HotWordMapper, HotWord> 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<HotWordMapper, HotWord> impl
}
long currentCount = this.count(new LambdaQueryWrapper<HotWord>()
.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<HotWordMapper, HotWord> impl
return hotWord;
}
private int getMaxHotWordsPerGroup() {
List<SysDictItemDTO> 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<List<String>> matrix, int index, String current, List<String> result) {
if (index == matrix.size()) {
result.add(current.trim());

View File

@ -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<SysUser> users = sysUserMapper.selectBatchIds(userIds);
String names = users.stream()
.map(u -> u.getDisplayName() != null ? u.getDisplayName() : u.getUsername())
Map<Long, String> userNameMap = users.stream().collect(Collectors.toMap(
SysUser::getUserId,
user -> resolveParticipantName(user, meeting.getTenantId())
));
List<MeetingParticipantVO> 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");

View File

@ -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;

View File

@ -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<MeetingCreateDrawerProps> = ({
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<MeetingCreateDrawerProps> = ({
<Row gutter={24}>
<Col xs={24} md={12}>
<Form.Item name="hotWordGroupId" label="热词组" tooltip={selectedPrompt?.hotWordGroupName ? `默认跟随模板:${selectedPrompt.hotWordGroupName}` : "模板未绑定热词组时可手动选择"} extra={watchedHotWordGroupId != null ? "创建会议时会优先使用这里选中的热词组" : undefined}>
<Select placeholder={selectedPrompt?.hotWordGroupId ? "默认已带出模板热词组,可按需修改" : "请选择热词组"} options={[{ label: "不使用热词组", value: 0 }, ...hotWordGroups.map((item) => ({ label: `${item.groupName} (${item.hotWordCount}/200)`, value: item.id }))]} />
<Select
placeholder={selectedPrompt?.hotWordGroupId ? "默认已带出模板热词组,可按需修改" : "请选择热词组"}
options={[{
label: "不使用热词组",
value: 0
}, ...hotWordGroups.map((item) => ({
label: `${item.groupName} (${item.hotWordCount}/${hotWordGroupLimit})`,
value: item.id
}))]}/>
</Form.Item>
</Col>
<Col xs={24} md={12}>

View File

@ -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<HotWordGroupFormValues>();
const [bulkGroupForm] = Form.useForm<BulkGroupFormValues>();
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
? (
<span className="hotwords-group-item__desc">
<Tag color={item.hotWordCount >= 200 ? "red" : item.status === 1 ? "processing" : "default"}>
{item.hotWordCount}/200
<Tag
color={item.hotWordCount >= hotWordGroupLimit ? "red" : item.status === 1 ? "processing" : "default"}>
{item.hotWordCount}/{hotWordGroupLimit}
</Tag>
<span>{item.remark || "暂无备注"}</span>
</span>
@ -766,7 +769,10 @@ const HotWords: React.FC = () => {
</Col>
<Col xs={24} sm={12}>
<Form.Item name="hotWordGroupId" label="所属热词组">
<Select placeholder="请选择热词组" allowClear options={groupOptions.map((item) => ({ label: `${item.groupName} (${item.hotWordCount}/200)`, value: item.id }))} />
<Select placeholder="请选择热词组" allowClear options={groupOptions.map((item) => ({
label: `${item.groupName} (${item.hotWordCount}/${hotWordGroupLimit})`,
value: item.id
}))}/>
</Form.Item>
</Col>
</Row>
@ -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
})),
]}
/>
</Form.Item>

View File

@ -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<ActiveTranscriptRowProps>(({
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,
})),
]}

View File

@ -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<PromptTemplateVO[]>([]);
@ -585,7 +587,10 @@ const PromptTemplates: React.FC = () => {
<Select
placeholder="请选择热词组"
allowClear
options={groupOptions.map((item) => ({ label: `${item.groupName} (${item.hotWordCount}/200)`, value: item.id }))}
options={groupOptions.map((item) => ({
label: `${item.groupName} (${item.hotWordCount}/${hotWordGroupLimit})`,
value: item.id
}))}
/>
</Form.Item>
</Col>

View File

@ -25,6 +25,11 @@ export interface TokenResponse {
refreshExpiresInDays: number;
}
export interface MeetingParticipant {
id: number;
name: string | null;
}
export interface MeetingVO {
id: number;
tenantId: number;
@ -36,6 +41,7 @@ export interface MeetingVO {
meetingTime: string;
participants: string;
participantIds?: number[];
participantUsers?: MeetingParticipant[];
tags: string;
audioUrl: string;
playbackAudioUrl?: string;