From 855e93ecbf1f1005e95de73f28d3cc5c4dcd1506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Mon, 13 Apr 2026 14:58:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B1=95=E7=A4=BA=20AI=20=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E5=88=9B=E5=BB=BA=E4=BA=BA=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为 Bot、工作流、知识库、插件列表补充创建人名称回填 - 在卡片中展示创建者与创建时间 - 补充后端与前端对应测试 --- easyflow-api/easyflow-api-admin/pom.xml | 12 ++ .../admin/controller/ai/BotController.java | 4 + .../ai/DocumentCollectionController.java | 4 + .../admin/controller/ai/PluginController.java | 10 +- .../controller/ai/WorkflowController.java | 4 + .../support/AiResourceCreatorNameSupport.java | 119 ++++++++++++++ .../controller/ai/BotControllerTest.java | 145 ++++++++++++++++++ .../java/tech/easyflow/ai/entity/Bot.java | 21 +++ .../ai/entity/DocumentCollection.java | 21 +++ .../java/tech/easyflow/ai/entity/Plugin.java | 21 +++ .../tech/easyflow/ai/entity/Workflow.java | 21 +++ .../easyflow-module-system/pom.xml | 12 ++ .../system/service/SysAccountService.java | 9 ++ .../service/impl/SysAccountServiceImpl.java | 52 +++++++ .../impl/SysAccountServiceImplTest.java | 54 +++++++ .../app/src/components/page/CardList.vue | 75 ++++++++- .../page/__tests__/CardList.test.ts | 61 ++++++++ 17 files changed, 642 insertions(+), 3 deletions(-) create mode 100644 easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/support/AiResourceCreatorNameSupport.java create mode 100644 easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/ai/BotControllerTest.java create mode 100644 easyflow-modules/easyflow-module-system/src/test/java/tech/easyflow/system/service/impl/SysAccountServiceImplTest.java diff --git a/easyflow-api/easyflow-api-admin/pom.xml b/easyflow-api/easyflow-api-admin/pom.xml index 6bec8d3..621bed7 100644 --- a/easyflow-api/easyflow-api-admin/pom.xml +++ b/easyflow-api/easyflow-api-admin/pom.xml @@ -36,5 +36,17 @@ tech.easyflow easyflow-common-captcha + + junit + junit + ${junit.version} + test + + + org.mockito + mockito-core + 5.12.0 + test + diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotController.java index da12346..45707e9 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotController.java @@ -16,6 +16,7 @@ import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport; import tech.easyflow.ai.easyagents.listener.PromptChoreChatStreamListener; import tech.easyflow.ai.entity.*; import tech.easyflow.ai.publish.BotPublishAppService; @@ -73,6 +74,8 @@ public class BotController extends BaseCurdController { private BotPublishAppService botPublishAppService; @Resource private AiResourceApprovalStateService aiResourceApprovalStateService; + @Resource + private AiResourceCreatorNameSupport aiResourceCreatorNameSupport; public BotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService, BotDocumentCollectionService botDocumentCollectionService, BotMessageService botMessageService) { @@ -305,6 +308,7 @@ public class BotController extends BaseCurdController { applyCategoryPermission(queryWrapper); Page result = super.queryPage(page, queryWrapper); aiResourceApprovalStateService.fillBotApprovalState(result.getRecords()); + aiResourceCreatorNameSupport.fillBotCreatorNames(result.getRecords()); return result; } diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentCollectionController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentCollectionController.java index 1ba5821..70e6ef9 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentCollectionController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentCollectionController.java @@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport; import tech.easyflow.ai.permission.KnowledgeVisibilityQueryHelper; import tech.easyflow.ai.documentimport.DocumentImportDtos; import tech.easyflow.ai.dto.KnowledgeSearchResultItem; @@ -76,6 +77,8 @@ public class DocumentCollectionController extends BaseCurdController result = super.queryPage(page, queryWrapper); aiResourceApprovalStateService.fillKnowledgeApprovalState(result.getRecords()); + aiResourceCreatorNameSupport.fillDocumentCollectionCreatorNames(result.getRecords()); return result; } diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/PluginController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/PluginController.java index 9d9a514..509b6d3 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/PluginController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/PluginController.java @@ -6,6 +6,7 @@ import com.mybatisflex.core.query.QueryWrapper; import jakarta.servlet.http.HttpServletRequest; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport; import tech.easyflow.ai.entity.Model; import tech.easyflow.ai.entity.Plugin; import tech.easyflow.ai.entity.Workflow; @@ -66,6 +67,8 @@ public class PluginController extends BaseCurdController private ResourceAccessService resourceAccessService; @Resource private WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver; + @Resource + private AiResourceCreatorNameSupport aiResourceCreatorNameSupport; @Override protected Result onSaveOrUpdateBefore(Plugin entity, boolean isSave) { @@ -117,7 +120,11 @@ public class PluginController extends BaseCurdController queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy())); return Result.ok(queryPage(new Page<>(pageNumber, pageSize), queryWrapper)); } else { - return pluginService.pageByCategory(pageNumber, pageSize, category); + Result> result = pluginService.pageByCategory(pageNumber, pageSize, category); + if (result != null && result.getData() != null) { + aiResourceCreatorNameSupport.fillPluginCreatorNames(result.getData().getRecords()); + } + return result; } } @@ -150,6 +157,7 @@ public class PluginController extends BaseCurdController List totalList = service.getMapper().selectListWithRelationsByQuery(queryWrapper); boolean availableOnly = isAvailableOnly(); List prepared = pluginService.preparePluginsForCurrentUser(totalList, !availableOnly, availableOnly); + aiResourceCreatorNameSupport.fillPluginCreatorNames(prepared); long total = prepared.size(); int fromIndex = Math.max(0, Math.toIntExact((page.getPageNumber() - 1) * page.getPageSize())); if (fromIndex >= prepared.size()) { diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowController.java index 510b8a9..640e01c 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowController.java @@ -15,6 +15,7 @@ import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport; import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper; import tech.easyflow.ai.easyagentsflow.entity.ChainInfo; import tech.easyflow.ai.easyagentsflow.entity.NodeInfo; @@ -93,6 +94,8 @@ public class WorkflowController extends BaseCurdController result = super.queryPage(page, queryWrapper); aiResourceApprovalStateService.fillWorkflowApprovalState(result.getRecords()); + aiResourceCreatorNameSupport.fillWorkflowCreatorNames(result.getRecords()); return result; } diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/support/AiResourceCreatorNameSupport.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/support/AiResourceCreatorNameSupport.java new file mode 100644 index 0000000..dcd2dcb --- /dev/null +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/support/AiResourceCreatorNameSupport.java @@ -0,0 +1,119 @@ +package tech.easyflow.admin.controller.ai.support; + +import org.springframework.stereotype.Component; +import tech.easyflow.ai.entity.Bot; +import tech.easyflow.ai.entity.DocumentCollection; +import tech.easyflow.ai.entity.Plugin; +import tech.easyflow.ai.entity.Workflow; +import tech.easyflow.system.service.SysAccountService; + +import javax.annotation.Resource; +import java.math.BigInteger; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * 为 AI 资源批量补充创建人展示名称。 + * + *

该组件只做展示字段填充,不参与权限或查询逻辑。

+ * + * @author Codex + * @since 2026-04-12 + */ +@Component +public class AiResourceCreatorNameSupport { + + @Resource + private SysAccountService sysAccountService; + + /** + * 批量填充工作流创建人名称。 + * + * @param workflows 工作流集合 + */ + public void fillWorkflowCreatorNames(Collection workflows) { + fillCreatorNames(workflows, Workflow::getCreatedBy, Workflow::setCreatedByName); + } + + /** + * 批量填充聊天助手创建人名称。 + * + * @param bots 聊天助手集合 + */ + public void fillBotCreatorNames(Collection bots) { + fillCreatorNames(bots, Bot::getCreatedBy, Bot::setCreatedByName); + } + + /** + * 批量填充知识库创建人名称。 + * + * @param collections 知识库集合 + */ + public void fillDocumentCollectionCreatorNames(Collection collections) { + fillCreatorNames(collections, DocumentCollection::getCreatedBy, DocumentCollection::setCreatedByName); + } + + /** + * 批量填充插件创建人名称。 + * + * @param plugins 插件集合 + */ + public void fillPluginCreatorNames(Collection plugins) { + fillCreatorNames(plugins, Plugin::getCreatedBy, Plugin::setCreatedByName); + } + + /** + * 通用的创建人名称填充逻辑。 + * + * @param resources 资源集合 + * @param createdByGetter 创建人 ID 提取函数 + * @param createdByNameSetter 创建人名称回填函数 + * @param 资源类型 + */ + private void fillCreatorNames( + Collection resources, + Function createdByGetter, + BiConsumer createdByNameSetter + ) { + if (resources == null || resources.isEmpty()) { + return; + } + LinkedHashSet creatorIds = resources.stream() + .map(createdByGetter) + .map(this::toBigInteger) + .filter(Objects::nonNull) + .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)); + if (creatorIds.isEmpty()) { + return; + } + + Map displayNameMap = sysAccountService.resolveDisplayNameMap(creatorIds); + for (T resource : resources) { + BigInteger creatorId = toBigInteger(createdByGetter.apply(resource)); + if (creatorId == null) { + continue; + } + createdByNameSetter.accept(resource, displayNameMap.getOrDefault(creatorId, creatorId.toString())); + } + } + + /** + * 统一把不同数值类型转换为 {@link BigInteger}。 + * + * @param value 原始数值 + * @return 归一化后的 {@link BigInteger} + */ + private BigInteger toBigInteger(Number value) { + if (value == null) { + return null; + } + if (value instanceof BigInteger) { + return (BigInteger) value; + } + return BigInteger.valueOf(value.longValue()); + } +} diff --git a/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/ai/BotControllerTest.java b/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/ai/BotControllerTest.java new file mode 100644 index 0000000..d91a335 --- /dev/null +++ b/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/ai/BotControllerTest.java @@ -0,0 +1,145 @@ +package tech.easyflow.admin.controller.ai; + +import com.mybatisflex.core.paginate.Page; +import com.mybatisflex.core.query.QueryWrapper; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport; +import tech.easyflow.ai.entity.Bot; +import tech.easyflow.ai.service.AiResourceApprovalStateService; +import tech.easyflow.ai.service.BotDocumentCollectionService; +import tech.easyflow.ai.service.BotMessageService; +import tech.easyflow.ai.service.BotService; +import tech.easyflow.ai.service.BotWorkflowService; +import tech.easyflow.ai.service.ModelService; +import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot; +import tech.easyflow.system.service.CategoryPermissionService; +import tech.easyflow.system.service.SysAccountService; + +import java.math.BigInteger; +import java.util.Collections; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * {@link BotController} 测试。 + */ +public class BotControllerTest { + + private BotService botService; + private ModelService modelService; + private BotWorkflowService botWorkflowService; + private BotDocumentCollectionService botDocumentCollectionService; + private BotMessageService botMessageService; + private CategoryPermissionService categoryPermissionService; + private AiResourceApprovalStateService aiResourceApprovalStateService; + private SysAccountService sysAccountService; + + /** + * 初始化测试 mock。 + */ + @BeforeMethod + public void setUp() { + botService = mock(BotService.class); + modelService = mock(ModelService.class); + botWorkflowService = mock(BotWorkflowService.class); + botDocumentCollectionService = mock(BotDocumentCollectionService.class); + botMessageService = mock(BotMessageService.class); + categoryPermissionService = mock(CategoryPermissionService.class); + aiResourceApprovalStateService = mock(AiResourceApprovalStateService.class); + sysAccountService = mock(SysAccountService.class); + } + + /** + * 验证分页结果会补充创建人展示名称,且只做一次批量账号查询。 + */ + @Test + public void shouldFillCreatedByNameForPageRecords() { + TestBotController controller = new TestBotController( + botService, + modelService, + botWorkflowService, + botDocumentCollectionService, + botMessageService + ); + AiResourceCreatorNameSupport creatorNameSupport = new AiResourceCreatorNameSupport(); + setField(creatorNameSupport, "sysAccountService", sysAccountService); + setField(controller, "categoryPermissionService", categoryPermissionService); + setField(controller, "aiResourceApprovalStateService", aiResourceApprovalStateService); + setField(controller, "aiResourceCreatorNameSupport", creatorNameSupport); + + Bot bot = new Bot(); + bot.setId(BigInteger.valueOf(101)); + bot.setCreatedBy(BigInteger.valueOf(7)); + Page page = new Page<>(Collections.singletonList(bot), 1, 10, 1); + + when(categoryPermissionService.getCurrentAccess("BOT")) + .thenReturn(new RoleCategoryAccessSnapshot("BOT", BigInteger.ONE, false, true, Collections.emptySet())); + when(botService.page(any(Page.class), any(QueryWrapper.class))).thenReturn(page); + when(sysAccountService.resolveDisplayNameMap(Collections.singleton(BigInteger.valueOf(7)))) + .thenReturn(Map.of(BigInteger.valueOf(7), "管理员")); + doNothing().when(aiResourceApprovalStateService).fillBotApprovalState(page.getRecords()); + + Page result = controller.invokeQueryPage(new Page<>(1, 10), QueryWrapper.create()); + + Assert.assertEquals(result.getRecords().get(0).getCreatedByName(), "管理员"); + verify(sysAccountService).resolveDisplayNameMap(Collections.singleton(BigInteger.valueOf(7))); + } + + /** + * 通过反射设置字段值。 + * + * @param target 目标对象 + * @param fieldName 字段名 + * @param value 字段值 + */ + private static void setField(Object target, String fieldName, Object value) { + Class current = target.getClass(); + while (current != null) { + try { + java.lang.reflect.Field field = current.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + return; + } catch (NoSuchFieldException ignored) { + current = current.getSuperclass(); + } catch (IllegalAccessException e) { + throw new IllegalStateException("设置测试字段失败: " + fieldName, e); + } + } + throw new IllegalArgumentException("未找到字段: " + fieldName); + } + + /** + * 暴露受保护分页方法的测试控制器。 + */ + private static class TestBotController extends BotController { + + TestBotController( + BotService service, + ModelService modelService, + BotWorkflowService botWorkflowService, + BotDocumentCollectionService botDocumentCollectionService, + BotMessageService botMessageService + ) { + super(service, modelService, botWorkflowService, botDocumentCollectionService, botMessageService); + } + + /** + * 调用受保护的分页查询方法。 + * + * @param page 分页对象 + * @param queryWrapper 查询条件 + * @return 查询结果 + */ + Page invokeQueryPage(Page page, QueryWrapper queryWrapper) { + return super.queryPage(page, queryWrapper); + } + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Bot.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Bot.java index f6471d6..2be82a4 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Bot.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Bot.java @@ -29,6 +29,9 @@ public class Bot extends BotBase { @Column(ignore = true) private String displayPublishStatus; + @Column(ignore = true) + private String createdByName; + public boolean isAnonymousEnabled() { Map options = getOptions(); if (options == null) { @@ -62,4 +65,22 @@ public class Bot extends BotBase { this.displayPublishStatus = displayPublishStatus; } + /** + * 获取创建人展示名称。 + * + * @return 创建人展示名称 + */ + public String getCreatedByName() { + return createdByName; + } + + /** + * 设置创建人展示名称。 + * + * @param createdByName 创建人展示名称 + */ + public void setCreatedByName(String createdByName) { + this.createdByName = createdByName; + } + } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/DocumentCollection.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/DocumentCollection.java index 3e40483..fb66f7e 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/DocumentCollection.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/DocumentCollection.java @@ -37,6 +37,9 @@ public class DocumentCollection extends DocumentCollectionBase implements Visibi @Column(ignore = true) private String displayPublishStatus; + @Column(ignore = true) + private String createdByName; + public static final String TYPE_DOCUMENT = "DOCUMENT"; public static final String TYPE_FAQ = "FAQ"; @@ -169,4 +172,22 @@ public class DocumentCollection extends DocumentCollectionBase implements Visibi public void setDisplayPublishStatus(String displayPublishStatus) { this.displayPublishStatus = displayPublishStatus; } + + /** + * 获取创建人展示名称。 + * + * @return 创建人展示名称 + */ + public String getCreatedByName() { + return createdByName; + } + + /** + * 设置创建人展示名称。 + * + * @param createdByName 创建人展示名称 + */ + public void setCreatedByName(String createdByName) { + this.createdByName = createdByName; + } } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Plugin.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Plugin.java index bd6156b..e898a8e 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Plugin.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Plugin.java @@ -31,6 +31,9 @@ public class Plugin extends PluginBase { @com.mybatisflex.annotation.Column(ignore = true) private String reasonMessage; + @com.mybatisflex.annotation.Column(ignore = true) + private String createdByName; + public String getTitle() { return this.getName(); } @@ -74,4 +77,22 @@ public class Plugin extends PluginBase { public void setReasonMessage(String reasonMessage) { this.reasonMessage = reasonMessage; } + + /** + * 获取创建人展示名称。 + * + * @return 创建人展示名称 + */ + public String getCreatedByName() { + return createdByName; + } + + /** + * 设置创建人展示名称。 + * + * @param createdByName 创建人展示名称 + */ + public void setCreatedByName(String createdByName) { + this.createdByName = createdByName; + } } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Workflow.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Workflow.java index 919b387..0140213 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Workflow.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Workflow.java @@ -26,6 +26,9 @@ public class Workflow extends WorkflowBase implements VisibilityResource { @Column(ignore = true) private String displayPublishStatus; + @Column(ignore = true) + private String createdByName; + public Tool toFunction(boolean needEnglishName) { return new WorkflowTool(this, needEnglishName); } @@ -57,4 +60,22 @@ public class Workflow extends WorkflowBase implements VisibilityResource { public void setDisplayPublishStatus(String displayPublishStatus) { this.displayPublishStatus = displayPublishStatus; } + + /** + * 获取创建人展示名称。 + * + * @return 创建人展示名称 + */ + public String getCreatedByName() { + return createdByName; + } + + /** + * 设置创建人展示名称。 + * + * @param createdByName 创建人展示名称 + */ + public void setCreatedByName(String createdByName) { + this.createdByName = createdByName; + } } diff --git a/easyflow-modules/easyflow-module-system/pom.xml b/easyflow-modules/easyflow-module-system/pom.xml index 700b8f6..ba1651f 100644 --- a/easyflow-modules/easyflow-module-system/pom.xml +++ b/easyflow-modules/easyflow-module-system/pom.xml @@ -40,5 +40,17 @@ tech.easyflow easyflow-module-log + + junit + junit + ${junit.version} + test + + + org.mockito + mockito-core + 5.12.0 + test + diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/SysAccountService.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/SysAccountService.java index 0b04eb5..6a16c5b 100644 --- a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/SysAccountService.java +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/SysAccountService.java @@ -10,6 +10,7 @@ import tech.easyflow.system.entity.vo.SysAccountImportResultVo; import java.io.OutputStream; import java.math.BigInteger; import java.util.Collection; +import java.util.Map; /** * 用户表 服务层。 @@ -19,6 +20,14 @@ import java.util.Collection; */ public interface SysAccountService extends IService { + /** + * 批量解析账号展示名称。 + * + * @param accountIds 账号 ID 集合 + * @return 账号 ID 到展示名称的映射,名称优先使用昵称,其次登录名,最后回退为 ID 字符串 + */ + Map resolveDisplayNameMap(Collection accountIds); + void syncRelations(SysAccount entity); SysAccount getByUsername(String userKey); diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysAccountServiceImpl.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysAccountServiceImpl.java index e5be820..2c26390 100644 --- a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysAccountServiceImpl.java +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysAccountServiceImpl.java @@ -55,6 +55,7 @@ import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.function.Function; @@ -110,6 +111,57 @@ public class SysAccountServiceImpl extends ServiceImpl resolveDisplayNameMap(Collection accountIds) { + if (accountIds == null || accountIds.isEmpty()) { + return Collections.emptyMap(); + } + LinkedHashSet normalizedIds = accountIds.stream() + .filter(Objects::nonNull) + .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)); + if (normalizedIds.isEmpty()) { + return Collections.emptyMap(); + } + + QueryWrapper queryWrapper = QueryWrapper.create() + .select(SysAccount::getId, SysAccount::getNickname, SysAccount::getLoginName) + .in(SysAccount::getId, normalizedIds); + List accounts = list(queryWrapper); + LinkedHashMap displayNameMap = new LinkedHashMap<>(normalizedIds.size()); + for (SysAccount account : accounts) { + if (account == null || account.getId() == null) { + continue; + } + displayNameMap.put(account.getId(), resolveDisplayName(account)); + } + normalizedIds.forEach(id -> displayNameMap.putIfAbsent(id, id.toString())); + return displayNameMap; + } + + /** + * 解析单个账号的展示名称。 + * + * @param account 账号实体 + * @return 展示名称 + */ + private String resolveDisplayName(SysAccount account) { + String nickname = trimToNull(account.getNickname()); + if (StringUtil.hasText(nickname)) { + return nickname; + } + String loginName = trimToNull(account.getLoginName()); + if (StringUtil.hasText(loginName)) { + return loginName; + } + return account.getId() == null ? "" : account.getId().toString(); + } + @Override @Transactional(rollbackFor = Exception.class) public void syncRelations(SysAccount entity) { diff --git a/easyflow-modules/easyflow-module-system/src/test/java/tech/easyflow/system/service/impl/SysAccountServiceImplTest.java b/easyflow-modules/easyflow-module-system/src/test/java/tech/easyflow/system/service/impl/SysAccountServiceImplTest.java new file mode 100644 index 0000000..076bc9a --- /dev/null +++ b/easyflow-modules/easyflow-module-system/src/test/java/tech/easyflow/system/service/impl/SysAccountServiceImplTest.java @@ -0,0 +1,54 @@ +package tech.easyflow.system.service.impl; + +import com.mybatisflex.core.query.QueryWrapper; +import org.junit.Test; + +import tech.easyflow.system.entity.SysAccount; + +import java.math.BigInteger; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * {@link SysAccountServiceImpl} 测试。 + */ +public class SysAccountServiceImplTest { + + /** + * 验证展示名解析优先级与缺失数据回退。 + */ + @Test + public void shouldResolveDisplayNameByNicknameThenLoginNameThenId() { + SysAccountServiceImpl service = spy(new SysAccountServiceImpl()); + + SysAccount nicknameAccount = new SysAccount(); + nicknameAccount.setId(BigInteger.ONE); + nicknameAccount.setNickname("管理员"); + nicknameAccount.setLoginName("admin"); + + SysAccount loginNameAccount = new SysAccount(); + loginNameAccount.setId(BigInteger.valueOf(2)); + loginNameAccount.setNickname(" "); + loginNameAccount.setLoginName("operator"); + + doReturn(List.of(nicknameAccount, loginNameAccount)) + .when(service) + .list(any(QueryWrapper.class)); + + Map displayNameMap = service.resolveDisplayNameMap( + List.of(BigInteger.ONE, BigInteger.valueOf(2), BigInteger.valueOf(3)) + ); + + assertEquals("管理员", displayNameMap.get(BigInteger.ONE)); + assertEquals("operator", displayNameMap.get(BigInteger.valueOf(2))); + assertEquals("3", displayNameMap.get(BigInteger.valueOf(3))); + verify(service, times(1)).list(any(QueryWrapper.class)); + } +} diff --git a/easyflow-ui-admin/app/src/components/page/CardList.vue b/easyflow-ui-admin/app/src/components/page/CardList.vue index e4f34be..cc90eaa 100644 --- a/easyflow-ui-admin/app/src/components/page/CardList.vue +++ b/easyflow-ui-admin/app/src/components/page/CardList.vue @@ -18,6 +18,8 @@ import { ElText, } from 'element-plus'; +import { $t } from '#/locales'; + export type ActionPlacement = 'inline' | 'menu'; export type ActionTone = 'danger' | 'default'; @@ -147,6 +149,23 @@ function hasVisibleActions(item: any) { resolveInlineActions(item).length > 0 || resolveMenuActions(item).length > 0 ); } + +function resolveMetaItems(item: any) { + const metaItems: Array<{ label: string; value: string }> = []; + if (item.createdByName) { + metaItems.push({ + label: $t('aiResource.createdBy'), + value: String(item.createdByName), + }); + } + if (item.created) { + metaItems.push({ + label: $t('aiResource.created'), + value: String(item.created), + }); + } + return metaItems; +}