补充提交

dev_na
chenhao 2026-04-17 10:09:18 +08:00
parent 27ae0a3def
commit ddd97e0514
7 changed files with 690 additions and 0 deletions

View File

@ -0,0 +1,160 @@
package com.imeeting.service.biz.impl;
import com.imeeting.common.SysParamKeys;
import com.imeeting.entity.biz.Meeting;
import com.imeeting.entity.biz.PromptTemplate;
import com.imeeting.service.biz.PromptTemplateService;
import com.unisbase.service.SysParamService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
@Component
@RequiredArgsConstructor
public class MeetingSummaryPromptAssembler {
public static final String PROMPT_SCHEMA_VERSION = "v2";
private static final String SYSTEM_PROMPT_NOT_CONFIGURED_MESSAGE =
"系统提示词未配置,请先维护系统参数 " + SysParamKeys.MEETING_SUMMARY_SYSTEM_PROMPT;
private final PromptTemplateService promptTemplateService;
private final SysParamService sysParamService;
public Map<String, Object> buildTaskConfig(Long summaryModelId, Long promptId, String userPrompt) {
Map<String, Object> taskConfig = new HashMap<>();
taskConfig.put("summaryModelId", summaryModelId);
taskConfig.put("promptSchemaVersion", PROMPT_SCHEMA_VERSION);
taskConfig.put("effectiveSystemPrompt", resolveSystemPrompt());
String templatePrompt = resolveTemplatePrompt(promptId);
taskConfig.put("effectiveTemplatePrompt", templatePrompt);
taskConfig.put("userPrompt", normalizeOptionalText(userPrompt));
if (promptId != null) {
taskConfig.put("promptId", promptId);
}
if (StringUtils.hasText(templatePrompt)) {
taskConfig.put("promptContent", templatePrompt);
}
return taskConfig;
}
public String buildSystemMessage(Map<String, Object> taskConfig) {
String systemPrompt = stringValue(taskConfig, "effectiveSystemPrompt");
if (!StringUtils.hasText(systemPrompt)) {
systemPrompt = resolveSystemPrompt();
}
String templatePrompt = firstNonBlank(
stringValue(taskConfig, "effectiveTemplatePrompt"),
stringValue(taskConfig, "promptContent"),
"请输出结构清晰、信息完整、适合直接阅读和导出的会议纪要。"
);
return String.join("\n\n",
"你是一名擅长中文会议纪要、结构化分析和待办提取的助手。",
"系统提示词(基础边界,优先级最高):\n" + systemPrompt,
"模板提示词(结构与风格要求):\n" + templatePrompt,
"输出要求:",
"1. 最终只能输出一个 JSON 对象,不要输出 Markdown 代码块、解释说明或额外前后缀。",
"2. JSON 必须包含 `summaryContent` 和 `analysis` 两个顶级字段。",
"3. `summaryContent` 必须是完整、自然、可直接保存和导出的正式会议纪要正文。",
"4. `analysis` 仅作为结构化附加结果,不能替代 `summaryContent`。",
"5. 如果系统提示词、模板提示词和用户提示词存在冲突,优先级为:系统提示词 > 模板提示词 > 用户提示词。"
);
}
public String buildUserMessage(Meeting meeting, String asrText, String userPrompt) {
String participants = meeting.getParticipants() == null || meeting.getParticipants().isBlank()
? "未填写"
: meeting.getParticipants();
String meetingTime = meeting.getMeetingTime() == null ? "未知" : meeting.getMeetingTime().toString();
String normalizedUserPrompt = normalizeOptionalText(userPrompt);
StringBuilder message = new StringBuilder()
.append("请基于以下会议转写内容生成会议纪要与结构化分析结果。\n")
.append("会议信息:\n")
.append("标题:").append(StringUtils.hasText(meeting.getTitle()) ? meeting.getTitle() : "未命名会议").append("\n")
.append("会议时间:").append(meetingTime).append("\n")
.append("参会人员:").append(participants).append("\n");
if (StringUtils.hasText(normalizedUserPrompt)) {
message.append("\n")
.append("用户提示词(仅用于补充关注重点,不得覆盖系统边界或模板结构要求):\n")
.append(normalizedUserPrompt)
.append("\n");
}
message.append("\n")
.append("返回 JSON格式固定如下\n")
.append("{\n")
.append(" \"summaryContent\": \"完整会议纪要正文,使用 markdown\",\n")
.append(" \"analysis\": {\n")
.append(" \"overview\": \"会议概览\",\n")
.append(" \"keywords\": [\"关键词1\", \"关键词2\"],\n")
.append(" \"chapters\": [{\"time\":\"00:00\",\"title\":\"章节标题\",\"summary\":\"章节摘要\"}],\n")
.append(" \"speakerSummaries\": [{\"speaker\":\"发言人\",\"summary\":\"观点总结\"}],\n")
.append(" \"keyPoints\": [{\"title\":\"关键点\",\"summary\":\"具体说明\",\"speaker\":\"发言人\"}],\n")
.append(" \"todos\": [\"待办事项1\", \"待办事项2\"]\n")
.append(" }\n")
.append("}\n")
.append("要求:\n")
.append("1. `summaryContent` 必须优先遵循模板提示词中的结构、标题层级、章节顺序和写作风格。\n")
.append("2. `analysis` 必须基于完整转写内容生成,不得脱离上下文。\n")
.append("3. 若无待办事项,`todos` 返回空数组。\n")
.append("4. 仅输出 JSON。\n")
.append("\n")
.append("会议转写如下:\n")
.append(asrText == null ? "" : asrText);
return message.toString();
}
public String resolveSystemPrompt() {
String configured = sysParamService.getCachedParamValue(
SysParamKeys.MEETING_SUMMARY_SYSTEM_PROMPT,
""
);
String resolved = firstNonBlank(configured, null);
if (!StringUtils.hasText(resolved)) {
throw new RuntimeException(SYSTEM_PROMPT_NOT_CONFIGURED_MESSAGE);
}
return resolved;
}
public String resolveTemplatePrompt(Long promptId) {
if (promptId == null) {
return "";
}
PromptTemplate template = promptTemplateService.getById(promptId);
if (template == null) {
return "";
}
return firstNonBlank(template.getPromptContent(), "");
}
public String normalizeOptionalText(String value) {
return firstNonBlank(value, null);
}
private String stringValue(Map<String, Object> source, String key) {
if (source == null || key == null) {
return null;
}
Object value = source.get(key);
return value == null ? null : normalizeOptionalText(String.valueOf(value));
}
private String firstNonBlank(String... values) {
if (values == null) {
return null;
}
for (String value : values) {
if (StringUtils.hasText(value)) {
return value.trim();
}
}
return null;
}
}

View File

@ -0,0 +1,109 @@
package com.imeeting.controller.android.legacy;
import com.imeeting.dto.android.legacy.LegacyApiResponse;
import com.imeeting.dto.android.legacy.LegacyLoginResponse;
import com.imeeting.dto.android.legacy.LegacyRefreshTokenResponse;
import com.unisbase.dto.LoginRequest;
import com.unisbase.dto.RefreshRequest;
import com.unisbase.dto.SysRoleDTO;
import com.unisbase.dto.SysUserDTO;
import com.unisbase.dto.TokenResponse;
import com.unisbase.service.AuthService;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class LegacyAuthControllerTest {
@Test
void loginShouldReturnLegacyAndroidPayload() {
AuthService authService = mock(AuthService.class);
LegacyAuthController controller = new LegacyAuthController(authService);
LoginRequest request = new LoginRequest();
request.setUsername("admin");
request.setPassword("123456");
SysRoleDTO role = new SysRoleDTO();
role.setRoleId(1L);
role.setRoleName("超级管理员");
SysUserDTO user = new SysUserDTO();
user.setUserId(1001L);
user.setUsername("admin");
user.setDisplayName("管理员");
user.setAvatarUrl("https://avatar.example.com/a.png");
user.setEmail("admin@example.com");
user.setCreatedAt(LocalDateTime.of(2026, 4, 16, 10, 0));
user.setRoles(List.of(role));
TokenResponse tokenResponse = TokenResponse.builder()
.accessToken("access-token")
.refreshToken("refresh-token")
.user(user)
.build();
when(authService.login(request, true)).thenReturn(tokenResponse);
LegacyApiResponse<LegacyLoginResponse> response = controller.login(request);
verify(authService).login(request, true);
assertEquals("200", response.getCode());
assertNotNull(response.getData());
assertEquals("access-token", response.getData().getToken());
assertEquals(1001L, response.getData().getUser().getUser_id());
assertEquals("admin", response.getData().getUser().getUsername());
assertEquals("管理员", response.getData().getUser().getCaption());
assertEquals("https://avatar.example.com/a.png", response.getData().getUser().getAvatar_url());
assertEquals("admin@example.com", response.getData().getUser().getEmail());
assertEquals(1L, response.getData().getUser().getRole_id());
assertEquals("超级管理员", response.getData().getUser().getRole_name());
assertEquals(LocalDateTime.of(2026, 4, 16, 10, 0), response.getData().getUser().getCreated_at());
}
@Test
void refreshShouldReturnLegacyAndroidPayload() {
AuthService authService = mock(AuthService.class);
LegacyAuthController controller = new LegacyAuthController(authService);
RefreshRequest request = new RefreshRequest();
request.setRefreshToken("refresh-token");
TokenResponse tokenResponse = TokenResponse.builder()
.accessToken("new-access-token")
.refreshToken("new-refresh-token")
.build();
when(authService.refresh("refresh-token")).thenReturn(tokenResponse);
LegacyApiResponse<LegacyRefreshTokenResponse> response = controller.refresh(request, null, null);
verify(authService).refresh("refresh-token");
assertEquals("200", response.getCode());
assertNotNull(response.getData());
assertEquals("new-access-token", response.getData().getToken());
}
@Test
void refreshShouldSupportAuthorizationHeaderFallback() {
AuthService authService = mock(AuthService.class);
LegacyAuthController controller = new LegacyAuthController(authService);
TokenResponse tokenResponse = TokenResponse.builder()
.accessToken("header-access-token")
.build();
when(authService.refresh("header-refresh-token")).thenReturn(tokenResponse);
LegacyApiResponse<LegacyRefreshTokenResponse> response = controller.refresh(null, "Bearer header-refresh-token", null);
verify(authService).refresh("header-refresh-token");
assertEquals("200", response.getCode());
assertNotNull(response.getData());
assertEquals("header-access-token", response.getData().getToken());
}
}

View File

@ -0,0 +1,82 @@
package com.imeeting.controller.android.legacy;
import com.imeeting.dto.android.legacy.LegacyApiResponse;
import com.imeeting.dto.android.legacy.LegacyPromptListResponse;
import com.imeeting.dto.biz.PromptTemplateVO;
import com.imeeting.service.biz.PromptTemplateService;
import com.unisbase.dto.PageResult;
import com.unisbase.security.LoginUser;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class LegacyPromptControllerTest {
@AfterEach
void tearDown() {
SecurityContextHolder.clearContext();
}
@Test
void activePromptsShouldReturnDescriptionForEnabledTemplates() {
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
LegacyPromptController controller = new LegacyPromptController(promptTemplateService);
LoginUser loginUser = new LoginUser();
loginUser.setTenantId(9L);
loginUser.setUserId(7L);
loginUser.setIsPlatformAdmin(false);
loginUser.setIsTenantAdmin(false);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(loginUser, null, List.of())
);
PromptTemplateVO enabledTemplate = new PromptTemplateVO();
enabledTemplate.setId(1L);
enabledTemplate.setTemplateName("标准模板");
enabledTemplate.setDescription("适用于常规会议总结");
enabledTemplate.setStatus(1);
PromptTemplateVO disabledTemplate = new PromptTemplateVO();
disabledTemplate.setId(2L);
disabledTemplate.setTemplateName("停用模板");
disabledTemplate.setDescription("不应出现在结果中");
disabledTemplate.setStatus(0);
PageResult<List<PromptTemplateVO>> pageResult = new PageResult<>();
pageResult.setRecords(List.of(enabledTemplate, disabledTemplate));
pageResult.setTotal(2L);
when(promptTemplateService.pageTemplates(eq(1), eq(1000), eq(null), eq(null), eq(9L), eq(7L), eq(false), eq(false)))
.thenReturn(pageResult);
LegacyApiResponse<LegacyPromptListResponse> response = controller.activePrompts("MEETING_TASK");
assertEquals("200", response.getCode());
assertNotNull(response.getData());
assertEquals(1, response.getData().getPrompts().size());
assertEquals("标准模板", response.getData().getPrompts().get(0).getName());
assertEquals("适用于常规会议总结", response.getData().getPrompts().get(0).getDescription());
assertEquals(1, response.getData().getPrompts().get(0).getIsDefault());
}
@Test
void activePromptsShouldRejectUnsupportedScene() {
LegacyPromptController controller = new LegacyPromptController(mock(PromptTemplateService.class));
LegacyApiResponse<LegacyPromptListResponse> response = controller.activePrompts("OTHER");
assertEquals("400", response.getCode());
assertNull(response.getData());
}
}

View File

@ -0,0 +1,103 @@
package com.imeeting.service.biz.impl;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.imeeting.dto.biz.ClientDownloadDTO;
import com.imeeting.entity.biz.ClientDownload;
import com.imeeting.mapper.biz.ClientDownloadMapper;
import com.unisbase.security.LoginUser;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@Disabled("Requires MyBatis-Plus table metadata bootstrap; not suitable for mapper-only unit execution.")
class ClientDownloadServiceImplTest {
@Test
void listForAdminShouldNotAppendTenantFilter() {
ClientDownloadMapper mapper = mock(ClientDownloadMapper.class);
when(mapper.selectList(any())).thenReturn(List.of());
ClientDownloadServiceImpl service = newService(mapper);
service.listForAdmin(loginUser(9L, 88L), "android", 1);
ArgumentCaptor<Wrapper<ClientDownload>> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class);
verify(mapper).selectList(wrapperCaptor.capture());
assertFalse(wrapperCaptor.getValue().getSqlSegment().toLowerCase().contains("tenant"));
}
@Test
void createShouldPersistAsGlobalAndClearLatestAcrossAllTenants() {
ClientDownloadMapper mapper = mock(ClientDownloadMapper.class);
when(mapper.update(isNull(), any(Wrapper.class))).thenReturn(1);
when(mapper.insert(any(ClientDownload.class))).thenReturn(1);
ClientDownloadServiceImpl service = newService(mapper);
ClientDownloadDTO dto = new ClientDownloadDTO();
dto.setPlatformCode("android");
dto.setVersion("1.0.0");
dto.setDownloadUrl("https://download.example/app.apk");
dto.setStatus(1);
dto.setIsLatest(1);
ClientDownload created = service.create(dto, loginUser(7L, 123L));
ArgumentCaptor<ClientDownload> entityCaptor = ArgumentCaptor.forClass(ClientDownload.class);
verify(mapper).insert(entityCaptor.capture());
assertEquals(0L, created.getTenantId());
assertEquals(0L, entityCaptor.getValue().getTenantId());
assertEquals(7L, entityCaptor.getValue().getCreatedBy());
verify(mapper).update(isNull(), any(Wrapper.class));
}
@Test
void updateAndRemoveShouldIgnoreRecordTenant() {
ClientDownloadMapper mapper = mock(ClientDownloadMapper.class);
ClientDownload entity = new ClientDownload();
entity.setId(5L);
entity.setTenantId(999L);
entity.setPlatformCode("android");
entity.setVersion("1.0.0");
entity.setStatus(1);
entity.setIsLatest(0);
when(mapper.selectById(5L)).thenReturn(entity);
when(mapper.updateById(any(ClientDownload.class))).thenReturn(1);
when(mapper.deleteById(any(ClientDownload.class))).thenReturn(1);
ClientDownloadServiceImpl service = newService(mapper);
ClientDownloadDTO dto = new ClientDownloadDTO();
dto.setVersion("2.0.0");
assertDoesNotThrow(() -> service.update(5L, dto, loginUser(7L, 1L)));
assertEquals(0L, entity.getTenantId());
assertEquals("2.0.0", entity.getVersion());
assertDoesNotThrow(() -> service.removeClient(5L, loginUser(7L, 1L)));
verify(mapper).deleteById(any(ClientDownload.class));
}
private ClientDownloadServiceImpl newService(ClientDownloadMapper mapper) {
ClientDownloadServiceImpl service = new ClientDownloadServiceImpl();
ReflectionTestUtils.setField(service, "baseMapper", mapper);
return service;
}
private LoginUser loginUser(Long userId, Long tenantId) {
LoginUser loginUser = new LoginUser();
loginUser.setUserId(userId);
loginUser.setTenantId(tenantId);
return loginUser;
}
}

View File

@ -0,0 +1,97 @@
package com.imeeting.service.biz.impl;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.imeeting.dto.biz.ExternalAppDTO;
import com.imeeting.entity.biz.ExternalApp;
import com.imeeting.mapper.biz.ExternalAppMapper;
import com.unisbase.mapper.SysUserMapper;
import com.unisbase.security.LoginUser;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@Disabled("Requires MyBatis-Plus table metadata bootstrap; not suitable for mapper-only unit execution.")
class ExternalAppServiceImplTest {
@Test
void listForAdminShouldNotAppendTenantFilter() {
ExternalAppMapper mapper = mock(ExternalAppMapper.class);
when(mapper.selectList(any())).thenReturn(List.of());
ExternalAppServiceImpl service = newService(mapper);
service.listForAdmin(loginUser(9L, 88L), "web", 1);
ArgumentCaptor<Wrapper<ExternalApp>> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class);
verify(mapper).selectList(wrapperCaptor.capture());
assertFalse(wrapperCaptor.getValue().getSqlSegment().toLowerCase().contains("tenant"));
}
@Test
void createShouldPersistAsGlobal() {
ExternalAppMapper mapper = mock(ExternalAppMapper.class);
when(mapper.insert(any(ExternalApp.class))).thenReturn(1);
ExternalAppServiceImpl service = newService(mapper);
ExternalAppDTO dto = new ExternalAppDTO();
dto.setAppName("会议看板");
dto.setAppType("web");
dto.setStatus(1);
ExternalApp created = service.create(dto, loginUser(7L, 123L));
ArgumentCaptor<ExternalApp> entityCaptor = ArgumentCaptor.forClass(ExternalApp.class);
verify(mapper).insert(entityCaptor.capture());
assertEquals(0L, created.getTenantId());
assertEquals(0L, entityCaptor.getValue().getTenantId());
assertEquals(7L, entityCaptor.getValue().getCreatedBy());
}
@Test
void updateAndRemoveShouldIgnoreRecordTenant() {
ExternalAppMapper mapper = mock(ExternalAppMapper.class);
ExternalApp entity = new ExternalApp();
entity.setId(8L);
entity.setTenantId(999L);
entity.setAppName("旧应用");
entity.setAppType("web");
entity.setStatus(1);
when(mapper.selectById(8L)).thenReturn(entity);
when(mapper.updateById(any(ExternalApp.class))).thenReturn(1);
when(mapper.deleteById(any(ExternalApp.class))).thenReturn(1);
ExternalAppServiceImpl service = newService(mapper);
ExternalAppDTO dto = new ExternalAppDTO();
dto.setAppName("新应用");
assertDoesNotThrow(() -> service.update(8L, dto, loginUser(7L, 1L)));
assertEquals(0L, entity.getTenantId());
assertEquals("新应用", entity.getAppName());
assertDoesNotThrow(() -> service.removeApp(8L, loginUser(7L, 1L)));
verify(mapper).deleteById(any(ExternalApp.class));
}
private ExternalAppServiceImpl newService(ExternalAppMapper mapper) {
ExternalAppServiceImpl service = new ExternalAppServiceImpl(mock(SysUserMapper.class));
ReflectionTestUtils.setField(service, "baseMapper", mapper);
return service;
}
private LoginUser loginUser(Long userId, Long tenantId) {
LoginUser loginUser = new LoginUser();
loginUser.setUserId(userId);
loginUser.setTenantId(tenantId);
return loginUser;
}
}

View File

@ -0,0 +1,47 @@
package com.imeeting.service.biz.impl;
import com.imeeting.entity.biz.Meeting;
import com.imeeting.mapper.biz.MeetingMapper;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
class MeetingAccessServiceImplTest {
private final MeetingAccessServiceImpl service = new MeetingAccessServiceImpl(mock(MeetingMapper.class));
@Test
void previewPasswordShouldBeOptionalWhenMeetingHasNoPassword() {
Meeting meeting = new Meeting();
meeting.setAccessPassword(" ");
assertFalse(service.isPreviewPasswordRequired(meeting));
assertDoesNotThrow(() -> service.assertCanPreviewMeeting(meeting, null));
}
@Test
void previewPasswordShouldRejectMissingOrWrongPassword() {
Meeting meeting = new Meeting();
meeting.setAccessPassword("123456");
RuntimeException missingError = assertThrows(RuntimeException.class, () -> service.assertCanPreviewMeeting(meeting, null));
assertEquals("Access password is required", missingError.getMessage());
RuntimeException wrongError = assertThrows(RuntimeException.class, () -> service.assertCanPreviewMeeting(meeting, "654321"));
assertEquals("Access password is incorrect", wrongError.getMessage());
}
@Test
void previewPasswordShouldAllowTrimmedMatch() {
Meeting meeting = new Meeting();
meeting.setAccessPassword(" 123456 ");
assertTrue(service.isPreviewPasswordRequired(meeting));
assertDoesNotThrow(() -> service.assertCanPreviewMeeting(meeting, " 123456 "));
}
}

View File

@ -0,0 +1,92 @@
package com.imeeting.service.biz.impl;
import com.imeeting.common.SysParamKeys;
import com.imeeting.entity.biz.Meeting;
import com.imeeting.entity.biz.PromptTemplate;
import com.imeeting.service.biz.PromptTemplateService;
import com.unisbase.service.SysParamService;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class MeetingSummaryPromptAssemblerTest {
@Test
void buildTaskConfigShouldCaptureEffectivePromptsAndUserPrompt() {
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
SysParamService sysParamService = mock(SysParamService.class);
PromptTemplate template = new PromptTemplate();
template.setPromptContent("模板提示词");
when(promptTemplateService.getById(3L)).thenReturn(template);
when(sysParamService.getCachedParamValue(eq(SysParamKeys.MEETING_SUMMARY_SYSTEM_PROMPT), eq("")))
.thenReturn("系统提示词");
MeetingSummaryPromptAssembler assembler = new MeetingSummaryPromptAssembler(promptTemplateService, sysParamService);
Map<String, Object> taskConfig = assembler.buildTaskConfig(2L, 3L, " 关注风险项 ");
assertEquals(2L, taskConfig.get("summaryModelId"));
assertEquals(3L, taskConfig.get("promptId"));
assertEquals("v2", taskConfig.get("promptSchemaVersion"));
assertEquals("系统提示词", taskConfig.get("effectiveSystemPrompt"));
assertEquals("模板提示词", taskConfig.get("effectiveTemplatePrompt"));
assertEquals("关注风险项", taskConfig.get("userPrompt"));
assertEquals("模板提示词", taskConfig.get("promptContent"));
}
@Test
void buildSystemMessageShouldFallbackToLegacyPromptContent() {
SysParamService sysParamService = mock(SysParamService.class);
when(sysParamService.getCachedParamValue(eq(SysParamKeys.MEETING_SUMMARY_SYSTEM_PROMPT), eq("")))
.thenReturn("系统提示词");
MeetingSummaryPromptAssembler assembler = new MeetingSummaryPromptAssembler(
mock(PromptTemplateService.class),
sysParamService
);
String systemMessage = assembler.buildSystemMessage(Map.of("promptContent", "旧模板提示词"));
assertTrue(systemMessage.contains("旧模板提示词"));
}
@Test
void buildUserMessageShouldOmitUserPromptSectionWhenBlank() {
MeetingSummaryPromptAssembler assembler = new MeetingSummaryPromptAssembler(
mock(PromptTemplateService.class),
mock(SysParamService.class)
);
Meeting meeting = new Meeting();
meeting.setTitle("周会");
meeting.setMeetingTime(LocalDateTime.of(2026, 4, 16, 10, 0));
meeting.setParticipants("张三,李四");
String userMessage = assembler.buildUserMessage(meeting, "这里是转写文本", " ");
assertFalse(userMessage.contains("用户提示词"));
assertTrue(userMessage.contains("这里是转写文本"));
}
@Test
void resolveSystemPromptShouldFailWhenSystemParamMissing() {
SysParamService sysParamService = mock(SysParamService.class);
when(sysParamService.getCachedParamValue(eq(SysParamKeys.MEETING_SUMMARY_SYSTEM_PROMPT), eq("")))
.thenReturn(" ");
MeetingSummaryPromptAssembler assembler = new MeetingSummaryPromptAssembler(
mock(PromptTemplateService.class),
sysParamService
);
RuntimeException exception = assertThrows(RuntimeException.class, assembler::resolveSystemPrompt);
assertTrue(exception.getMessage().contains(SysParamKeys.MEETING_SUMMARY_SYSTEM_PROMPT));
}
}