feat(core): 实现热词批量创建及新增会议终端枚举
后端新增热词批量创建接口与相关DTO和VO,新增QtMeetingController支持Qt端会议业务,并新增MeetingTerminalEnum统一管理多终端类型。前端同步适配热词批量创建API并完善会议详情页的终端展示。dev_na
parent
c296600060
commit
eb32f13507
|
|
@ -2,6 +2,8 @@ package com.imeeting.controller.biz;
|
|||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.imeeting.dto.biz.HotWordBatchCreateDTO;
|
||||
import com.imeeting.dto.biz.HotWordBatchCreateResultVO;
|
||||
import com.imeeting.dto.biz.HotWordBatchGroupDTO;
|
||||
import com.imeeting.dto.biz.HotWordDTO;
|
||||
import com.imeeting.dto.biz.HotWordVO;
|
||||
|
|
@ -52,6 +54,18 @@ public class HotWordController {
|
|||
return ApiResponse.ok(hotWordService.saveHotWord(hotWordDTO, loginUser.getUserId(), targetTenantId));
|
||||
}
|
||||
|
||||
@Operation(summary = "批量新增热词")
|
||||
@PostMapping("/batch")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "批量新增热词", type = "热词管理")
|
||||
public ApiResponse<HotWordBatchCreateResultVO> saveBatch(@RequestBody HotWordBatchCreateDTO dto) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser, dto.getTenantId());
|
||||
boolean platformAdmin = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin());
|
||||
return ApiResponse.ok(
|
||||
hotWordService.saveHotWordsBatch(dto, loginUser.getUserId(), targetTenantId, platformAdmin));
|
||||
}
|
||||
|
||||
@Operation(summary = "修改热词")
|
||||
@PutMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
|
|
|
|||
|
|
@ -26,11 +26,8 @@ public class HotWordGroupController {
|
|||
this.hotWordGroupService = hotWordGroupService;
|
||||
}
|
||||
|
||||
private Long resolveTargetTenantId(LoginUser loginUser, Long tenantId) {
|
||||
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) && Long.valueOf(0L).equals(tenantId)) {
|
||||
return 0L;
|
||||
}
|
||||
return null;
|
||||
private Long resolveTargetTenantId(LoginUser loginUser) {
|
||||
return loginUser.getTenantId();
|
||||
}
|
||||
|
||||
@Operation(summary = "新增热词组")
|
||||
|
|
@ -39,7 +36,7 @@ public class HotWordGroupController {
|
|||
@Log(value = "新增热词组", type = "热词组管理")
|
||||
public ApiResponse<HotWordGroupVO> save(@RequestBody HotWordGroupDTO dto) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser, dto.getTenantId());
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser);
|
||||
return ApiResponse.ok(hotWordGroupService.saveGroup(dto, loginUser.getUserId(), targetTenantId));
|
||||
}
|
||||
|
||||
|
|
@ -49,7 +46,7 @@ public class HotWordGroupController {
|
|||
@Log(value = "修改热词组", type = "热词组管理")
|
||||
public ApiResponse<HotWordGroupVO> update(@RequestBody HotWordGroupDTO dto) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser, dto.getTenantId());
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser);
|
||||
HotWordGroupVO existing = hotWordGroupService.listVisibleOptions(targetTenantId).stream()
|
||||
.filter(item -> item.getId().equals(dto.getId()))
|
||||
.findFirst()
|
||||
|
|
@ -57,7 +54,6 @@ public class HotWordGroupController {
|
|||
if (existing == null) {
|
||||
return ApiResponse.error("热词组不存在");
|
||||
}
|
||||
dto.setTenantId(targetTenantId);
|
||||
return ApiResponse.ok(hotWordGroupService.updateGroup(dto));
|
||||
}
|
||||
|
||||
|
|
@ -65,9 +61,9 @@ public class HotWordGroupController {
|
|||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "删除热词组", type = "热词组管理")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id, @RequestParam(required = false) Long tenantId) {
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser, tenantId);
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser);
|
||||
return ApiResponse.ok(hotWordGroupService.removeGroupById(id, targetTenantId));
|
||||
}
|
||||
|
||||
|
|
@ -78,19 +74,18 @@ public class HotWordGroupController {
|
|||
@RequestParam(defaultValue = "1") Integer current,
|
||||
@RequestParam(defaultValue = "10") Integer size,
|
||||
@RequestParam(required = false) String name,
|
||||
@RequestParam(required = false) Integer status,
|
||||
@RequestParam(required = false) Long tenantId) {
|
||||
@RequestParam(required = false) Integer status) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser, tenantId);
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser);
|
||||
return ApiResponse.ok(hotWordGroupService.pageGroups(current, size, name, status, targetTenantId));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询热词组选项")
|
||||
@GetMapping("/options")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<List<HotWordGroupVO>> options(@RequestParam(required = false) Long tenantId) {
|
||||
public ApiResponse<List<HotWordGroupVO>> options() {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser, tenantId);
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser);
|
||||
return ApiResponse.ok(hotWordGroupService.listVisibleOptions(targetTenantId));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
package com.imeeting.controller.qt;
|
||||
|
||||
import com.imeeting.dto.android.AndroidAuthContext;
|
||||
import com.imeeting.common.MeetingConstants;
|
||||
import com.imeeting.dto.biz.CreateMeetingCommand;
|
||||
import com.imeeting.dto.biz.MeetingVO;
|
||||
import com.imeeting.enums.MeetingTerminalEnum;
|
||||
import com.imeeting.service.android.AndroidAuthService;
|
||||
import com.imeeting.service.biz.MeetingAuthorizationService;
|
||||
import com.imeeting.service.biz.MeetingCommandService;
|
||||
import com.imeeting.service.biz.PromptTemplateService;
|
||||
import com.imeeting.support.AndroidRequestLogHelper;
|
||||
import com.unisbase.annotation.Anonymous;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.common.annotation.Log;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "Qt会议管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/qt/meetings")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class QtMeetingController {
|
||||
|
||||
private final AndroidAuthService androidAuthService;
|
||||
private final MeetingAuthorizationService meetingAuthorizationService;
|
||||
private final MeetingCommandService meetingCommandService;
|
||||
private final PromptTemplateService promptTemplateService;
|
||||
|
||||
@Operation(summary = "创建 Qt 离线会议")
|
||||
@PostMapping
|
||||
@Anonymous
|
||||
@Log(value = "新增 Qt 离线会议", type = "Qt会议管理")
|
||||
public ApiResponse<MeetingVO> createMeeting(HttpServletRequest request,
|
||||
@Valid @RequestBody CreateMeetingCommand command) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Qt会议", "创建离线会议接口", "request", command);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
meetingAuthorizationService.assertCanCreateMeeting(authContext);
|
||||
assertPromptAvailable(command.getPromptId(), authContext);
|
||||
MeetingVO meeting = meetingCommandService.createMeeting(
|
||||
command,
|
||||
authContext.getTenantId(),
|
||||
authContext.getUserId(),
|
||||
resolveCreatorName(authContext),
|
||||
MeetingTerminalEnum.resolve(authContext.getPlatform()).getCode(),
|
||||
authContext.getDeviceId(),
|
||||
resolveSourceDeviceMode(authContext)
|
||||
);
|
||||
return ApiResponse.ok(meeting);
|
||||
}
|
||||
|
||||
private String resolveCreatorName(AndroidAuthContext authContext) {
|
||||
if (hasText(authContext.getDisplayName())) {
|
||||
return authContext.getDisplayName().trim();
|
||||
}
|
||||
if (hasText(authContext.getUsername())) {
|
||||
return authContext.getUsername().trim();
|
||||
}
|
||||
return hasText(authContext.getDeviceId()) ? "qt:" + authContext.getDeviceId().trim() : "qt";
|
||||
}
|
||||
|
||||
private void assertPromptAvailable(Long promptId, AndroidAuthContext authContext) {
|
||||
if (promptId == null) {
|
||||
return;
|
||||
}
|
||||
boolean enabled = promptTemplateService.isTemplateEnabledForUser(
|
||||
promptId,
|
||||
authContext.getTenantId(),
|
||||
authContext.getUserId(),
|
||||
authContext.getPlatformAdmin(),
|
||||
authContext.getTenantAdmin()
|
||||
);
|
||||
if (!enabled) {
|
||||
throw new RuntimeException("总结模板不可用");
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveSourceDeviceMode(AndroidAuthContext authContext) {
|
||||
return authContext.isAnonymous()
|
||||
? MeetingConstants.DEVICE_MODE_PUBLIC
|
||||
: MeetingConstants.DEVICE_MODE_PRIVATE;
|
||||
}
|
||||
|
||||
private boolean hasText(String value) {
|
||||
return value != null && !value.isBlank();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.imeeting.dto.biz;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Schema(description = "热词批量新增请求参数")
|
||||
public class HotWordBatchCreateDTO {
|
||||
|
||||
@Schema(description = "租户 ID,平台管理员可传 0 表示平台范围")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "待新增的热词内容列表")
|
||||
private List<String> words;
|
||||
|
||||
@Schema(description = "所属热词组 ID,为空表示未分组")
|
||||
private Long hotWordGroupId;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.imeeting.dto.biz;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Schema(description = "热词批量新增结果")
|
||||
public class HotWordBatchCreateResultVO {
|
||||
|
||||
@Schema(description = "实际新增热词数量")
|
||||
private Integer createdCount;
|
||||
|
||||
@Schema(description = "目标热词组中已存在的热词")
|
||||
private List<String> existingWords;
|
||||
}
|
||||
|
|
@ -72,9 +72,15 @@ public class MeetingVO {
|
|||
@Schema(description = "总结模型ID")
|
||||
private Long summaryModelId;
|
||||
|
||||
@Schema(description = "总结模型名称")
|
||||
private String summaryModelName;
|
||||
|
||||
@Schema(description = "总结模板ID")
|
||||
private Long promptId;
|
||||
|
||||
@Schema(description = "总结模板名称")
|
||||
private String promptName;
|
||||
|
||||
@Schema(description = "最终生效热词组ID")
|
||||
private Long hotWordGroupId;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
package com.imeeting.enums;
|
||||
|
||||
import com.unisbase.common.exception.BusinessException;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public enum MeetingTerminalEnum {
|
||||
WINDOWS("WINDOWS", "Windows"),
|
||||
MACOS("MACOS", "macOS"),
|
||||
KYLIN("KYLIN", "麒麟"),
|
||||
UOS("UOS", "统信"),
|
||||
HARMONYOS("HARMONYOS", "鸿蒙"),
|
||||
WEB("WEB", "Web端"),
|
||||
CUSTOM_TERMINAL("CUSTOM_TERMINAL", "定制终端");
|
||||
|
||||
private static final String LEGACY_ANDROID_CODE = "ANDROID";
|
||||
|
||||
private final String code;
|
||||
private final String description;
|
||||
|
||||
MeetingTerminalEnum(String code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public static boolean isCustomTerminalSource(String source) {
|
||||
return CUSTOM_TERMINAL.matches(source) || LEGACY_ANDROID_CODE.equalsIgnoreCase(source);
|
||||
}
|
||||
|
||||
public static MeetingTerminalEnum resolve(String source) {
|
||||
if (source == null || source.isBlank()) {
|
||||
throw new BusinessException("会议终端类型不能为空");
|
||||
}
|
||||
String normalized = source.trim();
|
||||
if (LEGACY_ANDROID_CODE.equalsIgnoreCase(normalized)) {
|
||||
return CUSTOM_TERMINAL;
|
||||
}
|
||||
for (MeetingTerminalEnum terminal : values()) {
|
||||
if (terminal.matches(normalized)) {
|
||||
return terminal;
|
||||
}
|
||||
}
|
||||
throw new BusinessException("会议终端类型无效: " + source);
|
||||
}
|
||||
|
||||
public static boolean isSameTerminal(String source, String target) {
|
||||
if (isCustomTerminalSource(source) && isCustomTerminalSource(target)) {
|
||||
return true;
|
||||
}
|
||||
return source != null && target != null && source.equalsIgnoreCase(target);
|
||||
}
|
||||
|
||||
public boolean matches(String source) {
|
||||
return source != null && code.equalsIgnoreCase(source);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.imeeting.service.biz;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.imeeting.dto.biz.HotWordBatchCreateDTO;
|
||||
import com.imeeting.dto.biz.HotWordBatchCreateResultVO;
|
||||
import com.imeeting.dto.biz.HotWordDTO;
|
||||
import com.imeeting.dto.biz.HotWordVO;
|
||||
import com.imeeting.entity.biz.HotWord;
|
||||
|
|
@ -9,6 +11,8 @@ import java.util.List;
|
|||
|
||||
public interface HotWordService extends IService<HotWord> {
|
||||
HotWordVO saveHotWord(HotWordDTO hotWordDTO, Long userId, Long tenantId);
|
||||
|
||||
HotWordBatchCreateResultVO saveHotWordsBatch(HotWordBatchCreateDTO dto, Long userId, Long tenantId, boolean platformAdmin);
|
||||
HotWordVO updateHotWord(HotWordDTO hotWordDTO, Long userId, Long tenantId);
|
||||
Integer updateHotWordGroupBatch(List<Long> ids, Long hotWordGroupId, Long tenantId);
|
||||
List<String> generatePinyin(String word);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package com.imeeting.service.biz.impl;
|
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.imeeting.dto.biz.HotWordBatchCreateDTO;
|
||||
import com.imeeting.dto.biz.HotWordBatchCreateResultVO;
|
||||
import com.imeeting.dto.biz.HotWordDTO;
|
||||
import com.imeeting.dto.biz.HotWordVO;
|
||||
import com.imeeting.entity.biz.HotWord;
|
||||
|
|
@ -10,6 +12,7 @@ import com.imeeting.entity.biz.HotWordGroup;
|
|||
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 lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sourceforge.pinyin4j.PinyinHelper;
|
||||
|
|
@ -33,6 +36,9 @@ import java.util.stream.Collectors;
|
|||
public class HotWordServiceImpl extends ServiceImpl<HotWordMapper, HotWord> implements HotWordService {
|
||||
|
||||
private static final int 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;
|
||||
|
||||
|
|
@ -52,12 +58,39 @@ public class HotWordServiceImpl extends ServiceImpl<HotWordMapper, HotWord> impl
|
|||
return toVO(hotWord);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public HotWordBatchCreateResultVO saveHotWordsBatch(HotWordBatchCreateDTO dto, Long userId, Long tenantId, boolean platformAdmin) {
|
||||
Set<String> words = normalizeWords(dto.getWords());
|
||||
if (words.isEmpty()) {
|
||||
throw new BusinessException("请选择有效热词");
|
||||
}
|
||||
|
||||
Long groupId = validateGroupForBatchCreate(dto.getHotWordGroupId(), tenantId, platformAdmin);
|
||||
Set<String> existingWords = findExistingWords(words, groupId);
|
||||
List<String> existingWordList = words.stream().filter(existingWords::contains).toList();
|
||||
List<HotWord> hotWords = words.stream()
|
||||
.filter(word -> !existingWords.contains(word))
|
||||
.map(word -> buildBatchHotWord(word, groupId, dto.getRemark(), userId))
|
||||
.toList();
|
||||
HotWordBatchCreateResultVO result = new HotWordBatchCreateResultVO();
|
||||
result.setExistingWords(existingWordList);
|
||||
if (hotWords.isEmpty()) {
|
||||
result.setCreatedCount(0);
|
||||
return result;
|
||||
}
|
||||
|
||||
validateGroupCapacityForBatchCreate(groupId, hotWords.size());
|
||||
result.setCreatedCount(this.saveBatch(hotWords) ? hotWords.size() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public HotWordVO updateHotWord(HotWordDTO hotWordDTO, Long userId, Long tenantId) {
|
||||
HotWord hotWord = this.getById(hotWordDTO.getId());
|
||||
if (hotWord == null) {
|
||||
throw new IllegalArgumentException("热词不存在");
|
||||
throw new BusinessException("热词不存在");
|
||||
}
|
||||
|
||||
String oldWord = hotWord.getWord();
|
||||
|
|
@ -76,20 +109,20 @@ public class HotWordServiceImpl extends ServiceImpl<HotWordMapper, HotWord> impl
|
|||
@Transactional(rollbackFor = Exception.class)
|
||||
public Integer updateHotWordGroupBatch(List<Long> ids, Long hotWordGroupId, Long tenantId) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
throw new IllegalArgumentException("请选择热词");
|
||||
throw new BusinessException("请选择热词");
|
||||
}
|
||||
Set<Long> uniqueIds = ids.stream()
|
||||
.filter(id -> id != null)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
if (uniqueIds.isEmpty()) {
|
||||
throw new IllegalArgumentException("请选择热词");
|
||||
throw new BusinessException("请选择热词");
|
||||
}
|
||||
|
||||
List<HotWord> hotWords = this.list(new LambdaQueryWrapper<HotWord>()
|
||||
.in(HotWord::getId, uniqueIds)
|
||||
.eq(HotWord::getTenantId, tenantId));
|
||||
if (hotWords.size() != uniqueIds.size()) {
|
||||
throw new IllegalArgumentException("部分热词不存在或无权操作");
|
||||
throw new BusinessException("部分热词不存在或无权操作");
|
||||
}
|
||||
|
||||
if (hotWordGroupId != null) {
|
||||
|
|
@ -160,16 +193,16 @@ public class HotWordServiceImpl extends ServiceImpl<HotWordMapper, HotWord> impl
|
|||
}
|
||||
HotWordGroup group = hotWordGroupMapper.selectById(groupId);
|
||||
if (group == null || !tenantId.equals(group.getTenantId())) {
|
||||
throw new IllegalArgumentException("热词组不存在");
|
||||
throw new BusinessException("热词组不存在");
|
||||
}
|
||||
if (!Integer.valueOf(1).equals(group.getStatus())) {
|
||||
throw new IllegalArgumentException("热词组已禁用");
|
||||
throw new BusinessException("热词组已禁用");
|
||||
}
|
||||
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 IllegalArgumentException("热词组最多只能包含 200 个热词");
|
||||
throw new BusinessException("热词组最多只能包含 200 个热词");
|
||||
}
|
||||
return group.getId();
|
||||
}
|
||||
|
|
@ -177,10 +210,10 @@ public class HotWordServiceImpl extends ServiceImpl<HotWordMapper, HotWord> impl
|
|||
private void validateGroupCapacity(Long groupId, Long tenantId, List<HotWord> movingHotWords) {
|
||||
HotWordGroup group = hotWordGroupMapper.selectById(groupId);
|
||||
if (group == null || !tenantId.equals(group.getTenantId())) {
|
||||
throw new IllegalArgumentException("热词组不存在");
|
||||
throw new BusinessException("热词组不存在");
|
||||
}
|
||||
if (!Integer.valueOf(1).equals(group.getStatus())) {
|
||||
throw new IllegalArgumentException("热词组已禁用");
|
||||
throw new BusinessException("热词组已禁用");
|
||||
}
|
||||
Set<Long> movingIds = movingHotWords.stream().map(HotWord::getId).collect(Collectors.toSet());
|
||||
long currentCount = this.count(new LambdaQueryWrapper<HotWord>()
|
||||
|
|
@ -190,10 +223,71 @@ public class HotWordServiceImpl extends ServiceImpl<HotWordMapper, HotWord> impl
|
|||
.filter(item -> !groupId.equals(item.getHotWordGroupId()))
|
||||
.count();
|
||||
if (currentCount + incomingCount > MAX_HOT_WORDS_PER_GROUP) {
|
||||
throw new IllegalArgumentException("热词组最多只能包含 200 个热词");
|
||||
throw new BusinessException("热词组最多只能包含 200 个热词");
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> normalizeWords(List<String> words) {
|
||||
if (words == null || words.isEmpty()) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return words.stream()
|
||||
.filter(word -> word != null)
|
||||
.map(String::trim)
|
||||
.filter(word -> !word.isEmpty() && !"无".equals(word))
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
private Set<String> findExistingWords(Set<String> words, Long groupId) {
|
||||
return this.list(new LambdaQueryWrapper<HotWord>()
|
||||
.eq(groupId != null, HotWord::getHotWordGroupId, groupId)
|
||||
.isNull(groupId == null, HotWord::getHotWordGroupId)
|
||||
.in(HotWord::getWord, words))
|
||||
.stream()
|
||||
.map(HotWord::getWord)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private Long validateGroupForBatchCreate(Long groupId, Long tenantId, boolean platformAdmin) {
|
||||
if (groupId == null) {
|
||||
return null;
|
||||
}
|
||||
HotWordGroup group = hotWordGroupMapper.selectById(groupId);
|
||||
if (group == null || (!platformAdmin && !tenantId.equals(group.getTenantId()))) {
|
||||
throw new BusinessException("热词组不存在");
|
||||
}
|
||||
if (!Integer.valueOf(ENABLED_STATUS).equals(group.getStatus())) {
|
||||
throw new BusinessException("热词组已禁用");
|
||||
}
|
||||
return group.getId();
|
||||
}
|
||||
|
||||
private void validateGroupCapacityForBatchCreate(Long groupId, int incomingCount) {
|
||||
if (groupId == null) {
|
||||
return;
|
||||
}
|
||||
long currentCount = this.count(new LambdaQueryWrapper<HotWord>()
|
||||
.eq(HotWord::getHotWordGroupId, groupId));
|
||||
if (currentCount + incomingCount > MAX_HOT_WORDS_PER_GROUP) {
|
||||
throw new BusinessException("热词组最多只能包含 200 个热词");
|
||||
}
|
||||
}
|
||||
|
||||
private HotWord buildBatchHotWord(String word, Long groupId, String remark, Long userId) {
|
||||
HotWord hotWord = new HotWord();
|
||||
hotWord.setWord(word);
|
||||
hotWord.setPinyinList(generatePinyin(word));
|
||||
hotWord.setMatchStrategy(DEFAULT_MATCH_STRATEGY);
|
||||
hotWord.setCategory("");
|
||||
hotWord.setHotWordGroupId(groupId);
|
||||
hotWord.setWeight(DEFAULT_WEIGHT);
|
||||
hotWord.setStatus(ENABLED_STATUS);
|
||||
hotWord.setIsPublic(1);
|
||||
hotWord.setCreatorId(userId);
|
||||
hotWord.setRemark(remark);
|
||||
return hotWord;
|
||||
}
|
||||
|
||||
private void generateCombinations(List<List<String>> matrix, int index, String current, List<String> result) {
|
||||
if (index == matrix.size()) {
|
||||
result.add(current.trim());
|
||||
|
|
|
|||
|
|
@ -8,11 +8,15 @@ import com.imeeting.entity.biz.AiTask;
|
|||
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.event.MeetingCreatedEvent;
|
||||
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
||||
import com.imeeting.dto.biz.AiModelVO;
|
||||
import com.imeeting.service.biz.AiModelService;
|
||||
import com.imeeting.service.biz.AiTaskService;
|
||||
import com.imeeting.service.biz.HotWordGroupService;
|
||||
import com.imeeting.service.biz.MeetingPointsService;
|
||||
import com.imeeting.service.biz.PromptTemplateService;
|
||||
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
||||
import com.imeeting.service.biz.MeetingSummaryFileService;
|
||||
import com.imeeting.service.realtime.RealtimeMeetingAudioStorageService;
|
||||
|
|
@ -62,6 +66,8 @@ public class MeetingDomainSupport {
|
|||
private final MeetingPlaybackAudioResolver meetingPlaybackAudioResolver;
|
||||
private final HotWordGroupService hotWordGroupService;
|
||||
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
|
||||
private final AiModelService aiModelService;
|
||||
private final PromptTemplateService promptTemplateService;
|
||||
private final SysParamService sysParamService;
|
||||
|
||||
@Value("${unisbase.app.upload-path}")
|
||||
|
|
@ -433,6 +439,7 @@ public class MeetingDomainSupport {
|
|||
vo.setOfflineRecordingStatus(meeting.getOfflineRecordingStatus());
|
||||
vo.setSummaryModelId(meeting.getSummaryModelId());
|
||||
vo.setPromptId(meeting.getPromptId());
|
||||
fillSummaryConfigurationNames(meeting, vo);
|
||||
fillEffectiveHotWordGroup(meeting, vo);
|
||||
vo.setAiCatalogEnabled(resolveAiCatalogEnabled());
|
||||
vo.setSummaryDetailLevel(normalizeSummaryDetailLevel(meeting.getSummaryDetailLevel()));
|
||||
|
|
@ -475,6 +482,21 @@ public class MeetingDomainSupport {
|
|||
}
|
||||
}
|
||||
|
||||
private void fillSummaryConfigurationNames(Meeting meeting, com.imeeting.dto.biz.MeetingVO vo) {
|
||||
if (meeting.getSummaryModelId() != null) {
|
||||
AiModelVO summaryModel = aiModelService.getModelById(meeting.getSummaryModelId(), "LLM");
|
||||
if (summaryModel != null) {
|
||||
vo.setSummaryModelName(summaryModel.getModelName());
|
||||
}
|
||||
}
|
||||
if (meeting.getPromptId() != null) {
|
||||
PromptTemplate promptTemplate = promptTemplateService.getById(meeting.getPromptId());
|
||||
if (promptTemplate != null) {
|
||||
vo.setPromptName(promptTemplate.getTemplateName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void fillEffectiveHotWordGroup(Meeting meeting, com.imeeting.dto.biz.MeetingVO vo) {
|
||||
Long hotWordGroupId = resolveEffectiveHotWordGroupId(meeting);
|
||||
vo.setHotWordGroupId(hotWordGroupId);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,18 @@ export interface HotWordBatchGroupDTO {
|
|||
hotWordGroupId?: number;
|
||||
}
|
||||
|
||||
export interface HotWordBatchCreateDTO {
|
||||
tenantId?: number;
|
||||
words: string[];
|
||||
hotWordGroupId?: number;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface HotWordBatchCreateResultVO {
|
||||
createdCount: number;
|
||||
existingWords: string[];
|
||||
}
|
||||
|
||||
export const getHotWordPage = (params: {
|
||||
current: number;
|
||||
size: number;
|
||||
|
|
@ -64,6 +76,13 @@ export const saveHotWord = (data: HotWordDTO) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const createHotWordBatch = (data: HotWordBatchCreateDTO) => {
|
||||
return http.post<{ code: string; data: HotWordBatchCreateResultVO; msg: string }>(
|
||||
"/api/biz/hotword/batch",
|
||||
data
|
||||
);
|
||||
};
|
||||
|
||||
export const updateHotWord = (data: HotWordDTO) => {
|
||||
return http.put<{ code: string; data: HotWordVO; msg: string }>(
|
||||
"/api/biz/hotword",
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ export interface HotWordGroupVO {
|
|||
|
||||
export interface HotWordGroupDTO {
|
||||
id?: number;
|
||||
tenantId?: number;
|
||||
groupName: string;
|
||||
status: number;
|
||||
remark?: string;
|
||||
|
|
@ -25,7 +24,6 @@ export const getHotWordGroupPage = (params: {
|
|||
size: number;
|
||||
name?: string;
|
||||
status?: number;
|
||||
tenantId?: number;
|
||||
}) => {
|
||||
return http.get<{ code: string; data: { records: HotWordGroupVO[]; total: number }; msg: string }>(
|
||||
"/api/biz/hotword-group/page",
|
||||
|
|
@ -33,10 +31,9 @@ export const getHotWordGroupPage = (params: {
|
|||
);
|
||||
};
|
||||
|
||||
export const getHotWordGroupOptions = (tenantId?: number) => {
|
||||
export const getHotWordGroupOptions = () => {
|
||||
return http.get<{ code: string; data: HotWordGroupVO[]; msg: string }>(
|
||||
"/api/biz/hotword-group/options",
|
||||
{ params: { tenantId } }
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -54,9 +51,8 @@ export const updateHotWordGroup = (data: HotWordGroupDTO) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const deleteHotWordGroup = (id: number, tenantId?: number) => {
|
||||
export const deleteHotWordGroup = (id: number) => {
|
||||
return http.delete<{ code: string; data: boolean; msg: string }>(
|
||||
`/api/biz/hotword-group/${id}`,
|
||||
{ params: { tenantId } }
|
||||
`/api/biz/hotword-group/${id}`
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@ export interface MeetingVO {
|
|||
sourceDeviceMode?: "PUBLIC" | "PRIVATE";
|
||||
summaryDetailLevel?: SummaryDetailLevel;
|
||||
summaryModelId: number;
|
||||
summaryModelName?: string;
|
||||
promptId?: number;
|
||||
promptName?: string;
|
||||
hotWordGroupId?: number;
|
||||
hotWordGroupName?: string;
|
||||
aiCatalogEnabled?: boolean;
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ const HotWords: React.FC = () => {
|
|||
const [searchWord, setSearchWord] = useState("");
|
||||
const [searchCategory, setSearchCategory] = useState<string | undefined>(undefined);
|
||||
const [searchGroupId, setSearchGroupId] = useState<number | undefined>(undefined);
|
||||
const [hotWordGroupFilter, setHotWordGroupFilter] = useState<HotWordGroupFilter>("all");
|
||||
const [hotWordGroupFilter, setHotWordGroupFilter] = useState<HotWordGroupFilter>("ungrouped");
|
||||
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
|
|
@ -196,7 +196,7 @@ const HotWords: React.FC = () => {
|
|||
};
|
||||
|
||||
const loadGroupOptions = async () => {
|
||||
const res = await getHotWordGroupOptions(isPlatformAdmin ? activeTenantId : undefined);
|
||||
const res = await getHotWordGroupOptions();
|
||||
setGroupOptions(res.data?.data || []);
|
||||
};
|
||||
|
||||
|
|
@ -208,7 +208,6 @@ const HotWords: React.FC = () => {
|
|||
size: groupSize,
|
||||
name: groupSearchName || undefined,
|
||||
status: groupSearchStatus,
|
||||
tenantId: isPlatformAdmin ? activeTenantId : undefined,
|
||||
});
|
||||
setGroupData(res.data?.data?.records || []);
|
||||
setGroupTotal(res.data?.data?.total || 0);
|
||||
|
|
@ -315,10 +314,10 @@ const HotWords: React.FC = () => {
|
|||
const values = await groupForm.validateFields();
|
||||
setGroupSubmitLoading(true);
|
||||
if (editingGroupId) {
|
||||
await updateHotWordGroup({ ...values, id: editingGroupId, tenantId: isPlatformAdmin ? activeTenantId : undefined });
|
||||
await updateHotWordGroup({...values, id: editingGroupId});
|
||||
message.success("热词组更新成功");
|
||||
} else {
|
||||
await saveHotWordGroup({ ...values, tenantId: isPlatformAdmin ? activeTenantId : undefined });
|
||||
await saveHotWordGroup(values);
|
||||
message.success("热词组创建成功");
|
||||
}
|
||||
setGroupEditorVisible(false);
|
||||
|
|
@ -330,7 +329,7 @@ const HotWords: React.FC = () => {
|
|||
|
||||
const handleDeleteGroup = async (id: number, e?: React.MouseEvent) => {
|
||||
e?.stopPropagation();
|
||||
await deleteHotWordGroup(id, isPlatformAdmin ? activeTenantId : undefined);
|
||||
await deleteHotWordGroup(id);
|
||||
message.success("热词组删除成功");
|
||||
if (searchGroupId === id) {
|
||||
setSearchGroupId(undefined);
|
||||
|
|
@ -401,23 +400,25 @@ const HotWords: React.FC = () => {
|
|||
setSearchCategory(undefined);
|
||||
setSearchGroupId(undefined);
|
||||
setSelectedGroupName(undefined);
|
||||
setHotWordGroupFilter("all");
|
||||
setHotWordGroupFilter("ungrouped");
|
||||
bulkGroupForm.resetFields();
|
||||
setBulkGroupEditorVisible(false);
|
||||
setCurrent(1);
|
||||
void fetchData({ current: 1, word: "", category: null, groupFilter: "all", searchGroupId: null });
|
||||
void fetchData({current: 1, word: "", category: null, groupFilter: "ungrouped", searchGroupId: null});
|
||||
};
|
||||
|
||||
const handleSelectGroup = (item: GroupListItem) => {
|
||||
setSearchGroupId(item.id);
|
||||
setSelectedGroupName(item.id ? item.groupName : undefined);
|
||||
setHotWordGroupFilter(item.id ?? "all");
|
||||
setHotWordGroupFilter(item.id ?? "ungrouped");
|
||||
setCurrent(1);
|
||||
};
|
||||
|
||||
const hotWordGroupTitle = searchGroupId
|
||||
? selectedGroupName || groupData.find((item) => item.id === searchGroupId)?.groupName || groupNameMap[searchGroupId] || "热词列表"
|
||||
: "全部热词";
|
||||
const hotWordGroupTitle = hotWordGroupFilter === "ungrouped"
|
||||
? "未分组"
|
||||
: typeof hotWordGroupFilter === "number"
|
||||
? selectedGroupName || groupData.find((item) => item.id === hotWordGroupFilter)?.groupName || groupNameMap[hotWordGroupFilter] || "热词列表"
|
||||
: "热词列表";
|
||||
|
||||
const groupFilterOptions = useMemo(
|
||||
() => [
|
||||
|
|
@ -428,7 +429,7 @@ const HotWords: React.FC = () => {
|
|||
[groupOptions]
|
||||
);
|
||||
|
||||
const groupListData: GroupListItem[] = [{ id: undefined, groupName: "全部热词" }, ...groupData];
|
||||
const groupListData: GroupListItem[] = [{id: undefined, groupName: "未分组"}, ...groupData];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
|
|
@ -558,7 +559,7 @@ const HotWords: React.FC = () => {
|
|||
loading={groupLoading}
|
||||
dataSource={groupListData}
|
||||
renderItem={(item) => {
|
||||
const isSelected = searchGroupId === item.id;
|
||||
const isSelected = item.id ? hotWordGroupFilter === item.id : hotWordGroupFilter === "ungrouped";
|
||||
const actions = [];
|
||||
if (item.id) {
|
||||
actions.push(
|
||||
|
|
@ -611,7 +612,7 @@ const HotWords: React.FC = () => {
|
|||
<span>{item.remark || "暂无备注"}</span>
|
||||
</span>
|
||||
)
|
||||
: "查看所有热词"
|
||||
: "查看未分组热词"
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
|
|
@ -689,7 +690,8 @@ const HotWords: React.FC = () => {
|
|||
className="hotwords-search__category"
|
||||
options={categories.map((c) => ({ label: c.itemLabel, value: c.itemValue }))}
|
||||
/>
|
||||
<Button onClick={handleResetFilters} disabled={selectedHotWordIds.length === 0 && searchWord === "" && !searchCategory && !searchGroupId && hotWordGroupFilter === "all"}>
|
||||
<Button onClick={handleResetFilters}
|
||||
disabled={selectedHotWordIds.length === 0 && searchWord === "" && !searchCategory && !searchGroupId && hotWordGroupFilter === "ungrouped"}>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ import {
|
|||
updateSpeakerInfo,
|
||||
} from '../../api/business/meeting';
|
||||
import { getAiModelDefault, getAiModelPage, AiModelVO } from '../../api/business/aimodel';
|
||||
import { getHotWordPage, getPinyinSuggestion, saveHotWord } from '../../api/business/hotword';
|
||||
import {createHotWordBatch} from '../../api/business/hotword';
|
||||
import {getHotWordGroupOptions, type HotWordGroupVO} from '../../api/business/hotwordGroup';
|
||||
import { getPromptPage, PromptTemplateVO } from '../../api/business/prompt';
|
||||
import { listUsers } from '../../api';
|
||||
import { useDict } from '../../hooks/useDict';
|
||||
|
|
@ -248,18 +249,20 @@ const parseBulletList = (content?: string | null) =>
|
|||
const parseOverviewSection = (markdown: string) =>
|
||||
extractSection(markdown, ['全文概要', '概要', '摘要', '概览']) || markdown.replace(/^---[\s\S]*?---/, '').trim();
|
||||
|
||||
const isValidKeyword = (value: string) => value.trim() !== '' && value.trim() !== '无';
|
||||
|
||||
const parseKeywordsSection = (markdown: string, tags: string) => {
|
||||
const section = extractSection(markdown, ['关键词', '关键字', '标签']);
|
||||
const fromSection = parseBulletList(section)
|
||||
.flatMap((line) => line.split(/[,,、/]/))
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
.filter(isValidKeyword);
|
||||
|
||||
if (fromSection.length) {
|
||||
return Array.from(new Set(fromSection)).slice(0, 12);
|
||||
}
|
||||
|
||||
return Array.from(new Set((tags || '').split(',').map((item) => item.trim()).filter(Boolean))).slice(0, 12);
|
||||
return Array.from(new Set((tags || '').split(',').map((item) => item.trim()).filter(isValidKeyword))).slice(0, 12);
|
||||
};
|
||||
|
||||
const buildMeetingAnalysis = (
|
||||
|
|
@ -280,7 +283,7 @@ const buildMeetingAnalysis = (
|
|||
return {
|
||||
overview: String(parsed.overview || '').trim(),
|
||||
keywords: Array.from(
|
||||
new Set((Array.isArray(parsed.keywords) ? parsed.keywords : []).map((item) => String(item).trim()).filter(Boolean)),
|
||||
new Set((Array.isArray(parsed.keywords) ? parsed.keywords : []).map((item) => String(item).trim()).filter(isValidKeyword)),
|
||||
).slice(0, 12),
|
||||
chapters: chapters
|
||||
.map((item: any) => ({
|
||||
|
|
@ -1227,6 +1230,10 @@ const MeetingDetail: React.FC = () => {
|
|||
const [isEditingSummary, setIsEditingSummary] = useState(false);
|
||||
const [summaryDraft, setSummaryDraft] = useState('');
|
||||
const [selectedKeywords, setSelectedKeywords] = useState<string[]>([]);
|
||||
const [hotWordGroupModalOpen, setHotWordGroupModalOpen] = useState(false);
|
||||
const [hotWordGroupOptions, setHotWordGroupOptions] = useState<HotWordGroupVO[]>([]);
|
||||
const [selectedHotWordGroupId, setSelectedHotWordGroupId] = useState<number | undefined>();
|
||||
const [hotWordGroupLoading, setHotWordGroupLoading] = useState(false);
|
||||
const [workspaceTab, setWorkspaceTab] = useState<WorkspaceTab>('transcript');
|
||||
const [addingHotwords, setAddingHotwords] = useState(false);
|
||||
const [editingTranscriptId, setEditingTranscriptId] = useState<number | null>(null);
|
||||
|
|
@ -1288,15 +1295,20 @@ const MeetingDetail: React.FC = () => {
|
|||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const userProfile = useMemo(() => {
|
||||
const profileStr = sessionStorage.getItem("userProfile");
|
||||
return profileStr ? JSON.parse(profileStr) : {};
|
||||
}, []);
|
||||
const activeTenantId = useMemo(() => Number(localStorage.getItem("activeTenantId") || 0), []);
|
||||
const isPlatformAdmin = userProfile.isPlatformAdmin === true;
|
||||
const analysis = useMemo(
|
||||
() => buildMeetingAnalysis(meeting?.analysis, meeting?.summaryContent, meeting?.tags || ''),
|
||||
[meeting?.analysis, meeting?.summaryContent, meeting?.tags],
|
||||
);
|
||||
const expandKeywords = false;
|
||||
const expandKeywords = true;
|
||||
const visibleKeywords = expandKeywords ? analysis.keywords : analysis.keywords.slice(0, 9);
|
||||
const meetingTags = useMemo(
|
||||
() => (meeting?.tags?.split(',').map((item) => item.trim()).filter(Boolean) || []),
|
||||
() => (meeting?.tags?.split(',').map((item) => item.trim()).filter(isValidKeyword) || []),
|
||||
[meeting?.tags],
|
||||
);
|
||||
const discussionItems = useMemo(() => {
|
||||
|
|
@ -1388,25 +1400,23 @@ const MeetingDetail: React.FC = () => {
|
|||
return buildMeetingPreviewUrl(meetingShareBaseUrl, meetingId);
|
||||
}, [meetingShareBaseUrl, meeting?.id, id]);
|
||||
const summaryModelDisplayName = useMemo(() => {
|
||||
const matchedModel = llmModels.find((item) => item.id === meeting?.summaryModelId);
|
||||
if (matchedModel?.modelName) {
|
||||
return matchedModel.modelName;
|
||||
if (meeting?.summaryModelName?.trim()) {
|
||||
return meeting.summaryModelName.trim();
|
||||
}
|
||||
if (meeting?.summaryModelId) {
|
||||
return `模型 #${meeting.summaryModelId}`;
|
||||
}
|
||||
return '未配置';
|
||||
}, [llmModels, meeting?.summaryModelId]);
|
||||
}, [meeting?.summaryModelId, meeting?.summaryModelName]);
|
||||
const promptDisplayName = useMemo(() => {
|
||||
const matchedPrompt = prompts.find((item) => item.id === meeting?.promptId);
|
||||
if (matchedPrompt?.templateName) {
|
||||
return matchedPrompt.templateName;
|
||||
if (meeting?.promptName?.trim()) {
|
||||
return meeting.promptName.trim();
|
||||
}
|
||||
if (meeting?.promptId) {
|
||||
return `模板 #${meeting.promptId}`;
|
||||
}
|
||||
return '未配置';
|
||||
}, [meeting?.promptId, prompts]);
|
||||
}, [meeting?.promptId, meeting?.promptName]);
|
||||
const hotWordGroupDisplayName = useMemo(() => {
|
||||
if (meeting?.hotWordGroupName?.trim()) {
|
||||
return meeting.hotWordGroupName.trim();
|
||||
|
|
@ -1582,7 +1592,6 @@ const MeetingDetail: React.FC = () => {
|
|||
useEffect(() => {
|
||||
if (!id) return;
|
||||
fetchData(Number(id));
|
||||
loadAiConfigs();
|
||||
loadUsers();
|
||||
}, [id, fetchData]);
|
||||
|
||||
|
|
@ -1648,11 +1657,13 @@ const MeetingDetail: React.FC = () => {
|
|||
getPromptPage({ current: 1, size: 100 }),
|
||||
getAiModelDefault('LLM'),
|
||||
]);
|
||||
setLlmModels((modelRes.data?.data?.records || []).filter((item) => item.status === 1));
|
||||
setPrompts((promptRes.data?.data?.records || []).filter((item) => item.status === 1));
|
||||
summaryForm.setFieldsValue({ summaryModelId: defaultRes.data.data?.id });
|
||||
const models = (modelRes.data?.data?.records || []).filter((item) => item.status === 1);
|
||||
const promptTemplates = (promptRes.data?.data?.records || []).filter((item) => item.status === 1);
|
||||
setLlmModels(models);
|
||||
setPrompts(promptTemplates);
|
||||
return {models, promptTemplates, defaultModelId: defaultRes.data.data?.id};
|
||||
} catch {
|
||||
// ignore
|
||||
return {models: [], promptTemplates: [], defaultModelId: undefined};
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1743,17 +1754,19 @@ const MeetingDetail: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleOpenSummaryDrawer = () => {
|
||||
const handleOpenSummaryDrawer = async () => {
|
||||
const {models, promptTemplates, defaultModelId} = await loadAiConfigs();
|
||||
summaryForm.setFieldsValue({
|
||||
summaryModelId:
|
||||
summaryForm.getFieldValue('summaryModelId') ??
|
||||
meeting?.summaryModelId ??
|
||||
llmModels.find((model) => model.isDefault === 1)?.id ??
|
||||
llmModels[0]?.id,
|
||||
defaultModelId ??
|
||||
models.find((model) => model.isDefault === 1)?.id ??
|
||||
models[0]?.id,
|
||||
promptId:
|
||||
summaryForm.getFieldValue('promptId') ??
|
||||
meeting?.promptId ??
|
||||
prompts[0]?.id,
|
||||
promptTemplates[0]?.id,
|
||||
userPrompt: meeting?.lastUserPrompt ?? '',
|
||||
summaryDetailLevel:
|
||||
summaryForm.getFieldValue('summaryDetailLevel') ??
|
||||
|
|
@ -1868,58 +1881,51 @@ const MeetingDetail: React.FC = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const handleOpenHotWordGroupModal = async () => {
|
||||
if (!selectedKeywords.length) {
|
||||
message.warning('请先选择关键词');
|
||||
return;
|
||||
}
|
||||
|
||||
setHotWordGroupLoading(true);
|
||||
try {
|
||||
const response = await getHotWordGroupOptions();
|
||||
const options = (response.data?.data || []).filter((item) => item.status === 1 && item.hotWordCount < 200);
|
||||
setHotWordGroupOptions(options);
|
||||
setSelectedHotWordGroupId(options.some((item) => item.id === meeting?.hotWordGroupId) ? meeting?.hotWordGroupId : options[0]?.id);
|
||||
setHotWordGroupModalOpen(true);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setHotWordGroupLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddSelectedHotwords = async () => {
|
||||
const keywords = selectedKeywords.map((item) => item.trim()).filter(Boolean);
|
||||
const keywords = selectedKeywords.map((item) => item.trim()).filter(isValidKeyword);
|
||||
if (!keywords.length) {
|
||||
message.warning('请先选择关键词');
|
||||
return;
|
||||
}
|
||||
if (selectedHotWordGroupId === undefined) {
|
||||
message.warning('请选择热词组');
|
||||
return;
|
||||
}
|
||||
|
||||
setAddingHotwords(true);
|
||||
try {
|
||||
const existingRes = await getHotWordPage({ current: 1, size: 500, word: '' });
|
||||
const existingWords = new Set(
|
||||
(existingRes.data?.data?.records || [])
|
||||
.map((item) => item.word?.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
const toCreate = keywords.filter((item) => !existingWords.has(item));
|
||||
|
||||
if (!toCreate.length) {
|
||||
message.info('所选关键词已存在于热词库');
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
toCreate.map((word) =>
|
||||
(async () => {
|
||||
let pinyinList: string[] = [];
|
||||
try {
|
||||
const pinyinRes = await getPinyinSuggestion(word);
|
||||
pinyinList = (pinyinRes.data?.data || []).map((item) => item.trim()).filter(Boolean);
|
||||
} catch {
|
||||
pinyinList = [];
|
||||
}
|
||||
return saveHotWord({
|
||||
word,
|
||||
pinyinList,
|
||||
matchStrategy: 1,
|
||||
category: '',
|
||||
weight: 2,
|
||||
status: 1,
|
||||
remark: meeting ? `来源于会议:${meeting.title}` : '来源于会议关键词',
|
||||
});
|
||||
})(),
|
||||
),
|
||||
);
|
||||
|
||||
const skippedCount = keywords.length - toCreate.length;
|
||||
const response = await createHotWordBatch({
|
||||
words: keywords,
|
||||
hotWordGroupId: selectedHotWordGroupId || undefined,
|
||||
remark: meeting ? `来源于会议:${meeting.title}` : '来源于会议关键词',
|
||||
});
|
||||
const result = response.data?.data;
|
||||
const existingWords = result?.existingWords || [];
|
||||
message.success(
|
||||
skippedCount > 0
|
||||
? `已新增 ${toCreate.length} 个热词,跳过 ${skippedCount} 个重复项`
|
||||
: `已新增 ${toCreate.length} 个热词`,
|
||||
`新增 ${result?.createdCount || 0} 个热词${existingWords.length ? `,[${existingWords.join(', ')}]已存在热词组` : ''}`,
|
||||
);
|
||||
setSelectedKeywords([]);
|
||||
setHotWordGroupModalOpen(false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
|
|
@ -2392,7 +2398,7 @@ const MeetingDetail: React.FC = () => {
|
|||
</Button>
|
||||
)}
|
||||
{canRetrySummary && (
|
||||
<Button icon={<SyncOutlined />} onClick={handleOpenSummaryDrawer} disabled={actionLoading}>
|
||||
<Button icon={<SyncOutlined/>} onClick={() => void handleOpenSummaryDrawer()} disabled={actionLoading}>
|
||||
重新总结
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -2578,9 +2584,9 @@ const MeetingDetail: React.FC = () => {
|
|||
ghost
|
||||
disabled={!selectedKeywords.length}
|
||||
loading={addingHotwords}
|
||||
onClick={handleAddSelectedHotwords}
|
||||
onClick={() => void handleOpenHotWordGroupModal()}
|
||||
>
|
||||
加入热词 {selectedKeywords.length > 0 ? `(${selectedKeywords.length})` : ''}
|
||||
加入热词组 {selectedKeywords.length > 0 ? `(${selectedKeywords.length})` : ''}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -4238,6 +4244,33 @@ const MeetingDetail: React.FC = () => {
|
|||
}
|
||||
`}</style>
|
||||
|
||||
<Modal
|
||||
title="加入热词组"
|
||||
open={hotWordGroupModalOpen}
|
||||
onCancel={() => setHotWordGroupModalOpen(false)}
|
||||
onOk={() => void handleAddSelectedHotwords()}
|
||||
confirmLoading={addingHotwords}
|
||||
okText="加入"
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form layout="vertical" style={{marginTop: 16}}>
|
||||
<Form.Item label="目标热词组" required>
|
||||
<Select
|
||||
value={selectedHotWordGroupId}
|
||||
placeholder={hotWordGroupLoading ? '正在加载热词组' : '请选择热词组'}
|
||||
loading={hotWordGroupLoading}
|
||||
onChange={setSelectedHotWordGroupId}
|
||||
options={[
|
||||
...hotWordGroupOptions.map((item) => ({
|
||||
label: `${item.groupName} (${item.hotWordCount}/200)`,
|
||||
value: item.id,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{isOwner && (
|
||||
<Modal title="编辑会议信息" open={editVisible} onOk={handleUpdateBasic} onCancel={() => setEditVisible(false)} confirmLoading={actionLoading} width={600} forceRender>
|
||||
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
||||
|
|
|
|||
|
|
@ -95,8 +95,7 @@ const PromptTemplates: React.FC = () => {
|
|||
}, [isPlatformAdmin, templateLevel, activeTenantId]);
|
||||
|
||||
const loadGroupOptions = async () => {
|
||||
const targetTenantId = isPlatformAdmin && Number(templateLevel) === 1 ? 0 : undefined;
|
||||
const res = await getHotWordGroupOptions(targetTenantId ?? (isPlatformAdmin && activeTenantId === 0 ? 0 : undefined));
|
||||
const res = await getHotWordGroupOptions();
|
||||
setGroupOptions(res.data?.data || []);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue