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 @@ + + + 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 @@ + + + diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/form/index.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/form/index.vue.vm index 6c13045..b98423d 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/form/index.vue.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/form/index.vue.vm @@ -12,34 +12,48 @@ #foreach($column in $columns) - #if (($column.createOperation || $column.updateOperation) && !$column.primaryKey) - #set ($dictType = $column.dictType) - #set ($javaField = $column.javaField) - #set ($javaType = $column.javaType) - #set ($comment = $column.columnComment) - #set ($dictMethod = "getDictOptions") - #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short") - #set ($dictMethod = "getIntDictOptions") - #elseif ($javaType == "String") - #set ($dictMethod = "getStrDictOptions") - #elseif ($javaType == "Boolean") - #set ($dictMethod = "getBoolDictOptions") - #end - ## 下拉选择(字典)→ yd-form-picker,内部按 dictType 解析选项,适合选项较多 - #if ($column.htmlType == "select" && $dictType && "" != $dictType) +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey) +#set ($dictType = $column.dictType) +#set ($javaField = $column.javaField) +#set ($javaType = $column.javaType) +#set ($comment = $column.columnComment) +#set ($dictMethod = "getDictOptions") +#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short") +#set ($dictMethod = "getIntDictOptions") +#elseif ($javaType == "String") +#set ($dictMethod = "getStrDictOptions") +#elseif ($javaType == "Boolean") +#set ($dictMethod = "getBoolDictOptions") +#end +## 树表的父级选择 +#if ($table.templateType == 2 && $column.id == $treeParentColumn.id) + +## 下拉选择(字典)→ yd-form-picker,内部按 dictType 解析选项,适合选项较多 +#elseif ($column.htmlType == "select" && $dictType && "" != $dictType) - ## 单选(字典)→ radio-group,适合选项较少 - #elseif ($column.htmlType == "radio" && $dictType && "" != $dictType) +## 单选(字典)→ radio-group,适合选项较少 +#elseif ($column.htmlType == "radio" && $dictType && "" != $dictType) - ## 数字类型(无字典) - #elseif (${javaType.toLowerCase()} == "long" || ${javaType.toLowerCase()} == "integer" || ${javaType.toLowerCase()} == "short" || ${javaType.toLowerCase()} == "double" || ${javaType.toLowerCase()} == "bigdecimal" || ${javaType.toLowerCase()} == "byte") +## 数字类型(无字典) +#elseif (${javaType.toLowerCase()} == "long" || ${javaType.toLowerCase()} == "integer" || ${javaType.toLowerCase()} == "short" || ${javaType.toLowerCase()} == "float" || ${javaType.toLowerCase()} == "double" || ${javaType.toLowerCase()} == "bigdecimal" || ${javaType.toLowerCase()} == "byte") - ## 布尔类型 - #elseif (${javaType.toLowerCase()} == "boolean") +## 布尔类型 +#elseif (${javaType.toLowerCase()} == "boolean") - ## 日期时间类型 - #elseif (${javaType.toLowerCase()} == "date" || ${javaType.toLowerCase()} == "localdate" || ${javaType.toLowerCase()} == "localdatetime") - #set ($pickerType = "date") - #if (${javaType.toLowerCase()} == "localdatetime") - #set ($pickerType = "datetime") - #end +## 日期时间类型 +#elseif (${javaType.toLowerCase()} == "date" || ${javaType.toLowerCase()} == "localdate" || ${javaType.toLowerCase()} == "localdatetime") +#set ($pickerType = "date") +#if (${javaType.toLowerCase()} == "localdatetime") +#set ($pickerType = "datetime") +#end - ## 文本域 - #elseif ($column.htmlType == "textarea") +## 文本域 +#elseif ($column.htmlType == "textarea" || $column.htmlType == "editor") - ## 图片上传 → yd-upload-img(包 wd-form-item 才能注册到 wd-form 做校验/错误展示) - #elseif ($column.htmlType == "imageUpload") +## 图片上传 → yd-upload-img(包 wd-form-item 才能注册到 wd-form 做校验/错误展示) +#elseif ($column.htmlType == "imageUpload") - ## 文件上传 → yd-upload-file - #elseif ($column.htmlType == "fileUpload") +## 文件上传 → yd-upload-file +#elseif ($column.htmlType == "fileUpload") - + - ## 默认:文本输入 - #else +## 默认:文本输入 +#else - #end - #end +#end +#end #end +#if ($table.templateType == 10 || $table.templateType == 12) +#foreach ($subTable in $subTables) +#set ($index = $foreach.count - 1) +#set ($subColumns = $subColumnsList.get($index)) +#set ($subJoinColumn = $subJoinColumns.get($index)) +#set ($subClassNameVar = $subClassNameVars.get($index)) + + + + ${subTable.classComment} +#if ($subTable.subJoinMany) + + 添加 + +#else + + 添加 + +#end + + +#if ($subTable.subJoinMany) + + 暂无${subTable.classComment} + + + + + 删除 + + +#foreach ($column in $subColumns) +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id) +#set ($javaType = $column.javaType.toLowerCase()) +#set ($dictMethod = "getDictOptions") +#if ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short") +#set ($dictMethod = "getIntDictOptions") +#elseif ($column.javaType == "String") +#set ($dictMethod = "getStrDictOptions") +#elseif ($column.javaType == "Boolean") +#set ($dictMethod = "getBoolDictOptions") +#end +#if ($column.htmlType == "select" && $column.dictType && "" != $column.dictType) + +#elseif ($column.htmlType == "radio" && $column.dictType && "" != $column.dictType) + + + + {{ dict.label }} + + + +#elseif ($javaType == "boolean") + + + +#elseif ($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte") + + + +#elseif ($javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime") +#set ($subPickerType = "date") +#if ($javaType == "localdatetime") +#set ($subPickerType = "datetime") +#end + + +#elseif ($column.htmlType == "textarea" || $column.htmlType == "editor") + + + +#elseif ($column.htmlType == "imageUpload") + + + +#elseif ($column.htmlType == "fileUpload") + + + +#else + + + +#end +#end +#end + +#else + + 暂无${subTable.classComment} + + +#foreach ($column in $subColumns) +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id) +#set ($javaType = $column.javaType.toLowerCase()) +#set ($dictMethod = "getDictOptions") +#if ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short") +#set ($dictMethod = "getIntDictOptions") +#elseif ($column.javaType == "String") +#set ($dictMethod = "getStrDictOptions") +#elseif ($column.javaType == "Boolean") +#set ($dictMethod = "getBoolDictOptions") +#end +#if ($column.htmlType == "select" && $column.dictType && "" != $column.dictType) + +#elseif ($column.htmlType == "radio" && $column.dictType && "" != $column.dictType) + + + + {{ dict.label }} + + + +#elseif ($javaType == "boolean") + + + +#elseif ($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte") + + + +#elseif ($javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime") +#set ($subPickerType = "date") +#if ($javaType == "localdatetime") +#set ($subPickerType = "datetime") +#end + + +#elseif ($column.htmlType == "textarea" || $column.htmlType == "editor") + + + +#elseif ($column.htmlType == "imageUpload") + + + +#elseif ($column.htmlType == "fileUpload") + + + +#else + + + +#end +#end +#end + +#end + +#end +#end @@ -146,7 +434,7 @@ diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/form_sub_erp.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/form_sub_erp.vue.vm new file mode 100644 index 0000000..5c33246 --- /dev/null +++ b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/form_sub_erp.vue.vm @@ -0,0 +1,250 @@ +#set ($subTable = $subTables.get($subIndex)) +#set ($subColumns = $subColumnsList.get($subIndex)) +#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) +#set ($subPrimaryColumn = $subPrimaryColumns.get($subIndex)) +#set ($subJoinColumn = $subJoinColumns.get($subIndex)) +#set ($subPrimaryJavaType = $subPrimaryColumn.javaType.toLowerCase()) +#if(${subPrimaryJavaType} == "long" || ${subPrimaryJavaType} == "integer" || ${subPrimaryJavaType} == "short" || ${subPrimaryJavaType} == "float" || ${subPrimaryJavaType} == "double" || ${subPrimaryJavaType} == "bigdecimal" || ${subPrimaryJavaType} == "byte") +#set ($subPrimaryTsType = "number") +#else +#set ($subPrimaryTsType = "string") +#end +#set ($subJoinJavaType = $subJoinColumn.javaType.toLowerCase()) +#if(${subJoinJavaType} == "long" || ${subJoinJavaType} == "integer" || ${subJoinJavaType} == "short" || ${subJoinJavaType} == "float" || ${subJoinJavaType} == "double" || ${subJoinJavaType} == "bigdecimal" || ${subJoinJavaType} == "byte") +#set ($subJoinTsType = "number") +#else +#set ($subJoinTsType = "string") +#end +#set ($hasDict = 0) +#set ($hasBoolDict = 0) +#set ($hasDate = 0) +#set ($hasDateTime = 0) +#foreach ($column in $subColumns) +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id) +#if (($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType) +#set ($hasDict = 1) +#if ($column.javaType == "Boolean") +#set ($hasBoolDict = 1) +#end +#end +#if (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate") +#set ($hasDate = 1) +#end +#if (${column.javaType.toLowerCase()} == "localdatetime") +#set ($hasDateTime = 1) +#end +#end +#end + + + diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/index_tree.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/index_tree.vue.vm new file mode 100644 index 0000000..11c8be5 --- /dev/null +++ b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/index_tree.vue.vm @@ -0,0 +1,151 @@ + + + diff --git a/yudao-module-infra/src/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 @@ + + + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/index new file mode 100644 index 0000000..acb1b87 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/index @@ -0,0 +1,169 @@ + + + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/search-form b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/search-form new file mode 100644 index 0000000..0928d14 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/search-form @@ -0,0 +1,116 @@ + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/student-contact/form/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/student-contact/form/index new file mode 100644 index 0000000..9b976da --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/student-contact/form/index @@ -0,0 +1,167 @@ + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/student-teacher/form/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/student-teacher/form/index new file mode 100644 index 0000000..1d2639f --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/student-teacher/form/index @@ -0,0 +1,167 @@ + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/xml/InfraStudentMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/xml/InfraStudentMapper new file mode 100644 index 0000000..155aa5c --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/xml/InfraStudentMapper @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/assert.json b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/assert.json new file mode 100644 index 0000000..0a37471 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/assert.json @@ -0,0 +1,67 @@ +[ { + "contentPath" : "java/InfraStudentPageReqVO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java" +}, { + "contentPath" : "java/InfraStudentRespVO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java" +}, { + "contentPath" : "java/InfraStudentSaveReqVO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java" +}, { + "contentPath" : "java/InfraStudentController", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java" +}, { + "contentPath" : "java/InfraStudentDO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java" +}, { + "contentPath" : "java/InfraStudentContactDO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java" +}, { + "contentPath" : "java/InfraStudentTeacherDO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java" +}, { + "contentPath" : "java/InfraStudentMapper", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java" +}, { + "contentPath" : "java/InfraStudentContactMapper", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java" +}, { + "contentPath" : "java/InfraStudentTeacherMapper", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java" +}, { + "contentPath" : "xml/InfraStudentMapper", + "filePath" : "yudao-module-infra/src/main/resources/mapper/demo/InfraStudentMapper.xml" +}, { + "contentPath" : "java/InfraStudentServiceImpl", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java" +}, { + "contentPath" : "java/InfraStudentService", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java" +}, { + "contentPath" : "java/InfraStudentServiceImplTest", + "filePath" : "yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java" +}, { + "contentPath" : "java/ErrorCodeConstants_手动操作", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java" +}, { + "contentPath" : "sql/sql", + "filePath" : "sql/sql.sql" +}, { + "contentPath" : "sql/h2", + "filePath" : "sql/h2.sql" +}, { + "contentPath" : "ts/index", + "filePath" : "yudao-ui-admin-uniapp/src/api/infra/demo/index.ts" +}, { + "contentPath" : "vue/index", + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/index.vue" +}, { + "contentPath" : "vue/search-form", + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/components/search-form.vue" +}, { + "contentPath" : "vue/form/index", + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue" +}, { + "contentPath" : "vue/detail/index", + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue" +} ] \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/ErrorCodeConstants_手动操作 b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/ErrorCodeConstants_手动操作 new file mode 100644 index 0000000..a4df7d2 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/ErrorCodeConstants_手动操作 @@ -0,0 +1,3 @@ +// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!! +// ========== 学生 TODO 补充编号 ========== +ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在"); \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentContactDO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentContactDO new file mode 100644 index 0000000..17c668e --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentContactDO @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.demo; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; + +/** + * 学生联系人 DO + * + * @author 芋道源码 + */ +@TableName("infra_student_contact") +@KeySequence("infra_student_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InfraStudentContactDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 学生编号 + */ + private Long studentId; + /** + * 名字 + */ + private String name; + /** + * 简介 + */ + private String description; + /** + * 出生日期 + */ + private LocalDateTime birthday; + /** + * 性别 + * + * 枚举 {@link TODO system_user_sex 对应的类} + */ + private Integer sex; + /** + * 是否有效 + * + * 枚举 {@link TODO infra_boolean_string 对应的类} + */ + private Boolean enabled; + /** + * 头像 + */ + private String avatar; + /** + * 附件 + */ + private String video; + /** + * 备注 + */ + private String memo; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentContactMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentContactMapper new file mode 100644 index 0000000..9f16dcf --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentContactMapper @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo; + +import java.util.*; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 学生联系人 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface InfraStudentContactMapper extends BaseMapperX { + + default List selectListByStudentId(Long studentId) { + return selectList(InfraStudentContactDO::getStudentId, studentId); + } + + default int deleteByStudentId(Long studentId) { + return delete(InfraStudentContactDO::getStudentId, studentId); + } + + default int deleteByStudentIds(List studentIds) { + return deleteBatch(InfraStudentContactDO::getStudentId, studentIds); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentController b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentController new file mode 100644 index 0000000..0c9a9f9 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentController @@ -0,0 +1,126 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo; + +import org.springframework.web.bind.annotation.*; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.security.access.prepost.PreAuthorize; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; + +import jakarta.validation.constraints.*; +import jakarta.validation.*; +import jakarta.servlet.http.*; +import java.util.*; +import java.io.IOException; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*; + +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; +import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService; + +@Tag(name = "管理后台 - 学生") +@RestController +@RequestMapping("/infra/student") +@Validated +public class InfraStudentController { + + @Resource + private InfraStudentService studentService; + + @PostMapping("/create") + @Operation(summary = "创建学生") + @PreAuthorize("@ss.hasPermission('infra:student:create')") + public CommonResult createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) { + return success(studentService.createStudent(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新学生") + @PreAuthorize("@ss.hasPermission('infra:student:update')") + public CommonResult updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) { + studentService.updateStudent(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除学生") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:student:delete')") + public CommonResult deleteStudent(@RequestParam("id") Long id) { + studentService.deleteStudent(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Parameter(name = "ids", description = "编号", required = true) + @Operation(summary = "批量删除学生") + @PreAuthorize("@ss.hasPermission('infra:student:delete')") + public CommonResult deleteStudentList(@RequestParam("ids") List ids) { + studentService.deleteStudentListByIds(ids); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得学生") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:student:query')") + public CommonResult getStudent(@RequestParam("id") Long id) { + InfraStudentDO student = studentService.getStudent(id); + return success(BeanUtils.toBean(student, InfraStudentRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得学生分页") + @PreAuthorize("@ss.hasPermission('infra:student:query')") + public CommonResult> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) { + PageResult pageResult = studentService.getStudentPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出学生 Excel") + @PreAuthorize("@ss.hasPermission('infra:student:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = studentService.getStudentPage(pageReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class, + BeanUtils.toBean(list, InfraStudentRespVO.class)); + } + + // ==================== 子表(学生联系人) ==================== + + @GetMapping("/student-contact/list-by-student-id") + @Operation(summary = "获得学生联系人列表") + @Parameter(name = "studentId", description = "学生编号") + @PreAuthorize("@ss.hasPermission('infra:student:query')") + public CommonResult> getStudentContactListByStudentId(@RequestParam("studentId") Long studentId) { + return success(studentService.getStudentContactListByStudentId(studentId)); + } + + // ==================== 子表(学生班主任) ==================== + + @GetMapping("/student-teacher/get-by-student-id") + @Operation(summary = "获得学生班主任") + @Parameter(name = "studentId", description = "学生编号") + @PreAuthorize("@ss.hasPermission('infra:student:query')") + public CommonResult getStudentTeacherByStudentId(@RequestParam("studentId") Long studentId) { + return success(studentService.getStudentTeacherByStudentId(studentId)); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentDO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentDO new file mode 100644 index 0000000..f39fef2 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentDO @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.demo; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; + +/** + * 学生 DO + * + * @author 芋道源码 + */ +@TableName("infra_student") +@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InfraStudentDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 名字 + */ + private String name; + /** + * 简介 + */ + private String description; + /** + * 出生日期 + */ + private LocalDateTime birthday; + /** + * 性别 + * + * 枚举 {@link TODO system_user_sex 对应的类} + */ + private Integer sex; + /** + * 是否有效 + * + * 枚举 {@link TODO infra_boolean_string 对应的类} + */ + private Boolean enabled; + /** + * 头像 + */ + private String avatar; + /** + * 附件 + */ + private String video; + /** + * 备注 + */ + private String memo; + + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentMapper new file mode 100644 index 0000000..34e70a0 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentMapper @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo; + +import java.util.*; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import org.apache.ibatis.annotations.Mapper; +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; + +/** + * 学生 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface InfraStudentMapper extends BaseMapperX { + + default PageResult selectPage(InfraStudentPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(InfraStudentDO::getName, reqVO.getName()) + .eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday()) + .eqIfPresent(InfraStudentDO::getSex, reqVO.getSex()) + .eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled()) + .betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(InfraStudentDO::getId)); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentPageReqVO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentPageReqVO new file mode 100644 index 0000000..52d7a23 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentPageReqVO @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo; + +import lombok.*; +import java.util.*; +import io.swagger.v3.oas.annotations.media.Schema; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 学生分页 Request VO") +@Data +public class InfraStudentPageReqVO extends PageParam { + + @Schema(description = "名字", example = "芋头") + private String name; + + @Schema(description = "出生日期") + private LocalDateTime birthday; + + @Schema(description = "性别", example = "1") + private Integer sex; + + @Schema(description = "是否有效", example = "true") + private Boolean enabled; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentRespVO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentRespVO new file mode 100644 index 0000000..12166f4 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentRespVO @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; +import cn.idev.excel.annotation.*; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; + +@Schema(description = "管理后台 - 学生 Response VO") +@Data +@ExcelIgnoreUnannotated +public class InfraStudentRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("编号") + private Long id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头") + @ExcelProperty("名字") + private String name; + + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍") + @ExcelProperty("简介") + private String description; + + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("出生日期") + private LocalDateTime birthday; + + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "性别", converter = DictConvert.class) + @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 + private Integer sex; + + @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @ExcelProperty(value = "是否有效", converter = DictConvert.class) + @DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 + private Boolean enabled; + + @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + @ExcelProperty("头像") + private String avatar; + + @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4") + @ExcelProperty("附件") + private String video; + + @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注") + @ExcelProperty("备注") + private String memo; + + @Schema(description = "创建时间") + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentSaveReqVO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentSaveReqVO new file mode 100644 index 0000000..4cde1f8 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentSaveReqVO @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import jakarta.validation.constraints.*; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; + +@Schema(description = "管理后台 - 学生新增/修改 Request VO") +@Data +public class InfraStudentSaveReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头") + @NotEmpty(message = "名字不能为空") + private String name; + + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍") + @NotEmpty(message = "简介不能为空") + private String description; + + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "出生日期不能为空") + private LocalDateTime birthday; + + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "性别不能为空") + private Integer sex; + + @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否有效不能为空") + private Boolean enabled; + + @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + @NotEmpty(message = "头像不能为空") + private String avatar; + + @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4") + @NotEmpty(message = "附件不能为空") + private String video; + + @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注") + @NotEmpty(message = "备注不能为空") + private String memo; + + @Schema(description = "学生联系人列表") + private List studentContacts; + + @Schema(description = "学生班主任") + private InfraStudentTeacherDO studentTeacher; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentService b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentService new file mode 100644 index 0000000..0a3ee84 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentService @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.infra.service.demo; + +import java.util.*; +import jakarta.validation.*; +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; + +/** + * 学生 Service 接口 + * + * @author 芋道源码 + */ +public interface InfraStudentService { + + /** + * 创建学生 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createStudent(@Valid InfraStudentSaveReqVO createReqVO); + + /** + * 更新学生 + * + * @param updateReqVO 更新信息 + */ + void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO); + + /** + * 删除学生 + * + * @param id 编号 + */ + void deleteStudent(Long id); + + /** + * 批量删除学生 + * + * @param ids 编号 + */ + void deleteStudentListByIds(List ids); + + /** + * 获得学生 + * + * @param id 编号 + * @return 学生 + */ + InfraStudentDO getStudent(Long id); + + /** + * 获得学生分页 + * + * @param pageReqVO 分页查询 + * @return 学生分页 + */ + PageResult getStudentPage(InfraStudentPageReqVO pageReqVO); + + // ==================== 子表(学生联系人) ==================== + + /** + * 获得学生联系人列表 + * + * @param studentId 学生编号 + * @return 学生联系人列表 + */ + List getStudentContactListByStudentId(Long studentId); + + // ==================== 子表(学生班主任) ==================== + + /** + * 获得学生班主任 + * + * @param studentId 学生编号 + * @return 学生班主任 + */ + InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId); + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentServiceImpl b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentServiceImpl new file mode 100644 index 0000000..31d689e --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentServiceImpl @@ -0,0 +1,191 @@ +package cn.iocoder.yudao.module.infra.service.demo; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import org.springframework.stereotype.Service; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; + +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; + +/** + * 学生 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class InfraStudentServiceImpl implements InfraStudentService { + + @Resource + private InfraStudentMapper studentMapper; + @Resource + private InfraStudentContactMapper studentContactMapper; + @Resource + private InfraStudentTeacherMapper studentTeacherMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createStudent(InfraStudentSaveReqVO createReqVO) { + // 插入 + InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class); + studentMapper.insert(student); + + + // 插入子表 + createStudentContactList(student.getId(), createReqVO.getStudentContacts()); + createStudentTeacher(student.getId(), createReqVO.getStudentTeacher()); + // 返回 + return student.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateStudent(InfraStudentSaveReqVO updateReqVO) { + // 校验存在 + validateStudentExists(updateReqVO.getId()); + // 更新 + InfraStudentDO updateObj = BeanUtils.toBean(updateReqVO, InfraStudentDO.class); + studentMapper.updateById(updateObj); + + // 更新子表 + updateStudentContactList(updateReqVO.getId(), updateReqVO.getStudentContacts()); + updateStudentTeacher(updateReqVO.getId(), updateReqVO.getStudentTeacher()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteStudent(Long id) { + // 校验存在 + validateStudentExists(id); + // 删除 + studentMapper.deleteById(id); + + // 删除子表 + deleteStudentContactByStudentId(id); + deleteStudentTeacherByStudentId(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteStudentListByIds(List ids) { + // 删除 + studentMapper.deleteByIds(ids); + + // 删除子表 + deleteStudentContactByStudentIds(ids); + deleteStudentTeacherByStudentIds(ids); + } + + + private void validateStudentExists(Long id) { + if (studentMapper.selectById(id) == null) { + throw exception(STUDENT_NOT_EXISTS); + } + } + + @Override + public InfraStudentDO getStudent(Long id) { + return studentMapper.selectById(id); + } + + @Override + public PageResult getStudentPage(InfraStudentPageReqVO pageReqVO) { + return studentMapper.selectPage(pageReqVO); + } + + // ==================== 子表(学生联系人) ==================== + + @Override + public List getStudentContactListByStudentId(Long studentId) { + return studentContactMapper.selectListByStudentId(studentId); + } + + private void createStudentContactList(Long studentId, List list) { + if (CollUtil.isEmpty(list)) { + return; + } + list.forEach(o -> o.setStudentId(studentId).clean()); + studentContactMapper.insertBatch(list); + } + + private void updateStudentContactList(Long studentId, List list) { + list.forEach(o -> o.setStudentId(studentId).clean()); + List oldList = studentContactMapper.selectListByStudentId(studentId); + List> diffList = diffList(oldList, list, (oldVal, newVal) -> { + boolean same = ObjectUtil.equal(oldVal.getId(), newVal.getId()); + if (same) { + newVal.setId(oldVal.getId()).clean(); // 解决更新情况下:updateTime 不更新 + } + return same; + }); + + // 第二步,批量添加、修改、删除 + if (CollUtil.isNotEmpty(diffList.get(0))) { + studentContactMapper.insertBatch(diffList.get(0)); + } + if (CollUtil.isNotEmpty(diffList.get(1))) { + studentContactMapper.updateBatch(diffList.get(1)); + } + if (CollUtil.isNotEmpty(diffList.get(2))) { + studentContactMapper.deleteByIds(convertList(diffList.get(2), InfraStudentContactDO::getId)); + } + } + + private void deleteStudentContactByStudentId(Long studentId) { + studentContactMapper.deleteByStudentId(studentId); + } + + private void deleteStudentContactByStudentIds(List studentIds) { + studentContactMapper.deleteByStudentIds(studentIds); + } + + // ==================== 子表(学生班主任) ==================== + + @Override + public InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId) { + return studentTeacherMapper.selectByStudentId(studentId); + } + + private void createStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) { + if (studentTeacher == null) { + return; + } + studentTeacher.setStudentId(studentId); + studentTeacherMapper.insert(studentTeacher); + } + + private void updateStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) { + if (studentTeacher == null) { + return; + } + studentTeacher.setStudentId(studentId).clean();// 解决更新情况下:updateTime 不更新 + studentTeacherMapper.insertOrUpdate(studentTeacher); + } + + private void deleteStudentTeacherByStudentId(Long studentId) { + studentTeacherMapper.deleteByStudentId(studentId); + } + + private void deleteStudentTeacherByStudentIds(List studentIds) { + studentTeacherMapper.deleteByStudentIds(studentIds); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentServiceImplTest b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentServiceImplTest new file mode 100644 index 0000000..f1d3bb9 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentServiceImplTest @@ -0,0 +1,145 @@ +package cn.iocoder.yudao.module.infra.service.demo; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import jakarta.annotation.Resource; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; + +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper; +import cn.iocoder.yudao.framework.common.pojo.PageResult; + +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Import; +import java.util.*; +import java.time.LocalDateTime; + +import static cn.hutool.core.util.RandomUtil.*; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * {@link InfraStudentServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(InfraStudentServiceImpl.class) +public class InfraStudentServiceImplTest extends BaseDbUnitTest { + + @Resource + private InfraStudentServiceImpl studentService; + + @Resource + private InfraStudentMapper studentMapper; + + @Test + public void testCreateStudent_success() { + // 准备参数 + InfraStudentSaveReqVO createReqVO = randomPojo(InfraStudentSaveReqVO.class).setId(null); + + // 调用 + Long studentId = studentService.createStudent(createReqVO); + // 断言 + assertNotNull(studentId); + // 校验记录的属性是否正确 + InfraStudentDO student = studentMapper.selectById(studentId); + assertPojoEquals(createReqVO, student, "id"); + } + + @Test + public void testUpdateStudent_success() { + // mock 数据 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class); + studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据 + // 准备参数 + InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class, o -> { + o.setId(dbStudent.getId()); // 设置更新的 ID + }); + + // 调用 + studentService.updateStudent(updateReqVO); + // 校验是否更新正确 + InfraStudentDO student = studentMapper.selectById(updateReqVO.getId()); // 获取最新的 + assertPojoEquals(updateReqVO, student); + } + + @Test + public void testUpdateStudent_notExists() { + // 准备参数 + InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> studentService.updateStudent(updateReqVO), STUDENT_NOT_EXISTS); + } + + @Test + public void testDeleteStudent_success() { + // mock 数据 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class); + studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbStudent.getId(); + + // 调用 + studentService.deleteStudent(id); + // 校验数据不存在了 + assertNull(studentMapper.selectById(id)); + } + + @Test + public void testDeleteStudent_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> studentService.deleteStudent(id), STUDENT_NOT_EXISTS); + } + + @Test + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 + public void testGetStudentPage() { + // mock 数据 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class, o -> { // 等会查询到 + o.setName(null); + o.setBirthday(null); + o.setSex(null); + o.setEnabled(null); + o.setCreateTime(null); + }); + studentMapper.insert(dbStudent); + // 测试 name 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setName(null))); + // 测试 birthday 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setBirthday(null))); + // 测试 sex 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setSex(null))); + // 测试 enabled 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setEnabled(null))); + // 测试 createTime 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setCreateTime(null))); + // 准备参数 + InfraStudentPageReqVO reqVO = new InfraStudentPageReqVO(); + reqVO.setName(null); + reqVO.setBirthday(null); + reqVO.setSex(null); + reqVO.setEnabled(null); + reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + + // 调用 + PageResult pageResult = studentService.getStudentPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbStudent, pageResult.getList().get(0)); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentTeacherDO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentTeacherDO new file mode 100644 index 0000000..c19cf9f --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentTeacherDO @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.demo; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; + +/** + * 学生班主任 DO + * + * @author 芋道源码 + */ +@TableName("infra_student_teacher") +@KeySequence("infra_student_teacher_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InfraStudentTeacherDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 学生编号 + */ + private Long studentId; + /** + * 名字 + */ + private String name; + /** + * 简介 + */ + private String description; + /** + * 出生日期 + */ + private LocalDateTime birthday; + /** + * 性别 + * + * 枚举 {@link TODO system_user_sex 对应的类} + */ + private Integer sex; + /** + * 是否有效 + * + * 枚举 {@link TODO infra_boolean_string 对应的类} + */ + private Boolean enabled; + /** + * 头像 + */ + private String avatar; + /** + * 附件 + */ + private String video; + /** + * 备注 + */ + private String memo; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentTeacherMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentTeacherMapper new file mode 100644 index 0000000..ae4f27d --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentTeacherMapper @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo; + +import java.util.*; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 学生班主任 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface InfraStudentTeacherMapper extends BaseMapperX { + + default InfraStudentTeacherDO selectByStudentId(Long studentId) { + return selectOne(InfraStudentTeacherDO::getStudentId, studentId); + } + + default int deleteByStudentId(Long studentId) { + return delete(InfraStudentTeacherDO::getStudentId, studentId); + } + + default int deleteByStudentIds(List studentIds) { + return deleteBatch(InfraStudentTeacherDO::getStudentId, studentIds); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/sql/h2 b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/sql/h2 new file mode 100644 index 0000000..6c1875f --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/sql/h2 @@ -0,0 +1,17 @@ +-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里 +CREATE TABLE IF NOT EXISTS "infra_student" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar NOT NULL, + "description" varchar NOT NULL, + "birthday" varchar NOT NULL, + "sex" int NOT NULL, + "enabled" bit NOT NULL, + "avatar" varchar NOT NULL, + "video" varchar NOT NULL, + "memo" varchar NOT NULL, + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY ("id") +) COMMENT '学生表'; + +-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里 +DELETE FROM "infra_student"; \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/sql/sql b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/sql/sql new file mode 100644 index 0000000..c0e5f74 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/sql/sql @@ -0,0 +1,54 @@ +-- 菜单 SQL +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status, component_name +) +VALUES ( + '学生管理', '', 2, 0, 888, + 'student', '', 'infra/demo/index', 0, 'InfraStudent' +); + +-- 按钮父菜单ID +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生查询', 'infra:student:query', 3, 1, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生创建', 'infra:student:create', 3, 2, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生更新', 'infra:student:update', 3, 3, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生删除', 'infra:student:delete', 3, 4, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生导出', 'infra:student:export', 3, 5, @parentId, + '', '', '', 0 +); \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/ts/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/ts/index new file mode 100644 index 0000000..c98bd3b --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/ts/index @@ -0,0 +1,87 @@ +import type { PageParam, PageResult } from '@/http/types' +import { http } from '@/http/http' + +/** 学生联系人信息 */ +export interface StudentContact { + id?: number + studentId?: number + name: string + description: string + birthday: number + sex: number + enabled: boolean + avatar: string + video?: string + memo: string + createTime?: number +} + +/** 学生班主任信息 */ +export interface StudentTeacher { + id?: number + studentId?: number + name: string + description: string + birthday: number + sex: number + enabled: boolean + avatar: string + video?: string + memo: string + createTime?: number +} + +/** 学生信息 */ +export interface Student { + id?: number + name: string + description: string + birthday: number + sex: number + enabled: boolean + avatar: string + video: string + memo: string + createTime?: number + studentContacts?: StudentContact[] + studentTeacher?: StudentTeacher | null +} + +/** 获取学生分页列表 */ +export function getStudentPage(params: PageParam) { + return http.get>('/infra/student/page', params) +} + +/** 获取学生详情 */ +export function getStudent(id: number) { + return http.get(`/infra/student/get?id=${id}`) +} + +/** 创建学生 */ +export function createStudent(data: Student) { + return http.post('/infra/student/create', data) +} + +/** 更新学生 */ +export function updateStudent(data: Student) { + return http.put('/infra/student/update', data) +} + +/** 删除学生 */ +export function deleteStudent(id: number) { + return http.delete(`/infra/student/delete?id=${id}`) +} + +// ==================== 子表:学生联系人 ==================== + +/** 获取学生联系人列表 */ +export function getStudentContactListByStudentId(studentId: number) { + return http.get(`/infra/student/student-contact/list-by-student-id?studentId=${studentId}`) +} + +// ==================== 子表:学生班主任 ==================== + +/** 获取学生班主任 */ +export function getStudentTeacherByStudentId(studentId: number) { + return http.get(`/infra/student/student-teacher/get-by-student-id?studentId=${studentId}`) +} diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/detail/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/detail/index new file mode 100644 index 0000000..829c797 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/detail/index @@ -0,0 +1,229 @@ + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/form/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/form/index new file mode 100644 index 0000000..a43e177 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/form/index @@ -0,0 +1,480 @@ + + + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/index new file mode 100644 index 0000000..acb1b87 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/index @@ -0,0 +1,169 @@ + + + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/search-form b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/search-form new file mode 100644 index 0000000..0928d14 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/search-form @@ -0,0 +1,116 @@ + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/xml/InfraStudentMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/xml/InfraStudentMapper new file mode 100644 index 0000000..155aa5c --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/xml/InfraStudentMapper @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/assert.json b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/assert.json new file mode 100644 index 0000000..0a37471 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/assert.json @@ -0,0 +1,67 @@ +[ { + "contentPath" : "java/InfraStudentPageReqVO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java" +}, { + "contentPath" : "java/InfraStudentRespVO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java" +}, { + "contentPath" : "java/InfraStudentSaveReqVO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java" +}, { + "contentPath" : "java/InfraStudentController", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java" +}, { + "contentPath" : "java/InfraStudentDO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java" +}, { + "contentPath" : "java/InfraStudentContactDO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java" +}, { + "contentPath" : "java/InfraStudentTeacherDO", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java" +}, { + "contentPath" : "java/InfraStudentMapper", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java" +}, { + "contentPath" : "java/InfraStudentContactMapper", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java" +}, { + "contentPath" : "java/InfraStudentTeacherMapper", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java" +}, { + "contentPath" : "xml/InfraStudentMapper", + "filePath" : "yudao-module-infra/src/main/resources/mapper/demo/InfraStudentMapper.xml" +}, { + "contentPath" : "java/InfraStudentServiceImpl", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java" +}, { + "contentPath" : "java/InfraStudentService", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java" +}, { + "contentPath" : "java/InfraStudentServiceImplTest", + "filePath" : "yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java" +}, { + "contentPath" : "java/ErrorCodeConstants_手动操作", + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java" +}, { + "contentPath" : "sql/sql", + "filePath" : "sql/sql.sql" +}, { + "contentPath" : "sql/h2", + "filePath" : "sql/h2.sql" +}, { + "contentPath" : "ts/index", + "filePath" : "yudao-ui-admin-uniapp/src/api/infra/demo/index.ts" +}, { + "contentPath" : "vue/index", + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/index.vue" +}, { + "contentPath" : "vue/search-form", + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/components/search-form.vue" +}, { + "contentPath" : "vue/form/index", + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue" +}, { + "contentPath" : "vue/detail/index", + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue" +} ] \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/ErrorCodeConstants_手动操作 b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/ErrorCodeConstants_手动操作 new file mode 100644 index 0000000..a4df7d2 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/ErrorCodeConstants_手动操作 @@ -0,0 +1,3 @@ +// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!! +// ========== 学生 TODO 补充编号 ========== +ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在"); \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentContactDO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentContactDO new file mode 100644 index 0000000..17c668e --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentContactDO @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.demo; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; + +/** + * 学生联系人 DO + * + * @author 芋道源码 + */ +@TableName("infra_student_contact") +@KeySequence("infra_student_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InfraStudentContactDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 学生编号 + */ + private Long studentId; + /** + * 名字 + */ + private String name; + /** + * 简介 + */ + private String description; + /** + * 出生日期 + */ + private LocalDateTime birthday; + /** + * 性别 + * + * 枚举 {@link TODO system_user_sex 对应的类} + */ + private Integer sex; + /** + * 是否有效 + * + * 枚举 {@link TODO infra_boolean_string 对应的类} + */ + private Boolean enabled; + /** + * 头像 + */ + private String avatar; + /** + * 附件 + */ + private String video; + /** + * 备注 + */ + private String memo; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentContactMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentContactMapper new file mode 100644 index 0000000..9f16dcf --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentContactMapper @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo; + +import java.util.*; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 学生联系人 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface InfraStudentContactMapper extends BaseMapperX { + + default List selectListByStudentId(Long studentId) { + return selectList(InfraStudentContactDO::getStudentId, studentId); + } + + default int deleteByStudentId(Long studentId) { + return delete(InfraStudentContactDO::getStudentId, studentId); + } + + default int deleteByStudentIds(List studentIds) { + return deleteBatch(InfraStudentContactDO::getStudentId, studentIds); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentController b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentController new file mode 100644 index 0000000..0c9a9f9 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentController @@ -0,0 +1,126 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo; + +import org.springframework.web.bind.annotation.*; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.security.access.prepost.PreAuthorize; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; + +import jakarta.validation.constraints.*; +import jakarta.validation.*; +import jakarta.servlet.http.*; +import java.util.*; +import java.io.IOException; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*; + +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; +import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService; + +@Tag(name = "管理后台 - 学生") +@RestController +@RequestMapping("/infra/student") +@Validated +public class InfraStudentController { + + @Resource + private InfraStudentService studentService; + + @PostMapping("/create") + @Operation(summary = "创建学生") + @PreAuthorize("@ss.hasPermission('infra:student:create')") + public CommonResult createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) { + return success(studentService.createStudent(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新学生") + @PreAuthorize("@ss.hasPermission('infra:student:update')") + public CommonResult updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) { + studentService.updateStudent(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除学生") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:student:delete')") + public CommonResult deleteStudent(@RequestParam("id") Long id) { + studentService.deleteStudent(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Parameter(name = "ids", description = "编号", required = true) + @Operation(summary = "批量删除学生") + @PreAuthorize("@ss.hasPermission('infra:student:delete')") + public CommonResult deleteStudentList(@RequestParam("ids") List ids) { + studentService.deleteStudentListByIds(ids); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得学生") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:student:query')") + public CommonResult getStudent(@RequestParam("id") Long id) { + InfraStudentDO student = studentService.getStudent(id); + return success(BeanUtils.toBean(student, InfraStudentRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得学生分页") + @PreAuthorize("@ss.hasPermission('infra:student:query')") + public CommonResult> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) { + PageResult pageResult = studentService.getStudentPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出学生 Excel") + @PreAuthorize("@ss.hasPermission('infra:student:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = studentService.getStudentPage(pageReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class, + BeanUtils.toBean(list, InfraStudentRespVO.class)); + } + + // ==================== 子表(学生联系人) ==================== + + @GetMapping("/student-contact/list-by-student-id") + @Operation(summary = "获得学生联系人列表") + @Parameter(name = "studentId", description = "学生编号") + @PreAuthorize("@ss.hasPermission('infra:student:query')") + public CommonResult> getStudentContactListByStudentId(@RequestParam("studentId") Long studentId) { + return success(studentService.getStudentContactListByStudentId(studentId)); + } + + // ==================== 子表(学生班主任) ==================== + + @GetMapping("/student-teacher/get-by-student-id") + @Operation(summary = "获得学生班主任") + @Parameter(name = "studentId", description = "学生编号") + @PreAuthorize("@ss.hasPermission('infra:student:query')") + public CommonResult getStudentTeacherByStudentId(@RequestParam("studentId") Long studentId) { + return success(studentService.getStudentTeacherByStudentId(studentId)); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentDO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentDO new file mode 100644 index 0000000..f39fef2 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentDO @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.demo; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; + +/** + * 学生 DO + * + * @author 芋道源码 + */ +@TableName("infra_student") +@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InfraStudentDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 名字 + */ + private String name; + /** + * 简介 + */ + private String description; + /** + * 出生日期 + */ + private LocalDateTime birthday; + /** + * 性别 + * + * 枚举 {@link TODO system_user_sex 对应的类} + */ + private Integer sex; + /** + * 是否有效 + * + * 枚举 {@link TODO infra_boolean_string 对应的类} + */ + private Boolean enabled; + /** + * 头像 + */ + private String avatar; + /** + * 附件 + */ + private String video; + /** + * 备注 + */ + private String memo; + + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentMapper new file mode 100644 index 0000000..34e70a0 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentMapper @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo; + +import java.util.*; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import org.apache.ibatis.annotations.Mapper; +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; + +/** + * 学生 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface InfraStudentMapper extends BaseMapperX { + + default PageResult selectPage(InfraStudentPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(InfraStudentDO::getName, reqVO.getName()) + .eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday()) + .eqIfPresent(InfraStudentDO::getSex, reqVO.getSex()) + .eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled()) + .betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(InfraStudentDO::getId)); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentPageReqVO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentPageReqVO new file mode 100644 index 0000000..52d7a23 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentPageReqVO @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo; + +import lombok.*; +import java.util.*; +import io.swagger.v3.oas.annotations.media.Schema; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 学生分页 Request VO") +@Data +public class InfraStudentPageReqVO extends PageParam { + + @Schema(description = "名字", example = "芋头") + private String name; + + @Schema(description = "出生日期") + private LocalDateTime birthday; + + @Schema(description = "性别", example = "1") + private Integer sex; + + @Schema(description = "是否有效", example = "true") + private Boolean enabled; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentRespVO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentRespVO new file mode 100644 index 0000000..12166f4 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentRespVO @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; +import cn.idev.excel.annotation.*; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; + +@Schema(description = "管理后台 - 学生 Response VO") +@Data +@ExcelIgnoreUnannotated +public class InfraStudentRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("编号") + private Long id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头") + @ExcelProperty("名字") + private String name; + + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍") + @ExcelProperty("简介") + private String description; + + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("出生日期") + private LocalDateTime birthday; + + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "性别", converter = DictConvert.class) + @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 + private Integer sex; + + @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @ExcelProperty(value = "是否有效", converter = DictConvert.class) + @DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 + private Boolean enabled; + + @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + @ExcelProperty("头像") + private String avatar; + + @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4") + @ExcelProperty("附件") + private String video; + + @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注") + @ExcelProperty("备注") + private String memo; + + @Schema(description = "创建时间") + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentSaveReqVO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentSaveReqVO new file mode 100644 index 0000000..4cde1f8 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentSaveReqVO @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import jakarta.validation.constraints.*; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; + +@Schema(description = "管理后台 - 学生新增/修改 Request VO") +@Data +public class InfraStudentSaveReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头") + @NotEmpty(message = "名字不能为空") + private String name; + + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍") + @NotEmpty(message = "简介不能为空") + private String description; + + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "出生日期不能为空") + private LocalDateTime birthday; + + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "性别不能为空") + private Integer sex; + + @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否有效不能为空") + private Boolean enabled; + + @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + @NotEmpty(message = "头像不能为空") + private String avatar; + + @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4") + @NotEmpty(message = "附件不能为空") + private String video; + + @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注") + @NotEmpty(message = "备注不能为空") + private String memo; + + @Schema(description = "学生联系人列表") + private List studentContacts; + + @Schema(description = "学生班主任") + private InfraStudentTeacherDO studentTeacher; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentService b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentService new file mode 100644 index 0000000..0a3ee84 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentService @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.infra.service.demo; + +import java.util.*; +import jakarta.validation.*; +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; + +/** + * 学生 Service 接口 + * + * @author 芋道源码 + */ +public interface InfraStudentService { + + /** + * 创建学生 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createStudent(@Valid InfraStudentSaveReqVO createReqVO); + + /** + * 更新学生 + * + * @param updateReqVO 更新信息 + */ + void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO); + + /** + * 删除学生 + * + * @param id 编号 + */ + void deleteStudent(Long id); + + /** + * 批量删除学生 + * + * @param ids 编号 + */ + void deleteStudentListByIds(List ids); + + /** + * 获得学生 + * + * @param id 编号 + * @return 学生 + */ + InfraStudentDO getStudent(Long id); + + /** + * 获得学生分页 + * + * @param pageReqVO 分页查询 + * @return 学生分页 + */ + PageResult getStudentPage(InfraStudentPageReqVO pageReqVO); + + // ==================== 子表(学生联系人) ==================== + + /** + * 获得学生联系人列表 + * + * @param studentId 学生编号 + * @return 学生联系人列表 + */ + List getStudentContactListByStudentId(Long studentId); + + // ==================== 子表(学生班主任) ==================== + + /** + * 获得学生班主任 + * + * @param studentId 学生编号 + * @return 学生班主任 + */ + InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId); + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentServiceImpl b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentServiceImpl new file mode 100644 index 0000000..31d689e --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentServiceImpl @@ -0,0 +1,191 @@ +package cn.iocoder.yudao.module.infra.service.demo; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import org.springframework.stereotype.Service; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; + +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; + +/** + * 学生 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class InfraStudentServiceImpl implements InfraStudentService { + + @Resource + private InfraStudentMapper studentMapper; + @Resource + private InfraStudentContactMapper studentContactMapper; + @Resource + private InfraStudentTeacherMapper studentTeacherMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createStudent(InfraStudentSaveReqVO createReqVO) { + // 插入 + InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class); + studentMapper.insert(student); + + + // 插入子表 + createStudentContactList(student.getId(), createReqVO.getStudentContacts()); + createStudentTeacher(student.getId(), createReqVO.getStudentTeacher()); + // 返回 + return student.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateStudent(InfraStudentSaveReqVO updateReqVO) { + // 校验存在 + validateStudentExists(updateReqVO.getId()); + // 更新 + InfraStudentDO updateObj = BeanUtils.toBean(updateReqVO, InfraStudentDO.class); + studentMapper.updateById(updateObj); + + // 更新子表 + updateStudentContactList(updateReqVO.getId(), updateReqVO.getStudentContacts()); + updateStudentTeacher(updateReqVO.getId(), updateReqVO.getStudentTeacher()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteStudent(Long id) { + // 校验存在 + validateStudentExists(id); + // 删除 + studentMapper.deleteById(id); + + // 删除子表 + deleteStudentContactByStudentId(id); + deleteStudentTeacherByStudentId(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteStudentListByIds(List ids) { + // 删除 + studentMapper.deleteByIds(ids); + + // 删除子表 + deleteStudentContactByStudentIds(ids); + deleteStudentTeacherByStudentIds(ids); + } + + + private void validateStudentExists(Long id) { + if (studentMapper.selectById(id) == null) { + throw exception(STUDENT_NOT_EXISTS); + } + } + + @Override + public InfraStudentDO getStudent(Long id) { + return studentMapper.selectById(id); + } + + @Override + public PageResult getStudentPage(InfraStudentPageReqVO pageReqVO) { + return studentMapper.selectPage(pageReqVO); + } + + // ==================== 子表(学生联系人) ==================== + + @Override + public List getStudentContactListByStudentId(Long studentId) { + return studentContactMapper.selectListByStudentId(studentId); + } + + private void createStudentContactList(Long studentId, List list) { + if (CollUtil.isEmpty(list)) { + return; + } + list.forEach(o -> o.setStudentId(studentId).clean()); + studentContactMapper.insertBatch(list); + } + + private void updateStudentContactList(Long studentId, List list) { + list.forEach(o -> o.setStudentId(studentId).clean()); + List oldList = studentContactMapper.selectListByStudentId(studentId); + List> diffList = diffList(oldList, list, (oldVal, newVal) -> { + boolean same = ObjectUtil.equal(oldVal.getId(), newVal.getId()); + if (same) { + newVal.setId(oldVal.getId()).clean(); // 解决更新情况下:updateTime 不更新 + } + return same; + }); + + // 第二步,批量添加、修改、删除 + if (CollUtil.isNotEmpty(diffList.get(0))) { + studentContactMapper.insertBatch(diffList.get(0)); + } + if (CollUtil.isNotEmpty(diffList.get(1))) { + studentContactMapper.updateBatch(diffList.get(1)); + } + if (CollUtil.isNotEmpty(diffList.get(2))) { + studentContactMapper.deleteByIds(convertList(diffList.get(2), InfraStudentContactDO::getId)); + } + } + + private void deleteStudentContactByStudentId(Long studentId) { + studentContactMapper.deleteByStudentId(studentId); + } + + private void deleteStudentContactByStudentIds(List studentIds) { + studentContactMapper.deleteByStudentIds(studentIds); + } + + // ==================== 子表(学生班主任) ==================== + + @Override + public InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId) { + return studentTeacherMapper.selectByStudentId(studentId); + } + + private void createStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) { + if (studentTeacher == null) { + return; + } + studentTeacher.setStudentId(studentId); + studentTeacherMapper.insert(studentTeacher); + } + + private void updateStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) { + if (studentTeacher == null) { + return; + } + studentTeacher.setStudentId(studentId).clean();// 解决更新情况下:updateTime 不更新 + studentTeacherMapper.insertOrUpdate(studentTeacher); + } + + private void deleteStudentTeacherByStudentId(Long studentId) { + studentTeacherMapper.deleteByStudentId(studentId); + } + + private void deleteStudentTeacherByStudentIds(List studentIds) { + studentTeacherMapper.deleteByStudentIds(studentIds); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentServiceImplTest b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentServiceImplTest new file mode 100644 index 0000000..f1d3bb9 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentServiceImplTest @@ -0,0 +1,145 @@ +package cn.iocoder.yudao.module.infra.service.demo; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import jakarta.annotation.Resource; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; + +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper; +import cn.iocoder.yudao.framework.common.pojo.PageResult; + +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Import; +import java.util.*; +import java.time.LocalDateTime; + +import static cn.hutool.core.util.RandomUtil.*; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * {@link InfraStudentServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(InfraStudentServiceImpl.class) +public class InfraStudentServiceImplTest extends BaseDbUnitTest { + + @Resource + private InfraStudentServiceImpl studentService; + + @Resource + private InfraStudentMapper studentMapper; + + @Test + public void testCreateStudent_success() { + // 准备参数 + InfraStudentSaveReqVO createReqVO = randomPojo(InfraStudentSaveReqVO.class).setId(null); + + // 调用 + Long studentId = studentService.createStudent(createReqVO); + // 断言 + assertNotNull(studentId); + // 校验记录的属性是否正确 + InfraStudentDO student = studentMapper.selectById(studentId); + assertPojoEquals(createReqVO, student, "id"); + } + + @Test + public void testUpdateStudent_success() { + // mock 数据 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class); + studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据 + // 准备参数 + InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class, o -> { + o.setId(dbStudent.getId()); // 设置更新的 ID + }); + + // 调用 + studentService.updateStudent(updateReqVO); + // 校验是否更新正确 + InfraStudentDO student = studentMapper.selectById(updateReqVO.getId()); // 获取最新的 + assertPojoEquals(updateReqVO, student); + } + + @Test + public void testUpdateStudent_notExists() { + // 准备参数 + InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> studentService.updateStudent(updateReqVO), STUDENT_NOT_EXISTS); + } + + @Test + public void testDeleteStudent_success() { + // mock 数据 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class); + studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbStudent.getId(); + + // 调用 + studentService.deleteStudent(id); + // 校验数据不存在了 + assertNull(studentMapper.selectById(id)); + } + + @Test + public void testDeleteStudent_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> studentService.deleteStudent(id), STUDENT_NOT_EXISTS); + } + + @Test + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 + public void testGetStudentPage() { + // mock 数据 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class, o -> { // 等会查询到 + o.setName(null); + o.setBirthday(null); + o.setSex(null); + o.setEnabled(null); + o.setCreateTime(null); + }); + studentMapper.insert(dbStudent); + // 测试 name 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setName(null))); + // 测试 birthday 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setBirthday(null))); + // 测试 sex 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setSex(null))); + // 测试 enabled 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setEnabled(null))); + // 测试 createTime 不匹配 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setCreateTime(null))); + // 准备参数 + InfraStudentPageReqVO reqVO = new InfraStudentPageReqVO(); + reqVO.setName(null); + reqVO.setBirthday(null); + reqVO.setSex(null); + reqVO.setEnabled(null); + reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + + // 调用 + PageResult pageResult = studentService.getStudentPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbStudent, pageResult.getList().get(0)); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentTeacherDO b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentTeacherDO new file mode 100644 index 0000000..c19cf9f --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentTeacherDO @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.demo; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; + +/** + * 学生班主任 DO + * + * @author 芋道源码 + */ +@TableName("infra_student_teacher") +@KeySequence("infra_student_teacher_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InfraStudentTeacherDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 学生编号 + */ + private Long studentId; + /** + * 名字 + */ + private String name; + /** + * 简介 + */ + private String description; + /** + * 出生日期 + */ + private LocalDateTime birthday; + /** + * 性别 + * + * 枚举 {@link TODO system_user_sex 对应的类} + */ + private Integer sex; + /** + * 是否有效 + * + * 枚举 {@link TODO infra_boolean_string 对应的类} + */ + private Boolean enabled; + /** + * 头像 + */ + private String avatar; + /** + * 附件 + */ + private String video; + /** + * 备注 + */ + private String memo; + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentTeacherMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentTeacherMapper new file mode 100644 index 0000000..ae4f27d --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentTeacherMapper @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo; + +import java.util.*; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 学生班主任 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface InfraStudentTeacherMapper extends BaseMapperX { + + default InfraStudentTeacherDO selectByStudentId(Long studentId) { + return selectOne(InfraStudentTeacherDO::getStudentId, studentId); + } + + default int deleteByStudentId(Long studentId) { + return delete(InfraStudentTeacherDO::getStudentId, studentId); + } + + default int deleteByStudentIds(List studentIds) { + return deleteBatch(InfraStudentTeacherDO::getStudentId, studentIds); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/sql/h2 b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/sql/h2 new file mode 100644 index 0000000..6c1875f --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/sql/h2 @@ -0,0 +1,17 @@ +-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里 +CREATE TABLE IF NOT EXISTS "infra_student" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar NOT NULL, + "description" varchar NOT NULL, + "birthday" varchar NOT NULL, + "sex" int NOT NULL, + "enabled" bit NOT NULL, + "avatar" varchar NOT NULL, + "video" varchar NOT NULL, + "memo" varchar NOT NULL, + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY ("id") +) COMMENT '学生表'; + +-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里 +DELETE FROM "infra_student"; \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/sql/sql b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/sql/sql new file mode 100644 index 0000000..c0e5f74 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/sql/sql @@ -0,0 +1,54 @@ +-- 菜单 SQL +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status, component_name +) +VALUES ( + '学生管理', '', 2, 0, 888, + 'student', '', 'infra/demo/index', 0, 'InfraStudent' +); + +-- 按钮父菜单ID +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生查询', 'infra:student:query', 3, 1, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生创建', 'infra:student:create', 3, 2, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生更新', 'infra:student:update', 3, 3, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生删除', 'infra:student:delete', 3, 4, @parentId, + '', '', '', 0 +); +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '学生导出', 'infra:student:export', 3, 5, @parentId, + '', '', '', 0 +); \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/ts/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/ts/index new file mode 100644 index 0000000..c98bd3b --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/ts/index @@ -0,0 +1,87 @@ +import type { PageParam, PageResult } from '@/http/types' +import { http } from '@/http/http' + +/** 学生联系人信息 */ +export interface StudentContact { + id?: number + studentId?: number + name: string + description: string + birthday: number + sex: number + enabled: boolean + avatar: string + video?: string + memo: string + createTime?: number +} + +/** 学生班主任信息 */ +export interface StudentTeacher { + id?: number + studentId?: number + name: string + description: string + birthday: number + sex: number + enabled: boolean + avatar: string + video?: string + memo: string + createTime?: number +} + +/** 学生信息 */ +export interface Student { + id?: number + name: string + description: string + birthday: number + sex: number + enabled: boolean + avatar: string + video: string + memo: string + createTime?: number + studentContacts?: StudentContact[] + studentTeacher?: StudentTeacher | null +} + +/** 获取学生分页列表 */ +export function getStudentPage(params: PageParam) { + return http.get>('/infra/student/page', params) +} + +/** 获取学生详情 */ +export function getStudent(id: number) { + return http.get(`/infra/student/get?id=${id}`) +} + +/** 创建学生 */ +export function createStudent(data: Student) { + return http.post('/infra/student/create', data) +} + +/** 更新学生 */ +export function updateStudent(data: Student) { + return http.put('/infra/student/update', data) +} + +/** 删除学生 */ +export function deleteStudent(id: number) { + return http.delete(`/infra/student/delete?id=${id}`) +} + +// ==================== 子表:学生联系人 ==================== + +/** 获取学生联系人列表 */ +export function getStudentContactListByStudentId(studentId: number) { + return http.get(`/infra/student/student-contact/list-by-student-id?studentId=${studentId}`) +} + +// ==================== 子表:学生班主任 ==================== + +/** 获取学生班主任 */ +export function getStudentTeacherByStudentId(studentId: number) { + return http.get(`/infra/student/student-teacher/get-by-student-id?studentId=${studentId}`) +} diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/detail/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/detail/index new file mode 100644 index 0000000..829c797 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/detail/index @@ -0,0 +1,229 @@ + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/form/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/form/index new file mode 100644 index 0000000..a43e177 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/form/index @@ -0,0 +1,480 @@ + + + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/index new file mode 100644 index 0000000..acb1b87 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/index @@ -0,0 +1,169 @@ + + + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/search-form b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/search-form new file mode 100644 index 0000000..0928d14 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/search-form @@ -0,0 +1,116 @@ + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/xml/InfraStudentMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/xml/InfraStudentMapper new file mode 100644 index 0000000..155aa5c --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/xml/InfraStudentMapper @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/ts/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/ts/index index da8643a..85b1ac7 100644 --- a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/ts/index +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/ts/index @@ -6,13 +6,13 @@ export interface Student { id?: number name: string description: string - birthday: Date + birthday: number sex: number enabled: boolean avatar: string video: string memo: string - createTime?: Date + createTime?: number } /** 获取学生分页列表 */ @@ -38,4 +38,4 @@ export function updateStudent(data: Student) { /** 删除学生 */ export function deleteStudent(id: number) { return http.delete(`/infra/student/delete?id=${id}`) -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/detail/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/detail/index index d6972ec..8d44124 100644 --- a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/detail/index +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/detail/index @@ -38,7 +38,7 @@ 删除 @@ -51,6 +51,7 @@ import type { Student } from '@/api/infra/demo' import { onUnload } from '@dcloudio/uni-app' import { onMounted, ref } from 'vue' +import { useDialog } from '@wot-ui/ui/components/wd-dialog' import { useToast } from '@wot-ui/ui/components/wd-toast' import { deleteStudent, getStudent } from '@/api/infra/demo' import { useAccess } from '@/hooks/useAccess' @@ -59,7 +60,7 @@ import { DICT_TYPE } from '@/utils/constants' import { formatDateTime } from '@/utils/date' const props = defineProps<{ - id?: number | any + id?: number }>() definePage({ @@ -70,6 +71,7 @@ definePage({ }) const { hasAccessByCodes } = useAccess() +const dialog = useDialog() const toast = useToast() const formData = ref() // 详情数据 const deleting = ref(false) // 删除状态 @@ -100,28 +102,27 @@ function handleEdit() { } /** 删除学生 */ -function handleDelete() { +async function handleDelete() { if (!props.id) { return } - uni.showModal({ - title: '提示', - content: '确定要删除该学生吗?', - success: async (res) => { - if (!res.confirm) { - return - } - deleting.value = true - try { - await deleteStudent(props.id) - toast.success('删除成功') - uni.$emit('infra:demo:reload') - delay(handleBack) - } finally { - deleting.value = false - } - }, - }) + try { + await dialog.confirm({ + title: '提示', + msg: '确定要删除该学生吗?', + }) + } catch { + return + } + deleting.value = true + try { + await deleteStudent(props.id) + toast.success('删除成功') + uni.$emit('infra:demo:reload') + delay(handleBack) + } finally { + deleting.value = false + } } /** 初始化 */ diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/form/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/form/index index 06e1a54..36cd18d 100644 --- a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/form/index +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/form/index @@ -11,14 +11,14 @@ - + - + - - - + - + - - + + - - + @@ -93,7 +96,9 @@ diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_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 @@ + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/detail/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/detail/index new file mode 100644 index 0000000..93d65fa --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/detail/index @@ -0,0 +1,128 @@ + + + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/form/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/form/index new file mode 100644 index 0000000..26d3319 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/form/index @@ -0,0 +1,137 @@ + + + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/index b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/index new file mode 100644 index 0000000..064b26b --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/index @@ -0,0 +1,145 @@ + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/search-form b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/search-form new file mode 100644 index 0000000..b5616f0 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/search-form @@ -0,0 +1,69 @@ + + + diff --git a/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/xml/InfraCategoryMapper b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/xml/InfraCategoryMapper new file mode 100644 index 0000000..025ac85 --- /dev/null +++ b/yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/xml/InfraCategoryMapper @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/yudao-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 selectListByDeptIds(Collection deptIds) { diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java index 1957b22..52441b9 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java @@ -91,10 +91,7 @@ public class AdminAuthServiceImpl implements AdminAuthService { throw exception(AUTH_LOGIN_BAD_CREDENTIALS); } // 校验是否禁用 - if (CommonStatusEnum.isDisable(user.getStatus())) { - createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.USER_DISABLED); - throw exception(AUTH_LOGIN_USER_DISABLED); - } + validateUserStatus(user, username, logTypeEnum); return user; } @@ -113,7 +110,7 @@ public class AdminAuthServiceImpl implements AdminAuthService { reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState())); } // 创建 Token 令牌,记录登录日志 - return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME); + return createTokenAfterLoginSuccess(user, reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME); } @Override @@ -146,7 +143,7 @@ public class AdminAuthServiceImpl implements AdminAuthService { } // 创建 Token 令牌,记录登录日志 - return createTokenAfterLoginSuccess(user.getId(), reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE); + return createTokenAfterLoginSuccess(user, reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE); } private void createLoginLog(Long userId, String username, @@ -184,7 +181,7 @@ public class AdminAuthServiceImpl implements AdminAuthService { } // 创建 Token 令牌,记录登录日志 - return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL); + return createTokenAfterLoginSuccess(user, user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL); } @VisibleForTesting @@ -209,16 +206,26 @@ public class AdminAuthServiceImpl implements AdminAuthService { return captchaService.verification(captchaVO); } - private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType) { + private AuthLoginRespVO createTokenAfterLoginSuccess(AdminUserDO user, String username, LoginLogTypeEnum logType) { + // 统一校验用户状态,避免短信、社交等登录方式遗漏 + validateUserStatus(user, username, logType); + // 插入登陆日志 - createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS); + createLoginLog(user.getId(), username, logType, LoginResultEnum.SUCCESS); // 创建访问令牌 - OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(), + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(user.getId(), getUserType().getValue(), OAuth2ClientConstants.CLIENT_ID_DEFAULT, null); // 构建返回结果 return BeanUtils.toBean(accessTokenDO, AuthLoginRespVO.class); } + private void validateUserStatus(AdminUserDO user, String username, LoginLogTypeEnum logType) { + if (CommonStatusEnum.isDisable(user.getStatus())) { + createLoginLog(user.getId(), username, logType, LoginResultEnum.USER_DISABLED); + throw exception(AUTH_LOGIN_USER_DISABLED); + } + } + @Override public AuthLoginRespVO refreshToken(String refreshToken) { OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT); @@ -271,10 +278,10 @@ public class AdminAuthServiceImpl implements AdminAuthService { validateCaptcha(registerReqVO); // 2. 校验用户名是否已存在 - Long userId = userService.registerUser(registerReqVO); + AdminUserDO user = userService.registerUser(registerReqVO); // 3. 创建 Token 令牌,记录登录日志 - return createTokenAfterLoginSuccess(userId, registerReqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME); + return createTokenAfterLoginSuccess(user, registerReqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME); } @VisibleForTesting diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserService.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserService.java index 4ec9765..69904cc 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserService.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserService.java @@ -12,6 +12,7 @@ import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserPageReqV import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserSaveReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; import jakarta.validation.Valid; +import org.springframework.lang.Nullable; import java.util.Collection; import java.util.HashMap; @@ -37,9 +38,9 @@ public interface AdminUserService { * 注册用户 * * @param registerReqVO 用户信息 - * @return 用户编号 + * @return 用户信息 */ - Long registerUser(@Valid AuthRegisterReqVO registerReqVO); + AdminUserDO registerUser(@Valid AuthRegisterReqVO registerReqVO); /** * 修改用户 @@ -214,11 +215,13 @@ public interface AdminUserService { List getUserListByStatus(Integer status); /** - * 获得指定部门的用户 - * @param deptIds - * @return + * 获得指定状态和部门的用户们 + * + * @param status 状态 + * @param deptId 部门编号,{@code null} 表示不筛选部门 + * @return 用户们 */ - List getDeptUsers(Collection deptIds); + List getUserListByStatus(Integer status, @Nullable Long deptId); /** * 判断密码是否匹配 diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java index c2840b8..8159ee8 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java @@ -124,7 +124,7 @@ public class AdminUserServiceImpl implements AdminUserService { } @Override - public Long registerUser(AuthRegisterReqVO registerReqVO) { + public AdminUserDO registerUser(AuthRegisterReqVO registerReqVO) { // 1.1 校验是否开启注册 if (ObjUtil.notEqual(configApi.getConfigValueByKey(USER_REGISTER_ENABLED_KEY), "true")) { throw exception(USER_REGISTER_DISABLED); @@ -144,7 +144,7 @@ public class AdminUserServiceImpl implements AdminUserService { user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启 user.setPassword(encodePassword(registerReqVO.getPassword())); // 加密密码 userMapper.insert(user); - return user.getId(); + return user; } @Override @@ -560,12 +560,12 @@ public class AdminUserServiceImpl implements AdminUserService { @Override public List getUserListByStatus(Integer status) { - return userMapper.selectListByStatus(status); + return getUserListByStatus(status, null); } @Override - public List getDeptUsers(Collection deptIds) { - return userMapper.selectListByDeptIds(deptIds); + public List getUserListByStatus(Integer status, Long deptId) { + return userMapper.selectListByStatusAndDeptId(status, deptId); } @Override diff --git a/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java b/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java index 0475437..0883249 100644 --- a/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java +++ b/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java @@ -215,7 +215,8 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { return true; }))); // mock 方法(用户信息) - AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(1L)); + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(1L) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); when(userService.getUserByMobile(eq(mobile))).thenReturn(user); // mock 缓存登录用户到 Redis OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) @@ -243,7 +244,8 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { when(socialUserService.getSocialUserByCode(eq(UserTypeEnum.ADMIN.getValue()), eq(reqVO.getType()), eq(reqVO.getCode()), eq(reqVO.getState()))).thenReturn(new SocialUserRespDTO(randomString(), randomString(), randomString(), userId)); // mock(用户) - AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(userId)); + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(userId) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); when(userService.getUser(eq(userId))).thenReturn(user); // mock 缓存登录用户到 Redis OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) @@ -263,6 +265,62 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { } @Test + public void testSmsLogin_userDisabled() { + // 准备参数 + AuthSmsLoginReqVO reqVO = new AuthSmsLoginReqVO(randomString(), randomString()); + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())); + when(userService.getUserByMobile(eq(reqVO.getMobile()))).thenReturn(user); + + // 调用,并断言 + assertServiceException(() -> authService.smsLogin(reqVO), AUTH_LOGIN_USER_DISABLED); + verify(loginLogService).createLoginLog(argThat(o -> + o.getLogType().equals(LoginLogTypeEnum.LOGIN_MOBILE.getType()) + && o.getResult().equals(LoginResultEnum.USER_DISABLED.getResult()) + && o.getUserId().equals(user.getId()))); + verify(oauth2TokenService, never()).createAccessToken(anyLong(), anyInt(), anyString(), any()); + } + + @Test + public void testSocialLogin_userDisabled() { + // 准备参数 + AuthSocialLoginReqVO reqVO = randomPojo(AuthSocialLoginReqVO.class); + Long userId = 1L; + when(socialUserService.getSocialUserByCode(eq(UserTypeEnum.ADMIN.getValue()), eq(reqVO.getType()), + eq(reqVO.getCode()), eq(reqVO.getState()))) + .thenReturn(new SocialUserRespDTO(randomString(), randomString(), randomString(), userId)); + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(userId) + .setStatus(CommonStatusEnum.DISABLE.getStatus())); + when(userService.getUser(eq(userId))).thenReturn(user); + + // 调用,并断言 + assertServiceException(() -> authService.socialLogin(reqVO), AUTH_LOGIN_USER_DISABLED); + verify(loginLogService).createLoginLog(argThat(o -> + o.getLogType().equals(LoginLogTypeEnum.LOGIN_SOCIAL.getType()) + && o.getResult().equals(LoginResultEnum.USER_DISABLED.getResult()) + && o.getUserId().equals(user.getId()))); + verify(oauth2TokenService, never()).createAccessToken(anyLong(), anyInt(), anyString(), any()); + } + + @Test + public void testRegister_success() { + // 准备参数 + AuthRegisterReqVO reqVO = randomPojo(AuthRegisterReqVO.class); + authService.setCaptchaEnable(false); + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(1L) + .setUsername(reqVO.getUsername()).setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(userService.registerUser(reqVO)).thenReturn(user); + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(user.getId()) + .setUserType(UserTypeEnum.ADMIN.getValue())); + when(oauth2TokenService.createAccessToken(eq(user.getId()), eq(UserTypeEnum.ADMIN.getValue()), + eq("default"), isNull())).thenReturn(accessTokenDO); + + // 调用,并断言 + AuthLoginRespVO loginRespVO = authService.register(reqVO); + assertPojoEquals(accessTokenDO, loginRespVO); + verify(userService, never()).getUser(anyLong()); + } + + @Test public void testValidateCaptcha_successWithEnable() { // 准备参数 AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); diff --git a/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java b/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java index 68c8510..5d4cd40 100644 --- a/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java +++ b/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.system.service.user; import cn.hutool.core.util.RandomUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; @@ -278,13 +279,14 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { userMapper.insert(dbUser); // 准备参数 Long userId = dbUser.getId(); - Integer status = randomCommonStatus(); + Integer status = CommonStatusEnum.DISABLE.getStatus(); // 调用 userService.updateUserStatus(userId, status); // 断言 AdminUserDO user = userMapper.selectById(userId); assertEquals(status, user.getStatus()); + verify(oauth2TokenService).removeAccessToken(userId, UserTypeEnum.ADMIN.getValue()); } @Test @@ -722,6 +724,23 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { } @Test + public void testGetUserListByStatusAndDeptId() { + // mock 数据 + AdminUserDO user = randomAdminUserDO(o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()).setDeptId(1L)); + userMapper.insert(user); + // 测试 status 不匹配 + userMapper.insert(cloneIgnoreId(user, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 deptId 不匹配 + userMapper.insert(cloneIgnoreId(user, o -> o.setDeptId(2L))); + + // 调用 + List result = userService.getUserListByStatus(CommonStatusEnum.ENABLE.getStatus(), 1L); + // 断言 + assertEquals(1, result.size()); + assertEquals(user, result.get(0)); + } + + @Test public void testValidateUserList_success() { // mock 数据 AdminUserDO userDO = randomAdminUserDO().setStatus(CommonStatusEnum.ENABLE.getStatus()); diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index de6f6d6..e51ee22 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -164,36 +164,28 @@ spring: client: host: 127.0.0.1 port: 19530 - qianfan: # 文心一言 - api-key: x0cuLZ7XsaTCU08vuJWO87Lg - secret-key: R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK - zhipuai: # 智谱 AI - api-key: 32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs openai: # OpenAI 官方 - api-key: sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17 - base-url: https://api.gptsapi.net + base-url: ${OPENAI_BASE_URL:https://api.openai.com} + api-key: ${OPENAI_API_KEY:sk-xxxx} azure: # OpenAI 微软 openai: endpoint: https://eastusprejade.openai.azure.com anthropic: # Anthropic Claude - api-key: sk-muubv7cXeLw0Etgs743f365cD5Ea44429946Fa7e672d8942 + base-url: ${ANTHROPIC_BASE_URL:https://api.anthropic.com} + api-key: ${ANTHROPIC_API_KEY:sk-xxxx} ollama: base-url: http://127.0.0.1:11434 chat: model: llama3 stabilityai: - api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx + api-key: ${STABILITYAI_API_KEY:sk-xxxx} dashscope: # 通义千问 - api-key: sk-47aa124781be4bfb95244cc62f6xxxx - minimax: # Minimax:https://www.minimaxi.com/ - api-key: xxxx - moonshot: # 月之暗面(KIMI) - api-key: sk-abc + api-key: ${DASHSCOPE_API_KEY:sk-xxxx} deepseek: # DeepSeek - api-key: sk-e94db327cc7d457d99a8de8810fc6b12 + api-key: ${DEEPSEEK_API_KEY:sk-xxxx} chat: options: - model: deepseek-chat + model: deepseek-v4-flash model: rerank: false # 是否开启“通义千问”的 Rerank 模型,填写 dashscope 开启 mcp: @@ -218,34 +210,57 @@ yudao: ai: gemini: # 谷歌 Gemini enable: true - api-key: AIzaSyAVoBxgoFvvte820vEQMma2LKBnC98bqMQ + api-key: ${GEMINI_API_KEY:sk-xxxx} model: gemini-2.5-flash doubao: # 字节豆包 enable: true - api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272 - model: doubao-1-5-lite-32k-250115 + api-key: ${DOUBAO_API_KEY:sk-xxxx} + model: doubao-seed-2-1-turbo-260628 hunyuan: # 腾讯混元 enable: true - api-key: sk-abc - model: hunyuan-turbo + api-key: ${HUNYUAN_API_KEY:sk-xxxx} + model: hy3-preview siliconflow: # 硅基流动 enable: true - api-key: sk-epsakfenqnyzoxhmbucsxlhkdqlcbnimslqoivkshalvdozz - model: deepseek-ai/DeepSeek-R1-Distill-Qwen-7B + api-key: ${SILICONFLOW_API_KEY:sk-xxxx} + model: deepseek-ai/DeepSeek-V4-Pro xinghuo: # 讯飞星火 enable: true - appKey: 75b161ed2aef4719b275d6e7f2a4d4cd - secretKey: YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz - model: x1 + api-key: ${XINGHUO_API_KEY:sk-xxxx} + model: x2-flash baichuan: # 百川智能 enable: true - api-key: sk-abc - model: Baichuan4-Turbo + api-key: ${BAICHUAN_API_KEY:sk-xxxx} + model: Baichuan-M3 + yiyan: # 文心一言 + enable: false + api-key: ${YIYAN_API_KEY:sk-xxxx} + model: ernie-5.1 + zhipu: # 智谱 AI(GLM) + enable: false + api-key: ${ZHIPU_API_KEY:sk-xxxx} + model: glm-5.2 + minimax: # MiniMax + enable: false + api-key: ${MINIMAX_API_KEY:sk-xxxx} + model: MiniMax-M3 + moonshot: # 月之暗面(KIMI) + enable: false + api-key: ${MOONSHOT_API_KEY:sk-xxxx} + model: kimi-k2.6 + stepfun: # 阶跃星辰 + enable: false + api-key: ${STEPFUN_API_KEY:sk-xxxx} + model: step-3.7-flash + grok: # Grok + enable: false + api-key: ${GROK_API_KEY:sk-xxxx} + model: grok-4.3 midjourney: enable: true # base-url: https://api.holdai.top/mj-relax/mj base-url: https://api.holdai.top/mj - api-key: sk-dZEPiVaNcT3FHhef51996bAa0bC74806BeAb620dA5Da10Bf + api-key: ${MIDJOURNEY_API_KEY:sk-xxxx} notify-url: http://java.nat300.top/admin-api/ai/image/midjourney/notify suno: enable: true @@ -253,7 +268,7 @@ yudao: base-url: http://127.0.0.1:3001 web-search: enable: true - api-key: sk-40500e52840f4d24b956d0b1d80d9abe + api-key: ${WEB_SEARCH_API_KEY:sk-xxxx} --- #################### 芋道相关配置 #################### @@ -351,4 +366,4 @@ yudao: message-bus: type: redis # 消息总线的类型 -debug: false \ No newline at end of file +debug: false