feat(prompt): 增加提示词模板默认配置与作用域

重构 PromptTemplate 相关服务与实体,增加 isDefault 和 defaultScope 字段以支持个人、租户和平台级别的默认模板配置。前端同步更新模板管理页面与 API 接口,并在会议创建组件中集成相关逻辑。
dev_na
chenhao 2026-08-03 15:32:35 +08:00
parent a53d85bbec
commit 7dcf4f0646
16 changed files with 366 additions and 20 deletions

View File

@ -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<List<AiModelVO>> modelList = aiModelService.pageModels(1, 1000, null, "LLM", tenantId, false);
List<AiModelVO> enabledModels = modelList.getRecords() == null

View File

@ -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(),

View File

@ -119,6 +119,37 @@ public class PromptTemplateController {
return ApiResponse.ok(true);
}
@Operation(summary = "设置默认提示词模板")
@PutMapping("/{id}/default")
@PreAuthorize("isAuthenticated()")
@Log(value = "设置默认提示词模板", type = "提示词模板管理")
public ApiResponse<Boolean> 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<Boolean> 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()")

View File

@ -30,6 +30,14 @@ public class PromptTemplateVO {
private String hotWordGroupName;
@Schema(description = "绑定热词列表")
private List<String> 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 = "提示词正文")

View File

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

View File

@ -23,4 +23,7 @@ public class PromptTemplateUserConfig extends BaseEntity {
@Schema(description = "模板ID")
private Long templateId;
@Schema(description = "是否为当前用户默认模板1-是0-否")
private Integer isDefault;
}

View File

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

View File

@ -6,6 +6,7 @@ import java.util.List;
public interface MeetingRuntimeProfileResolver {
RealtimeMeetingRuntimeProfile resolve(Long tenantId,
Long userId,
Long asrModelId,
Long summaryModelId,
Long promptId,

View File

@ -17,4 +17,10 @@ public interface PromptTemplateService extends IService<PromptTemplate> {
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);
}

View File

@ -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(),

View File

@ -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>()
PromptTemplate template = promptTemplateService.findEffectiveUserDefaultTemplate(tenantId, userId);
if (template != null) {
return template;
}
template = promptTemplateService.getOne(new LambdaQueryWrapper<PromptTemplate>()
.eq(PromptTemplate::getStatus, 1)
.eq(PromptTemplate::getIsSystem, 1)
.and(wrapper -> wrapper.eq(PromptTemplate::getTenantId, tenantId).or().eq(PromptTemplate::getTenantId, 0L))

View File

@ -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<PromptTemplateMapper,
Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin) {
LambdaQueryWrapper<PromptTemplate> 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<PromptTemplate> page = this.page(new Page<>(current, size), wrapper);
List<PromptTemplate> records = page.getRecords();
@ -80,7 +91,16 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
Map<Long, HotWordGroup> hotWordGroupMap = queryHotWordGroupMap(records.stream().map(PromptTemplate::getHotWordGroupId).toList());
List<PromptTemplateVO> 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<List<PromptTemplateVO>> result = new PageResult<>();
@ -98,7 +118,17 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
throw new IllegalArgumentException("模板不存在");
}
Map<Long, HotWordGroup> 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<PromptTemplateMapper,
return effectiveStatus(template.getIsSystem(), template.getStatus(), userStatus) == 1;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean setUserDefaultTemplate(Long templateId, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin) {
if (Boolean.TRUE.equals(isPlatformAdmin)) {
return setSystemDefaultTemplate(templateId, 0L);
}
if (Boolean.TRUE.equals(isTenantAdmin)) {
return setSystemDefaultTemplate(templateId, tenantId);
}
if (!isTemplateEnabledForUser(templateId, tenantId, userId, isPlatformAdmin, isTenantAdmin)) {
return false;
}
userConfigMapper.update(null, new LambdaUpdateWrapper<PromptTemplateUserConfig>()
.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<PromptTemplateUserConfig>()
.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<PromptTemplate>()
.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<PromptTemplate>()
.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<PromptTemplate>()
.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<PromptTemplateMapper,
return statusMap;
}
private PromptTemplateUserConfig findUserConfig(Long tenantId, Long userId, Long templateId) {
return userConfigMapper.selectOne(new LambdaQueryWrapper<PromptTemplateUserConfig>()
.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<PromptTemplateUserConfig>()
.eq(PromptTemplateUserConfig::getTenantId, tenantId)
.eq(PromptTemplateUserConfig::getUserId, userId)
.eq(PromptTemplateUserConfig::getIsDefault, 1)
.last("LIMIT 1"));
}
private Map<Long, HotWordGroup> queryHotWordGroupMap(List<Long> hotWordGroupIds) {
List<Long> ids = hotWordGroupIds == null ? List.of() : hotWordGroupIds.stream()
.filter(Objects::nonNull)
@ -271,6 +452,7 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
vo.setDescription(entity.getDescription());
vo.setCategory(entity.getCategory());
vo.setIsSystem(entity.getIsSystem());
vo.setIsTemplateDefault(Integer.valueOf(1).equals(entity.getIsDefault()));
vo.setTags(entity.getTags());
Long hotWordGroupId = entity.getHotWordGroupId();
vo.setHotWordGroupId(hotWordGroupId);
@ -284,4 +466,11 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
vo.setUpdatedAt(entity.getUpdatedAt());
return vo;
}
private static final String DEFAULT_SCOPE_PERSONAL = "PERSONAL";
private static final String DEFAULT_SCOPE_TENANT = "TENANT";
private static final String DEFAULT_SCOPE_PLATFORM = "PLATFORM";
private record DefaultTemplateSelection(PromptTemplate template, String scope) {
}
}

View File

@ -12,6 +12,10 @@ export interface PromptTemplateVO {
hotWordGroupId?: number;
hotWordGroupName?: string;
hotWords?: string[];
isDefault?: boolean;
defaultScope?: "PERSONAL" | "TENANT" | "PLATFORM";
isTemplateDefault?: boolean;
defaultAvailable?: boolean;
usageCount: number;
promptContent: string;
status: number;
@ -78,3 +82,11 @@ export const updatePromptStatus = (id: number, status: number) => {
{ 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`);
};

View File

@ -212,7 +212,7 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({
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<MeetingCreateDrawerProps> = ({
</Col>
</Row>
<Form.Item name="promptId" label="总结模板" rules={[{ required: true }]}>
<Form.Item
name="promptId"
label="总结模板"
extra="未选择时,系统会按当前可用模板规则自动选择"
>
{prompts.length > 15 ? (
<Select placeholder="请选择总结模板" showSearch optionFilterProp="children">
<Select allowClear placeholder="请选择总结模板" showSearch optionFilterProp="children">
{prompts.map(p => <Option key={p.id} value={p.id}>{p.templateName}</Option>)}
</Select>
) : (
<div className="meeting-create-template-grid">
<Row gutter={[12, 12]}>
<Col xs={24} sm={12} md={8}>
<div
onClick={() => form.setFieldsValue({promptId: undefined})}
className={watchedPromptId == null ? "meeting-create-template-card is-selected" : "meeting-create-template-card"}
>
<div className="meeting-create-template-card__name"></div>
{watchedPromptId == null &&
<div className="meeting-create-template-card__check"><CheckOutlined/></div>}
</div>
</Col>
{prompts.map(p => {
const isSelected = watchedPromptId === p.id;
return (

View File

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

View File

@ -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) => (
<div className="prompt-template-name-cell">
<Text strong ellipsis={{ tooltip: item.templateName }}>{item.templateName}</Text>
<div className="prompt-template-name-cell__title-row">
<Text strong ellipsis={{tooltip: item.templateName}}>{item.templateName}</Text>
{item.isDefault ? <Tag
color={item.defaultAvailable ? 'gold' : 'default'}>{item.defaultAvailable ? '默认' : '默认已失效'}</Tag> : null}
</div>
{item.description ? <Text type="secondary" className="prompt-template-description"
ellipsis={{tooltip: item.description}}>{item.description}</Text> : null}
</div>
@ -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 (
<Space size={2} onClick={(e) => e.stopPropagation()}>
<Tooltip title="查看">
@ -361,6 +389,23 @@ const PromptTemplates: React.FC = () => {
<Button type="text" size="small" icon={<CopyOutlined/>} onClick={() => handleOpenDrawer(item, true)}
aria-label="以此创建模板"/>
</Tooltip>
{canManageDefault && (
<Tooltip title={isCurrentScopeDefault ? `取消${defaultActionLabel}` : `设为${defaultActionLabel}`}>
<Button
type="text"
size="small"
icon={isCurrentScopeDefault ? <StarFilled/> : <StarOutlined/>}
onClick={() => {
const request = isCurrentScopeDefault ? clearPromptDefault(item.id) : setPromptDefault(item.id);
request.then(() => {
message.success(isCurrentScopeDefault ? `已取消${defaultActionLabel}` : `已设为${defaultActionLabel}`);
void fetchData();
});
}}
aria-label={isCurrentScopeDefault ? `取消${defaultActionLabel}` : `设为${defaultActionLabel}`}
/>
</Tooltip>
)}
{canEdit && (
<Popconfirm
title="确认删除该模板吗?"