From ddd97e05148890c8fe9f5fc5a4dca91ff2b11823 Mon Sep 17 00:00:00 2001 From: chenhao Date: Fri, 17 Apr 2026 10:09:18 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/MeetingSummaryPromptAssembler.java | 160 ++++++++++++++++++ .../legacy/LegacyAuthControllerTest.java | 109 ++++++++++++ .../legacy/LegacyPromptControllerTest.java | 82 +++++++++ .../impl/ClientDownloadServiceImplTest.java | 103 +++++++++++ .../biz/impl/ExternalAppServiceImplTest.java | 97 +++++++++++ .../impl/MeetingAccessServiceImplTest.java | 47 +++++ .../MeetingSummaryPromptAssemblerTest.java | 92 ++++++++++ 7 files changed, 690 insertions(+) create mode 100644 backend/src/main/java/com/imeeting/service/biz/impl/MeetingSummaryPromptAssembler.java create mode 100644 backend/src/test/java/com/imeeting/controller/android/legacy/LegacyAuthControllerTest.java create mode 100644 backend/src/test/java/com/imeeting/controller/android/legacy/LegacyPromptControllerTest.java create mode 100644 backend/src/test/java/com/imeeting/service/biz/impl/ClientDownloadServiceImplTest.java create mode 100644 backend/src/test/java/com/imeeting/service/biz/impl/ExternalAppServiceImplTest.java create mode 100644 backend/src/test/java/com/imeeting/service/biz/impl/MeetingAccessServiceImplTest.java create mode 100644 backend/src/test/java/com/imeeting/service/biz/impl/MeetingSummaryPromptAssemblerTest.java diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingSummaryPromptAssembler.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingSummaryPromptAssembler.java new file mode 100644 index 0000000..3c94b24 --- /dev/null +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingSummaryPromptAssembler.java @@ -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 buildTaskConfig(Long summaryModelId, Long promptId, String userPrompt) { + Map 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 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 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; + } +} diff --git a/backend/src/test/java/com/imeeting/controller/android/legacy/LegacyAuthControllerTest.java b/backend/src/test/java/com/imeeting/controller/android/legacy/LegacyAuthControllerTest.java new file mode 100644 index 0000000..a09fd4e --- /dev/null +++ b/backend/src/test/java/com/imeeting/controller/android/legacy/LegacyAuthControllerTest.java @@ -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 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 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 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()); + } +} diff --git a/backend/src/test/java/com/imeeting/controller/android/legacy/LegacyPromptControllerTest.java b/backend/src/test/java/com/imeeting/controller/android/legacy/LegacyPromptControllerTest.java new file mode 100644 index 0000000..53b9322 --- /dev/null +++ b/backend/src/test/java/com/imeeting/controller/android/legacy/LegacyPromptControllerTest.java @@ -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> 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 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 response = controller.activePrompts("OTHER"); + + assertEquals("400", response.getCode()); + assertNull(response.getData()); + } +} diff --git a/backend/src/test/java/com/imeeting/service/biz/impl/ClientDownloadServiceImplTest.java b/backend/src/test/java/com/imeeting/service/biz/impl/ClientDownloadServiceImplTest.java new file mode 100644 index 0000000..518bc28 --- /dev/null +++ b/backend/src/test/java/com/imeeting/service/biz/impl/ClientDownloadServiceImplTest.java @@ -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> 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 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; + } +} diff --git a/backend/src/test/java/com/imeeting/service/biz/impl/ExternalAppServiceImplTest.java b/backend/src/test/java/com/imeeting/service/biz/impl/ExternalAppServiceImplTest.java new file mode 100644 index 0000000..d75e198 --- /dev/null +++ b/backend/src/test/java/com/imeeting/service/biz/impl/ExternalAppServiceImplTest.java @@ -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> 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 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; + } +} diff --git a/backend/src/test/java/com/imeeting/service/biz/impl/MeetingAccessServiceImplTest.java b/backend/src/test/java/com/imeeting/service/biz/impl/MeetingAccessServiceImplTest.java new file mode 100644 index 0000000..b5698f8 --- /dev/null +++ b/backend/src/test/java/com/imeeting/service/biz/impl/MeetingAccessServiceImplTest.java @@ -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 ")); + } +} diff --git a/backend/src/test/java/com/imeeting/service/biz/impl/MeetingSummaryPromptAssemblerTest.java b/backend/src/test/java/com/imeeting/service/biz/impl/MeetingSummaryPromptAssemblerTest.java new file mode 100644 index 0000000..e47bc67 --- /dev/null +++ b/backend/src/test/java/com/imeeting/service/biz/impl/MeetingSummaryPromptAssemblerTest.java @@ -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 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)); + } +}