diff --git a/yudao-module-ai/pom.xml b/yudao-module-ai/pom.xml index 9dba070..25947cf 100644 --- a/yudao-module-ai/pom.xml +++ b/yudao-module-ai/pom.xml @@ -19,12 +19,24 @@ 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno - 1.1.5 + 1.1.8 1.1.2.2 1.2.6 + + + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + + + cn.iocoder.boot @@ -112,37 +124,11 @@ ${spring-ai.version} - - org.springframework.ai - spring-ai-starter-model-zhipuai - ${spring-ai.version} - - - org.springframework.ai - spring-ai-starter-model-minimax - ${spring-ai.version} - - - com.alibaba.cloud.ai spring-ai-alibaba-starter-dashscope ${alibaba-ai.version} - - - - org.springaicommunity - qianfan-spring-boot-starter - 1.0.0 - - - - org.springaicommunity - moonshot-spring-boot-starter - 1.0.0 - - @@ -260,4 +246,4 @@ - \ No newline at end of file + diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java index 1450fe9..a1ef930 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java @@ -106,6 +106,20 @@ public class AiChatConversationController { conversation -> conversation.setMessageCount(messageCountMap.getOrDefault(conversation.getId(), 0)))); } + @GetMapping("/get") + @Operation(summary = "获得对话", description = "用于【对话管理】菜单") + @Parameter(name = "id", required = true, description = "对话编号", example = "1024") + @PreAuthorize("@ss.hasPermission('ai:chat-conversation:query')") + @TransMethodResult + public CommonResult getChatConversation(@RequestParam("id") Long id) { + AiChatConversationDO conversation = chatConversationService.getChatConversation(id); + AiChatConversationRespVO respVO = BeanUtils.toBean(conversation, AiChatConversationRespVO.class); + if (respVO != null) { + respVO.setMessageCount(chatMessageService.getChatMessageCount(id)); + } + return success(respVO); + } + @Operation(summary = "管理员删除对话") @DeleteMapping("/delete-by-admin") @Parameter(name = "id", required = true, description = "对话编号", example = "1024") diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java index bfd1e41..8e087d4 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java @@ -131,7 +131,7 @@ public class AiChatMessageController { @GetMapping("/page") @Operation(summary = "获得消息分页", description = "用于【对话管理】菜单") - @PreAuthorize("@ss.hasPermission('ai:chat-conversation:query')") + @PreAuthorize("@ss.hasPermission('ai:chat-message:query')") public CommonResult> getChatMessagePage(AiChatMessagePageReqVO pageReqVO) { PageResult pageResult = chatMessageService.getChatMessagePage(pageReqVO); if (CollUtil.isEmpty(pageResult.getList())) { @@ -145,6 +145,20 @@ public class AiChatMessageController { role -> respVO.setRoleName(role.getName())))); } + @GetMapping("/get") + @Operation(summary = "获得消息", description = "用于【对话管理】菜单") + @Parameter(name = "id", required = true, description = "消息编号", example = "1024") + @PreAuthorize("@ss.hasPermission('ai:chat-message:query')") + public CommonResult getChatMessage(@RequestParam("id") Long id) { + AiChatMessageDO message = chatMessageService.getChatMessage(id); + AiChatMessageRespVO respVO = BeanUtils.toBean(message, AiChatMessageRespVO.class); + if (respVO != null && respVO.getRoleId() != null) { + AiChatRoleDO role = chatRoleService.getChatRole(respVO.getRoleId()); + respVO.setRoleName(role != null ? role.getName() : null); + } + return success(respVO); + } + @Operation(summary = "管理员删除消息") @DeleteMapping("/delete-by-admin") @Parameter(name = "id", required = true, description = "消息编号", example = "1024") diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java index 1d9503f..4065697 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java @@ -119,6 +119,15 @@ public class AiImageController { return success(BeanUtils.toBean(pageResult, AiImageRespVO.class)); } + @GetMapping("/get") + @Operation(summary = "获得绘画") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('ai:image:query')") + public CommonResult getImage(@RequestParam("id") Long id) { + AiImageDO image = imageService.getImage(id); + return success(BeanUtils.toBean(image, AiImageRespVO.class)); + } + @PutMapping("/update") @Operation(summary = "更新绘画") @PreAuthorize("@ss.hasPermission('ai:image:update')") @@ -136,4 +145,4 @@ public class AiImageController { return success(true); } -} \ No newline at end of file +} diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java index db015e5..fc0f39e 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java @@ -46,6 +46,15 @@ public class AiMindMapController { return success(true); } + @GetMapping("/get") + @Operation(summary = "获得思维导图") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('ai:mind-map:query')") + public CommonResult getMindMap(@RequestParam("id") Long id) { + AiMindMapDO mindMap = mindMapService.getMindMap(id); + return success(BeanUtils.toBean(mindMap, AiMindMapRespVO.class)); + } + @GetMapping("/page") @Operation(summary = "获得思维导图分页") @PreAuthorize("@ss.hasPermission('ai:mind-map:query')") diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java index 6c09e4b..df76026 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java @@ -78,6 +78,15 @@ public class AiMusicController { return success(BeanUtils.toBean(pageResult, AiMusicRespVO.class)); } + @GetMapping("/get") + @Operation(summary = "获得音乐") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('ai:music:query')") + public CommonResult getMusic(@RequestParam("id") Long id) { + AiMusicDO music = musicService.getMusic(id); + return success(BeanUtils.toBean(music, AiMusicRespVO.class)); + } + @DeleteMapping("/delete") @Operation(summary = "删除音乐") @Parameter(name = "id", description = "编号", required = true) diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java index 7ef208b..d50f5f9 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java @@ -46,6 +46,15 @@ public class AiWriteController { return success(true); } + @GetMapping("/get") + @Operation(summary = "获得写作") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('ai:write:query')") + public CommonResult getWrite(@RequestParam("id") Long id) { + AiWriteDO write = writeService.getWrite(id); + return success(BeanUtils.toBean(write, AiWriteRespVO.class)); + } + @GetMapping("/page") @Operation(summary = "获得写作分页") @PreAuthorize("@ss.hasPermission('ai:write:query')") diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java index 8a8a388..4658a08 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java @@ -12,6 +12,7 @@ public interface ErrorCodeConstants { // ========== API 密钥 1-040-000-000 ========== ErrorCode API_KEY_NOT_EXISTS = new ErrorCode(1_040_000_000, "API 密钥不存在"); ErrorCode API_KEY_DISABLE = new ErrorCode(1_040_000_001, "API 密钥已禁用!"); + ErrorCode API_CONFIG_PLACEHOLDER_NOT_RESOLVED = new ErrorCode(1_040_000_002, "AI 配置({})无法解析,请检查环境变量或配置项"); // ========== API 模型 1-040-001-000 ========== ErrorCode MODEL_NOT_EXISTS = new ErrorCode(1_040_001_000, "模型不存在!"); diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiPlatformEnum.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiPlatformEnum.java index bf15ec1..3c09144 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiPlatformEnum.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiPlatformEnum.java @@ -28,6 +28,7 @@ public enum AiPlatformEnum implements ArrayValuable { MINI_MAX("MiniMax", "MiniMax"), // 稀宇科技 MOONSHOT("Moonshot", "月之暗面"), // KIMI BAI_CHUAN("BaiChuan", "百川智能"), // 百川智能 + STEP_FUN("StepFun", "阶跃星辰"), // 阶跃星辰 // ========== 国外平台 ========== @@ -40,7 +41,7 @@ public enum AiPlatformEnum implements ArrayValuable { STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI MIDJOURNEY("Midjourney", "Midjourney"), // Midjourney SUNO("Suno", "Suno"), // Suno AI - GROK("Grok","Grok"), // Grok + GROK("Grok", "Grok"), // Grok ; diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java index 9009cbc..3939f6c 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java @@ -10,10 +10,15 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel import cn.iocoder.yudao.module.ai.framework.ai.core.model.grok.GrokChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax.MiniMaxChatModel; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot.MoonshotChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants; import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun.StepFunChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan.YiYanChatModel; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu.ZhiPuChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient; import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient; import cn.iocoder.yudao.module.ai.tool.method.PersonService; @@ -44,7 +49,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.List; -import java.util.Optional; /** * 芋道 AI 自动配置 @@ -113,13 +117,13 @@ public class AiAutoConfiguration { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(DouBaoChatModel.MODEL_DEFAULT); } - OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() - .openAiApi(OpenAiApi.builder() + DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() .baseUrl(DouBaoChatModel.BASE_URL) .completionsPath(DouBaoChatModel.COMPLETE_PATH) .apiKey(properties.getApiKey()) .build()) - .defaultOptions(OpenAiChatOptions.builder() + .defaultOptions(DeepSeekChatOptions.builder() .model(properties.getModel()) .temperature(properties.getTemperature()) .maxTokens(properties.getMaxTokens()) @@ -168,11 +172,8 @@ public class AiAutoConfiguration { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(HunYuanChatModel.MODEL_DEFAULT); } - // 特殊:由于混元大模型不提供 deepseek,而是通过知识引擎,所以需要区分下 URL if (StrUtil.isEmpty(properties.getBaseUrl())) { - properties.setBaseUrl( - StrUtil.startWithIgnoreCase(properties.getModel(), "deepseek") ? HunYuanChatModel.DEEP_SEEK_BASE_URL - : HunYuanChatModel.BASE_URL); + properties.setBaseUrl(HunYuanChatModel.BASE_URL); } // 创建 DeepSeekChatModel、HunYuanChatModel 对象 DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() @@ -203,25 +204,15 @@ public class AiAutoConfiguration { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(XingHuoChatModel.MODEL_DEFAULT); } - OpenAiApi.Builder builder = OpenAiApi.builder() - .baseUrl(XingHuoChatModel.BASE_URL_V1) - .apiKey(properties.getAppKey() + ":" + properties.getSecretKey()); - if ("x1".equals(properties.getModel())) { - builder.baseUrl(XingHuoChatModel.BASE_URL_V2) - .completionsPath(XingHuoChatModel.BASE_COMPLETIONS_PATH_V2); - } - OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() - .openAiApi(builder.build()) - .defaultOptions(OpenAiChatOptions.builder() + return XingHuoChatModel.builder() + .apiKey(properties.getApiKey()) + .options(DeepSeekChatOptions.builder() .model(properties.getModel()) .temperature(properties.getTemperature()) .maxTokens(properties.getMaxTokens()) .topP(properties.getTopP()) .build()) - // TODO @芋艿:星火的 function call 有 bug,会报 ToolResponseMessage must have an id 错误!!! - .toolCallingManager(getToolCallingManager()) .build(); - return new XingHuoChatModel(openAiChatModel); } @Bean @@ -235,12 +226,13 @@ public class AiAutoConfiguration { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(BaiChuanChatModel.MODEL_DEFAULT); } - OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() - .openAiApi(OpenAiApi.builder() + DeepSeekChatModel deepSeekChatModel = DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() .baseUrl(BaiChuanChatModel.BASE_URL) .apiKey(properties.getApiKey()) + .completionsPath(BaiChuanChatModel.COMPLETE_PATH) .build()) - .defaultOptions(OpenAiChatOptions.builder() + .defaultOptions(DeepSeekChatOptions.builder() .model(properties.getModel()) .temperature(properties.getTemperature()) .maxTokens(properties.getMaxTokens()) @@ -248,7 +240,123 @@ public class AiAutoConfiguration { .build()) .toolCallingManager(getToolCallingManager()) .build(); - return new BaiChuanChatModel(openAiChatModel); + return new BaiChuanChatModel(deepSeekChatModel); + } + + @Bean + @ConditionalOnProperty(value = "yudao.ai.yiyan.enable", havingValue = "true") + public YiYanChatModel yiYanChatClient(YudaoAiProperties yudaoAiProperties) { + YudaoAiProperties.YiYan properties = yudaoAiProperties.getYiyan(); + return buildYiYanChatClient(properties); + } + + public YiYanChatModel buildYiYanChatClient(YudaoAiProperties.YiYan properties) { + if (StrUtil.isEmpty(properties.getModel())) { + properties.setModel(YiYanChatModel.MODEL_DEFAULT); + } + return new YiYanChatModel(buildDeepSeekCompatibleChatModel( + StrUtil.blankToDefault(properties.getBaseUrl(), YiYanChatModel.BASE_URL), + null, properties.getApiKey(), properties.getModel(), properties.getTemperature(), + properties.getMaxTokens(), properties.getTopP())); + } + + @Bean + @ConditionalOnProperty(value = "yudao.ai.zhipu.enable", havingValue = "true") + public ZhiPuChatModel zhiPuChatClient(YudaoAiProperties yudaoAiProperties) { + YudaoAiProperties.ZhiPu properties = yudaoAiProperties.getZhipu(); + return buildZhiPuChatClient(properties); + } + + public ZhiPuChatModel buildZhiPuChatClient(YudaoAiProperties.ZhiPu properties) { + if (StrUtil.isEmpty(properties.getModel())) { + properties.setModel(ZhiPuChatModel.MODEL_DEFAULT); + } + DeepSeekChatModel deepSeekChatModel = DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() + .baseUrl(StrUtil.blankToDefault(properties.getBaseUrl(), ZhiPuChatModel.BASE_URL)) + .apiKey(properties.getApiKey()) + .build()) + .defaultOptions(DeepSeekChatOptions.builder() + .model(properties.getModel()) + .temperature(properties.getTemperature()) + .maxTokens(properties.getMaxTokens()) + .topP(properties.getTopP()) + .build()) + .build(); + return new ZhiPuChatModel(deepSeekChatModel); + } + + @Bean + @ConditionalOnProperty(value = "yudao.ai.minimax.enable", havingValue = "true") + public MiniMaxChatModel miniMaxChatClient(YudaoAiProperties yudaoAiProperties) { + YudaoAiProperties.MiniMax properties = yudaoAiProperties.getMinimax(); + return buildMiniMaxChatClient(properties); + } + + public MiniMaxChatModel buildMiniMaxChatClient(YudaoAiProperties.MiniMax properties) { + if (StrUtil.isEmpty(properties.getModel())) { + properties.setModel(MiniMaxChatModel.MODEL_DEFAULT); + } + return new MiniMaxChatModel(buildDeepSeekCompatibleChatModel( + StrUtil.blankToDefault(properties.getBaseUrl(), MiniMaxChatModel.BASE_URL), + null, properties.getApiKey(), properties.getModel(), properties.getTemperature(), + properties.getMaxTokens(), properties.getTopP())); + } + + @Bean + @ConditionalOnProperty(value = "yudao.ai.moonshot.enable", havingValue = "true") + public MoonshotChatModel moonshotChatClient(YudaoAiProperties yudaoAiProperties) { + YudaoAiProperties.Moonshot properties = yudaoAiProperties.getMoonshot(); + return buildMoonshotChatClient(properties); + } + + public MoonshotChatModel buildMoonshotChatClient(YudaoAiProperties.Moonshot properties) { + if (StrUtil.isEmpty(properties.getModel())) { + properties.setModel(MoonshotChatModel.MODEL_DEFAULT); + } + return new MoonshotChatModel(buildDeepSeekCompatibleChatModel( + StrUtil.blankToDefault(properties.getBaseUrl(), MoonshotChatModel.BASE_URL), + MoonshotChatModel.COMPLETE_PATH, properties.getApiKey(), properties.getModel(), + properties.getTemperature(), properties.getMaxTokens(), properties.getTopP())); + } + + @Bean + @ConditionalOnProperty(value = "yudao.ai.stepfun.enable", havingValue = "true") + public StepFunChatModel stepFunChatClient(YudaoAiProperties yudaoAiProperties) { + YudaoAiProperties.StepFun properties = yudaoAiProperties.getStepfun(); + return buildStepFunChatClient(properties); + } + + public StepFunChatModel buildStepFunChatClient(YudaoAiProperties.StepFun properties) { + if (StrUtil.isEmpty(properties.getModel())) { + properties.setModel(StepFunChatModel.MODEL_DEFAULT); + } + return new StepFunChatModel(buildDeepSeekCompatibleChatModel( + StrUtil.blankToDefault(properties.getBaseUrl(), StepFunChatModel.BASE_URL), + StepFunChatModel.COMPLETE_PATH, properties.getApiKey(), properties.getModel(), + properties.getTemperature(), properties.getMaxTokens(), properties.getTopP())); + } + + private static DeepSeekChatModel buildDeepSeekCompatibleChatModel(String baseUrl, String completionsPath, + String apiKey, String model, + Double temperature, Integer maxTokens, + Double topP) { + DeepSeekApi.Builder apiBuilder = DeepSeekApi.builder() + .baseUrl(baseUrl) + .apiKey(apiKey); + if (StrUtil.isNotEmpty(completionsPath)) { + apiBuilder.completionsPath(completionsPath); + } + return DeepSeekChatModel.builder() + .deepSeekApi(apiBuilder.build()) + .defaultOptions(DeepSeekChatOptions.builder() + .model(model) + .temperature(temperature) + .maxTokens(maxTokens) + .topP(topP) + .build()) + .toolCallingManager(getToolCallingManager()) + .build(); } @Bean @@ -264,14 +372,20 @@ public class AiAutoConfiguration { return new SunoApi(yudaoAiProperties.getSuno().getBaseUrl()); } - public ChatModel buildGrokChatClient(YudaoAiProperties.Grok properties) { + @Bean + @ConditionalOnProperty(value = "yudao.ai.grok.enable", havingValue = "true") + public GrokChatModel grokChatClient(YudaoAiProperties yudaoAiProperties) { + YudaoAiProperties.Grok properties = yudaoAiProperties.getGrok(); + return buildGrokChatClient(properties); + } + + public GrokChatModel buildGrokChatClient(YudaoAiProperties.Grok properties) { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(GrokChatModel.MODEL_DEFAULT); } OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() .openAiApi(OpenAiApi.builder() - .baseUrl(Optional.ofNullable(properties.getBaseUrl()) - .orElse(GrokChatModel.BASE_URL)) + .baseUrl(StrUtil.blankToDefault(properties.getBaseUrl(), GrokChatModel.BASE_URL)) .completionsPath(GrokChatModel.COMPLETE_PATH) .apiKey(properties.getApiKey()) .build()) @@ -283,7 +397,7 @@ public class AiAutoConfiguration { .build()) .toolCallingManager(getToolCallingManager()) .build(); - return new DouBaoChatModel(openAiChatModel); + return new GrokChatModel(openAiChatModel); } // ========== RAG 相关 ========== @@ -320,4 +434,4 @@ public class AiAutoConfiguration { return List.of(ToolCallbacks.from(personService)); } -} \ No newline at end of file +} diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java index 986c24c..2551104 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java @@ -44,6 +44,36 @@ public class YudaoAiProperties { private BaiChuan baichuan; /** + * 文心一言 + */ + private YiYan yiyan; + + /** + * 智谱 + */ + private ZhiPu zhipu; + + /** + * MiniMax + */ + private MiniMax minimax; + + /** + * 月之暗面 + */ + private Moonshot moonshot; + + /** + * 阶跃星辰 + */ + private StepFun stepfun; + + /** + * Grok + */ + private Grok grok; + + /** * Midjourney 绘图 */ private Midjourney midjourney; @@ -116,9 +146,7 @@ public class YudaoAiProperties { public static class XingHuo { private String enable; - private String appId; - private String appKey; - private String secretKey; + private String apiKey; private String model; private Double temperature; @@ -141,6 +169,76 @@ public class YudaoAiProperties { } @Data + public static class YiYan { + + private String enable; + private String baseUrl; + private String apiKey; + + private String model; + private Double temperature; + private Integer maxTokens; + private Double topP; + + } + + @Data + public static class ZhiPu { + + private String enable; + private String baseUrl; + private String apiKey; + + private String model; + private Double temperature; + private Integer maxTokens; + private Double topP; + + } + + @Data + public static class MiniMax { + + private String enable; + private String baseUrl; + private String apiKey; + + private String model; + private Double temperature; + private Integer maxTokens; + private Double topP; + + } + + @Data + public static class Moonshot { + + private String enable; + private String baseUrl; + private String apiKey; + + private String model; + private Double temperature; + private Integer maxTokens; + private Double topP; + + } + + @Data + public static class StepFun { + + private String enable; + private String apiKey; + private String baseUrl; + + private String model; + private Double temperature; + private Integer maxTokens; + private Double topP; + + } + + @Data public static class Midjourney { private String enable; diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactoryImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactoryImpl.java index f8067de..e5fb92c 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactoryImpl.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactoryImpl.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model; import cn.hutool.core.io.FileUtil; -import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Singleton; import cn.hutool.core.lang.func.Func0; import cn.hutool.core.util.ArrayUtil; @@ -15,14 +14,21 @@ import cn.iocoder.yudao.module.ai.framework.ai.config.YudaoAiProperties; import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.grok.GrokChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax.MiniMaxChatModel; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot.MoonshotChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants; import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageModel; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun.StepFunChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan.YiYanChatModel; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu.ZhiPuChatModel; +import cn.iocoder.yudao.module.ai.util.AiUtils; import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeChatAutoConfiguration; import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeEmbeddingAutoConfiguration; import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeImageAutoConfiguration; @@ -40,18 +46,6 @@ import io.milvus.client.MilvusServiceClient; import io.qdrant.client.QdrantClient; import io.qdrant.client.QdrantGrpcClient; import lombok.SneakyThrows; -import org.springaicommunity.moonshot.MoonshotChatModel; -import org.springaicommunity.moonshot.MoonshotChatOptions; -import org.springaicommunity.moonshot.api.MoonshotApi; -import org.springaicommunity.moonshot.autoconfigure.MoonshotChatAutoConfiguration; -import org.springaicommunity.qianfan.QianFanChatModel; -import org.springaicommunity.qianfan.QianFanEmbeddingModel; -import org.springaicommunity.qianfan.QianFanEmbeddingOptions; -import org.springaicommunity.qianfan.QianFanImageModel; -import org.springaicommunity.qianfan.api.QianFanApi; -import org.springaicommunity.qianfan.api.QianFanImageApi; -import org.springaicommunity.qianfan.autoconfigure.QianFanChatAutoConfiguration; -import org.springaicommunity.qianfan.autoconfigure.QianFanEmbeddingAutoConfiguration; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel; import org.springframework.ai.chat.model.ChatModel; @@ -63,27 +57,17 @@ import org.springframework.ai.embedding.BatchingStrategy; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; import org.springframework.ai.image.ImageModel; -import org.springframework.ai.minimax.MiniMaxChatModel; -import org.springframework.ai.minimax.MiniMaxChatOptions; -import org.springframework.ai.minimax.MiniMaxEmbeddingModel; -import org.springframework.ai.minimax.MiniMaxEmbeddingOptions; -import org.springframework.ai.minimax.api.MiniMaxApi; import org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration; import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration; import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingAutoConfiguration; import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingProperties; import org.springframework.ai.model.deepseek.autoconfigure.DeepSeekChatAutoConfiguration; -import org.springframework.ai.model.minimax.autoconfigure.MiniMaxChatAutoConfiguration; -import org.springframework.ai.model.minimax.autoconfigure.MiniMaxEmbeddingAutoConfiguration; import org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration; import org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration; import org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration; import org.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfiguration; import org.springframework.ai.model.stabilityai.autoconfigure.StabilityAiImageAutoConfiguration; import org.springframework.ai.model.tool.ToolCallingManager; -import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiChatAutoConfiguration; -import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiEmbeddingAutoConfiguration; -import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiImageAutoConfiguration; import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.ollama.OllamaEmbeddingModel; import org.springframework.ai.ollama.api.OllamaApi; @@ -114,13 +98,9 @@ import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStore import org.springframework.ai.vectorstore.redis.RedisVectorStore; import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreAutoConfiguration; import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreProperties; -import org.springframework.ai.zhipuai.*; -import org.springframework.ai.zhipuai.api.ZhiPuAiApi; -import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.data.redis.RedisProperties; -import org.springframework.web.client.RestClient; import redis.clients.jedis.JedisPooled; import java.io.File; @@ -131,7 +111,6 @@ import java.util.Timer; import java.util.TimerTask; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; -import static org.springframework.ai.retry.RetryUtils.DEFAULT_RETRY_TEMPLATE; /** * AI Model 模型工厂的实现类 @@ -141,7 +120,9 @@ import static org.springframework.ai.retry.RetryUtils.DEFAULT_RETRY_TEMPLATE; public class AiModelFactoryImpl implements AiModelFactory { @Override - public ChatModel getOrCreateChatModel(AiPlatformEnum platform, String apiKey, String url) { + public ChatModel getOrCreateChatModel(AiPlatformEnum platform, String rawApiKey, String rawUrl) { + final String apiKey = resolveSpringPlaceholders(rawApiKey); + final String url = resolveSpringPlaceholders(rawUrl); String cacheKey = buildClientCacheKey(ChatModel.class, platform, apiKey, url); return Singleton.get(cacheKey, (Func0) () -> { // noinspection EnhancedSwitchMigration @@ -164,6 +145,8 @@ public class AiModelFactoryImpl implements AiModelFactory { return buildMiniMaxChatModel(apiKey, url); case MOONSHOT: return buildMoonshotChatModel(apiKey, url); + case STEP_FUN: + return buildStepFunChatModel(apiKey, url); case XING_HUO: return buildXingHuoChatModel(apiKey); case BAI_CHUAN: @@ -179,7 +162,7 @@ public class AiModelFactoryImpl implements AiModelFactory { case OLLAMA: return buildOllamaChatModel(url); case GROK: - return buildGrokChatModel(apiKey,url); + return buildGrokChatModel(apiKey, url); default: throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); } @@ -193,7 +176,7 @@ public class AiModelFactoryImpl implements AiModelFactory { case TONG_YI: return SpringUtil.getBean(DashScopeChatModel.class); case YI_YAN: - return SpringUtil.getBean(QianFanChatModel.class); + return SpringUtil.getBean(YiYanChatModel.class); case DEEP_SEEK: return SpringUtil.getBean(DeepSeekChatModel.class); case DOU_BAO: @@ -203,11 +186,13 @@ public class AiModelFactoryImpl implements AiModelFactory { case SILICON_FLOW: return SpringUtil.getBean(SiliconFlowChatModel.class); case ZHI_PU: - return SpringUtil.getBean(ZhiPuAiChatModel.class); + return SpringUtil.getBean(ZhiPuChatModel.class); case MINI_MAX: return SpringUtil.getBean(MiniMaxChatModel.class); case MOONSHOT: return SpringUtil.getBean(MoonshotChatModel.class); + case STEP_FUN: + return SpringUtil.getBean(StepFunChatModel.class); case XING_HUO: return SpringUtil.getBean(XingHuoChatModel.class); case BAI_CHUAN: @@ -222,6 +207,8 @@ public class AiModelFactoryImpl implements AiModelFactory { return SpringUtil.getBean(GeminiChatModel.class); case OLLAMA: return SpringUtil.getBean(OllamaChatModel.class); + case GROK: + return SpringUtil.getBean(GrokChatModel.class); default: throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); } @@ -233,10 +220,6 @@ public class AiModelFactoryImpl implements AiModelFactory { switch (platform) { case TONG_YI: return SpringUtil.getBean(DashScopeImageModel.class); - case YI_YAN: - return SpringUtil.getBean(QianFanImageModel.class); - case ZHI_PU: - return SpringUtil.getBean(ZhiPuAiImageModel.class); case SILICON_FLOW: return SpringUtil.getBean(SiliconFlowImageModel.class); case OPENAI: @@ -249,19 +232,17 @@ public class AiModelFactoryImpl implements AiModelFactory { } @Override - public ImageModel getOrCreateImageModel(AiPlatformEnum platform, String apiKey, String url) { + public ImageModel getOrCreateImageModel(AiPlatformEnum platform, String rawApiKey, String rawUrl) { + String apiKey = resolveSpringPlaceholders(rawApiKey); + String url = resolveSpringPlaceholders(rawUrl); // noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: return buildTongYiImagesModel(apiKey); - case YI_YAN: - return buildQianFanImageModel(apiKey); - case ZHI_PU: - return buildZhiPuAiImageModel(apiKey, url); case OPENAI: return buildOpenAiImageModel(apiKey, url); case SILICON_FLOW: - return buildSiliconFlowImageModel(apiKey,url); + return buildSiliconFlowImageModel(apiKey, url); case STABLE_DIFFUSION: return buildStabilityAiImageModel(apiKey, url); default: @@ -270,9 +251,11 @@ public class AiModelFactoryImpl implements AiModelFactory { } @Override - public MidjourneyApi getOrCreateMidjourneyApi(String apiKey, String url) { - String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey, - url); + public MidjourneyApi getOrCreateMidjourneyApi(String rawApiKey, String rawUrl) { + final String apiKey = resolveSpringPlaceholders(rawApiKey); + final String url = resolveSpringPlaceholders(rawUrl); + String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), + apiKey, url); return Singleton.get(cacheKey, (Func0) () -> { YudaoAiProperties.Midjourney properties = SpringUtil.getBean(YudaoAiProperties.class) .getMidjourney(); @@ -281,25 +264,23 @@ public class AiModelFactoryImpl implements AiModelFactory { } @Override - public SunoApi getOrCreateSunoApi(String apiKey, String url) { + public SunoApi getOrCreateSunoApi(String rawApiKey, String rawUrl) { + final String apiKey = resolveSpringPlaceholders(rawApiKey); + final String url = resolveSpringPlaceholders(rawUrl); String cacheKey = buildClientCacheKey(SunoApi.class, AiPlatformEnum.SUNO.getPlatform(), apiKey, url); return Singleton.get(cacheKey, (Func0) () -> new SunoApi(url)); } @Override @SuppressWarnings("EnhancedSwitchMigration") - public EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url, String model) { + public EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String rawApiKey, String rawUrl, String model) { + final String apiKey = resolveSpringPlaceholders(rawApiKey); + final String url = resolveSpringPlaceholders(rawUrl); String cacheKey = buildClientCacheKey(EmbeddingModel.class, platform, apiKey, url, model); return Singleton.get(cacheKey, (Func0) () -> { switch (platform) { case TONG_YI: return buildTongYiEmbeddingModel(apiKey, model); - case YI_YAN: - return buildYiYanEmbeddingModel(apiKey, model); - case ZHI_PU: - return buildZhiPuEmbeddingModel(apiKey, url, model); - case MINI_MAX: - return buildMiniMaxEmbeddingModel(apiKey, url, model); case OPENAI: return buildOpenAiEmbeddingModel(apiKey, url, model); case AZURE_OPENAI: @@ -341,6 +322,11 @@ public class AiModelFactoryImpl implements AiModelFactory { return StrUtil.format("{}#{}", clazz.getName(), ArrayUtil.join(params, "_")); } + private static String resolveSpringPlaceholders(String value) { + // yml 配置的占位符由 Spring 自动解析;DB 里保存的 ${xxx} 需要在这里手动解析。 + return AiUtils.resolveSpringPlaceholders(value); + } + // ========== 各种创建 spring-ai 客户端的方法 ========== /** @@ -367,30 +353,10 @@ public class AiModelFactoryImpl implements AiModelFactory { .build(); } - /** - * 可参考 {@link QianFanChatAutoConfiguration} 的 qianFanChatModel 方法 - */ - private static QianFanChatModel buildYiYanChatModel(String key) { - // TODO spring ai qianfan 有 bug,无法使用 https://github.com/spring-ai-community/qianfan/issues/6 - List keys = StrUtil.split(key, '|'); - Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); - String appKey = keys.get(0); - String secretKey = keys.get(1); - QianFanApi qianFanApi = new QianFanApi(appKey, secretKey); - return new QianFanChatModel(qianFanApi); - } - - /** - * 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanImageModel 方法 - */ - private QianFanImageModel buildQianFanImageModel(String key) { - // TODO spring ai qianfan 有 bug,无法使用 https://github.com/spring-ai-community/qianfan/issues/6 - List keys = StrUtil.split(key, '|'); - Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); - String appKey = keys.get(0); - String secretKey = keys.get(1); - QianFanImageApi qianFanApi = new QianFanImageApi(appKey, secretKey); - return new QianFanImageModel(qianFanApi); + private ChatModel buildYiYanChatModel(String apiKey) { + YudaoAiProperties.YiYan properties = new YudaoAiProperties.YiYan() + .setApiKey(apiKey); + return new AiAutoConfiguration().buildYiYanChatClient(properties); } /** @@ -435,62 +401,47 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link ZhiPuAiChatAutoConfiguration} 的 zhiPuAiChatModel 方法 + * 可参考 {@link AiAutoConfiguration#zhiPuChatClient(YudaoAiProperties)} */ - private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) { - ZhiPuAiApi.Builder zhiPuAiApiBuilder = ZhiPuAiApi.builder().apiKey(apiKey); - if (StrUtil.isNotEmpty(url)) { - zhiPuAiApiBuilder.baseUrl(url); - } - ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.DEFAULT_CHAT_MODEL).temperature(0.7).build(); - return new ZhiPuAiChatModel(zhiPuAiApiBuilder.build(), options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE, - getObservationRegistry().getIfAvailable()); + private ZhiPuChatModel buildZhiPuChatModel(String apiKey, String url) { + YudaoAiProperties.ZhiPu properties = new YudaoAiProperties.ZhiPu() + .setBaseUrl(url).setApiKey(apiKey); + return new AiAutoConfiguration().buildZhiPuChatClient(properties); } /** - * 可参考 {@link ZhiPuAiImageAutoConfiguration} 的 zhiPuAiImageModel 方法 + * 可参考 {@link AiAutoConfiguration#miniMaxChatClient(YudaoAiProperties)} */ - private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) { - ZhiPuAiImageApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiImageApi(apiKey) - : new ZhiPuAiImageApi(url, apiKey, RestClient.builder()); - return new ZhiPuAiImageModel(zhiPuAiApi); + private MiniMaxChatModel buildMiniMaxChatModel(String apiKey, String url) { + YudaoAiProperties.MiniMax properties = new YudaoAiProperties.MiniMax() + .setBaseUrl(url).setApiKey(apiKey); + return new AiAutoConfiguration().buildMiniMaxChatClient(properties); } /** - * 可参考 {@link MiniMaxChatAutoConfiguration} 的 miniMaxChatModel 方法 + * 可参考 {@link AiAutoConfiguration#moonshotChatClient(YudaoAiProperties)} */ - private MiniMaxChatModel buildMiniMaxChatModel(String apiKey, String url) { - MiniMaxApi miniMaxApi = StrUtil.isEmpty(url) ? new MiniMaxApi(apiKey) - : new MiniMaxApi(url, apiKey); - MiniMaxChatOptions options = MiniMaxChatOptions.builder().model(MiniMaxApi.DEFAULT_CHAT_MODEL).temperature(0.7).build(); - return new MiniMaxChatModel(miniMaxApi, options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE); + private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) { + YudaoAiProperties.Moonshot properties = new YudaoAiProperties.Moonshot() + .setBaseUrl(url).setApiKey(apiKey); + return new AiAutoConfiguration().buildMoonshotChatClient(properties); } /** - * 可参考 {@link MoonshotChatAutoConfiguration} 的 moonshotChatModel 方法 + * 可参考 {@link AiAutoConfiguration#stepFunChatClient(YudaoAiProperties)} */ - private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) { - MoonshotApi.Builder moonshotApiBuilder = MoonshotApi.builder() - .apiKey(apiKey); - if (StrUtil.isNotEmpty(url)) { - moonshotApiBuilder.baseUrl(url); - } - MoonshotChatOptions options = MoonshotChatOptions.builder().model(MoonshotApi.DEFAULT_CHAT_MODEL).build(); - return MoonshotChatModel.builder() - .moonshotApi(moonshotApiBuilder.build()) - .defaultOptions(options) - .toolCallingManager(getToolCallingManager()) - .build(); + private StepFunChatModel buildStepFunChatModel(String apiKey, String url) { + YudaoAiProperties.StepFun properties = new YudaoAiProperties.StepFun() + .setBaseUrl(url).setApiKey(apiKey); + return new AiAutoConfiguration().buildStepFunChatClient(properties); } /** * 可参考 {@link AiAutoConfiguration#xingHuoChatClient(YudaoAiProperties)} */ - private static XingHuoChatModel buildXingHuoChatModel(String key) { - List keys = StrUtil.split(key, '|'); - Assert.equals(keys.size(), 2, "XingHuoChatClient 的密钥需要 (appKey|secretKey) 格式"); + private static XingHuoChatModel buildXingHuoChatModel(String apiKey) { YudaoAiProperties.XingHuo properties = new YudaoAiProperties.XingHuo() - .setAppKey(keys.get(0)).setSecretKey(keys.get(1)); + .setApiKey(apiKey).setModel(XingHuoChatModel.MODEL_DEFAULT); return new AiAutoConfiguration().buildXingHuoChatClient(properties); } @@ -590,7 +541,7 @@ public class AiModelFactoryImpl implements AiModelFactory { return new StabilityAiImageModel(stabilityAiApi); } - private ChatModel buildGrokChatModel(String apiKey,String url) { + private GrokChatModel buildGrokChatModel(String apiKey, String url) { YudaoAiProperties.Grok properties = new YudaoAiProperties.Grok() .setBaseUrl(url) .setApiKey(apiKey); @@ -608,41 +559,6 @@ public class AiModelFactoryImpl implements AiModelFactory { return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, dashScopeEmbeddingOptions); } - /** - * 可参考 {@link ZhiPuAiEmbeddingAutoConfiguration} 的 zhiPuAiEmbeddingModel 方法 - */ - private ZhiPuAiEmbeddingModel buildZhiPuEmbeddingModel(String apiKey, String url, String model) { - ZhiPuAiApi.Builder zhiPuAiApiBuilder = ZhiPuAiApi.builder().apiKey(apiKey); - if (StrUtil.isNotEmpty(url)) { - zhiPuAiApiBuilder.baseUrl(url); - } - ZhiPuAiEmbeddingOptions zhiPuAiEmbeddingOptions = ZhiPuAiEmbeddingOptions.builder().model(model).build(); - return new ZhiPuAiEmbeddingModel(zhiPuAiApiBuilder.build(), MetadataMode.EMBED, zhiPuAiEmbeddingOptions); - } - - /** - * 可参考 {@link MiniMaxEmbeddingAutoConfiguration} 的 miniMaxEmbeddingModel 方法 - */ - private EmbeddingModel buildMiniMaxEmbeddingModel(String apiKey, String url, String model) { - MiniMaxApi miniMaxApi = StrUtil.isEmpty(url)? new MiniMaxApi(apiKey) - : new MiniMaxApi(url, apiKey); - MiniMaxEmbeddingOptions miniMaxEmbeddingOptions = MiniMaxEmbeddingOptions.builder().model(model).build(); - return new MiniMaxEmbeddingModel(miniMaxApi, MetadataMode.EMBED, miniMaxEmbeddingOptions); - } - - /** - * 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanEmbeddingModel 方法 - */ - private QianFanEmbeddingModel buildYiYanEmbeddingModel(String key, String model) { - List keys = StrUtil.split(key, '|'); - Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); - String appKey = keys.get(0); - String secretKey = keys.get(1); - QianFanApi qianFanApi = new QianFanApi(appKey, secretKey); - QianFanEmbeddingOptions qianFanEmbeddingOptions = QianFanEmbeddingOptions.builder().model(model).build(); - return new QianFanEmbeddingModel(qianFanApi, MetadataMode.EMBED, qianFanEmbeddingOptions); - } - private OllamaEmbeddingModel buildOllamaEmbeddingModel(String url, String model) { OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build(); OllamaEmbeddingOptions ollamaOptions = OllamaEmbeddingOptions.builder().model(model).build(); diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/baichuan/BaiChuanChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/baichuan/BaiChuanChatModel.java index 5fb71c9..7974194 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/baichuan/BaiChuanChatModel.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/baichuan/BaiChuanChatModel.java @@ -6,7 +6,7 @@ import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.deepseek.DeepSeekChatModel; import reactor.core.publisher.Flux; /** @@ -20,26 +20,28 @@ public class BaiChuanChatModel implements ChatModel { public static final String BASE_URL = "https://api.baichuan-ai.com"; - public static final String MODEL_DEFAULT = "Baichuan4-Turbo"; + public static final String COMPLETE_PATH = "/v1/chat/completions"; + + public static final String MODEL_DEFAULT = "Baichuan-M3"; /** - * 兼容 OpenAI 接口,进行复用 + * 兼容 OpenAI 接口,复用 DeepSeek 客户端 */ - private final OpenAiChatModel openAiChatModel; + private final DeepSeekChatModel deepSeekChatModel; @Override public ChatResponse call(Prompt prompt) { - return openAiChatModel.call(prompt); + return deepSeekChatModel.call(prompt); } @Override public Flux stream(Prompt prompt) { - return openAiChatModel.stream(prompt); + return deepSeekChatModel.stream(prompt); } @Override public ChatOptions getDefaultOptions() { - return openAiChatModel.getDefaultOptions(); + return deepSeekChatModel.getDefaultOptions(); } } diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/doubao/DouBaoChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/doubao/DouBaoChatModel.java index a542cb3..7b6e10c 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/doubao/DouBaoChatModel.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/doubao/DouBaoChatModel.java @@ -20,7 +20,7 @@ public class DouBaoChatModel implements ChatModel { public static final String BASE_URL = "https://ark.cn-beijing.volces.com/api"; public static final String COMPLETE_PATH = "/v3/chat/completions"; - public static final String MODEL_DEFAULT = "doubao-1-5-lite-32k-250115"; + public static final String MODEL_DEFAULT = "doubao-seed-2-1-turbo-260628"; /** * 兼容 OpenAI 接口,进行复用 diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/grok/GrokChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/grok/GrokChatModel.java index 06eed25..86c503e 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/grok/GrokChatModel.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/grok/GrokChatModel.java @@ -19,7 +19,7 @@ public class GrokChatModel implements ChatModel { public static final String BASE_URL = "https://api.x.ai"; public static final String COMPLETE_PATH = "/v1/chat/completions"; - public static final String MODEL_DEFAULT = "grok-4-fast-reasoning"; + public static final String MODEL_DEFAULT = "grok-4.3"; /** * 兼容 OpenAI 接口,进行复用 diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/hunyuan/HunYuanChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/hunyuan/HunYuanChatModel.java index 9513c6c..6711ef2 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/hunyuan/HunYuanChatModel.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/hunyuan/HunYuanChatModel.java @@ -9,10 +9,9 @@ import org.springframework.ai.chat.prompt.Prompt; import reactor.core.publisher.Flux; /** - * 腾云混元 {@link ChatModel} 实现类 + * 腾讯混元 {@link ChatModel} 实现类 * - * 1. 混元大模型:基于 知识引擎原子能力 实现 - * 2. 知识引擎原子能力:基于 知识引擎原子能力 实现 + * 基于 TokenHub OpenAI 兼容接口 实现 * * @author fansili */ @@ -20,14 +19,10 @@ import reactor.core.publisher.Flux; @RequiredArgsConstructor public class HunYuanChatModel implements ChatModel { - public static final String BASE_URL = "https://api.hunyuan.cloud.tencent.com"; + public static final String BASE_URL = "https://tokenhub.tencentmaas.com"; public static final String COMPLETE_PATH = "/v1/chat/completions"; - public static final String MODEL_DEFAULT = "hunyuan-turbo"; - - public static final String DEEP_SEEK_BASE_URL = "https://api.lkeap.cloud.tencent.com"; - - public static final String DEEP_SEEK_MODEL_DEFAULT = "deepseek-v3"; + public static final String MODEL_DEFAULT = "hy3-preview"; /** * 兼容 OpenAI 接口,进行复用 diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/minimax/MiniMaxChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/minimax/MiniMaxChatModel.java new file mode 100644 index 0000000..d421aab --- /dev/null +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/minimax/MiniMaxChatModel.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import reactor.core.publisher.Flux; + +/** + * MiniMax {@link ChatModel} 实现类 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class MiniMaxChatModel implements ChatModel { + + public static final String BASE_URL = "https://api.minimaxi.com/v1"; + + public static final String MODEL_DEFAULT = "MiniMax-M3"; + + /** + * 兼容 OpenAI 接口,复用 DeepSeek 客户端 + */ + private final DeepSeekChatModel deepSeekChatModel; + + @Override + public ChatResponse call(Prompt prompt) { + return deepSeekChatModel.call(prompt); + } + + @Override + public Flux stream(Prompt prompt) { + return deepSeekChatModel.stream(prompt); + } + + @Override + public ChatOptions getDefaultOptions() { + return deepSeekChatModel.getDefaultOptions(); + } + +} diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/moonshot/MoonshotChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/moonshot/MoonshotChatModel.java new file mode 100644 index 0000000..27287e3 --- /dev/null +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/moonshot/MoonshotChatModel.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import reactor.core.publisher.Flux; + +/** + * 月之暗面 {@link ChatModel} 实现类 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class MoonshotChatModel implements ChatModel { + + public static final String BASE_URL = "https://api.moonshot.cn"; + + public static final String COMPLETE_PATH = "/v1/chat/completions"; + + public static final String MODEL_DEFAULT = "kimi-k2.6"; + + /** + * 兼容 OpenAI 接口,复用 DeepSeek 客户端 + */ + private final DeepSeekChatModel deepSeekChatModel; + + @Override + public ChatResponse call(Prompt prompt) { + return deepSeekChatModel.call(prompt); + } + + @Override + public Flux stream(Prompt prompt) { + return deepSeekChatModel.stream(prompt); + } + + @Override + public ChatOptions getDefaultOptions() { + return deepSeekChatModel.getDefaultOptions(); + } + +} diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/stepfun/StepFunChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/stepfun/StepFunChatModel.java new file mode 100644 index 0000000..7d61141 --- /dev/null +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/stepfun/StepFunChatModel.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import reactor.core.publisher.Flux; + +/** + * 阶跃星辰 {@link ChatModel} 实现类 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class StepFunChatModel implements ChatModel { + + public static final String BASE_URL = "https://api.stepfun.com"; + + public static final String COMPLETE_PATH = "/v1/chat/completions"; + + public static final String MODEL_DEFAULT = "step-3.7-flash"; + + /** + * 兼容 OpenAI 接口,复用 DeepSeek 客户端 + */ + private final DeepSeekChatModel deepSeekChatModel; + + @Override + public ChatResponse call(Prompt prompt) { + return deepSeekChatModel.call(prompt); + } + + @Override + public Flux stream(Prompt prompt) { + return deepSeekChatModel.stream(prompt); + } + + @Override + public ChatOptions getDefaultOptions() { + return deepSeekChatModel.getDefaultOptions(); + } + +} diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/xinghuo/XingHuoChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/xinghuo/XingHuoChatModel.java index cbac3b6..e7e4368 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/xinghuo/XingHuoChatModel.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/xinghuo/XingHuoChatModel.java @@ -1,11 +1,14 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo; -import lombok.RequiredArgsConstructor; +import cn.hutool.core.util.StrUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import org.springframework.ai.deepseek.DeepSeekChatOptions; +import org.springframework.ai.deepseek.api.DeepSeekApi; import reactor.core.publisher.Flux; /** @@ -14,37 +17,140 @@ import reactor.core.publisher.Flux; * @author fansili */ @Slf4j -@RequiredArgsConstructor public class XingHuoChatModel implements ChatModel { - public static final String BASE_URL_V1 = "https://spark-api-open.xf-yun.com"; - - public static final String BASE_URL_V2 = "https://spark-api-open.xf-yun.com"; - public static final String BASE_COMPLETIONS_PATH_V2 = "/v2/chat/completions"; + /** + * 星火 X2 + * + * @see 接口地址 + */ + public static final String MODEL_X2 = "x2"; /** - * 已知模型名列表:x1、4.0Ultra、generalv3.5、max-32k、generalv3、pro-128k、lite + * 星火 X2 Flash + * + * @see 接口地址 */ - public static final String MODEL_DEFAULT = "4.0Ultra"; + public static final String MODEL_X2_FLASH = "x2-flash"; + + private static final String BASE_URL_X2 = "https://spark-api-open.xf-yun.com/x2"; + + private static final String BASE_URL_X2_FLASH = "https://spark-api-open.xf-yun.com/agent/v1"; + + public static final String MODEL_DEFAULT = MODEL_X2_FLASH; + + private static String getBaseUrl(String model) { + if (MODEL_X2_FLASH.equals(model)) { + return BASE_URL_X2_FLASH; + } + return BASE_URL_X2; + } + + private final String apiKey; + + private final DeepSeekChatOptions options; /** - * v1 兼容 OpenAI 接口,进行复用 + * 兼容 OpenAI 接口,进行复用 */ - private final ChatModel openAiChatModelV1; + private final ChatModel chatModelX2; + + private final ChatModel chatModelX2Flash; + + private XingHuoChatModel(String apiKey, DeepSeekChatOptions options) { + this.apiKey = apiKey; + this.options = options; + this.chatModelX2 = buildChatModel(MODEL_X2); + this.chatModelX2Flash = buildChatModel(MODEL_X2_FLASH); + } + + public static Builder builder() { + return new Builder(); + } @Override public ChatResponse call(Prompt prompt) { - return openAiChatModelV1.call(prompt); + return getChatModel(prompt).call(buildApiPrompt(prompt)); } @Override public Flux stream(Prompt prompt) { - return openAiChatModelV1.stream(prompt); + return getChatModel(prompt).stream(buildApiPrompt(prompt)); } @Override public ChatOptions getDefaultOptions() { - return openAiChatModelV1.getDefaultOptions(); + return options; + } + + private ChatModel getChatModel(Prompt prompt) { + String model = options.getModel(); + ChatOptions options = prompt.getOptions(); + if (options != null && isBusinessModel(options.getModel())) { + model = options.getModel(); + } + return getChatModel(model); + } + + private ChatModel getChatModel(String model) { + if (MODEL_X2_FLASH.equals(model)) { + return chatModelX2Flash; + } + return chatModelX2; + } + + private ChatModel buildChatModel(String model) { + return DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() + .baseUrl(getBaseUrl(model)) + .apiKey(apiKey) + .build()) + .defaultOptions(new DeepSeekChatOptions.Builder(options).model("spark-x").build()) + .build(); + } + + private static Prompt buildApiPrompt(Prompt prompt) { + ChatOptions options = prompt.getOptions(); + if (options == null) { + return prompt; + } + if (!(options instanceof DeepSeekChatOptions)) { + return prompt; + } + return Prompt.builder() + .messages(prompt.getInstructions()) + .chatOptions(new DeepSeekChatOptions.Builder((DeepSeekChatOptions) options).model("spark-x").build()) + .build(); + } + + private static boolean isBusinessModel(String model) { + return MODEL_X2.equals(model) || MODEL_X2_FLASH.equals(model); + } + + public static final class Builder { + + private String apiKey; + + private DeepSeekChatOptions options; + + public Builder apiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + + public Builder options(DeepSeekChatOptions options) { + this.options = options; + return this; + } + + public XingHuoChatModel build() { + DeepSeekChatOptions options = this.options != null ? this.options : DeepSeekChatOptions.builder().build(); + if (StrUtil.isEmpty(options.getModel())) { + options = new DeepSeekChatOptions.Builder(options).model(MODEL_DEFAULT).build(); + } + return new XingHuoChatModel(apiKey, options); + } + } } diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/yiyan/YiYanChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/yiyan/YiYanChatModel.java new file mode 100644 index 0000000..876d6bd --- /dev/null +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/yiyan/YiYanChatModel.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import reactor.core.publisher.Flux; + +/** + * 文心一言 {@link ChatModel} 实现类 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class YiYanChatModel implements ChatModel { + + public static final String BASE_URL = "https://qianfan.baidubce.com/v2"; + + public static final String MODEL_DEFAULT = "ernie-5.1"; + + /** + * 兼容 OpenAI 接口,复用 DeepSeek 客户端 + */ + private final DeepSeekChatModel deepSeekChatModel; + + @Override + public ChatResponse call(Prompt prompt) { + return deepSeekChatModel.call(prompt); + } + + @Override + public Flux stream(Prompt prompt) { + return deepSeekChatModel.stream(prompt); + } + + @Override + public ChatOptions getDefaultOptions() { + return deepSeekChatModel.getDefaultOptions(); + } + +} diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/zhipu/ZhiPuChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/zhipu/ZhiPuChatModel.java new file mode 100644 index 0000000..858f22c --- /dev/null +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/zhipu/ZhiPuChatModel.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import reactor.core.publisher.Flux; + +/** + * 智谱 {@link ChatModel} 实现类 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class ZhiPuChatModel implements ChatModel { + + public static final String BASE_URL = "https://open.bigmodel.cn/api/paas/v4"; + + public static final String MODEL_DEFAULT = "glm-5.2"; + + /** + * 兼容 OpenAI 接口,复用 DeepSeek 客户端 + */ + private final DeepSeekChatModel deepSeekChatModel; + + @Override + public ChatResponse call(Prompt prompt) { + return deepSeekChatModel.call(prompt); + } + + @Override + public Flux stream(Prompt prompt) { + return deepSeekChatModel.stream(prompt); + } + + @Override + public ChatOptions getDefaultOptions() { + return deepSeekChatModel.getDefaultOptions(); + } + +} diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageService.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageService.java index f572bdd..e8c5bc1 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageService.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageService.java @@ -46,6 +46,14 @@ public interface AiChatMessageService { List getChatMessageListByConversationId(Long conversationId); /** + * 获得聊天消息 + * + * @param id 消息编号 + * @return 聊天消息 + */ + AiChatMessageDO getChatMessage(Long id); + + /** * 删除消息 * * @param id 消息编号 @@ -69,6 +77,14 @@ public interface AiChatMessageService { void deleteChatMessageByAdmin(Long id); /** + * 获得聊天对话的消息数量 + * + * @param conversationId 对话编号 + * @return 消息数量 + */ + Integer getChatMessageCount(Long conversationId); + + /** * 获得聊天对话的消息数量 Map * * @param conversationIds 对话编号数组 diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java index bdd2ba2..6a7f54a 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java @@ -525,6 +525,11 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { } @Override + public AiChatMessageDO getChatMessage(Long id) { + return chatMessageMapper.selectById(id); + } + + @Override public void deleteChatMessage(Long id, Long userId) { // 1. 校验消息存在 AiChatMessageDO message = chatMessageMapper.selectById(id); @@ -558,6 +563,11 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { } @Override + public Integer getChatMessageCount(Long conversationId) { + return chatMessageMapper.selectCount(AiChatMessageDO::getConversationId, conversationId).intValue(); + } + + @Override public Map getChatMessageCountMap(Collection conversationIds) { return chatMessageMapper.selectCountMapByConversationId(conversationIds); } diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java index 3def2a9..0163edb 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java @@ -29,14 +29,12 @@ import cn.iocoder.yudao.module.infra.api.file.FileApi; import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; -import org.springaicommunity.qianfan.QianFanImageOptions; import org.springframework.ai.image.ImageModel; import org.springframework.ai.image.ImageOptions; import org.springframework.ai.image.ImagePrompt; import org.springframework.ai.image.ImageResponse; import org.springframework.ai.openai.OpenAiImageOptions; import org.springframework.ai.stabilityai.api.StabilityAiImageOptions; -import org.springframework.ai.zhipuai.ZhiPuAiImageOptions; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -168,15 +166,6 @@ public class AiImageServiceImpl implements AiImageService { .model(model.getModel()).n(1) .height(draw.getHeight()).width(draw.getWidth()) .build(); - } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.YI_YAN.getPlatform())) { - return QianFanImageOptions.builder() - .model(model.getModel()).N(1) - .height(draw.getHeight()).width(draw.getWidth()) - .build(); - } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.ZHI_PU.getPlatform())) { - return ZhiPuAiImageOptions.builder() - .model(model.getModel()) - .build(); } throw new IllegalArgumentException("不支持的 AI 平台:" + model.getPlatform()); } diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapService.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapService.java index 65a5aaf..15fa9f2 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapService.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapService.java @@ -31,6 +31,14 @@ public interface AiMindMapService { void deleteMindMap(Long id); /** + * 获得思维导图 + * + * @param id 编号 + * @return 思维导图 + */ + AiMindMapDO getMindMap(Long id); + + /** * 获得思维导图分页 * * @param pageReqVO 分页查询 diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java index f307dd7..a7e9f21 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java @@ -154,6 +154,11 @@ public class AiMindMapServiceImpl implements AiMindMapService { } @Override + public AiMindMapDO getMindMap(Long id) { + return mindMapMapper.selectById(id); + } + + @Override public PageResult getMindMapPage(AiMindMapPageReqVO pageReqVO) { return mindMapMapper.selectPage(pageReqVO); } diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteService.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteService.java index f2dd489..f6a3f0a 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteService.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteService.java @@ -31,6 +31,14 @@ public interface AiWriteService { void deleteWrite(Long id); /** + * 获得写作 + * + * @param id 编号 + * @return AI 写作 + */ + AiWriteDO getWrite(Long id); + + /** * 获得写作分页 * * @param pageReqVO 分页查询 diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java index 179188d..48b60b1 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java @@ -171,6 +171,11 @@ public class AiWriteServiceImpl implements AiWriteService { } @Override + public AiWriteDO getWrite(Long id) { + return writeMapper.selectById(id); + } + + @Override public PageResult getWritePage(AiWritePageReqVO pageReqVO) { return writeMapper.selectPage(pageReqVO); } diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java index 1518523..95fe446 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java @@ -8,8 +8,7 @@ import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; -import org.springaicommunity.moonshot.MoonshotChatOptions; -import org.springaicommunity.qianfan.QianFanChatOptions; +import cn.hutool.extra.spring.SpringUtil; import org.springframework.ai.anthropic.AnthropicChatOptions; import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; import org.springframework.ai.chat.messages.*; @@ -17,14 +16,16 @@ import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.deepseek.DeepSeekAssistantMessage; import org.springframework.ai.deepseek.DeepSeekChatOptions; -import org.springframework.ai.minimax.MiniMaxChatOptions; import org.springframework.ai.ollama.api.OllamaChatOptions; import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.tool.ToolCallback; -import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; +import org.springframework.core.env.Environment; import java.util.*; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.API_CONFIG_PLACEHOLDER_NOT_RESOLVED; + /** * Spring AI 工具类 * @@ -36,13 +37,42 @@ public class AiUtils { public static final String TOOL_CONTEXT_TENANT_ID = "TENANT_ID"; /** + * 解析 DB 等动态配置里的 Spring 占位符,例如 ${OPENAI_API_KEY} + * + * @param value 待解析的配置值 + * @return 解析后的配置值 + */ + public static String resolveSpringPlaceholders(String value) { + if (StrUtil.isBlank(value) || !StrUtil.contains(value, "${")) { + return value; + } + try { + return SpringUtil.getBean(Environment.class).resolveRequiredPlaceholders(value); + } catch (IllegalArgumentException ex) { + throw exception(API_CONFIG_PLACEHOLDER_NOT_RESOLVED, value); + } + } + + /** + * 校验 API Key,避免集成测试使用默认占位值发起调用。 + * + * @param apiKey API Key + */ + public static void validateApiKey(String apiKey) { + if (StrUtil.isBlank(apiKey) || "sk-xxxx".equals(apiKey)) { + throw new IllegalStateException("apiKey 不能为空"); + } + } + + /** * 通义千问支持多模态的模型 * * @see 模型广场 * @see 必须开启 withMultiModel 参数 */ public static final Set TONG_YI_MULTI_MODELS = SetUtils.asSet( - // qwen3.5 / 3.6 系列(统一多模态主干) + // qwen3.5 / 3.6 / 3.7 系列(统一多模态主干) + "qwen3.7-max", "qwen3.7-plus", "qwen3.7-flash", "qwen3.6-plus", "qwen3.6-flash", "qwen3.5-plus", "qwen3.5-flash", // qwen-vl 视觉理解 @@ -72,27 +102,21 @@ public class AiUtils { .enableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置 .multiModel(TONG_YI_MULTI_MODELS.contains(model)) // 是否多模态模型 .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); - case YI_YAN: - return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case DEEP_SEEK: case DOU_BAO: // 复用 DeepSeek 客户端 case HUN_YUAN: // 复用 DeepSeek 客户端 case SILICON_FLOW: // 复用 DeepSeek 客户端 + case YI_YAN: // 复用 DeepSeek 客户端 + case ZHI_PU: // 复用 DeepSeek 客户端 case XING_HUO: // 复用 DeepSeek 客户端 + case MINI_MAX: // 复用 DeepSeek 客户端 + case MOONSHOT: // 复用 DeepSeek 客户端 + case BAI_CHUAN: // 复用 DeepSeek 客户端 + case STEP_FUN: // 复用 DeepSeek 客户端 return DeepSeekChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); - case ZHI_PU: - return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) - .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); - case MINI_MAX: - return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) - .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); - case MOONSHOT: - return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) - .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); case OPENAI: case GEMINI: // 复用 OpenAI 客户端 - case BAI_CHUAN: // 复用 OpenAI 客户端 case GROK: // 复用 OpenAI 客户端 return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); @@ -159,4 +183,4 @@ public class AiUtils { return MapUtil.getStr(output.getMetadata(), "reasoningContent"); } -} \ No newline at end of file +} diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java index 06b0b25..dea9023 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; +import cn.hutool.system.SystemUtil; import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -8,13 +9,16 @@ import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.openai.OpenAiChatModel; -import org.springframework.ai.openai.OpenAiChatOptions; -import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import org.springframework.ai.deepseek.DeepSeekChatOptions; +import org.springframework.ai.deepseek.api.DeepSeekApi; import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; /** * {@link BaiChuanChatModel} 集成测试 @@ -23,22 +27,27 @@ import java.util.List; */ public class BaiChuanChatModelTests { - private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() - .openAiApi(OpenAiApi.builder() + private static final String API_KEY = SystemUtil.get("BAICHUAN_API_KEY", + "sk-xxxx"); // 按需改成你的百川 API Key + private static final String MODEL = SystemUtil.get("BAICHUAN_MODEL", + BaiChuanChatModel.MODEL_DEFAULT); + + private final BaiChuanChatModel chatModel = new BaiChuanChatModel(DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() .baseUrl(BaiChuanChatModel.BASE_URL) - .apiKey("sk-61b6766a94c70786ed02673f5e16af3c") // apiKey + .completionsPath(BaiChuanChatModel.COMPLETE_PATH) + .apiKey(API_KEY) .build()) - .defaultOptions(OpenAiChatOptions.builder() - .model("Baichuan4-Turbo") // 模型(https://platform.baichuan-ai.com/docs/api) + .defaultOptions(DeepSeekChatOptions.builder() + .model(MODEL) .temperature(0.7) .build()) - .build(); - - private final BaiChuanChatModel chatModel = new BaiChuanChatModel(openAiChatModel); + .build()); @Test @Disabled public void testCall() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -48,11 +57,13 @@ public class BaiChuanChatModelTests { ChatResponse response = chatModel.call(new Prompt(messages)); // 打印结果 System.out.println(response); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); } @Test @Disabled public void testStream() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -61,7 +72,10 @@ public class BaiChuanChatModelTests { // 调用 Flux flux = chatModel.stream(new Prompt(messages)); // 打印结果 - flux.doOnNext(System.out::println).then().block(); + flux.doOnNext(response -> { +// System.out.println(response); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); + }).then().block(); } } diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java index 2fbe0ee..64a28f8 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; +import cn.hutool.system.SystemUtil; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.ai.chat.messages.Message; @@ -14,6 +15,9 @@ import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; /** * {@link DeepSeekChatModel} 集成测试 @@ -22,12 +26,17 @@ import java.util.List; */ public class DeepSeekChatModelTests { + private static final String API_KEY = SystemUtil.get("DEEPSEEK_API_KEY", + "sk-xxxx"); + private static final String MODEL = SystemUtil.get("DEEPSEEK_MODEL", + "deepseek-v4-flash"); + private final DeepSeekChatModel chatModel = DeepSeekChatModel.builder() .deepSeekApi(DeepSeekApi.builder() - .apiKey("sk-eaf4172a057344dd9bc64b1f806b6axx") // apiKey + .apiKey(API_KEY) // apiKey .build()) .defaultOptions(DeepSeekChatOptions.builder() - .model("deepseek-chat") // 模型 + .model(MODEL) // 模型 .temperature(0.7) .build()) .build(); @@ -35,6 +44,7 @@ public class DeepSeekChatModelTests { @Test @Disabled public void testCall() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -43,12 +53,13 @@ public class DeepSeekChatModelTests { // 调用 ChatResponse response = chatModel.call(new Prompt(messages)); // 打印结果 - System.out.println(response); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); } @Test @Disabled public void testStream() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -63,11 +74,12 @@ public class DeepSeekChatModelTests { @Test @Disabled public void testStream_thinking() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); DeepSeekChatOptions options = DeepSeekChatOptions.builder() - .model("deepseek-reasoner") + .model(MODEL) .build(); // 调用 diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DouBaoChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DouBaoChatModelTests.java index 38c4f0b..5ab3da4 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DouBaoChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DouBaoChatModelTests.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; +import cn.hutool.system.SystemUtil; import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -15,6 +16,9 @@ import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; /** * {@link DouBaoChatModel} 集成测试 @@ -23,19 +27,19 @@ import java.util.List; */ public class DouBaoChatModelTests { - /** - * 相比 OpenAIChatModel 来说,DeepSeekChatModel 可以兼容豆包的 thinking 能力! - */ + private static final String API_KEY = SystemUtil.get("DOUBAO_API_KEY", + "sk-xxxx"); // 按需改成你的豆包 API Key + private static final String MODEL = SystemUtil.get("DOUBAO_MODEL", + DouBaoChatModel.MODEL_DEFAULT); + private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() .deepSeekApi(DeepSeekApi.builder() .baseUrl(DouBaoChatModel.BASE_URL) .completionsPath(DouBaoChatModel.COMPLETE_PATH) - .apiKey("5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272") // apiKey + .apiKey(API_KEY) .build()) .defaultOptions(DeepSeekChatOptions.builder() - .model("doubao-1-5-lite-32k-250115") // 模型(doubao) -// .model("doubao-seed-1-6-thinking-250715") // 模型(doubao) -// .model("deepseek-r1-250120") // 模型(deepseek) + .model(MODEL) .temperature(0.7) .build()) .build(); @@ -45,6 +49,7 @@ public class DouBaoChatModelTests { @Test @Disabled public void testCall() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -54,11 +59,13 @@ public class DouBaoChatModelTests { ChatResponse response = chatModel.call(new Prompt(messages)); // 打印结果 System.out.println(response); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); } @Test @Disabled public void testStream() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -67,25 +74,26 @@ public class DouBaoChatModelTests { // 调用 Flux flux = chatModel.stream(new Prompt(messages)); // 打印结果 - flux.doOnNext(System.out::println).then().block(); + flux.doOnNext(response -> { +// System.out.println(response); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); + }).then().block(); } @Test @Disabled public void testStream_thinking() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); - DeepSeekChatOptions options = DeepSeekChatOptions.builder() - .model("doubao-seed-1-6-thinking-250715") - .build(); // 调用 - Flux flux = chatModel.stream(new Prompt(messages, options)); + Flux flux = chatModel.stream(new Prompt(messages)); // 打印结果 flux.doOnNext(response -> { // System.out.println(response); - System.out.println(response.getResult().getOutput()); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); }).then().block(); } diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/GrokChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/GrokChatModelTests.java new file mode 100644 index 0000000..9443def --- /dev/null +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/GrokChatModelTests.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; + +import cn.hutool.system.SystemUtil; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.grok.GrokChatModel; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; + +/** + * {@link GrokChatModel} 集成测试 + * + * @author 芋道源码 + */ +public class GrokChatModelTests { + + private static final String API_KEY = SystemUtil.get("GROK_API_KEY", + "sk-xxxx"); + private static final String MODEL = SystemUtil.get("GROK_MODEL", + GrokChatModel.MODEL_DEFAULT); + + private final GrokChatModel chatModel = new GrokChatModel(OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(GrokChatModel.BASE_URL) + .completionsPath(GrokChatModel.COMPLETE_PATH) + .apiKey(API_KEY) + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model(MODEL) + .temperature(0.7) + .build()) + .build()); + + @Test + @Disabled + public void testCall() { + validateApiKey(API_KEY); + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); + } + + @Test + @Disabled + public void testStream() { + validateApiKey(API_KEY); + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(response -> { +// System.out.println(response); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); + }).then().block(); + } + +} diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/HunYuanChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/HunYuanChatModelTests.java index eeafef2..2543fce 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/HunYuanChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/HunYuanChatModelTests.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; +import cn.hutool.system.SystemUtil; import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -15,6 +16,9 @@ import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; /** * {@link HunYuanChatModel} 集成测试 @@ -23,14 +27,19 @@ import java.util.List; */ public class HunYuanChatModelTests { + private static final String API_KEY = SystemUtil.get("HUNYUAN_API_KEY", + "sk-xxxx"); + private static final String MODEL = SystemUtil.get("HUNYUAN_MODEL", + HunYuanChatModel.MODEL_DEFAULT); + private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() .deepSeekApi(DeepSeekApi.builder() .baseUrl(HunYuanChatModel.BASE_URL) .completionsPath(HunYuanChatModel.COMPLETE_PATH) - .apiKey("sk-abc") // apiKey + .apiKey(API_KEY) // apiKey .build()) .defaultOptions(DeepSeekChatOptions.builder() - .model(HunYuanChatModel.MODEL_DEFAULT) // 模型 + .model(MODEL) // 模型 .temperature(0.7) .build()) .build(); @@ -40,6 +49,7 @@ public class HunYuanChatModelTests { @Test @Disabled public void testCall() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -48,12 +58,13 @@ public class HunYuanChatModelTests { // 调用 ChatResponse response = chatModel.call(new Prompt(messages)); // 打印结果 - System.out.println(response); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); } @Test @Disabled public void testStream() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -68,12 +79,12 @@ public class HunYuanChatModelTests { @Test @Disabled public void testStream_thinking() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); DeepSeekChatOptions options = DeepSeekChatOptions.builder() - .model("hunyuan-a13b") -// .model("hunyuan-turbos-latest") + .model(MODEL) .build(); // 调用 @@ -85,66 +96,4 @@ public class HunYuanChatModelTests { }).then().block(); } - private final DeepSeekChatModel deepSeekOpenAiChatModel = DeepSeekChatModel.builder() - .deepSeekApi(DeepSeekApi.builder() - .baseUrl(HunYuanChatModel.DEEP_SEEK_BASE_URL) - .completionsPath(HunYuanChatModel.COMPLETE_PATH) - .apiKey("sk-abc") // apiKey - .build()) - .defaultOptions(DeepSeekChatOptions.builder() -// .model(HunYuanChatModel.DEEP_SEEK_MODEL_DEFAULT) // 模型("deepseek-v3") - .model("deepseek-r1") // 模型("deepseek-r1") - .temperature(0.7) - .build()) - .build(); - - private final HunYuanChatModel deepSeekChatModel = new HunYuanChatModel(deepSeekOpenAiChatModel); - - @Test - @Disabled - public void testCall_deepseek() { - // 准备参数 - List messages = new ArrayList<>(); - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); - messages.add(new UserMessage("1 + 1 = ?")); - - // 调用 - ChatResponse response = deepSeekChatModel.call(new Prompt(messages)); - // 打印结果 - System.out.println(response); - } - - @Test - @Disabled - public void testStream_deepseek() { - // 准备参数 - List messages = new ArrayList<>(); - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); - messages.add(new UserMessage("1 + 1 = ?")); - - // 调用 - Flux flux = deepSeekChatModel.stream(new Prompt(messages)); - // 打印结果 - flux.doOnNext(System.out::println).then().block(); - } - - @Test - @Disabled - public void testStream_deepseek_thinking() { - // 准备参数 - List messages = new ArrayList<>(); - messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); - DeepSeekChatOptions options = DeepSeekChatOptions.builder() - .model("deepseek-r1") - .build(); - - // 调用 - Flux flux = deepSeekChatModel.stream(new Prompt(messages, options)); - // 打印结果 - flux.doOnNext(response -> { -// System.out.println(response); - System.out.println(response.getResult().getOutput()); - }).then().block(); - } - } diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MiniMaxChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MiniMaxChatModelTests.java index 8fb133d..45615c8 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MiniMaxChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MiniMaxChatModelTests.java @@ -1,5 +1,7 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; +import cn.hutool.system.SystemUtil; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax.MiniMaxChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.ai.chat.messages.Message; @@ -7,13 +9,16 @@ import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.minimax.MiniMaxChatModel; -import org.springframework.ai.minimax.MiniMaxChatOptions; -import org.springframework.ai.minimax.api.MiniMaxApi; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import org.springframework.ai.deepseek.DeepSeekChatOptions; +import org.springframework.ai.deepseek.api.DeepSeekApi; import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; /** * {@link MiniMaxChatModel} 的集成测试 @@ -22,14 +27,24 @@ import java.util.List; */ public class MiniMaxChatModelTests { - private final MiniMaxChatModel chatModel = new MiniMaxChatModel( - new MiniMaxApi("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJHcm91cE5hbWUiOiLnjovmlofmlowiLCJVc2VyTmFtZSI6IueOi-aWh-aWjCIsIkFjY291bnQiOiIiLCJTdWJqZWN0SUQiOiIxODk3Mjg3MjQ5NDU2ODA4MzQ2IiwiUGhvbmUiOiIxNTYwMTY5MTM5OSIsIkdyb3VwSUQiOiIxODk3Mjg3MjQ5NDQ4NDE5NzM4IiwiUGFnZU5hbWUiOiIiLCJNYWlsIjoiIiwiQ3JlYXRlVGltZSI6IjIwMjUtMDMtMTEgMTI6NTI6MDIiLCJUb2tlblR5cGUiOjEsImlzcyI6Im1pbmltYXgifQ.aAuB7gWW_oA4IYhh-CF7c9MfWWxKN49B_HK-DYjXaDwwffhiG-H1571z1WQhp9QytWG-DqgLejneeSxkiq1wQIe3FsEP2wz4BmGBct31LehbJu8ehLxg_vg75Uod1nFAHbm5mZz6JSVLNIlSo87Xr3UtSzJhAXlapEkcqlA4YOzOpKrZ8l5_OJPTORTCmHWZYgJcRS-faNiH62ZnUEHUozesTFhubJHo5GfJCw_edlnmfSUocERV1BjWvenhZ9My-aYXNktcW9WaSj9l6gayV7A0Ium_PL55T9ln1PcI8gayiVUKJGJDoqNyF1AF9_aF9NOKtTnQzwNqnZdlTYH6hw"), // 密钥 - MiniMaxChatOptions.builder() - .model(MiniMaxApi.ChatModel.ABAB_6_5_G_Chat.getValue()) // 模型 - .build()); + private static final String API_KEY = SystemUtil.get("MINIMAX_API_KEY", + "sk-xxxx"); // 按需改成你的 MiniMax API Key + private static final String MODEL = SystemUtil.get("MINIMAX_MODEL", + MiniMaxChatModel.MODEL_DEFAULT); + + private final MiniMaxChatModel chatModel = new MiniMaxChatModel(DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() + .baseUrl(MiniMaxChatModel.BASE_URL) + .apiKey(API_KEY) + .build()) + .defaultOptions(DeepSeekChatOptions.builder() + .model(MODEL) + .build()) + .build()); @Test @Disabled public void testCall() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -39,12 +54,13 @@ public class MiniMaxChatModelTests { ChatResponse response = chatModel.call(new Prompt(messages)); // 打印结果 System.out.println(response); - System.out.println(response.getResult().getOutput()); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); } @Test @Disabled public void testStream() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -55,19 +71,19 @@ public class MiniMaxChatModelTests { // 打印结果 flux.doOnNext(response -> { // System.out.println(response); - System.out.println(response.getResult().getOutput()); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); }).then().block(); } - // TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复 @Test @Disabled public void testStream_thinking() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); - MiniMaxChatOptions options = MiniMaxChatOptions.builder() - .model("MiniMax-M1") + DeepSeekChatOptions options = DeepSeekChatOptions.builder() + .model(MODEL) .build(); // 调用 @@ -75,7 +91,7 @@ public class MiniMaxChatModelTests { // 打印结果 flux.doOnNext(response -> { // System.out.println(response); - System.out.println(response.getResult().getOutput()); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); }).then().block(); } diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java index b50ab80..9aaed7c 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java @@ -1,19 +1,24 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; +import cn.hutool.system.SystemUtil; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot.MoonshotChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.springaicommunity.moonshot.MoonshotChatModel; -import org.springaicommunity.moonshot.MoonshotChatOptions; -import org.springaicommunity.moonshot.api.MoonshotApi; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import org.springframework.ai.deepseek.DeepSeekChatOptions; +import org.springframework.ai.deepseek.api.DeepSeekApi; import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; /** * {@link MoonshotChatModel} 的集成测试 @@ -22,18 +27,27 @@ import java.util.List; */ public class MoonshotChatModelTests { - private final MoonshotChatModel chatModel = MoonshotChatModel.builder() - .moonshotApi(MoonshotApi.builder() - .apiKey("sk-aHYYV1SARscItye5QQRRNbXij4fy65Ee7pNZlC9gsSQnUKXA") // 密钥 + private static final String API_KEY = SystemUtil.get("MOONSHOT_API_KEY", + "sk-xxxx"); // 按需改成你的 Moonshot API Key + private static final String MODEL = SystemUtil.get("MOONSHOT_MODEL", + MoonshotChatModel.MODEL_DEFAULT); + + private final MoonshotChatModel chatModel = new MoonshotChatModel(DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() + .baseUrl(MoonshotChatModel.BASE_URL) + .completionsPath(MoonshotChatModel.COMPLETE_PATH) + .apiKey(API_KEY) .build()) - .defaultOptions(MoonshotChatOptions.builder() - .model("kimi-k2-0711-preview") // 模型 + .defaultOptions(DeepSeekChatOptions.builder() + .model(MODEL) + .temperature(1D) .build()) - .build(); + .build()); @Test @Disabled public void testCall() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -43,12 +57,13 @@ public class MoonshotChatModelTests { ChatResponse response = chatModel.call(new Prompt(messages)); // 打印结果 System.out.println(response); - System.out.println(response.getResult().getOutput()); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); } @Test @Disabled public void testStream() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -59,20 +74,19 @@ public class MoonshotChatModelTests { // 打印结果 flux.doOnNext(response -> { // System.out.println(response); - System.out.println(response.getResult().getOutput()); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); }).then().block(); } - // TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复 @Test @Disabled public void testStream_thinking() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); - MoonshotChatOptions options = MoonshotChatOptions.builder() -// .model("kimi-k2-0711-preview") - .model("kimi-thinking-preview") + DeepSeekChatOptions options = DeepSeekChatOptions.builder() + .model(MODEL) .build(); // 调用 @@ -80,7 +94,7 @@ public class MoonshotChatModelTests { // 打印结果 flux.doOnNext(response -> { // System.out.println(response); - System.out.println(response.getResult().getOutput()); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); }).then().block(); } diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/StepFunChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/StepFunChatModelTests.java new file mode 100644 index 0000000..1a6c47d --- /dev/null +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/StepFunChatModelTests.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; + +import cn.hutool.system.SystemUtil; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun.StepFunChatModel; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import org.springframework.ai.deepseek.DeepSeekChatOptions; +import org.springframework.ai.deepseek.api.DeepSeekApi; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; + +/** + * {@link StepFunChatModel} 集成测试 + * + * @author 芋道源码 + */ +public class StepFunChatModelTests { + + private static final String API_KEY = SystemUtil.get("STEPFUN_API_KEY", + "sk-xxxx"); + private static final String MODEL = SystemUtil.get("STEPFUN_MODEL", + StepFunChatModel.MODEL_DEFAULT); + + private final StepFunChatModel chatModel = new StepFunChatModel(DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() + .baseUrl(StepFunChatModel.BASE_URL) + .completionsPath(StepFunChatModel.COMPLETE_PATH) + .apiKey(API_KEY) + .build()) + .defaultOptions(DeepSeekChatOptions.builder() + .model(MODEL) + .temperature(0.7) + .build()) + .build()); + + @Test + @Disabled + public void testCall() { + validateApiKey(API_KEY); + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); + } + + @Test + @Disabled + public void testStream() { + validateApiKey(API_KEY); + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(response -> { +// System.out.println(response); +// System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); + System.out.println(response.getResult() != null ? response.getResult().getOutput() : null); + }).then().block(); + } + +} diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/XingHuoChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/XingHuoChatModelTests.java index 77dbd2b..2e08cf6 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/XingHuoChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/XingHuoChatModelTests.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; +import cn.hutool.system.SystemUtil; import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -8,13 +9,14 @@ import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.deepseek.DeepSeekChatModel; import org.springframework.ai.deepseek.DeepSeekChatOptions; -import org.springframework.ai.deepseek.api.DeepSeekApi; import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; /** * {@link XingHuoChatModel} 集成测试 @@ -23,24 +25,23 @@ import java.util.List; */ public class XingHuoChatModelTests { - private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() - .deepSeekApi(DeepSeekApi.builder() - .baseUrl(XingHuoChatModel.BASE_URL_V2) - .completionsPath(XingHuoChatModel.BASE_COMPLETIONS_PATH_V2) - .apiKey("75b161ed2aef4719b275d6e7f2a4d4cd:YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz") // appKey:secretKey - .build()) - .defaultOptions(DeepSeekChatOptions.builder() -// .model("generalv3.5") // 模型 - .model("x1") // 模型 - .temperature(0.7) - .build()) - .build(); + private static final String API_KEY = SystemUtil.get("XINGHUO_API_KEY", + "sk-xxxx"); // 按需改成你的讯飞星火 API Key + private static final String MODEL = SystemUtil.get("XINGHUO_MODEL", + XingHuoChatModel.MODEL_DEFAULT); - private final XingHuoChatModel chatModel = new XingHuoChatModel(openAiChatModel); + private final XingHuoChatModel chatModel = XingHuoChatModel.builder() + .apiKey(API_KEY) + .options(DeepSeekChatOptions.builder() + .model(MODEL) + .temperature(0.7) + .build()) + .build(); @Test @Disabled public void testCall() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -55,6 +56,7 @@ public class XingHuoChatModelTests { @Test @Disabled public void testStream() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -69,11 +71,12 @@ public class XingHuoChatModelTests { @Test @Disabled public void testStream_thinking() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); DeepSeekChatOptions options = DeepSeekChatOptions.builder() - .model("x1") + .model(MODEL) .build(); // 调用 @@ -81,7 +84,7 @@ public class XingHuoChatModelTests { // 打印结果 flux.doOnNext(response -> { // System.out.println(response); - System.out.println(response.getResult().getOutput()); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); }).then().block(); } diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java index cb7be2a..72086de 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java @@ -1,62 +1,76 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; +import cn.hutool.system.SystemUtil; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan.YiYanChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.springaicommunity.qianfan.QianFanChatModel; -import org.springaicommunity.qianfan.QianFanChatOptions; -import org.springaicommunity.qianfan.api.QianFanApi; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import org.springframework.ai.deepseek.DeepSeekChatOptions; +import org.springframework.ai.deepseek.api.DeepSeekApi; import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; -// TODO @芋艿:百度千帆 API 提供了 V2 版本,目前 Spring AI 不兼容,可关键 进展 /** - * {@link QianFanChatModel} 的集成测试 + * {@link YiYanChatModel} 的集成测试 * * @author fansili */ public class YiYanChatModelTests { - private final QianFanChatModel chatModel = new QianFanChatModel( - new QianFanApi("DGnyzREuaY7av7c38bOM9Ji2", "9aR8myflEOPDrEeLhoXv0FdqANOAyIZW"), // 密钥 - QianFanChatOptions.builder() - .model("ERNIE-4.5-8K-Preview") - .build() - ); + private static final String API_KEY = SystemUtil.get("YIYAN_API_KEY", + "sk-xxxx"); // 按需改成你的文心一言 API Key + private static final String MODEL = SystemUtil.get("YIYAN_MODEL", + YiYanChatModel.MODEL_DEFAULT); + + private final YiYanChatModel chatModel = new YiYanChatModel(DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() + .baseUrl(YiYanChatModel.BASE_URL) + .apiKey(API_KEY) + .build()) + .defaultOptions(DeepSeekChatOptions.builder() + .model(MODEL) + .build()) + .build()); @Test @Disabled public void testCall() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); - // TODO @芋艿:文心一言,只要带上 system message 就报错,已经各种测试,很莫名! -// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); messages.add(new UserMessage("1 + 1 = ?")); // 调用 ChatResponse response = chatModel.call(new Prompt(messages)); // 打印结果 System.out.println(response); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); } @Test @Disabled public void testStream() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); - // TODO @芋艿:文心一言,只要带上 system message 就报错,已经各种测试,很莫名! -// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); messages.add(new UserMessage("1 + 1 = ?")); // 调用 Flux flux = chatModel.stream(new Prompt(messages)); // 打印结果 - flux.doOnNext(System.out::println).then().block(); + flux.doOnNext(response -> { +// System.out.println(response); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); + }).then().block(); } } diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/ZhiPuAiChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/ZhiPuAiChatModelTests.java index 18c3179..290d19d 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/ZhiPuAiChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/ZhiPuAiChatModelTests.java @@ -1,5 +1,7 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; +import cn.hutool.system.SystemUtil; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu.ZhiPuChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.ai.chat.messages.Message; @@ -7,31 +9,43 @@ import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.zhipuai.ZhiPuAiChatModel; -import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; -import org.springframework.ai.zhipuai.api.ZhiPuAiApi; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import org.springframework.ai.deepseek.DeepSeekChatOptions; +import org.springframework.ai.deepseek.api.DeepSeekApi; import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; /** - * {@link ZhiPuAiChatModel} 的集成测试 + * {@link ZhiPuChatModel} 的集成测试 * * @author 芋道源码 */ public class ZhiPuAiChatModelTests { - private final ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel( - ZhiPuAiApi.builder().apiKey("2f35fb6ca4ea41fab898729b7fac086c.6ESSfPcCkxaKEUlR").build(), // 密钥 - ZhiPuAiChatOptions.builder() - .model(ZhiPuAiApi.ChatModel.GLM_4.getName()) // 模型 + private static final String API_KEY = SystemUtil.get("ZHIPU_API_KEY", + "sk-xxxx"); // 按需改成你的智谱 API Key + private static final String MODEL = SystemUtil.get("ZHIPU_MODEL", + ZhiPuChatModel.MODEL_DEFAULT); + + private final ZhiPuChatModel chatModel = new ZhiPuChatModel(DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() + .baseUrl(ZhiPuChatModel.BASE_URL) + .apiKey(API_KEY) + .build()) + .defaultOptions(DeepSeekChatOptions.builder() + .model(MODEL) .build() - ); + ).build()); @Test @Disabled public void testCall() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -41,12 +55,13 @@ public class ZhiPuAiChatModelTests { ChatResponse response = chatModel.call(new Prompt(messages)); // 打印结果 System.out.println(response); - System.out.println(response.getResult().getOutput()); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); } @Test @Disabled public void testStream() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -57,19 +72,19 @@ public class ZhiPuAiChatModelTests { // 打印结果 flux.doOnNext(response -> { // System.out.println(response); - System.out.println(response.getResult().getOutput()); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); }).then().block(); } - // TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复 @Test @Disabled public void testStream_thinking() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); - ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder() - .model("GLM-4.5") + DeepSeekChatOptions options = DeepSeekChatOptions.builder() + .model(MODEL) .build(); // 调用 @@ -77,7 +92,7 @@ public class ZhiPuAiChatModelTests { // 打印结果 flux.doOnNext(response -> { // System.out.println(response); - System.out.println(response.getResult().getOutput()); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); }).then().block(); } diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java index 1b12452..7d53fe6 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.image; +import cn.hutool.system.SystemUtil; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.ai.image.ImageOptions; @@ -9,6 +10,8 @@ import org.springframework.ai.openai.OpenAiImageModel; import org.springframework.ai.openai.OpenAiImageOptions; import org.springframework.ai.openai.api.OpenAiImageApi; +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; + /** * {@link OpenAiImageModel} 集成测试类 * @@ -16,17 +19,20 @@ import org.springframework.ai.openai.api.OpenAiImageApi; */ public class OpenAiImageModelTests { + private static final String API_KEY = SystemUtil.get("OPENAI_API_KEY", "sk-xxxx"); + private final OpenAiImageModel imageModel = new OpenAiImageModel(OpenAiImageApi.builder() .baseUrl("https://api.holdai.top") // apiKey - .apiKey("sk-PytRecQlmjEteoa2RRN6cGnwslo72UUPLQVNEMS6K9yjbmpD") + .apiKey(API_KEY) .build()); @Test @Disabled public void testCall() { + validateApiKey(API_KEY); // 准备参数 ImageOptions options = OpenAiImageOptions.builder() - .model(OpenAiImageApi.ImageModel.DALL_E_2.getValue()) // 这个模型比较便宜 + .model("gpt-image-1") .height(256).width(256) .build(); ImagePrompt prompt = new ImagePrompt("中国长城!", options); diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/QianFanImageTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/QianFanImageTests.java deleted file mode 100644 index 156360f..0000000 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/QianFanImageTests.java +++ /dev/null @@ -1,43 +0,0 @@ -package cn.iocoder.yudao.module.ai.framework.ai.core.model.image; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.springaicommunity.qianfan.QianFanImageModel; -import org.springaicommunity.qianfan.QianFanImageOptions; -import org.springaicommunity.qianfan.api.QianFanImageApi; -import org.springframework.ai.image.ImagePrompt; -import org.springframework.ai.image.ImageResponse; - -import static cn.iocoder.yudao.module.ai.framework.ai.core.model.image.StabilityAiImageModelTests.viewImage; - -// TODO @芋艿:百度千帆 API 提供了 V2 版本,目前 Spring AI 不兼容,可关键 进展 - -/** - * {@link QianFanImageModel} 集成测试类 - */ -public class QianFanImageTests { - - private final QianFanImageModel imageModel = new QianFanImageModel( - new QianFanImageApi("qS8k8dYr2nXunagK4SSU8Xjj", "pHGbx51ql2f0hOyabQvSZezahVC3hh3e")); // 密钥 - - @Test - @Disabled - public void testCall() { - // 准备参数 - // 只支持 1024x1024、768x768、768x1024、1024x768、576x1024、1024x576 - QianFanImageOptions imageOptions = QianFanImageOptions.builder() - .model(QianFanImageApi.ImageModel.Stable_Diffusion_XL.getValue()) - .width(1024).height(1024) - .N(1) - .build(); - ImagePrompt prompt = new ImagePrompt("good", imageOptions); - - // 方法调用 - ImageResponse response = imageModel.call(prompt); - // 打印结果 - String b64Json = response.getResult().getOutput().getB64Json(); - System.out.println(response); - viewImage(b64Json); - } - -} diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/ZhiPuAiImageModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/ZhiPuAiImageModelTests.java deleted file mode 100644 index bb16aac..0000000 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/ZhiPuAiImageModelTests.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.iocoder.yudao.module.ai.framework.ai.core.model.image; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.springframework.ai.image.ImagePrompt; -import org.springframework.ai.image.ImageResponse; -import org.springframework.ai.zhipuai.ZhiPuAiImageModel; -import org.springframework.ai.zhipuai.ZhiPuAiImageOptions; -import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; - -/** - * {@link ZhiPuAiImageModel} 集成测试 - */ -public class ZhiPuAiImageModelTests { - - private final ZhiPuAiImageModel imageModel = new ZhiPuAiImageModel( - new ZhiPuAiImageApi("78d3228c1d9e5e342a3e1ab349e2dd7b.VXLoq5vrwK2ofboy") // 密钥 - ); - - @Test - @Disabled - public void testCall() { - // 准备参数 - ZhiPuAiImageOptions imageOptions = ZhiPuAiImageOptions.builder() - .model(ZhiPuAiImageApi.ImageModel.CogView_3.getValue()) - .build(); - ImagePrompt prompt = new ImagePrompt("万里长城", imageOptions); - - // 方法调用 - ImageResponse response = imageModel.call(prompt); - // 打印结果 - System.out.println(response); - } - -} diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java index 7cf23c8..94b89a2 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java @@ -48,6 +48,10 @@ public interface ErrorCodeConstants { ErrorCode CODEGEN_MASTER_TABLE_NOT_EXISTS = new ErrorCode(1_001_004_010, "主表(id={})定义不存在,请检查"); ErrorCode CODEGEN_SUB_COLUMN_NOT_EXISTS = new ErrorCode(1_001_004_011, "子表的字段(id={})不存在,请检查"); ErrorCode CODEGEN_MASTER_GENERATION_FAIL_NO_SUB_TABLE = new ErrorCode(1_001_004_012, "主表生成代码失败,原因:它没有子表"); + ErrorCode CODEGEN_MASTER_TABLE_NAME_DUPLICATE = new ErrorCode(1_001_004_013, + "主子表规范化类名({})重复,请调整主表或子表类名"); + ErrorCode CODEGEN_MASTER_TABLE_FIELD_DUPLICATE = new ErrorCode(1_001_004_014, + "主子表属性名({})重复,请调整主表字段、子表类名或关联关系"); // ========== 文件配置 1-001-006-000 ========== ErrorCode FILE_CONFIG_NOT_EXISTS = new ErrorCode(1_001_006_000, "文件配置不存在"); diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java index d675558..8bf14ba 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java @@ -48,6 +48,9 @@ import java.util.*; import static cn.hutool.core.map.MapUtil.getStr; import static cn.hutool.core.text.CharSequenceUtil.*; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.CODEGEN_MASTER_TABLE_FIELD_DUPLICATE; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.CODEGEN_MASTER_TABLE_NAME_DUPLICATE; /** * 代码生成的引擎,用于具体生成代码 @@ -152,6 +155,16 @@ public class CodegenEngine { vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/form/index.vue")) .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/detail/index.vue"), vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/detail/index.vue")) + .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/index_tree.vue"), + vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/index.vue")) + .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("components/breadcrumb_tree.vue"), + vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/components/breadcrumb.vue")) + .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/detail_master.vue"), + vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/detail/index.vue")) + .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/detail_erp.vue"), + vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/detail/index.vue")) + .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/form_sub_erp.vue"), + vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/${subSimpleClassName_strikeCase}/form/index.vue")) // VUE3_VBEN2_ANTD_SCHEMA .put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/data.ts"), vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts")) @@ -380,12 +393,17 @@ public class CodegenEngine { // 2. 执行生成 Map result = Maps.newLinkedHashMapWithExpectedSize(templates.size()); // 有序 + Map generatedSources = Maps.newHashMapWithExpectedSize(templates.size()); templates.forEach((vmPath, filePath) -> { - // 2.1 特殊:主子表专属逻辑 + // 2.1 特殊:主子表、Uniapp 树表使用独立页面模版 + if (isVue3AdminUniappTemplate(vmPath) && !matchVue3AdminUniappTemplate(table, vmPath)) { + return; + } + // 2.2 特殊:主子表专属逻辑 if (isSubTemplate(vmPath)) { - generateSubCode(table, subTables, result, vmPath, filePath, bindingMap); + generateSubCode(table, subTables, result, generatedSources, vmPath, filePath, bindingMap); return; - // 2.2 特殊:树表专属逻辑 + // 2.3 特殊:树表专属逻辑 } else if (isPageReqVOTemplate(vmPath)) { // 减少多余的类生成,例如说 PageVO.java 类 if (CodegenTemplateTypeEnum.isTree(table.getTemplateType())) { @@ -402,23 +420,36 @@ public class CodegenEngine { return; } } - // 2.3 默认生成 - generateCode(result, vmPath, filePath, bindingMap); + // 2.4 默认生成 + generateCode(result, generatedSources, vmPath, filePath, bindingMap); }); return result; } - private void generateCode(Map result, String vmPath, + private void generateCode(Map result, Map generatedSources, String vmPath, String filePath, Map bindingMap) { filePath = formatFilePath(filePath, bindingMap); + registerGeneratedSource(generatedSources, filePath, vmPath, bindingMap); String content = templateEngine.getTemplate(vmPath).render(bindingMap); // 格式化代码 content = prettyCode(content, vmPath); result.put(filePath, content); } + @VisibleForTesting + static void registerGeneratedSource(Map generatedSources, String filePath, String vmPath, + Map bindingMap) { + String source = vmPath + + (bindingMap.containsKey("subIndex") ? "[subIndex=" + bindingMap.get("subIndex") + "]" : ""); + String previousSource = generatedSources.putIfAbsent(filePath, source); + if (previousSource != null) { + throw new IllegalStateException(String.format("生成文件路径重复:%s,来源模板:%s、%s", + filePath, previousSource, source)); + } + } + private void generateSubCode(CodegenTableDO table, List subTables, - Map result, String vmPath, + Map result, Map generatedSources, String vmPath, String filePath, Map bindingMap) { // 没有子表,所以不生成 if (CollUtil.isEmpty(subTables)) { @@ -441,7 +472,7 @@ public class CodegenEngine { // 逐个生成 for (int i = 0; i < subTables.size(); i++) { bindingMap.put("subIndex", i); - generateCode(result, vmPath, filePath, bindingMap); + generateCode(result, generatedSources, vmPath, filePath, bindingMap); } bindingMap.remove("subIndex"); } @@ -482,6 +513,13 @@ public class CodegenEngine { if (StrUtil.count(content, "DICT_TYPE.") == 0) { content = StrUtils.removeLineContains(content, "DICT_TYPE"); } + // Vue3 Admin UniApp:清理空白行,并补充文件末尾换行,满足 ESLint 格式要求 + if (vmPath.contains("vue3_admin_uniapp")) { + content = content.replaceAll("(?m)^[\\t ]+$", ""); + if (!content.endsWith("\n")) { + content += "\n"; + } + } return content; } @@ -533,6 +571,14 @@ public class CodegenEngine { List subClassNameVars = new ArrayList<>(); List simpleClassNameUnderlineCases = new ArrayList<>(); List subSimpleClassNameStrikeCases = new ArrayList<>(); + Set masterSymbols = new HashSet<>(); + masterSymbols.add(simpleClassName); + boolean mergeSubTableFields = ObjectUtils.equalsAny(table.getTemplateType(), + CodegenTemplateTypeEnum.MASTER_NORMAL.getType(), CodegenTemplateTypeEnum.MASTER_INNER.getType()); + Set masterFieldSymbols = new HashSet<>(); + if (mergeSubTableFields) { + columns.forEach(column -> masterFieldSymbols.add(column.getJavaField())); + } for (int i = 0; i < subTables.size(); i++) { CodegenTableDO subTable = subTables.get(i); List subColumns = subColumnsList.get(i); @@ -543,9 +589,18 @@ public class CodegenEngine { subJoinColumnStrikeCases.add(toSymbolCase(subColumn.getJavaField(), '-')); // 将 DictType 转换成 dict-type // className 相关 String subSimpleClassName = removePrefix(subTable.getClassName(), upperFirst(subTable.getModuleName())); + if (!masterSymbols.add(subSimpleClassName)) { + throw exception(CODEGEN_MASTER_TABLE_NAME_DUPLICATE, subSimpleClassName); + } + String subClassNameVar = lowerFirst(subSimpleClassName); + String subFieldName = subClassNameVar + + (Boolean.TRUE.equals(subTable.getSubJoinMany()) ? "s" : ""); + if (mergeSubTableFields && !masterFieldSymbols.add(subFieldName)) { + throw exception(CODEGEN_MASTER_TABLE_FIELD_DUPLICATE, subFieldName); + } subSimpleClassNames.add(subSimpleClassName); simpleClassNameUnderlineCases.add(toUnderlineCase(subSimpleClassName)); // 将 DictType 转换成 dict_type - subClassNameVars.add(lowerFirst(subSimpleClassName)); // 将 DictType 转换成 dictType,用于变量 + subClassNameVars.add(subClassNameVar); // 将 DictType 转换成 dictType,用于变量 subSimpleClassNameStrikeCases.add(toSymbolCase(subSimpleClassName, '-')); // 将 DictType 转换成 dict-type } bindingMap.put("subPrimaryColumns", subPrimaryColumns); @@ -734,6 +789,33 @@ public class CodegenEngine { return path.contains("_sub"); } + private static boolean isVue3AdminUniappTemplate(String path) { + return path.contains("codegen/vue3_admin_uniapp/"); + } + + /** + * 匹配 Uniapp 不同表类型的专用页面模版 + */ + private static boolean matchVue3AdminUniappTemplate(CodegenTableDO table, String path) { + boolean tree = CodegenTemplateTypeEnum.isTree(table.getTemplateType()); + boolean masterSubmit = ObjectUtil.equals(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_NORMAL.getType()) + || ObjectUtil.equals(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_INNER.getType()); + boolean masterErp = ObjectUtil.equals(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_ERP.getType()); + if (path.contains("_tree")) { + return tree; + } + if (path.contains("detail_master")) { + return masterSubmit; + } + if (path.contains("_erp")) { + return masterErp; + } + if (tree && path.endsWith("views/index.vue.vm")) { + return false; + } + return !(masterSubmit || masterErp) || !path.endsWith("views/detail/index.vue.vm"); + } + private static boolean isPageReqVOTemplate(String path) { return path.contains("pageReqVO"); } diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/api/api.ts.vm b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/api/api.ts.vm index 6c5d706..9285223 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/api/api.ts.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/api/api.ts.vm @@ -1,36 +1,80 @@ +#if ($table.templateType != 2) import type { PageParam, PageResult } from '@/http/types' +#end import { http } from '@/http/http' #set ($primaryJavaType = $primaryColumn.javaType.toLowerCase()) -#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte") +#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "float" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte") #set ($primaryTsType = "number") #else #set ($primaryTsType = "string") #end +#foreach ($subTable in $subTables) +#set ($index = $foreach.count - 1) +#set ($subSimpleClassName = $subSimpleClassNames.get($index)) +#set ($subColumns = $subColumnsList.get($index)) +#set ($subJoinColumn = $subJoinColumns.get($index)) +/** ${subTable.classComment}信息 */ +export interface ${subSimpleClassName} { +#foreach ($column in $subColumns) +#if ($column.primaryKey || $column.createOperation || $column.updateOperation || $column.listOperationResult) +#set ($javaType = $column.javaType.toLowerCase()) +#set ($optional = $column.nullable || $column.primaryKey || ($table.templateType != 11 && $column.id == $subJoinColumn.id)) +#if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "float" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte") + ${column.javaField}#if($optional)?#end: number +#elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime") + ${column.javaField}#if($optional)?#end: number +#elseif(${javaType} == "boolean") + ${column.javaField}#if($optional)?#end: boolean +#else + ${column.javaField}#if($optional)?#end: string +#end +#end +#end +} + +#end /** ${table.classComment}信息 */ export interface ${simpleClassName} { #foreach ($column in $columns) - #if ($column.primaryKey || $column.createOperation || $column.updateOperation || $column.listOperationResult) - #set ($javaType = $column.javaType.toLowerCase()) - #set ($javaFieldLower = $column.javaField.toLowerCase()) - #set ($optional = $column.nullable || $column.primaryKey || $javaFieldLower == "createtime" || $javaFieldLower == "updatetime") - #if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte") +#if ($column.primaryKey || $column.createOperation || $column.updateOperation || $column.listOperationResult) +#set ($javaType = $column.javaType.toLowerCase()) +#set ($optional = $column.nullable || $column.primaryKey) +#if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "float" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte") + ${column.javaField}#if($optional)?#end: number +#elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime") ${column.javaField}#if($optional)?#end: number - #elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime") - ${column.javaField}#if($optional)?#end: Date - #elseif(${javaType} == "boolean") +#elseif(${javaType} == "boolean") ${column.javaField}#if($optional)?#end: boolean - #else +#else ${column.javaField}#if($optional)?#end: string - #end - #end +#end +#end +#end +#if ($table.templateType == 2) + children?: ${simpleClassName}[] +#end +#if ($table.templateType == 10 || $table.templateType == 12) +#foreach ($subTable in $subTables) +#set ($index = $foreach.count - 1) +#set ($subSimpleClassName = $subSimpleClassNames.get($index)) +#set ($subClassNameVar = $subClassNameVars.get($index)) + ${subClassNameVar}#if($subTable.subJoinMany)s#end?: ${subSimpleClassName}#if($subTable.subJoinMany)[]#else | null#end +#end #end } +#if ($table.templateType == 2) +/** 获取${table.classComment}列表 */ +export function get${simpleClassName}List(params?: Record) { + return http.get<${simpleClassName}[]>('/${table.moduleName}/${simpleClassName_strikeCase}/list', params) +} +#else /** 获取${table.classComment}分页列表 */ export function get${simpleClassName}Page(params: PageParam) { return http.get>('/${table.moduleName}/${simpleClassName_strikeCase}/page', params) } +#end /** 获取${table.classComment}详情 */ export function get${simpleClassName}(id: ${primaryTsType}) { @@ -39,7 +83,7 @@ export function get${simpleClassName}(id: ${primaryTsType}) { /** 创建${table.classComment} */ export function create${simpleClassName}(data: ${simpleClassName}) { - return http.post('/${table.moduleName}/${simpleClassName_strikeCase}/create', data) + return http.post<${primaryTsType}>('/${table.moduleName}/${simpleClassName_strikeCase}/create', data) } /** 更新${table.classComment} */ @@ -51,3 +95,65 @@ export function update${simpleClassName}(data: ${simpleClassName}) { export function delete${simpleClassName}(id: ${primaryTsType}) { return http.delete(`/${table.moduleName}/${simpleClassName_strikeCase}/delete?id=${id}`) } +#foreach ($subTable in $subTables) +#set ($index = $foreach.count - 1) +#set ($subSimpleClassName = $subSimpleClassNames.get($index)) +#set ($subPrimaryColumn = $subPrimaryColumns.get($index)) +#set ($subJoinColumn = $subJoinColumns.get($index)) +#set ($subPrimaryJavaType = $subPrimaryColumn.javaType.toLowerCase()) +#if(${subPrimaryJavaType} == "long" || ${subPrimaryJavaType} == "integer" || ${subPrimaryJavaType} == "short" || ${subPrimaryJavaType} == "float" || ${subPrimaryJavaType} == "double" || ${subPrimaryJavaType} == "bigdecimal" || ${subPrimaryJavaType} == "byte") +#set ($subPrimaryTsType = "number") +#else +#set ($subPrimaryTsType = "string") +#end +#set ($subJoinJavaType = $subJoinColumn.javaType.toLowerCase()) +#if(${subJoinJavaType} == "long" || ${subJoinJavaType} == "integer" || ${subJoinJavaType} == "short" || ${subJoinJavaType} == "float" || ${subJoinJavaType} == "double" || ${subJoinJavaType} == "bigdecimal" || ${subJoinJavaType} == "byte") +#set ($subJoinTsType = "number") +#else +#set ($subJoinTsType = "string") +#end +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + $subJoinColumn.javaField.substring(1)) +#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index)) +#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index)) + +// ==================== 子表:${subTable.classComment} ==================== +#if ($table.templateType == 11) + +/** 获取${subTable.classComment}分页列表 */ +export function get${subSimpleClassName}Page(params: PageParam & { ${subJoinColumn.javaField}: ${subJoinTsType} }) { + return http.get>('/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/page', params) +} + +/** 获取${subTable.classComment}详情 */ +export function get${subSimpleClassName}(id: ${subPrimaryTsType}) { + return http.get<${subSimpleClassName}>(`/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/get?id=${id}`) +} + +/** 创建${subTable.classComment} */ +export function create${subSimpleClassName}(data: ${subSimpleClassName}) { + return http.post<${subPrimaryTsType}>('/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/create', data) +} + +/** 更新${subTable.classComment} */ +export function update${subSimpleClassName}(data: ${subSimpleClassName}) { + return http.put('/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/update', data) +} + +/** 删除${subTable.classComment} */ +export function delete${subSimpleClassName}(id: ${subPrimaryTsType}) { + return http.delete(`/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/delete?id=${id}`) +} +#elseif ($subTable.subJoinMany) + +/** 获取${subTable.classComment}列表 */ +export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}: ${subJoinTsType}) { + return http.get<${subSimpleClassName}[]>(`/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=#[[${]]#${subJoinColumn.javaField}#[[}]]#`) +} +#else + +/** 获取${subTable.classComment} */ +export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}: ${subJoinTsType}) { + return http.get<${subSimpleClassName} | null>(`/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=#[[${]]#${subJoinColumn.javaField}#[[}]]#`) +} +#end +#end diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/components/breadcrumb_tree.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/components/breadcrumb_tree.vue.vm new file mode 100644 index 0000000..e79d65a --- /dev/null +++ b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/components/breadcrumb_tree.vue.vm @@ -0,0 +1,87 @@ + + + diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/components/search-form.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/components/search-form.vue.vm index 0aa06fd..483af6b 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/components/search-form.vue.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/components/search-form.vue.vm @@ -145,7 +145,7 @@ const formData = reactive({ #else ${column.javaField}: undefined as number | undefined, #end - #elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte") + #elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "float" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte") ${column.javaField}: undefined as number | undefined, #elseif(${javaType} == "boolean") ${column.javaField}: undefined as boolean | undefined, @@ -174,7 +174,7 @@ const placeholder = computed(() => { conditions.push(`${comment}:#[[${]]#getDictLabel(DICT_TYPE.${dictType.toUpperCase()}, formData.${javaField})#[[}]]#`) } #else - #if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte" || ${javaType} == "boolean") + #if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "float" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte" || ${javaType} == "boolean") if (formData.${javaField} !== undefined) { conditions.push(`${comment}:#[[${]]#formData.${javaField}#[[}]]#`) } diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/detail/index.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/detail/index.vue.vm index bde41c2..817ab5e 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/detail/index.vue.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/detail/index.vue.vm @@ -39,7 +39,7 @@ 删除 @@ -49,6 +49,12 @@ diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/detail_master.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/detail_master.vue.vm new file mode 100644 index 0000000..88d1767 --- /dev/null +++ b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/detail_master.vue.vm @@ -0,0 +1,239 @@ + + + diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/form/index.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/form/index.vue.vm index 6c13045..b98423d 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/form/index.vue.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/form/index.vue.vm @@ -12,34 +12,48 @@ #foreach($column in $columns) - #if (($column.createOperation || $column.updateOperation) && !$column.primaryKey) - #set ($dictType = $column.dictType) - #set ($javaField = $column.javaField) - #set ($javaType = $column.javaType) - #set ($comment = $column.columnComment) - #set ($dictMethod = "getDictOptions") - #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short") - #set ($dictMethod = "getIntDictOptions") - #elseif ($javaType == "String") - #set ($dictMethod = "getStrDictOptions") - #elseif ($javaType == "Boolean") - #set ($dictMethod = "getBoolDictOptions") - #end - ## 下拉选择(字典)→ yd-form-picker,内部按 dictType 解析选项,适合选项较多 - #if ($column.htmlType == "select" && $dictType && "" != $dictType) +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey) +#set ($dictType = $column.dictType) +#set ($javaField = $column.javaField) +#set ($javaType = $column.javaType) +#set ($comment = $column.columnComment) +#set ($dictMethod = "getDictOptions") +#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short") +#set ($dictMethod = "getIntDictOptions") +#elseif ($javaType == "String") +#set ($dictMethod = "getStrDictOptions") +#elseif ($javaType == "Boolean") +#set ($dictMethod = "getBoolDictOptions") +#end +## 树表的父级选择 +#if ($table.templateType == 2 && $column.id == $treeParentColumn.id) + +## 下拉选择(字典)→ yd-form-picker,内部按 dictType 解析选项,适合选项较多 +#elseif ($column.htmlType == "select" && $dictType && "" != $dictType) - ## 单选(字典)→ radio-group,适合选项较少 - #elseif ($column.htmlType == "radio" && $dictType && "" != $dictType) +## 单选(字典)→ radio-group,适合选项较少 +#elseif ($column.htmlType == "radio" && $dictType && "" != $dictType) - ## 数字类型(无字典) - #elseif (${javaType.toLowerCase()} == "long" || ${javaType.toLowerCase()} == "integer" || ${javaType.toLowerCase()} == "short" || ${javaType.toLowerCase()} == "double" || ${javaType.toLowerCase()} == "bigdecimal" || ${javaType.toLowerCase()} == "byte") +## 数字类型(无字典) +#elseif (${javaType.toLowerCase()} == "long" || ${javaType.toLowerCase()} == "integer" || ${javaType.toLowerCase()} == "short" || ${javaType.toLowerCase()} == "float" || ${javaType.toLowerCase()} == "double" || ${javaType.toLowerCase()} == "bigdecimal" || ${javaType.toLowerCase()} == "byte") - ## 布尔类型 - #elseif (${javaType.toLowerCase()} == "boolean") +## 布尔类型 +#elseif (${javaType.toLowerCase()} == "boolean") - ## 日期时间类型 - #elseif (${javaType.toLowerCase()} == "date" || ${javaType.toLowerCase()} == "localdate" || ${javaType.toLowerCase()} == "localdatetime") - #set ($pickerType = "date") - #if (${javaType.toLowerCase()} == "localdatetime") - #set ($pickerType = "datetime") - #end +## 日期时间类型 +#elseif (${javaType.toLowerCase()} == "date" || ${javaType.toLowerCase()} == "localdate" || ${javaType.toLowerCase()} == "localdatetime") +#set ($pickerType = "date") +#if (${javaType.toLowerCase()} == "localdatetime") +#set ($pickerType = "datetime") +#end - ## 文本域 - #elseif ($column.htmlType == "textarea") +## 文本域 +#elseif ($column.htmlType == "textarea" || $column.htmlType == "editor") - ## 图片上传 → yd-upload-img(包 wd-form-item 才能注册到 wd-form 做校验/错误展示) - #elseif ($column.htmlType == "imageUpload") +## 图片上传 → yd-upload-img(包 wd-form-item 才能注册到 wd-form 做校验/错误展示) +#elseif ($column.htmlType == "imageUpload") - ## 文件上传 → yd-upload-file - #elseif ($column.htmlType == "fileUpload") +## 文件上传 → yd-upload-file +#elseif ($column.htmlType == "fileUpload") - + - ## 默认:文本输入 - #else +## 默认:文本输入 +#else - #end - #end +#end +#end #end +#if ($table.templateType == 10 || $table.templateType == 12) +#foreach ($subTable in $subTables) +#set ($index = $foreach.count - 1) +#set ($subColumns = $subColumnsList.get($index)) +#set ($subJoinColumn = $subJoinColumns.get($index)) +#set ($subClassNameVar = $subClassNameVars.get($index)) + + + + ${subTable.classComment} +#if ($subTable.subJoinMany) + + 添加 + +#else + + 添加 + +#end + + +#if ($subTable.subJoinMany) + + 暂无${subTable.classComment} + + + + + 删除 + + +#foreach ($column in $subColumns) +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id) +#set ($javaType = $column.javaType.toLowerCase()) +#set ($dictMethod = "getDictOptions") +#if ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short") +#set ($dictMethod = "getIntDictOptions") +#elseif ($column.javaType == "String") +#set ($dictMethod = "getStrDictOptions") +#elseif ($column.javaType == "Boolean") +#set ($dictMethod = "getBoolDictOptions") +#end +#if ($column.htmlType == "select" && $column.dictType && "" != $column.dictType) + +#elseif ($column.htmlType == "radio" && $column.dictType && "" != $column.dictType) + + + + {{ dict.label }} + + + +#elseif ($javaType == "boolean") + + + +#elseif ($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte") + + + +#elseif ($javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime") +#set ($subPickerType = "date") +#if ($javaType == "localdatetime") +#set ($subPickerType = "datetime") +#end + + +#elseif ($column.htmlType == "textarea" || $column.htmlType == "editor") + + + +#elseif ($column.htmlType == "imageUpload") + + + +#elseif ($column.htmlType == "fileUpload") + + + +#else + + + +#end +#end +#end + +#else + + 暂无${subTable.classComment} + + +#foreach ($column in $subColumns) +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id) +#set ($javaType = $column.javaType.toLowerCase()) +#set ($dictMethod = "getDictOptions") +#if ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short") +#set ($dictMethod = "getIntDictOptions") +#elseif ($column.javaType == "String") +#set ($dictMethod = "getStrDictOptions") +#elseif ($column.javaType == "Boolean") +#set ($dictMethod = "getBoolDictOptions") +#end +#if ($column.htmlType == "select" && $column.dictType && "" != $column.dictType) + +#elseif ($column.htmlType == "radio" && $column.dictType && "" != $column.dictType) + + + + {{ dict.label }} + + + +#elseif ($javaType == "boolean") + + + +#elseif ($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte") + + + +#elseif ($javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime") +#set ($subPickerType = "date") +#if ($javaType == "localdatetime") +#set ($subPickerType = "datetime") +#end + + +#elseif ($column.htmlType == "textarea" || $column.htmlType == "editor") + + + +#elseif ($column.htmlType == "imageUpload") + + + +#elseif ($column.htmlType == "fileUpload") + + + +#else + + + +#end +#end +#end + +#end + +#end +#end @@ -146,7 +434,7 @@ diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/form_sub_erp.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/form_sub_erp.vue.vm new file mode 100644 index 0000000..5c33246 --- /dev/null +++ b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/form_sub_erp.vue.vm @@ -0,0 +1,250 @@ +#set ($subTable = $subTables.get($subIndex)) +#set ($subColumns = $subColumnsList.get($subIndex)) +#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) +#set ($subPrimaryColumn = $subPrimaryColumns.get($subIndex)) +#set ($subJoinColumn = $subJoinColumns.get($subIndex)) +#set ($subPrimaryJavaType = $subPrimaryColumn.javaType.toLowerCase()) +#if(${subPrimaryJavaType} == "long" || ${subPrimaryJavaType} == "integer" || ${subPrimaryJavaType} == "short" || ${subPrimaryJavaType} == "float" || ${subPrimaryJavaType} == "double" || ${subPrimaryJavaType} == "bigdecimal" || ${subPrimaryJavaType} == "byte") +#set ($subPrimaryTsType = "number") +#else +#set ($subPrimaryTsType = "string") +#end +#set ($subJoinJavaType = $subJoinColumn.javaType.toLowerCase()) +#if(${subJoinJavaType} == "long" || ${subJoinJavaType} == "integer" || ${subJoinJavaType} == "short" || ${subJoinJavaType} == "float" || ${subJoinJavaType} == "double" || ${subJoinJavaType} == "bigdecimal" || ${subJoinJavaType} == "byte") +#set ($subJoinTsType = "number") +#else +#set ($subJoinTsType = "string") +#end +#set ($hasDict = 0) +#set ($hasBoolDict = 0) +#set ($hasDate = 0) +#set ($hasDateTime = 0) +#foreach ($column in $subColumns) +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id) +#if (($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType) +#set ($hasDict = 1) +#if ($column.javaType == "Boolean") +#set ($hasBoolDict = 1) +#end +#end +#if (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate") +#set ($hasDate = 1) +#end +#if (${column.javaType.toLowerCase()} == "localdatetime") +#set ($hasDateTime = 1) +#end +#end +#end + + + diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/index_tree.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/index_tree.vue.vm new file mode 100644 index 0000000..11c8be5 --- /dev/null +++ b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/index_tree.vue.vm @@ -0,0 +1,151 @@ + + + diff --git a/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineAbstractTest.java b/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineAbstractTest.java index ba0850a..3dee985 100644 --- a/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineAbstractTest.java +++ b/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineAbstractTest.java @@ -157,13 +157,17 @@ public abstract class CodegenEngineAbstractTest extends BaseMockitoUnitTest { String lastFilePath = StrUtil.subAfter(filePath, '/', true); String ext = StrUtil.subAfter(lastFilePath, '.', true); String name = StrUtil.subBefore(lastFilePath, '.', true); - String contentPath = ext + '/' + name; - // 同名文件(如 index.vue 同时出现在 列表/form/detail)会撞名,撞名时前缀补上级目录区分;仍撞则加序号兜底,避免快照互相覆盖 + String parentPath = StrUtil.subBefore(filePath, '/' + lastFilePath, true); + String parentDir = StrUtil.subAfter(parentPath, '/', true); + // index.vue 按页面语义稳定归档,避免结果迭代顺序导致列表页和表单页的快照路径互换 + String contentPath = "index".equals(name) && ("form".equals(parentDir) || "detail".equals(parentDir)) + ? ext + '/' + parentDir + '/' + name : ext + '/' + name; + // ERP 多个子表的 form/index.vue 仍可能撞名,继续补充祖父目录区分 if (usedContentPaths.contains(contentPath)) { - String parentDir = StrUtil.subAfter(StrUtil.subBefore(filePath, '/' + lastFilePath, true), '/', true); - contentPath = ext + '/' + parentDir + '/' + name; + String grandParentDir = StrUtil.subAfter(StrUtil.subBefore(parentPath, '/' + parentDir, true), '/', true); + contentPath = ext + '/' + grandParentDir + '/' + parentDir + '/' + name; for (int i = 2; usedContentPaths.contains(contentPath); i++) { - contentPath = ext + '/' + parentDir + '/' + name + '-' + i; + contentPath = ext + '/' + grandParentDir + '/' + parentDir + '/' + name + '-' + i; } } usedContentPaths.add(contentPath); diff --git a/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineUniappTest.java b/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineUniappTest.java index 958afe2..fbb0d60 100644 --- a/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineUniappTest.java +++ b/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineUniappTest.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.infra.service.codegen.inner; +import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO; import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO; import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum; @@ -7,10 +8,14 @@ import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum; import com.baomidou.mybatisplus.annotation.DbType; import org.junit.jupiter.api.Test; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -59,4 +64,315 @@ public class CodegenEngineUniappTest extends CodegenEngineAbstractTest { assertTrue(searchForm.contains("sex: undefined as string | undefined,")); } + @Test + public void testExecute_tree() { + // 准备参数 + CodegenTableDO table = getTable("category") + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType()) + .setTemplateType(CodegenTemplateTypeEnum.TREE.getType()); + List columns = getColumnList("category"); + + // 调用 + Map result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null); + // 断言 + assertResult(result, "/vue3_admin_uniapp_tree"); + } + + @Test + public void testExecute_semantics() { + // 准备参数 + CodegenTableDO table = getTable("student") + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType()) + .setTemplateType(CodegenTemplateTypeEnum.ONE.getType()); + List columns = getColumnList("student"); + columns.stream().filter(column -> "memo".equals(column.getJavaField())).findFirst().orElseThrow() + .setJavaType("Float").setHtmlType("input"); + + // 调用 + Map result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null); + // 断言 + String api = result.get("yudao-ui-admin-uniapp/src/api/infra/demo/index.ts"); + assertTrue(api.contains("birthday: number")); + assertTrue(api.contains("createTime?: number")); + assertTrue(api.contains("memo: number")); + assertFalse(api.contains("createTime?: Date")); + String form = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue"); + assertTrue(form.contains("v-model=\"formData.memo\"")); + assertTrue(form.contains(":precision=\"2\"")); + String detail = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue"); + assertTrue(detail.contains("type=\"danger\"")); + assertFalse(detail.contains("type=\"error\"")); + } + + @Test + public void testExecute_stringPrimaryKey() { + // 准备主表 + CodegenTableDO table = getTable("student") + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType()) + .setTemplateType(CodegenTemplateTypeEnum.MASTER_ERP.getType()); + List columns = getColumnList("student"); + columns.stream().filter(CodegenColumnDO::getPrimaryKey).findFirst().orElseThrow().setJavaType("String"); + // 准备子表 + CodegenTableDO contactTable = getTable("contact") + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType()) + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType()) + .setSubJoinColumnId(100L).setSubJoinMany(true); + List contactColumns = getColumnList("contact"); + contactColumns.stream().filter(CodegenColumnDO::getPrimaryKey).findFirst().orElseThrow().setJavaType("String"); + contactColumns.stream().filter(column -> Objects.equals(column.getId(), 100L)) + .findFirst().orElseThrow().setJavaType("String"); + + // 调用 + Map result = codegenEngine.execute(DbType.MYSQL, table, columns, + List.of(contactTable), List.of(contactColumns)); + // 断言 + String api = result.get("yudao-ui-admin-uniapp/src/api/infra/demo/index.ts"); + assertTrue(api.contains("return http.post('/infra/student/create', data)")); + assertTrue(api.contains("studentId: string")); + assertTrue(api.contains("getStudentContact(id: string)")); + assertTrue(api.contains("deleteStudentContact(id: string)")); + String detail = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue"); + assertFalse(detail.contains("Number(props.id)")); + String subForm = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/student-contact/form/index.vue"); + assertTrue(subForm.contains("id?: string")); + assertTrue(subForm.contains("studentId: string")); + } + + @Test + public void testExecute_treeSearch() { + // 准备参数 + CodegenTableDO table = getTable("category") + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType()) + .setTemplateType(CodegenTemplateTypeEnum.TREE.getType()); + List columns = getColumnList("category"); + + // 调用 + Map result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null); + // 断言 + String index = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/index.vue"); + assertTrue(index.contains("currentParentId.value === 0\n ? list.value")); + assertFalse(index.contains("list.value.filter")); + String breadcrumb = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/components/breadcrumb.vue"); + assertTrue(breadcrumb.contains("modelValue: number")); + assertFalse(breadcrumb.contains("TreeValue")); + } + + @Test + public void testExecute_duplicateErpSymbol() { + // 准备主表 + CodegenTableDO table = getTable("student") + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType()) + .setTemplateType(CodegenTemplateTypeEnum.MASTER_ERP.getType()); + List columns = getColumnList("student"); + // 准备两个归一化名称相同的子表 + CodegenTableDO crmItemTable = getTable("contact").setModuleName("crm").setClassName("CrmOrderItem") + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType()) + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType()) + .setSubJoinColumnId(100L).setSubJoinMany(true); + CodegenTableDO erpItemTable = getTable("contact").setModuleName("erp").setClassName("ErpOrderItem") + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType()) + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType()) + .setSubJoinColumnId(100L).setSubJoinMany(true); + List crmItemColumns = getColumnList("contact"); + List erpItemColumns = getColumnList("contact"); + + // 调用 + ServiceException exception = assertThrows(ServiceException.class, + () -> codegenEngine.execute(DbType.MYSQL, table, columns, + Arrays.asList(crmItemTable, erpItemTable), Arrays.asList(crmItemColumns, erpItemColumns))); + // 断言 + assertTrue(exception.getMessage().contains("主子表规范化类名(OrderItem)重复")); + } + + @Test + public void testExecute_duplicateMasterSymbol() { + // 准备主表 + CodegenTableDO table = getTable("student") + .setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType()) + .setTemplateType(CodegenTemplateTypeEnum.MASTER_NORMAL.getType()); + List columns = getColumnList("student"); + // 准备两个规范化名称相同的子表 + CodegenTableDO crmItemTable = getTable("contact").setModuleName("crm").setClassName("CrmOrderItem") + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(true); + CodegenTableDO erpItemTable = getTable("contact").setModuleName("erp").setClassName("ErpOrderItem") + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(true); + List crmItemColumns = getColumnList("contact"); + List erpItemColumns = getColumnList("contact"); + + // 调用并断言 + ServiceException exception = assertThrows(ServiceException.class, + () -> codegenEngine.execute(DbType.MYSQL, table, columns, + Arrays.asList(crmItemTable, erpItemTable), Arrays.asList(crmItemColumns, erpItemColumns))); + assertTrue(exception.getMessage().contains("主子表规范化类名(OrderItem)重复")); + } + + @Test + public void testExecute_duplicateMasterFieldNormal() { + testExecute_duplicateMasterField(CodegenTemplateTypeEnum.MASTER_NORMAL); + } + + @Test + public void testExecute_duplicateMasterFieldInner() { + testExecute_duplicateMasterField(CodegenTemplateTypeEnum.MASTER_INNER); + } + + @Test + public void testExecute_duplicateMasterFieldWithMainNormal() { + testExecute_duplicateMasterFieldWithMain(CodegenTemplateTypeEnum.MASTER_NORMAL); + } + + @Test + public void testExecute_duplicateMasterFieldWithMainInner() { + testExecute_duplicateMasterFieldWithMain(CodegenTemplateTypeEnum.MASTER_INNER); + } + + @Test + public void testExecute_duplicateMasterFieldErp() { + // 准备主表 + CodegenTableDO table = getTable("student") + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType()) + .setTemplateType(CodegenTemplateTypeEnum.MASTER_ERP.getType()); + List columns = getColumnList("student"); + // 准备属性名同为 items 的子表:Item 一对多、Items 一对一 + CodegenTableDO crmItemTable = getTable("contact").setModuleName("crm").setClassName("CrmItem") + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(true); + CodegenTableDO erpItemsTable = getTable("contact").setModuleName("erp").setClassName("ErpItems") + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(false); + + // 调用 + Map result = codegenEngine.execute(DbType.MYSQL, table, columns, + Arrays.asList(crmItemTable, erpItemsTable), + Arrays.asList(getColumnList("contact"), getColumnList("contact"))); + // 断言 + assertTrue(result.containsKey("yudao-ui-admin-uniapp/src/pages-infra/demo/item/form/index.vue")); + assertTrue(result.containsKey("yudao-ui-admin-uniapp/src/pages-infra/demo/items/form/index.vue")); + } + + @Test + public void testRegisterGeneratedSource_duplicatePath() { + // 准备参数 + Map generatedSources = new HashMap<>(); + Map bindingMap = new HashMap<>(); + bindingMap.put("subIndex", 0); + CodegenEngine.registerGeneratedSource(generatedSources, "same.vue", "form_sub.vm", bindingMap); + bindingMap.put("subIndex", 1); + + // 调用 + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> CodegenEngine.registerGeneratedSource(generatedSources, "same.vue", "form_sub.vm", bindingMap)); + // 断言 + assertTrue(exception.getMessage().contains("生成文件路径重复:same.vue")); + assertTrue(exception.getMessage().contains("form_sub.vm[subIndex=0]")); + assertTrue(exception.getMessage().contains("form_sub.vm[subIndex=1]")); + } + + @Test + public void testExecute_masterNormal() { + testExecute_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "/vue3_admin_uniapp_master_normal"); + } + + @Test + public void testExecute_masterInner() { + testExecute_master(CodegenTemplateTypeEnum.MASTER_INNER, "/vue3_admin_uniapp_master_inner"); + } + + @Test + public void testExecute_masterErp() { + testExecute_master(CodegenTemplateTypeEnum.MASTER_ERP, "/vue3_admin_uniapp_master_erp"); + } + + private void testExecute_duplicateMasterField(CodegenTemplateTypeEnum templateType) { + // 准备主表 + CodegenTableDO table = getTable("student") + .setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType()) + .setTemplateType(templateType.getType()); + List columns = getColumnList("student"); + // 准备属性名同为 items 的子表:Item 一对多、Items 一对一 + CodegenTableDO crmItemTable = getTable("contact").setModuleName("crm").setClassName("CrmItem") + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(true); + CodegenTableDO erpItemsTable = getTable("contact").setModuleName("erp").setClassName("ErpItems") + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(false); + List crmItemColumns = getColumnList("contact"); + List erpItemsColumns = getColumnList("contact"); + + // 调用并断言 + ServiceException exception = assertThrows(ServiceException.class, + () -> codegenEngine.execute(DbType.MYSQL, table, columns, + Arrays.asList(crmItemTable, erpItemsTable), Arrays.asList(crmItemColumns, erpItemsColumns))); + assertTrue(exception.getMessage().contains("主子表属性名(items)重复")); + } + + private void testExecute_duplicateMasterFieldWithMain(CodegenTemplateTypeEnum templateType) { + // 准备主表 + CodegenTableDO table = getTable("student") + .setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType()) + .setTemplateType(templateType.getType()); + List columns = getColumnList("student"); + columns.stream().filter(column -> "memo".equals(column.getJavaField())).findFirst().orElseThrow() + .setJavaField("items"); + // 准备一对多子表 Item,生成属性名 items + CodegenTableDO itemTable = getTable("contact").setModuleName("crm").setClassName("CrmItem") + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(true); + + // 调用并断言 + ServiceException exception = assertThrows(ServiceException.class, + () -> codegenEngine.execute(DbType.MYSQL, table, columns, + List.of(itemTable), List.of(getColumnList("contact")))); + assertTrue(exception.getMessage().contains("主子表属性名(items)重复")); + } + + private void testExecute_master(CodegenTemplateTypeEnum templateType, String path) { + // 准备主表 + CodegenTableDO table = getTable("student") + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType()) + .setTemplateType(templateType.getType()); + List columns = getColumnList("student"); + // 准备一对多子表 + CodegenTableDO contactTable = getTable("contact") + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType()) + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType()) + .setSubJoinColumnId(100L).setSubJoinMany(true); + List contactColumns = getColumnList("contact"); + // 准备一对一子表 + CodegenTableDO teacherTable = getTable("teacher") + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType()) + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType()) + .setSubJoinColumnId(200L).setSubJoinMany(false); + List teacherColumns = getColumnList("teacher"); + + // 调用 + Map result = codegenEngine.execute(DbType.MYSQL, table, columns, + Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns)); + // 断言 + assertResult(result, path); + if (templateType != CodegenTemplateTypeEnum.MASTER_ERP) { + String api = result.get("yudao-ui-admin-uniapp/src/api/infra/demo/index.ts"); + assertTrue(api.contains("studentTeacher?: StudentTeacher | null")); + String form = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue"); + assertTrue(form.contains("ref | null>(null)")); + assertTrue(form.contains("studentTeacher.value = studentTeacherData || null")); + assertTrue(form.contains(":model-value=\"item.birthday || Date.now()\"")); + assertTrue(form.contains("v-model=\"item.sex\"")); + assertTrue(form.contains(" { + + default PageResult selectPage(PageParam reqVO, Long studentId) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eq(InfraStudentContactDO::getStudentId, studentId) + .orderByDesc(InfraStudentContactDO::getId)); + } + + default int deleteByStudentId(Long studentId) { + return delete(InfraStudentContactDO::getStudentId, studentId); + } + + default int deleteByStudentIds(List studentIds) { + return deleteBatch(InfraStudentContactDO::getStudentId, studentIds); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentController b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentController new file mode 100644 index 0000000..02bd597 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentController @@ -0,0 +1,210 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo; + +import org.springframework.web.bind.annotation.*; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.security.access.prepost.PreAuthorize; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; + +import jakarta.validation.constraints.*; +import jakarta.validation.*; +import jakarta.servlet.http.*; +import java.util.*; +import java.io.IOException; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*; + +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; +import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService; + +@Tag(name = "管理后台 - 学生") +@RestController +@RequestMapping("/infra/student") +@Validated +public class InfraStudentController { + + @Resource + private InfraStudentService studentService; + + @PostMapping("/create") + @Operation(summary = "创建学生") + @PreAuthorize("@ss.hasPermission('infra:student:create')") + public CommonResult createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) { + return success(studentService.createStudent(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新学生") + @PreAuthorize("@ss.hasPermission('infra:student:update')") + public CommonResult updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) { + studentService.updateStudent(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除学生") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:student:delete')") + public CommonResult deleteStudent(@RequestParam("id") Long id) { + studentService.deleteStudent(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Parameter(name = "ids", description = "编号", required = true) + @Operation(summary = "批量删除学生") + @PreAuthorize("@ss.hasPermission('infra:student:delete')") + public CommonResult deleteStudentList(@RequestParam("ids") List ids) { + studentService.deleteStudentListByIds(ids); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得学生") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:student:query')") + public CommonResult getStudent(@RequestParam("id") Long id) { + InfraStudentDO student = studentService.getStudent(id); + return success(BeanUtils.toBean(student, InfraStudentRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得学生分页") + @PreAuthorize("@ss.hasPermission('infra:student:query')") + public CommonResult> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) { + PageResult pageResult = studentService.getStudentPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出学生 Excel") + @PreAuthorize("@ss.hasPermission('infra:student:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = studentService.getStudentPage(pageReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class, + BeanUtils.toBean(list, InfraStudentRespVO.class)); + } + + // ==================== 子表(学生联系人) ==================== + + @GetMapping("/student-contact/page") + @Operation(summary = "获得学生联系人分页") + @Parameter(name = "studentId", description = "学生编号") + @PreAuthorize("@ss.hasPermission('infra:student:query')") + public CommonResult> getStudentContactPage(PageParam pageReqVO, + @RequestParam("studentId") Long studentId) { + return success(studentService.getStudentContactPage(pageReqVO, studentId)); + } + + @PostMapping("/student-contact/create") + @Operation(summary = "创建学生联系人") + @PreAuthorize("@ss.hasPermission('infra:student:create')") + public CommonResult createStudentContact(@Valid @RequestBody InfraStudentContactDO studentContact) { + return success(studentService.createStudentContact(studentContact)); + } + + @PutMapping("/student-contact/update") + @Operation(summary = "更新学生联系人") + @PreAuthorize("@ss.hasPermission('infra:student:update')") + public CommonResult updateStudentContact(@Valid @RequestBody InfraStudentContactDO studentContact) { + studentService.updateStudentContact(studentContact); + return success(true); + } + + @DeleteMapping("/student-contact/delete") + @Parameter(name = "id", description = "编号", required = true) + @Operation(summary = "删除学生联系人") + @PreAuthorize("@ss.hasPermission('infra:student:delete')") + public CommonResult deleteStudentContact(@RequestParam("id") Long id) { + studentService.deleteStudentContact(id); + return success(true); + } + + @DeleteMapping("/student-contact/delete-list") + @Parameter(name = "ids", description = "编号", required = true) + @Operation(summary = "批量删除学生联系人") + @PreAuthorize("@ss.hasPermission('infra:student:delete')") + public CommonResult deleteStudentContactList(@RequestParam("ids") List ids) { + studentService.deleteStudentContactListByIds(ids); + return success(true); + } + + @GetMapping("/student-contact/get") + @Operation(summary = "获得学生联系人") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:student:query')") + public CommonResult getStudentContact(@RequestParam("id") Long id) { + return success(studentService.getStudentContact(id)); + } + + // ==================== 子表(学生班主任) ==================== + + @GetMapping("/student-teacher/page") + @Operation(summary = "获得学生班主任分页") + @Parameter(name = "studentId", description = "学生编号") + @PreAuthorize("@ss.hasPermission('infra:student:query')") + public CommonResult> getStudentTeacherPage(PageParam pageReqVO, + @RequestParam("studentId") Long studentId) { + return success(studentService.getStudentTeacherPage(pageReqVO, studentId)); + } + + @PostMapping("/student-teacher/create") + @Operation(summary = "创建学生班主任") + @PreAuthorize("@ss.hasPermission('infra:student:create')") + public CommonResult createStudentTeacher(@Valid @RequestBody InfraStudentTeacherDO studentTeacher) { + return success(studentService.createStudentTeacher(studentTeacher)); + } + + @PutMapping("/student-teacher/update") + @Operation(summary = "更新学生班主任") + @PreAuthorize("@ss.hasPermission('infra:student:update')") + public CommonResult updateStudentTeacher(@Valid @RequestBody InfraStudentTeacherDO studentTeacher) { + studentService.updateStudentTeacher(studentTeacher); + return success(true); + } + + @DeleteMapping("/student-teacher/delete") + @Parameter(name = "id", description = "编号", required = true) + @Operation(summary = "删除学生班主任") + @PreAuthorize("@ss.hasPermission('infra:student:delete')") + public CommonResult deleteStudentTeacher(@RequestParam("id") Long id) { + studentService.deleteStudentTeacher(id); + return success(true); + } + + @DeleteMapping("/student-teacher/delete-list") + @Parameter(name = "ids", description = "编号", required = true) + @Operation(summary = "批量删除学生班主任") + @PreAuthorize("@ss.hasPermission('infra:student:delete')") + public CommonResult deleteStudentTeacherList(@RequestParam("ids") List ids) { + studentService.deleteStudentTeacherListByIds(ids); + return success(true); + } + + @GetMapping("/student-teacher/get") + @Operation(summary = "获得学生班主任") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:student:query')") + public CommonResult getStudentTeacher(@RequestParam("id") Long id) { + return success(studentService.getStudentTeacher(id)); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentDO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentDO new file mode 100644 index 0000000..f39fef2 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentDO @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.demo; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; + +/** + * 学生 DO + * + * @author 芋道源码 + */ +@TableName("infra_student") +@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InfraStudentDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 名字 + */ + private String name; + /** + * 简介 + */ + private String description; + /** + * 出生日期 + */ + private LocalDateTime birthday; + /** + * 性别 + * + * 枚举 {@link TODO system_user_sex 对应的类} + */ + private Integer sex; + /** + * 是否有效 + * + * 枚举 {@link TODO infra_boolean_string 对应的类} + */ + private Boolean enabled; + /** + * 头像 + */ + private String avatar; + /** + * 附件 + */ + private String video; + /** + * 备注 + */ + private String memo; + + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentMapper new file mode 100644 index 0000000..34e70a0 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentMapper @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo; + +import java.util.*; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import org.apache.ibatis.annotations.Mapper; +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; + +/** + * 学生 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface InfraStudentMapper extends BaseMapperX { + + default PageResult selectPage(InfraStudentPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(InfraStudentDO::getName, reqVO.getName()) + .eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday()) + .eqIfPresent(InfraStudentDO::getSex, reqVO.getSex()) + .eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled()) + .betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(InfraStudentDO::getId)); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentPageReqVO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentPageReqVO new file mode 100644 index 0000000..52d7a23 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentPageReqVO @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo; + +import lombok.*; +import java.util.*; +import io.swagger.v3.oas.annotations.media.Schema; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 学生分页 Request VO") +@Data +public class InfraStudentPageReqVO extends PageParam { + + @Schema(description = "名字", example = "芋头") + private String name; + + @Schema(description = "出生日期") + private LocalDateTime birthday; + + @Schema(description = "性别", example = "1") + private Integer sex; + + @Schema(description = "是否有效", example = "true") + private Boolean enabled; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentRespVO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentRespVO new file mode 100644 index 0000000..12166f4 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentRespVO @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; +import cn.idev.excel.annotation.*; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; + +@Schema(description = "管理后台 - 学生 Response VO") +@Data +@ExcelIgnoreUnannotated +public class InfraStudentRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("编号") + private Long id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头") + @ExcelProperty("名字") + private String name; + + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍") + @ExcelProperty("简介") + private String description; + + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("出生日期") + private LocalDateTime birthday; + + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "性别", converter = DictConvert.class) + @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 + private Integer sex; + + @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @ExcelProperty(value = "是否有效", converter = DictConvert.class) + @DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 + private Boolean enabled; + + @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + @ExcelProperty("头像") + private String avatar; + + @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4") + @ExcelProperty("附件") + private String video; + + @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注") + @ExcelProperty("备注") + private String memo; + + @Schema(description = "创建时间") + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentSaveReqVO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentSaveReqVO new file mode 100644 index 0000000..2378872 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentSaveReqVO @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import jakarta.validation.constraints.*; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; + +@Schema(description = "管理后台 - 学生新增/修改 Request VO") +@Data +public class InfraStudentSaveReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头") + @NotEmpty(message = "名字不能为空") + private String name; + + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍") + @NotEmpty(message = "简介不能为空") + private String description; + + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "出生日期不能为空") + private LocalDateTime birthday; + + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "性别不能为空") + private Integer sex; + + @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否有效不能为空") + private Boolean enabled; + + @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + @NotEmpty(message = "头像不能为空") + private String avatar; + + @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4") + @NotEmpty(message = "附件不能为空") + private String video; + + @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注") + @NotEmpty(message = "备注不能为空") + private String memo; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentService b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentService new file mode 100644 index 0000000..fea24fa --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentService @@ -0,0 +1,160 @@ +package cn.iocoder.yudao.module.infra.service.demo; + +import java.util.*; +import jakarta.validation.*; +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; + +/** + * 学生 Service 接口 + * + * @author 芋道源码 + */ +public interface InfraStudentService { + + /** + * 创建学生 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createStudent(@Valid InfraStudentSaveReqVO createReqVO); + + /** + * 更新学生 + * + * @param updateReqVO 更新信息 + */ + void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO); + + /** + * 删除学生 + * + * @param id 编号 + */ + void deleteStudent(Long id); + + /** + * 批量删除学生 + * + * @param ids 编号 + */ + void deleteStudentListByIds(List ids); + + /** + * 获得学生 + * + * @param id 编号 + * @return 学生 + */ + InfraStudentDO getStudent(Long id); + + /** + * 获得学生分页 + * + * @param pageReqVO 分页查询 + * @return 学生分页 + */ + PageResult getStudentPage(InfraStudentPageReqVO pageReqVO); + + // ==================== 子表(学生联系人) ==================== + + /** + * 获得学生联系人分页 + * + * @param pageReqVO 分页查询 + * @param studentId 学生编号 + * @return 学生联系人分页 + */ + PageResult getStudentContactPage(PageParam pageReqVO, Long studentId); + + /** + * 创建学生联系人 + * + * @param studentContact 创建信息 + * @return 编号 + */ + Long createStudentContact(@Valid InfraStudentContactDO studentContact); + + /** + * 更新学生联系人 + * + * @param studentContact 更新信息 + */ + void updateStudentContact(@Valid InfraStudentContactDO studentContact); + + /** + * 删除学生联系人 + * + * @param id 编号 + */ + void deleteStudentContact(Long id); + + /** + * 批量删除学生联系人 + * + * @param ids 编号 + */ + void deleteStudentContactListByIds(List ids); + + /** + * 获得学生联系人 + * + * @param id 编号 + * @return 学生联系人 + */ + InfraStudentContactDO getStudentContact(Long id); + + // ==================== 子表(学生班主任) ==================== + + /** + * 获得学生班主任分页 + * + * @param pageReqVO 分页查询 + * @param studentId 学生编号 + * @return 学生班主任分页 + */ + PageResult getStudentTeacherPage(PageParam pageReqVO, Long studentId); + + /** + * 创建学生班主任 + * + * @param studentTeacher 创建信息 + * @return 编号 + */ + Long createStudentTeacher(@Valid InfraStudentTeacherDO studentTeacher); + + /** + * 更新学生班主任 + * + * @param studentTeacher 更新信息 + */ + void updateStudentTeacher(@Valid InfraStudentTeacherDO studentTeacher); + + /** + * 删除学生班主任 + * + * @param id 编号 + */ + void deleteStudentTeacher(Long id); + + /** + * 批量删除学生班主任 + * + * @param ids 编号 + */ + void deleteStudentTeacherListByIds(List ids); + + /** + * 获得学生班主任 + * + * @param id 编号 + * @return 学生班主任 + */ + InfraStudentTeacherDO getStudentTeacher(Long id); + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentServiceImpl b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentServiceImpl new file mode 100644 index 0000000..6c86267 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentServiceImpl @@ -0,0 +1,216 @@ +package cn.iocoder.yudao.module.infra.service.demo; + +import cn.hutool.core.collection.CollUtil; +import org.springframework.stereotype.Service; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; + +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; + +/** + * 学生 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class InfraStudentServiceImpl implements InfraStudentService { + + @Resource + private InfraStudentMapper studentMapper; + @Resource + private InfraStudentContactMapper studentContactMapper; + @Resource + private InfraStudentTeacherMapper studentTeacherMapper; + + @Override + public Long createStudent(InfraStudentSaveReqVO createReqVO) { + // 插入 + InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class); + studentMapper.insert(student); + + // 返回 + return student.getId(); + } + + @Override + public void updateStudent(InfraStudentSaveReqVO updateReqVO) { + // 校验存在 + validateStudentExists(updateReqVO.getId()); + // 更新 + InfraStudentDO updateObj = BeanUtils.toBean(updateReqVO, InfraStudentDO.class); + studentMapper.updateById(updateObj); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteStudent(Long id) { + // 校验存在 + validateStudentExists(id); + // 删除 + studentMapper.deleteById(id); + + // 删除子表 + deleteStudentContactByStudentId(id); + deleteStudentTeacherByStudentId(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteStudentListByIds(List ids) { + // 删除 + studentMapper.deleteByIds(ids); + + // 删除子表 + deleteStudentContactByStudentIds(ids); + deleteStudentTeacherByStudentIds(ids); + } + + + private void validateStudentExists(Long id) { + if (studentMapper.selectById(id) == null) { + throw exception(STUDENT_NOT_EXISTS); + } + } + + @Override + public InfraStudentDO getStudent(Long id) { + return studentMapper.selectById(id); + } + + @Override + public PageResult getStudentPage(InfraStudentPageReqVO pageReqVO) { + return studentMapper.selectPage(pageReqVO); + } + + // ==================== 子表(学生联系人) ==================== + + @Override + public PageResult getStudentContactPage(PageParam pageReqVO, Long studentId) { + return studentContactMapper.selectPage(pageReqVO, studentId); + } + + @Override + public Long createStudentContact(InfraStudentContactDO studentContact) { + studentContact.clean(); // 清理掉创建、更新时间等相关属性值 + studentContactMapper.insert(studentContact); + return studentContact.getId(); + } + + @Override + public void updateStudentContact(InfraStudentContactDO studentContact) { + // 校验存在 + validateStudentContactExists(studentContact.getId()); + // 更新 + studentContact.clean(); // 解决更新情况下:updateTime 不更新 + studentContactMapper.updateById(studentContact); + } + + @Override + public void deleteStudentContact(Long id) { + // 删除 + studentContactMapper.deleteById(id); + } + + @Override + public void deleteStudentContactListByIds(List ids) { + // 删除 + studentContactMapper.deleteByIds(ids); + } + + @Override + public InfraStudentContactDO getStudentContact(Long id) { + return studentContactMapper.selectById(id); + } + + private void validateStudentContactExists(Long id) { + if (studentContactMapper.selectById(id) == null) { + throw exception(STUDENT_CONTACT_NOT_EXISTS); + } + } + + private void deleteStudentContactByStudentId(Long studentId) { + studentContactMapper.deleteByStudentId(studentId); + } + + private void deleteStudentContactByStudentIds(List studentIds) { + studentContactMapper.deleteByStudentIds(studentIds); + } + + // ==================== 子表(学生班主任) ==================== + + @Override + public PageResult getStudentTeacherPage(PageParam pageReqVO, Long studentId) { + return studentTeacherMapper.selectPage(pageReqVO, studentId); + } + + @Override + public Long createStudentTeacher(InfraStudentTeacherDO studentTeacher) { + // 校验是否已经存在 + if (studentTeacherMapper.selectByStudentId(studentTeacher.getStudentId()) != null) { + throw exception(STUDENT_TEACHER_EXISTS); + } + // 插入 + studentTeacher.clean(); // 清理掉创建、更新时间等相关属性值 + studentTeacherMapper.insert(studentTeacher); + return studentTeacher.getId(); + } + + @Override + public void updateStudentTeacher(InfraStudentTeacherDO studentTeacher) { + // 校验存在 + validateStudentTeacherExists(studentTeacher.getId()); + // 更新 + studentTeacher.clean(); // 解决更新情况下:updateTime 不更新 + studentTeacherMapper.updateById(studentTeacher); + } + + @Override + public void deleteStudentTeacher(Long id) { + // 删除 + studentTeacherMapper.deleteById(id); + } + + @Override + public void deleteStudentTeacherListByIds(List ids) { + // 删除 + studentTeacherMapper.deleteByIds(ids); + } + + @Override + public InfraStudentTeacherDO getStudentTeacher(Long id) { + return studentTeacherMapper.selectById(id); + } + + private void validateStudentTeacherExists(Long id) { + if (studentTeacherMapper.selectById(id) == null) { + throw exception(STUDENT_TEACHER_NOT_EXISTS); + } + } + + private void deleteStudentTeacherByStudentId(Long studentId) { + studentTeacherMapper.deleteByStudentId(studentId); + } + + private void deleteStudentTeacherByStudentIds(List studentIds) { + studentTeacherMapper.deleteByStudentIds(studentIds); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentServiceImplTest b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentServiceImplTest new file mode 100644 index 0000000..f1d3bb9 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentServiceImplTest @@ -0,0 +1,145 @@ +package cn.iocoder.yudao.module.infra.service.demo; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import jakarta.annotation.Resource; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; + +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper; +import cn.iocoder.yudao.framework.common.pojo.PageResult; + +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Import; +import java.util.*; +import java.time.LocalDateTime; + +import static cn.hutool.core.util.RandomUtil.*; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * {@link InfraStudentServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(InfraStudentServiceImpl.class) +public class InfraStudentServiceImplTest extends BaseDbUnitTest { + + @Resource + private InfraStudentServiceImpl studentService; + + @Resource + private InfraStudentMapper studentMapper; + + @Test + public void testCreateStudent_success() { + // 准备参数 + InfraStudentSaveReqVO createReqVO = randomPojo(InfraStudentSaveReqVO.class).setId(null); + + // 调用 + Long studentId = studentService.createStudent(createReqVO); + // 断言 + assertNotNull(studentId); + // 校验记录的属性是否正确 + InfraStudentDO student = studentMapper.selectById(studentId); + assertPojoEquals(createReqVO, student, "id"); + } + + @Test + public void testUpdateStudent_success() { + // mock 数据 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class); + studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据 + // 准备参数 + InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class, o -> { + o.setId(dbStudent.getId()); // 设置更新的 ID + }); + + // 调用 + studentService.updateStudent(updateReqVO); + // 校验是否更新正确 + InfraStudentDO student = studentMapper.selectById(updateReqVO.getId()); // 获取最新的 + assertPojoEquals(updateReqVO, student); + } + + @Test + public void testUpdateStudent_notExists() { + // 准备参数 + InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> studentService.updateStudent(updateReqVO), STUDENT_NOT_EXISTS); + } + + @Test + public void testDeleteStudent_success() { + // mock 数据 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class); + studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbStudent.getId(); + + // 调用 + studentService.deleteStudent(id); + // 校验数据不存在了 + assertNull(studentMapper.selectById(id)); + } + + @Test + public void testDeleteStudent_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> studentService.deleteStudent(id), STUDENT_NOT_EXISTS); + } + + @Test + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 + public void testGetStudentPage() { + // mock 数据 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class, o -> { // 等会查询到 + o.setName(null); + o.setBirthday(null); + o.setSex(null); + o.setEnabled(null); + o.setCreateTime(null); + }); + studentMapper.insert(dbStudent); + // 测试 name 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setName(null))); + // 测试 birthday 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setBirthday(null))); + // 测试 sex 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setSex(null))); + // 测试 enabled 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setEnabled(null))); + // 测试 createTime 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setCreateTime(null))); + // 准备参数 + InfraStudentPageReqVO reqVO = new InfraStudentPageReqVO(); + reqVO.setName(null); + reqVO.setBirthday(null); + reqVO.setSex(null); + reqVO.setEnabled(null); + reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + + // 调用 + PageResult pageResult = studentService.getStudentPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbStudent, pageResult.getList().get(0)); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentTeacherDO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentTeacherDO new file mode 100644 index 0000000..c19cf9f --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentTeacherDO @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.demo; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; + +/** + * 学生班主任 DO + * + * @author 芋道源码 + */ +@TableName("infra_student_teacher") +@KeySequence("infra_student_teacher_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InfraStudentTeacherDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 学生编号 + */ + private Long studentId; + /** + * 名字 + */ + private String name; + /** + * 简介 + */ + private String description; + /** + * 出生日期 + */ + private LocalDateTime birthday; + /** + * 性别 + * + * 枚举 {@link TODO system_user_sex 对应的类} + */ + private Integer sex; + /** + * 是否有效 + * + * 枚举 {@link TODO infra_boolean_string 对应的类} + */ + private Boolean enabled; + /** + * 头像 + */ + private String avatar; + /** + * 附件 + */ + private String video; + /** + * 备注 + */ + private String memo; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentTeacherMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentTeacherMapper new file mode 100644 index 0000000..0e84a58 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentTeacherMapper @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo; + +import java.util.*; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 学生班主任 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface InfraStudentTeacherMapper extends BaseMapperX { + + default PageResult selectPage(PageParam reqVO, Long studentId) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eq(InfraStudentTeacherDO::getStudentId, studentId) + .orderByDesc(InfraStudentTeacherDO::getId)); + } + default InfraStudentTeacherDO selectByStudentId(Long studentId) { + return selectOne(InfraStudentTeacherDO::getStudentId, studentId); + } + + default int deleteByStudentId(Long studentId) { + return delete(InfraStudentTeacherDO::getStudentId, studentId); + } + + default int deleteByStudentIds(List studentIds) { + return deleteBatch(InfraStudentTeacherDO::getStudentId, studentIds); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/sql/h2 b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/sql/h2 new file mode 100644 index 0000000..6c1875f --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/sql/h2 @@ -0,0 +1,17 @@ +-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里 +CREATE TABLE IF NOT EXISTS "infra_student" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar NOT NULL, + "description" varchar NOT NULL, + "birthday" varchar NOT NULL, + "sex" int NOT NULL, + "enabled" bit NOT NULL, + "avatar" varchar NOT NULL, + "video" varchar NOT NULL, + "memo" varchar NOT NULL, + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY ("id") +) COMMENT '学生表'; + +-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里 +DELETE FROM "infra_student"; \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/sql/sql b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/sql/sql new file mode 100644 index 0000000..c0e5f74 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/sql/sql @@ -0,0 +1,54 @@ +-- 菜单 SQL +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status, component_name +) +VALUES ( + '学生管理', '', 2, 0, 888, + 'student', '', 'infra/demo/index', 0, 'InfraStudent' +); + +-- 按钮父菜单ID +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生查询', 'infra:student:query', 3, 1, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生创建', 'infra:student:create', 3, 2, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生更新', 'infra:student:update', 3, 3, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生删除', 'infra:student:delete', 3, 4, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生导出', 'infra:student:export', 3, 5, @parentId, + '', '', '', 0 +); \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/ts/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/ts/index new file mode 100644 index 0000000..0fd0ce5 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/ts/index @@ -0,0 +1,125 @@ +import type { PageParam, PageResult } from '@/http/types' +import { http } from '@/http/http' + +/** 学生联系人信息 */ +export interface StudentContact { + id?: number + studentId: number + name: string + description: string + birthday: number + sex: number + enabled: boolean + avatar: string + video?: string + memo: string + createTime?: number +} + +/** 学生班主任信息 */ +export interface StudentTeacher { + id?: number + studentId: number + name: string + description: string + birthday: number + sex: number + enabled: boolean + avatar: string + video?: string + memo: string + createTime?: number +} + +/** 学生信息 */ +export interface Student { + id?: number + name: string + description: string + birthday: number + sex: number + enabled: boolean + avatar: string + video: string + memo: string + createTime?: number +} + +/** 获取学生分页列表 */ +export function getStudentPage(params: PageParam) { + return http.get>('/infra/student/page', params) +} + +/** 获取学生详情 */ +export function getStudent(id: number) { + return http.get(`/infra/student/get?id=${id}`) +} + +/** 创建学生 */ +export function createStudent(data: Student) { + return http.post('/infra/student/create', data) +} + +/** 更新学生 */ +export function updateStudent(data: Student) { + return http.put('/infra/student/update', data) +} + +/** 删除学生 */ +export function deleteStudent(id: number) { + return http.delete(`/infra/student/delete?id=${id}`) +} + +// ==================== 子表:学生联系人 ==================== + +/** 获取学生联系人分页列表 */ +export function getStudentContactPage(params: PageParam & { studentId: number }) { + return http.get>('/infra/student/student-contact/page', params) +} + +/** 获取学生联系人详情 */ +export function getStudentContact(id: number) { + return http.get(`/infra/student/student-contact/get?id=${id}`) +} + +/** 创建学生联系人 */ +export function createStudentContact(data: StudentContact) { + return http.post('/infra/student/student-contact/create', data) +} + +/** 更新学生联系人 */ +export function updateStudentContact(data: StudentContact) { + return http.put('/infra/student/student-contact/update', data) +} + +/** 删除学生联系人 */ +export function deleteStudentContact(id: number) { + return http.delete(`/infra/student/student-contact/delete?id=${id}`) +} + +// ==================== 子表:学生班主任 ==================== + +/** 获取学生班主任分页列表 */ +export function getStudentTeacherPage(params: PageParam & { studentId: number }) { + return http.get>('/infra/student/student-teacher/page', params) +} + +/** 获取学生班主任详情 */ +export function getStudentTeacher(id: number) { + return http.get(`/infra/student/student-teacher/get?id=${id}`) +} + +/** 创建学生班主任 */ +export function createStudentTeacher(data: StudentTeacher) { + return http.post('/infra/student/student-teacher/create', data) +} + +/** 更新学生班主任 */ +export function updateStudentTeacher(data: StudentTeacher) { + return http.put('/infra/student/student-teacher/update', data) +} + +/** 删除学生班主任 */ +export function deleteStudentTeacher(id: number) { + return http.delete(`/infra/student/student-teacher/delete?id=${id}`) +} diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/detail/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/detail/index new file mode 100644 index 0000000..dfd1881 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/detail/index @@ -0,0 +1,342 @@ + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/form/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/form/index new file mode 100644 index 0000000..36cd18d --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/form/index @@ -0,0 +1,193 @@ + + + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/index new file mode 100644 index 0000000..acb1b87 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/index @@ -0,0 +1,169 @@ + + + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/search-form b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/search-form new file mode 100644 index 0000000..0928d14 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/search-form @@ -0,0 +1,116 @@ + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/student-contact/form/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/student-contact/form/index new file mode 100644 index 0000000..9b976da --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/student-contact/form/index @@ -0,0 +1,167 @@ + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/student-teacher/form/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/student-teacher/form/index new file mode 100644 index 0000000..1d2639f --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/student-teacher/form/index @@ -0,0 +1,167 @@ + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/xml/InfraStudentMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/xml/InfraStudentMapper new file mode 100644 index 0000000..155aa5c --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/xml/InfraStudentMapper @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/assert.json b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/assert.json new file mode 100644 index 0000000..0a37471 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/assert.json @@ -0,0 +1,67 @@ +[ { + "contentPath" : "java/InfraStudentPageReqVO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java" +}, { + "contentPath" : "java/InfraStudentRespVO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java" +}, { + "contentPath" : "java/InfraStudentSaveReqVO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java" +}, { + "contentPath" : "java/InfraStudentController", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java" +}, { + "contentPath" : "java/InfraStudentDO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java" +}, { + "contentPath" : "java/InfraStudentContactDO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java" +}, { + "contentPath" : "java/InfraStudentTeacherDO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java" +}, { + "contentPath" : "java/InfraStudentMapper", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java" +}, { + "contentPath" : "java/InfraStudentContactMapper", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java" +}, { + "contentPath" : "java/InfraStudentTeacherMapper", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java" +}, { + "contentPath" : "xml/InfraStudentMapper", + "filePath" : "yudao-module-infra/src/main/resources/mapper/demo/InfraStudentMapper.xml" +}, { + "contentPath" : "java/InfraStudentServiceImpl", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java" +}, { + "contentPath" : "java/InfraStudentService", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java" +}, { + "contentPath" : "java/InfraStudentServiceImplTest", + "filePath" : "yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java" +}, { + "contentPath" : "java/ErrorCodeConstants_手动操作", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java" +}, { + "contentPath" : "sql/sql", + "filePath" : "sql/sql.sql" +}, { + "contentPath" : "sql/h2", + "filePath" : "sql/h2.sql" +}, { + "contentPath" : "ts/index", + "filePath" : "yudao-ui-admin-uniapp/src/api/infra/demo/index.ts" +}, { + "contentPath" : "vue/index", + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/index.vue" +}, { + "contentPath" : "vue/search-form", + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/components/search-form.vue" +}, { + "contentPath" : "vue/form/index", + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue" +}, { + "contentPath" : "vue/detail/index", + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue" +} ] \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/ErrorCodeConstants_手动操作 b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/ErrorCodeConstants_手动操作 new file mode 100644 index 0000000..a4df7d2 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/ErrorCodeConstants_手动操作 @@ -0,0 +1,3 @@ +// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!! +// ========== 学生 TODO 补充编号 ========== +ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在"); \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentContactDO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentContactDO new file mode 100644 index 0000000..17c668e --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentContactDO @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.demo; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; + +/** + * 学生联系人 DO + * + * @author 芋道源码 + */ +@TableName("infra_student_contact") +@KeySequence("infra_student_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InfraStudentContactDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 学生编号 + */ + private Long studentId; + /** + * 名字 + */ + private String name; + /** + * 简介 + */ + private String description; + /** + * 出生日期 + */ + private LocalDateTime birthday; + /** + * 性别 + * + * 枚举 {@link TODO system_user_sex 对应的类} + */ + private Integer sex; + /** + * 是否有效 + * + * 枚举 {@link TODO infra_boolean_string 对应的类} + */ + private Boolean enabled; + /** + * 头像 + */ + private String avatar; + /** + * 附件 + */ + private String video; + /** + * 备注 + */ + private String memo; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentContactMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentContactMapper new file mode 100644 index 0000000..9f16dcf --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentContactMapper @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo; + +import java.util.*; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 学生联系人 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface InfraStudentContactMapper extends BaseMapperX { + + default List selectListByStudentId(Long studentId) { + return selectList(InfraStudentContactDO::getStudentId, studentId); + } + + default int deleteByStudentId(Long studentId) { + return delete(InfraStudentContactDO::getStudentId, studentId); + } + + default int deleteByStudentIds(List studentIds) { + return deleteBatch(InfraStudentContactDO::getStudentId, studentIds); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentController b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentController new file mode 100644 index 0000000..0c9a9f9 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentController @@ -0,0 +1,126 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo; + +import org.springframework.web.bind.annotation.*; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.security.access.prepost.PreAuthorize; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; + +import jakarta.validation.constraints.*; +import jakarta.validation.*; +import jakarta.servlet.http.*; +import java.util.*; +import java.io.IOException; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*; + +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; +import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService; + +@Tag(name = "管理后台 - 学生") +@RestController +@RequestMapping("/infra/student") +@Validated +public class InfraStudentController { + + @Resource + private InfraStudentService studentService; + + @PostMapping("/create") + @Operation(summary = "创建学生") + @PreAuthorize("@ss.hasPermission('infra:student:create')") + public CommonResult createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) { + return success(studentService.createStudent(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新学生") + @PreAuthorize("@ss.hasPermission('infra:student:update')") + public CommonResult updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) { + studentService.updateStudent(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除学生") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:student:delete')") + public CommonResult deleteStudent(@RequestParam("id") Long id) { + studentService.deleteStudent(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Parameter(name = "ids", description = "编号", required = true) + @Operation(summary = "批量删除学生") + @PreAuthorize("@ss.hasPermission('infra:student:delete')") + public CommonResult deleteStudentList(@RequestParam("ids") List ids) { + studentService.deleteStudentListByIds(ids); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得学生") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:student:query')") + public CommonResult getStudent(@RequestParam("id") Long id) { + InfraStudentDO student = studentService.getStudent(id); + return success(BeanUtils.toBean(student, InfraStudentRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得学生分页") + @PreAuthorize("@ss.hasPermission('infra:student:query')") + public CommonResult> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) { + PageResult pageResult = studentService.getStudentPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出学生 Excel") + @PreAuthorize("@ss.hasPermission('infra:student:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = studentService.getStudentPage(pageReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class, + BeanUtils.toBean(list, InfraStudentRespVO.class)); + } + + // ==================== 子表(学生联系人) ==================== + + @GetMapping("/student-contact/list-by-student-id") + @Operation(summary = "获得学生联系人列表") + @Parameter(name = "studentId", description = "学生编号") + @PreAuthorize("@ss.hasPermission('infra:student:query')") + public CommonResult> getStudentContactListByStudentId(@RequestParam("studentId") Long studentId) { + return success(studentService.getStudentContactListByStudentId(studentId)); + } + + // ==================== 子表(学生班主任) ==================== + + @GetMapping("/student-teacher/get-by-student-id") + @Operation(summary = "获得学生班主任") + @Parameter(name = "studentId", description = "学生编号") + @PreAuthorize("@ss.hasPermission('infra:student:query')") + public CommonResult getStudentTeacherByStudentId(@RequestParam("studentId") Long studentId) { + return success(studentService.getStudentTeacherByStudentId(studentId)); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentDO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentDO new file mode 100644 index 0000000..f39fef2 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentDO @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.demo; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; + +/** + * 学生 DO + * + * @author 芋道源码 + */ +@TableName("infra_student") +@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InfraStudentDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 名字 + */ + private String name; + /** + * 简介 + */ + private String description; + /** + * 出生日期 + */ + private LocalDateTime birthday; + /** + * 性别 + * + * 枚举 {@link TODO system_user_sex 对应的类} + */ + private Integer sex; + /** + * 是否有效 + * + * 枚举 {@link TODO infra_boolean_string 对应的类} + */ + private Boolean enabled; + /** + * 头像 + */ + private String avatar; + /** + * 附件 + */ + private String video; + /** + * 备注 + */ + private String memo; + + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentMapper new file mode 100644 index 0000000..34e70a0 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentMapper @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo; + +import java.util.*; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import org.apache.ibatis.annotations.Mapper; +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; + +/** + * 学生 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface InfraStudentMapper extends BaseMapperX { + + default PageResult selectPage(InfraStudentPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(InfraStudentDO::getName, reqVO.getName()) + .eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday()) + .eqIfPresent(InfraStudentDO::getSex, reqVO.getSex()) + .eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled()) + .betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(InfraStudentDO::getId)); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentPageReqVO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentPageReqVO new file mode 100644 index 0000000..52d7a23 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentPageReqVO @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo; + +import lombok.*; +import java.util.*; +import io.swagger.v3.oas.annotations.media.Schema; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 学生分页 Request VO") +@Data +public class InfraStudentPageReqVO extends PageParam { + + @Schema(description = "名字", example = "芋头") + private String name; + + @Schema(description = "出生日期") + private LocalDateTime birthday; + + @Schema(description = "性别", example = "1") + private Integer sex; + + @Schema(description = "是否有效", example = "true") + private Boolean enabled; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentRespVO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentRespVO new file mode 100644 index 0000000..12166f4 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentRespVO @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; +import cn.idev.excel.annotation.*; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; + +@Schema(description = "管理后台 - 学生 Response VO") +@Data +@ExcelIgnoreUnannotated +public class InfraStudentRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("编号") + private Long id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头") + @ExcelProperty("名字") + private String name; + + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍") + @ExcelProperty("简介") + private String description; + + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("出生日期") + private LocalDateTime birthday; + + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "性别", converter = DictConvert.class) + @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 + private Integer sex; + + @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @ExcelProperty(value = "是否有效", converter = DictConvert.class) + @DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 + private Boolean enabled; + + @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + @ExcelProperty("头像") + private String avatar; + + @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4") + @ExcelProperty("附件") + private String video; + + @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注") + @ExcelProperty("备注") + private String memo; + + @Schema(description = "创建时间") + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentSaveReqVO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentSaveReqVO new file mode 100644 index 0000000..4cde1f8 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentSaveReqVO @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import jakarta.validation.constraints.*; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; + +@Schema(description = "管理后台 - 学生新增/修改 Request VO") +@Data +public class InfraStudentSaveReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头") + @NotEmpty(message = "名字不能为空") + private String name; + + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍") + @NotEmpty(message = "简介不能为空") + private String description; + + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "出生日期不能为空") + private LocalDateTime birthday; + + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "性别不能为空") + private Integer sex; + + @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否有效不能为空") + private Boolean enabled; + + @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + @NotEmpty(message = "头像不能为空") + private String avatar; + + @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4") + @NotEmpty(message = "附件不能为空") + private String video; + + @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注") + @NotEmpty(message = "备注不能为空") + private String memo; + + @Schema(description = "学生联系人列表") + private List studentContacts; + + @Schema(description = "学生班主任") + private InfraStudentTeacherDO studentTeacher; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentService b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentService new file mode 100644 index 0000000..0a3ee84 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentService @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.infra.service.demo; + +import java.util.*; +import jakarta.validation.*; +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; + +/** + * 学生 Service 接口 + * + * @author 芋道源码 + */ +public interface InfraStudentService { + + /** + * 创建学生 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createStudent(@Valid InfraStudentSaveReqVO createReqVO); + + /** + * 更新学生 + * + * @param updateReqVO 更新信息 + */ + void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO); + + /** + * 删除学生 + * + * @param id 编号 + */ + void deleteStudent(Long id); + + /** + * 批量删除学生 + * + * @param ids 编号 + */ + void deleteStudentListByIds(List ids); + + /** + * 获得学生 + * + * @param id 编号 + * @return 学生 + */ + InfraStudentDO getStudent(Long id); + + /** + * 获得学生分页 + * + * @param pageReqVO 分页查询 + * @return 学生分页 + */ + PageResult getStudentPage(InfraStudentPageReqVO pageReqVO); + + // ==================== 子表(学生联系人) ==================== + + /** + * 获得学生联系人列表 + * + * @param studentId 学生编号 + * @return 学生联系人列表 + */ + List getStudentContactListByStudentId(Long studentId); + + // ==================== 子表(学生班主任) ==================== + + /** + * 获得学生班主任 + * + * @param studentId 学生编号 + * @return 学生班主任 + */ + InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId); + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentServiceImpl b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentServiceImpl new file mode 100644 index 0000000..31d689e --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentServiceImpl @@ -0,0 +1,191 @@ +package cn.iocoder.yudao.module.infra.service.demo; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import org.springframework.stereotype.Service; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; + +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; + +/** + * 学生 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class InfraStudentServiceImpl implements InfraStudentService { + + @Resource + private InfraStudentMapper studentMapper; + @Resource + private InfraStudentContactMapper studentContactMapper; + @Resource + private InfraStudentTeacherMapper studentTeacherMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createStudent(InfraStudentSaveReqVO createReqVO) { + // 插入 + InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class); + studentMapper.insert(student); + + + // 插入子表 + createStudentContactList(student.getId(), createReqVO.getStudentContacts()); + createStudentTeacher(student.getId(), createReqVO.getStudentTeacher()); + // 返回 + return student.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateStudent(InfraStudentSaveReqVO updateReqVO) { + // 校验存在 + validateStudentExists(updateReqVO.getId()); + // 更新 + InfraStudentDO updateObj = BeanUtils.toBean(updateReqVO, InfraStudentDO.class); + studentMapper.updateById(updateObj); + + // 更新子表 + updateStudentContactList(updateReqVO.getId(), updateReqVO.getStudentContacts()); + updateStudentTeacher(updateReqVO.getId(), updateReqVO.getStudentTeacher()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteStudent(Long id) { + // 校验存在 + validateStudentExists(id); + // 删除 + studentMapper.deleteById(id); + + // 删除子表 + deleteStudentContactByStudentId(id); + deleteStudentTeacherByStudentId(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteStudentListByIds(List ids) { + // 删除 + studentMapper.deleteByIds(ids); + + // 删除子表 + deleteStudentContactByStudentIds(ids); + deleteStudentTeacherByStudentIds(ids); + } + + + private void validateStudentExists(Long id) { + if (studentMapper.selectById(id) == null) { + throw exception(STUDENT_NOT_EXISTS); + } + } + + @Override + public InfraStudentDO getStudent(Long id) { + return studentMapper.selectById(id); + } + + @Override + public PageResult getStudentPage(InfraStudentPageReqVO pageReqVO) { + return studentMapper.selectPage(pageReqVO); + } + + // ==================== 子表(学生联系人) ==================== + + @Override + public List getStudentContactListByStudentId(Long studentId) { + return studentContactMapper.selectListByStudentId(studentId); + } + + private void createStudentContactList(Long studentId, List list) { + if (CollUtil.isEmpty(list)) { + return; + } + list.forEach(o -> o.setStudentId(studentId).clean()); + studentContactMapper.insertBatch(list); + } + + private void updateStudentContactList(Long studentId, List list) { + list.forEach(o -> o.setStudentId(studentId).clean()); + List oldList = studentContactMapper.selectListByStudentId(studentId); + List> diffList = diffList(oldList, list, (oldVal, newVal) -> { + boolean same = ObjectUtil.equal(oldVal.getId(), newVal.getId()); + if (same) { + newVal.setId(oldVal.getId()).clean(); // 解决更新情况下:updateTime 不更新 + } + return same; + }); + + // 第二步,批量添加、修改、删除 + if (CollUtil.isNotEmpty(diffList.get(0))) { + studentContactMapper.insertBatch(diffList.get(0)); + } + if (CollUtil.isNotEmpty(diffList.get(1))) { + studentContactMapper.updateBatch(diffList.get(1)); + } + if (CollUtil.isNotEmpty(diffList.get(2))) { + studentContactMapper.deleteByIds(convertList(diffList.get(2), InfraStudentContactDO::getId)); + } + } + + private void deleteStudentContactByStudentId(Long studentId) { + studentContactMapper.deleteByStudentId(studentId); + } + + private void deleteStudentContactByStudentIds(List studentIds) { + studentContactMapper.deleteByStudentIds(studentIds); + } + + // ==================== 子表(学生班主任) ==================== + + @Override + public InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId) { + return studentTeacherMapper.selectByStudentId(studentId); + } + + private void createStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) { + if (studentTeacher == null) { + return; + } + studentTeacher.setStudentId(studentId); + studentTeacherMapper.insert(studentTeacher); + } + + private void updateStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) { + if (studentTeacher == null) { + return; + } + studentTeacher.setStudentId(studentId).clean();// 解决更新情况下:updateTime 不更新 + studentTeacherMapper.insertOrUpdate(studentTeacher); + } + + private void deleteStudentTeacherByStudentId(Long studentId) { + studentTeacherMapper.deleteByStudentId(studentId); + } + + private void deleteStudentTeacherByStudentIds(List studentIds) { + studentTeacherMapper.deleteByStudentIds(studentIds); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentServiceImplTest b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentServiceImplTest new file mode 100644 index 0000000..f1d3bb9 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentServiceImplTest @@ -0,0 +1,145 @@ +package cn.iocoder.yudao.module.infra.service.demo; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import jakarta.annotation.Resource; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; + +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper; +import cn.iocoder.yudao.framework.common.pojo.PageResult; + +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Import; +import java.util.*; +import java.time.LocalDateTime; + +import static cn.hutool.core.util.RandomUtil.*; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * {@link InfraStudentServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(InfraStudentServiceImpl.class) +public class InfraStudentServiceImplTest extends BaseDbUnitTest { + + @Resource + private InfraStudentServiceImpl studentService; + + @Resource + private InfraStudentMapper studentMapper; + + @Test + public void testCreateStudent_success() { + // 准备参数 + InfraStudentSaveReqVO createReqVO = randomPojo(InfraStudentSaveReqVO.class).setId(null); + + // 调用 + Long studentId = studentService.createStudent(createReqVO); + // 断言 + assertNotNull(studentId); + // 校验记录的属性是否正确 + InfraStudentDO student = studentMapper.selectById(studentId); + assertPojoEquals(createReqVO, student, "id"); + } + + @Test + public void testUpdateStudent_success() { + // mock 数据 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class); + studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据 + // 准备参数 + InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class, o -> { + o.setId(dbStudent.getId()); // 设置更新的 ID + }); + + // 调用 + studentService.updateStudent(updateReqVO); + // 校验是否更新正确 + InfraStudentDO student = studentMapper.selectById(updateReqVO.getId()); // 获取最新的 + assertPojoEquals(updateReqVO, student); + } + + @Test + public void testUpdateStudent_notExists() { + // 准备参数 + InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> studentService.updateStudent(updateReqVO), STUDENT_NOT_EXISTS); + } + + @Test + public void testDeleteStudent_success() { + // mock 数据 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class); + studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbStudent.getId(); + + // 调用 + studentService.deleteStudent(id); + // 校验数据不存在了 + assertNull(studentMapper.selectById(id)); + } + + @Test + public void testDeleteStudent_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> studentService.deleteStudent(id), STUDENT_NOT_EXISTS); + } + + @Test + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 + public void testGetStudentPage() { + // mock 数据 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class, o -> { // 等会查询到 + o.setName(null); + o.setBirthday(null); + o.setSex(null); + o.setEnabled(null); + o.setCreateTime(null); + }); + studentMapper.insert(dbStudent); + // 测试 name 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setName(null))); + // 测试 birthday 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setBirthday(null))); + // 测试 sex 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setSex(null))); + // 测试 enabled 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setEnabled(null))); + // 测试 createTime 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setCreateTime(null))); + // 准备参数 + InfraStudentPageReqVO reqVO = new InfraStudentPageReqVO(); + reqVO.setName(null); + reqVO.setBirthday(null); + reqVO.setSex(null); + reqVO.setEnabled(null); + reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + + // 调用 + PageResult pageResult = studentService.getStudentPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbStudent, pageResult.getList().get(0)); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentTeacherDO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentTeacherDO new file mode 100644 index 0000000..c19cf9f --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentTeacherDO @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.demo; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; + +/** + * 学生班主任 DO + * + * @author 芋道源码 + */ +@TableName("infra_student_teacher") +@KeySequence("infra_student_teacher_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InfraStudentTeacherDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 学生编号 + */ + private Long studentId; + /** + * 名字 + */ + private String name; + /** + * 简介 + */ + private String description; + /** + * 出生日期 + */ + private LocalDateTime birthday; + /** + * 性别 + * + * 枚举 {@link TODO system_user_sex 对应的类} + */ + private Integer sex; + /** + * 是否有效 + * + * 枚举 {@link TODO infra_boolean_string 对应的类} + */ + private Boolean enabled; + /** + * 头像 + */ + private String avatar; + /** + * 附件 + */ + private String video; + /** + * 备注 + */ + private String memo; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentTeacherMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentTeacherMapper new file mode 100644 index 0000000..ae4f27d --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentTeacherMapper @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo; + +import java.util.*; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 学生班主任 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface InfraStudentTeacherMapper extends BaseMapperX { + + default InfraStudentTeacherDO selectByStudentId(Long studentId) { + return selectOne(InfraStudentTeacherDO::getStudentId, studentId); + } + + default int deleteByStudentId(Long studentId) { + return delete(InfraStudentTeacherDO::getStudentId, studentId); + } + + default int deleteByStudentIds(List studentIds) { + return deleteBatch(InfraStudentTeacherDO::getStudentId, studentIds); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/sql/h2 b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/sql/h2 new file mode 100644 index 0000000..6c1875f --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/sql/h2 @@ -0,0 +1,17 @@ +-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里 +CREATE TABLE IF NOT EXISTS "infra_student" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar NOT NULL, + "description" varchar NOT NULL, + "birthday" varchar NOT NULL, + "sex" int NOT NULL, + "enabled" bit NOT NULL, + "avatar" varchar NOT NULL, + "video" varchar NOT NULL, + "memo" varchar NOT NULL, + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY ("id") +) COMMENT '学生表'; + +-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里 +DELETE FROM "infra_student"; \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/sql/sql b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/sql/sql new file mode 100644 index 0000000..c0e5f74 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/sql/sql @@ -0,0 +1,54 @@ +-- 菜单 SQL +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status, component_name +) +VALUES ( + '学生管理', '', 2, 0, 888, + 'student', '', 'infra/demo/index', 0, 'InfraStudent' +); + +-- 按钮父菜单ID +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生查询', 'infra:student:query', 3, 1, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生创建', 'infra:student:create', 3, 2, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生更新', 'infra:student:update', 3, 3, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生删除', 'infra:student:delete', 3, 4, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生导出', 'infra:student:export', 3, 5, @parentId, + '', '', '', 0 +); \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/ts/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/ts/index new file mode 100644 index 0000000..c98bd3b --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/ts/index @@ -0,0 +1,87 @@ +import type { PageParam, PageResult } from '@/http/types' +import { http } from '@/http/http' + +/** 学生联系人信息 */ +export interface StudentContact { + id?: number + studentId?: number + name: string + description: string + birthday: number + sex: number + enabled: boolean + avatar: string + video?: string + memo: string + createTime?: number +} + +/** 学生班主任信息 */ +export interface StudentTeacher { + id?: number + studentId?: number + name: string + description: string + birthday: number + sex: number + enabled: boolean + avatar: string + video?: string + memo: string + createTime?: number +} + +/** 学生信息 */ +export interface Student { + id?: number + name: string + description: string + birthday: number + sex: number + enabled: boolean + avatar: string + video: string + memo: string + createTime?: number + studentContacts?: StudentContact[] + studentTeacher?: StudentTeacher | null +} + +/** 获取学生分页列表 */ +export function getStudentPage(params: PageParam) { + return http.get>('/infra/student/page', params) +} + +/** 获取学生详情 */ +export function getStudent(id: number) { + return http.get(`/infra/student/get?id=${id}`) +} + +/** 创建学生 */ +export function createStudent(data: Student) { + return http.post('/infra/student/create', data) +} + +/** 更新学生 */ +export function updateStudent(data: Student) { + return http.put('/infra/student/update', data) +} + +/** 删除学生 */ +export function deleteStudent(id: number) { + return http.delete(`/infra/student/delete?id=${id}`) +} + +// ==================== 子表:学生联系人 ==================== + +/** 获取学生联系人列表 */ +export function getStudentContactListByStudentId(studentId: number) { + return http.get(`/infra/student/student-contact/list-by-student-id?studentId=${studentId}`) +} + +// ==================== 子表:学生班主任 ==================== + +/** 获取学生班主任 */ +export function getStudentTeacherByStudentId(studentId: number) { + return http.get(`/infra/student/student-teacher/get-by-student-id?studentId=${studentId}`) +} diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/detail/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/detail/index new file mode 100644 index 0000000..829c797 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/detail/index @@ -0,0 +1,229 @@ + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/form/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/form/index new file mode 100644 index 0000000..a43e177 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/form/index @@ -0,0 +1,480 @@ + + + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/index new file mode 100644 index 0000000..acb1b87 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/index @@ -0,0 +1,169 @@ + + + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/search-form b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/search-form new file mode 100644 index 0000000..0928d14 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/search-form @@ -0,0 +1,116 @@ + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/xml/InfraStudentMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/xml/InfraStudentMapper new file mode 100644 index 0000000..155aa5c --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/xml/InfraStudentMapper @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/assert.json b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/assert.json new file mode 100644 index 0000000..0a37471 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/assert.json @@ -0,0 +1,67 @@ +[ { + "contentPath" : "java/InfraStudentPageReqVO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java" +}, { + "contentPath" : "java/InfraStudentRespVO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java" +}, { + "contentPath" : "java/InfraStudentSaveReqVO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java" +}, { + "contentPath" : "java/InfraStudentController", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java" +}, { + "contentPath" : "java/InfraStudentDO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java" +}, { + "contentPath" : "java/InfraStudentContactDO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java" +}, { + "contentPath" : "java/InfraStudentTeacherDO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java" +}, { + "contentPath" : "java/InfraStudentMapper", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java" +}, { + "contentPath" : "java/InfraStudentContactMapper", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java" +}, { + "contentPath" : "java/InfraStudentTeacherMapper", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java" +}, { + "contentPath" : "xml/InfraStudentMapper", + "filePath" : "yudao-module-infra/src/main/resources/mapper/demo/InfraStudentMapper.xml" +}, { + "contentPath" : "java/InfraStudentServiceImpl", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java" +}, { + "contentPath" : "java/InfraStudentService", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java" +}, { + "contentPath" : "java/InfraStudentServiceImplTest", + "filePath" : "yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java" +}, { + "contentPath" : "java/ErrorCodeConstants_手动操作", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java" +}, { + "contentPath" : "sql/sql", + "filePath" : "sql/sql.sql" +}, { + "contentPath" : "sql/h2", + "filePath" : "sql/h2.sql" +}, { + "contentPath" : "ts/index", + "filePath" : "yudao-ui-admin-uniapp/src/api/infra/demo/index.ts" +}, { + "contentPath" : "vue/index", + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/index.vue" +}, { + "contentPath" : "vue/search-form", + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/components/search-form.vue" +}, { + "contentPath" : "vue/form/index", + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue" +}, { + "contentPath" : "vue/detail/index", + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue" +} ] \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/ErrorCodeConstants_手动操作 b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/ErrorCodeConstants_手动操作 new file mode 100644 index 0000000..a4df7d2 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/ErrorCodeConstants_手动操作 @@ -0,0 +1,3 @@ +// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!! +// ========== 学生 TODO 补充编号 ========== +ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在"); \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentContactDO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentContactDO new file mode 100644 index 0000000..17c668e --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentContactDO @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.demo; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; + +/** + * 学生联系人 DO + * + * @author 芋道源码 + */ +@TableName("infra_student_contact") +@KeySequence("infra_student_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InfraStudentContactDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 学生编号 + */ + private Long studentId; + /** + * 名字 + */ + private String name; + /** + * 简介 + */ + private String description; + /** + * 出生日期 + */ + private LocalDateTime birthday; + /** + * 性别 + * + * 枚举 {@link TODO system_user_sex 对应的类} + */ + private Integer sex; + /** + * 是否有效 + * + * 枚举 {@link TODO infra_boolean_string 对应的类} + */ + private Boolean enabled; + /** + * 头像 + */ + private String avatar; + /** + * 附件 + */ + private String video; + /** + * 备注 + */ + private String memo; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentContactMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentContactMapper new file mode 100644 index 0000000..9f16dcf --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentContactMapper @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo; + +import java.util.*; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 学生联系人 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface InfraStudentContactMapper extends BaseMapperX { + + default List selectListByStudentId(Long studentId) { + return selectList(InfraStudentContactDO::getStudentId, studentId); + } + + default int deleteByStudentId(Long studentId) { + return delete(InfraStudentContactDO::getStudentId, studentId); + } + + default int deleteByStudentIds(List studentIds) { + return deleteBatch(InfraStudentContactDO::getStudentId, studentIds); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentController b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentController new file mode 100644 index 0000000..0c9a9f9 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentController @@ -0,0 +1,126 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo; + +import org.springframework.web.bind.annotation.*; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.security.access.prepost.PreAuthorize; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; + +import jakarta.validation.constraints.*; +import jakarta.validation.*; +import jakarta.servlet.http.*; +import java.util.*; +import java.io.IOException; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*; + +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; +import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService; + +@Tag(name = "管理后台 - 学生") +@RestController +@RequestMapping("/infra/student") +@Validated +public class InfraStudentController { + + @Resource + private InfraStudentService studentService; + + @PostMapping("/create") + @Operation(summary = "创建学生") + @PreAuthorize("@ss.hasPermission('infra:student:create')") + public CommonResult createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) { + return success(studentService.createStudent(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新学生") + @PreAuthorize("@ss.hasPermission('infra:student:update')") + public CommonResult updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) { + studentService.updateStudent(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除学生") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:student:delete')") + public CommonResult deleteStudent(@RequestParam("id") Long id) { + studentService.deleteStudent(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Parameter(name = "ids", description = "编号", required = true) + @Operation(summary = "批量删除学生") + @PreAuthorize("@ss.hasPermission('infra:student:delete')") + public CommonResult deleteStudentList(@RequestParam("ids") List ids) { + studentService.deleteStudentListByIds(ids); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得学生") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:student:query')") + public CommonResult getStudent(@RequestParam("id") Long id) { + InfraStudentDO student = studentService.getStudent(id); + return success(BeanUtils.toBean(student, InfraStudentRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得学生分页") + @PreAuthorize("@ss.hasPermission('infra:student:query')") + public CommonResult> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) { + PageResult pageResult = studentService.getStudentPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出学生 Excel") + @PreAuthorize("@ss.hasPermission('infra:student:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = studentService.getStudentPage(pageReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class, + BeanUtils.toBean(list, InfraStudentRespVO.class)); + } + + // ==================== 子表(学生联系人) ==================== + + @GetMapping("/student-contact/list-by-student-id") + @Operation(summary = "获得学生联系人列表") + @Parameter(name = "studentId", description = "学生编号") + @PreAuthorize("@ss.hasPermission('infra:student:query')") + public CommonResult> getStudentContactListByStudentId(@RequestParam("studentId") Long studentId) { + return success(studentService.getStudentContactListByStudentId(studentId)); + } + + // ==================== 子表(学生班主任) ==================== + + @GetMapping("/student-teacher/get-by-student-id") + @Operation(summary = "获得学生班主任") + @Parameter(name = "studentId", description = "学生编号") + @PreAuthorize("@ss.hasPermission('infra:student:query')") + public CommonResult getStudentTeacherByStudentId(@RequestParam("studentId") Long studentId) { + return success(studentService.getStudentTeacherByStudentId(studentId)); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentDO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentDO new file mode 100644 index 0000000..f39fef2 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentDO @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.demo; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; + +/** + * 学生 DO + * + * @author 芋道源码 + */ +@TableName("infra_student") +@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InfraStudentDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 名字 + */ + private String name; + /** + * 简介 + */ + private String description; + /** + * 出生日期 + */ + private LocalDateTime birthday; + /** + * 性别 + * + * 枚举 {@link TODO system_user_sex 对应的类} + */ + private Integer sex; + /** + * 是否有效 + * + * 枚举 {@link TODO infra_boolean_string 对应的类} + */ + private Boolean enabled; + /** + * 头像 + */ + private String avatar; + /** + * 附件 + */ + private String video; + /** + * 备注 + */ + private String memo; + + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentMapper new file mode 100644 index 0000000..34e70a0 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentMapper @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo; + +import java.util.*; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import org.apache.ibatis.annotations.Mapper; +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; + +/** + * 学生 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface InfraStudentMapper extends BaseMapperX { + + default PageResult selectPage(InfraStudentPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(InfraStudentDO::getName, reqVO.getName()) + .eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday()) + .eqIfPresent(InfraStudentDO::getSex, reqVO.getSex()) + .eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled()) + .betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(InfraStudentDO::getId)); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentPageReqVO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentPageReqVO new file mode 100644 index 0000000..52d7a23 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentPageReqVO @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo; + +import lombok.*; +import java.util.*; +import io.swagger.v3.oas.annotations.media.Schema; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 学生分页 Request VO") +@Data +public class InfraStudentPageReqVO extends PageParam { + + @Schema(description = "名字", example = "芋头") + private String name; + + @Schema(description = "出生日期") + private LocalDateTime birthday; + + @Schema(description = "性别", example = "1") + private Integer sex; + + @Schema(description = "是否有效", example = "true") + private Boolean enabled; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentRespVO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentRespVO new file mode 100644 index 0000000..12166f4 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentRespVO @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; +import cn.idev.excel.annotation.*; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; + +@Schema(description = "管理后台 - 学生 Response VO") +@Data +@ExcelIgnoreUnannotated +public class InfraStudentRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("编号") + private Long id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头") + @ExcelProperty("名字") + private String name; + + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍") + @ExcelProperty("简介") + private String description; + + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("出生日期") + private LocalDateTime birthday; + + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "性别", converter = DictConvert.class) + @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 + private Integer sex; + + @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @ExcelProperty(value = "是否有效", converter = DictConvert.class) + @DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 + private Boolean enabled; + + @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + @ExcelProperty("头像") + private String avatar; + + @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4") + @ExcelProperty("附件") + private String video; + + @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注") + @ExcelProperty("备注") + private String memo; + + @Schema(description = "创建时间") + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentSaveReqVO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentSaveReqVO new file mode 100644 index 0000000..4cde1f8 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentSaveReqVO @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import jakarta.validation.constraints.*; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; + +@Schema(description = "管理后台 - 学生新增/修改 Request VO") +@Data +public class InfraStudentSaveReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头") + @NotEmpty(message = "名字不能为空") + private String name; + + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍") + @NotEmpty(message = "简介不能为空") + private String description; + + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "出生日期不能为空") + private LocalDateTime birthday; + + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "性别不能为空") + private Integer sex; + + @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否有效不能为空") + private Boolean enabled; + + @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + @NotEmpty(message = "头像不能为空") + private String avatar; + + @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4") + @NotEmpty(message = "附件不能为空") + private String video; + + @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注") + @NotEmpty(message = "备注不能为空") + private String memo; + + @Schema(description = "学生联系人列表") + private List studentContacts; + + @Schema(description = "学生班主任") + private InfraStudentTeacherDO studentTeacher; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentService b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentService new file mode 100644 index 0000000..0a3ee84 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentService @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.infra.service.demo; + +import java.util.*; +import jakarta.validation.*; +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; + +/** + * 学生 Service 接口 + * + * @author 芋道源码 + */ +public interface InfraStudentService { + + /** + * 创建学生 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createStudent(@Valid InfraStudentSaveReqVO createReqVO); + + /** + * 更新学生 + * + * @param updateReqVO 更新信息 + */ + void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO); + + /** + * 删除学生 + * + * @param id 编号 + */ + void deleteStudent(Long id); + + /** + * 批量删除学生 + * + * @param ids 编号 + */ + void deleteStudentListByIds(List ids); + + /** + * 获得学生 + * + * @param id 编号 + * @return 学生 + */ + InfraStudentDO getStudent(Long id); + + /** + * 获得学生分页 + * + * @param pageReqVO 分页查询 + * @return 学生分页 + */ + PageResult getStudentPage(InfraStudentPageReqVO pageReqVO); + + // ==================== 子表(学生联系人) ==================== + + /** + * 获得学生联系人列表 + * + * @param studentId 学生编号 + * @return 学生联系人列表 + */ + List getStudentContactListByStudentId(Long studentId); + + // ==================== 子表(学生班主任) ==================== + + /** + * 获得学生班主任 + * + * @param studentId 学生编号 + * @return 学生班主任 + */ + InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId); + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentServiceImpl b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentServiceImpl new file mode 100644 index 0000000..31d689e --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentServiceImpl @@ -0,0 +1,191 @@ +package cn.iocoder.yudao.module.infra.service.demo; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import org.springframework.stereotype.Service; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; + +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; + +/** + * 学生 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class InfraStudentServiceImpl implements InfraStudentService { + + @Resource + private InfraStudentMapper studentMapper; + @Resource + private InfraStudentContactMapper studentContactMapper; + @Resource + private InfraStudentTeacherMapper studentTeacherMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createStudent(InfraStudentSaveReqVO createReqVO) { + // 插入 + InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class); + studentMapper.insert(student); + + + // 插入子表 + createStudentContactList(student.getId(), createReqVO.getStudentContacts()); + createStudentTeacher(student.getId(), createReqVO.getStudentTeacher()); + // 返回 + return student.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateStudent(InfraStudentSaveReqVO updateReqVO) { + // 校验存在 + validateStudentExists(updateReqVO.getId()); + // 更新 + InfraStudentDO updateObj = BeanUtils.toBean(updateReqVO, InfraStudentDO.class); + studentMapper.updateById(updateObj); + + // 更新子表 + updateStudentContactList(updateReqVO.getId(), updateReqVO.getStudentContacts()); + updateStudentTeacher(updateReqVO.getId(), updateReqVO.getStudentTeacher()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteStudent(Long id) { + // 校验存在 + validateStudentExists(id); + // 删除 + studentMapper.deleteById(id); + + // 删除子表 + deleteStudentContactByStudentId(id); + deleteStudentTeacherByStudentId(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteStudentListByIds(List ids) { + // 删除 + studentMapper.deleteByIds(ids); + + // 删除子表 + deleteStudentContactByStudentIds(ids); + deleteStudentTeacherByStudentIds(ids); + } + + + private void validateStudentExists(Long id) { + if (studentMapper.selectById(id) == null) { + throw exception(STUDENT_NOT_EXISTS); + } + } + + @Override + public InfraStudentDO getStudent(Long id) { + return studentMapper.selectById(id); + } + + @Override + public PageResult getStudentPage(InfraStudentPageReqVO pageReqVO) { + return studentMapper.selectPage(pageReqVO); + } + + // ==================== 子表(学生联系人) ==================== + + @Override + public List getStudentContactListByStudentId(Long studentId) { + return studentContactMapper.selectListByStudentId(studentId); + } + + private void createStudentContactList(Long studentId, List list) { + if (CollUtil.isEmpty(list)) { + return; + } + list.forEach(o -> o.setStudentId(studentId).clean()); + studentContactMapper.insertBatch(list); + } + + private void updateStudentContactList(Long studentId, List list) { + list.forEach(o -> o.setStudentId(studentId).clean()); + List oldList = studentContactMapper.selectListByStudentId(studentId); + List> diffList = diffList(oldList, list, (oldVal, newVal) -> { + boolean same = ObjectUtil.equal(oldVal.getId(), newVal.getId()); + if (same) { + newVal.setId(oldVal.getId()).clean(); // 解决更新情况下:updateTime 不更新 + } + return same; + }); + + // 第二步,批量添加、修改、删除 + if (CollUtil.isNotEmpty(diffList.get(0))) { + studentContactMapper.insertBatch(diffList.get(0)); + } + if (CollUtil.isNotEmpty(diffList.get(1))) { + studentContactMapper.updateBatch(diffList.get(1)); + } + if (CollUtil.isNotEmpty(diffList.get(2))) { + studentContactMapper.deleteByIds(convertList(diffList.get(2), InfraStudentContactDO::getId)); + } + } + + private void deleteStudentContactByStudentId(Long studentId) { + studentContactMapper.deleteByStudentId(studentId); + } + + private void deleteStudentContactByStudentIds(List studentIds) { + studentContactMapper.deleteByStudentIds(studentIds); + } + + // ==================== 子表(学生班主任) ==================== + + @Override + public InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId) { + return studentTeacherMapper.selectByStudentId(studentId); + } + + private void createStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) { + if (studentTeacher == null) { + return; + } + studentTeacher.setStudentId(studentId); + studentTeacherMapper.insert(studentTeacher); + } + + private void updateStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) { + if (studentTeacher == null) { + return; + } + studentTeacher.setStudentId(studentId).clean();// 解决更新情况下:updateTime 不更新 + studentTeacherMapper.insertOrUpdate(studentTeacher); + } + + private void deleteStudentTeacherByStudentId(Long studentId) { + studentTeacherMapper.deleteByStudentId(studentId); + } + + private void deleteStudentTeacherByStudentIds(List studentIds) { + studentTeacherMapper.deleteByStudentIds(studentIds); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentServiceImplTest b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentServiceImplTest new file mode 100644 index 0000000..f1d3bb9 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentServiceImplTest @@ -0,0 +1,145 @@ +package cn.iocoder.yudao.module.infra.service.demo; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import jakarta.annotation.Resource; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; + +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper; +import cn.iocoder.yudao.framework.common.pojo.PageResult; + +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Import; +import java.util.*; +import java.time.LocalDateTime; + +import static cn.hutool.core.util.RandomUtil.*; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * {@link InfraStudentServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(InfraStudentServiceImpl.class) +public class InfraStudentServiceImplTest extends BaseDbUnitTest { + + @Resource + private InfraStudentServiceImpl studentService; + + @Resource + private InfraStudentMapper studentMapper; + + @Test + public void testCreateStudent_success() { + // 准备参数 + InfraStudentSaveReqVO createReqVO = randomPojo(InfraStudentSaveReqVO.class).setId(null); + + // 调用 + Long studentId = studentService.createStudent(createReqVO); + // 断言 + assertNotNull(studentId); + // 校验记录的属性是否正确 + InfraStudentDO student = studentMapper.selectById(studentId); + assertPojoEquals(createReqVO, student, "id"); + } + + @Test + public void testUpdateStudent_success() { + // mock 数据 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class); + studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据 + // 准备参数 + InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class, o -> { + o.setId(dbStudent.getId()); // 设置更新的 ID + }); + + // 调用 + studentService.updateStudent(updateReqVO); + // 校验是否更新正确 + InfraStudentDO student = studentMapper.selectById(updateReqVO.getId()); // 获取最新的 + assertPojoEquals(updateReqVO, student); + } + + @Test + public void testUpdateStudent_notExists() { + // 准备参数 + InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> studentService.updateStudent(updateReqVO), STUDENT_NOT_EXISTS); + } + + @Test + public void testDeleteStudent_success() { + // mock 数据 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class); + studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbStudent.getId(); + + // 调用 + studentService.deleteStudent(id); + // 校验数据不存在了 + assertNull(studentMapper.selectById(id)); + } + + @Test + public void testDeleteStudent_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> studentService.deleteStudent(id), STUDENT_NOT_EXISTS); + } + + @Test + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 + public void testGetStudentPage() { + // mock 数据 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class, o -> { // 等会查询到 + o.setName(null); + o.setBirthday(null); + o.setSex(null); + o.setEnabled(null); + o.setCreateTime(null); + }); + studentMapper.insert(dbStudent); + // 测试 name 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setName(null))); + // 测试 birthday 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setBirthday(null))); + // 测试 sex 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setSex(null))); + // 测试 enabled 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setEnabled(null))); + // 测试 createTime 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setCreateTime(null))); + // 准备参数 + InfraStudentPageReqVO reqVO = new InfraStudentPageReqVO(); + reqVO.setName(null); + reqVO.setBirthday(null); + reqVO.setSex(null); + reqVO.setEnabled(null); + reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + + // 调用 + PageResult pageResult = studentService.getStudentPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbStudent, pageResult.getList().get(0)); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentTeacherDO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentTeacherDO new file mode 100644 index 0000000..c19cf9f --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentTeacherDO @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.demo; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; + +/** + * 学生班主任 DO + * + * @author 芋道源码 + */ +@TableName("infra_student_teacher") +@KeySequence("infra_student_teacher_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InfraStudentTeacherDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 学生编号 + */ + private Long studentId; + /** + * 名字 + */ + private String name; + /** + * 简介 + */ + private String description; + /** + * 出生日期 + */ + private LocalDateTime birthday; + /** + * 性别 + * + * 枚举 {@link TODO system_user_sex 对应的类} + */ + private Integer sex; + /** + * 是否有效 + * + * 枚举 {@link TODO infra_boolean_string 对应的类} + */ + private Boolean enabled; + /** + * 头像 + */ + private String avatar; + /** + * 附件 + */ + private String video; + /** + * 备注 + */ + private String memo; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentTeacherMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentTeacherMapper new file mode 100644 index 0000000..ae4f27d --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentTeacherMapper @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo; + +import java.util.*; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 学生班主任 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface InfraStudentTeacherMapper extends BaseMapperX { + + default InfraStudentTeacherDO selectByStudentId(Long studentId) { + return selectOne(InfraStudentTeacherDO::getStudentId, studentId); + } + + default int deleteByStudentId(Long studentId) { + return delete(InfraStudentTeacherDO::getStudentId, studentId); + } + + default int deleteByStudentIds(List studentIds) { + return deleteBatch(InfraStudentTeacherDO::getStudentId, studentIds); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/sql/h2 b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/sql/h2 new file mode 100644 index 0000000..6c1875f --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/sql/h2 @@ -0,0 +1,17 @@ +-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里 +CREATE TABLE IF NOT EXISTS "infra_student" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar NOT NULL, + "description" varchar NOT NULL, + "birthday" varchar NOT NULL, + "sex" int NOT NULL, + "enabled" bit NOT NULL, + "avatar" varchar NOT NULL, + "video" varchar NOT NULL, + "memo" varchar NOT NULL, + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY ("id") +) COMMENT '学生表'; + +-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里 +DELETE FROM "infra_student"; \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/sql/sql b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/sql/sql new file mode 100644 index 0000000..c0e5f74 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/sql/sql @@ -0,0 +1,54 @@ +-- 菜单 SQL +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status, component_name +) +VALUES ( + '学生管理', '', 2, 0, 888, + 'student', '', 'infra/demo/index', 0, 'InfraStudent' +); + +-- 按钮父菜单ID +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生查询', 'infra:student:query', 3, 1, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生创建', 'infra:student:create', 3, 2, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生更新', 'infra:student:update', 3, 3, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生删除', 'infra:student:delete', 3, 4, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生导出', 'infra:student:export', 3, 5, @parentId, + '', '', '', 0 +); \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/ts/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/ts/index new file mode 100644 index 0000000..c98bd3b --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/ts/index @@ -0,0 +1,87 @@ +import type { PageParam, PageResult } from '@/http/types' +import { http } from '@/http/http' + +/** 学生联系人信息 */ +export interface StudentContact { + id?: number + studentId?: number + name: string + description: string + birthday: number + sex: number + enabled: boolean + avatar: string + video?: string + memo: string + createTime?: number +} + +/** 学生班主任信息 */ +export interface StudentTeacher { + id?: number + studentId?: number + name: string + description: string + birthday: number + sex: number + enabled: boolean + avatar: string + video?: string + memo: string + createTime?: number +} + +/** 学生信息 */ +export interface Student { + id?: number + name: string + description: string + birthday: number + sex: number + enabled: boolean + avatar: string + video: string + memo: string + createTime?: number + studentContacts?: StudentContact[] + studentTeacher?: StudentTeacher | null +} + +/** 获取学生分页列表 */ +export function getStudentPage(params: PageParam) { + return http.get>('/infra/student/page', params) +} + +/** 获取学生详情 */ +export function getStudent(id: number) { + return http.get(`/infra/student/get?id=${id}`) +} + +/** 创建学生 */ +export function createStudent(data: Student) { + return http.post('/infra/student/create', data) +} + +/** 更新学生 */ +export function updateStudent(data: Student) { + return http.put('/infra/student/update', data) +} + +/** 删除学生 */ +export function deleteStudent(id: number) { + return http.delete(`/infra/student/delete?id=${id}`) +} + +// ==================== 子表:学生联系人 ==================== + +/** 获取学生联系人列表 */ +export function getStudentContactListByStudentId(studentId: number) { + return http.get(`/infra/student/student-contact/list-by-student-id?studentId=${studentId}`) +} + +// ==================== 子表:学生班主任 ==================== + +/** 获取学生班主任 */ +export function getStudentTeacherByStudentId(studentId: number) { + return http.get(`/infra/student/student-teacher/get-by-student-id?studentId=${studentId}`) +} diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/detail/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/detail/index new file mode 100644 index 0000000..829c797 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/detail/index @@ -0,0 +1,229 @@ + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/form/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/form/index new file mode 100644 index 0000000..a43e177 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/form/index @@ -0,0 +1,480 @@ + + + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/index new file mode 100644 index 0000000..acb1b87 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/index @@ -0,0 +1,169 @@ + + + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/search-form b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/search-form new file mode 100644 index 0000000..0928d14 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/search-form @@ -0,0 +1,116 @@ + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/xml/InfraStudentMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/xml/InfraStudentMapper new file mode 100644 index 0000000..155aa5c --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/xml/InfraStudentMapper @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/ts/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/ts/index index da8643a..85b1ac7 100644 --- a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/ts/index +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/ts/index @@ -6,13 +6,13 @@ export interface Student { id?: number name: string description: string - birthday: Date + birthday: number sex: number enabled: boolean avatar: string video: string memo: string - createTime?: Date + createTime?: number } /** 获取学生分页列表 */ @@ -38,4 +38,4 @@ export function updateStudent(data: Student) { /** 删除学生 */ export function deleteStudent(id: number) { return http.delete(`/infra/student/delete?id=${id}`) -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/detail/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/detail/index index d6972ec..8d44124 100644 --- a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/detail/index +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/detail/index @@ -38,7 +38,7 @@ 删除 @@ -51,6 +51,7 @@ import type { Student } from '@/api/infra/demo' import { onUnload } from '@dcloudio/uni-app' import { onMounted, ref } from 'vue' +import { useDialog } from '@wot-ui/ui/components/wd-dialog' import { useToast } from '@wot-ui/ui/components/wd-toast' import { deleteStudent, getStudent } from '@/api/infra/demo' import { useAccess } from '@/hooks/useAccess' @@ -59,7 +60,7 @@ import { DICT_TYPE } from '@/utils/constants' import { formatDateTime } from '@/utils/date' const props = defineProps<{ - id?: number | any + id?: number }>() definePage({ @@ -70,6 +71,7 @@ definePage({ }) const { hasAccessByCodes } = useAccess() +const dialog = useDialog() const toast = useToast() const formData = ref() // 详情数据 const deleting = ref(false) // 删除状态 @@ -100,28 +102,27 @@ function handleEdit() { } /** 删除学生 */ -function handleDelete() { +async function handleDelete() { if (!props.id) { return } - uni.showModal({ - title: '提示', - content: '确定要删除该学生吗?', - success: async (res) => { - if (!res.confirm) { - return - } - deleting.value = true - try { - await deleteStudent(props.id) - toast.success('删除成功') - uni.$emit('infra:demo:reload') - delay(handleBack) - } finally { - deleting.value = false - } - }, - }) + try { + await dialog.confirm({ + title: '提示', + msg: '确定要删除该学生吗?', + }) + } catch { + return + } + deleting.value = true + try { + await deleteStudent(props.id) + toast.success('删除成功') + uni.$emit('infra:demo:reload') + delay(handleBack) + } finally { + deleting.value = false + } } /** 初始化 */ diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/form/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/form/index index 06e1a54..36cd18d 100644 --- a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/form/index +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/form/index @@ -11,14 +11,14 @@ - + - + - - - + - + - - + + - - + @@ -93,7 +96,9 @@ diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/assert.json b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/assert.json new file mode 100644 index 0000000..4086c6e --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/assert.json @@ -0,0 +1,58 @@ +[ { + "contentPath" : "java/InfraCategoryListReqVO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraCategoryListReqVO.java" +}, { + "contentPath" : "java/InfraCategoryRespVO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraCategoryRespVO.java" +}, { + "contentPath" : "java/InfraCategorySaveReqVO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraCategorySaveReqVO.java" +}, { + "contentPath" : "java/InfraCategoryController", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraCategoryController.java" +}, { + "contentPath" : "java/InfraCategoryDO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraCategoryDO.java" +}, { + "contentPath" : "java/InfraCategoryMapper", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraCategoryMapper.java" +}, { + "contentPath" : "xml/InfraCategoryMapper", + "filePath" : "yudao-module-infra/src/main/resources/mapper/demo/InfraCategoryMapper.xml" +}, { + "contentPath" : "java/InfraCategoryServiceImpl", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraCategoryServiceImpl.java" +}, { + "contentPath" : "java/InfraCategoryService", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraCategoryService.java" +}, { + "contentPath" : "java/InfraCategoryServiceImplTest", + "filePath" : "yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraCategoryServiceImplTest.java" +}, { + "contentPath" : "java/ErrorCodeConstants_手动操作", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java" +}, { + "contentPath" : "sql/sql", + "filePath" : "sql/sql.sql" +}, { + "contentPath" : "sql/h2", + "filePath" : "sql/h2.sql" +}, { + "contentPath" : "ts/index", + "filePath" : "yudao-ui-admin-uniapp/src/api/infra/demo/index.ts" +}, { + "contentPath" : "vue/search-form", + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/components/search-form.vue" +}, { + "contentPath" : "vue/form/index", + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue" +}, { + "contentPath" : "vue/detail/index", + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue" +}, { + "contentPath" : "vue/index", + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/index.vue" +}, { + "contentPath" : "vue/breadcrumb", + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/components/breadcrumb.vue" +} ] \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/ErrorCodeConstants_手动操作 b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/ErrorCodeConstants_手动操作 new file mode 100644 index 0000000..42e399f --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/ErrorCodeConstants_手动操作 @@ -0,0 +1,8 @@ +// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!! +// ========== 分类 TODO 补充编号 ========== +ErrorCode CATEGORY_NOT_EXISTS = new ErrorCode(TODO 补充编号, "分类不存在"); +ErrorCode CATEGORY_EXITS_CHILDREN = new ErrorCode(TODO 补充编号, "存在存在子分类,无法删除"); +ErrorCode CATEGORY_PARENT_NOT_EXITS = new ErrorCode(TODO 补充编号,"父级分类不存在"); +ErrorCode CATEGORY_PARENT_ERROR = new ErrorCode(TODO 补充编号, "不能设置自己为父分类"); +ErrorCode CATEGORY_NAME_DUPLICATE = new ErrorCode(TODO 补充编号, "已经存在该名字的分类"); +ErrorCode CATEGORY_PARENT_IS_CHILD = new ErrorCode(TODO 补充编号, "不能设置自己的子InfraCategory为父InfraCategory"); \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryController b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryController new file mode 100644 index 0000000..7725253 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryController @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo; + +import org.springframework.web.bind.annotation.*; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.security.access.prepost.PreAuthorize; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; + +import jakarta.validation.constraints.*; +import jakarta.validation.*; +import jakarta.servlet.http.*; +import java.util.*; +import java.io.IOException; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*; + +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraCategoryDO; +import cn.iocoder.yudao.module.infra.service.demo.InfraCategoryService; + +@Tag(name = "管理后台 - 分类") +@RestController +@RequestMapping("/infra/category") +@Validated +public class InfraCategoryController { + + @Resource + private InfraCategoryService categoryService; + + @PostMapping("/create") + @Operation(summary = "创建分类") + @PreAuthorize("@ss.hasPermission('infra:category:create')") + public CommonResult createCategory(@Valid @RequestBody InfraCategorySaveReqVO createReqVO) { + return success(categoryService.createCategory(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新分类") + @PreAuthorize("@ss.hasPermission('infra:category:update')") + public CommonResult updateCategory(@Valid @RequestBody InfraCategorySaveReqVO updateReqVO) { + categoryService.updateCategory(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除分类") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:category:delete')") + public CommonResult deleteCategory(@RequestParam("id") Long id) { + categoryService.deleteCategory(id); + return success(true); + } + + + @GetMapping("/get") + @Operation(summary = "获得分类") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:category:query')") + public CommonResult getCategory(@RequestParam("id") Long id) { + InfraCategoryDO category = categoryService.getCategory(id); + return success(BeanUtils.toBean(category, InfraCategoryRespVO.class)); + } + + @GetMapping("/list") + @Operation(summary = "获得分类列表") + @PreAuthorize("@ss.hasPermission('infra:category:query')") + public CommonResult> getCategoryList(@Valid InfraCategoryListReqVO listReqVO) { + List list = categoryService.getCategoryList(listReqVO); + return success(BeanUtils.toBean(list, InfraCategoryRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出分类 Excel") + @PreAuthorize("@ss.hasPermission('infra:category:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportCategoryExcel(@Valid InfraCategoryListReqVO listReqVO, + HttpServletResponse response) throws IOException { + List list = categoryService.getCategoryList(listReqVO); + // 导出 Excel + ExcelUtils.write(response, "分类.xls", "数据", InfraCategoryRespVO.class, + BeanUtils.toBean(list, InfraCategoryRespVO.class)); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryDO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryDO new file mode 100644 index 0000000..7705f07 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryDO @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.demo; + +import lombok.*; +import java.util.*; +import com.baomidou.mybatisplus.annotation.*; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; + +/** + * 分类 DO + * + * @author 芋道源码 + */ +@TableName("infra_category") +@KeySequence("infra_category_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InfraCategoryDO extends BaseDO { + + public static final Long PARENT_ID_ROOT = 0L; + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 名字 + */ + private String name; + /** + * 父编号 + */ + private Long parentId; + + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryListReqVO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryListReqVO new file mode 100644 index 0000000..e5c6f18 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryListReqVO @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo; + +import lombok.*; +import java.util.*; +import io.swagger.v3.oas.annotations.media.Schema; +import cn.iocoder.yudao.framework.common.pojo.PageParam; + +@Schema(description = "管理后台 - 分类列表 Request VO") +@Data +public class InfraCategoryListReqVO { + + @Schema(description = "名字", example = "芋头") + private String name; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryMapper new file mode 100644 index 0000000..9dadbf1 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryMapper @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo; + +import java.util.*; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraCategoryDO; +import org.apache.ibatis.annotations.Mapper; +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; + +/** + * 分类 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface InfraCategoryMapper extends BaseMapperX { + + default List selectList(InfraCategoryListReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(InfraCategoryDO::getName, reqVO.getName()) + .orderByDesc(InfraCategoryDO::getId)); + } + + default InfraCategoryDO selectByParentIdAndName(Long parentId, String name) { + return selectOne(InfraCategoryDO::getParentId, parentId, InfraCategoryDO::getName, name); + } + + default Long selectCountByParentId(Long parentId) { + return selectCount(InfraCategoryDO::getParentId, parentId); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryRespVO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryRespVO new file mode 100644 index 0000000..4e77b00 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryRespVO @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import cn.idev.excel.annotation.*; + +@Schema(description = "管理后台 - 分类 Response VO") +@Data +@ExcelIgnoreUnannotated +public class InfraCategoryRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("编号") + private Long id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头") + @ExcelProperty("名字") + private String name; + + @Schema(description = "父编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @ExcelProperty("父编号") + private Long parentId; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategorySaveReqVO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategorySaveReqVO new file mode 100644 index 0000000..5846795 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategorySaveReqVO @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import jakarta.validation.constraints.*; + +@Schema(description = "管理后台 - 分类新增/修改 Request VO") +@Data +public class InfraCategorySaveReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头") + @NotEmpty(message = "名字不能为空") + private String name; + + @Schema(description = "父编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "父编号不能为空") + private Long parentId; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryService b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryService new file mode 100644 index 0000000..df887b0 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryService @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.infra.service.demo; + +import java.util.*; +import jakarta.validation.*; +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraCategoryDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; + +/** + * 分类 Service 接口 + * + * @author 芋道源码 + */ +public interface InfraCategoryService { + + /** + * 创建分类 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createCategory(@Valid InfraCategorySaveReqVO createReqVO); + + /** + * 更新分类 + * + * @param updateReqVO 更新信息 + */ + void updateCategory(@Valid InfraCategorySaveReqVO updateReqVO); + + /** + * 删除分类 + * + * @param id 编号 + */ + void deleteCategory(Long id); + + + /** + * 获得分类 + * + * @param id 编号 + * @return 分类 + */ + InfraCategoryDO getCategory(Long id); + + /** + * 获得分类列表 + * + * @param listReqVO 查询条件 + * @return 分类列表 + */ + List getCategoryList(InfraCategoryListReqVO listReqVO); + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryServiceImpl b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryServiceImpl new file mode 100644 index 0000000..ecf3566 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryServiceImpl @@ -0,0 +1,141 @@ +package cn.iocoder.yudao.module.infra.service.demo; + +import cn.hutool.core.collection.CollUtil; +import org.springframework.stereotype.Service; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraCategoryDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; + +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraCategoryMapper; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; + +/** + * 分类 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class InfraCategoryServiceImpl implements InfraCategoryService { + + @Resource + private InfraCategoryMapper categoryMapper; + + @Override + public Long createCategory(InfraCategorySaveReqVO createReqVO) { + // 校验父编号的有效性 + validateParentCategory(null, createReqVO.getParentId()); + // 校验名字的唯一性 + validateCategoryNameUnique(null, createReqVO.getParentId(), createReqVO.getName()); + + // 插入 + InfraCategoryDO category = BeanUtils.toBean(createReqVO, InfraCategoryDO.class); + categoryMapper.insert(category); + + // 返回 + return category.getId(); + } + + @Override + public void updateCategory(InfraCategorySaveReqVO updateReqVO) { + // 校验存在 + validateCategoryExists(updateReqVO.getId()); + // 校验父编号的有效性 + validateParentCategory(updateReqVO.getId(), updateReqVO.getParentId()); + // 校验名字的唯一性 + validateCategoryNameUnique(updateReqVO.getId(), updateReqVO.getParentId(), updateReqVO.getName()); + + // 更新 + InfraCategoryDO updateObj = BeanUtils.toBean(updateReqVO, InfraCategoryDO.class); + categoryMapper.updateById(updateObj); + } + + @Override + public void deleteCategory(Long id) { + // 校验存在 + validateCategoryExists(id); + // 校验是否有子分类 + if (categoryMapper.selectCountByParentId(id) > 0) { + throw exception(CATEGORY_EXITS_CHILDREN); + } + // 删除 + categoryMapper.deleteById(id); + } + + + private void validateCategoryExists(Long id) { + if (categoryMapper.selectById(id) == null) { + throw exception(CATEGORY_NOT_EXISTS); + } + } + + private void validateParentCategory(Long id, Long parentId) { + if (parentId == null || CategoryDO.PARENT_ID_ROOT.equals(parentId)) { + return; + } + // 1. 不能设置自己为父分类 + if (Objects.equals(id, parentId)) { + throw exception(CATEGORY_PARENT_ERROR); + } + // 2. 父分类不存在 + CategoryDO parentCategory = categoryMapper.selectById(parentId); + if (parentCategory == null) { + throw exception(CATEGORY_PARENT_NOT_EXITS); + } + // 3. 递归校验父分类,如果父分类是自己的子分类,则报错,避免形成环路 + if (id == null) { // id 为空,说明新增,不需要考虑环路 + return; + } + for (int i = 0; i < Short.MAX_VALUE; i++) { + // 3.1 校验环路 + parentId = parentCategory.getParentId(); + if (Objects.equals(id, parentId)) { + throw exception(CATEGORY_PARENT_IS_CHILD); + } + // 3.2 继续递归下一级父分类 + if (parentId == null || CategoryDO.PARENT_ID_ROOT.equals(parentId)) { + break; + } + parentCategory = categoryMapper.selectById(parentId); + if (parentCategory == null) { + break; + } + } + } + + private void validateCategoryNameUnique(Long id, Long parentId, String name) { + CategoryDO category = categoryMapper.selectByParentIdAndName(parentId, name); + if (category == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的分类 + if (id == null) { + throw exception(CATEGORY_NAME_DUPLICATE); + } + if (!Objects.equals(category.getId(), id)) { + throw exception(CATEGORY_NAME_DUPLICATE); + } + } + + @Override + public InfraCategoryDO getCategory(Long id) { + return categoryMapper.selectById(id); + } + + @Override + public List getCategoryList(InfraCategoryListReqVO listReqVO) { + return categoryMapper.selectList(listReqVO); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryServiceImplTest b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryServiceImplTest new file mode 100644 index 0000000..92672ee --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryServiceImplTest @@ -0,0 +1,128 @@ +package cn.iocoder.yudao.module.infra.service.demo; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import jakarta.annotation.Resource; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; + +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraCategoryDO; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraCategoryMapper; +import cn.iocoder.yudao.framework.common.pojo.PageResult; + +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Import; +import java.util.*; +import java.time.LocalDateTime; + +import static cn.hutool.core.util.RandomUtil.*; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * {@link InfraCategoryServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(InfraCategoryServiceImpl.class) +public class InfraCategoryServiceImplTest extends BaseDbUnitTest { + + @Resource + private InfraCategoryServiceImpl categoryService; + + @Resource + private InfraCategoryMapper categoryMapper; + + @Test + public void testCreateCategory_success() { + // 准备参数 + InfraCategorySaveReqVO createReqVO = randomPojo(InfraCategorySaveReqVO.class).setId(null); + + // 调用 + Long categoryId = categoryService.createCategory(createReqVO); + // 断言 + assertNotNull(categoryId); + // 校验记录的属性是否正确 + InfraCategoryDO category = categoryMapper.selectById(categoryId); + assertPojoEquals(createReqVO, category, "id"); + } + + @Test + public void testUpdateCategory_success() { + // mock 数据 + InfraCategoryDO dbCategory = randomPojo(InfraCategoryDO.class); + categoryMapper.insert(dbCategory);// @Sql: 先插入出一条存在的数据 + // 准备参数 + InfraCategorySaveReqVO updateReqVO = randomPojo(InfraCategorySaveReqVO.class, o -> { + o.setId(dbCategory.getId()); // 设置更新的 ID + }); + + // 调用 + categoryService.updateCategory(updateReqVO); + // 校验是否更新正确 + InfraCategoryDO category = categoryMapper.selectById(updateReqVO.getId()); // 获取最新的 + assertPojoEquals(updateReqVO, category); + } + + @Test + public void testUpdateCategory_notExists() { + // 准备参数 + InfraCategorySaveReqVO updateReqVO = randomPojo(InfraCategorySaveReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> categoryService.updateCategory(updateReqVO), CATEGORY_NOT_EXISTS); + } + + @Test + public void testDeleteCategory_success() { + // mock 数据 + InfraCategoryDO dbCategory = randomPojo(InfraCategoryDO.class); + categoryMapper.insert(dbCategory);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbCategory.getId(); + + // 调用 + categoryService.deleteCategory(id); + // 校验数据不存在了 + assertNull(categoryMapper.selectById(id)); + } + + @Test + public void testDeleteCategory_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> categoryService.deleteCategory(id), CATEGORY_NOT_EXISTS); + } + + @Test + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 + public void testGetCategoryList() { + // mock 数据 + InfraCategoryDO dbCategory = randomPojo(InfraCategoryDO.class, o -> { // 等会查询到 + o.setName(null); + }); + categoryMapper.insert(dbCategory); + // 测试 name 不匹配 + categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setName(null))); + // 准备参数 + InfraCategoryListReqVO reqVO = new InfraCategoryListReqVO(); + reqVO.setName(null); + + // 调用 + List list = categoryService.getCategoryList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbCategory, list.get(0)); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/sql/h2 b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/sql/h2 new file mode 100644 index 0000000..4141766 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/sql/h2 @@ -0,0 +1,10 @@ +-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里 +CREATE TABLE IF NOT EXISTS "infra_category" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar NOT NULL, + "description" bigint NOT NULL, + PRIMARY KEY ("id") +) COMMENT '分类表'; + +-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里 +DELETE FROM "infra_category"; \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/sql/sql b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/sql/sql new file mode 100644 index 0000000..ccd892d --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/sql/sql @@ -0,0 +1,54 @@ +-- 菜单 SQL +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status, component_name +) +VALUES ( + '分类管理', '', 2, 0, 888, + 'category', '', 'infra/demo/index', 0, 'InfraCategory' +); + +-- 按钮父菜单ID +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '分类查询', 'infra:category:query', 3, 1, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '分类创建', 'infra:category:create', 3, 2, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '分类更新', 'infra:category:update', 3, 3, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '分类删除', 'infra:category:delete', 3, 4, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '分类导出', 'infra:category:export', 3, 5, @parentId, + '', '', '', 0 +); \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/ts/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/ts/index new file mode 100644 index 0000000..3fc4a1c --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/ts/index @@ -0,0 +1,34 @@ +import { http } from '@/http/http' + +/** 分类信息 */ +export interface Category { + id?: number + name: string + parentId: number + children?: Category[] +} + +/** 获取分类列表 */ +export function getCategoryList(params?: Record) { + return http.get('/infra/category/list', params) +} + +/** 获取分类详情 */ +export function getCategory(id: number) { + return http.get(`/infra/category/get?id=${id}`) +} + +/** 创建分类 */ +export function createCategory(data: Category) { + return http.post('/infra/category/create', data) +} + +/** 更新分类 */ +export function updateCategory(data: Category) { + return http.put('/infra/category/update', data) +} + +/** 删除分类 */ +export function deleteCategory(id: number) { + return http.delete(`/infra/category/delete?id=${id}`) +} diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/breadcrumb b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/breadcrumb new file mode 100644 index 0000000..2a76e93 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/breadcrumb @@ -0,0 +1,87 @@ + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/detail/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/detail/index new file mode 100644 index 0000000..93d65fa --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/detail/index @@ -0,0 +1,128 @@ + + + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/form/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/form/index new file mode 100644 index 0000000..26d3319 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/form/index @@ -0,0 +1,137 @@ + + + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/index new file mode 100644 index 0000000..064b26b --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/index @@ -0,0 +1,145 @@ + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/search-form b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/search-form new file mode 100644 index 0000000..b5616f0 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/search-form @@ -0,0 +1,69 @@ + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/xml/InfraCategoryMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/xml/InfraCategoryMapper new file mode 100644 index 0000000..025ac85 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/xml/InfraCategoryMapper @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index de6f6d6..e51ee22 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -164,36 +164,28 @@ spring: client: host: 127.0.0.1 port: 19530 - qianfan: # 文心一言 - api-key: x0cuLZ7XsaTCU08vuJWO87Lg - secret-key: R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK - zhipuai: # 智谱 AI - api-key: 32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs openai: # OpenAI 官方 - api-key: sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17 - base-url: https://api.gptsapi.net + base-url: ${OPENAI_BASE_URL:https://api.openai.com} + api-key: ${OPENAI_API_KEY:sk-xxxx} azure: # OpenAI 微软 openai: endpoint: https://eastusprejade.openai.azure.com anthropic: # Anthropic Claude - api-key: sk-muubv7cXeLw0Etgs743f365cD5Ea44429946Fa7e672d8942 + base-url: ${ANTHROPIC_BASE_URL:https://api.anthropic.com} + api-key: ${ANTHROPIC_API_KEY:sk-xxxx} ollama: base-url: http://127.0.0.1:11434 chat: model: llama3 stabilityai: - api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx + api-key: ${STABILITYAI_API_KEY:sk-xxxx} dashscope: # 通义千问 - api-key: sk-47aa124781be4bfb95244cc62f6xxxx - minimax: # Minimax:https://www.minimaxi.com/ - api-key: xxxx - moonshot: # 月之暗面(KIMI) - api-key: sk-abc + api-key: ${DASHSCOPE_API_KEY:sk-xxxx} deepseek: # DeepSeek - api-key: sk-e94db327cc7d457d99a8de8810fc6b12 + api-key: ${DEEPSEEK_API_KEY:sk-xxxx} chat: options: - model: deepseek-chat + model: deepseek-v4-flash model: rerank: false # 是否开启“通义千问”的 Rerank 模型,填写 dashscope 开启 mcp: @@ -218,34 +210,57 @@ yudao: ai: gemini: # 谷歌 Gemini enable: true - api-key: AIzaSyAVoBxgoFvvte820vEQMma2LKBnC98bqMQ + api-key: ${GEMINI_API_KEY:sk-xxxx} model: gemini-2.5-flash doubao: # 字节豆包 enable: true - api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272 - model: doubao-1-5-lite-32k-250115 + api-key: ${DOUBAO_API_KEY:sk-xxxx} + model: doubao-seed-2-1-turbo-260628 hunyuan: # 腾讯混元 enable: true - api-key: sk-abc - model: hunyuan-turbo + api-key: ${HUNYUAN_API_KEY:sk-xxxx} + model: hy3-preview siliconflow: # 硅基流动 enable: true - api-key: sk-epsakfenqnyzoxhmbucsxlhkdqlcbnimslqoivkshalvdozz - model: deepseek-ai/DeepSeek-R1-Distill-Qwen-7B + api-key: ${SILICONFLOW_API_KEY:sk-xxxx} + model: deepseek-ai/DeepSeek-V4-Pro xinghuo: # 讯飞星火 enable: true - appKey: 75b161ed2aef4719b275d6e7f2a4d4cd - secretKey: YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz - model: x1 + api-key: ${XINGHUO_API_KEY:sk-xxxx} + model: x2-flash baichuan: # 百川智能 enable: true - api-key: sk-abc - model: Baichuan4-Turbo + api-key: ${BAICHUAN_API_KEY:sk-xxxx} + model: Baichuan-M3 + yiyan: # 文心一言 + enable: false + api-key: ${YIYAN_API_KEY:sk-xxxx} + model: ernie-5.1 + zhipu: # 智谱 AI(GLM) + enable: false + api-key: ${ZHIPU_API_KEY:sk-xxxx} + model: glm-5.2 + minimax: # MiniMax + enable: false + api-key: ${MINIMAX_API_KEY:sk-xxxx} + model: MiniMax-M3 + moonshot: # 月之暗面(KIMI) + enable: false + api-key: ${MOONSHOT_API_KEY:sk-xxxx} + model: kimi-k2.6 + stepfun: # 阶跃星辰 + enable: false + api-key: ${STEPFUN_API_KEY:sk-xxxx} + model: step-3.7-flash + grok: # Grok + enable: false + api-key: ${GROK_API_KEY:sk-xxxx} + model: grok-4.3 midjourney: enable: true # base-url: https://api.holdai.top/mj-relax/mj base-url: https://api.holdai.top/mj - api-key: sk-dZEPiVaNcT3FHhef51996bAa0bC74806BeAb620dA5Da10Bf + api-key: ${MIDJOURNEY_API_KEY:sk-xxxx} notify-url: http://java.nat300.top/admin-api/ai/image/midjourney/notify suno: enable: true @@ -253,7 +268,7 @@ yudao: base-url: http://127.0.0.1:3001 web-search: enable: true - api-key: sk-40500e52840f4d24b956d0b1d80d9abe + api-key: ${WEB_SEARCH_API_KEY:sk-xxxx} --- #################### 芋道相关配置 #################### @@ -351,4 +366,4 @@ yudao: message-bus: type: redis # 消息总线的类型 -debug: false \ No newline at end of file +debug: false