feat: 展示 AI 资源创建人信息

- 为 Bot、工作流、知识库、插件列表补充创建人名称回填

- 在卡片中展示创建者与创建时间

- 补充后端与前端对应测试
This commit is contained in:
2026-04-13 14:58:14 +08:00
parent ae10383f17
commit 855e93ecbf
17 changed files with 642 additions and 3 deletions

View File

@@ -36,5 +36,17 @@
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-captcha</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.12.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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<BotService, Bot> {
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<BotService, Bot> {
applyCategoryPermission(queryWrapper);
Page<Bot> result = super.queryPage(page, queryWrapper);
aiResourceApprovalStateService.fillBotApprovalState(result.getRecords());
aiResourceCreatorNameSupport.fillBotCreatorNames(result.getRecords());
return result;
}

View File

@@ -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<DocumentCol
private KnowledgePublishAppService knowledgePublishAppService;
@Resource
private AiResourceApprovalStateService aiResourceApprovalStateService;
@Resource
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
public DocumentCollectionController(DocumentCollectionService service, DocumentChunkService chunkService, ModelService llmService) {
super(service);
@@ -310,6 +313,7 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
applyPublishedOnlyFilter(queryWrapper);
Page<DocumentCollection> result = super.queryPage(page, queryWrapper);
aiResourceApprovalStateService.fillKnowledgeApprovalState(result.getRecords());
aiResourceCreatorNameSupport.fillDocumentCollectionCreatorNames(result.getRecords());
return result;
}

View File

@@ -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<PluginService, Plugin>
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<PluginService, Plugin>
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
return Result.ok(queryPage(new Page<>(pageNumber, pageSize), queryWrapper));
} else {
return pluginService.pageByCategory(pageNumber, pageSize, category);
Result<Page<Plugin>> 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<PluginService, Plugin>
List<Plugin> totalList = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
boolean availableOnly = isAvailableOnly();
List<Plugin> 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()) {

View File

@@ -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<WorkflowService, Work
private WorkflowPublishAppService workflowPublishAppService;
@Resource
private AiResourceApprovalStateService aiResourceApprovalStateService;
@Resource
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
public WorkflowController(WorkflowService service, ModelService modelService) {
super(service);
@@ -434,6 +437,7 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
applyPublishedOnlyFilter(queryWrapper);
Page<Workflow> result = super.queryPage(page, queryWrapper);
aiResourceApprovalStateService.fillWorkflowApprovalState(result.getRecords());
aiResourceCreatorNameSupport.fillWorkflowCreatorNames(result.getRecords());
return result;
}

View File

@@ -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 资源批量补充创建人展示名称。
*
* <p>该组件只做展示字段填充,不参与权限或查询逻辑。</p>
*
* @author Codex
* @since 2026-04-12
*/
@Component
public class AiResourceCreatorNameSupport {
@Resource
private SysAccountService sysAccountService;
/**
* 批量填充工作流创建人名称。
*
* @param workflows 工作流集合
*/
public void fillWorkflowCreatorNames(Collection<Workflow> workflows) {
fillCreatorNames(workflows, Workflow::getCreatedBy, Workflow::setCreatedByName);
}
/**
* 批量填充聊天助手创建人名称。
*
* @param bots 聊天助手集合
*/
public void fillBotCreatorNames(Collection<Bot> bots) {
fillCreatorNames(bots, Bot::getCreatedBy, Bot::setCreatedByName);
}
/**
* 批量填充知识库创建人名称。
*
* @param collections 知识库集合
*/
public void fillDocumentCollectionCreatorNames(Collection<DocumentCollection> collections) {
fillCreatorNames(collections, DocumentCollection::getCreatedBy, DocumentCollection::setCreatedByName);
}
/**
* 批量填充插件创建人名称。
*
* @param plugins 插件集合
*/
public void fillPluginCreatorNames(Collection<Plugin> plugins) {
fillCreatorNames(plugins, Plugin::getCreatedBy, Plugin::setCreatedByName);
}
/**
* 通用的创建人名称填充逻辑。
*
* @param resources 资源集合
* @param createdByGetter 创建人 ID 提取函数
* @param createdByNameSetter 创建人名称回填函数
* @param <T> 资源类型
*/
private <T> void fillCreatorNames(
Collection<T> resources,
Function<T, Number> createdByGetter,
BiConsumer<T, String> createdByNameSetter
) {
if (resources == null || resources.isEmpty()) {
return;
}
LinkedHashSet<BigInteger> creatorIds = resources.stream()
.map(createdByGetter)
.map(this::toBigInteger)
.filter(Objects::nonNull)
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
if (creatorIds.isEmpty()) {
return;
}
Map<BigInteger, String> 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());
}
}

View File

@@ -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<Bot> 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<Bot> 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<Bot> invokeQueryPage(Page<Bot> page, QueryWrapper queryWrapper) {
return super.queryPage(page, queryWrapper);
}
}
}