From 7dcf4f0646f64fad7befcc1e4d01ca4c9e55d5b2 Mon Sep 17 00:00:00 2001 From: chenhao Date: Mon, 3 Aug 2026 15:32:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(prompt):=20=E5=A2=9E=E5=8A=A0=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E8=AF=8D=E6=A8=A1=E6=9D=BF=E9=BB=98=E8=AE=A4=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E4=B8=8E=E4=BD=9C=E7=94=A8=E5=9F=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构 PromptTemplate 相关服务与实体,增加 isDefault 和 defaultScope 字段以支持个人、租户和平台级别的默认模板配置。前端同步更新模板管理页面与 API 接口,并在会议创建组件中集成相关逻辑。 --- .../android/AndroidMeetingController.java | 9 +- .../AndroidMeetingRealtimeController.java | 1 + .../biz/PromptTemplateController.java | 31 +++ .../imeeting/dto/biz/PromptTemplateVO.java | 8 + .../imeeting/entity/biz/PromptTemplate.java | 3 + .../entity/biz/PromptTemplateUserConfig.java | 3 + .../impl/LegacyMeetingAdapterServiceImpl.java | 3 + .../biz/MeetingRuntimeProfileResolver.java | 1 + .../service/biz/PromptTemplateService.java | 6 + .../biz/impl/MeetingCommandServiceImpl.java | 11 +- .../MeetingRuntimeProfileResolverImpl.java | 12 +- .../biz/impl/PromptTemplateServiceImpl.java | 199 +++++++++++++++++- frontend/src/api/business/prompt.ts | 12 ++ .../business/MeetingCreateDrawer.tsx | 20 +- .../src/pages/business/PromptTemplates.css | 16 +- .../src/pages/business/PromptTemplates.tsx | 51 ++++- 16 files changed, 366 insertions(+), 20 deletions(-) diff --git a/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java b/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java index d2ddfdb..6dd503b 100644 --- a/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java +++ b/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java @@ -76,6 +76,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.Arrays; +import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -454,7 +455,13 @@ public class AndroidMeetingController { ? List.of() : promptTemplateList.getRecords().stream() .filter(item -> Integer.valueOf(1).equals(item.getStatus())) - .toList(); + .collect(Collectors.toList()); + PromptTemplate effectiveDefault = promptTemplateService.findEffectiveUserDefaultTemplate(tenantId, userId); + if (effectiveDefault != null) { + enabledTemplates.sort(Comparator.comparing( + item -> !Objects.equals(item.getId(), effectiveDefault.getId()) + )); + } resultVo.setTemplateList(enabledTemplates); PageResult> modelList = aiModelService.pageModels(1, 1000, null, "LLM", tenantId, false); List enabledModels = modelList.getRecords() == null diff --git a/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingRealtimeController.java b/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingRealtimeController.java index fe1c74e..e6a67ce 100644 --- a/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingRealtimeController.java +++ b/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingRealtimeController.java @@ -78,6 +78,7 @@ public class AndroidMeetingRealtimeController { meetingAuthorizationService.assertCanCreateMeeting(authContext); RealtimeMeetingRuntimeProfile runtimeProfile = meetingRuntimeProfileResolver.resolve( authContext.getTenantId(), + authContext.getUserId(), command == null ? null : command.getAsrModelId(), command == null ? null : command.getSummaryModelId(), command == null ? null : command.getPromptId(), diff --git a/backend/src/main/java/com/imeeting/controller/biz/PromptTemplateController.java b/backend/src/main/java/com/imeeting/controller/biz/PromptTemplateController.java index 0162205..0265474 100644 --- a/backend/src/main/java/com/imeeting/controller/biz/PromptTemplateController.java +++ b/backend/src/main/java/com/imeeting/controller/biz/PromptTemplateController.java @@ -119,6 +119,37 @@ public class PromptTemplateController { return ApiResponse.ok(true); } + @Operation(summary = "设置默认提示词模板") + @PutMapping("/{id}/default") + @PreAuthorize("isAuthenticated()") + @Log(value = "设置默认提示词模板", type = "提示词模板管理") + public ApiResponse setDefault(@PathVariable Long id) { + LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + boolean success = promptTemplateService.setUserDefaultTemplate( + id, + loginUser.getTenantId(), + loginUser.getUserId(), + loginUser.getIsPlatformAdmin(), + loginUser.getIsTenantAdmin() + ); + return success ? ApiResponse.ok(true) : ApiResponse.error("模板不存在、不可用或无权限访问"); + } + + @Operation(summary = "取消默认提示词模板") + @DeleteMapping("/{id}/default") + @PreAuthorize("isAuthenticated()") + @Log(value = "取消默认提示词模板", type = "提示词模板管理") + public ApiResponse clearDefault(@PathVariable Long id) { + LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + return ApiResponse.ok(promptTemplateService.clearUserDefaultTemplate( + id, + loginUser.getTenantId(), + loginUser.getUserId(), + loginUser.getIsPlatformAdmin(), + loginUser.getIsTenantAdmin() + )); + } + @Operation(summary = "删除提示词模板") @DeleteMapping("/{id}") @PreAuthorize("isAuthenticated()") diff --git a/backend/src/main/java/com/imeeting/dto/biz/PromptTemplateVO.java b/backend/src/main/java/com/imeeting/dto/biz/PromptTemplateVO.java index 3dbbe4e..dea9b8c 100644 --- a/backend/src/main/java/com/imeeting/dto/biz/PromptTemplateVO.java +++ b/backend/src/main/java/com/imeeting/dto/biz/PromptTemplateVO.java @@ -30,6 +30,14 @@ public class PromptTemplateVO { private String hotWordGroupName; @Schema(description = "绑定热词列表") private List hotWords; + @Schema(description = "是否为当前用户默认模板") + private Boolean isDefault; + @Schema(description = "默认模板来源:PERSONAL-个人,TENANT-租户,PLATFORM-平台") + private String defaultScope; + @Schema(description = "是否为模板所属层级默认模板") + private Boolean isTemplateDefault; + @Schema(description = "默认模板当前是否有效") + private Boolean defaultAvailable; @Schema(description = "使用次数") private Integer usageCount; @Schema(description = "提示词正文") diff --git a/backend/src/main/java/com/imeeting/entity/biz/PromptTemplate.java b/backend/src/main/java/com/imeeting/entity/biz/PromptTemplate.java index d24cd8b..4db4b69 100644 --- a/backend/src/main/java/com/imeeting/entity/biz/PromptTemplate.java +++ b/backend/src/main/java/com/imeeting/entity/biz/PromptTemplate.java @@ -29,6 +29,9 @@ public class PromptTemplate extends BaseEntity { @Schema(description = "是否系统内置") private Integer isSystem; + @Schema(description = "是否为所属层级默认模板:1-是,0-否") + private Integer isDefault; + @Schema(description = "创建人ID") private Long creatorId; diff --git a/backend/src/main/java/com/imeeting/entity/biz/PromptTemplateUserConfig.java b/backend/src/main/java/com/imeeting/entity/biz/PromptTemplateUserConfig.java index 464c685..045a359 100644 --- a/backend/src/main/java/com/imeeting/entity/biz/PromptTemplateUserConfig.java +++ b/backend/src/main/java/com/imeeting/entity/biz/PromptTemplateUserConfig.java @@ -23,4 +23,7 @@ public class PromptTemplateUserConfig extends BaseEntity { @Schema(description = "模板ID") private Long templateId; + + @Schema(description = "是否为当前用户默认模板:1-是,0-否") + private Integer isDefault; } diff --git a/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java b/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java index 6d5fe98..b80a599 100644 --- a/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java @@ -117,6 +117,7 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ : MeetingConstants.SUMMARY_DETAIL_STANDARD; RealtimeMeetingRuntimeProfile runtimeProfile = runtimeProfileResolver.resolve( tenantId, + creatorUserId, null, requestedSummaryModelId, requestedPromptId, @@ -202,6 +203,7 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ RealtimeMeetingRuntimeProfile profile = runtimeProfileResolver.resolve( loginUser.getTenantId(), + loginUser.getUserId(), null, effectiveSummaryModelId, effectivePromptId, @@ -279,6 +281,7 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ } RealtimeMeetingRuntimeProfile profile = runtimeProfileResolver.resolve( meeting.getTenantId(), + loginUser.getUserId(), null, effectiveSummaryModelId, effectivePromptId, diff --git a/backend/src/main/java/com/imeeting/service/biz/MeetingRuntimeProfileResolver.java b/backend/src/main/java/com/imeeting/service/biz/MeetingRuntimeProfileResolver.java index 6def540..84db6d4 100644 --- a/backend/src/main/java/com/imeeting/service/biz/MeetingRuntimeProfileResolver.java +++ b/backend/src/main/java/com/imeeting/service/biz/MeetingRuntimeProfileResolver.java @@ -6,6 +6,7 @@ import java.util.List; public interface MeetingRuntimeProfileResolver { RealtimeMeetingRuntimeProfile resolve(Long tenantId, + Long userId, Long asrModelId, Long summaryModelId, Long promptId, diff --git a/backend/src/main/java/com/imeeting/service/biz/PromptTemplateService.java b/backend/src/main/java/com/imeeting/service/biz/PromptTemplateService.java index f0637c1..b795e8f 100644 --- a/backend/src/main/java/com/imeeting/service/biz/PromptTemplateService.java +++ b/backend/src/main/java/com/imeeting/service/biz/PromptTemplateService.java @@ -17,4 +17,10 @@ public interface PromptTemplateService extends IService { Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin); boolean updateUserTemplateStatus(Long templateId, Integer status, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin); boolean isTemplateEnabledForUser(Long templateId, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin); + + boolean setUserDefaultTemplate(Long templateId, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin); + + boolean clearUserDefaultTemplate(Long templateId, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin); + + PromptTemplate findEffectiveUserDefaultTemplate(Long tenantId, Long userId); } diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java index a4f8745..65af57c 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java @@ -165,7 +165,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { String meetingSource, String sourceDeviceCode, String sourceDeviceMode) { - RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId); + RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId, creatorId); Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId); String resolvedCreatorName = resolveMeetingUserName(creatorId, creatorName); String hostName = resolveMeetingUserName(hostUserId, resolvedCreatorName); @@ -245,7 +245,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { @Override @Transactional(rollbackFor = Exception.class) public MeetingVO createRealtimeMeeting(CreateRealtimeMeetingCommand command, Long tenantId, Long creatorId, String creatorName, String meetingSource) { - RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId); + RealtimeMeetingRuntimeProfile runtimeProfile = resolveCreateProfile(command, tenantId, creatorId); Long hostUserId = resolveHostUserId(command.getHostUserId(), creatorId); String resolvedCreatorName = resolveMeetingUserName(creatorId, creatorName); String hostName = resolveMeetingUserName(hostUserId, resolvedCreatorName); @@ -299,6 +299,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { String deviceCode) { RealtimeMeetingRuntimeProfile runtimeProfile = meetingRuntimeProfileResolver.resolve( tenantId, + creatorId, command.getAsrModelId(), command.getSummaryModelId(), command.getPromptId(), @@ -1542,9 +1543,10 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { return resumeConfig; } - private RealtimeMeetingRuntimeProfile resolveCreateProfile(CreateMeetingCommand command, Long tenantId) { + private RealtimeMeetingRuntimeProfile resolveCreateProfile(CreateMeetingCommand command, Long tenantId, Long userId) { return meetingRuntimeProfileResolver.resolve( tenantId, + userId, command.getAsrModelId(), command.getSummaryModelId(), command.getPromptId(), @@ -1560,9 +1562,10 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { ); } - private RealtimeMeetingRuntimeProfile resolveCreateProfile(CreateRealtimeMeetingCommand command, Long tenantId) { + private RealtimeMeetingRuntimeProfile resolveCreateProfile(CreateRealtimeMeetingCommand command, Long tenantId, Long userId) { return meetingRuntimeProfileResolver.resolve( tenantId, + userId, command.getAsrModelId(), command.getSummaryModelId(), command.getPromptId(), diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingRuntimeProfileResolverImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingRuntimeProfileResolverImpl.java index 157c170..7f6d66b 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingRuntimeProfileResolverImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingRuntimeProfileResolverImpl.java @@ -34,6 +34,7 @@ public class MeetingRuntimeProfileResolverImpl implements MeetingRuntimeProfileR @Override public RealtimeMeetingRuntimeProfile resolve(Long tenantId, + Long userId, Long asrModelId, Long summaryModelId, Long promptId, @@ -49,7 +50,7 @@ public class MeetingRuntimeProfileResolverImpl implements MeetingRuntimeProfileR long resolvedTenantId = tenantId == null ? 0L : tenantId; AiModelVO asrModel = resolveModel("ASR", asrModelId, resolvedTenantId); AiModelVO summaryModel = resolveModel("LLM", summaryModelId, resolvedTenantId); - PromptTemplate promptTemplate = resolvePrompt(promptId, resolvedTenantId); + PromptTemplate promptTemplate = resolvePrompt(promptId, resolvedTenantId, userId); RealtimeMeetingRuntimeProfile profile = new RealtimeMeetingRuntimeProfile(); profile.setResolvedAsrModelId(asrModel.getId()); @@ -194,7 +195,7 @@ public class MeetingRuntimeProfileResolverImpl implements MeetingRuntimeProfileR return entity == null ? null : entity.getId(); } - private PromptTemplate resolvePrompt(Long requestedId, Long tenantId) { + private PromptTemplate resolvePrompt(Long requestedId, Long tenantId, Long userId) { if (requestedId != null) { PromptTemplate template = promptTemplateService.getById(requestedId); if (template == null) { @@ -204,7 +205,12 @@ public class MeetingRuntimeProfileResolverImpl implements MeetingRuntimeProfileR return template; } - PromptTemplate template = promptTemplateService.getOne(new LambdaQueryWrapper() + PromptTemplate template = promptTemplateService.findEffectiveUserDefaultTemplate(tenantId, userId); + if (template != null) { + return template; + } + + template = promptTemplateService.getOne(new LambdaQueryWrapper() .eq(PromptTemplate::getStatus, 1) .eq(PromptTemplate::getIsSystem, 1) .and(wrapper -> wrapper.eq(PromptTemplate::getTenantId, tenantId).or().eq(PromptTemplate::getTenantId, 0L)) diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/PromptTemplateServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/PromptTemplateServiceImpl.java index 64cce28..93b6d06 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/PromptTemplateServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/PromptTemplateServiceImpl.java @@ -1,6 +1,7 @@ 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.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.imeeting.dto.biz.PromptTemplateDTO; @@ -69,10 +70,20 @@ public class PromptTemplateServiceImpl extends ServiceImpl wrapper = buildVisibilityWrapper(tenantId, userId, isPlatformAdmin, isTenantAdmin); wrapper.like(name != null && !name.isEmpty(), PromptTemplate::getTemplateName, name) - .eq(category != null && !category.isEmpty(), PromptTemplate::getCategory, category) - .orderByAsc(PromptTemplate::getIsSystem) + .eq(category != null && !category.isEmpty(), PromptTemplate::getCategory, category); + + PromptTemplateUserConfig configuredDefault = findUserDefaultConfig(tenantId, userId); + Long configuredDefaultId = configuredDefault == null ? null : configuredDefault.getTemplateId(); + DefaultTemplateSelection defaultSelection = findEffectiveDefaultTemplate(tenantId, userId); + PromptTemplate defaultTemplate = defaultSelection == null ? null : defaultSelection.template(); + if (defaultTemplate == null) { + wrapper.orderByAsc(PromptTemplate::getIsSystem) .orderByDesc(PromptTemplate::getTenantId) - .orderByDesc(PromptTemplate::getCreatedAt); + .orderByDesc(PromptTemplate::getCreatedAt); + } else { + wrapper.last("ORDER BY CASE WHEN id = " + defaultTemplate.getId() + + " THEN 0 ELSE 1 END, is_system ASC, tenant_id DESC, created_at DESC"); + } Page page = this.page(new Page<>(current, size), wrapper); List records = page.getRecords(); @@ -80,7 +91,16 @@ public class PromptTemplateServiceImpl extends ServiceImpl hotWordGroupMap = queryHotWordGroupMap(records.stream().map(PromptTemplate::getHotWordGroupId).toList()); List vos = records.stream() - .map(template -> toVO(template, effectiveStatus(template.getIsSystem(), template.getStatus(), userStatusMap.get(template.getId())), hotWordGroupMap)) + .map(template -> { + Integer status = effectiveStatus(template.getIsSystem(), template.getStatus(), userStatusMap.get(template.getId())); + PromptTemplateVO vo = toVO(template, status, hotWordGroupMap); + boolean isConfiguredPersonalDefault = Objects.equals(configuredDefaultId, template.getId()); + boolean isEffectiveDefault = defaultTemplate != null && Objects.equals(defaultTemplate.getId(), template.getId()); + vo.setIsDefault(isEffectiveDefault || isConfiguredPersonalDefault); + vo.setDefaultAvailable(isEffectiveDefault); + vo.setDefaultScope(isEffectiveDefault ? defaultSelection.scope() : isConfiguredPersonalDefault ? DEFAULT_SCOPE_PERSONAL : null); + return vo; + }) .collect(Collectors.toList()); PageResult> result = new PageResult<>(); @@ -98,7 +118,17 @@ public class PromptTemplateServiceImpl extends ServiceImpl hotWordGroupMap = queryHotWordGroupMap(java.util.Collections.singletonList(template.getHotWordGroupId())); - PromptTemplateVO vo = toVO(template, template.getStatus(), hotWordGroupMap); + Integer userStatus = queryUserStatusMap(tenantId, userId, java.util.Collections.singletonList(template.getId())) + .get(template.getId()); + Integer status = effectiveStatus(template.getIsSystem(), template.getStatus(), userStatus); + PromptTemplateVO vo = toVO(template, status, hotWordGroupMap); + PromptTemplateUserConfig configuredDefault = findUserDefaultConfig(tenantId, userId); + DefaultTemplateSelection defaultSelection = findEffectiveDefaultTemplate(tenantId, userId); + boolean isConfiguredPersonalDefault = configuredDefault != null && Objects.equals(configuredDefault.getTemplateId(), template.getId()); + boolean isEffectiveDefault = defaultSelection != null && Objects.equals(defaultSelection.template().getId(), template.getId()); + vo.setIsDefault(isEffectiveDefault || isConfiguredPersonalDefault); + vo.setDefaultAvailable(isEffectiveDefault); + vo.setDefaultScope(isEffectiveDefault ? defaultSelection.scope() : isConfiguredPersonalDefault ? DEFAULT_SCOPE_PERSONAL : null); vo.setHotWords(resolveHotWords(template.getHotWordGroupId())); return vo; } @@ -156,6 +186,138 @@ public class PromptTemplateServiceImpl extends ServiceImpl() + .eq(PromptTemplateUserConfig::getTenantId, tenantId) + .eq(PromptTemplateUserConfig::getUserId, userId) + .eq(PromptTemplateUserConfig::getIsDefault, 1) + .set(PromptTemplateUserConfig::getIsDefault, 0)); + + PromptTemplateUserConfig existing = findUserConfig(tenantId, userId, templateId); + if (existing == null) { + PromptTemplateUserConfig entity = new PromptTemplateUserConfig(); + entity.setTenantId(tenantId); + entity.setUserId(userId); + entity.setTemplateId(templateId); + entity.setStatus(1); + entity.setIsDefault(1); + return userConfigMapper.insert(entity) > 0; + } + existing.setIsDefault(1); + return userConfigMapper.updateById(existing) > 0; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean clearUserDefaultTemplate(Long templateId, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin) { + if (Boolean.TRUE.equals(isPlatformAdmin)) { + return clearSystemDefaultTemplate(templateId, 0L); + } + if (Boolean.TRUE.equals(isTenantAdmin)) { + return clearSystemDefaultTemplate(templateId, tenantId); + } + userConfigMapper.update(null, new LambdaUpdateWrapper() + .eq(PromptTemplateUserConfig::getTenantId, tenantId) + .eq(PromptTemplateUserConfig::getUserId, userId) + .eq(PromptTemplateUserConfig::getTemplateId, templateId) + .eq(PromptTemplateUserConfig::getIsDefault, 1) + .set(PromptTemplateUserConfig::getIsDefault, 0)); + return true; + } + + @Override + public PromptTemplate findEffectiveUserDefaultTemplate(Long tenantId, Long userId) { + if (tenantId == null) { + return null; + } + DefaultTemplateSelection selection = findEffectiveDefaultTemplate(tenantId, userId); + return selection == null ? null : selection.template(); + } + + private boolean setSystemDefaultTemplate(Long templateId, Long scopeTenantId) { + PromptTemplate template = this.getById(templateId); + if (template == null + || !Integer.valueOf(1).equals(template.getIsSystem()) + || !Objects.equals(template.getTenantId(), scopeTenantId) + || !Integer.valueOf(1).equals(template.getStatus())) { + return false; + } + this.update(new LambdaUpdateWrapper() + .eq(PromptTemplate::getTenantId, scopeTenantId) + .eq(PromptTemplate::getIsSystem, 1) + .eq(PromptTemplate::getIsDefault, 1) + .set(PromptTemplate::getIsDefault, 0)); + template.setIsDefault(1); + return this.updateById(template); + } + + private boolean clearSystemDefaultTemplate(Long templateId, Long scopeTenantId) { + return this.update(new LambdaUpdateWrapper() + .eq(PromptTemplate::getId, templateId) + .eq(PromptTemplate::getTenantId, scopeTenantId) + .eq(PromptTemplate::getIsSystem, 1) + .eq(PromptTemplate::getIsDefault, 1) + .set(PromptTemplate::getIsDefault, 0)); + } + + private DefaultTemplateSelection findEffectiveDefaultTemplate(Long tenantId, Long userId) { + PromptTemplateUserConfig config = findUserDefaultConfig(tenantId, userId); + if (config != null && Integer.valueOf(1).equals(config.getStatus())) { + PromptTemplate template = findAvailableDefaultTemplate(config.getTemplateId(), tenantId, userId); + if (template != null) { + return new DefaultTemplateSelection(template, DEFAULT_SCOPE_PERSONAL); + } + } + PromptTemplate template = findAvailableSystemDefaultTemplate(tenantId, tenantId, userId); + if (template != null) { + return new DefaultTemplateSelection(template, DEFAULT_SCOPE_TENANT); + } + template = findAvailableSystemDefaultTemplate(0L, tenantId, userId); + return template == null ? null : new DefaultTemplateSelection(template, DEFAULT_SCOPE_PLATFORM); + } + + private PromptTemplate findAvailableDefaultTemplate(Long templateId, Long tenantId, Long userId) { + PromptTemplate template = this.getById(templateId); + if (template == null || !Integer.valueOf(1).equals(template.getStatus())) { + return null; + } + if (Integer.valueOf(0).equals(template.getIsSystem()) && !Objects.equals(template.getCreatorId(), userId)) { + return null; + } + return Objects.equals(template.getTenantId(), tenantId) || Long.valueOf(0L).equals(template.getTenantId()) ? template : null; + } + + private PromptTemplate findAvailableSystemDefaultTemplate(Long scopeTenantId, Long userTenantId, Long userId) { + PromptTemplate template = this.getOne(new LambdaQueryWrapper() + .eq(PromptTemplate::getTenantId, scopeTenantId) + .eq(PromptTemplate::getIsSystem, 1) + .eq(PromptTemplate::getIsDefault, 1) + .eq(PromptTemplate::getStatus, 1) + .last("LIMIT 1")); + if (template == null) { + return null; + } + if (userId == null) { + return template; + } + PromptTemplateUserConfig config = findUserConfig(userTenantId, userId, template.getId()); + return effectiveStatus(template.getIsSystem(), template.getStatus(), config == null ? null : config.getStatus()) == 1 + ? template + : null; + } + private void validateHotWordGroupBinding(Long hotWordGroupId, Long templateTenantId) { if (hotWordGroupId == null) { return; @@ -215,6 +377,25 @@ public class PromptTemplateServiceImpl extends ServiceImpl() + .eq(PromptTemplateUserConfig::getTenantId, tenantId) + .eq(PromptTemplateUserConfig::getUserId, userId) + .eq(PromptTemplateUserConfig::getTemplateId, templateId) + .last("LIMIT 1")); + } + + private PromptTemplateUserConfig findUserDefaultConfig(Long tenantId, Long userId) { + if (tenantId == null || userId == null) { + return null; + } + return userConfigMapper.selectOne(new LambdaQueryWrapper() + .eq(PromptTemplateUserConfig::getTenantId, tenantId) + .eq(PromptTemplateUserConfig::getUserId, userId) + .eq(PromptTemplateUserConfig::getIsDefault, 1) + .last("LIMIT 1")); + } + private Map queryHotWordGroupMap(List hotWordGroupIds) { List ids = hotWordGroupIds == null ? List.of() : hotWordGroupIds.stream() .filter(Objects::nonNull) @@ -271,6 +452,7 @@ public class PromptTemplateServiceImpl extends ServiceImpl { { params: { status } } ); }; + +export const setPromptDefault = (id: number) => { + return http.put<{ code: string; data: boolean; msg: string }>(`/api/biz/prompt/${id}/default`); +}; + +export const clearPromptDefault = (id: number) => { + return http.delete<{ code: string; data: boolean; msg: string }>(`/api/biz/prompt/${id}/default`); +}; diff --git a/frontend/src/components/business/MeetingCreateDrawer.tsx b/frontend/src/components/business/MeetingCreateDrawer.tsx index 79e2f72..5fc9935 100644 --- a/frontend/src/components/business/MeetingCreateDrawer.tsx +++ b/frontend/src/components/business/MeetingCreateDrawer.tsx @@ -212,7 +212,7 @@ export const MeetingCreateDrawer: React.FC = ({ setHotWordGroups((hotWordGroupRes.data.data || []).filter((item: HotWordGroupVO) => item.status === 1)); setUserList(users || []); - const defaultPrompt = activePrompts[0]; + const defaultPrompt = activePrompts.find((item: PromptTemplateVO) => item.isDefault && item.defaultAvailable); form.setFieldsValue({ title: nextType === "upload" ? `文件会议 ${dayjs().format("MM-DD HH:mm")}` : `实时会议 ${dayjs().format("MM-DD HH:mm")}`, meetingTime: dayjs(), @@ -517,14 +517,28 @@ export const MeetingCreateDrawer: React.FC = ({ - + {prompts.length > 15 ? ( - {prompts.map(p => )} ) : (
+ +
form.setFieldsValue({promptId: undefined})} + className={watchedPromptId == null ? "meeting-create-template-card is-selected" : "meeting-create-template-card"} + > +
系统自动选择
+ {watchedPromptId == null && +
} +
+ {prompts.map(p => { const isSelected = watchedPromptId === p.id; return ( diff --git a/frontend/src/pages/business/PromptTemplates.css b/frontend/src/pages/business/PromptTemplates.css index 96c8e72..061a807 100644 --- a/frontend/src/pages/business/PromptTemplates.css +++ b/frontend/src/pages/business/PromptTemplates.css @@ -40,11 +40,25 @@ gap: 4px; } -.prompt-template-name-cell > .ant-typography { +.prompt-template-name-cell__title-row { + display: flex; + align-items: center; + min-width: 0; + gap: 6px; +} + +.prompt-template-name-cell__title-row > .ant-typography { + min-width: 0; max-width: 100%; margin: 0; } +.prompt-template-name-cell__title-row > .ant-tag { + flex: 0 0 auto; + margin: 0; + border-radius: 4px; +} + .prompt-template-description.ant-typography { display: block; max-width: 360px; diff --git a/frontend/src/pages/business/PromptTemplates.tsx b/frontend/src/pages/business/PromptTemplates.tsx index 958914a..0bb36e8 100644 --- a/frontend/src/pages/business/PromptTemplates.tsx +++ b/frontend/src/pages/business/PromptTemplates.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { App, Button, @@ -21,16 +21,27 @@ import PageContainer from "@/components/shared/PageContainer"; import DataListPanel from "@/components/shared/DataListPanel"; import FormDrawer from "@/components/shared/FormDrawer"; import SectionCard from "@/components/shared/SectionCard"; -import { CopyOutlined, DeleteOutlined, EditOutlined, EyeOutlined, PlusOutlined, SaveOutlined } from '@ant-design/icons'; +import { + CopyOutlined, + DeleteOutlined, + EditOutlined, + EyeOutlined, + PlusOutlined, + SaveOutlined, + StarFilled, + StarOutlined +} from '@ant-design/icons'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { useTranslation } from 'react-i18next'; import { useDict } from '../../hooks/useDict'; import { deletePromptTemplate, + clearPromptDefault, getPromptDetail, getPromptPage, savePromptTemplate, + setPromptDefault, updatePromptStatus, updatePromptTemplate, type PromptTemplateVO, @@ -295,7 +306,11 @@ const PromptTemplates: React.FC = () => { width: 280, render: (_: unknown, item: PromptTemplateVO) => (
- {item.templateName} +
+ {item.templateName} + {item.isDefault ? {item.defaultAvailable ? '默认' : '默认已失效'} : null} +
{item.description ? {item.description} : null}
@@ -345,6 +360,19 @@ const PromptTemplates: React.FC = () => { fixed: 'right' as const, render: (_: unknown, item: PromptTemplateVO) => { const canEdit = canManageTemplate(item); + const canManageDefault = isPlatformAdmin + ? item.isSystem === 1 && Number(item.tenantId) === 0 + : isTenantAdmin + ? item.isSystem === 1 && Number(item.tenantId) === activeTenantId + : item.status === 1; + const isCurrentScopeDefault = isPlatformAdmin || isTenantAdmin + ? item.isTemplateDefault === true + : item.isDefault === true && item.defaultScope === "PERSONAL"; + const defaultActionLabel = isPlatformAdmin + ? "平台默认" + : isTenantAdmin + ? "租户默认" + : "个人默认"; return ( e.stopPropagation()}> @@ -361,6 +389,23 @@ const PromptTemplates: React.FC = () => {