diff --git a/script/livekit-poc/README.md b/script/livekit-poc/README.md
index 104cdb2..41e7318 100644
--- a/script/livekit-poc/README.md
+++ b/script/livekit-poc/README.md
@@ -15,7 +15,7 @@ bash verify.sh
- 7880:HTTP / WebSocket 信令;
- 7881:WebRTC TCP fallback;
- 7882/UDP:WebRTC 媒体;
-- macOS / Windows:当前 `docker-compose.yml` 走端口映射模式,webhook URL 用 `host.docker.internal:48080` 让容器访问到宿主机 yudao 后端;
+- macOS / Windows:当前 `docker-compose.yml` 走端口映射模式,`rtc.node_ip` 使用 `127.0.0.1` 让浏览器访问映射后的媒体端口,webhook URL 用 `host.docker.internal:48080` 让容器访问到宿主机 yudao 后端;
- macOS 上 host network(`network_mode: host`)需要 Docker Desktop 4.34+ 并在 Settings → Resources → Network 勾选「Enable host networking」,老版本静默失败(容器跑得起来但端口完全不通);
- Linux:可以把 `docker-compose.yml` 改成 `network_mode: host` + 删 `ports:` 段,并把 `livekit.yaml` 的 webhook URL 改为 `http://127.0.0.1:48080/admin-api/im/livekit/webhook`。
diff --git a/script/livekit-poc/livekit.yaml b/script/livekit-poc/livekit.yaml
index 0dd255e..8e52aca 100644
--- a/script/livekit-poc/livekit.yaml
+++ b/script/livekit-poc/livekit.yaml
@@ -10,6 +10,8 @@ rtc:
tcp_port: 7881
udp_port: 7882
use_external_ip: false
+ # macOS / Windows 端口映射模式下显式广播宿主机回环地址,避免把容器 172.x 地址发给浏览器
+ node_ip: 127.0.0.1
# Webhook:成员离开 / 房间结束等事件回调到 yudao 后端做业务态兜底清理
# host.docker.internal 让容器访问宿主机 macOS / Windows 上的 yudao 后端
diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml
index ee1bf82..54fe272 100644
--- a/yudao-dependencies/pom.xml
+++ b/yudao-dependencies/pom.xml
@@ -14,7 +14,7 @@
https://github.com/YunaiV/ruoyi-vue-pro
- 2026.06-SNAPSHOT
+ 2026.07-SNAPSHOT
1.7.2
3.5.15
@@ -59,8 +59,7 @@
1.3.0
5.3.3
2.4.1
- 1.2.83
- 2.0.62
+ 2.0.63
33.6.0-jre
2.14.5
3.13.0
@@ -244,6 +243,12 @@
org.dromara
easy-trans-anno
${easy-trans.version}
+
+
+ com.alibaba
+ fastjson
+
+
@@ -315,6 +320,12 @@
org.apache.rocketmq
rocketmq-spring-boot-starter
${rocketmq-spring.version}
+
+
+ com.alibaba
+ fastjson
+
+
@@ -509,7 +520,7 @@
com.alibaba
fastjson
- ${fastjson.version}
+ ${fastjson2.version}
com.alibaba.fastjson2
@@ -638,6 +649,12 @@
me.zhyd.oauth
JustAuth
${justauth.version}
+
+
+ com.alibaba
+ fastjson
+
+
com.xkcoding.justauth
@@ -661,6 +678,10 @@
org.bouncycastle
bcprov-jdk15on
+
+ com.alibaba
+ fastjson
+
@@ -703,6 +724,12 @@
org.jeecgframework.jimureport
jimureport-spring-boot3-starter
${jimureport.version}
+
+
+ com.alibaba
+ fastjson
+
+
org.jeecgframework.jimureport
@@ -717,6 +744,10 @@
cn.hutool
hutool-core
+
+ com.alibaba
+ fastjson
+
diff --git a/yudao-framework/yudao-common/pom.xml b/yudao-framework/yudao-common/pom.xml
index 357c12b..c43dd2a 100644
--- a/yudao-framework/yudao-common/pom.xml
+++ b/yudao-framework/yudao-common/pom.xml
@@ -134,6 +134,11 @@
+ com.alibaba
+ fastjson
+
+
+
org.dromara
easy-trans-anno
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/OAuth2TokenCommonApi.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/OAuth2TokenCommonApi.java
index c23d936..69da9e6 100644
--- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/OAuth2TokenCommonApi.java
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/OAuth2TokenCommonApi.java
@@ -38,6 +38,15 @@ public interface OAuth2TokenCommonApi {
OAuth2AccessTokenRespDTO removeAccessToken(String accessToken);
/**
+ * 移除用户的所有访问令牌
+ * 注意:该流程中,会移除相关的刷新令牌
+ *
+ * @param userId 用户编号
+ * @param userType 用户类型
+ */
+ void removeAccessToken(Long userId, Integer userType);
+
+ /**
* 刷新访问令牌
*
* @param refreshToken 刷新令牌
diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java
index 00c5466..ac51664 100644
--- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java
+++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java
@@ -23,6 +23,7 @@ import jakarta.validation.ConstraintViolationException;
import jakarta.validation.ValidationException;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.annotation.Order;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.util.Assert;
@@ -53,6 +54,7 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC
* @author 芋道源码
*/
@RestControllerAdvice
+@Order(0) // 优先于三方库默认的全局异常处理器,例如 JimuReport
@AllArgsConstructor
@Slf4j
public class GlobalExceptionHandler {
diff --git a/yudao-module-ai/pom.xml b/yudao-module-ai/pom.xml
index 9dba070..b205413 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
-
-
@@ -256,8 +242,12 @@
org.slf4j
slf4j-reload4j
+
+ com.alibaba
+ fastjson
+
-
\ 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/workflow/AiWorkflowServiceImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/workflow/AiWorkflowServiceImpl.java
index ac16e87..8ee3b4e 100644
--- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/workflow/AiWorkflowServiceImpl.java
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/workflow/AiWorkflowServiceImpl.java
@@ -10,8 +10,8 @@ import cn.iocoder.yudao.module.ai.controller.admin.workflow.vo.AiWorkflowTestReq
import cn.iocoder.yudao.module.ai.dal.dataobject.workflow.AiWorkflowDO;
import cn.iocoder.yudao.module.ai.dal.mysql.workflow.AiWorkflowMapper;
import cn.iocoder.yudao.module.ai.service.model.AiModelService;
-import com.alibaba.fastjson.JSONArray;
-import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
import dev.tinyflow.core.Tinyflow;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
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-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java
index aa4c484..c09f223 100644
--- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java
+++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java
@@ -1,7 +1,10 @@
package cn.iocoder.yudao.module.bpm.controller.admin.definition;
import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.*;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelUpdateReqVO;
@@ -18,6 +21,7 @@ import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
@@ -27,16 +31,20 @@ import org.flowable.engine.repository.ProcessDefinition;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.MODEL_IMPORT_FAIL_INVALID;
@Tag(name = "管理后台 - 流程模型")
@RestController
@@ -112,6 +120,14 @@ public class BpmModelController {
return success(BpmModelConvert.INSTANCE.buildModel(model, bpmnBytes, simpleModel));
}
+ @GetMapping("/export")
+ @Operation(summary = "导出模型")
+ @Parameter(name = "id", description = "编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('bpm:model:export')")
+ public CommonResult exportModel(@RequestParam("id") String id) {
+ return success(modelService.exportModel(id));
+ }
+
@PostMapping("/create")
@Operation(summary = "新建模型")
@PreAuthorize("@ss.hasPermission('bpm:model:create')")
@@ -119,6 +135,36 @@ public class BpmModelController {
return success(modelService.createModel(createRetVO));
}
+ @PostMapping("/import")
+ @Operation(summary = "导入模型")
+ @Parameters({
+ @Parameter(name = "file", description = "流程模型 JSON 文件", required = true),
+ @Parameter(name = "key", description = "流程标识,替换导入文件中的标识", example = "process_yudao"),
+ @Parameter(name = "name", description = "流程名称,替换导入文件中的名称", example = "芋道")
+ })
+ @PreAuthorize("@ss.hasPermission('bpm:model:import')")
+ public CommonResult importModel(@RequestParam("file") MultipartFile file,
+ @RequestParam(value = "key", required = false) String key,
+ @RequestParam(value = "name", required = false) String name) throws IOException {
+ BpmModelSaveReqVO reqVO;
+ try {
+ reqVO = JsonUtils.parseObject(file.getBytes(), BpmModelSaveReqVO.class);
+ } catch (RuntimeException ex) {
+ throw exception(MODEL_IMPORT_FAIL_INVALID);
+ }
+ if (reqVO == null) {
+ throw exception(MODEL_IMPORT_FAIL_INVALID);
+ }
+ if (StrUtil.isNotEmpty(key)) {
+ reqVO.setKey(key);
+ }
+ if (StrUtil.isNotEmpty(name)) {
+ reqVO.setName(name);
+ }
+ ValidationUtils.validate(reqVO);
+ return success(modelService.importModel(reqVO));
+ }
+
@PutMapping("/update")
@Operation(summary = "修改模型")
@PreAuthorize("@ss.hasPermission('bpm:model:update')")
diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java
index 2101b22..68d62a0 100644
--- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java
+++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java
@@ -111,6 +111,18 @@ public interface BpmModelConvert {
model.setMetaInfo(JsonUtils.toJsonString(BeanUtils.toBean(reqVO, BpmModelMetaInfoVO.class)));
}
+ default BpmModelSaveReqVO convertToSaveReqVO(Model model) {
+ BpmModelMetaInfoVO metaInfo = parseMetaInfo(model);
+ BpmModelSaveReqVO reqVO = new BpmModelSaveReqVO();
+ reqVO.setKey(model.getKey());
+ reqVO.setName(model.getName());
+ reqVO.setCategory(model.getCategory());
+ if (metaInfo != null) {
+ BeanUtils.copyProperties(metaInfo, reqVO);
+ }
+ return reqVO;
+ }
+
default BpmModelMetaInfoVO parseMetaInfo(Model model) {
BpmModelMetaInfoVO vo = JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoVO.class);
if (vo == null) {
diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java
index bfd350d..ca4bcb3 100644
--- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java
+++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java
@@ -25,6 +25,7 @@ public interface ErrorCodeConstants {
ErrorCode MODEL_DEPLOY_FAIL_BPMN_USER_TASK_NAME_NOT_EXISTS = new ErrorCode(1_009_002_006, "部署流程失败,原因:BPMN 流程图中,用户任务({})的名字不存在");
ErrorCode MODEL_UPDATE_FAIL_NOT_MANAGER = new ErrorCode(1_009_002_007, "操作流程失败,原因:你不是该流程({})的管理员");
ErrorCode MODEL_DEPLOY_FAIL_FIRST_USER_TASK_CANDIDATE_STRATEGY_ERROR = new ErrorCode(1_009_002_008, "部署流程失败,原因:首个任务({})的审批人不能是【审批人自选】");
+ ErrorCode MODEL_IMPORT_FAIL_INVALID = new ErrorCode(1_009_002_009, "导入流程失败,流程模型文件为空或 JSON 格式不正确");
// ========== 流程定义 1-009-003-000 ==========
ErrorCode PROCESS_DEFINITION_KEY_NOT_MATCH = new ErrorCode(1_009_003_000, "流程定义的标识期望是({}),当前是({}),请修改 BPMN 流程图");
diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java
index 3273d24..9325493 100644
--- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java
+++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java
@@ -41,6 +41,22 @@ public interface BpmModelService {
String createModel(@Valid BpmModelSaveReqVO modelVO);
/**
+ * 导入流程模型
+ *
+ * @param reqVO 导入信息
+ * @return 创建的流程模型的编号
+ */
+ String importModel(@Valid BpmModelSaveReqVO reqVO);
+
+ /**
+ * 导出流程模型
+ *
+ * @param id 编号
+ * @return 导出的流程模型
+ */
+ BpmModelSaveReqVO exportModel(String id);
+
+ /**
* 获得流程模块
*
* @param id 编号
diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java
index c23ea9c..7151851 100644
--- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java
+++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java
@@ -120,6 +120,42 @@ public class BpmModelServiceImpl implements BpmModelService {
}
@Override
+ @Transactional(rollbackFor = Exception.class)
+ public String importModel(@Valid BpmModelSaveReqVO reqVO) {
+ if (!ValidationUtils.isXmlNCName(reqVO.getKey())) {
+ throw exception(MODEL_KEY_VALID);
+ }
+ // 1. 校验流程标识已经存在
+ Model keyModel = getModelByKey(reqVO.getKey());
+ if (keyModel != null) {
+ throw exception(MODEL_KEY_EXISTS, reqVO.getKey());
+ }
+
+ // 2. 创建 Model 对象,并归属到当前租户
+ reqVO.setSort(System.currentTimeMillis());
+ Model model = repositoryService.newModel();
+ BpmModelConvert.INSTANCE.copyToModel(model, reqVO);
+ model.setTenantId(FlowableUtils.getTenantId());
+
+ // 3. 保存模型
+ saveModel(model, reqVO);
+ return model.getId();
+ }
+
+ @Override
+ public BpmModelSaveReqVO exportModel(String id) {
+ Model model = validateModelExists(id);
+ BpmModelSaveReqVO result = BpmModelConvert.INSTANCE.convertToSaveReqVO(model);
+ if (Objects.equals(result.getType(), BpmModelTypeEnum.BPMN.getType())) {
+ byte[] bpmnBytes = getModelBpmnXML(id);
+ result.setBpmnXml(ArrayUtil.isEmpty(bpmnBytes) ? null : StrUtil.utf8Str(bpmnBytes));
+ } else if (Objects.equals(result.getType(), BpmModelTypeEnum.SIMPLE.getType())) {
+ result.setSimpleModel(JsonUtils.parseObject(getModelSimpleJson(id), BpmSimpleModelNodeVO.class));
+ }
+ return result;
+ }
+
+ @Override
@Transactional(rollbackFor = Exception.class) // 因为进行多个操作,所以开启事务
public void updateModel(Long userId, BpmModelSaveReqVO updateReqVO) {
// 1. 校验流程模型存在
@@ -187,7 +223,8 @@ public class BpmModelServiceImpl implements BpmModelService {
}
private Model validateModelExists(String id) {
- Model model = repositoryService.getModel(id);
+ Model model = repositoryService.createModelQuery()
+ .modelId(id).modelTenantId(FlowableUtils.getTenantId()).singleResult();
if (model == null) {
throw exception(MODEL_NOT_EXISTS);
}
@@ -429,7 +466,8 @@ public class BpmModelServiceImpl implements BpmModelService {
@Override
public Model getModel(String id) {
- return repositoryService.getModel(id);
+ return repositoryService.createModelQuery()
+ .modelId(id).modelTenantId(FlowableUtils.getTenantId()).singleResult();
}
@Override
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImplTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImplTest.java
new file mode 100644
index 0000000..217d1b3
--- /dev/null
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImplTest.java
@@ -0,0 +1,175 @@
+package cn.iocoder.yudao.module.bpm.service.definition;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelSaveReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum;
+import org.flowable.engine.RepositoryService;
+import org.flowable.engine.repository.Model;
+import org.flowable.engine.repository.ModelQuery;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.MODEL_NOT_EXISTS;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.AdditionalMatchers.aryEq;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link BpmModelServiceImpl} 的单元测试
+ *
+ * @author 芋道源码
+ */
+public class BpmModelServiceImplTest extends BaseMockitoUnitTest {
+
+ private static final Long TENANT_ID = 1L;
+ private static final String MODEL_ID = "model-id";
+
+ @InjectMocks
+ private BpmModelServiceImpl modelService;
+
+ @Mock
+ private RepositoryService repositoryService;
+ @Mock
+ private ModelQuery modelQuery;
+
+ @BeforeEach
+ public void setUp() {
+ TenantContextHolder.setTenantId(TENANT_ID);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ TenantContextHolder.clear();
+ }
+
+ @Test
+ public void testExportModel_bpmn() {
+ // 准备参数
+ Model model = mockModel(BpmModelTypeEnum.BPMN.getType());
+ String bpmnXml = "";
+ // mock 方法(repositoryService)
+ mockGetModel(model);
+ when(repositoryService.getModelEditorSource(eq(MODEL_ID))).thenReturn(StrUtil.utf8Bytes(bpmnXml));
+
+ // 调用
+ BpmModelSaveReqVO result = modelService.exportModel(MODEL_ID);
+
+ // 断言
+ assertEquals(model.getKey(), result.getKey());
+ assertEquals(model.getName(), result.getName());
+ assertEquals(model.getCategory(), result.getCategory());
+ assertEquals(BpmModelTypeEnum.BPMN.getType(), result.getType());
+ assertEquals(bpmnXml, result.getBpmnXml());
+ assertNull(result.getSimpleModel());
+ }
+
+ @Test
+ public void testExportModel_simple() {
+ // 准备参数
+ Model model = mockModel(BpmModelTypeEnum.SIMPLE.getType());
+ BpmSimpleModelNodeVO simpleModel = new BpmSimpleModelNodeVO();
+ simpleModel.setId("start");
+ simpleModel.setType(BpmSimpleModelNodeTypeEnum.START_NODE.getType());
+ // mock 方法(repositoryService)
+ mockGetModel(model);
+ when(repositoryService.getModelEditorSourceExtra(eq(MODEL_ID)))
+ .thenReturn(JsonUtils.toJsonByte(simpleModel));
+
+ // 调用
+ BpmModelSaveReqVO result = modelService.exportModel(MODEL_ID);
+
+ // 断言
+ assertEquals(BpmModelTypeEnum.SIMPLE.getType(), result.getType());
+ assertNotNull(result.getSimpleModel());
+ assertEquals(simpleModel.getId(), result.getSimpleModel().getId());
+ assertEquals(simpleModel.getType(), result.getSimpleModel().getType());
+ assertNull(result.getBpmnXml());
+ }
+
+ @Test
+ public void testExportModel_notExists() {
+ // mock 方法(repositoryService)
+ when(repositoryService.createModelQuery()).thenReturn(modelQuery);
+ when(modelQuery.modelId(eq(MODEL_ID))).thenReturn(modelQuery);
+ when(modelQuery.modelTenantId(eq(TENANT_ID.toString()))).thenReturn(modelQuery);
+ when(modelQuery.singleResult()).thenReturn(null);
+
+ // 调用,并断言异常
+ assertServiceException(() -> modelService.exportModel(MODEL_ID), MODEL_NOT_EXISTS);
+ }
+
+ @Test
+ public void testImportModel_bpmn() {
+ // 准备参数
+ BpmModelSaveReqVO reqVO = new BpmModelSaveReqVO();
+ reqVO.setKey("test_process");
+ reqVO.setName("测试流程");
+ reqVO.setCategory("OA");
+ reqVO.setType(BpmModelTypeEnum.BPMN.getType());
+ reqVO.setBpmnXml("");
+ reqVO.setStartUserIds(Arrays.asList(10L, 20L));
+ reqVO.setStartDeptIds(Collections.singletonList(30L));
+ reqVO.setManagerUserIds(Collections.singletonList(40L));
+ Model model = mock(Model.class);
+ when(model.getId()).thenReturn(MODEL_ID);
+ // mock 方法(repositoryService)
+ when(repositoryService.createModelQuery()).thenReturn(modelQuery);
+ when(modelQuery.modelTenantId(eq(TENANT_ID.toString()))).thenReturn(modelQuery);
+ when(modelQuery.modelKey(eq(reqVO.getKey()))).thenReturn(modelQuery);
+ when(modelQuery.singleResult()).thenReturn(null);
+ when(repositoryService.newModel()).thenReturn(model);
+
+ // 调用
+ String result = modelService.importModel(reqVO);
+
+ // 断言
+ assertEquals(MODEL_ID, result);
+ verify(model).setTenantId(TENANT_ID.toString());
+ verify(repositoryService).saveModel(same(model));
+ verify(repositoryService).addModelEditorSource(eq(MODEL_ID),
+ aryEq(StrUtil.utf8Bytes(reqVO.getBpmnXml())));
+ ArgumentCaptor metaInfoCaptor = ArgumentCaptor.forClass(String.class);
+ verify(model).setMetaInfo(metaInfoCaptor.capture());
+ BpmModelMetaInfoVO metaInfo = JsonUtils.parseObject(metaInfoCaptor.getValue(), BpmModelMetaInfoVO.class);
+ assertNotNull(metaInfo);
+ assertEquals(Arrays.asList(10L, 20L), metaInfo.getStartUserIds());
+ assertEquals(Collections.singletonList(30L), metaInfo.getStartDeptIds());
+ assertEquals(Collections.singletonList(40L), metaInfo.getManagerUserIds());
+ }
+
+ private Model mockModel(Integer type) {
+ Model model = mock(Model.class);
+ when(model.getKey()).thenReturn("test_process");
+ when(model.getName()).thenReturn("测试流程");
+ when(model.getCategory()).thenReturn("OA");
+ when(model.getCreateTime()).thenReturn(new Date(1_000L));
+ BpmModelMetaInfoVO metaInfo = new BpmModelMetaInfoVO();
+ metaInfo.setType(type);
+ when(model.getMetaInfo()).thenReturn(JsonUtils.toJsonString(metaInfo));
+ return model;
+ }
+
+ private void mockGetModel(Model model) {
+ when(repositoryService.createModelQuery()).thenReturn(modelQuery);
+ when(modelQuery.modelId(eq(MODEL_ID))).thenReturn(modelQuery);
+ when(modelQuery.modelTenantId(eq(TENANT_ID.toString()))).thenReturn(modelQuery);
+ when(modelQuery.singleResult()).thenReturn(model);
+ }
+
+}
diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/ImChannelMessageManagerController.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/ImChannelMessageManagerController.java
index 36a3b60..9eeff67 100644
--- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/ImChannelMessageManagerController.java
+++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/ImChannelMessageManagerController.java
@@ -23,6 +23,8 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
+import java.util.Collections;
+import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -62,19 +64,42 @@ public class ImChannelMessageManagerController {
@PreAuthorize("@ss.hasPermission('im:manager:channel-message:query')")
public CommonResult> getMessagePage(@Valid ImChannelMessagePageReqVO pageReqVO) {
PageResult pageResult = channelMessageService.getMessagePage(pageReqVO);
- if (CollUtil.isEmpty(pageResult.getList())) {
- return success(PageResult.empty(pageResult.getTotal()));
+ return success(new PageResult<>(buildMessageRespVOList(pageResult.getList()), pageResult.getTotal()));
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "获得频道消息详情")
+ @Parameter(name = "id", description = "消息编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('im:manager:channel-message:query')")
+ public CommonResult getMessage(@RequestParam("id") Long id) {
+ ImChannelMessageDO message = channelMessageService.getMessage(id);
+ return success(buildMessageRespVO(message));
+ }
+
+ // ==================== 拼接 VO ====================
+
+ private ImChannelMessageRespVO buildMessageRespVO(ImChannelMessageDO message) {
+ if (message == null) {
+ return null;
+ }
+ return CollUtil.getFirst(buildMessageRespVOList(Collections.singletonList(message)));
+ }
+
+ private List buildMessageRespVOList(List messages) {
+ if (CollUtil.isEmpty(messages)) {
+ return Collections.emptyList();
}
- // 批量查询频道和素材,并回填频道名 / 素材标题
+ // 1. 查询关联数据
Map channelMap = channelService.getChannelMap(
- convertSet(pageResult.getList(), ImChannelMessageDO::getChannelId));
+ convertSet(messages, ImChannelMessageDO::getChannelId));
Map materialMap = channelMaterialService.getMaterialMap(
- convertSet(pageResult.getList(), ImChannelMessageDO::getMaterialId));
- return success(BeanUtils.toBean(pageResult, ImChannelMessageRespVO.class, vo -> {
- MapUtils.findAndThen(channelMap, vo.getChannelId(), c -> vo.setChannelName(c.getName()));
+ convertSet(messages, ImChannelMessageDO::getMaterialId));
+ // 2. 拼接 VO
+ return BeanUtils.toBean(messages, ImChannelMessageRespVO.class, vo -> {
+ MapUtils.findAndThen(channelMap, vo.getChannelId(), channel -> vo.setChannelName(channel.getName()));
MapUtils.findAndThen(materialMap, vo.getMaterialId(),
material -> vo.setMaterialTitle(material.getTitle()).setMaterialCoverUrl(material.getCoverUrl()));
- }));
+ });
}
}
diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/framework/rtc/core/LiveKitClient.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/framework/rtc/core/LiveKitClient.java
index 9aea183..2d1b2dc 100644
--- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/framework/rtc/core/LiveKitClient.java
+++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/framework/rtc/core/LiveKitClient.java
@@ -208,17 +208,21 @@ public class LiveKitClient {
/**
* 签发管理 Token;用于调 Server API(DeleteRoom / ListParticipants / RemoveParticipant 等)
*
+ * @param room 房间名;LiveKit 的 roomAdmin 权限必须限定到具体房间
* @return JWT 字符串
*/
- private String signAdminToken() {
+ private String signAdminToken(String room) {
ImProperties.Rtc cfg = imProperties.getRtc();
// roomAdmin claim 给管理类 API 必备
+ Map video = new HashMap<>();
+ video.put("roomAdmin", true);
+ video.put("room", room);
long nowSec = Instant.now().getEpochSecond();
return JWT.create()
.setIssuer(cfg.getApiKey())
.setNotBefore(new Date(nowSec * 1000))
.setExpiresAt(new Date((nowSec + ADMIN_TOKEN_TTL.getSeconds()) * 1000))
- .setPayload("video", MapUtil.of("roomAdmin", true))
+ .setPayload("video", video)
.setSigner(JWTSignerUtil.hs256(cfg.getApiSecret().getBytes(StandardCharsets.UTF_8)))
.sign();
}
@@ -232,7 +236,7 @@ public class LiveKitClient {
*/
private HttpResponse postTwirp(String path, String room) {
Assert.notBlank(room, "room 不可为空");
- String token = signAdminToken();
+ String token = signAdminToken(room);
return HttpRequest.post(HttpUtils.wsUrlToHttp(imProperties.getRtc().getLivekitUrl()) + path)
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImChannelMessageService.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImChannelMessageService.java
index b1e00ee..8b2d474 100644
--- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImChannelMessageService.java
+++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImChannelMessageService.java
@@ -66,6 +66,14 @@ public interface ImChannelMessageService {
PageResult getMessagePage(ImChannelMessagePageReqVO reqVO);
/**
+ * 获得频道消息
+ *
+ * @param id 消息编号
+ * @return 频道消息
+ */
+ ImChannelMessageDO getMessage(Long id);
+
+ /**
* 删除消息
*
* @param id 消息编号
diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImChannelMessageServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImChannelMessageServiceImpl.java
index fc9ad8e..84ed22a 100644
--- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImChannelMessageServiceImpl.java
+++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImChannelMessageServiceImpl.java
@@ -138,6 +138,11 @@ public class ImChannelMessageServiceImpl implements ImChannelMessageService {
}
@Override
+ public ImChannelMessageDO getMessage(Long id) {
+ return channelMessageMapper.selectById(id);
+ }
+
+ @Override
public void deleteMessage(Long id) {
if (channelMessageMapper.selectById(id) == null) {
throw exception(IM_CHANNEL_MESSAGE_NOT_EXISTS);
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 3a009db..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
@@ -42,23 +42,12 @@
#elseif ($column.htmlType == "datetime" && $listOperationCondition == "BETWEEN")
#elseif (($column.htmlType == "select" || $column.htmlType == "radio") && $dictType && "" != $dictType)
-
-
- ${comment}
-
-
-
- 全部
-
-
- {{ dict.label }}
-
-
-
+
#else
@@ -150,13 +139,13 @@ const formData = reactive({
${column.javaField}: [undefined, undefined] as [number | undefined, number | undefined],
#elseif ($column.dictType && "" != $column.dictType)
#if(${javaType} == "string")
- ${column.javaField}: -1 as -1 | string, // -1 表示全部
+ ${column.javaField}: undefined as string | undefined,
#elseif(${javaType} == "boolean")
- ${column.javaField}: -1 as -1 | boolean, // -1 表示全部
+ ${column.javaField}: undefined as boolean | undefined,
#else
- ${column.javaField}: -1, // -1 表示全部
+ ${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,
@@ -181,11 +170,11 @@ const placeholder = computed(() => {
conditions.push(`${comment}:#[[${]]#formatDate(formData.${javaField}[0])#[[}]]#~#[[${]]#formatDate(formData.${javaField}[1])#[[}]]#`)
}
#elseif ($dictType && "" != $dictType)
- if (formData.${javaField} !== -1) {
+ if (formData.${javaField} !== undefined) {
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}#[[}]]#`)
}
@@ -207,9 +196,7 @@ function handleSearch() {
...formData,
#foreach($column in $columns)
#if ($column.listOperation)
- #if ($column.dictType && "" != $column.dictType)
- ${column.javaField}: formData.${column.javaField} === -1 ? undefined : formData.${column.javaField},
- #elseif ($column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
+ #if ($column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
${column.javaField}: formatDateRange(formData.${column.javaField}),
#end
#end
@@ -224,7 +211,7 @@ function handleReset() {
#if ($column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
formData.${column.javaField} = [undefined, undefined]
#elseif ($column.dictType && "" != $column.dictType)
- formData.${column.javaField} = -1
+ formData.${column.javaField} = undefined
#else
formData.${column.javaField} = undefined
#end
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/main/resources/codegen/vue3_vben5_antdv_next/general/views/form.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antdv_next/general/views/form.vue.vm
index 03db716..c8ff573 100644
--- a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antdv_next/general/views/form.vue.vm
+++ b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antdv_next/general/views/form.vue.vm
@@ -1,5 +1,11 @@
+#set ($selectOptionEnable = false)
+#foreach ($column in $columns)
+ #if (($column.createOperation || $column.updateOperation) && $column.htmlType == "select")
+ #set ($selectOptionEnable = true)
+ #end
+#end
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 ids) {
+ // 删除
+ studentMapper.deleteByIds(ids);
+
+ // 删除子表
+ deleteStudentContactByStudentIds(ids);
+ deleteStudentTeacherByStudentIds(ids);
+ }
+
+
+ private void validateStudentExists(Long id) {
+ if (studentMapper.selectById(id) == null) {
+ throw exception(STUDENT_NOT_EXISTS);
+ }
+ }
+
+ @Override
+ public InfraStudentDO getStudent(Long id) {
+ return studentMapper.selectById(id);
+ }
+
+ @Override
+ public PageResult getStudentPage(InfraStudentPageReqVO pageReqVO) {
+ return studentMapper.selectPage(pageReqVO);
+ }
+
+ // ==================== 子表(学生联系人) ====================
+
+ @Override
+ public List getStudentContactListByStudentId(Long studentId) {
+ return studentContactMapper.selectListByStudentId(studentId);
+ }
+
+ private void createStudentContactList(Long studentId, List list) {
+ if (CollUtil.isEmpty(list)) {
+ return;
+ }
+ list.forEach(o -> o.setStudentId(studentId).clean());
+ studentContactMapper.insertBatch(list);
+ }
+
+ private void updateStudentContactList(Long studentId, List list) {
+ list.forEach(o -> o.setStudentId(studentId).clean());
+ List oldList = studentContactMapper.selectListByStudentId(studentId);
+ List> diffList = diffList(oldList, list, (oldVal, newVal) -> {
+ boolean same = ObjectUtil.equal(oldVal.getId(), newVal.getId());
+ if (same) {
+ newVal.setId(oldVal.getId()).clean(); // 解决更新情况下:updateTime 不更新
+ }
+ return same;
+ });
+
+ // 第二步,批量添加、修改、删除
+ if (CollUtil.isNotEmpty(diffList.get(0))) {
+ studentContactMapper.insertBatch(diffList.get(0));
+ }
+ if (CollUtil.isNotEmpty(diffList.get(1))) {
+ studentContactMapper.updateBatch(diffList.get(1));
+ }
+ if (CollUtil.isNotEmpty(diffList.get(2))) {
+ studentContactMapper.deleteByIds(convertList(diffList.get(2), InfraStudentContactDO::getId));
+ }
+ }
+
+ private void deleteStudentContactByStudentId(Long studentId) {
+ studentContactMapper.deleteByStudentId(studentId);
+ }
+
+ private void deleteStudentContactByStudentIds(List studentIds) {
+ studentContactMapper.deleteByStudentIds(studentIds);
+ }
+
+ // ==================== 子表(学生班主任) ====================
+
+ @Override
+ public InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId) {
+ return studentTeacherMapper.selectByStudentId(studentId);
+ }
+
+ private void createStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) {
+ if (studentTeacher == null) {
+ return;
+ }
+ studentTeacher.setStudentId(studentId);
+ studentTeacherMapper.insert(studentTeacher);
+ }
+
+ private void updateStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) {
+ if (studentTeacher == null) {
+ return;
+ }
+ studentTeacher.setStudentId(studentId).clean();// 解决更新情况下:updateTime 不更新
+ studentTeacherMapper.insertOrUpdate(studentTeacher);
+ }
+
+ private void deleteStudentTeacherByStudentId(Long studentId) {
+ studentTeacherMapper.deleteByStudentId(studentId);
+ }
+
+ private void deleteStudentTeacherByStudentIds(List studentIds) {
+ studentTeacherMapper.deleteByStudentIds(studentIds);
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentServiceImplTest b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentServiceImplTest
new file mode 100644
index 0000000..f1d3bb9
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentServiceImplTest
@@ -0,0 +1,145 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import jakarta.annotation.Resource;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Import;
+import java.util.*;
+import java.time.LocalDateTime;
+
+import static cn.hutool.core.util.RandomUtil.*;
+import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link InfraStudentServiceImpl} 的单元测试类
+ *
+ * @author 芋道源码
+ */
+@Import(InfraStudentServiceImpl.class)
+public class InfraStudentServiceImplTest extends BaseDbUnitTest {
+
+ @Resource
+ private InfraStudentServiceImpl studentService;
+
+ @Resource
+ private InfraStudentMapper studentMapper;
+
+ @Test
+ public void testCreateStudent_success() {
+ // 准备参数
+ InfraStudentSaveReqVO createReqVO = randomPojo(InfraStudentSaveReqVO.class).setId(null);
+
+ // 调用
+ Long studentId = studentService.createStudent(createReqVO);
+ // 断言
+ assertNotNull(studentId);
+ // 校验记录的属性是否正确
+ InfraStudentDO student = studentMapper.selectById(studentId);
+ assertPojoEquals(createReqVO, student, "id");
+ }
+
+ @Test
+ public void testUpdateStudent_success() {
+ // mock 数据
+ InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
+ studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
+ // 准备参数
+ InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class, o -> {
+ o.setId(dbStudent.getId()); // 设置更新的 ID
+ });
+
+ // 调用
+ studentService.updateStudent(updateReqVO);
+ // 校验是否更新正确
+ InfraStudentDO student = studentMapper.selectById(updateReqVO.getId()); // 获取最新的
+ assertPojoEquals(updateReqVO, student);
+ }
+
+ @Test
+ public void testUpdateStudent_notExists() {
+ // 准备参数
+ InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class);
+
+ // 调用, 并断言异常
+ assertServiceException(() -> studentService.updateStudent(updateReqVO), STUDENT_NOT_EXISTS);
+ }
+
+ @Test
+ public void testDeleteStudent_success() {
+ // mock 数据
+ InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
+ studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
+ // 准备参数
+ Long id = dbStudent.getId();
+
+ // 调用
+ studentService.deleteStudent(id);
+ // 校验数据不存在了
+ assertNull(studentMapper.selectById(id));
+ }
+
+ @Test
+ public void testDeleteStudent_notExists() {
+ // 准备参数
+ Long id = randomLongId();
+
+ // 调用, 并断言异常
+ assertServiceException(() -> studentService.deleteStudent(id), STUDENT_NOT_EXISTS);
+ }
+
+ @Test
+ @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+ public void testGetStudentPage() {
+ // mock 数据
+ InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class, o -> { // 等会查询到
+ o.setName(null);
+ o.setBirthday(null);
+ o.setSex(null);
+ o.setEnabled(null);
+ o.setCreateTime(null);
+ });
+ studentMapper.insert(dbStudent);
+ // 测试 name 不匹配
+ studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setName(null)));
+ // 测试 birthday 不匹配
+ studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setBirthday(null)));
+ // 测试 sex 不匹配
+ studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setSex(null)));
+ // 测试 enabled 不匹配
+ studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setEnabled(null)));
+ // 测试 createTime 不匹配
+ studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setCreateTime(null)));
+ // 准备参数
+ InfraStudentPageReqVO reqVO = new InfraStudentPageReqVO();
+ reqVO.setName(null);
+ reqVO.setBirthday(null);
+ reqVO.setSex(null);
+ reqVO.setEnabled(null);
+ reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+ // 调用
+ PageResult pageResult = studentService.getStudentPage(reqVO);
+ // 断言
+ assertEquals(1, pageResult.getTotal());
+ assertEquals(1, pageResult.getList().size());
+ assertPojoEquals(dbStudent, pageResult.getList().get(0));
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentTeacherDO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentTeacherDO
new file mode 100644
index 0000000..c19cf9f
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentTeacherDO
@@ -0,0 +1,71 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 学生班主任 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("infra_student_teacher")
+@KeySequence("infra_student_teacher_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InfraStudentTeacherDO extends BaseDO {
+
+ /**
+ * 编号
+ */
+ @TableId
+ private Long id;
+ /**
+ * 学生编号
+ */
+ private Long studentId;
+ /**
+ * 名字
+ */
+ private String name;
+ /**
+ * 简介
+ */
+ private String description;
+ /**
+ * 出生日期
+ */
+ private LocalDateTime birthday;
+ /**
+ * 性别
+ *
+ * 枚举 {@link TODO system_user_sex 对应的类}
+ */
+ private Integer sex;
+ /**
+ * 是否有效
+ *
+ * 枚举 {@link TODO infra_boolean_string 对应的类}
+ */
+ private Boolean enabled;
+ /**
+ * 头像
+ */
+ private String avatar;
+ /**
+ * 附件
+ */
+ private String video;
+ /**
+ * 备注
+ */
+ private String memo;
+
+}
\ No newline at end of file
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentTeacherMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentTeacherMapper
new file mode 100644
index 0000000..ae4f27d
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentTeacherMapper
@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 学生班主任 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface InfraStudentTeacherMapper extends BaseMapperX {
+
+ default InfraStudentTeacherDO selectByStudentId(Long studentId) {
+ return selectOne(InfraStudentTeacherDO::getStudentId, studentId);
+ }
+
+ default int deleteByStudentId(Long studentId) {
+ return delete(InfraStudentTeacherDO::getStudentId, studentId);
+ }
+
+ default int deleteByStudentIds(List studentIds) {
+ return deleteBatch(InfraStudentTeacherDO::getStudentId, studentIds);
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/sql/h2 b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/sql/h2
new file mode 100644
index 0000000..6c1875f
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/sql/h2
@@ -0,0 +1,17 @@
+-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里
+CREATE TABLE IF NOT EXISTS "infra_student" (
+ "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+ "name" varchar NOT NULL,
+ "description" varchar NOT NULL,
+ "birthday" varchar NOT NULL,
+ "sex" int NOT NULL,
+ "enabled" bit NOT NULL,
+ "avatar" varchar NOT NULL,
+ "video" varchar NOT NULL,
+ "memo" varchar NOT NULL,
+ "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY ("id")
+) COMMENT '学生表';
+
+-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里
+DELETE FROM "infra_student";
\ No newline at end of file
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/sql/sql b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/sql/sql
new file mode 100644
index 0000000..c0e5f74
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/sql/sql
@@ -0,0 +1,54 @@
+-- 菜单 SQL
+INSERT INTO system_menu(
+ name, permission, type, sort, parent_id,
+ path, icon, component, status, component_name
+)
+VALUES (
+ '学生管理', '', 2, 0, 888,
+ 'student', '', 'infra/demo/index', 0, 'InfraStudent'
+);
+
+-- 按钮父菜单ID
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(
+ name, permission, type, sort, parent_id,
+ path, icon, component, status
+)
+VALUES (
+ '学生查询', 'infra:student:query', 3, 1, @parentId,
+ '', '', '', 0
+);
+INSERT INTO system_menu(
+ name, permission, type, sort, parent_id,
+ path, icon, component, status
+)
+VALUES (
+ '学生创建', 'infra:student:create', 3, 2, @parentId,
+ '', '', '', 0
+);
+INSERT INTO system_menu(
+ name, permission, type, sort, parent_id,
+ path, icon, component, status
+)
+VALUES (
+ '学生更新', 'infra:student:update', 3, 3, @parentId,
+ '', '', '', 0
+);
+INSERT INTO system_menu(
+ name, permission, type, sort, parent_id,
+ path, icon, component, status
+)
+VALUES (
+ '学生删除', 'infra:student:delete', 3, 4, @parentId,
+ '', '', '', 0
+);
+INSERT INTO system_menu(
+ name, permission, type, sort, parent_id,
+ path, icon, component, status
+)
+VALUES (
+ '学生导出', 'infra:student:export', 3, 5, @parentId,
+ '', '', '', 0
+);
\ No newline at end of file
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/ts/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/ts/index
new file mode 100644
index 0000000..c98bd3b
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/ts/index
@@ -0,0 +1,87 @@
+import type { PageParam, PageResult } from '@/http/types'
+import { http } from '@/http/http'
+
+/** 学生联系人信息 */
+export interface StudentContact {
+ id?: number
+ studentId?: number
+ name: string
+ description: string
+ birthday: number
+ sex: number
+ enabled: boolean
+ avatar: string
+ video?: string
+ memo: string
+ createTime?: number
+}
+
+/** 学生班主任信息 */
+export interface StudentTeacher {
+ id?: number
+ studentId?: number
+ name: string
+ description: string
+ birthday: number
+ sex: number
+ enabled: boolean
+ avatar: string
+ video?: string
+ memo: string
+ createTime?: number
+}
+
+/** 学生信息 */
+export interface Student {
+ id?: number
+ name: string
+ description: string
+ birthday: number
+ sex: number
+ enabled: boolean
+ avatar: string
+ video: string
+ memo: string
+ createTime?: number
+ studentContacts?: StudentContact[]
+ studentTeacher?: StudentTeacher | null
+}
+
+/** 获取学生分页列表 */
+export function getStudentPage(params: PageParam) {
+ return http.get>('/infra/student/page', params)
+}
+
+/** 获取学生详情 */
+export function getStudent(id: number) {
+ return http.get(`/infra/student/get?id=${id}`)
+}
+
+/** 创建学生 */
+export function createStudent(data: Student) {
+ return http.post('/infra/student/create', data)
+}
+
+/** 更新学生 */
+export function updateStudent(data: Student) {
+ return http.put('/infra/student/update', data)
+}
+
+/** 删除学生 */
+export function deleteStudent(id: number) {
+ return http.delete(`/infra/student/delete?id=${id}`)
+}
+
+// ==================== 子表:学生联系人 ====================
+
+/** 获取学生联系人列表 */
+export function getStudentContactListByStudentId(studentId: number) {
+ return http.get(`/infra/student/student-contact/list-by-student-id?studentId=${studentId}`)
+}
+
+// ==================== 子表:学生班主任 ====================
+
+/** 获取学生班主任 */
+export function getStudentTeacherByStudentId(studentId: number) {
+ return http.get(`/infra/student/student-teacher/get-by-student-id?studentId=${studentId}`)
+}
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/detail/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/detail/index
new file mode 100644
index 0000000..829c797
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/detail/index
@@ -0,0 +1,229 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 学生联系人({{ 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_normal/vue/form/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/form/index
new file mode 100644
index 0000000..a43e177
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/form/index
@@ -0,0 +1,480 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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_normal/vue/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/index
new file mode 100644
index 0000000..acb1b87
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/index
@@ -0,0 +1,169 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 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_normal/vue/search-form b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/search-form
new file mode 100644
index 0000000..0928d14
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/search-form
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+ 名字
+
+
+
+
+
+ 出生日期
+
+
+
+
+
+
+
+
+ 重置
+
+
+ 搜索
+
+
+
+
+
+
+
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/xml/InfraStudentMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/xml/InfraStudentMapper
new file mode 100644
index 0000000..155aa5c
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/xml/InfraStudentMapper
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/ts/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/ts/index
index da8643a..85b1ac7 100644
--- a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/ts/index
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/ts/index
@@ -6,13 +6,13 @@ export interface Student {
id?: number
name: string
description: string
- birthday: Date
+ birthday: number
sex: number
enabled: boolean
avatar: string
video: string
memo: string
- createTime?: Date
+ createTime?: number
}
/** 获取学生分页列表 */
@@ -38,4 +38,4 @@ export function updateStudent(data: Student) {
/** 删除学生 */
export function deleteStudent(id: number) {
return http.delete(`/infra/student/delete?id=${id}`)
-}
\ No newline at end of file
+}
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/detail/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/detail/index
index d6972ec..8d44124 100644
--- a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/detail/index
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/detail/index
@@ -38,7 +38,7 @@
删除
@@ -51,6 +51,7 @@
import type { Student } from '@/api/infra/demo'
import { onUnload } from '@dcloudio/uni-app'
import { onMounted, ref } from 'vue'
+import { useDialog } from '@wot-ui/ui/components/wd-dialog'
import { useToast } from '@wot-ui/ui/components/wd-toast'
import { deleteStudent, getStudent } from '@/api/infra/demo'
import { useAccess } from '@/hooks/useAccess'
@@ -59,7 +60,7 @@ import { DICT_TYPE } from '@/utils/constants'
import { formatDateTime } from '@/utils/date'
const props = defineProps<{
- id?: number | any
+ id?: number
}>()
definePage({
@@ -70,6 +71,7 @@ definePage({
})
const { hasAccessByCodes } = useAccess()
+const dialog = useDialog()
const toast = useToast()
const formData = ref() // 详情数据
const deleting = ref(false) // 删除状态
@@ -100,28 +102,27 @@ function handleEdit() {
}
/** 删除学生 */
-function handleDelete() {
+async function handleDelete() {
if (!props.id) {
return
}
- uni.showModal({
- title: '提示',
- content: '确定要删除该学生吗?',
- success: async (res) => {
- if (!res.confirm) {
- return
- }
- deleting.value = true
- try {
- await deleteStudent(props.id)
- toast.success('删除成功')
- uni.$emit('infra:demo:reload')
- delay(handleBack)
- } finally {
- deleting.value = false
- }
- },
- })
+ try {
+ await dialog.confirm({
+ title: '提示',
+ msg: '确定要删除该学生吗?',
+ })
+ } catch {
+ return
+ }
+ deleting.value = true
+ try {
+ await deleteStudent(props.id)
+ toast.success('删除成功')
+ uni.$emit('infra:demo:reload')
+ delay(handleBack)
+ } finally {
+ deleting.value = false
+ }
}
/** 初始化 */
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/form/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/form/index
index 06e1a54..36cd18d 100644
--- a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/form/index
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/form/index
@@ -11,14 +11,14 @@
-
+
-
+
-
formData.birthday = value"
/>
-
-
+
-
+
-
-
+
+
-
-
+
@@ -93,7 +96,9 @@
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/search-form b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/search-form
index 9571c6e..0928d14 100644
--- a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/search-form
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/search-form
@@ -27,40 +27,18 @@
clearable
/>
-
-
- 性别
-
-
-
- 全部
-
-
- {{ dict.label }}
-
-
-
-
-
- 是否有效
-
-
-
- 全部
-
-
- {{ dict.label }}
-
-
-
+
+
@@ -90,8 +68,8 @@ const visible = ref(false) // 搜索弹窗显示状态
const formData = reactive({
name: undefined as string | undefined,
birthday: undefined as string | undefined,
- sex: -1, // -1 表示全部
- enabled: -1 as -1 | boolean, // -1 表示全部
+ sex: undefined as number | undefined,
+ enabled: undefined as boolean | undefined,
createTime: [undefined, undefined] as [number | undefined, number | undefined],
}) // 搜索表单数据
@@ -104,10 +82,10 @@ const placeholder = computed(() => {
if (formData.birthday) {
conditions.push(`出生日期:${formData.birthday}`)
}
- if (formData.sex !== -1) {
+ if (formData.sex !== undefined) {
conditions.push(`性别:${getDictLabel(DICT_TYPE.SYSTEM_USER_SEX, formData.sex)}`)
}
- if (formData.enabled !== -1) {
+ if (formData.enabled !== undefined) {
conditions.push(`是否有效:${getDictLabel(DICT_TYPE.INFRA_BOOLEAN_STRING, formData.enabled)}`)
}
if (formData.createTime?.[0] && formData.createTime?.[1]) {
@@ -121,8 +99,6 @@ function handleSearch() {
visible.value = false
emit('search', {
...formData,
- sex: formData.sex === -1 ? undefined : formData.sex,
- enabled: formData.enabled === -1 ? undefined : formData.enabled,
createTime: formatDateRange(formData.createTime),
})
}
@@ -131,8 +107,8 @@ function handleSearch() {
function handleReset() {
formData.name = undefined
formData.birthday = undefined
- formData.sex = -1
- formData.enabled = -1
+ formData.sex = undefined
+ formData.enabled = undefined
formData.createTime = [undefined, undefined]
visible.value = false
emit('reset')
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/assert.json b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/assert.json
new file mode 100644
index 0000000..4086c6e
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/assert.json
@@ -0,0 +1,58 @@
+[ {
+ "contentPath" : "java/InfraCategoryListReqVO",
+ "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraCategoryListReqVO.java"
+}, {
+ "contentPath" : "java/InfraCategoryRespVO",
+ "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraCategoryRespVO.java"
+}, {
+ "contentPath" : "java/InfraCategorySaveReqVO",
+ "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraCategorySaveReqVO.java"
+}, {
+ "contentPath" : "java/InfraCategoryController",
+ "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraCategoryController.java"
+}, {
+ "contentPath" : "java/InfraCategoryDO",
+ "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraCategoryDO.java"
+}, {
+ "contentPath" : "java/InfraCategoryMapper",
+ "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraCategoryMapper.java"
+}, {
+ "contentPath" : "xml/InfraCategoryMapper",
+ "filePath" : "yudao-module-infra/src/main/resources/mapper/demo/InfraCategoryMapper.xml"
+}, {
+ "contentPath" : "java/InfraCategoryServiceImpl",
+ "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraCategoryServiceImpl.java"
+}, {
+ "contentPath" : "java/InfraCategoryService",
+ "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraCategoryService.java"
+}, {
+ "contentPath" : "java/InfraCategoryServiceImplTest",
+ "filePath" : "yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraCategoryServiceImplTest.java"
+}, {
+ "contentPath" : "java/ErrorCodeConstants_手动操作",
+ "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
+}, {
+ "contentPath" : "sql/sql",
+ "filePath" : "sql/sql.sql"
+}, {
+ "contentPath" : "sql/h2",
+ "filePath" : "sql/h2.sql"
+}, {
+ "contentPath" : "ts/index",
+ "filePath" : "yudao-ui-admin-uniapp/src/api/infra/demo/index.ts"
+}, {
+ "contentPath" : "vue/search-form",
+ "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/components/search-form.vue"
+}, {
+ "contentPath" : "vue/form/index",
+ "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue"
+}, {
+ "contentPath" : "vue/detail/index",
+ "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue"
+}, {
+ "contentPath" : "vue/index",
+ "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/index.vue"
+}, {
+ "contentPath" : "vue/breadcrumb",
+ "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/components/breadcrumb.vue"
+} ]
\ No newline at end of file
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/ErrorCodeConstants_手动操作 b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/ErrorCodeConstants_手动操作
new file mode 100644
index 0000000..42e399f
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/ErrorCodeConstants_手动操作
@@ -0,0 +1,8 @@
+// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
+// ========== 分类 TODO 补充编号 ==========
+ErrorCode CATEGORY_NOT_EXISTS = new ErrorCode(TODO 补充编号, "分类不存在");
+ErrorCode CATEGORY_EXITS_CHILDREN = new ErrorCode(TODO 补充编号, "存在存在子分类,无法删除");
+ErrorCode CATEGORY_PARENT_NOT_EXITS = new ErrorCode(TODO 补充编号,"父级分类不存在");
+ErrorCode CATEGORY_PARENT_ERROR = new ErrorCode(TODO 补充编号, "不能设置自己为父分类");
+ErrorCode CATEGORY_NAME_DUPLICATE = new ErrorCode(TODO 补充编号, "已经存在该名字的分类");
+ErrorCode CATEGORY_PARENT_IS_CHILD = new ErrorCode(TODO 补充编号, "不能设置自己的子InfraCategory为父InfraCategory");
\ No newline at end of file
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryController b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryController
new file mode 100644
index 0000000..7725253
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryController
@@ -0,0 +1,95 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo;
+
+import org.springframework.web.bind.annotation.*;
+import jakarta.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.security.access.prepost.PreAuthorize;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Operation;
+
+import jakarta.validation.constraints.*;
+import jakarta.validation.*;
+import jakarta.servlet.http.*;
+import java.util.*;
+import java.io.IOException;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+
+import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
+import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
+
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraCategoryDO;
+import cn.iocoder.yudao.module.infra.service.demo.InfraCategoryService;
+
+@Tag(name = "管理后台 - 分类")
+@RestController
+@RequestMapping("/infra/category")
+@Validated
+public class InfraCategoryController {
+
+ @Resource
+ private InfraCategoryService categoryService;
+
+ @PostMapping("/create")
+ @Operation(summary = "创建分类")
+ @PreAuthorize("@ss.hasPermission('infra:category:create')")
+ public CommonResult createCategory(@Valid @RequestBody InfraCategorySaveReqVO createReqVO) {
+ return success(categoryService.createCategory(createReqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "更新分类")
+ @PreAuthorize("@ss.hasPermission('infra:category:update')")
+ public CommonResult updateCategory(@Valid @RequestBody InfraCategorySaveReqVO updateReqVO) {
+ categoryService.updateCategory(updateReqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除分类")
+ @Parameter(name = "id", description = "编号", required = true)
+ @PreAuthorize("@ss.hasPermission('infra:category:delete')")
+ public CommonResult deleteCategory(@RequestParam("id") Long id) {
+ categoryService.deleteCategory(id);
+ return success(true);
+ }
+
+
+ @GetMapping("/get")
+ @Operation(summary = "获得分类")
+ @Parameter(name = "id", description = "编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('infra:category:query')")
+ public CommonResult getCategory(@RequestParam("id") Long id) {
+ InfraCategoryDO category = categoryService.getCategory(id);
+ return success(BeanUtils.toBean(category, InfraCategoryRespVO.class));
+ }
+
+ @GetMapping("/list")
+ @Operation(summary = "获得分类列表")
+ @PreAuthorize("@ss.hasPermission('infra:category:query')")
+ public CommonResult> getCategoryList(@Valid InfraCategoryListReqVO listReqVO) {
+ List list = categoryService.getCategoryList(listReqVO);
+ return success(BeanUtils.toBean(list, InfraCategoryRespVO.class));
+ }
+
+ @GetMapping("/export-excel")
+ @Operation(summary = "导出分类 Excel")
+ @PreAuthorize("@ss.hasPermission('infra:category:export')")
+ @ApiAccessLog(operateType = EXPORT)
+ public void exportCategoryExcel(@Valid InfraCategoryListReqVO listReqVO,
+ HttpServletResponse response) throws IOException {
+ List list = categoryService.getCategoryList(listReqVO);
+ // 导出 Excel
+ ExcelUtils.write(response, "分类.xls", "数据", InfraCategoryRespVO.class,
+ BeanUtils.toBean(list, InfraCategoryRespVO.class));
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryDO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryDO
new file mode 100644
index 0000000..7705f07
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryDO
@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
+
+import lombok.*;
+import java.util.*;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 分类 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("infra_category")
+@KeySequence("infra_category_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InfraCategoryDO extends BaseDO {
+
+ public static final Long PARENT_ID_ROOT = 0L;
+
+ /**
+ * 编号
+ */
+ @TableId
+ private Long id;
+ /**
+ * 名字
+ */
+ private String name;
+ /**
+ * 父编号
+ */
+ private Long parentId;
+
+
+}
\ No newline at end of file
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryListReqVO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryListReqVO
new file mode 100644
index 0000000..e5c6f18
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryListReqVO
@@ -0,0 +1,15 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+
+@Schema(description = "管理后台 - 分类列表 Request VO")
+@Data
+public class InfraCategoryListReqVO {
+
+ @Schema(description = "名字", example = "芋头")
+ private String name;
+
+}
\ No newline at end of file
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryMapper
new file mode 100644
index 0000000..9dadbf1
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryMapper
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraCategoryDO;
+import org.apache.ibatis.annotations.Mapper;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+
+/**
+ * 分类 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface InfraCategoryMapper extends BaseMapperX {
+
+ default List selectList(InfraCategoryListReqVO reqVO) {
+ return selectList(new LambdaQueryWrapperX()
+ .likeIfPresent(InfraCategoryDO::getName, reqVO.getName())
+ .orderByDesc(InfraCategoryDO::getId));
+ }
+
+ default InfraCategoryDO selectByParentIdAndName(Long parentId, String name) {
+ return selectOne(InfraCategoryDO::getParentId, parentId, InfraCategoryDO::getName, name);
+ }
+
+ default Long selectCountByParentId(Long parentId) {
+ return selectCount(InfraCategoryDO::getParentId, parentId);
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryRespVO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryRespVO
new file mode 100644
index 0000000..4e77b00
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryRespVO
@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import cn.idev.excel.annotation.*;
+
+@Schema(description = "管理后台 - 分类 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class InfraCategoryRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @ExcelProperty("编号")
+ private Long id;
+
+ @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
+ @ExcelProperty("名字")
+ private String name;
+
+ @Schema(description = "父编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+ @ExcelProperty("父编号")
+ private Long parentId;
+
+}
\ No newline at end of file
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategorySaveReqVO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategorySaveReqVO
new file mode 100644
index 0000000..5846795
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategorySaveReqVO
@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import jakarta.validation.constraints.*;
+
+@Schema(description = "管理后台 - 分类新增/修改 Request VO")
+@Data
+public class InfraCategorySaveReqVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
+ @NotEmpty(message = "名字不能为空")
+ private String name;
+
+ @Schema(description = "父编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+ @NotNull(message = "父编号不能为空")
+ private Long parentId;
+
+}
\ No newline at end of file
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryService b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryService
new file mode 100644
index 0000000..df887b0
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryService
@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import java.util.*;
+import jakarta.validation.*;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraCategoryDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+
+/**
+ * 分类 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface InfraCategoryService {
+
+ /**
+ * 创建分类
+ *
+ * @param createReqVO 创建信息
+ * @return 编号
+ */
+ Long createCategory(@Valid InfraCategorySaveReqVO createReqVO);
+
+ /**
+ * 更新分类
+ *
+ * @param updateReqVO 更新信息
+ */
+ void updateCategory(@Valid InfraCategorySaveReqVO updateReqVO);
+
+ /**
+ * 删除分类
+ *
+ * @param id 编号
+ */
+ void deleteCategory(Long id);
+
+
+ /**
+ * 获得分类
+ *
+ * @param id 编号
+ * @return 分类
+ */
+ InfraCategoryDO getCategory(Long id);
+
+ /**
+ * 获得分类列表
+ *
+ * @param listReqVO 查询条件
+ * @return 分类列表
+ */
+ List getCategoryList(InfraCategoryListReqVO listReqVO);
+
+}
\ No newline at end of file
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryServiceImpl b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryServiceImpl
new file mode 100644
index 0000000..ecf3566
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryServiceImpl
@@ -0,0 +1,141 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import cn.hutool.core.collection.CollUtil;
+import org.springframework.stereotype.Service;
+import jakarta.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.*;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraCategoryDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraCategoryMapper;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList;
+import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
+
+/**
+ * 分类 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class InfraCategoryServiceImpl implements InfraCategoryService {
+
+ @Resource
+ private InfraCategoryMapper categoryMapper;
+
+ @Override
+ public Long createCategory(InfraCategorySaveReqVO createReqVO) {
+ // 校验父编号的有效性
+ validateParentCategory(null, createReqVO.getParentId());
+ // 校验名字的唯一性
+ validateCategoryNameUnique(null, createReqVO.getParentId(), createReqVO.getName());
+
+ // 插入
+ InfraCategoryDO category = BeanUtils.toBean(createReqVO, InfraCategoryDO.class);
+ categoryMapper.insert(category);
+
+ // 返回
+ return category.getId();
+ }
+
+ @Override
+ public void updateCategory(InfraCategorySaveReqVO updateReqVO) {
+ // 校验存在
+ validateCategoryExists(updateReqVO.getId());
+ // 校验父编号的有效性
+ validateParentCategory(updateReqVO.getId(), updateReqVO.getParentId());
+ // 校验名字的唯一性
+ validateCategoryNameUnique(updateReqVO.getId(), updateReqVO.getParentId(), updateReqVO.getName());
+
+ // 更新
+ InfraCategoryDO updateObj = BeanUtils.toBean(updateReqVO, InfraCategoryDO.class);
+ categoryMapper.updateById(updateObj);
+ }
+
+ @Override
+ public void deleteCategory(Long id) {
+ // 校验存在
+ validateCategoryExists(id);
+ // 校验是否有子分类
+ if (categoryMapper.selectCountByParentId(id) > 0) {
+ throw exception(CATEGORY_EXITS_CHILDREN);
+ }
+ // 删除
+ categoryMapper.deleteById(id);
+ }
+
+
+ private void validateCategoryExists(Long id) {
+ if (categoryMapper.selectById(id) == null) {
+ throw exception(CATEGORY_NOT_EXISTS);
+ }
+ }
+
+ private void validateParentCategory(Long id, Long parentId) {
+ if (parentId == null || CategoryDO.PARENT_ID_ROOT.equals(parentId)) {
+ return;
+ }
+ // 1. 不能设置自己为父分类
+ if (Objects.equals(id, parentId)) {
+ throw exception(CATEGORY_PARENT_ERROR);
+ }
+ // 2. 父分类不存在
+ CategoryDO parentCategory = categoryMapper.selectById(parentId);
+ if (parentCategory == null) {
+ throw exception(CATEGORY_PARENT_NOT_EXITS);
+ }
+ // 3. 递归校验父分类,如果父分类是自己的子分类,则报错,避免形成环路
+ if (id == null) { // id 为空,说明新增,不需要考虑环路
+ return;
+ }
+ for (int i = 0; i < Short.MAX_VALUE; i++) {
+ // 3.1 校验环路
+ parentId = parentCategory.getParentId();
+ if (Objects.equals(id, parentId)) {
+ throw exception(CATEGORY_PARENT_IS_CHILD);
+ }
+ // 3.2 继续递归下一级父分类
+ if (parentId == null || CategoryDO.PARENT_ID_ROOT.equals(parentId)) {
+ break;
+ }
+ parentCategory = categoryMapper.selectById(parentId);
+ if (parentCategory == null) {
+ break;
+ }
+ }
+ }
+
+ private void validateCategoryNameUnique(Long id, Long parentId, String name) {
+ CategoryDO category = categoryMapper.selectByParentIdAndName(parentId, name);
+ if (category == null) {
+ return;
+ }
+ // 如果 id 为空,说明不用比较是否为相同 id 的分类
+ if (id == null) {
+ throw exception(CATEGORY_NAME_DUPLICATE);
+ }
+ if (!Objects.equals(category.getId(), id)) {
+ throw exception(CATEGORY_NAME_DUPLICATE);
+ }
+ }
+
+ @Override
+ public InfraCategoryDO getCategory(Long id) {
+ return categoryMapper.selectById(id);
+ }
+
+ @Override
+ public List getCategoryList(InfraCategoryListReqVO listReqVO) {
+ return categoryMapper.selectList(listReqVO);
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryServiceImplTest b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryServiceImplTest
new file mode 100644
index 0000000..92672ee
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryServiceImplTest
@@ -0,0 +1,128 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import jakarta.annotation.Resource;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraCategoryDO;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraCategoryMapper;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Import;
+import java.util.*;
+import java.time.LocalDateTime;
+
+import static cn.hutool.core.util.RandomUtil.*;
+import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link InfraCategoryServiceImpl} 的单元测试类
+ *
+ * @author 芋道源码
+ */
+@Import(InfraCategoryServiceImpl.class)
+public class InfraCategoryServiceImplTest extends BaseDbUnitTest {
+
+ @Resource
+ private InfraCategoryServiceImpl categoryService;
+
+ @Resource
+ private InfraCategoryMapper categoryMapper;
+
+ @Test
+ public void testCreateCategory_success() {
+ // 准备参数
+ InfraCategorySaveReqVO createReqVO = randomPojo(InfraCategorySaveReqVO.class).setId(null);
+
+ // 调用
+ Long categoryId = categoryService.createCategory(createReqVO);
+ // 断言
+ assertNotNull(categoryId);
+ // 校验记录的属性是否正确
+ InfraCategoryDO category = categoryMapper.selectById(categoryId);
+ assertPojoEquals(createReqVO, category, "id");
+ }
+
+ @Test
+ public void testUpdateCategory_success() {
+ // mock 数据
+ InfraCategoryDO dbCategory = randomPojo(InfraCategoryDO.class);
+ categoryMapper.insert(dbCategory);// @Sql: 先插入出一条存在的数据
+ // 准备参数
+ InfraCategorySaveReqVO updateReqVO = randomPojo(InfraCategorySaveReqVO.class, o -> {
+ o.setId(dbCategory.getId()); // 设置更新的 ID
+ });
+
+ // 调用
+ categoryService.updateCategory(updateReqVO);
+ // 校验是否更新正确
+ InfraCategoryDO category = categoryMapper.selectById(updateReqVO.getId()); // 获取最新的
+ assertPojoEquals(updateReqVO, category);
+ }
+
+ @Test
+ public void testUpdateCategory_notExists() {
+ // 准备参数
+ InfraCategorySaveReqVO updateReqVO = randomPojo(InfraCategorySaveReqVO.class);
+
+ // 调用, 并断言异常
+ assertServiceException(() -> categoryService.updateCategory(updateReqVO), CATEGORY_NOT_EXISTS);
+ }
+
+ @Test
+ public void testDeleteCategory_success() {
+ // mock 数据
+ InfraCategoryDO dbCategory = randomPojo(InfraCategoryDO.class);
+ categoryMapper.insert(dbCategory);// @Sql: 先插入出一条存在的数据
+ // 准备参数
+ Long id = dbCategory.getId();
+
+ // 调用
+ categoryService.deleteCategory(id);
+ // 校验数据不存在了
+ assertNull(categoryMapper.selectById(id));
+ }
+
+ @Test
+ public void testDeleteCategory_notExists() {
+ // 准备参数
+ Long id = randomLongId();
+
+ // 调用, 并断言异常
+ assertServiceException(() -> categoryService.deleteCategory(id), CATEGORY_NOT_EXISTS);
+ }
+
+ @Test
+ @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+ public void testGetCategoryList() {
+ // mock 数据
+ InfraCategoryDO dbCategory = randomPojo(InfraCategoryDO.class, o -> { // 等会查询到
+ o.setName(null);
+ });
+ categoryMapper.insert(dbCategory);
+ // 测试 name 不匹配
+ categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setName(null)));
+ // 准备参数
+ InfraCategoryListReqVO reqVO = new InfraCategoryListReqVO();
+ reqVO.setName(null);
+
+ // 调用
+ List list = categoryService.getCategoryList(reqVO);
+ // 断言
+ assertEquals(1, list.size());
+ assertPojoEquals(dbCategory, list.get(0));
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/sql/h2 b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/sql/h2
new file mode 100644
index 0000000..4141766
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/sql/h2
@@ -0,0 +1,10 @@
+-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里
+CREATE TABLE IF NOT EXISTS "infra_category" (
+ "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+ "name" varchar NOT NULL,
+ "description" bigint NOT NULL,
+ PRIMARY KEY ("id")
+) COMMENT '分类表';
+
+-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里
+DELETE FROM "infra_category";
\ No newline at end of file
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/sql/sql b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/sql/sql
new file mode 100644
index 0000000..ccd892d
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/sql/sql
@@ -0,0 +1,54 @@
+-- 菜单 SQL
+INSERT INTO system_menu(
+ name, permission, type, sort, parent_id,
+ path, icon, component, status, component_name
+)
+VALUES (
+ '分类管理', '', 2, 0, 888,
+ 'category', '', 'infra/demo/index', 0, 'InfraCategory'
+);
+
+-- 按钮父菜单ID
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(
+ name, permission, type, sort, parent_id,
+ path, icon, component, status
+)
+VALUES (
+ '分类查询', 'infra:category:query', 3, 1, @parentId,
+ '', '', '', 0
+);
+INSERT INTO system_menu(
+ name, permission, type, sort, parent_id,
+ path, icon, component, status
+)
+VALUES (
+ '分类创建', 'infra:category:create', 3, 2, @parentId,
+ '', '', '', 0
+);
+INSERT INTO system_menu(
+ name, permission, type, sort, parent_id,
+ path, icon, component, status
+)
+VALUES (
+ '分类更新', 'infra:category:update', 3, 3, @parentId,
+ '', '', '', 0
+);
+INSERT INTO system_menu(
+ name, permission, type, sort, parent_id,
+ path, icon, component, status
+)
+VALUES (
+ '分类删除', 'infra:category:delete', 3, 4, @parentId,
+ '', '', '', 0
+);
+INSERT INTO system_menu(
+ name, permission, type, sort, parent_id,
+ path, icon, component, status
+)
+VALUES (
+ '分类导出', 'infra:category:export', 3, 5, @parentId,
+ '', '', '', 0
+);
\ No newline at end of file
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/ts/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/ts/index
new file mode 100644
index 0000000..3fc4a1c
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/ts/index
@@ -0,0 +1,34 @@
+import { http } from '@/http/http'
+
+/** 分类信息 */
+export interface Category {
+ id?: number
+ name: string
+ parentId: number
+ children?: Category[]
+}
+
+/** 获取分类列表 */
+export function getCategoryList(params?: Record) {
+ return http.get('/infra/category/list', params)
+}
+
+/** 获取分类详情 */
+export function getCategory(id: number) {
+ return http.get(`/infra/category/get?id=${id}`)
+}
+
+/** 创建分类 */
+export function createCategory(data: Category) {
+ return http.post('/infra/category/create', data)
+}
+
+/** 更新分类 */
+export function updateCategory(data: Category) {
+ return http.put('/infra/category/update', data)
+}
+
+/** 删除分类 */
+export function deleteCategory(id: number) {
+ return http.delete(`/infra/category/delete?id=${id}`)
+}
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/breadcrumb b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/breadcrumb
new file mode 100644
index 0000000..2a76e93
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/breadcrumb
@@ -0,0 +1,87 @@
+
+
+
+
+
+ 全部分类
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+
+
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/detail/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/detail/index
new file mode 100644
index 0000000..93d65fa
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/detail/index
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/form/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/form/index
new file mode 100644
index 0000000..26d3319
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/form/index
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/index
new file mode 100644
index 0000000..064b26b
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/index
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+ 子级 ({{ item.children.length }})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/search-form b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/search-form
new file mode 100644
index 0000000..b5616f0
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/search-form
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+ 名字
+
+
+
+
+
+ 重置
+
+
+ 搜索
+
+
+
+
+
+
+
diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/xml/InfraCategoryMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/xml/InfraCategoryMapper
new file mode 100644
index 0000000..025ac85
--- /dev/null
+++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/xml/InfraCategoryMapper
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/AppCouponTemplateController.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/AppCouponTemplateController.java
index 5f80f4e..116b898 100755
--- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/AppCouponTemplateController.java
+++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/AppCouponTemplateController.java
@@ -50,7 +50,7 @@ public class AppCouponTemplateController {
@Operation(summary = "获得优惠劵模版")
@Parameter(name = "id", description = "优惠券模板编号", required = true, example = "1024")
@PermitAll
- public CommonResult getCouponTemplate(Long id) {
+ public CommonResult getCouponTemplate(@RequestParam("id") Long id) {
CouponTemplateDO template = couponTemplateService.getCouponTemplate(id);
if (template == null) {
return success(null);
diff --git a/yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java b/yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java
index d8c2377..8e1bda1 100644
--- a/yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java
+++ b/yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java
@@ -88,10 +88,7 @@ public class MemberAuthServiceImpl implements MemberAuthService {
Assert.notNull(user, "获取用户失败,结果为空");
// 校验是否禁用
- if (CommonStatusEnum.isDisable(user.getStatus())) {
- createLoginLog(user.getId(), reqVO.getMobile(), LoginLogTypeEnum.LOGIN_SMS, LoginResultEnum.USER_DISABLED);
- throw exception(AUTH_LOGIN_USER_DISABLED);
- }
+ validateUserStatus(user, reqVO.getMobile(), LoginLogTypeEnum.LOGIN_SMS);
// 如果 socialType 非空,说明需要绑定社交用户
String openid = null;
@@ -128,6 +125,9 @@ public class MemberAuthServiceImpl implements MemberAuthService {
throw exception(USER_NOT_EXISTS);
}
+ // 校验是否禁用
+ validateUserStatus(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL);
+
// 创建 Token 令牌,记录登录日志
return createTokenAfterLoginSuccess(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL, socialUser.getOpenid());
}
@@ -143,6 +143,7 @@ public class MemberAuthServiceImpl implements MemberAuthService {
MemberUserDO user = userService.createUserIfAbsent(phoneNumberInfo.getPurePhoneNumber(),
getClientIP(), TerminalEnum.WECHAT_MINI_PROGRAM.getTerminal());
Assert.notNull(user, "获取用户失败,结果为空");
+ validateUserStatus(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL);
// 绑定社交用户
String openid = socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
@@ -154,6 +155,9 @@ public class MemberAuthServiceImpl implements MemberAuthService {
private AppAuthLoginRespVO createTokenAfterLoginSuccess(MemberUserDO user, String mobile,
LoginLogTypeEnum logType, String openid) {
+ // 统一校验用户状态,避免登录方式增加后遗漏
+ validateUserStatus(user, mobile, logType);
+
// 插入登陆日志
createLoginLog(user.getId(), mobile, logType, LoginResultEnum.SUCCESS);
// 创建 Token 令牌
@@ -164,6 +168,13 @@ public class MemberAuthServiceImpl implements MemberAuthService {
return AuthConvert.INSTANCE.convert(accessTokenRespDTO, openid);
}
+ private void validateUserStatus(MemberUserDO user, String mobile, LoginLogTypeEnum logType) {
+ if (CommonStatusEnum.isDisable(user.getStatus())) {
+ createLoginLog(user.getId(), mobile, logType, LoginResultEnum.USER_DISABLED);
+ throw exception(AUTH_LOGIN_USER_DISABLED);
+ }
+ }
+
@Override
public String getSocialAuthorizeUrl(Integer type, String redirectUri) {
return socialClientApi.getAuthorizeUrl(type, UserTypeEnum.MEMBER.getValue(), redirectUri);
@@ -182,10 +193,7 @@ public class MemberAuthServiceImpl implements MemberAuthService {
throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
}
// 校验是否禁用
- if (CommonStatusEnum.isDisable(user.getStatus())) {
- createLoginLog(user.getId(), mobile, logTypeEnum, LoginResultEnum.USER_DISABLED);
- throw exception(AUTH_LOGIN_USER_DISABLED);
- }
+ validateUserStatus(user, mobile, logTypeEnum);
return user;
}
diff --git a/yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java b/yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java
index 7cfbeb5..67a59f8 100644
--- a/yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java
+++ b/yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java
@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.*;
+import cn.iocoder.yudao.framework.common.biz.system.oauth2.OAuth2TokenCommonApi;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
@@ -54,9 +55,10 @@ public class MemberUserServiceImpl implements MemberUserService {
@Resource
private SmsCodeApi smsCodeApi;
-
@Resource
private SocialClientApi socialClientApi;
+ @Resource
+ private OAuth2TokenCommonApi oauth2TokenApi;
@Resource
private PasswordEncoder passwordEncoder;
@@ -250,6 +252,11 @@ public class MemberUserServiceImpl implements MemberUserService {
// 更新
MemberUserDO updateObj = MemberUserConvert.INSTANCE.convert(updateReqVO);
memberUserMapper.updateById(updateObj);
+
+ // 如果是禁用用户,则删除其 Token 信息
+ if (CommonStatusEnum.isDisable(updateObj.getStatus())) {
+ oauth2TokenApi.removeAccessToken(updateObj.getId(), UserTypeEnum.MEMBER.getValue());
+ }
}
@VisibleForTesting
diff --git a/yudao-module-member/src/test/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImplStatusTest.java b/yudao-module-member/src/test/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImplStatusTest.java
new file mode 100644
index 0000000..e63a523
--- /dev/null
+++ b/yudao-module-member/src/test/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImplStatusTest.java
@@ -0,0 +1,91 @@
+package cn.iocoder.yudao.module.member.service.auth;
+
+import cn.iocoder.yudao.framework.common.biz.system.oauth2.OAuth2TokenCommonApi;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import cn.iocoder.yudao.module.member.controller.app.auth.vo.AppAuthSocialLoginReqVO;
+import cn.iocoder.yudao.module.member.controller.app.auth.vo.AppAuthWeixinMiniAppLoginReqVO;
+import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
+import cn.iocoder.yudao.module.member.service.user.MemberUserService;
+import cn.iocoder.yudao.module.system.api.logger.LoginLogApi;
+import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
+import cn.iocoder.yudao.module.system.api.social.SocialUserApi;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxPhoneNumberInfoRespDTO;
+import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
+import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum;
+import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
+import static cn.iocoder.yudao.module.member.enums.ErrorCodeConstants.AUTH_LOGIN_USER_DISABLED;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class MemberAuthServiceImplStatusTest extends BaseMockitoUnitTest {
+
+ @InjectMocks
+ private MemberAuthServiceImpl authService;
+
+ @Mock
+ private MemberUserService userService;
+ @Mock
+ private LoginLogApi loginLogApi;
+ @Mock
+ private SocialUserApi socialUserApi;
+ @Mock
+ private SocialClientApi socialClientApi;
+ @Mock
+ private OAuth2TokenCommonApi oauth2TokenApi;
+
+ @Test
+ void testSocialLogin_userDisabled() {
+ // 准备参数
+ AppAuthSocialLoginReqVO reqVO = AppAuthSocialLoginReqVO.builder()
+ .type(SocialTypeEnum.GITEE.getType()).code("code").state("state").build();
+ MemberUserDO user = MemberUserDO.builder().id(1L).mobile("15601691300")
+ .status(CommonStatusEnum.DISABLE.getStatus()).build();
+ when(socialUserApi.getSocialUserByCode(UserTypeEnum.MEMBER.getValue(), reqVO.getType(),
+ reqVO.getCode(), reqVO.getState()))
+ .thenReturn(new SocialUserRespDTO("openid", "nickname", "avatar", user.getId()));
+ when(userService.getUser(user.getId())).thenReturn(user);
+
+ // 调用,并断言
+ assertServiceException(() -> authService.socialLogin(reqVO), AUTH_LOGIN_USER_DISABLED);
+ verify(loginLogApi).createLoginLog(argThat(o ->
+ o.getLogType().equals(LoginLogTypeEnum.LOGIN_SOCIAL.getType())
+ && o.getResult().equals(LoginResultEnum.USER_DISABLED.getResult())
+ && o.getUserId().equals(user.getId())));
+ verify(oauth2TokenApi, never()).createAccessToken(any());
+ }
+
+ @Test
+ void testWeixinMiniAppLogin_userDisabled() {
+ // 准备参数
+ AppAuthWeixinMiniAppLoginReqVO reqVO = AppAuthWeixinMiniAppLoginReqVO.builder()
+ .phoneCode("phone-code").loginCode("login-code").state("state").build();
+ SocialWxPhoneNumberInfoRespDTO phoneNumberInfo = new SocialWxPhoneNumberInfoRespDTO();
+ phoneNumberInfo.setPurePhoneNumber("15601691300");
+ when(socialClientApi.getWxMaPhoneNumberInfo(UserTypeEnum.MEMBER.getValue(), reqVO.getPhoneCode()))
+ .thenReturn(phoneNumberInfo);
+ MemberUserDO user = MemberUserDO.builder().id(1L).mobile(phoneNumberInfo.getPurePhoneNumber())
+ .status(CommonStatusEnum.DISABLE.getStatus()).build();
+ when(userService.createUserIfAbsent(eq(phoneNumberInfo.getPurePhoneNumber()), isNull(), anyInt()))
+ .thenReturn(user);
+
+ // 调用,并断言
+ assertServiceException(() -> authService.weixinMiniAppLogin(reqVO), AUTH_LOGIN_USER_DISABLED);
+ verify(loginLogApi).createLoginLog(argThat(o ->
+ o.getLogType().equals(LoginLogTypeEnum.LOGIN_SOCIAL.getType())
+ && o.getResult().equals(LoginResultEnum.USER_DISABLED.getResult())
+ && o.getUserId().equals(user.getId())));
+ verify(socialUserApi, never()).bindSocialUser(any());
+ verify(oauth2TokenApi, never()).createAccessToken(any());
+ }
+
+}
diff --git a/yudao-module-member/src/test/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImplStatusTest.java b/yudao-module-member/src/test/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImplStatusTest.java
new file mode 100644
index 0000000..6660d8b
--- /dev/null
+++ b/yudao-module-member/src/test/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImplStatusTest.java
@@ -0,0 +1,63 @@
+package cn.iocoder.yudao.module.member.service.user;
+
+import cn.iocoder.yudao.framework.common.biz.system.oauth2.OAuth2TokenCommonApi;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserUpdateReqVO;
+import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
+import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class MemberUserServiceImplStatusTest extends BaseMockitoUnitTest {
+
+ @InjectMocks
+ private MemberUserServiceImpl userService;
+
+ @Mock
+ private MemberUserMapper memberUserMapper;
+ @Mock
+ private OAuth2TokenCommonApi oauth2TokenApi;
+
+ @Test
+ void testUpdateUser_disableRevokesToken() {
+ // 准备参数
+ Long userId = 1L;
+ when(memberUserMapper.selectById(userId)).thenReturn(MemberUserDO.builder().id(userId).build());
+ MemberUserUpdateReqVO reqVO = new MemberUserUpdateReqVO();
+ reqVO.setId(userId);
+ reqVO.setStatus(CommonStatusEnum.DISABLE.getStatus().byteValue());
+
+ // 调用
+ userService.updateUser(reqVO);
+
+ // 断言
+ verify(memberUserMapper).updateById(argThat((MemberUserDO user) -> user.getId().equals(userId)
+ && CommonStatusEnum.isDisable(user.getStatus())));
+ verify(oauth2TokenApi).removeAccessToken(userId, UserTypeEnum.MEMBER.getValue());
+ }
+
+ @Test
+ void testUpdateUser_enableKeepsToken() {
+ // 准备参数
+ Long userId = 1L;
+ when(memberUserMapper.selectById(userId)).thenReturn(MemberUserDO.builder().id(userId).build());
+ MemberUserUpdateReqVO reqVO = new MemberUserUpdateReqVO();
+ reqVO.setId(userId);
+ reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus().byteValue());
+
+ // 调用
+ userService.updateUser(reqVO);
+
+ // 断言
+ verify(oauth2TokenApi, never()).removeAccessToken(userId, UserTypeEnum.MEMBER.getValue());
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/checkrecord/MesDvCheckRecordLineController.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/checkrecord/MesDvCheckRecordLineController.java
index 080971c..7fb9b4c 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/checkrecord/MesDvCheckRecordLineController.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/checkrecord/MesDvCheckRecordLineController.java
@@ -83,6 +83,16 @@ public class MesDvCheckRecordLineController {
return success(new PageResult<>(buildCheckRecordLineRespVOList(pageResult.getList()), pageResult.getTotal()));
}
+ @GetMapping("/list-by-record-id")
+ @Operation(summary = "获得指定点检记录的明细列表")
+ @Parameter(name = "recordId", description = "点检记录编号", required = true)
+ @PreAuthorize("@ss.hasPermission('mes:dv-check-record:query')")
+ public CommonResult> getCheckRecordLineListByRecordId(
+ @RequestParam("recordId") Long recordId) {
+ List list = checkRecordLineService.getCheckRecordLineListByRecordId(recordId);
+ return success(buildCheckRecordLineRespVOList(list));
+ }
+
// ==================== 拼接 VO ====================
private List buildCheckRecordLineRespVOList(List list) {
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/maintenrecord/MesDvMaintenRecordLineController.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/maintenrecord/MesDvMaintenRecordLineController.java
index 39e40c9..72742fd 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/maintenrecord/MesDvMaintenRecordLineController.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/maintenrecord/MesDvMaintenRecordLineController.java
@@ -89,6 +89,16 @@ public class MesDvMaintenRecordLineController {
return success(new PageResult<>(buildMaintenRecordLineRespVOList(pageResult.getList()), pageResult.getTotal()));
}
+ @GetMapping("/list-by-record-id")
+ @Operation(summary = "获得指定保养记录的明细列表")
+ @Parameter(name = "recordId", description = "保养记录编号", required = true)
+ @PreAuthorize("@ss.hasPermission('mes:dv-mainten-record:query')")
+ public CommonResult> getMaintenRecordLineListByRecordId(
+ @RequestParam("recordId") Long recordId) {
+ List list = maintenRecordLineService.getMaintenRecordLineListByRecordId(recordId);
+ return success(buildMaintenRecordLineRespVOList(list));
+ }
+
@GetMapping("/export-excel")
@Operation(summary = "导出设备保养记录明细 Excel")
@PreAuthorize("@ss.hasPermission('mes:dv-mainten-record:export')")
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/repair/MesDvRepairLineController.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/repair/MesDvRepairLineController.java
index 18ec6cd..dbe3155 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/repair/MesDvRepairLineController.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/repair/MesDvRepairLineController.java
@@ -89,6 +89,16 @@ public class MesDvRepairLineController {
return success(new PageResult<>(buildRepairLineRespVOList(pageResult.getList()), pageResult.getTotal()));
}
+ @GetMapping("/list-by-repair-id")
+ @Operation(summary = "获得指定维修工单的明细列表")
+ @Parameter(name = "repairId", description = "维修工单编号", required = true)
+ @PreAuthorize("@ss.hasPermission('mes:dv-repair:query')")
+ public CommonResult> getRepairLineListByRepairId(
+ @RequestParam("repairId") Long repairId) {
+ List list = repairLineService.getRepairLineListByRepairId(repairId);
+ return success(buildRepairLineRespVOList(list));
+ }
+
@GetMapping("/export-excel")
@Operation(summary = "导出维修工单行 Excel")
@PreAuthorize("@ss.hasPermission('mes:dv-repair:export')")
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderBomController.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderBomController.java
index c2d586c..8cfa634 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderBomController.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderBomController.java
@@ -96,6 +96,16 @@ public class MesProWorkOrderBomController {
return success(new PageResult<>(buildWorkOrderBomRespVOList(pageResult.getList()), pageResult.getTotal()));
}
+ @GetMapping("/list-by-work-order-id")
+ @Operation(summary = "获得工单 BOM 列表")
+ @Parameter(name = "workOrderId", description = "工单编号", required = true)
+ @PreAuthorize("@ss.hasPermission('mes:pro-work-order:query')")
+ public CommonResult> getWorkOrderBomListByWorkOrderId(
+ @RequestParam("workOrderId") Long workOrderId) {
+ List list = workOrderBomService.getWorkOrderBomListByWorkOrderId(workOrderId);
+ return success(buildWorkOrderBomRespVOList(list));
+ }
+
@GetMapping("/item-list-by-work-order-id")
@Operation(summary = "获得工单物料需求列表")
@Parameter(name = "workOrderId", description = "工单编号", required = true)
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/stocktaking/task/vo/result/MesWmStockTakingTaskResultRespVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/stocktaking/task/vo/result/MesWmStockTakingTaskResultRespVO.java
index 8f074d8..cc5638c 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/stocktaking/task/vo/result/MesWmStockTakingTaskResultRespVO.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/stocktaking/task/vo/result/MesWmStockTakingTaskResultRespVO.java
@@ -77,6 +77,10 @@ public class MesWmStockTakingTaskResultRespVO {
@ExcelProperty("账面数量")
private BigDecimal quantity;
+ @Schema(description = "实盘数量", example = "98.000000")
+ @ExcelProperty("实盘数量")
+ private BigDecimal takingQuantity;
+
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("创建时间")
private LocalDateTime createTime;
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/repair/MesDvRepairLineService.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/repair/MesDvRepairLineService.java
index 5fdb0e8..3d2b981 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/repair/MesDvRepairLineService.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/repair/MesDvRepairLineService.java
@@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.mes.dal.dataobject.dv.repair.MesDvRepairLineDO;
import jakarta.validation.Valid;
+import java.util.List;
+
/**
* MES 维修工单行 Service 接口
*
@@ -53,6 +55,14 @@ public interface MesDvRepairLineService {
PageResult getRepairLinePage(MesDvRepairLinePageReqVO pageReqVO);
/**
+ * 获得指定维修工单的明细列表
+ *
+ * @param repairId 维修工单编号
+ * @return 明细列表
+ */
+ List getRepairLineListByRepairId(Long repairId);
+
+ /**
* 根据维修工单编号删除所有行
*
* @param repairId 维修工单编号
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/repair/MesDvRepairLineServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/repair/MesDvRepairLineServiceImpl.java
index abd31fd..61dce77 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/repair/MesDvRepairLineServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/repair/MesDvRepairLineServiceImpl.java
@@ -12,6 +12,8 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
+import java.util.List;
+
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
@@ -100,6 +102,11 @@ public class MesDvRepairLineServiceImpl implements MesDvRepairLineService {
}
@Override
+ public List getRepairLineListByRepairId(Long repairId) {
+ return repairLineMapper.selectListByRepairId(repairId);
+ }
+
+ @Override
public void deleteByRepairId(Long repairId) {
repairLineMapper.deleteByRepairId(repairId);
}
diff --git a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceTest.java b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceTest.java
index eb8f694..67c80db 100644
--- a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceTest.java
+++ b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceTest.java
@@ -12,7 +12,6 @@ import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig;
-import com.alibaba.fastjson.JSON;
import jakarta.annotation.Resource;
import jakarta.validation.Validator;
import org.junit.jupiter.api.Test;
@@ -109,7 +108,7 @@ public class PayChannelServiceTest extends BaseDbUnitTest {
AlipayPayClientConfig payClientPublicKeyConfig = randomAlipayPayClientConfig();
PayChannelUpdateReqVO reqVO = randomPojo(PayChannelUpdateReqVO.class, o -> {
o.setStatus(CommonStatusEnum.ENABLE.getStatus());
- o.setConfig(JSON.toJSONString(payClientPublicKeyConfig));
+ o.setConfig(JsonUtils.toJsonString(payClientPublicKeyConfig));
});
// 调用, 并断言异常
diff --git a/yudao-module-report/src/main/java/cn/iocoder/yudao/module/report/framework/jmreport/core/service/JmOnlDragExternalServiceImpl.java b/yudao-module-report/src/main/java/cn/iocoder/yudao/module/report/framework/jmreport/core/service/JmOnlDragExternalServiceImpl.java
index a61a42f..f61512f 100644
--- a/yudao-module-report/src/main/java/cn/iocoder/yudao/module/report/framework/jmreport/core/service/JmOnlDragExternalServiceImpl.java
+++ b/yudao-module-report/src/main/java/cn/iocoder/yudao/module/report/framework/jmreport/core/service/JmOnlDragExternalServiceImpl.java
@@ -1,13 +1,11 @@
package cn.iocoder.yudao.module.report.framework.jmreport.core.service;
-import com.alibaba.fastjson.JSONObject;
import lombok.RequiredArgsConstructor;
import org.jeecg.modules.drag.service.IOnlDragExternalService;
import org.jeecg.modules.drag.vo.DragDictModel;
import org.jeecg.modules.drag.vo.DragLogDTO;
import java.util.List;
-import java.util.Map;
/**
* {@link IOnlDragExternalService} 实现类,提供积木仪表盘的查询等功能
@@ -24,11 +22,6 @@ public class JmOnlDragExternalServiceImpl implements IOnlDragExternalService {
// ========== DictItem 相关 ==========
@Override
- public Map> getManyDictItems(List codeList, List tableDictList) {
- return IOnlDragExternalService.super.getManyDictItems(codeList, tableDictList);
- }
-
- @Override
public List getDictItems(String dictCode) {
return IOnlDragExternalService.super.getDictItems(dictCode);
}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/oauth2/OAuth2TokenApiImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/oauth2/OAuth2TokenApiImpl.java
index 94fed14..60ffdc6 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/oauth2/OAuth2TokenApiImpl.java
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/oauth2/OAuth2TokenApiImpl.java
@@ -42,6 +42,11 @@ public class OAuth2TokenApiImpl implements OAuth2TokenCommonApi {
}
@Override
+ public void removeAccessToken(Long userId, Integer userType) {
+ oauth2TokenService.removeAccessToken(userId, userType);
+ }
+
+ @Override
public OAuth2AccessTokenRespDTO refreshAccessToken(String refreshToken, String clientId) {
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, clientId);
return BeanUtils.toBean(accessTokenDO, OAuth2AccessTokenRespDTO.class);
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/DictTypeController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/DictTypeController.java
index 3bb279d..b85f18c 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/DictTypeController.java
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/DictTypeController.java
@@ -57,7 +57,7 @@ public class DictTypeController {
@Operation(summary = "删除字典类型")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('system:dict:delete')")
- public CommonResult deleteDictType(Long id) {
+ public CommonResult deleteDictType(@RequestParam("id") Long id) {
dictTypeService.deleteDictType(id);
return success(true);
}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/LoginLogController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/LoginLogController.java
index 0361b05..e6ce1a6 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/LoginLogController.java
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/LoginLogController.java
@@ -19,6 +19,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
@@ -39,7 +40,7 @@ public class LoginLogController {
@GetMapping("/get")
@Operation(summary = "获得登录日志")
@PreAuthorize("@ss.hasPermission('system:login-log:query')")
- public CommonResult getLoginLog(Long id) {
+ public CommonResult getLoginLog(@RequestParam("id") Long id) {
LoginLogDO loginLog = loginLogService.getLoginLog(id);
return success(BeanUtils.toBean(loginLog, LoginLogRespVO.class));
}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java
index e19e285..0c78477 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java
@@ -89,7 +89,7 @@ public class MenuController {
@GetMapping("/get")
@Operation(summary = "获取菜单信息")
@PreAuthorize("@ss.hasPermission('system:menu:query')")
- public CommonResult getMenu(Long id) {
+ public CommonResult getMenu(@RequestParam("id") Long id) {
MenuDO menu = menuService.getMenu(id);
return success(BeanUtils.toBean(menu, MenuRespVO.class));
}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/PermissionController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/PermissionController.java
index 147ce82..90168ec 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/PermissionController.java
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/PermissionController.java
@@ -34,7 +34,7 @@ public class PermissionController {
@Parameter(name = "roleId", description = "角色编号", required = true)
@GetMapping("/list-role-menus")
@PreAuthorize("@ss.hasPermission('system:permission:assign-role-menu')")
- public CommonResult> getRoleMenuList(Long roleId) {
+ public CommonResult> getRoleMenuList(@RequestParam("roleId") Long roleId) {
return success(permissionService.getRoleMenuListByRoleId(roleId));
}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialUserController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialUserController.java
index f26ca4d..eb27683 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialUserController.java
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialUserController.java
@@ -57,7 +57,7 @@ public class SocialUserController {
List list = socialUserService.getSocialUserList(getLoginUserId(), UserTypeEnum.ADMIN.getValue());
return success(convertList(list, socialUser -> new SocialUserRespVO() // 返回精简信息
.setId(socialUser.getId()).setType(socialUser.getType()).setOpenid(socialUser.getOpenid())
- .setNickname(socialUser.getNickname()).setAvatar(socialUser.getNickname())));
+ .setNickname(socialUser.getNickname()).setAvatar(socialUser.getAvatar())));
}
// ==================== 社交用户 CRUD ====================
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java
index 7cb7d78..534d743 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java
@@ -134,13 +134,8 @@ public class UserController {
@Operation(summary = "获取用户精简信息列表", description = "只包含被开启的用户,主要用于前端的下拉选项")
public CommonResult> getSimpleUserList(
@RequestParam(value = "deptId", required = false) Long deptId) {
- List list;
- if (deptId != null) {
- List deptIds = Collections.singletonList(deptId);
- list = userService.getDeptUsers(deptIds);
- } else {
- list = userService.getUserListByStatus(CommonStatusEnum.ENABLE.getStatus());
- }
+ List list = userService.getUserListByStatus(
+ CommonStatusEnum.ENABLE.getStatus(), deptId);
// 拼接数据
Map deptMap = deptService.getDeptMap(
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/user/AdminUserMapper.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/user/AdminUserMapper.java
index c0c9be8..4a17944 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/user/AdminUserMapper.java
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/user/AdminUserMapper.java
@@ -41,7 +41,13 @@ public interface AdminUserMapper extends BaseMapperX {
}
default List selectListByStatus(Integer status) {
- return selectList(AdminUserDO::getStatus, status);
+ return selectListByStatusAndDeptId(status, null);
+ }
+
+ default List selectListByStatusAndDeptId(Integer status, Long deptId) {
+ return selectList(new LambdaQueryWrapperX()
+ .eq(AdminUserDO::getStatus, status)
+ .eqIfPresent(AdminUserDO::getDeptId, deptId));
}
default List