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 @@
+
+
+
+
+
+ 全部${table.classComment}
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+#foreach($column in $columns)
+#if ($column.primaryKey || $column.listOperationResult || $column.createOperation || $column.updateOperation)
+#if ($column.dictType && "" != $column.dictType)
+
+
+
+#elseif ($column.javaType == "LocalDateTime")
+
+#else
+
+#end
+#end
+#end
+
+
+#foreach ($subTable in $subTables)
+#set ($index = $foreach.count - 1)
+#set ($subColumns = $subColumnsList.get($index))
+#set ($subClassNameVar = $subClassNameVars.get($index))
+#set ($subPrimaryColumn = $subPrimaryColumns.get($index))
+
+
+ ${subTable.classComment}#if($subTable.subJoinMany)({{ ${subClassNameVar}s.length }})#end
+
+
+#if ($subTable.subJoinMany)
+
+ 暂无${subTable.classComment}
+
+
+#foreach ($column in $subColumns)
+#if ($column.listOperationResult && !$column.primaryKey)
+
+ ${column.columnComment}:{{ item.${column.javaField} ?? '-' }}
+
+#end
+#end
+
+#else
+
+
+#foreach ($column in $subColumns)
+#if ($column.listOperationResult && !$column.primaryKey)
+
+ ${column.columnComment}:{{ ${subClassNameVar}.${column.javaField} ?? '-' }}
+
+#end
+#end
+
+
+ 暂无${subTable.classComment}
+
+
+#end
+
+#end
+
+
+
+
+
+
+
+
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
formData.${javaField} = value"
/>
- ## 文本域
- #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
+
+ item.${column.javaField} = value"
+ />
+#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
+
+ ${subClassNameVar}.${column.javaField} = value"
+ />
+#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
+
+
+
+
+
+
+
+
+#foreach ($column in $subColumns)
+#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
+#set ($javaType = $column.javaType.toLowerCase())
+#if (($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType)
+
+#elseif ($javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime")
+#set ($pickerType = "date")
+#if ($javaType == "localdatetime")
+#set ($pickerType = "datetime")
+#end
+
+ formData.${column.javaField} = value"
+ />
+#else
+
+#if ($javaType == "boolean")
+
+#elseif ($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte")
+
+#elseif ($column.htmlType == "textarea" || $column.htmlType == "editor")
+
+#elseif ($column.htmlType == "imageUpload")
+
+#elseif ($column.htmlType == "fileUpload")
+
+#else
+
+#end
+
+#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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.${treeNameColumn.javaField} }}
+
+
+
+ 子级 ({{ item.children.length }})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 学生联系人({{ studentContactTotal }})
+
+ 添加
+
+
+
+
+
+
+ 学生编号:{{ item.studentId ?? '-' }}
+
+
+ 名字:{{ item.name ?? '-' }}
+
+
+ 简介:{{ item.description ?? '-' }}
+
+
+ 出生日期:{{ item.birthday ?? '-' }}
+
+
+ 性别:{{ item.sex ?? '-' }}
+
+
+ 是否有效:{{ item.enabled ?? '-' }}
+
+
+ 头像:{{ item.avatar ?? '-' }}
+
+
+ 附件:{{ item.video ?? '-' }}
+
+
+ 备注:{{ item.memo ?? '-' }}
+
+
+ 创建时间:{{ item.createTime ?? '-' }}
+
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
+
+
+ 学生班主任({{ studentTeacherTotal }})
+
+ 添加
+
+
+
+
+
+
+ 学生编号:{{ item.studentId ?? '-' }}
+
+
+ 名字:{{ item.name ?? '-' }}
+
+
+ 简介:{{ item.description ?? '-' }}
+
+
+ 出生日期:{{ item.birthday ?? '-' }}
+
+
+ 性别:{{ item.sex ?? '-' }}
+
+
+ 是否有效:{{ item.enabled ?? '-' }}
+
+
+ 头像:{{ item.avatar ?? '-' }}
+
+
+ 附件:{{ item.video ?? '-' }}
+
+
+ 备注:{{ item.memo ?? '-' }}
+
+
+ 创建时间:{{ item.createTime ?? '-' }}
+
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ formData.birthday = value"
+ />
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+ 简介:
+ {{ item.description }}
+
+
+ 出生日期:
+ {{ formatDateTime(item.birthday) || '-' }}
+
+
+ 是否有效:
+
+
+
+ 头像:
+ {{ item.avatar }}
+
+
+ 附件:
+ {{ item.video }}
+
+
+ 备注:
+ {{ item.memo }}
+
+
+ 创建时间:
+ {{ formatDateTime(item.createTime) || '-' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ formData.birthday = value"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ formData.birthday = value"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 学生联系人({{ studentContacts.length }})
+
+
+
+ 暂无学生联系人
+
+
+
+ 学生编号:{{ item.studentId ?? '-' }}
+
+
+ 名字:{{ item.name ?? '-' }}
+
+
+ 简介:{{ item.description ?? '-' }}
+
+
+ 出生日期:{{ item.birthday ?? '-' }}
+
+
+ 性别:{{ item.sex ?? '-' }}
+
+
+ 是否有效:{{ item.enabled ?? '-' }}
+
+
+ 头像:{{ item.avatar ?? '-' }}
+
+
+ 附件:{{ item.video ?? '-' }}
+
+
+ 备注:{{ item.memo ?? '-' }}
+
+
+ 创建时间:{{ item.createTime ?? '-' }}
+
+
+
+
+
+ 学生班主任
+
+
+
+
+
+ 学生编号:{{ studentTeacher.studentId ?? '-' }}
+
+
+ 名字:{{ studentTeacher.name ?? '-' }}
+
+
+ 简介:{{ studentTeacher.description ?? '-' }}
+
+
+ 出生日期:{{ studentTeacher.birthday ?? '-' }}
+
+
+ 性别:{{ studentTeacher.sex ?? '-' }}
+
+
+ 是否有效:{{ studentTeacher.enabled ?? '-' }}
+
+
+ 头像:{{ studentTeacher.avatar ?? '-' }}
+
+
+ 附件:{{ studentTeacher.video ?? '-' }}
+
+
+ 备注:{{ studentTeacher.memo ?? '-' }}
+
+
+ 创建时间:{{ studentTeacher.createTime ?? '-' }}
+
+
+
+ 暂无学生班主任
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ formData.birthday = value"
+ />
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 学生联系人
+
+ 添加
+
+
+
+
+ 暂无学生联系人
+
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+ item.birthday = value"
+ />
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 学生班主任
+
+ 添加
+
+
+
+
+ 暂无学生班主任
+
+
+
+
+
+
+
+
+
+ studentTeacher.birthday = value"
+ />
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+ 简介:
+ {{ item.description }}
+
+
+ 出生日期:
+ {{ formatDateTime(item.birthday) || '-' }}
+
+
+ 是否有效:
+
+
+
+ 头像:
+ {{ item.avatar }}
+
+
+ 附件:
+ {{ item.video }}
+
+
+ 备注:
+ {{ item.memo }}
+
+
+ 创建时间:
+ {{ formatDateTime(item.createTime) || '-' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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