Commit 5f8d9ac8288360e637d86b95d407aa36428e016f

Authored by YunaiV
2 parents 53a86075 c3523cd0

Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/hrm

Showing 212 changed files with 13327 additions and 1003 deletions

Too many changes.

To preserve performance only 100 of 212 files are displayed.

script/livekit-poc/README.md
@@ -15,7 +15,7 @@ bash verify.sh @@ -15,7 +15,7 @@ bash verify.sh
15 - 7880:HTTP / WebSocket 信令; 15 - 7880:HTTP / WebSocket 信令;
16 - 7881:WebRTC TCP fallback; 16 - 7881:WebRTC TCP fallback;
17 - 7882/UDP:WebRTC 媒体; 17 - 7882/UDP:WebRTC 媒体;
18 -- macOS / Windows:当前 `docker-compose.yml` 走端口映射模式,webhook URL 用 `host.docker.internal:48080` 让容器访问到宿主机 yudao 后端; 18 +- macOS / Windows:当前 `docker-compose.yml` 走端口映射模式,`rtc.node_ip` 使用 `127.0.0.1` 让浏览器访问映射后的媒体端口,webhook URL 用 `host.docker.internal:48080` 让容器访问到宿主机 yudao 后端;
19 - macOS 上 host network(`network_mode: host`)需要 Docker Desktop 4.34+ 并在 Settings → Resources → Network 勾选「Enable host networking」,老版本静默失败(容器跑得起来但端口完全不通); 19 - macOS 上 host network(`network_mode: host`)需要 Docker Desktop 4.34+ 并在 Settings → Resources → Network 勾选「Enable host networking」,老版本静默失败(容器跑得起来但端口完全不通);
20 - Linux:可以把 `docker-compose.yml` 改成 `network_mode: host` + 删 `ports:` 段,并把 `livekit.yaml` 的 webhook URL 改为 `http://127.0.0.1:48080/admin-api/im/livekit/webhook`。 20 - Linux:可以把 `docker-compose.yml` 改成 `network_mode: host` + 删 `ports:` 段,并把 `livekit.yaml` 的 webhook URL 改为 `http://127.0.0.1:48080/admin-api/im/livekit/webhook`。
21 21
script/livekit-poc/livekit.yaml
@@ -10,6 +10,8 @@ rtc: @@ -10,6 +10,8 @@ rtc:
10 tcp_port: 7881 10 tcp_port: 7881
11 udp_port: 7882 11 udp_port: 7882
12 use_external_ip: false 12 use_external_ip: false
  13 + # macOS / Windows 端口映射模式下显式广播宿主机回环地址,避免把容器 172.x 地址发给浏览器
  14 + node_ip: 127.0.0.1
13 15
14 # Webhook:成员离开 / 房间结束等事件回调到 yudao 后端做业务态兜底清理 16 # Webhook:成员离开 / 房间结束等事件回调到 yudao 后端做业务态兜底清理
15 # host.docker.internal 让容器访问宿主机 macOS / Windows 上的 yudao 后端 17 # host.docker.internal 让容器访问宿主机 macOS / Windows 上的 yudao 后端
yudao-dependencies/pom.xml
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 <url>https://github.com/YunaiV/ruoyi-vue-pro</url> 14 <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
15 15
16 <properties> 16 <properties>
17 - <revision>2026.06-SNAPSHOT</revision> 17 + <revision>2026.07-SNAPSHOT</revision>
18 <flatten-maven-plugin.version>1.7.2</flatten-maven-plugin.version> 18 <flatten-maven-plugin.version>1.7.2</flatten-maven-plugin.version>
19 <!-- 统一依赖管理 --> 19 <!-- 统一依赖管理 -->
20 <spring.boot.version>3.5.15</spring.boot.version> 20 <spring.boot.version>3.5.15</spring.boot.version>
@@ -59,8 +59,7 @@ @@ -59,8 +59,7 @@
59 <fastexcel.version>1.3.0</fastexcel.version> 59 <fastexcel.version>1.3.0</fastexcel.version>
60 <aviator.version>5.3.3</aviator.version> 60 <aviator.version>5.3.3</aviator.version>
61 <velocity.version>2.4.1</velocity.version> 61 <velocity.version>2.4.1</velocity.version>
62 - <fastjson.version>1.2.83</fastjson.version>  
63 - <fastjson2.version>2.0.62</fastjson2.version> 62 + <fastjson2.version>2.0.63</fastjson2.version>
64 <guava.version>33.6.0-jre</guava.version> 63 <guava.version>33.6.0-jre</guava.version>
65 <transmittable-thread-local.version>2.14.5</transmittable-thread-local.version> 64 <transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
66 <commons-net.version>3.13.0</commons-net.version> 65 <commons-net.version>3.13.0</commons-net.version>
@@ -244,6 +243,12 @@ @@ -244,6 +243,12 @@
244 <groupId>org.dromara</groupId> 243 <groupId>org.dromara</groupId>
245 <artifactId>easy-trans-anno</artifactId> 244 <artifactId>easy-trans-anno</artifactId>
246 <version>${easy-trans.version}</version> 245 <version>${easy-trans.version}</version>
  246 + <exclusions>
  247 + <exclusion>
  248 + <groupId>com.alibaba</groupId>
  249 + <artifactId>fastjson</artifactId>
  250 + </exclusion>
  251 + </exclusions>
247 </dependency> 252 </dependency>
248 253
249 <dependency> 254 <dependency>
@@ -315,6 +320,12 @@ @@ -315,6 +320,12 @@
315 <groupId>org.apache.rocketmq</groupId> 320 <groupId>org.apache.rocketmq</groupId>
316 <artifactId>rocketmq-spring-boot-starter</artifactId> 321 <artifactId>rocketmq-spring-boot-starter</artifactId>
317 <version>${rocketmq-spring.version}</version> 322 <version>${rocketmq-spring.version}</version>
  323 + <exclusions>
  324 + <exclusion>
  325 + <groupId>com.alibaba</groupId>
  326 + <artifactId>fastjson</artifactId>
  327 + </exclusion>
  328 + </exclusions>
318 </dependency> 329 </dependency>
319 330
320 <!-- 服务保障相关 --> 331 <!-- 服务保障相关 -->
@@ -509,7 +520,7 @@ @@ -509,7 +520,7 @@
509 <dependency> 520 <dependency>
510 <groupId>com.alibaba</groupId> 521 <groupId>com.alibaba</groupId>
511 <artifactId>fastjson</artifactId> 522 <artifactId>fastjson</artifactId>
512 - <version>${fastjson.version}</version> 523 + <version>${fastjson2.version}</version> <!-- fastjson2 官方提供的 1.x API 兼容实现 -->
513 </dependency> 524 </dependency>
514 <dependency> 525 <dependency>
515 <groupId>com.alibaba.fastjson2</groupId> 526 <groupId>com.alibaba.fastjson2</groupId>
@@ -638,6 +649,12 @@ @@ -638,6 +649,12 @@
638 <groupId>me.zhyd.oauth</groupId> 649 <groupId>me.zhyd.oauth</groupId>
639 <artifactId>JustAuth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) --> 650 <artifactId>JustAuth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
640 <version>${justauth.version}</version> 651 <version>${justauth.version}</version>
  652 + <exclusions>
  653 + <exclusion>
  654 + <groupId>com.alibaba</groupId>
  655 + <artifactId>fastjson</artifactId>
  656 + </exclusion>
  657 + </exclusions>
641 </dependency> 658 </dependency>
642 <dependency> 659 <dependency>
643 <groupId>com.xkcoding.justauth</groupId> 660 <groupId>com.xkcoding.justauth</groupId>
@@ -661,6 +678,10 @@ @@ -661,6 +678,10 @@
661 <groupId>org.bouncycastle</groupId> 678 <groupId>org.bouncycastle</groupId>
662 <artifactId>bcprov-jdk15on</artifactId> 679 <artifactId>bcprov-jdk15on</artifactId>
663 </exclusion> 680 </exclusion>
  681 + <exclusion>
  682 + <groupId>com.alibaba</groupId>
  683 + <artifactId>fastjson</artifactId>
  684 + </exclusion>
664 </exclusions> 685 </exclusions>
665 </dependency> 686 </dependency>
666 687
@@ -703,6 +724,12 @@ @@ -703,6 +724,12 @@
703 <groupId>org.jeecgframework.jimureport</groupId> 724 <groupId>org.jeecgframework.jimureport</groupId>
704 <artifactId>jimureport-spring-boot3-starter</artifactId> 725 <artifactId>jimureport-spring-boot3-starter</artifactId>
705 <version>${jimureport.version}</version> 726 <version>${jimureport.version}</version>
  727 + <exclusions>
  728 + <exclusion>
  729 + <groupId>com.alibaba</groupId>
  730 + <artifactId>fastjson</artifactId>
  731 + </exclusion>
  732 + </exclusions>
706 </dependency> 733 </dependency>
707 <dependency> 734 <dependency>
708 <groupId>org.jeecgframework.jimureport</groupId> 735 <groupId>org.jeecgframework.jimureport</groupId>
@@ -717,6 +744,10 @@ @@ -717,6 +744,10 @@
717 <groupId>cn.hutool</groupId> 744 <groupId>cn.hutool</groupId>
718 <artifactId>hutool-core</artifactId> 745 <artifactId>hutool-core</artifactId>
719 </exclusion> 746 </exclusion>
  747 + <exclusion>
  748 + <groupId>com.alibaba</groupId>
  749 + <artifactId>fastjson</artifactId>
  750 + </exclusion>
720 </exclusions> 751 </exclusions>
721 </dependency> 752 </dependency>
722 </dependencies> 753 </dependencies>
yudao-framework/yudao-common/pom.xml
@@ -134,6 +134,11 @@ @@ -134,6 +134,11 @@
134 </dependency> 134 </dependency>
135 135
136 <dependency> 136 <dependency>
  137 + <groupId>com.alibaba</groupId>
  138 + <artifactId>fastjson</artifactId> <!-- JustAuth、Jimu 等三方库仍使用 1.x 包名,由项目统一提供 fastjson2 兼容实现 -->
  139 + </dependency>
  140 +
  141 + <dependency>
137 <groupId>org.dromara</groupId> <!-- VO 数据翻译 --> 142 <groupId>org.dromara</groupId> <!-- VO 数据翻译 -->
138 <artifactId>easy-trans-anno</artifactId> <!-- 默认引入的原因,方便 xxx-module-api 包使用 --> 143 <artifactId>easy-trans-anno</artifactId> <!-- 默认引入的原因,方便 xxx-module-api 包使用 -->
139 </dependency> 144 </dependency>
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/OAuth2TokenCommonApi.java
@@ -38,6 +38,15 @@ public interface OAuth2TokenCommonApi { @@ -38,6 +38,15 @@ public interface OAuth2TokenCommonApi {
38 OAuth2AccessTokenRespDTO removeAccessToken(String accessToken); 38 OAuth2AccessTokenRespDTO removeAccessToken(String accessToken);
39 39
40 /** 40 /**
  41 + * 移除用户的所有访问令牌
  42 + * 注意:该流程中,会移除相关的刷新令牌
  43 + *
  44 + * @param userId 用户编号
  45 + * @param userType 用户类型
  46 + */
  47 + void removeAccessToken(Long userId, Integer userType);
  48 +
  49 + /**
41 * 刷新访问令牌 50 * 刷新访问令牌
42 * 51 *
43 * @param refreshToken 刷新令牌 52 * @param refreshToken 刷新令牌
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; @@ -23,6 +23,7 @@ import jakarta.validation.ConstraintViolationException;
23 import jakarta.validation.ValidationException; 23 import jakarta.validation.ValidationException;
24 import lombok.AllArgsConstructor; 24 import lombok.AllArgsConstructor;
25 import lombok.extern.slf4j.Slf4j; 25 import lombok.extern.slf4j.Slf4j;
  26 +import org.springframework.core.annotation.Order;
26 import org.springframework.http.converter.HttpMessageNotReadableException; 27 import org.springframework.http.converter.HttpMessageNotReadableException;
27 import org.springframework.security.access.AccessDeniedException; 28 import org.springframework.security.access.AccessDeniedException;
28 import org.springframework.util.Assert; 29 import org.springframework.util.Assert;
@@ -53,6 +54,7 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC @@ -53,6 +54,7 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC
53 * @author 芋道源码 54 * @author 芋道源码
54 */ 55 */
55 @RestControllerAdvice 56 @RestControllerAdvice
  57 +@Order(0) // 优先于三方库默认的全局异常处理器,例如 JimuReport
56 @AllArgsConstructor 58 @AllArgsConstructor
57 @Slf4j 59 @Slf4j
58 public class GlobalExceptionHandler { 60 public class GlobalExceptionHandler {
yudao-module-ai/pom.xml
@@ -19,12 +19,24 @@ @@ -19,12 +19,24 @@
19 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno 19 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno
20 </description> 20 </description>
21 <properties> 21 <properties>
22 - <spring-ai.version>1.1.5</spring-ai.version> 22 + <spring-ai.version>1.1.8</spring-ai.version>
23 <!-- https://mvnrepository.com/artifact/com.alibaba.cloud.ai/spring-ai-alibaba --> 23 <!-- https://mvnrepository.com/artifact/com.alibaba.cloud.ai/spring-ai-alibaba -->
24 <alibaba-ai.version>1.1.2.2</alibaba-ai.version> 24 <alibaba-ai.version>1.1.2.2</alibaba-ai.version>
25 <tinyflow.version>1.2.6</tinyflow.version> 25 <tinyflow.version>1.2.6</tinyflow.version>
26 </properties> 26 </properties>
27 27
  28 + <dependencyManagement>
  29 + <dependencies>
  30 + <dependency>
  31 + <groupId>org.springframework.ai</groupId>
  32 + <artifactId>spring-ai-bom</artifactId>
  33 + <version>${spring-ai.version}</version>
  34 + <type>pom</type>
  35 + <scope>import</scope>
  36 + </dependency>
  37 + </dependencies>
  38 + </dependencyManagement>
  39 +
28 <dependencies> 40 <dependencies>
29 <dependency> 41 <dependency>
30 <groupId>cn.iocoder.boot</groupId> 42 <groupId>cn.iocoder.boot</groupId>
@@ -112,37 +124,11 @@ @@ -112,37 +124,11 @@
112 <version>${spring-ai.version}</version> 124 <version>${spring-ai.version}</version>
113 </dependency> 125 </dependency>
114 <dependency> 126 <dependency>
115 - <!-- 智谱 GLM -->  
116 - <groupId>org.springframework.ai</groupId>  
117 - <artifactId>spring-ai-starter-model-zhipuai</artifactId>  
118 - <version>${spring-ai.version}</version>  
119 - </dependency>  
120 - <dependency>  
121 - <groupId>org.springframework.ai</groupId>  
122 - <artifactId>spring-ai-starter-model-minimax</artifactId>  
123 - <version>${spring-ai.version}</version>  
124 - </dependency>  
125 -  
126 - <dependency>  
127 <!-- 通义千问 --> 127 <!-- 通义千问 -->
128 <groupId>com.alibaba.cloud.ai</groupId> 128 <groupId>com.alibaba.cloud.ai</groupId>
129 <artifactId>spring-ai-alibaba-starter-dashscope</artifactId> 129 <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
130 <version>${alibaba-ai.version}</version> 130 <version>${alibaba-ai.version}</version>
131 </dependency> 131 </dependency>
132 -  
133 - <dependency>  
134 - <!-- 文心一言 -->  
135 - <groupId>org.springaicommunity</groupId>  
136 - <artifactId>qianfan-spring-boot-starter</artifactId>  
137 - <version>1.0.0</version>  
138 - </dependency>  
139 - <dependency>  
140 - <!-- 月之暗面 -->  
141 - <groupId>org.springaicommunity</groupId>  
142 - <artifactId>moonshot-spring-boot-starter</artifactId>  
143 - <version>1.0.0</version>  
144 - </dependency>  
145 -  
146 <!-- 向量存储:https://db-engines.com/en/ranking/vector+dbms --> 132 <!-- 向量存储:https://db-engines.com/en/ranking/vector+dbms -->
147 <dependency> 133 <dependency>
148 <!-- Qdrant:https://qdrant.tech/ --> 134 <!-- Qdrant:https://qdrant.tech/ -->
@@ -256,8 +242,12 @@ @@ -256,8 +242,12 @@
256 <groupId>org.slf4j</groupId> 242 <groupId>org.slf4j</groupId>
257 <artifactId>slf4j-reload4j</artifactId> 243 <artifactId>slf4j-reload4j</artifactId>
258 </exclusion> 244 </exclusion>
  245 + <exclusion>
  246 + <groupId>com.alibaba</groupId>
  247 + <artifactId>fastjson</artifactId>
  248 + </exclusion>
259 </exclusions> 249 </exclusions>
260 </dependency> 250 </dependency>
261 </dependencies> 251 </dependencies>
262 252
263 -</project>  
264 \ No newline at end of file 253 \ No newline at end of file
  254 +</project>
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java
@@ -106,6 +106,20 @@ public class AiChatConversationController { @@ -106,6 +106,20 @@ public class AiChatConversationController {
106 conversation -> conversation.setMessageCount(messageCountMap.getOrDefault(conversation.getId(), 0)))); 106 conversation -> conversation.setMessageCount(messageCountMap.getOrDefault(conversation.getId(), 0))));
107 } 107 }
108 108
  109 + @GetMapping("/get")
  110 + @Operation(summary = "获得对话", description = "用于【对话管理】菜单")
  111 + @Parameter(name = "id", required = true, description = "对话编号", example = "1024")
  112 + @PreAuthorize("@ss.hasPermission('ai:chat-conversation:query')")
  113 + @TransMethodResult
  114 + public CommonResult<AiChatConversationRespVO> getChatConversation(@RequestParam("id") Long id) {
  115 + AiChatConversationDO conversation = chatConversationService.getChatConversation(id);
  116 + AiChatConversationRespVO respVO = BeanUtils.toBean(conversation, AiChatConversationRespVO.class);
  117 + if (respVO != null) {
  118 + respVO.setMessageCount(chatMessageService.getChatMessageCount(id));
  119 + }
  120 + return success(respVO);
  121 + }
  122 +
109 @Operation(summary = "管理员删除对话") 123 @Operation(summary = "管理员删除对话")
110 @DeleteMapping("/delete-by-admin") 124 @DeleteMapping("/delete-by-admin")
111 @Parameter(name = "id", required = true, description = "对话编号", example = "1024") 125 @Parameter(name = "id", required = true, description = "对话编号", example = "1024")
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java
@@ -131,7 +131,7 @@ public class AiChatMessageController { @@ -131,7 +131,7 @@ public class AiChatMessageController {
131 131
132 @GetMapping("/page") 132 @GetMapping("/page")
133 @Operation(summary = "获得消息分页", description = "用于【对话管理】菜单") 133 @Operation(summary = "获得消息分页", description = "用于【对话管理】菜单")
134 - @PreAuthorize("@ss.hasPermission('ai:chat-conversation:query')") 134 + @PreAuthorize("@ss.hasPermission('ai:chat-message:query')")
135 public CommonResult<PageResult<AiChatMessageRespVO>> getChatMessagePage(AiChatMessagePageReqVO pageReqVO) { 135 public CommonResult<PageResult<AiChatMessageRespVO>> getChatMessagePage(AiChatMessagePageReqVO pageReqVO) {
136 PageResult<AiChatMessageDO> pageResult = chatMessageService.getChatMessagePage(pageReqVO); 136 PageResult<AiChatMessageDO> pageResult = chatMessageService.getChatMessagePage(pageReqVO);
137 if (CollUtil.isEmpty(pageResult.getList())) { 137 if (CollUtil.isEmpty(pageResult.getList())) {
@@ -145,6 +145,20 @@ public class AiChatMessageController { @@ -145,6 +145,20 @@ public class AiChatMessageController {
145 role -> respVO.setRoleName(role.getName())))); 145 role -> respVO.setRoleName(role.getName()))));
146 } 146 }
147 147
  148 + @GetMapping("/get")
  149 + @Operation(summary = "获得消息", description = "用于【对话管理】菜单")
  150 + @Parameter(name = "id", required = true, description = "消息编号", example = "1024")
  151 + @PreAuthorize("@ss.hasPermission('ai:chat-message:query')")
  152 + public CommonResult<AiChatMessageRespVO> getChatMessage(@RequestParam("id") Long id) {
  153 + AiChatMessageDO message = chatMessageService.getChatMessage(id);
  154 + AiChatMessageRespVO respVO = BeanUtils.toBean(message, AiChatMessageRespVO.class);
  155 + if (respVO != null && respVO.getRoleId() != null) {
  156 + AiChatRoleDO role = chatRoleService.getChatRole(respVO.getRoleId());
  157 + respVO.setRoleName(role != null ? role.getName() : null);
  158 + }
  159 + return success(respVO);
  160 + }
  161 +
148 @Operation(summary = "管理员删除消息") 162 @Operation(summary = "管理员删除消息")
149 @DeleteMapping("/delete-by-admin") 163 @DeleteMapping("/delete-by-admin")
150 @Parameter(name = "id", required = true, description = "消息编号", example = "1024") 164 @Parameter(name = "id", required = true, description = "消息编号", example = "1024")
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java
@@ -119,6 +119,15 @@ public class AiImageController { @@ -119,6 +119,15 @@ public class AiImageController {
119 return success(BeanUtils.toBean(pageResult, AiImageRespVO.class)); 119 return success(BeanUtils.toBean(pageResult, AiImageRespVO.class));
120 } 120 }
121 121
  122 + @GetMapping("/get")
  123 + @Operation(summary = "获得绘画")
  124 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  125 + @PreAuthorize("@ss.hasPermission('ai:image:query')")
  126 + public CommonResult<AiImageRespVO> getImage(@RequestParam("id") Long id) {
  127 + AiImageDO image = imageService.getImage(id);
  128 + return success(BeanUtils.toBean(image, AiImageRespVO.class));
  129 + }
  130 +
122 @PutMapping("/update") 131 @PutMapping("/update")
123 @Operation(summary = "更新绘画") 132 @Operation(summary = "更新绘画")
124 @PreAuthorize("@ss.hasPermission('ai:image:update')") 133 @PreAuthorize("@ss.hasPermission('ai:image:update')")
@@ -136,4 +145,4 @@ public class AiImageController { @@ -136,4 +145,4 @@ public class AiImageController {
136 return success(true); 145 return success(true);
137 } 146 }
138 147
139 -}  
140 \ No newline at end of file 148 \ No newline at end of file
  149 +}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java
@@ -46,6 +46,15 @@ public class AiMindMapController { @@ -46,6 +46,15 @@ public class AiMindMapController {
46 return success(true); 46 return success(true);
47 } 47 }
48 48
  49 + @GetMapping("/get")
  50 + @Operation(summary = "获得思维导图")
  51 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  52 + @PreAuthorize("@ss.hasPermission('ai:mind-map:query')")
  53 + public CommonResult<AiMindMapRespVO> getMindMap(@RequestParam("id") Long id) {
  54 + AiMindMapDO mindMap = mindMapService.getMindMap(id);
  55 + return success(BeanUtils.toBean(mindMap, AiMindMapRespVO.class));
  56 + }
  57 +
49 @GetMapping("/page") 58 @GetMapping("/page")
50 @Operation(summary = "获得思维导图分页") 59 @Operation(summary = "获得思维导图分页")
51 @PreAuthorize("@ss.hasPermission('ai:mind-map:query')") 60 @PreAuthorize("@ss.hasPermission('ai:mind-map:query')")
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java
@@ -78,6 +78,15 @@ public class AiMusicController { @@ -78,6 +78,15 @@ public class AiMusicController {
78 return success(BeanUtils.toBean(pageResult, AiMusicRespVO.class)); 78 return success(BeanUtils.toBean(pageResult, AiMusicRespVO.class));
79 } 79 }
80 80
  81 + @GetMapping("/get")
  82 + @Operation(summary = "获得音乐")
  83 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  84 + @PreAuthorize("@ss.hasPermission('ai:music:query')")
  85 + public CommonResult<AiMusicRespVO> getMusic(@RequestParam("id") Long id) {
  86 + AiMusicDO music = musicService.getMusic(id);
  87 + return success(BeanUtils.toBean(music, AiMusicRespVO.class));
  88 + }
  89 +
81 @DeleteMapping("/delete") 90 @DeleteMapping("/delete")
82 @Operation(summary = "删除音乐") 91 @Operation(summary = "删除音乐")
83 @Parameter(name = "id", description = "编号", required = true) 92 @Parameter(name = "id", description = "编号", required = true)
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java
@@ -46,6 +46,15 @@ public class AiWriteController { @@ -46,6 +46,15 @@ public class AiWriteController {
46 return success(true); 46 return success(true);
47 } 47 }
48 48
  49 + @GetMapping("/get")
  50 + @Operation(summary = "获得写作")
  51 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  52 + @PreAuthorize("@ss.hasPermission('ai:write:query')")
  53 + public CommonResult<AiWriteRespVO> getWrite(@RequestParam("id") Long id) {
  54 + AiWriteDO write = writeService.getWrite(id);
  55 + return success(BeanUtils.toBean(write, AiWriteRespVO.class));
  56 + }
  57 +
49 @GetMapping("/page") 58 @GetMapping("/page")
50 @Operation(summary = "获得写作分页") 59 @Operation(summary = "获得写作分页")
51 @PreAuthorize("@ss.hasPermission('ai:write:query')") 60 @PreAuthorize("@ss.hasPermission('ai:write:query')")
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java
@@ -12,6 +12,7 @@ public interface ErrorCodeConstants { @@ -12,6 +12,7 @@ public interface ErrorCodeConstants {
12 // ========== API 密钥 1-040-000-000 ========== 12 // ========== API 密钥 1-040-000-000 ==========
13 ErrorCode API_KEY_NOT_EXISTS = new ErrorCode(1_040_000_000, "API 密钥不存在"); 13 ErrorCode API_KEY_NOT_EXISTS = new ErrorCode(1_040_000_000, "API 密钥不存在");
14 ErrorCode API_KEY_DISABLE = new ErrorCode(1_040_000_001, "API 密钥已禁用!"); 14 ErrorCode API_KEY_DISABLE = new ErrorCode(1_040_000_001, "API 密钥已禁用!");
  15 + ErrorCode API_CONFIG_PLACEHOLDER_NOT_RESOLVED = new ErrorCode(1_040_000_002, "AI 配置({})无法解析,请检查环境变量或配置项");
15 16
16 // ========== API 模型 1-040-001-000 ========== 17 // ========== API 模型 1-040-001-000 ==========
17 ErrorCode MODEL_NOT_EXISTS = new ErrorCode(1_040_001_000, "模型不存在!"); 18 ErrorCode MODEL_NOT_EXISTS = new ErrorCode(1_040_001_000, "模型不存在!");
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiPlatformEnum.java
@@ -28,6 +28,7 @@ public enum AiPlatformEnum implements ArrayValuable&lt;String&gt; { @@ -28,6 +28,7 @@ public enum AiPlatformEnum implements ArrayValuable&lt;String&gt; {
28 MINI_MAX("MiniMax", "MiniMax"), // 稀宇科技 28 MINI_MAX("MiniMax", "MiniMax"), // 稀宇科技
29 MOONSHOT("Moonshot", "月之暗面"), // KIMI 29 MOONSHOT("Moonshot", "月之暗面"), // KIMI
30 BAI_CHUAN("BaiChuan", "百川智能"), // 百川智能 30 BAI_CHUAN("BaiChuan", "百川智能"), // 百川智能
  31 + STEP_FUN("StepFun", "阶跃星辰"), // 阶跃星辰
31 32
32 // ========== 国外平台 ========== 33 // ========== 国外平台 ==========
33 34
@@ -40,7 +41,7 @@ public enum AiPlatformEnum implements ArrayValuable&lt;String&gt; { @@ -40,7 +41,7 @@ public enum AiPlatformEnum implements ArrayValuable&lt;String&gt; {
40 STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI 41 STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI
41 MIDJOURNEY("Midjourney", "Midjourney"), // Midjourney 42 MIDJOURNEY("Midjourney", "Midjourney"), // Midjourney
42 SUNO("Suno", "Suno"), // Suno AI 43 SUNO("Suno", "Suno"), // Suno AI
43 - GROK("Grok","Grok"), // Grok 44 + GROK("Grok", "Grok"), // Grok
44 45
45 ; 46 ;
46 47
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 @@ -10,10 +10,15 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel
10 import cn.iocoder.yudao.module.ai.framework.ai.core.model.grok.GrokChatModel; 10 import cn.iocoder.yudao.module.ai.framework.ai.core.model.grok.GrokChatModel;
11 import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; 11 import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel;
12 import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; 12 import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
  13 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax.MiniMaxChatModel;
  14 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot.MoonshotChatModel;
13 import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants; 15 import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants;
14 import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel; 16 import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel;
  17 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun.StepFunChatModel;
15 import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; 18 import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
16 import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; 19 import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
  20 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan.YiYanChatModel;
  21 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu.ZhiPuChatModel;
17 import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient; 22 import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient;
18 import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient; 23 import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient;
19 import cn.iocoder.yudao.module.ai.tool.method.PersonService; 24 import cn.iocoder.yudao.module.ai.tool.method.PersonService;
@@ -44,7 +49,6 @@ import org.springframework.context.annotation.Bean; @@ -44,7 +49,6 @@ import org.springframework.context.annotation.Bean;
44 import org.springframework.context.annotation.Configuration; 49 import org.springframework.context.annotation.Configuration;
45 50
46 import java.util.List; 51 import java.util.List;
47 -import java.util.Optional;  
48 52
49 /** 53 /**
50 * 芋道 AI 自动配置 54 * 芋道 AI 自动配置
@@ -113,13 +117,13 @@ public class AiAutoConfiguration { @@ -113,13 +117,13 @@ public class AiAutoConfiguration {
113 if (StrUtil.isEmpty(properties.getModel())) { 117 if (StrUtil.isEmpty(properties.getModel())) {
114 properties.setModel(DouBaoChatModel.MODEL_DEFAULT); 118 properties.setModel(DouBaoChatModel.MODEL_DEFAULT);
115 } 119 }
116 - OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()  
117 - .openAiApi(OpenAiApi.builder() 120 + DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
  121 + .deepSeekApi(DeepSeekApi.builder()
118 .baseUrl(DouBaoChatModel.BASE_URL) 122 .baseUrl(DouBaoChatModel.BASE_URL)
119 .completionsPath(DouBaoChatModel.COMPLETE_PATH) 123 .completionsPath(DouBaoChatModel.COMPLETE_PATH)
120 .apiKey(properties.getApiKey()) 124 .apiKey(properties.getApiKey())
121 .build()) 125 .build())
122 - .defaultOptions(OpenAiChatOptions.builder() 126 + .defaultOptions(DeepSeekChatOptions.builder()
123 .model(properties.getModel()) 127 .model(properties.getModel())
124 .temperature(properties.getTemperature()) 128 .temperature(properties.getTemperature())
125 .maxTokens(properties.getMaxTokens()) 129 .maxTokens(properties.getMaxTokens())
@@ -168,11 +172,8 @@ public class AiAutoConfiguration { @@ -168,11 +172,8 @@ public class AiAutoConfiguration {
168 if (StrUtil.isEmpty(properties.getModel())) { 172 if (StrUtil.isEmpty(properties.getModel())) {
169 properties.setModel(HunYuanChatModel.MODEL_DEFAULT); 173 properties.setModel(HunYuanChatModel.MODEL_DEFAULT);
170 } 174 }
171 - // 特殊:由于混元大模型不提供 deepseek,而是通过知识引擎,所以需要区分下 URL  
172 if (StrUtil.isEmpty(properties.getBaseUrl())) { 175 if (StrUtil.isEmpty(properties.getBaseUrl())) {
173 - properties.setBaseUrl(  
174 - StrUtil.startWithIgnoreCase(properties.getModel(), "deepseek") ? HunYuanChatModel.DEEP_SEEK_BASE_URL  
175 - : HunYuanChatModel.BASE_URL); 176 + properties.setBaseUrl(HunYuanChatModel.BASE_URL);
176 } 177 }
177 // 创建 DeepSeekChatModel、HunYuanChatModel 对象 178 // 创建 DeepSeekChatModel、HunYuanChatModel 对象
178 DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() 179 DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
@@ -203,25 +204,15 @@ public class AiAutoConfiguration { @@ -203,25 +204,15 @@ public class AiAutoConfiguration {
203 if (StrUtil.isEmpty(properties.getModel())) { 204 if (StrUtil.isEmpty(properties.getModel())) {
204 properties.setModel(XingHuoChatModel.MODEL_DEFAULT); 205 properties.setModel(XingHuoChatModel.MODEL_DEFAULT);
205 } 206 }
206 - OpenAiApi.Builder builder = OpenAiApi.builder()  
207 - .baseUrl(XingHuoChatModel.BASE_URL_V1)  
208 - .apiKey(properties.getAppKey() + ":" + properties.getSecretKey());  
209 - if ("x1".equals(properties.getModel())) {  
210 - builder.baseUrl(XingHuoChatModel.BASE_URL_V2)  
211 - .completionsPath(XingHuoChatModel.BASE_COMPLETIONS_PATH_V2);  
212 - }  
213 - OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()  
214 - .openAiApi(builder.build())  
215 - .defaultOptions(OpenAiChatOptions.builder() 207 + return XingHuoChatModel.builder()
  208 + .apiKey(properties.getApiKey())
  209 + .options(DeepSeekChatOptions.builder()
216 .model(properties.getModel()) 210 .model(properties.getModel())
217 .temperature(properties.getTemperature()) 211 .temperature(properties.getTemperature())
218 .maxTokens(properties.getMaxTokens()) 212 .maxTokens(properties.getMaxTokens())
219 .topP(properties.getTopP()) 213 .topP(properties.getTopP())
220 .build()) 214 .build())
221 - // TODO @芋艿:星火的 function call 有 bug,会报 ToolResponseMessage must have an id 错误!!!  
222 - .toolCallingManager(getToolCallingManager())  
223 .build(); 215 .build();
224 - return new XingHuoChatModel(openAiChatModel);  
225 } 216 }
226 217
227 @Bean 218 @Bean
@@ -235,12 +226,13 @@ public class AiAutoConfiguration { @@ -235,12 +226,13 @@ public class AiAutoConfiguration {
235 if (StrUtil.isEmpty(properties.getModel())) { 226 if (StrUtil.isEmpty(properties.getModel())) {
236 properties.setModel(BaiChuanChatModel.MODEL_DEFAULT); 227 properties.setModel(BaiChuanChatModel.MODEL_DEFAULT);
237 } 228 }
238 - OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()  
239 - .openAiApi(OpenAiApi.builder() 229 + DeepSeekChatModel deepSeekChatModel = DeepSeekChatModel.builder()
  230 + .deepSeekApi(DeepSeekApi.builder()
240 .baseUrl(BaiChuanChatModel.BASE_URL) 231 .baseUrl(BaiChuanChatModel.BASE_URL)
241 .apiKey(properties.getApiKey()) 232 .apiKey(properties.getApiKey())
  233 + .completionsPath(BaiChuanChatModel.COMPLETE_PATH)
242 .build()) 234 .build())
243 - .defaultOptions(OpenAiChatOptions.builder() 235 + .defaultOptions(DeepSeekChatOptions.builder()
244 .model(properties.getModel()) 236 .model(properties.getModel())
245 .temperature(properties.getTemperature()) 237 .temperature(properties.getTemperature())
246 .maxTokens(properties.getMaxTokens()) 238 .maxTokens(properties.getMaxTokens())
@@ -248,7 +240,123 @@ public class AiAutoConfiguration { @@ -248,7 +240,123 @@ public class AiAutoConfiguration {
248 .build()) 240 .build())
249 .toolCallingManager(getToolCallingManager()) 241 .toolCallingManager(getToolCallingManager())
250 .build(); 242 .build();
251 - return new BaiChuanChatModel(openAiChatModel); 243 + return new BaiChuanChatModel(deepSeekChatModel);
  244 + }
  245 +
  246 + @Bean
  247 + @ConditionalOnProperty(value = "yudao.ai.yiyan.enable", havingValue = "true")
  248 + public YiYanChatModel yiYanChatClient(YudaoAiProperties yudaoAiProperties) {
  249 + YudaoAiProperties.YiYan properties = yudaoAiProperties.getYiyan();
  250 + return buildYiYanChatClient(properties);
  251 + }
  252 +
  253 + public YiYanChatModel buildYiYanChatClient(YudaoAiProperties.YiYan properties) {
  254 + if (StrUtil.isEmpty(properties.getModel())) {
  255 + properties.setModel(YiYanChatModel.MODEL_DEFAULT);
  256 + }
  257 + return new YiYanChatModel(buildDeepSeekCompatibleChatModel(
  258 + StrUtil.blankToDefault(properties.getBaseUrl(), YiYanChatModel.BASE_URL),
  259 + null, properties.getApiKey(), properties.getModel(), properties.getTemperature(),
  260 + properties.getMaxTokens(), properties.getTopP()));
  261 + }
  262 +
  263 + @Bean
  264 + @ConditionalOnProperty(value = "yudao.ai.zhipu.enable", havingValue = "true")
  265 + public ZhiPuChatModel zhiPuChatClient(YudaoAiProperties yudaoAiProperties) {
  266 + YudaoAiProperties.ZhiPu properties = yudaoAiProperties.getZhipu();
  267 + return buildZhiPuChatClient(properties);
  268 + }
  269 +
  270 + public ZhiPuChatModel buildZhiPuChatClient(YudaoAiProperties.ZhiPu properties) {
  271 + if (StrUtil.isEmpty(properties.getModel())) {
  272 + properties.setModel(ZhiPuChatModel.MODEL_DEFAULT);
  273 + }
  274 + DeepSeekChatModel deepSeekChatModel = DeepSeekChatModel.builder()
  275 + .deepSeekApi(DeepSeekApi.builder()
  276 + .baseUrl(StrUtil.blankToDefault(properties.getBaseUrl(), ZhiPuChatModel.BASE_URL))
  277 + .apiKey(properties.getApiKey())
  278 + .build())
  279 + .defaultOptions(DeepSeekChatOptions.builder()
  280 + .model(properties.getModel())
  281 + .temperature(properties.getTemperature())
  282 + .maxTokens(properties.getMaxTokens())
  283 + .topP(properties.getTopP())
  284 + .build())
  285 + .build();
  286 + return new ZhiPuChatModel(deepSeekChatModel);
  287 + }
  288 +
  289 + @Bean
  290 + @ConditionalOnProperty(value = "yudao.ai.minimax.enable", havingValue = "true")
  291 + public MiniMaxChatModel miniMaxChatClient(YudaoAiProperties yudaoAiProperties) {
  292 + YudaoAiProperties.MiniMax properties = yudaoAiProperties.getMinimax();
  293 + return buildMiniMaxChatClient(properties);
  294 + }
  295 +
  296 + public MiniMaxChatModel buildMiniMaxChatClient(YudaoAiProperties.MiniMax properties) {
  297 + if (StrUtil.isEmpty(properties.getModel())) {
  298 + properties.setModel(MiniMaxChatModel.MODEL_DEFAULT);
  299 + }
  300 + return new MiniMaxChatModel(buildDeepSeekCompatibleChatModel(
  301 + StrUtil.blankToDefault(properties.getBaseUrl(), MiniMaxChatModel.BASE_URL),
  302 + null, properties.getApiKey(), properties.getModel(), properties.getTemperature(),
  303 + properties.getMaxTokens(), properties.getTopP()));
  304 + }
  305 +
  306 + @Bean
  307 + @ConditionalOnProperty(value = "yudao.ai.moonshot.enable", havingValue = "true")
  308 + public MoonshotChatModel moonshotChatClient(YudaoAiProperties yudaoAiProperties) {
  309 + YudaoAiProperties.Moonshot properties = yudaoAiProperties.getMoonshot();
  310 + return buildMoonshotChatClient(properties);
  311 + }
  312 +
  313 + public MoonshotChatModel buildMoonshotChatClient(YudaoAiProperties.Moonshot properties) {
  314 + if (StrUtil.isEmpty(properties.getModel())) {
  315 + properties.setModel(MoonshotChatModel.MODEL_DEFAULT);
  316 + }
  317 + return new MoonshotChatModel(buildDeepSeekCompatibleChatModel(
  318 + StrUtil.blankToDefault(properties.getBaseUrl(), MoonshotChatModel.BASE_URL),
  319 + MoonshotChatModel.COMPLETE_PATH, properties.getApiKey(), properties.getModel(),
  320 + properties.getTemperature(), properties.getMaxTokens(), properties.getTopP()));
  321 + }
  322 +
  323 + @Bean
  324 + @ConditionalOnProperty(value = "yudao.ai.stepfun.enable", havingValue = "true")
  325 + public StepFunChatModel stepFunChatClient(YudaoAiProperties yudaoAiProperties) {
  326 + YudaoAiProperties.StepFun properties = yudaoAiProperties.getStepfun();
  327 + return buildStepFunChatClient(properties);
  328 + }
  329 +
  330 + public StepFunChatModel buildStepFunChatClient(YudaoAiProperties.StepFun properties) {
  331 + if (StrUtil.isEmpty(properties.getModel())) {
  332 + properties.setModel(StepFunChatModel.MODEL_DEFAULT);
  333 + }
  334 + return new StepFunChatModel(buildDeepSeekCompatibleChatModel(
  335 + StrUtil.blankToDefault(properties.getBaseUrl(), StepFunChatModel.BASE_URL),
  336 + StepFunChatModel.COMPLETE_PATH, properties.getApiKey(), properties.getModel(),
  337 + properties.getTemperature(), properties.getMaxTokens(), properties.getTopP()));
  338 + }
  339 +
  340 + private static DeepSeekChatModel buildDeepSeekCompatibleChatModel(String baseUrl, String completionsPath,
  341 + String apiKey, String model,
  342 + Double temperature, Integer maxTokens,
  343 + Double topP) {
  344 + DeepSeekApi.Builder apiBuilder = DeepSeekApi.builder()
  345 + .baseUrl(baseUrl)
  346 + .apiKey(apiKey);
  347 + if (StrUtil.isNotEmpty(completionsPath)) {
  348 + apiBuilder.completionsPath(completionsPath);
  349 + }
  350 + return DeepSeekChatModel.builder()
  351 + .deepSeekApi(apiBuilder.build())
  352 + .defaultOptions(DeepSeekChatOptions.builder()
  353 + .model(model)
  354 + .temperature(temperature)
  355 + .maxTokens(maxTokens)
  356 + .topP(topP)
  357 + .build())
  358 + .toolCallingManager(getToolCallingManager())
  359 + .build();
252 } 360 }
253 361
254 @Bean 362 @Bean
@@ -264,14 +372,20 @@ public class AiAutoConfiguration { @@ -264,14 +372,20 @@ public class AiAutoConfiguration {
264 return new SunoApi(yudaoAiProperties.getSuno().getBaseUrl()); 372 return new SunoApi(yudaoAiProperties.getSuno().getBaseUrl());
265 } 373 }
266 374
267 - public ChatModel buildGrokChatClient(YudaoAiProperties.Grok properties) { 375 + @Bean
  376 + @ConditionalOnProperty(value = "yudao.ai.grok.enable", havingValue = "true")
  377 + public GrokChatModel grokChatClient(YudaoAiProperties yudaoAiProperties) {
  378 + YudaoAiProperties.Grok properties = yudaoAiProperties.getGrok();
  379 + return buildGrokChatClient(properties);
  380 + }
  381 +
  382 + public GrokChatModel buildGrokChatClient(YudaoAiProperties.Grok properties) {
268 if (StrUtil.isEmpty(properties.getModel())) { 383 if (StrUtil.isEmpty(properties.getModel())) {
269 properties.setModel(GrokChatModel.MODEL_DEFAULT); 384 properties.setModel(GrokChatModel.MODEL_DEFAULT);
270 } 385 }
271 OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() 386 OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
272 .openAiApi(OpenAiApi.builder() 387 .openAiApi(OpenAiApi.builder()
273 - .baseUrl(Optional.ofNullable(properties.getBaseUrl())  
274 - .orElse(GrokChatModel.BASE_URL)) 388 + .baseUrl(StrUtil.blankToDefault(properties.getBaseUrl(), GrokChatModel.BASE_URL))
275 .completionsPath(GrokChatModel.COMPLETE_PATH) 389 .completionsPath(GrokChatModel.COMPLETE_PATH)
276 .apiKey(properties.getApiKey()) 390 .apiKey(properties.getApiKey())
277 .build()) 391 .build())
@@ -283,7 +397,7 @@ public class AiAutoConfiguration { @@ -283,7 +397,7 @@ public class AiAutoConfiguration {
283 .build()) 397 .build())
284 .toolCallingManager(getToolCallingManager()) 398 .toolCallingManager(getToolCallingManager())
285 .build(); 399 .build();
286 - return new DouBaoChatModel(openAiChatModel); 400 + return new GrokChatModel(openAiChatModel);
287 } 401 }
288 402
289 // ========== RAG 相关 ========== 403 // ========== RAG 相关 ==========
@@ -320,4 +434,4 @@ public class AiAutoConfiguration { @@ -320,4 +434,4 @@ public class AiAutoConfiguration {
320 return List.of(ToolCallbacks.from(personService)); 434 return List.of(ToolCallbacks.from(personService));
321 } 435 }
322 436
323 -}  
324 \ No newline at end of file 437 \ No newline at end of file
  438 +}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java
@@ -44,6 +44,36 @@ public class YudaoAiProperties { @@ -44,6 +44,36 @@ public class YudaoAiProperties {
44 private BaiChuan baichuan; 44 private BaiChuan baichuan;
45 45
46 /** 46 /**
  47 + * 文心一言
  48 + */
  49 + private YiYan yiyan;
  50 +
  51 + /**
  52 + * 智谱
  53 + */
  54 + private ZhiPu zhipu;
  55 +
  56 + /**
  57 + * MiniMax
  58 + */
  59 + private MiniMax minimax;
  60 +
  61 + /**
  62 + * 月之暗面
  63 + */
  64 + private Moonshot moonshot;
  65 +
  66 + /**
  67 + * 阶跃星辰
  68 + */
  69 + private StepFun stepfun;
  70 +
  71 + /**
  72 + * Grok
  73 + */
  74 + private Grok grok;
  75 +
  76 + /**
47 * Midjourney 绘图 77 * Midjourney 绘图
48 */ 78 */
49 private Midjourney midjourney; 79 private Midjourney midjourney;
@@ -116,9 +146,7 @@ public class YudaoAiProperties { @@ -116,9 +146,7 @@ public class YudaoAiProperties {
116 public static class XingHuo { 146 public static class XingHuo {
117 147
118 private String enable; 148 private String enable;
119 - private String appId;  
120 - private String appKey;  
121 - private String secretKey; 149 + private String apiKey;
122 150
123 private String model; 151 private String model;
124 private Double temperature; 152 private Double temperature;
@@ -141,6 +169,76 @@ public class YudaoAiProperties { @@ -141,6 +169,76 @@ public class YudaoAiProperties {
141 } 169 }
142 170
143 @Data 171 @Data
  172 + public static class YiYan {
  173 +
  174 + private String enable;
  175 + private String baseUrl;
  176 + private String apiKey;
  177 +
  178 + private String model;
  179 + private Double temperature;
  180 + private Integer maxTokens;
  181 + private Double topP;
  182 +
  183 + }
  184 +
  185 + @Data
  186 + public static class ZhiPu {
  187 +
  188 + private String enable;
  189 + private String baseUrl;
  190 + private String apiKey;
  191 +
  192 + private String model;
  193 + private Double temperature;
  194 + private Integer maxTokens;
  195 + private Double topP;
  196 +
  197 + }
  198 +
  199 + @Data
  200 + public static class MiniMax {
  201 +
  202 + private String enable;
  203 + private String baseUrl;
  204 + private String apiKey;
  205 +
  206 + private String model;
  207 + private Double temperature;
  208 + private Integer maxTokens;
  209 + private Double topP;
  210 +
  211 + }
  212 +
  213 + @Data
  214 + public static class Moonshot {
  215 +
  216 + private String enable;
  217 + private String baseUrl;
  218 + private String apiKey;
  219 +
  220 + private String model;
  221 + private Double temperature;
  222 + private Integer maxTokens;
  223 + private Double topP;
  224 +
  225 + }
  226 +
  227 + @Data
  228 + public static class StepFun {
  229 +
  230 + private String enable;
  231 + private String apiKey;
  232 + private String baseUrl;
  233 +
  234 + private String model;
  235 + private Double temperature;
  236 + private Integer maxTokens;
  237 + private Double topP;
  238 +
  239 + }
  240 +
  241 + @Data
144 public static class Midjourney { 242 public static class Midjourney {
145 243
146 private String enable; 244 private String enable;
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactoryImpl.java
1 package cn.iocoder.yudao.module.ai.framework.ai.core.model; 1 package cn.iocoder.yudao.module.ai.framework.ai.core.model;
2 2
3 import cn.hutool.core.io.FileUtil; 3 import cn.hutool.core.io.FileUtil;
4 -import cn.hutool.core.lang.Assert;  
5 import cn.hutool.core.lang.Singleton; 4 import cn.hutool.core.lang.Singleton;
6 import cn.hutool.core.lang.func.Func0; 5 import cn.hutool.core.lang.func.Func0;
7 import cn.hutool.core.util.ArrayUtil; 6 import cn.hutool.core.util.ArrayUtil;
@@ -15,14 +14,21 @@ import cn.iocoder.yudao.module.ai.framework.ai.config.YudaoAiProperties; @@ -15,14 +14,21 @@ import cn.iocoder.yudao.module.ai.framework.ai.config.YudaoAiProperties;
15 import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; 14 import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
16 import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel; 15 import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel;
17 import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel; 16 import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel;
  17 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.grok.GrokChatModel;
18 import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; 18 import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel;
19 import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; 19 import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
  20 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax.MiniMaxChatModel;
  21 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot.MoonshotChatModel;
20 import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants; 22 import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants;
21 import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel; 23 import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel;
22 import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageApi; 24 import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageApi;
23 import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageModel; 25 import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageModel;
  26 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun.StepFunChatModel;
24 import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; 27 import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
25 import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; 28 import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
  29 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan.YiYanChatModel;
  30 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu.ZhiPuChatModel;
  31 +import cn.iocoder.yudao.module.ai.util.AiUtils;
26 import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeChatAutoConfiguration; 32 import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeChatAutoConfiguration;
27 import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeEmbeddingAutoConfiguration; 33 import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeEmbeddingAutoConfiguration;
28 import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeImageAutoConfiguration; 34 import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeImageAutoConfiguration;
@@ -40,18 +46,6 @@ import io.milvus.client.MilvusServiceClient; @@ -40,18 +46,6 @@ import io.milvus.client.MilvusServiceClient;
40 import io.qdrant.client.QdrantClient; 46 import io.qdrant.client.QdrantClient;
41 import io.qdrant.client.QdrantGrpcClient; 47 import io.qdrant.client.QdrantGrpcClient;
42 import lombok.SneakyThrows; 48 import lombok.SneakyThrows;
43 -import org.springaicommunity.moonshot.MoonshotChatModel;  
44 -import org.springaicommunity.moonshot.MoonshotChatOptions;  
45 -import org.springaicommunity.moonshot.api.MoonshotApi;  
46 -import org.springaicommunity.moonshot.autoconfigure.MoonshotChatAutoConfiguration;  
47 -import org.springaicommunity.qianfan.QianFanChatModel;  
48 -import org.springaicommunity.qianfan.QianFanEmbeddingModel;  
49 -import org.springaicommunity.qianfan.QianFanEmbeddingOptions;  
50 -import org.springaicommunity.qianfan.QianFanImageModel;  
51 -import org.springaicommunity.qianfan.api.QianFanApi;  
52 -import org.springaicommunity.qianfan.api.QianFanImageApi;  
53 -import org.springaicommunity.qianfan.autoconfigure.QianFanChatAutoConfiguration;  
54 -import org.springaicommunity.qianfan.autoconfigure.QianFanEmbeddingAutoConfiguration;  
55 import org.springframework.ai.azure.openai.AzureOpenAiChatModel; 49 import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
56 import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel; 50 import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel;
57 import org.springframework.ai.chat.model.ChatModel; 51 import org.springframework.ai.chat.model.ChatModel;
@@ -63,27 +57,17 @@ import org.springframework.ai.embedding.BatchingStrategy; @@ -63,27 +57,17 @@ import org.springframework.ai.embedding.BatchingStrategy;
63 import org.springframework.ai.embedding.EmbeddingModel; 57 import org.springframework.ai.embedding.EmbeddingModel;
64 import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; 58 import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
65 import org.springframework.ai.image.ImageModel; 59 import org.springframework.ai.image.ImageModel;
66 -import org.springframework.ai.minimax.MiniMaxChatModel;  
67 -import org.springframework.ai.minimax.MiniMaxChatOptions;  
68 -import org.springframework.ai.minimax.MiniMaxEmbeddingModel;  
69 -import org.springframework.ai.minimax.MiniMaxEmbeddingOptions;  
70 -import org.springframework.ai.minimax.api.MiniMaxApi;  
71 import org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration; 60 import org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration;
72 import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration; 61 import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration;
73 import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingAutoConfiguration; 62 import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingAutoConfiguration;
74 import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingProperties; 63 import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingProperties;
75 import org.springframework.ai.model.deepseek.autoconfigure.DeepSeekChatAutoConfiguration; 64 import org.springframework.ai.model.deepseek.autoconfigure.DeepSeekChatAutoConfiguration;
76 -import org.springframework.ai.model.minimax.autoconfigure.MiniMaxChatAutoConfiguration;  
77 -import org.springframework.ai.model.minimax.autoconfigure.MiniMaxEmbeddingAutoConfiguration;  
78 import org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration; 65 import org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration;
79 import org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration; 66 import org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration;
80 import org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration; 67 import org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration;
81 import org.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfiguration; 68 import org.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfiguration;
82 import org.springframework.ai.model.stabilityai.autoconfigure.StabilityAiImageAutoConfiguration; 69 import org.springframework.ai.model.stabilityai.autoconfigure.StabilityAiImageAutoConfiguration;
83 import org.springframework.ai.model.tool.ToolCallingManager; 70 import org.springframework.ai.model.tool.ToolCallingManager;
84 -import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiChatAutoConfiguration;  
85 -import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiEmbeddingAutoConfiguration;  
86 -import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiImageAutoConfiguration;  
87 import org.springframework.ai.ollama.OllamaChatModel; 71 import org.springframework.ai.ollama.OllamaChatModel;
88 import org.springframework.ai.ollama.OllamaEmbeddingModel; 72 import org.springframework.ai.ollama.OllamaEmbeddingModel;
89 import org.springframework.ai.ollama.api.OllamaApi; 73 import org.springframework.ai.ollama.api.OllamaApi;
@@ -114,13 +98,9 @@ import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStore @@ -114,13 +98,9 @@ import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStore
114 import org.springframework.ai.vectorstore.redis.RedisVectorStore; 98 import org.springframework.ai.vectorstore.redis.RedisVectorStore;
115 import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreAutoConfiguration; 99 import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreAutoConfiguration;
116 import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreProperties; 100 import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreProperties;
117 -import org.springframework.ai.zhipuai.*;  
118 -import org.springframework.ai.zhipuai.api.ZhiPuAiApi;  
119 -import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi;  
120 import org.springframework.beans.BeansException; 101 import org.springframework.beans.BeansException;
121 import org.springframework.beans.factory.ObjectProvider; 102 import org.springframework.beans.factory.ObjectProvider;
122 import org.springframework.boot.autoconfigure.data.redis.RedisProperties; 103 import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
123 -import org.springframework.web.client.RestClient;  
124 import redis.clients.jedis.JedisPooled; 104 import redis.clients.jedis.JedisPooled;
125 105
126 import java.io.File; 106 import java.io.File;
@@ -131,7 +111,6 @@ import java.util.Timer; @@ -131,7 +111,6 @@ import java.util.Timer;
131 import java.util.TimerTask; 111 import java.util.TimerTask;
132 112
133 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; 113 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
134 -import static org.springframework.ai.retry.RetryUtils.DEFAULT_RETRY_TEMPLATE;  
135 114
136 /** 115 /**
137 * AI Model 模型工厂的实现类 116 * AI Model 模型工厂的实现类
@@ -141,7 +120,9 @@ import static org.springframework.ai.retry.RetryUtils.DEFAULT_RETRY_TEMPLATE; @@ -141,7 +120,9 @@ import static org.springframework.ai.retry.RetryUtils.DEFAULT_RETRY_TEMPLATE;
141 public class AiModelFactoryImpl implements AiModelFactory { 120 public class AiModelFactoryImpl implements AiModelFactory {
142 121
143 @Override 122 @Override
144 - public ChatModel getOrCreateChatModel(AiPlatformEnum platform, String apiKey, String url) { 123 + public ChatModel getOrCreateChatModel(AiPlatformEnum platform, String rawApiKey, String rawUrl) {
  124 + final String apiKey = resolveSpringPlaceholders(rawApiKey);
  125 + final String url = resolveSpringPlaceholders(rawUrl);
145 String cacheKey = buildClientCacheKey(ChatModel.class, platform, apiKey, url); 126 String cacheKey = buildClientCacheKey(ChatModel.class, platform, apiKey, url);
146 return Singleton.get(cacheKey, (Func0<ChatModel>) () -> { 127 return Singleton.get(cacheKey, (Func0<ChatModel>) () -> {
147 // noinspection EnhancedSwitchMigration 128 // noinspection EnhancedSwitchMigration
@@ -164,6 +145,8 @@ public class AiModelFactoryImpl implements AiModelFactory { @@ -164,6 +145,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
164 return buildMiniMaxChatModel(apiKey, url); 145 return buildMiniMaxChatModel(apiKey, url);
165 case MOONSHOT: 146 case MOONSHOT:
166 return buildMoonshotChatModel(apiKey, url); 147 return buildMoonshotChatModel(apiKey, url);
  148 + case STEP_FUN:
  149 + return buildStepFunChatModel(apiKey, url);
167 case XING_HUO: 150 case XING_HUO:
168 return buildXingHuoChatModel(apiKey); 151 return buildXingHuoChatModel(apiKey);
169 case BAI_CHUAN: 152 case BAI_CHUAN:
@@ -179,7 +162,7 @@ public class AiModelFactoryImpl implements AiModelFactory { @@ -179,7 +162,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
179 case OLLAMA: 162 case OLLAMA:
180 return buildOllamaChatModel(url); 163 return buildOllamaChatModel(url);
181 case GROK: 164 case GROK:
182 - return buildGrokChatModel(apiKey,url); 165 + return buildGrokChatModel(apiKey, url);
183 default: 166 default:
184 throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); 167 throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
185 } 168 }
@@ -193,7 +176,7 @@ public class AiModelFactoryImpl implements AiModelFactory { @@ -193,7 +176,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
193 case TONG_YI: 176 case TONG_YI:
194 return SpringUtil.getBean(DashScopeChatModel.class); 177 return SpringUtil.getBean(DashScopeChatModel.class);
195 case YI_YAN: 178 case YI_YAN:
196 - return SpringUtil.getBean(QianFanChatModel.class); 179 + return SpringUtil.getBean(YiYanChatModel.class);
197 case DEEP_SEEK: 180 case DEEP_SEEK:
198 return SpringUtil.getBean(DeepSeekChatModel.class); 181 return SpringUtil.getBean(DeepSeekChatModel.class);
199 case DOU_BAO: 182 case DOU_BAO:
@@ -203,11 +186,13 @@ public class AiModelFactoryImpl implements AiModelFactory { @@ -203,11 +186,13 @@ public class AiModelFactoryImpl implements AiModelFactory {
203 case SILICON_FLOW: 186 case SILICON_FLOW:
204 return SpringUtil.getBean(SiliconFlowChatModel.class); 187 return SpringUtil.getBean(SiliconFlowChatModel.class);
205 case ZHI_PU: 188 case ZHI_PU:
206 - return SpringUtil.getBean(ZhiPuAiChatModel.class); 189 + return SpringUtil.getBean(ZhiPuChatModel.class);
207 case MINI_MAX: 190 case MINI_MAX:
208 return SpringUtil.getBean(MiniMaxChatModel.class); 191 return SpringUtil.getBean(MiniMaxChatModel.class);
209 case MOONSHOT: 192 case MOONSHOT:
210 return SpringUtil.getBean(MoonshotChatModel.class); 193 return SpringUtil.getBean(MoonshotChatModel.class);
  194 + case STEP_FUN:
  195 + return SpringUtil.getBean(StepFunChatModel.class);
211 case XING_HUO: 196 case XING_HUO:
212 return SpringUtil.getBean(XingHuoChatModel.class); 197 return SpringUtil.getBean(XingHuoChatModel.class);
213 case BAI_CHUAN: 198 case BAI_CHUAN:
@@ -222,6 +207,8 @@ public class AiModelFactoryImpl implements AiModelFactory { @@ -222,6 +207,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
222 return SpringUtil.getBean(GeminiChatModel.class); 207 return SpringUtil.getBean(GeminiChatModel.class);
223 case OLLAMA: 208 case OLLAMA:
224 return SpringUtil.getBean(OllamaChatModel.class); 209 return SpringUtil.getBean(OllamaChatModel.class);
  210 + case GROK:
  211 + return SpringUtil.getBean(GrokChatModel.class);
225 default: 212 default:
226 throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); 213 throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
227 } 214 }
@@ -233,10 +220,6 @@ public class AiModelFactoryImpl implements AiModelFactory { @@ -233,10 +220,6 @@ public class AiModelFactoryImpl implements AiModelFactory {
233 switch (platform) { 220 switch (platform) {
234 case TONG_YI: 221 case TONG_YI:
235 return SpringUtil.getBean(DashScopeImageModel.class); 222 return SpringUtil.getBean(DashScopeImageModel.class);
236 - case YI_YAN:  
237 - return SpringUtil.getBean(QianFanImageModel.class);  
238 - case ZHI_PU:  
239 - return SpringUtil.getBean(ZhiPuAiImageModel.class);  
240 case SILICON_FLOW: 223 case SILICON_FLOW:
241 return SpringUtil.getBean(SiliconFlowImageModel.class); 224 return SpringUtil.getBean(SiliconFlowImageModel.class);
242 case OPENAI: 225 case OPENAI:
@@ -249,19 +232,17 @@ public class AiModelFactoryImpl implements AiModelFactory { @@ -249,19 +232,17 @@ public class AiModelFactoryImpl implements AiModelFactory {
249 } 232 }
250 233
251 @Override 234 @Override
252 - public ImageModel getOrCreateImageModel(AiPlatformEnum platform, String apiKey, String url) { 235 + public ImageModel getOrCreateImageModel(AiPlatformEnum platform, String rawApiKey, String rawUrl) {
  236 + String apiKey = resolveSpringPlaceholders(rawApiKey);
  237 + String url = resolveSpringPlaceholders(rawUrl);
253 // noinspection EnhancedSwitchMigration 238 // noinspection EnhancedSwitchMigration
254 switch (platform) { 239 switch (platform) {
255 case TONG_YI: 240 case TONG_YI:
256 return buildTongYiImagesModel(apiKey); 241 return buildTongYiImagesModel(apiKey);
257 - case YI_YAN:  
258 - return buildQianFanImageModel(apiKey);  
259 - case ZHI_PU:  
260 - return buildZhiPuAiImageModel(apiKey, url);  
261 case OPENAI: 242 case OPENAI:
262 return buildOpenAiImageModel(apiKey, url); 243 return buildOpenAiImageModel(apiKey, url);
263 case SILICON_FLOW: 244 case SILICON_FLOW:
264 - return buildSiliconFlowImageModel(apiKey,url); 245 + return buildSiliconFlowImageModel(apiKey, url);
265 case STABLE_DIFFUSION: 246 case STABLE_DIFFUSION:
266 return buildStabilityAiImageModel(apiKey, url); 247 return buildStabilityAiImageModel(apiKey, url);
267 default: 248 default:
@@ -270,9 +251,11 @@ public class AiModelFactoryImpl implements AiModelFactory { @@ -270,9 +251,11 @@ public class AiModelFactoryImpl implements AiModelFactory {
270 } 251 }
271 252
272 @Override 253 @Override
273 - public MidjourneyApi getOrCreateMidjourneyApi(String apiKey, String url) {  
274 - String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey,  
275 - url); 254 + public MidjourneyApi getOrCreateMidjourneyApi(String rawApiKey, String rawUrl) {
  255 + final String apiKey = resolveSpringPlaceholders(rawApiKey);
  256 + final String url = resolveSpringPlaceholders(rawUrl);
  257 + String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(),
  258 + apiKey, url);
276 return Singleton.get(cacheKey, (Func0<MidjourneyApi>) () -> { 259 return Singleton.get(cacheKey, (Func0<MidjourneyApi>) () -> {
277 YudaoAiProperties.Midjourney properties = SpringUtil.getBean(YudaoAiProperties.class) 260 YudaoAiProperties.Midjourney properties = SpringUtil.getBean(YudaoAiProperties.class)
278 .getMidjourney(); 261 .getMidjourney();
@@ -281,25 +264,23 @@ public class AiModelFactoryImpl implements AiModelFactory { @@ -281,25 +264,23 @@ public class AiModelFactoryImpl implements AiModelFactory {
281 } 264 }
282 265
283 @Override 266 @Override
284 - public SunoApi getOrCreateSunoApi(String apiKey, String url) { 267 + public SunoApi getOrCreateSunoApi(String rawApiKey, String rawUrl) {
  268 + final String apiKey = resolveSpringPlaceholders(rawApiKey);
  269 + final String url = resolveSpringPlaceholders(rawUrl);
285 String cacheKey = buildClientCacheKey(SunoApi.class, AiPlatformEnum.SUNO.getPlatform(), apiKey, url); 270 String cacheKey = buildClientCacheKey(SunoApi.class, AiPlatformEnum.SUNO.getPlatform(), apiKey, url);
286 return Singleton.get(cacheKey, (Func0<SunoApi>) () -> new SunoApi(url)); 271 return Singleton.get(cacheKey, (Func0<SunoApi>) () -> new SunoApi(url));
287 } 272 }
288 273
289 @Override 274 @Override
290 @SuppressWarnings("EnhancedSwitchMigration") 275 @SuppressWarnings("EnhancedSwitchMigration")
291 - public EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url, String model) { 276 + public EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String rawApiKey, String rawUrl, String model) {
  277 + final String apiKey = resolveSpringPlaceholders(rawApiKey);
  278 + final String url = resolveSpringPlaceholders(rawUrl);
292 String cacheKey = buildClientCacheKey(EmbeddingModel.class, platform, apiKey, url, model); 279 String cacheKey = buildClientCacheKey(EmbeddingModel.class, platform, apiKey, url, model);
293 return Singleton.get(cacheKey, (Func0<EmbeddingModel>) () -> { 280 return Singleton.get(cacheKey, (Func0<EmbeddingModel>) () -> {
294 switch (platform) { 281 switch (platform) {
295 case TONG_YI: 282 case TONG_YI:
296 return buildTongYiEmbeddingModel(apiKey, model); 283 return buildTongYiEmbeddingModel(apiKey, model);
297 - case YI_YAN:  
298 - return buildYiYanEmbeddingModel(apiKey, model);  
299 - case ZHI_PU:  
300 - return buildZhiPuEmbeddingModel(apiKey, url, model);  
301 - case MINI_MAX:  
302 - return buildMiniMaxEmbeddingModel(apiKey, url, model);  
303 case OPENAI: 284 case OPENAI:
304 return buildOpenAiEmbeddingModel(apiKey, url, model); 285 return buildOpenAiEmbeddingModel(apiKey, url, model);
305 case AZURE_OPENAI: 286 case AZURE_OPENAI:
@@ -341,6 +322,11 @@ public class AiModelFactoryImpl implements AiModelFactory { @@ -341,6 +322,11 @@ public class AiModelFactoryImpl implements AiModelFactory {
341 return StrUtil.format("{}#{}", clazz.getName(), ArrayUtil.join(params, "_")); 322 return StrUtil.format("{}#{}", clazz.getName(), ArrayUtil.join(params, "_"));
342 } 323 }
343 324
  325 + private static String resolveSpringPlaceholders(String value) {
  326 + // yml 配置的占位符由 Spring 自动解析;DB 里保存的 ${xxx} 需要在这里手动解析。
  327 + return AiUtils.resolveSpringPlaceholders(value);
  328 + }
  329 +
344 // ========== 各种创建 spring-ai 客户端的方法 ========== 330 // ========== 各种创建 spring-ai 客户端的方法 ==========
345 331
346 /** 332 /**
@@ -367,30 +353,10 @@ public class AiModelFactoryImpl implements AiModelFactory { @@ -367,30 +353,10 @@ public class AiModelFactoryImpl implements AiModelFactory {
367 .build(); 353 .build();
368 } 354 }
369 355
370 - /**  
371 - * 可参考 {@link QianFanChatAutoConfiguration} 的 qianFanChatModel 方法  
372 - */  
373 - private static QianFanChatModel buildYiYanChatModel(String key) {  
374 - // TODO spring ai qianfan 有 bug,无法使用 https://github.com/spring-ai-community/qianfan/issues/6  
375 - List<String> keys = StrUtil.split(key, '|');  
376 - Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式");  
377 - String appKey = keys.get(0);  
378 - String secretKey = keys.get(1);  
379 - QianFanApi qianFanApi = new QianFanApi(appKey, secretKey);  
380 - return new QianFanChatModel(qianFanApi);  
381 - }  
382 -  
383 - /**  
384 - * 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanImageModel 方法  
385 - */  
386 - private QianFanImageModel buildQianFanImageModel(String key) {  
387 - // TODO spring ai qianfan 有 bug,无法使用 https://github.com/spring-ai-community/qianfan/issues/6  
388 - List<String> keys = StrUtil.split(key, '|');  
389 - Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式");  
390 - String appKey = keys.get(0);  
391 - String secretKey = keys.get(1);  
392 - QianFanImageApi qianFanApi = new QianFanImageApi(appKey, secretKey);  
393 - return new QianFanImageModel(qianFanApi); 356 + private ChatModel buildYiYanChatModel(String apiKey) {
  357 + YudaoAiProperties.YiYan properties = new YudaoAiProperties.YiYan()
  358 + .setApiKey(apiKey);
  359 + return new AiAutoConfiguration().buildYiYanChatClient(properties);
394 } 360 }
395 361
396 /** 362 /**
@@ -435,62 +401,47 @@ public class AiModelFactoryImpl implements AiModelFactory { @@ -435,62 +401,47 @@ public class AiModelFactoryImpl implements AiModelFactory {
435 } 401 }
436 402
437 /** 403 /**
438 - * 可参考 {@link ZhiPuAiChatAutoConfiguration} 的 zhiPuAiChatModel 方法 404 + * 可参考 {@link AiAutoConfiguration#zhiPuChatClient(YudaoAiProperties)}
439 */ 405 */
440 - private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) {  
441 - ZhiPuAiApi.Builder zhiPuAiApiBuilder = ZhiPuAiApi.builder().apiKey(apiKey);  
442 - if (StrUtil.isNotEmpty(url)) {  
443 - zhiPuAiApiBuilder.baseUrl(url);  
444 - }  
445 - ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.DEFAULT_CHAT_MODEL).temperature(0.7).build();  
446 - return new ZhiPuAiChatModel(zhiPuAiApiBuilder.build(), options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE,  
447 - getObservationRegistry().getIfAvailable()); 406 + private ZhiPuChatModel buildZhiPuChatModel(String apiKey, String url) {
  407 + YudaoAiProperties.ZhiPu properties = new YudaoAiProperties.ZhiPu()
  408 + .setBaseUrl(url).setApiKey(apiKey);
  409 + return new AiAutoConfiguration().buildZhiPuChatClient(properties);
448 } 410 }
449 411
450 /** 412 /**
451 - * 可参考 {@link ZhiPuAiImageAutoConfiguration} 的 zhiPuAiImageModel 方法 413 + * 可参考 {@link AiAutoConfiguration#miniMaxChatClient(YudaoAiProperties)}
452 */ 414 */
453 - private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) {  
454 - ZhiPuAiImageApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiImageApi(apiKey)  
455 - : new ZhiPuAiImageApi(url, apiKey, RestClient.builder());  
456 - return new ZhiPuAiImageModel(zhiPuAiApi); 415 + private MiniMaxChatModel buildMiniMaxChatModel(String apiKey, String url) {
  416 + YudaoAiProperties.MiniMax properties = new YudaoAiProperties.MiniMax()
  417 + .setBaseUrl(url).setApiKey(apiKey);
  418 + return new AiAutoConfiguration().buildMiniMaxChatClient(properties);
457 } 419 }
458 420
459 /** 421 /**
460 - * 可参考 {@link MiniMaxChatAutoConfiguration} 的 miniMaxChatModel 方法 422 + * 可参考 {@link AiAutoConfiguration#moonshotChatClient(YudaoAiProperties)}
461 */ 423 */
462 - private MiniMaxChatModel buildMiniMaxChatModel(String apiKey, String url) {  
463 - MiniMaxApi miniMaxApi = StrUtil.isEmpty(url) ? new MiniMaxApi(apiKey)  
464 - : new MiniMaxApi(url, apiKey);  
465 - MiniMaxChatOptions options = MiniMaxChatOptions.builder().model(MiniMaxApi.DEFAULT_CHAT_MODEL).temperature(0.7).build();  
466 - return new MiniMaxChatModel(miniMaxApi, options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE); 424 + private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) {
  425 + YudaoAiProperties.Moonshot properties = new YudaoAiProperties.Moonshot()
  426 + .setBaseUrl(url).setApiKey(apiKey);
  427 + return new AiAutoConfiguration().buildMoonshotChatClient(properties);
467 } 428 }
468 429
469 /** 430 /**
470 - * 可参考 {@link MoonshotChatAutoConfiguration} 的 moonshotChatModel 方法 431 + * 可参考 {@link AiAutoConfiguration#stepFunChatClient(YudaoAiProperties)}
471 */ 432 */
472 - private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) {  
473 - MoonshotApi.Builder moonshotApiBuilder = MoonshotApi.builder()  
474 - .apiKey(apiKey);  
475 - if (StrUtil.isNotEmpty(url)) {  
476 - moonshotApiBuilder.baseUrl(url);  
477 - }  
478 - MoonshotChatOptions options = MoonshotChatOptions.builder().model(MoonshotApi.DEFAULT_CHAT_MODEL).build();  
479 - return MoonshotChatModel.builder()  
480 - .moonshotApi(moonshotApiBuilder.build())  
481 - .defaultOptions(options)  
482 - .toolCallingManager(getToolCallingManager())  
483 - .build(); 433 + private StepFunChatModel buildStepFunChatModel(String apiKey, String url) {
  434 + YudaoAiProperties.StepFun properties = new YudaoAiProperties.StepFun()
  435 + .setBaseUrl(url).setApiKey(apiKey);
  436 + return new AiAutoConfiguration().buildStepFunChatClient(properties);
484 } 437 }
485 438
486 /** 439 /**
487 * 可参考 {@link AiAutoConfiguration#xingHuoChatClient(YudaoAiProperties)} 440 * 可参考 {@link AiAutoConfiguration#xingHuoChatClient(YudaoAiProperties)}
488 */ 441 */
489 - private static XingHuoChatModel buildXingHuoChatModel(String key) {  
490 - List<String> keys = StrUtil.split(key, '|');  
491 - Assert.equals(keys.size(), 2, "XingHuoChatClient 的密钥需要 (appKey|secretKey) 格式"); 442 + private static XingHuoChatModel buildXingHuoChatModel(String apiKey) {
492 YudaoAiProperties.XingHuo properties = new YudaoAiProperties.XingHuo() 443 YudaoAiProperties.XingHuo properties = new YudaoAiProperties.XingHuo()
493 - .setAppKey(keys.get(0)).setSecretKey(keys.get(1)); 444 + .setApiKey(apiKey).setModel(XingHuoChatModel.MODEL_DEFAULT);
494 return new AiAutoConfiguration().buildXingHuoChatClient(properties); 445 return new AiAutoConfiguration().buildXingHuoChatClient(properties);
495 } 446 }
496 447
@@ -590,7 +541,7 @@ public class AiModelFactoryImpl implements AiModelFactory { @@ -590,7 +541,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
590 return new StabilityAiImageModel(stabilityAiApi); 541 return new StabilityAiImageModel(stabilityAiApi);
591 } 542 }
592 543
593 - private ChatModel buildGrokChatModel(String apiKey,String url) { 544 + private GrokChatModel buildGrokChatModel(String apiKey, String url) {
594 YudaoAiProperties.Grok properties = new YudaoAiProperties.Grok() 545 YudaoAiProperties.Grok properties = new YudaoAiProperties.Grok()
595 .setBaseUrl(url) 546 .setBaseUrl(url)
596 .setApiKey(apiKey); 547 .setApiKey(apiKey);
@@ -608,41 +559,6 @@ public class AiModelFactoryImpl implements AiModelFactory { @@ -608,41 +559,6 @@ public class AiModelFactoryImpl implements AiModelFactory {
608 return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, dashScopeEmbeddingOptions); 559 return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, dashScopeEmbeddingOptions);
609 } 560 }
610 561
611 - /**  
612 - * 可参考 {@link ZhiPuAiEmbeddingAutoConfiguration} 的 zhiPuAiEmbeddingModel 方法  
613 - */  
614 - private ZhiPuAiEmbeddingModel buildZhiPuEmbeddingModel(String apiKey, String url, String model) {  
615 - ZhiPuAiApi.Builder zhiPuAiApiBuilder = ZhiPuAiApi.builder().apiKey(apiKey);  
616 - if (StrUtil.isNotEmpty(url)) {  
617 - zhiPuAiApiBuilder.baseUrl(url);  
618 - }  
619 - ZhiPuAiEmbeddingOptions zhiPuAiEmbeddingOptions = ZhiPuAiEmbeddingOptions.builder().model(model).build();  
620 - return new ZhiPuAiEmbeddingModel(zhiPuAiApiBuilder.build(), MetadataMode.EMBED, zhiPuAiEmbeddingOptions);  
621 - }  
622 -  
623 - /**  
624 - * 可参考 {@link MiniMaxEmbeddingAutoConfiguration} 的 miniMaxEmbeddingModel 方法  
625 - */  
626 - private EmbeddingModel buildMiniMaxEmbeddingModel(String apiKey, String url, String model) {  
627 - MiniMaxApi miniMaxApi = StrUtil.isEmpty(url)? new MiniMaxApi(apiKey)  
628 - : new MiniMaxApi(url, apiKey);  
629 - MiniMaxEmbeddingOptions miniMaxEmbeddingOptions = MiniMaxEmbeddingOptions.builder().model(model).build();  
630 - return new MiniMaxEmbeddingModel(miniMaxApi, MetadataMode.EMBED, miniMaxEmbeddingOptions);  
631 - }  
632 -  
633 - /**  
634 - * 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanEmbeddingModel 方法  
635 - */  
636 - private QianFanEmbeddingModel buildYiYanEmbeddingModel(String key, String model) {  
637 - List<String> keys = StrUtil.split(key, '|');  
638 - Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式");  
639 - String appKey = keys.get(0);  
640 - String secretKey = keys.get(1);  
641 - QianFanApi qianFanApi = new QianFanApi(appKey, secretKey);  
642 - QianFanEmbeddingOptions qianFanEmbeddingOptions = QianFanEmbeddingOptions.builder().model(model).build();  
643 - return new QianFanEmbeddingModel(qianFanApi, MetadataMode.EMBED, qianFanEmbeddingOptions);  
644 - }  
645 -  
646 private OllamaEmbeddingModel buildOllamaEmbeddingModel(String url, String model) { 562 private OllamaEmbeddingModel buildOllamaEmbeddingModel(String url, String model) {
647 OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build(); 563 OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build();
648 OllamaEmbeddingOptions ollamaOptions = OllamaEmbeddingOptions.builder().model(model).build(); 564 OllamaEmbeddingOptions ollamaOptions = OllamaEmbeddingOptions.builder().model(model).build();
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; @@ -6,7 +6,7 @@ import org.springframework.ai.chat.model.ChatModel;
6 import org.springframework.ai.chat.model.ChatResponse; 6 import org.springframework.ai.chat.model.ChatResponse;
7 import org.springframework.ai.chat.prompt.ChatOptions; 7 import org.springframework.ai.chat.prompt.ChatOptions;
8 import org.springframework.ai.chat.prompt.Prompt; 8 import org.springframework.ai.chat.prompt.Prompt;
9 -import org.springframework.ai.openai.OpenAiChatModel; 9 +import org.springframework.ai.deepseek.DeepSeekChatModel;
10 import reactor.core.publisher.Flux; 10 import reactor.core.publisher.Flux;
11 11
12 /** 12 /**
@@ -20,26 +20,28 @@ public class BaiChuanChatModel implements ChatModel { @@ -20,26 +20,28 @@ public class BaiChuanChatModel implements ChatModel {
20 20
21 public static final String BASE_URL = "https://api.baichuan-ai.com"; 21 public static final String BASE_URL = "https://api.baichuan-ai.com";
22 22
23 - public static final String MODEL_DEFAULT = "Baichuan4-Turbo"; 23 + public static final String COMPLETE_PATH = "/v1/chat/completions";
  24 +
  25 + public static final String MODEL_DEFAULT = "Baichuan-M3";
24 26
25 /** 27 /**
26 - * 兼容 OpenAI 接口,进行复用 28 + * 兼容 OpenAI 接口,复用 DeepSeek 客户端
27 */ 29 */
28 - private final OpenAiChatModel openAiChatModel; 30 + private final DeepSeekChatModel deepSeekChatModel;
29 31
30 @Override 32 @Override
31 public ChatResponse call(Prompt prompt) { 33 public ChatResponse call(Prompt prompt) {
32 - return openAiChatModel.call(prompt); 34 + return deepSeekChatModel.call(prompt);
33 } 35 }
34 36
35 @Override 37 @Override
36 public Flux<ChatResponse> stream(Prompt prompt) { 38 public Flux<ChatResponse> stream(Prompt prompt) {
37 - return openAiChatModel.stream(prompt); 39 + return deepSeekChatModel.stream(prompt);
38 } 40 }
39 41
40 @Override 42 @Override
41 public ChatOptions getDefaultOptions() { 43 public ChatOptions getDefaultOptions() {
42 - return openAiChatModel.getDefaultOptions(); 44 + return deepSeekChatModel.getDefaultOptions();
43 } 45 }
44 46
45 } 47 }
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 { @@ -20,7 +20,7 @@ public class DouBaoChatModel implements ChatModel {
20 public static final String BASE_URL = "https://ark.cn-beijing.volces.com/api"; 20 public static final String BASE_URL = "https://ark.cn-beijing.volces.com/api";
21 public static final String COMPLETE_PATH = "/v3/chat/completions"; 21 public static final String COMPLETE_PATH = "/v3/chat/completions";
22 22
23 - public static final String MODEL_DEFAULT = "doubao-1-5-lite-32k-250115"; 23 + public static final String MODEL_DEFAULT = "doubao-seed-2-1-turbo-260628";
24 24
25 /** 25 /**
26 * 兼容 OpenAI 接口,进行复用 26 * 兼容 OpenAI 接口,进行复用
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 { @@ -19,7 +19,7 @@ public class GrokChatModel implements ChatModel {
19 19
20 public static final String BASE_URL = "https://api.x.ai"; 20 public static final String BASE_URL = "https://api.x.ai";
21 public static final String COMPLETE_PATH = "/v1/chat/completions"; 21 public static final String COMPLETE_PATH = "/v1/chat/completions";
22 - public static final String MODEL_DEFAULT = "grok-4-fast-reasoning"; 22 + public static final String MODEL_DEFAULT = "grok-4.3";
23 23
24 /** 24 /**
25 * 兼容 OpenAI 接口,进行复用 25 * 兼容 OpenAI 接口,进行复用
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; @@ -9,10 +9,9 @@ import org.springframework.ai.chat.prompt.Prompt;
9 import reactor.core.publisher.Flux; 9 import reactor.core.publisher.Flux;
10 10
11 /** 11 /**
12 - * 腾云混元 {@link ChatModel} 实现类 12 + * 腾讯混元 {@link ChatModel} 实现类
13 * 13 *
14 - * 1. 混元大模型:基于 <a href="https://cloud.tencent.com/document/product/1729/111007">知识引擎原子能力</a> 实现  
15 - * 2. 知识引擎原子能力:基于 <a href="https://cloud.tencent.com/document/product/1772/115969">知识引擎原子能力</a> 实现 14 + * 基于 <a href="https://cloud.tencent.com/document/product/1823/132252">TokenHub OpenAI 兼容接口</a> 实现
16 * 15 *
17 * @author fansili 16 * @author fansili
18 */ 17 */
@@ -20,14 +19,10 @@ import reactor.core.publisher.Flux; @@ -20,14 +19,10 @@ import reactor.core.publisher.Flux;
20 @RequiredArgsConstructor 19 @RequiredArgsConstructor
21 public class HunYuanChatModel implements ChatModel { 20 public class HunYuanChatModel implements ChatModel {
22 21
23 - public static final String BASE_URL = "https://api.hunyuan.cloud.tencent.com"; 22 + public static final String BASE_URL = "https://tokenhub.tencentmaas.com";
24 public static final String COMPLETE_PATH = "/v1/chat/completions"; 23 public static final String COMPLETE_PATH = "/v1/chat/completions";
25 24
26 - public static final String MODEL_DEFAULT = "hunyuan-turbo";  
27 -  
28 - public static final String DEEP_SEEK_BASE_URL = "https://api.lkeap.cloud.tencent.com";  
29 -  
30 - public static final String DEEP_SEEK_MODEL_DEFAULT = "deepseek-v3"; 25 + public static final String MODEL_DEFAULT = "hy3-preview";
31 26
32 /** 27 /**
33 * 兼容 OpenAI 接口,进行复用 28 * 兼容 OpenAI 接口,进行复用
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/minimax/MiniMaxChatModel.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax;
  2 +
  3 +import lombok.RequiredArgsConstructor;
  4 +import lombok.extern.slf4j.Slf4j;
  5 +import org.springframework.ai.chat.model.ChatModel;
  6 +import org.springframework.ai.chat.model.ChatResponse;
  7 +import org.springframework.ai.chat.prompt.ChatOptions;
  8 +import org.springframework.ai.chat.prompt.Prompt;
  9 +import org.springframework.ai.deepseek.DeepSeekChatModel;
  10 +import reactor.core.publisher.Flux;
  11 +
  12 +/**
  13 + * MiniMax {@link ChatModel} 实现类
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Slf4j
  18 +@RequiredArgsConstructor
  19 +public class MiniMaxChatModel implements ChatModel {
  20 +
  21 + public static final String BASE_URL = "https://api.minimaxi.com/v1";
  22 +
  23 + public static final String MODEL_DEFAULT = "MiniMax-M3";
  24 +
  25 + /**
  26 + * 兼容 OpenAI 接口,复用 DeepSeek 客户端
  27 + */
  28 + private final DeepSeekChatModel deepSeekChatModel;
  29 +
  30 + @Override
  31 + public ChatResponse call(Prompt prompt) {
  32 + return deepSeekChatModel.call(prompt);
  33 + }
  34 +
  35 + @Override
  36 + public Flux<ChatResponse> stream(Prompt prompt) {
  37 + return deepSeekChatModel.stream(prompt);
  38 + }
  39 +
  40 + @Override
  41 + public ChatOptions getDefaultOptions() {
  42 + return deepSeekChatModel.getDefaultOptions();
  43 + }
  44 +
  45 +}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/moonshot/MoonshotChatModel.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot;
  2 +
  3 +import lombok.RequiredArgsConstructor;
  4 +import lombok.extern.slf4j.Slf4j;
  5 +import org.springframework.ai.chat.model.ChatModel;
  6 +import org.springframework.ai.chat.model.ChatResponse;
  7 +import org.springframework.ai.chat.prompt.ChatOptions;
  8 +import org.springframework.ai.chat.prompt.Prompt;
  9 +import org.springframework.ai.deepseek.DeepSeekChatModel;
  10 +import reactor.core.publisher.Flux;
  11 +
  12 +/**
  13 + * 月之暗面 {@link ChatModel} 实现类
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Slf4j
  18 +@RequiredArgsConstructor
  19 +public class MoonshotChatModel implements ChatModel {
  20 +
  21 + public static final String BASE_URL = "https://api.moonshot.cn";
  22 +
  23 + public static final String COMPLETE_PATH = "/v1/chat/completions";
  24 +
  25 + public static final String MODEL_DEFAULT = "kimi-k2.6";
  26 +
  27 + /**
  28 + * 兼容 OpenAI 接口,复用 DeepSeek 客户端
  29 + */
  30 + private final DeepSeekChatModel deepSeekChatModel;
  31 +
  32 + @Override
  33 + public ChatResponse call(Prompt prompt) {
  34 + return deepSeekChatModel.call(prompt);
  35 + }
  36 +
  37 + @Override
  38 + public Flux<ChatResponse> stream(Prompt prompt) {
  39 + return deepSeekChatModel.stream(prompt);
  40 + }
  41 +
  42 + @Override
  43 + public ChatOptions getDefaultOptions() {
  44 + return deepSeekChatModel.getDefaultOptions();
  45 + }
  46 +
  47 +}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/stepfun/StepFunChatModel.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun;
  2 +
  3 +import lombok.RequiredArgsConstructor;
  4 +import lombok.extern.slf4j.Slf4j;
  5 +import org.springframework.ai.chat.model.ChatModel;
  6 +import org.springframework.ai.chat.model.ChatResponse;
  7 +import org.springframework.ai.chat.prompt.ChatOptions;
  8 +import org.springframework.ai.chat.prompt.Prompt;
  9 +import org.springframework.ai.deepseek.DeepSeekChatModel;
  10 +import reactor.core.publisher.Flux;
  11 +
  12 +/**
  13 + * 阶跃星辰 {@link ChatModel} 实现类
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Slf4j
  18 +@RequiredArgsConstructor
  19 +public class StepFunChatModel implements ChatModel {
  20 +
  21 + public static final String BASE_URL = "https://api.stepfun.com";
  22 +
  23 + public static final String COMPLETE_PATH = "/v1/chat/completions";
  24 +
  25 + public static final String MODEL_DEFAULT = "step-3.7-flash";
  26 +
  27 + /**
  28 + * 兼容 OpenAI 接口,复用 DeepSeek 客户端
  29 + */
  30 + private final DeepSeekChatModel deepSeekChatModel;
  31 +
  32 + @Override
  33 + public ChatResponse call(Prompt prompt) {
  34 + return deepSeekChatModel.call(prompt);
  35 + }
  36 +
  37 + @Override
  38 + public Flux<ChatResponse> stream(Prompt prompt) {
  39 + return deepSeekChatModel.stream(prompt);
  40 + }
  41 +
  42 + @Override
  43 + public ChatOptions getDefaultOptions() {
  44 + return deepSeekChatModel.getDefaultOptions();
  45 + }
  46 +
  47 +}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/xinghuo/XingHuoChatModel.java
1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo; 1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo;
2 2
3 -import lombok.RequiredArgsConstructor; 3 +import cn.hutool.core.util.StrUtil;
4 import lombok.extern.slf4j.Slf4j; 4 import lombok.extern.slf4j.Slf4j;
5 import org.springframework.ai.chat.model.ChatModel; 5 import org.springframework.ai.chat.model.ChatModel;
6 import org.springframework.ai.chat.model.ChatResponse; 6 import org.springframework.ai.chat.model.ChatResponse;
7 import org.springframework.ai.chat.prompt.ChatOptions; 7 import org.springframework.ai.chat.prompt.ChatOptions;
8 import org.springframework.ai.chat.prompt.Prompt; 8 import org.springframework.ai.chat.prompt.Prompt;
  9 +import org.springframework.ai.deepseek.DeepSeekChatModel;
  10 +import org.springframework.ai.deepseek.DeepSeekChatOptions;
  11 +import org.springframework.ai.deepseek.api.DeepSeekApi;
9 import reactor.core.publisher.Flux; 12 import reactor.core.publisher.Flux;
10 13
11 /** 14 /**
@@ -14,37 +17,140 @@ import reactor.core.publisher.Flux; @@ -14,37 +17,140 @@ import reactor.core.publisher.Flux;
14 * @author fansili 17 * @author fansili
15 */ 18 */
16 @Slf4j 19 @Slf4j
17 -@RequiredArgsConstructor  
18 public class XingHuoChatModel implements ChatModel { 20 public class XingHuoChatModel implements ChatModel {
19 21
20 - public static final String BASE_URL_V1 = "https://spark-api-open.xf-yun.com";  
21 -  
22 - public static final String BASE_URL_V2 = "https://spark-api-open.xf-yun.com";  
23 - public static final String BASE_COMPLETIONS_PATH_V2 = "/v2/chat/completions"; 22 + /**
  23 + * 星火 X2
  24 + *
  25 + * @see <a href="https://spark-api-open.xf-yun.com/x2/chat/completions">接口地址</a>
  26 + */
  27 + public static final String MODEL_X2 = "x2";
24 28
25 /** 29 /**
26 - * 已知模型名列表:x1、4.0Ultra、generalv3.5、max-32k、generalv3、pro-128k、lite 30 + * 星火 X2 Flash
  31 + *
  32 + * @see <a href="https://spark-api-open.xf-yun.com/agent/v1/chat/completions">接口地址</a>
27 */ 33 */
28 - public static final String MODEL_DEFAULT = "4.0Ultra"; 34 + public static final String MODEL_X2_FLASH = "x2-flash";
  35 +
  36 + private static final String BASE_URL_X2 = "https://spark-api-open.xf-yun.com/x2";
  37 +
  38 + private static final String BASE_URL_X2_FLASH = "https://spark-api-open.xf-yun.com/agent/v1";
  39 +
  40 + public static final String MODEL_DEFAULT = MODEL_X2_FLASH;
  41 +
  42 + private static String getBaseUrl(String model) {
  43 + if (MODEL_X2_FLASH.equals(model)) {
  44 + return BASE_URL_X2_FLASH;
  45 + }
  46 + return BASE_URL_X2;
  47 + }
  48 +
  49 + private final String apiKey;
  50 +
  51 + private final DeepSeekChatOptions options;
29 52
30 /** 53 /**
31 - * v1 兼容 OpenAI 接口,进行复用 54 + * 兼容 OpenAI 接口,进行复用
32 */ 55 */
33 - private final ChatModel openAiChatModelV1; 56 + private final ChatModel chatModelX2;
  57 +
  58 + private final ChatModel chatModelX2Flash;
  59 +
  60 + private XingHuoChatModel(String apiKey, DeepSeekChatOptions options) {
  61 + this.apiKey = apiKey;
  62 + this.options = options;
  63 + this.chatModelX2 = buildChatModel(MODEL_X2);
  64 + this.chatModelX2Flash = buildChatModel(MODEL_X2_FLASH);
  65 + }
  66 +
  67 + public static Builder builder() {
  68 + return new Builder();
  69 + }
34 70
35 @Override 71 @Override
36 public ChatResponse call(Prompt prompt) { 72 public ChatResponse call(Prompt prompt) {
37 - return openAiChatModelV1.call(prompt); 73 + return getChatModel(prompt).call(buildApiPrompt(prompt));
38 } 74 }
39 75
40 @Override 76 @Override
41 public Flux<ChatResponse> stream(Prompt prompt) { 77 public Flux<ChatResponse> stream(Prompt prompt) {
42 - return openAiChatModelV1.stream(prompt); 78 + return getChatModel(prompt).stream(buildApiPrompt(prompt));
43 } 79 }
44 80
45 @Override 81 @Override
46 public ChatOptions getDefaultOptions() { 82 public ChatOptions getDefaultOptions() {
47 - return openAiChatModelV1.getDefaultOptions(); 83 + return options;
  84 + }
  85 +
  86 + private ChatModel getChatModel(Prompt prompt) {
  87 + String model = options.getModel();
  88 + ChatOptions options = prompt.getOptions();
  89 + if (options != null && isBusinessModel(options.getModel())) {
  90 + model = options.getModel();
  91 + }
  92 + return getChatModel(model);
  93 + }
  94 +
  95 + private ChatModel getChatModel(String model) {
  96 + if (MODEL_X2_FLASH.equals(model)) {
  97 + return chatModelX2Flash;
  98 + }
  99 + return chatModelX2;
  100 + }
  101 +
  102 + private ChatModel buildChatModel(String model) {
  103 + return DeepSeekChatModel.builder()
  104 + .deepSeekApi(DeepSeekApi.builder()
  105 + .baseUrl(getBaseUrl(model))
  106 + .apiKey(apiKey)
  107 + .build())
  108 + .defaultOptions(new DeepSeekChatOptions.Builder(options).model("spark-x").build())
  109 + .build();
  110 + }
  111 +
  112 + private static Prompt buildApiPrompt(Prompt prompt) {
  113 + ChatOptions options = prompt.getOptions();
  114 + if (options == null) {
  115 + return prompt;
  116 + }
  117 + if (!(options instanceof DeepSeekChatOptions)) {
  118 + return prompt;
  119 + }
  120 + return Prompt.builder()
  121 + .messages(prompt.getInstructions())
  122 + .chatOptions(new DeepSeekChatOptions.Builder((DeepSeekChatOptions) options).model("spark-x").build())
  123 + .build();
  124 + }
  125 +
  126 + private static boolean isBusinessModel(String model) {
  127 + return MODEL_X2.equals(model) || MODEL_X2_FLASH.equals(model);
  128 + }
  129 +
  130 + public static final class Builder {
  131 +
  132 + private String apiKey;
  133 +
  134 + private DeepSeekChatOptions options;
  135 +
  136 + public Builder apiKey(String apiKey) {
  137 + this.apiKey = apiKey;
  138 + return this;
  139 + }
  140 +
  141 + public Builder options(DeepSeekChatOptions options) {
  142 + this.options = options;
  143 + return this;
  144 + }
  145 +
  146 + public XingHuoChatModel build() {
  147 + DeepSeekChatOptions options = this.options != null ? this.options : DeepSeekChatOptions.builder().build();
  148 + if (StrUtil.isEmpty(options.getModel())) {
  149 + options = new DeepSeekChatOptions.Builder(options).model(MODEL_DEFAULT).build();
  150 + }
  151 + return new XingHuoChatModel(apiKey, options);
  152 + }
  153 +
48 } 154 }
49 155
50 } 156 }
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/yiyan/YiYanChatModel.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan;
  2 +
  3 +import lombok.RequiredArgsConstructor;
  4 +import lombok.extern.slf4j.Slf4j;
  5 +import org.springframework.ai.chat.model.ChatModel;
  6 +import org.springframework.ai.chat.model.ChatResponse;
  7 +import org.springframework.ai.chat.prompt.ChatOptions;
  8 +import org.springframework.ai.chat.prompt.Prompt;
  9 +import org.springframework.ai.deepseek.DeepSeekChatModel;
  10 +import reactor.core.publisher.Flux;
  11 +
  12 +/**
  13 + * 文心一言 {@link ChatModel} 实现类
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Slf4j
  18 +@RequiredArgsConstructor
  19 +public class YiYanChatModel implements ChatModel {
  20 +
  21 + public static final String BASE_URL = "https://qianfan.baidubce.com/v2";
  22 +
  23 + public static final String MODEL_DEFAULT = "ernie-5.1";
  24 +
  25 + /**
  26 + * 兼容 OpenAI 接口,复用 DeepSeek 客户端
  27 + */
  28 + private final DeepSeekChatModel deepSeekChatModel;
  29 +
  30 + @Override
  31 + public ChatResponse call(Prompt prompt) {
  32 + return deepSeekChatModel.call(prompt);
  33 + }
  34 +
  35 + @Override
  36 + public Flux<ChatResponse> stream(Prompt prompt) {
  37 + return deepSeekChatModel.stream(prompt);
  38 + }
  39 +
  40 + @Override
  41 + public ChatOptions getDefaultOptions() {
  42 + return deepSeekChatModel.getDefaultOptions();
  43 + }
  44 +
  45 +}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/zhipu/ZhiPuChatModel.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu;
  2 +
  3 +import lombok.RequiredArgsConstructor;
  4 +import lombok.extern.slf4j.Slf4j;
  5 +import org.springframework.ai.chat.model.ChatModel;
  6 +import org.springframework.ai.chat.model.ChatResponse;
  7 +import org.springframework.ai.chat.prompt.ChatOptions;
  8 +import org.springframework.ai.chat.prompt.Prompt;
  9 +import org.springframework.ai.deepseek.DeepSeekChatModel;
  10 +import reactor.core.publisher.Flux;
  11 +
  12 +/**
  13 + * 智谱 {@link ChatModel} 实现类
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Slf4j
  18 +@RequiredArgsConstructor
  19 +public class ZhiPuChatModel implements ChatModel {
  20 +
  21 + public static final String BASE_URL = "https://open.bigmodel.cn/api/paas/v4";
  22 +
  23 + public static final String MODEL_DEFAULT = "glm-5.2";
  24 +
  25 + /**
  26 + * 兼容 OpenAI 接口,复用 DeepSeek 客户端
  27 + */
  28 + private final DeepSeekChatModel deepSeekChatModel;
  29 +
  30 + @Override
  31 + public ChatResponse call(Prompt prompt) {
  32 + return deepSeekChatModel.call(prompt);
  33 + }
  34 +
  35 + @Override
  36 + public Flux<ChatResponse> stream(Prompt prompt) {
  37 + return deepSeekChatModel.stream(prompt);
  38 + }
  39 +
  40 + @Override
  41 + public ChatOptions getDefaultOptions() {
  42 + return deepSeekChatModel.getDefaultOptions();
  43 + }
  44 +
  45 +}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageService.java
@@ -46,6 +46,14 @@ public interface AiChatMessageService { @@ -46,6 +46,14 @@ public interface AiChatMessageService {
46 List<AiChatMessageDO> getChatMessageListByConversationId(Long conversationId); 46 List<AiChatMessageDO> getChatMessageListByConversationId(Long conversationId);
47 47
48 /** 48 /**
  49 + * 获得聊天消息
  50 + *
  51 + * @param id 消息编号
  52 + * @return 聊天消息
  53 + */
  54 + AiChatMessageDO getChatMessage(Long id);
  55 +
  56 + /**
49 * 删除消息 57 * 删除消息
50 * 58 *
51 * @param id 消息编号 59 * @param id 消息编号
@@ -69,6 +77,14 @@ public interface AiChatMessageService { @@ -69,6 +77,14 @@ public interface AiChatMessageService {
69 void deleteChatMessageByAdmin(Long id); 77 void deleteChatMessageByAdmin(Long id);
70 78
71 /** 79 /**
  80 + * 获得聊天对话的消息数量
  81 + *
  82 + * @param conversationId 对话编号
  83 + * @return 消息数量
  84 + */
  85 + Integer getChatMessageCount(Long conversationId);
  86 +
  87 + /**
72 * 获得聊天对话的消息数量 Map 88 * 获得聊天对话的消息数量 Map
73 * 89 *
74 * @param conversationIds 对话编号数组 90 * @param conversationIds 对话编号数组
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java
@@ -525,6 +525,11 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { @@ -525,6 +525,11 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
525 } 525 }
526 526
527 @Override 527 @Override
  528 + public AiChatMessageDO getChatMessage(Long id) {
  529 + return chatMessageMapper.selectById(id);
  530 + }
  531 +
  532 + @Override
528 public void deleteChatMessage(Long id, Long userId) { 533 public void deleteChatMessage(Long id, Long userId) {
529 // 1. 校验消息存在 534 // 1. 校验消息存在
530 AiChatMessageDO message = chatMessageMapper.selectById(id); 535 AiChatMessageDO message = chatMessageMapper.selectById(id);
@@ -558,6 +563,11 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { @@ -558,6 +563,11 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
558 } 563 }
559 564
560 @Override 565 @Override
  566 + public Integer getChatMessageCount(Long conversationId) {
  567 + return chatMessageMapper.selectCount(AiChatMessageDO::getConversationId, conversationId).intValue();
  568 + }
  569 +
  570 + @Override
561 public Map<Long, Integer> getChatMessageCountMap(Collection<Long> conversationIds) { 571 public Map<Long, Integer> getChatMessageCountMap(Collection<Long> conversationIds) {
562 return chatMessageMapper.selectCountMapByConversationId(conversationIds); 572 return chatMessageMapper.selectCountMapByConversationId(conversationIds);
563 } 573 }
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; @@ -29,14 +29,12 @@ import cn.iocoder.yudao.module.infra.api.file.FileApi;
29 import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; 29 import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions;
30 import jakarta.annotation.Resource; 30 import jakarta.annotation.Resource;
31 import lombok.extern.slf4j.Slf4j; 31 import lombok.extern.slf4j.Slf4j;
32 -import org.springaicommunity.qianfan.QianFanImageOptions;  
33 import org.springframework.ai.image.ImageModel; 32 import org.springframework.ai.image.ImageModel;
34 import org.springframework.ai.image.ImageOptions; 33 import org.springframework.ai.image.ImageOptions;
35 import org.springframework.ai.image.ImagePrompt; 34 import org.springframework.ai.image.ImagePrompt;
36 import org.springframework.ai.image.ImageResponse; 35 import org.springframework.ai.image.ImageResponse;
37 import org.springframework.ai.openai.OpenAiImageOptions; 36 import org.springframework.ai.openai.OpenAiImageOptions;
38 import org.springframework.ai.stabilityai.api.StabilityAiImageOptions; 37 import org.springframework.ai.stabilityai.api.StabilityAiImageOptions;
39 -import org.springframework.ai.zhipuai.ZhiPuAiImageOptions;  
40 import org.springframework.scheduling.annotation.Async; 38 import org.springframework.scheduling.annotation.Async;
41 import org.springframework.stereotype.Service; 39 import org.springframework.stereotype.Service;
42 import org.springframework.transaction.annotation.Transactional; 40 import org.springframework.transaction.annotation.Transactional;
@@ -168,15 +166,6 @@ public class AiImageServiceImpl implements AiImageService { @@ -168,15 +166,6 @@ public class AiImageServiceImpl implements AiImageService {
168 .model(model.getModel()).n(1) 166 .model(model.getModel()).n(1)
169 .height(draw.getHeight()).width(draw.getWidth()) 167 .height(draw.getHeight()).width(draw.getWidth())
170 .build(); 168 .build();
171 - } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.YI_YAN.getPlatform())) {  
172 - return QianFanImageOptions.builder()  
173 - .model(model.getModel()).N(1)  
174 - .height(draw.getHeight()).width(draw.getWidth())  
175 - .build();  
176 - } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.ZHI_PU.getPlatform())) {  
177 - return ZhiPuAiImageOptions.builder()  
178 - .model(model.getModel())  
179 - .build();  
180 } 169 }
181 throw new IllegalArgumentException("不支持的 AI 平台:" + model.getPlatform()); 170 throw new IllegalArgumentException("不支持的 AI 平台:" + model.getPlatform());
182 } 171 }
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapService.java
@@ -31,6 +31,14 @@ public interface AiMindMapService { @@ -31,6 +31,14 @@ public interface AiMindMapService {
31 void deleteMindMap(Long id); 31 void deleteMindMap(Long id);
32 32
33 /** 33 /**
  34 + * 获得思维导图
  35 + *
  36 + * @param id 编号
  37 + * @return 思维导图
  38 + */
  39 + AiMindMapDO getMindMap(Long id);
  40 +
  41 + /**
34 * 获得思维导图分页 42 * 获得思维导图分页
35 * 43 *
36 * @param pageReqVO 分页查询 44 * @param pageReqVO 分页查询
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java
@@ -154,6 +154,11 @@ public class AiMindMapServiceImpl implements AiMindMapService { @@ -154,6 +154,11 @@ public class AiMindMapServiceImpl implements AiMindMapService {
154 } 154 }
155 155
156 @Override 156 @Override
  157 + public AiMindMapDO getMindMap(Long id) {
  158 + return mindMapMapper.selectById(id);
  159 + }
  160 +
  161 + @Override
157 public PageResult<AiMindMapDO> getMindMapPage(AiMindMapPageReqVO pageReqVO) { 162 public PageResult<AiMindMapDO> getMindMapPage(AiMindMapPageReqVO pageReqVO) {
158 return mindMapMapper.selectPage(pageReqVO); 163 return mindMapMapper.selectPage(pageReqVO);
159 } 164 }
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 @@ -10,8 +10,8 @@ import cn.iocoder.yudao.module.ai.controller.admin.workflow.vo.AiWorkflowTestReq
10 import cn.iocoder.yudao.module.ai.dal.dataobject.workflow.AiWorkflowDO; 10 import cn.iocoder.yudao.module.ai.dal.dataobject.workflow.AiWorkflowDO;
11 import cn.iocoder.yudao.module.ai.dal.mysql.workflow.AiWorkflowMapper; 11 import cn.iocoder.yudao.module.ai.dal.mysql.workflow.AiWorkflowMapper;
12 import cn.iocoder.yudao.module.ai.service.model.AiModelService; 12 import cn.iocoder.yudao.module.ai.service.model.AiModelService;
13 -import com.alibaba.fastjson.JSONArray;  
14 -import com.alibaba.fastjson.JSONObject; 13 +import com.alibaba.fastjson2.JSONArray;
  14 +import com.alibaba.fastjson2.JSONObject;
15 import dev.tinyflow.core.Tinyflow; 15 import dev.tinyflow.core.Tinyflow;
16 import jakarta.annotation.Resource; 16 import jakarta.annotation.Resource;
17 import lombok.extern.slf4j.Slf4j; 17 import lombok.extern.slf4j.Slf4j;
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteService.java
@@ -31,6 +31,14 @@ public interface AiWriteService { @@ -31,6 +31,14 @@ public interface AiWriteService {
31 void deleteWrite(Long id); 31 void deleteWrite(Long id);
32 32
33 /** 33 /**
  34 + * 获得写作
  35 + *
  36 + * @param id 编号
  37 + * @return AI 写作
  38 + */
  39 + AiWriteDO getWrite(Long id);
  40 +
  41 + /**
34 * 获得写作分页 42 * 获得写作分页
35 * 43 *
36 * @param pageReqVO 分页查询 44 * @param pageReqVO 分页查询
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java
@@ -171,6 +171,11 @@ public class AiWriteServiceImpl implements AiWriteService { @@ -171,6 +171,11 @@ public class AiWriteServiceImpl implements AiWriteService {
171 } 171 }
172 172
173 @Override 173 @Override
  174 + public AiWriteDO getWrite(Long id) {
  175 + return writeMapper.selectById(id);
  176 + }
  177 +
  178 + @Override
174 public PageResult<AiWriteDO> getWritePage(AiWritePageReqVO pageReqVO) { 179 public PageResult<AiWriteDO> getWritePage(AiWritePageReqVO pageReqVO) {
175 return writeMapper.selectPage(pageReqVO); 180 return writeMapper.selectPage(pageReqVO);
176 } 181 }
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; @@ -8,8 +8,7 @@ import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
8 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; 8 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
9 import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; 9 import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
10 import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; 10 import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
11 -import org.springaicommunity.moonshot.MoonshotChatOptions;  
12 -import org.springaicommunity.qianfan.QianFanChatOptions; 11 +import cn.hutool.extra.spring.SpringUtil;
13 import org.springframework.ai.anthropic.AnthropicChatOptions; 12 import org.springframework.ai.anthropic.AnthropicChatOptions;
14 import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; 13 import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
15 import org.springframework.ai.chat.messages.*; 14 import org.springframework.ai.chat.messages.*;
@@ -17,14 +16,16 @@ import org.springframework.ai.chat.model.ChatResponse; @@ -17,14 +16,16 @@ import org.springframework.ai.chat.model.ChatResponse;
17 import org.springframework.ai.chat.prompt.ChatOptions; 16 import org.springframework.ai.chat.prompt.ChatOptions;
18 import org.springframework.ai.deepseek.DeepSeekAssistantMessage; 17 import org.springframework.ai.deepseek.DeepSeekAssistantMessage;
19 import org.springframework.ai.deepseek.DeepSeekChatOptions; 18 import org.springframework.ai.deepseek.DeepSeekChatOptions;
20 -import org.springframework.ai.minimax.MiniMaxChatOptions;  
21 import org.springframework.ai.ollama.api.OllamaChatOptions; 19 import org.springframework.ai.ollama.api.OllamaChatOptions;
22 import org.springframework.ai.openai.OpenAiChatOptions; 20 import org.springframework.ai.openai.OpenAiChatOptions;
23 import org.springframework.ai.tool.ToolCallback; 21 import org.springframework.ai.tool.ToolCallback;
24 -import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; 22 +import org.springframework.core.env.Environment;
25 23
26 import java.util.*; 24 import java.util.*;
27 25
  26 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  27 +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.API_CONFIG_PLACEHOLDER_NOT_RESOLVED;
  28 +
28 /** 29 /**
29 * Spring AI 工具类 30 * Spring AI 工具类
30 * 31 *
@@ -36,13 +37,42 @@ public class AiUtils { @@ -36,13 +37,42 @@ public class AiUtils {
36 public static final String TOOL_CONTEXT_TENANT_ID = "TENANT_ID"; 37 public static final String TOOL_CONTEXT_TENANT_ID = "TENANT_ID";
37 38
38 /** 39 /**
  40 + * 解析 DB 等动态配置里的 Spring 占位符,例如 ${OPENAI_API_KEY}
  41 + *
  42 + * @param value 待解析的配置值
  43 + * @return 解析后的配置值
  44 + */
  45 + public static String resolveSpringPlaceholders(String value) {
  46 + if (StrUtil.isBlank(value) || !StrUtil.contains(value, "${")) {
  47 + return value;
  48 + }
  49 + try {
  50 + return SpringUtil.getBean(Environment.class).resolveRequiredPlaceholders(value);
  51 + } catch (IllegalArgumentException ex) {
  52 + throw exception(API_CONFIG_PLACEHOLDER_NOT_RESOLVED, value);
  53 + }
  54 + }
  55 +
  56 + /**
  57 + * 校验 API Key,避免集成测试使用默认占位值发起调用。
  58 + *
  59 + * @param apiKey API Key
  60 + */
  61 + public static void validateApiKey(String apiKey) {
  62 + if (StrUtil.isBlank(apiKey) || "sk-xxxx".equals(apiKey)) {
  63 + throw new IllegalStateException("apiKey 不能为空");
  64 + }
  65 + }
  66 +
  67 + /**
39 * 通义千问支持多模态的模型 68 * 通义千问支持多模态的模型
40 * 69 *
41 * @see <a href="https://bailian.console.aliyun.com/cn-beijing/?tab=model#/model-market/all?providers=qwen&capabilities=VU">模型广场</a> 70 * @see <a href="https://bailian.console.aliyun.com/cn-beijing/?tab=model#/model-market/all?providers=qwen&capabilities=VU">模型广场</a>
42 * @see <a href="https://help.aliyun.com/zh/model-studio/error-code#error-url">必须开启 withMultiModel 参数</a> 71 * @see <a href="https://help.aliyun.com/zh/model-studio/error-code#error-url">必须开启 withMultiModel 参数</a>
43 */ 72 */
44 public static final Set<String> TONG_YI_MULTI_MODELS = SetUtils.asSet( 73 public static final Set<String> TONG_YI_MULTI_MODELS = SetUtils.asSet(
45 - // qwen3.5 / 3.6 系列(统一多模态主干) 74 + // qwen3.5 / 3.6 / 3.7 系列(统一多模态主干)
  75 + "qwen3.7-max", "qwen3.7-plus", "qwen3.7-flash",
46 "qwen3.6-plus", "qwen3.6-flash", 76 "qwen3.6-plus", "qwen3.6-flash",
47 "qwen3.5-plus", "qwen3.5-flash", 77 "qwen3.5-plus", "qwen3.5-flash",
48 // qwen-vl 视觉理解 78 // qwen-vl 视觉理解
@@ -72,27 +102,21 @@ public class AiUtils { @@ -72,27 +102,21 @@ public class AiUtils {
72 .enableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置 102 .enableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置
73 .multiModel(TONG_YI_MULTI_MODELS.contains(model)) // 是否多模态模型 103 .multiModel(TONG_YI_MULTI_MODELS.contains(model)) // 是否多模态模型
74 .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); 104 .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
75 - case YI_YAN:  
76 - return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();  
77 case DEEP_SEEK: 105 case DEEP_SEEK:
78 case DOU_BAO: // 复用 DeepSeek 客户端 106 case DOU_BAO: // 复用 DeepSeek 客户端
79 case HUN_YUAN: // 复用 DeepSeek 客户端 107 case HUN_YUAN: // 复用 DeepSeek 客户端
80 case SILICON_FLOW: // 复用 DeepSeek 客户端 108 case SILICON_FLOW: // 复用 DeepSeek 客户端
  109 + case YI_YAN: // 复用 DeepSeek 客户端
  110 + case ZHI_PU: // 复用 DeepSeek 客户端
81 case XING_HUO: // 复用 DeepSeek 客户端 111 case XING_HUO: // 复用 DeepSeek 客户端
  112 + case MINI_MAX: // 复用 DeepSeek 客户端
  113 + case MOONSHOT: // 复用 DeepSeek 客户端
  114 + case BAI_CHUAN: // 复用 DeepSeek 客户端
  115 + case STEP_FUN: // 复用 DeepSeek 客户端
82 return DeepSeekChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) 116 return DeepSeekChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
83 .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); 117 .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
84 - case ZHI_PU:  
85 - return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)  
86 - .toolCallbacks(toolCallbacks).toolContext(toolContext).build();  
87 - case MINI_MAX:  
88 - return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)  
89 - .toolCallbacks(toolCallbacks).toolContext(toolContext).build();  
90 - case MOONSHOT:  
91 - return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)  
92 - .toolCallbacks(toolCallbacks).toolContext(toolContext).build();  
93 case OPENAI: 118 case OPENAI:
94 case GEMINI: // 复用 OpenAI 客户端 119 case GEMINI: // 复用 OpenAI 客户端
95 - case BAI_CHUAN: // 复用 OpenAI 客户端  
96 case GROK: // 复用 OpenAI 客户端 120 case GROK: // 复用 OpenAI 客户端
97 return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) 121 return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
98 .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); 122 .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
@@ -159,4 +183,4 @@ public class AiUtils { @@ -159,4 +183,4 @@ public class AiUtils {
159 return MapUtil.getStr(output.getMetadata(), "reasoningContent"); 183 return MapUtil.getStr(output.getMetadata(), "reasoningContent");
160 } 184 }
161 185
162 -}  
163 \ No newline at end of file 186 \ No newline at end of file
  187 +}
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java
1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; 1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
2 2
  3 +import cn.hutool.system.SystemUtil;
3 import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; 4 import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
4 import org.junit.jupiter.api.Disabled; 5 import org.junit.jupiter.api.Disabled;
5 import org.junit.jupiter.api.Test; 6 import org.junit.jupiter.api.Test;
@@ -8,13 +9,16 @@ import org.springframework.ai.chat.messages.SystemMessage; @@ -8,13 +9,16 @@ import org.springframework.ai.chat.messages.SystemMessage;
8 import org.springframework.ai.chat.messages.UserMessage; 9 import org.springframework.ai.chat.messages.UserMessage;
9 import org.springframework.ai.chat.model.ChatResponse; 10 import org.springframework.ai.chat.model.ChatResponse;
10 import org.springframework.ai.chat.prompt.Prompt; 11 import org.springframework.ai.chat.prompt.Prompt;
11 -import org.springframework.ai.openai.OpenAiChatModel;  
12 -import org.springframework.ai.openai.OpenAiChatOptions;  
13 -import org.springframework.ai.openai.api.OpenAiApi; 12 +import org.springframework.ai.deepseek.DeepSeekChatModel;
  13 +import org.springframework.ai.deepseek.DeepSeekChatOptions;
  14 +import org.springframework.ai.deepseek.api.DeepSeekApi;
14 import reactor.core.publisher.Flux; 15 import reactor.core.publisher.Flux;
15 16
16 import java.util.ArrayList; 17 import java.util.ArrayList;
17 import java.util.List; 18 import java.util.List;
  19 +import java.util.Objects;
  20 +
  21 +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
18 22
19 /** 23 /**
20 * {@link BaiChuanChatModel} 集成测试 24 * {@link BaiChuanChatModel} 集成测试
@@ -23,22 +27,27 @@ import java.util.List; @@ -23,22 +27,27 @@ import java.util.List;
23 */ 27 */
24 public class BaiChuanChatModelTests { 28 public class BaiChuanChatModelTests {
25 29
26 - private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()  
27 - .openAiApi(OpenAiApi.builder() 30 + private static final String API_KEY = SystemUtil.get("BAICHUAN_API_KEY",
  31 + "sk-xxxx"); // 按需改成你的百川 API Key
  32 + private static final String MODEL = SystemUtil.get("BAICHUAN_MODEL",
  33 + BaiChuanChatModel.MODEL_DEFAULT);
  34 +
  35 + private final BaiChuanChatModel chatModel = new BaiChuanChatModel(DeepSeekChatModel.builder()
  36 + .deepSeekApi(DeepSeekApi.builder()
28 .baseUrl(BaiChuanChatModel.BASE_URL) 37 .baseUrl(BaiChuanChatModel.BASE_URL)
29 - .apiKey("sk-61b6766a94c70786ed02673f5e16af3c") // apiKey 38 + .completionsPath(BaiChuanChatModel.COMPLETE_PATH)
  39 + .apiKey(API_KEY)
30 .build()) 40 .build())
31 - .defaultOptions(OpenAiChatOptions.builder()  
32 - .model("Baichuan4-Turbo") // 模型(https://platform.baichuan-ai.com/docs/api) 41 + .defaultOptions(DeepSeekChatOptions.builder()
  42 + .model(MODEL)
33 .temperature(0.7) 43 .temperature(0.7)
34 .build()) 44 .build())
35 - .build();  
36 -  
37 - private final BaiChuanChatModel chatModel = new BaiChuanChatModel(openAiChatModel); 45 + .build());
38 46
39 @Test 47 @Test
40 @Disabled 48 @Disabled
41 public void testCall() { 49 public void testCall() {
  50 + validateApiKey(API_KEY);
42 // 准备参数 51 // 准备参数
43 List<Message> messages = new ArrayList<>(); 52 List<Message> messages = new ArrayList<>();
44 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); 53 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -48,11 +57,13 @@ public class BaiChuanChatModelTests { @@ -48,11 +57,13 @@ public class BaiChuanChatModelTests {
48 ChatResponse response = chatModel.call(new Prompt(messages)); 57 ChatResponse response = chatModel.call(new Prompt(messages));
49 // 打印结果 58 // 打印结果
50 System.out.println(response); 59 System.out.println(response);
  60 + System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
51 } 61 }
52 62
53 @Test 63 @Test
54 @Disabled 64 @Disabled
55 public void testStream() { 65 public void testStream() {
  66 + validateApiKey(API_KEY);
56 // 准备参数 67 // 准备参数
57 List<Message> messages = new ArrayList<>(); 68 List<Message> messages = new ArrayList<>();
58 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); 69 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -61,7 +72,10 @@ public class BaiChuanChatModelTests { @@ -61,7 +72,10 @@ public class BaiChuanChatModelTests {
61 // 调用 72 // 调用
62 Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); 73 Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
63 // 打印结果 74 // 打印结果
64 - flux.doOnNext(System.out::println).then().block(); 75 + flux.doOnNext(response -> {
  76 +// System.out.println(response);
  77 + System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
  78 + }).then().block();
65 } 79 }
66 80
67 } 81 }
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java
1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; 1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
2 2
  3 +import cn.hutool.system.SystemUtil;
3 import org.junit.jupiter.api.Disabled; 4 import org.junit.jupiter.api.Disabled;
4 import org.junit.jupiter.api.Test; 5 import org.junit.jupiter.api.Test;
5 import org.springframework.ai.chat.messages.Message; 6 import org.springframework.ai.chat.messages.Message;
@@ -14,6 +15,9 @@ import reactor.core.publisher.Flux; @@ -14,6 +15,9 @@ import reactor.core.publisher.Flux;
14 15
15 import java.util.ArrayList; 16 import java.util.ArrayList;
16 import java.util.List; 17 import java.util.List;
  18 +import java.util.Objects;
  19 +
  20 +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
17 21
18 /** 22 /**
19 * {@link DeepSeekChatModel} 集成测试 23 * {@link DeepSeekChatModel} 集成测试
@@ -22,12 +26,17 @@ import java.util.List; @@ -22,12 +26,17 @@ import java.util.List;
22 */ 26 */
23 public class DeepSeekChatModelTests { 27 public class DeepSeekChatModelTests {
24 28
  29 + private static final String API_KEY = SystemUtil.get("DEEPSEEK_API_KEY",
  30 + "sk-xxxx");
  31 + private static final String MODEL = SystemUtil.get("DEEPSEEK_MODEL",
  32 + "deepseek-v4-flash");
  33 +
25 private final DeepSeekChatModel chatModel = DeepSeekChatModel.builder() 34 private final DeepSeekChatModel chatModel = DeepSeekChatModel.builder()
26 .deepSeekApi(DeepSeekApi.builder() 35 .deepSeekApi(DeepSeekApi.builder()
27 - .apiKey("sk-eaf4172a057344dd9bc64b1f806b6axx") // apiKey 36 + .apiKey(API_KEY) // apiKey
28 .build()) 37 .build())
29 .defaultOptions(DeepSeekChatOptions.builder() 38 .defaultOptions(DeepSeekChatOptions.builder()
30 - .model("deepseek-chat") // 模型 39 + .model(MODEL) // 模型
31 .temperature(0.7) 40 .temperature(0.7)
32 .build()) 41 .build())
33 .build(); 42 .build();
@@ -35,6 +44,7 @@ public class DeepSeekChatModelTests { @@ -35,6 +44,7 @@ public class DeepSeekChatModelTests {
35 @Test 44 @Test
36 @Disabled 45 @Disabled
37 public void testCall() { 46 public void testCall() {
  47 + validateApiKey(API_KEY);
38 // 准备参数 48 // 准备参数
39 List<Message> messages = new ArrayList<>(); 49 List<Message> messages = new ArrayList<>();
40 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); 50 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -43,12 +53,13 @@ public class DeepSeekChatModelTests { @@ -43,12 +53,13 @@ public class DeepSeekChatModelTests {
43 // 调用 53 // 调用
44 ChatResponse response = chatModel.call(new Prompt(messages)); 54 ChatResponse response = chatModel.call(new Prompt(messages));
45 // 打印结果 55 // 打印结果
46 - System.out.println(response); 56 + System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
47 } 57 }
48 58
49 @Test 59 @Test
50 @Disabled 60 @Disabled
51 public void testStream() { 61 public void testStream() {
  62 + validateApiKey(API_KEY);
52 // 准备参数 63 // 准备参数
53 List<Message> messages = new ArrayList<>(); 64 List<Message> messages = new ArrayList<>();
54 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); 65 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -63,11 +74,12 @@ public class DeepSeekChatModelTests { @@ -63,11 +74,12 @@ public class DeepSeekChatModelTests {
63 @Test 74 @Test
64 @Disabled 75 @Disabled
65 public void testStream_thinking() { 76 public void testStream_thinking() {
  77 + validateApiKey(API_KEY);
66 // 准备参数 78 // 准备参数
67 List<Message> messages = new ArrayList<>(); 79 List<Message> messages = new ArrayList<>();
68 messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); 80 messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
69 DeepSeekChatOptions options = DeepSeekChatOptions.builder() 81 DeepSeekChatOptions options = DeepSeekChatOptions.builder()
70 - .model("deepseek-reasoner") 82 + .model(MODEL)
71 .build(); 83 .build();
72 84
73 // 调用 85 // 调用
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DouBaoChatModelTests.java
1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; 1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
2 2
  3 +import cn.hutool.system.SystemUtil;
3 import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel; 4 import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel;
4 import org.junit.jupiter.api.Disabled; 5 import org.junit.jupiter.api.Disabled;
5 import org.junit.jupiter.api.Test; 6 import org.junit.jupiter.api.Test;
@@ -15,6 +16,9 @@ import reactor.core.publisher.Flux; @@ -15,6 +16,9 @@ import reactor.core.publisher.Flux;
15 16
16 import java.util.ArrayList; 17 import java.util.ArrayList;
17 import java.util.List; 18 import java.util.List;
  19 +import java.util.Objects;
  20 +
  21 +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
18 22
19 /** 23 /**
20 * {@link DouBaoChatModel} 集成测试 24 * {@link DouBaoChatModel} 集成测试
@@ -23,19 +27,19 @@ import java.util.List; @@ -23,19 +27,19 @@ import java.util.List;
23 */ 27 */
24 public class DouBaoChatModelTests { 28 public class DouBaoChatModelTests {
25 29
26 - /**  
27 - * 相比 OpenAIChatModel 来说,DeepSeekChatModel 可以兼容豆包的 thinking 能力!  
28 - */ 30 + private static final String API_KEY = SystemUtil.get("DOUBAO_API_KEY",
  31 + "sk-xxxx"); // 按需改成你的豆包 API Key
  32 + private static final String MODEL = SystemUtil.get("DOUBAO_MODEL",
  33 + DouBaoChatModel.MODEL_DEFAULT);
  34 +
29 private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() 35 private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
30 .deepSeekApi(DeepSeekApi.builder() 36 .deepSeekApi(DeepSeekApi.builder()
31 .baseUrl(DouBaoChatModel.BASE_URL) 37 .baseUrl(DouBaoChatModel.BASE_URL)
32 .completionsPath(DouBaoChatModel.COMPLETE_PATH) 38 .completionsPath(DouBaoChatModel.COMPLETE_PATH)
33 - .apiKey("5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272") // apiKey 39 + .apiKey(API_KEY)
34 .build()) 40 .build())
35 .defaultOptions(DeepSeekChatOptions.builder() 41 .defaultOptions(DeepSeekChatOptions.builder()
36 - .model("doubao-1-5-lite-32k-250115") // 模型(doubao)  
37 -// .model("doubao-seed-1-6-thinking-250715") // 模型(doubao)  
38 -// .model("deepseek-r1-250120") // 模型(deepseek) 42 + .model(MODEL)
39 .temperature(0.7) 43 .temperature(0.7)
40 .build()) 44 .build())
41 .build(); 45 .build();
@@ -45,6 +49,7 @@ public class DouBaoChatModelTests { @@ -45,6 +49,7 @@ public class DouBaoChatModelTests {
45 @Test 49 @Test
46 @Disabled 50 @Disabled
47 public void testCall() { 51 public void testCall() {
  52 + validateApiKey(API_KEY);
48 // 准备参数 53 // 准备参数
49 List<Message> messages = new ArrayList<>(); 54 List<Message> messages = new ArrayList<>();
50 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); 55 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -54,11 +59,13 @@ public class DouBaoChatModelTests { @@ -54,11 +59,13 @@ public class DouBaoChatModelTests {
54 ChatResponse response = chatModel.call(new Prompt(messages)); 59 ChatResponse response = chatModel.call(new Prompt(messages));
55 // 打印结果 60 // 打印结果
56 System.out.println(response); 61 System.out.println(response);
  62 + System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
57 } 63 }
58 64
59 @Test 65 @Test
60 @Disabled 66 @Disabled
61 public void testStream() { 67 public void testStream() {
  68 + validateApiKey(API_KEY);
62 // 准备参数 69 // 准备参数
63 List<Message> messages = new ArrayList<>(); 70 List<Message> messages = new ArrayList<>();
64 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); 71 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -67,25 +74,26 @@ public class DouBaoChatModelTests { @@ -67,25 +74,26 @@ public class DouBaoChatModelTests {
67 // 调用 74 // 调用
68 Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); 75 Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
69 // 打印结果 76 // 打印结果
70 - flux.doOnNext(System.out::println).then().block(); 77 + flux.doOnNext(response -> {
  78 +// System.out.println(response);
  79 + System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
  80 + }).then().block();
71 } 81 }
72 82
73 @Test 83 @Test
74 @Disabled 84 @Disabled
75 public void testStream_thinking() { 85 public void testStream_thinking() {
  86 + validateApiKey(API_KEY);
76 // 准备参数 87 // 准备参数
77 List<Message> messages = new ArrayList<>(); 88 List<Message> messages = new ArrayList<>();
78 messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); 89 messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
79 - DeepSeekChatOptions options = DeepSeekChatOptions.builder()  
80 - .model("doubao-seed-1-6-thinking-250715")  
81 - .build();  
82 90
83 // 调用 91 // 调用
84 - Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options)); 92 + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
85 // 打印结果 93 // 打印结果
86 flux.doOnNext(response -> { 94 flux.doOnNext(response -> {
87 // System.out.println(response); 95 // System.out.println(response);
88 - System.out.println(response.getResult().getOutput()); 96 + System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
89 }).then().block(); 97 }).then().block();
90 } 98 }
91 99
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/GrokChatModelTests.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
  2 +
  3 +import cn.hutool.system.SystemUtil;
  4 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.grok.GrokChatModel;
  5 +import org.junit.jupiter.api.Disabled;
  6 +import org.junit.jupiter.api.Test;
  7 +import org.springframework.ai.chat.messages.Message;
  8 +import org.springframework.ai.chat.messages.SystemMessage;
  9 +import org.springframework.ai.chat.messages.UserMessage;
  10 +import org.springframework.ai.chat.model.ChatResponse;
  11 +import org.springframework.ai.chat.prompt.Prompt;
  12 +import org.springframework.ai.openai.OpenAiChatModel;
  13 +import org.springframework.ai.openai.OpenAiChatOptions;
  14 +import org.springframework.ai.openai.api.OpenAiApi;
  15 +import reactor.core.publisher.Flux;
  16 +
  17 +import java.util.ArrayList;
  18 +import java.util.List;
  19 +import java.util.Objects;
  20 +
  21 +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
  22 +
  23 +/**
  24 + * {@link GrokChatModel} 集成测试
  25 + *
  26 + * @author 芋道源码
  27 + */
  28 +public class GrokChatModelTests {
  29 +
  30 + private static final String API_KEY = SystemUtil.get("GROK_API_KEY",
  31 + "sk-xxxx");
  32 + private static final String MODEL = SystemUtil.get("GROK_MODEL",
  33 + GrokChatModel.MODEL_DEFAULT);
  34 +
  35 + private final GrokChatModel chatModel = new GrokChatModel(OpenAiChatModel.builder()
  36 + .openAiApi(OpenAiApi.builder()
  37 + .baseUrl(GrokChatModel.BASE_URL)
  38 + .completionsPath(GrokChatModel.COMPLETE_PATH)
  39 + .apiKey(API_KEY)
  40 + .build())
  41 + .defaultOptions(OpenAiChatOptions.builder()
  42 + .model(MODEL)
  43 + .temperature(0.7)
  44 + .build())
  45 + .build());
  46 +
  47 + @Test
  48 + @Disabled
  49 + public void testCall() {
  50 + validateApiKey(API_KEY);
  51 + // 准备参数
  52 + List<Message> messages = new ArrayList<>();
  53 + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
  54 + messages.add(new UserMessage("1 + 1 = ?"));
  55 +
  56 + // 调用
  57 + ChatResponse response = chatModel.call(new Prompt(messages));
  58 + // 打印结果
  59 + System.out.println(response);
  60 + System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
  61 + }
  62 +
  63 + @Test
  64 + @Disabled
  65 + public void testStream() {
  66 + validateApiKey(API_KEY);
  67 + // 准备参数
  68 + List<Message> messages = new ArrayList<>();
  69 + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
  70 + messages.add(new UserMessage("1 + 1 = ?"));
  71 +
  72 + // 调用
  73 + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
  74 + // 打印结果
  75 + flux.doOnNext(response -> {
  76 +// System.out.println(response);
  77 + System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
  78 + }).then().block();
  79 + }
  80 +
  81 +}
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/HunYuanChatModelTests.java
1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; 1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
2 2
  3 +import cn.hutool.system.SystemUtil;
3 import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; 4 import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel;
4 import org.junit.jupiter.api.Disabled; 5 import org.junit.jupiter.api.Disabled;
5 import org.junit.jupiter.api.Test; 6 import org.junit.jupiter.api.Test;
@@ -15,6 +16,9 @@ import reactor.core.publisher.Flux; @@ -15,6 +16,9 @@ import reactor.core.publisher.Flux;
15 16
16 import java.util.ArrayList; 17 import java.util.ArrayList;
17 import java.util.List; 18 import java.util.List;
  19 +import java.util.Objects;
  20 +
  21 +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
18 22
19 /** 23 /**
20 * {@link HunYuanChatModel} 集成测试 24 * {@link HunYuanChatModel} 集成测试
@@ -23,14 +27,19 @@ import java.util.List; @@ -23,14 +27,19 @@ import java.util.List;
23 */ 27 */
24 public class HunYuanChatModelTests { 28 public class HunYuanChatModelTests {
25 29
  30 + private static final String API_KEY = SystemUtil.get("HUNYUAN_API_KEY",
  31 + "sk-xxxx");
  32 + private static final String MODEL = SystemUtil.get("HUNYUAN_MODEL",
  33 + HunYuanChatModel.MODEL_DEFAULT);
  34 +
26 private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() 35 private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
27 .deepSeekApi(DeepSeekApi.builder() 36 .deepSeekApi(DeepSeekApi.builder()
28 .baseUrl(HunYuanChatModel.BASE_URL) 37 .baseUrl(HunYuanChatModel.BASE_URL)
29 .completionsPath(HunYuanChatModel.COMPLETE_PATH) 38 .completionsPath(HunYuanChatModel.COMPLETE_PATH)
30 - .apiKey("sk-abc") // apiKey 39 + .apiKey(API_KEY) // apiKey
31 .build()) 40 .build())
32 .defaultOptions(DeepSeekChatOptions.builder() 41 .defaultOptions(DeepSeekChatOptions.builder()
33 - .model(HunYuanChatModel.MODEL_DEFAULT) // 模型 42 + .model(MODEL) // 模型
34 .temperature(0.7) 43 .temperature(0.7)
35 .build()) 44 .build())
36 .build(); 45 .build();
@@ -40,6 +49,7 @@ public class HunYuanChatModelTests { @@ -40,6 +49,7 @@ public class HunYuanChatModelTests {
40 @Test 49 @Test
41 @Disabled 50 @Disabled
42 public void testCall() { 51 public void testCall() {
  52 + validateApiKey(API_KEY);
43 // 准备参数 53 // 准备参数
44 List<Message> messages = new ArrayList<>(); 54 List<Message> messages = new ArrayList<>();
45 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); 55 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -48,12 +58,13 @@ public class HunYuanChatModelTests { @@ -48,12 +58,13 @@ public class HunYuanChatModelTests {
48 // 调用 58 // 调用
49 ChatResponse response = chatModel.call(new Prompt(messages)); 59 ChatResponse response = chatModel.call(new Prompt(messages));
50 // 打印结果 60 // 打印结果
51 - System.out.println(response); 61 + System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
52 } 62 }
53 63
54 @Test 64 @Test
55 @Disabled 65 @Disabled
56 public void testStream() { 66 public void testStream() {
  67 + validateApiKey(API_KEY);
57 // 准备参数 68 // 准备参数
58 List<Message> messages = new ArrayList<>(); 69 List<Message> messages = new ArrayList<>();
59 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); 70 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -68,12 +79,12 @@ public class HunYuanChatModelTests { @@ -68,12 +79,12 @@ public class HunYuanChatModelTests {
68 @Test 79 @Test
69 @Disabled 80 @Disabled
70 public void testStream_thinking() { 81 public void testStream_thinking() {
  82 + validateApiKey(API_KEY);
71 // 准备参数 83 // 准备参数
72 List<Message> messages = new ArrayList<>(); 84 List<Message> messages = new ArrayList<>();
73 messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); 85 messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
74 DeepSeekChatOptions options = DeepSeekChatOptions.builder() 86 DeepSeekChatOptions options = DeepSeekChatOptions.builder()
75 - .model("hunyuan-a13b")  
76 -// .model("hunyuan-turbos-latest") 87 + .model(MODEL)
77 .build(); 88 .build();
78 89
79 // 调用 90 // 调用
@@ -85,66 +96,4 @@ public class HunYuanChatModelTests { @@ -85,66 +96,4 @@ public class HunYuanChatModelTests {
85 }).then().block(); 96 }).then().block();
86 } 97 }
87 98
88 - private final DeepSeekChatModel deepSeekOpenAiChatModel = DeepSeekChatModel.builder()  
89 - .deepSeekApi(DeepSeekApi.builder()  
90 - .baseUrl(HunYuanChatModel.DEEP_SEEK_BASE_URL)  
91 - .completionsPath(HunYuanChatModel.COMPLETE_PATH)  
92 - .apiKey("sk-abc") // apiKey  
93 - .build())  
94 - .defaultOptions(DeepSeekChatOptions.builder()  
95 -// .model(HunYuanChatModel.DEEP_SEEK_MODEL_DEFAULT) // 模型("deepseek-v3")  
96 - .model("deepseek-r1") // 模型("deepseek-r1")  
97 - .temperature(0.7)  
98 - .build())  
99 - .build();  
100 -  
101 - private final HunYuanChatModel deepSeekChatModel = new HunYuanChatModel(deepSeekOpenAiChatModel);  
102 -  
103 - @Test  
104 - @Disabled  
105 - public void testCall_deepseek() {  
106 - // 准备参数  
107 - List<Message> messages = new ArrayList<>();  
108 - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));  
109 - messages.add(new UserMessage("1 + 1 = ?"));  
110 -  
111 - // 调用  
112 - ChatResponse response = deepSeekChatModel.call(new Prompt(messages));  
113 - // 打印结果  
114 - System.out.println(response);  
115 - }  
116 -  
117 - @Test  
118 - @Disabled  
119 - public void testStream_deepseek() {  
120 - // 准备参数  
121 - List<Message> messages = new ArrayList<>();  
122 - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));  
123 - messages.add(new UserMessage("1 + 1 = ?"));  
124 -  
125 - // 调用  
126 - Flux<ChatResponse> flux = deepSeekChatModel.stream(new Prompt(messages));  
127 - // 打印结果  
128 - flux.doOnNext(System.out::println).then().block();  
129 - }  
130 -  
131 - @Test  
132 - @Disabled  
133 - public void testStream_deepseek_thinking() {  
134 - // 准备参数  
135 - List<Message> messages = new ArrayList<>();  
136 - messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));  
137 - DeepSeekChatOptions options = DeepSeekChatOptions.builder()  
138 - .model("deepseek-r1")  
139 - .build();  
140 -  
141 - // 调用  
142 - Flux<ChatResponse> flux = deepSeekChatModel.stream(new Prompt(messages, options));  
143 - // 打印结果  
144 - flux.doOnNext(response -> {  
145 -// System.out.println(response);  
146 - System.out.println(response.getResult().getOutput());  
147 - }).then().block();  
148 - }  
149 -  
150 } 99 }
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MiniMaxChatModelTests.java
1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; 1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
2 2
  3 +import cn.hutool.system.SystemUtil;
  4 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax.MiniMaxChatModel;
3 import org.junit.jupiter.api.Disabled; 5 import org.junit.jupiter.api.Disabled;
4 import org.junit.jupiter.api.Test; 6 import org.junit.jupiter.api.Test;
5 import org.springframework.ai.chat.messages.Message; 7 import org.springframework.ai.chat.messages.Message;
@@ -7,13 +9,16 @@ import org.springframework.ai.chat.messages.SystemMessage; @@ -7,13 +9,16 @@ import org.springframework.ai.chat.messages.SystemMessage;
7 import org.springframework.ai.chat.messages.UserMessage; 9 import org.springframework.ai.chat.messages.UserMessage;
8 import org.springframework.ai.chat.model.ChatResponse; 10 import org.springframework.ai.chat.model.ChatResponse;
9 import org.springframework.ai.chat.prompt.Prompt; 11 import org.springframework.ai.chat.prompt.Prompt;
10 -import org.springframework.ai.minimax.MiniMaxChatModel;  
11 -import org.springframework.ai.minimax.MiniMaxChatOptions;  
12 -import org.springframework.ai.minimax.api.MiniMaxApi; 12 +import org.springframework.ai.deepseek.DeepSeekChatModel;
  13 +import org.springframework.ai.deepseek.DeepSeekChatOptions;
  14 +import org.springframework.ai.deepseek.api.DeepSeekApi;
13 import reactor.core.publisher.Flux; 15 import reactor.core.publisher.Flux;
14 16
15 import java.util.ArrayList; 17 import java.util.ArrayList;
16 import java.util.List; 18 import java.util.List;
  19 +import java.util.Objects;
  20 +
  21 +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
17 22
18 /** 23 /**
19 * {@link MiniMaxChatModel} 的集成测试 24 * {@link MiniMaxChatModel} 的集成测试
@@ -22,14 +27,24 @@ import java.util.List; @@ -22,14 +27,24 @@ import java.util.List;
22 */ 27 */
23 public class MiniMaxChatModelTests { 28 public class MiniMaxChatModelTests {
24 29
25 - private final MiniMaxChatModel chatModel = new MiniMaxChatModel(  
26 - new MiniMaxApi("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJHcm91cE5hbWUiOiLnjovmlofmlowiLCJVc2VyTmFtZSI6IueOi-aWh-aWjCIsIkFjY291bnQiOiIiLCJTdWJqZWN0SUQiOiIxODk3Mjg3MjQ5NDU2ODA4MzQ2IiwiUGhvbmUiOiIxNTYwMTY5MTM5OSIsIkdyb3VwSUQiOiIxODk3Mjg3MjQ5NDQ4NDE5NzM4IiwiUGFnZU5hbWUiOiIiLCJNYWlsIjoiIiwiQ3JlYXRlVGltZSI6IjIwMjUtMDMtMTEgMTI6NTI6MDIiLCJUb2tlblR5cGUiOjEsImlzcyI6Im1pbmltYXgifQ.aAuB7gWW_oA4IYhh-CF7c9MfWWxKN49B_HK-DYjXaDwwffhiG-H1571z1WQhp9QytWG-DqgLejneeSxkiq1wQIe3FsEP2wz4BmGBct31LehbJu8ehLxg_vg75Uod1nFAHbm5mZz6JSVLNIlSo87Xr3UtSzJhAXlapEkcqlA4YOzOpKrZ8l5_OJPTORTCmHWZYgJcRS-faNiH62ZnUEHUozesTFhubJHo5GfJCw_edlnmfSUocERV1BjWvenhZ9My-aYXNktcW9WaSj9l6gayV7A0Ium_PL55T9ln1PcI8gayiVUKJGJDoqNyF1AF9_aF9NOKtTnQzwNqnZdlTYH6hw"), // 密钥  
27 - MiniMaxChatOptions.builder()  
28 - .model(MiniMaxApi.ChatModel.ABAB_6_5_G_Chat.getValue()) // 模型  
29 - .build()); 30 + private static final String API_KEY = SystemUtil.get("MINIMAX_API_KEY",
  31 + "sk-xxxx"); // 按需改成你的 MiniMax API Key
  32 + private static final String MODEL = SystemUtil.get("MINIMAX_MODEL",
  33 + MiniMaxChatModel.MODEL_DEFAULT);
  34 +
  35 + private final MiniMaxChatModel chatModel = new MiniMaxChatModel(DeepSeekChatModel.builder()
  36 + .deepSeekApi(DeepSeekApi.builder()
  37 + .baseUrl(MiniMaxChatModel.BASE_URL)
  38 + .apiKey(API_KEY)
  39 + .build())
  40 + .defaultOptions(DeepSeekChatOptions.builder()
  41 + .model(MODEL)
  42 + .build())
  43 + .build());
30 @Test 44 @Test
31 @Disabled 45 @Disabled
32 public void testCall() { 46 public void testCall() {
  47 + validateApiKey(API_KEY);
33 // 准备参数 48 // 准备参数
34 List<Message> messages = new ArrayList<>(); 49 List<Message> messages = new ArrayList<>();
35 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); 50 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -39,12 +54,13 @@ public class MiniMaxChatModelTests { @@ -39,12 +54,13 @@ public class MiniMaxChatModelTests {
39 ChatResponse response = chatModel.call(new Prompt(messages)); 54 ChatResponse response = chatModel.call(new Prompt(messages));
40 // 打印结果 55 // 打印结果
41 System.out.println(response); 56 System.out.println(response);
42 - System.out.println(response.getResult().getOutput()); 57 + System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
43 } 58 }
44 59
45 @Test 60 @Test
46 @Disabled 61 @Disabled
47 public void testStream() { 62 public void testStream() {
  63 + validateApiKey(API_KEY);
48 // 准备参数 64 // 准备参数
49 List<Message> messages = new ArrayList<>(); 65 List<Message> messages = new ArrayList<>();
50 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); 66 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -55,19 +71,19 @@ public class MiniMaxChatModelTests { @@ -55,19 +71,19 @@ public class MiniMaxChatModelTests {
55 // 打印结果 71 // 打印结果
56 flux.doOnNext(response -> { 72 flux.doOnNext(response -> {
57 // System.out.println(response); 73 // System.out.println(response);
58 - System.out.println(response.getResult().getOutput()); 74 + System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
59 }).then().block(); 75 }).then().block();
60 } 76 }
61 77
62 - // TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复  
63 @Test 78 @Test
64 @Disabled 79 @Disabled
65 public void testStream_thinking() { 80 public void testStream_thinking() {
  81 + validateApiKey(API_KEY);
66 // 准备参数 82 // 准备参数
67 List<Message> messages = new ArrayList<>(); 83 List<Message> messages = new ArrayList<>();
68 messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); 84 messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
69 - MiniMaxChatOptions options = MiniMaxChatOptions.builder()  
70 - .model("MiniMax-M1") 85 + DeepSeekChatOptions options = DeepSeekChatOptions.builder()
  86 + .model(MODEL)
71 .build(); 87 .build();
72 88
73 // 调用 89 // 调用
@@ -75,7 +91,7 @@ public class MiniMaxChatModelTests { @@ -75,7 +91,7 @@ public class MiniMaxChatModelTests {
75 // 打印结果 91 // 打印结果
76 flux.doOnNext(response -> { 92 flux.doOnNext(response -> {
77 // System.out.println(response); 93 // System.out.println(response);
78 - System.out.println(response.getResult().getOutput()); 94 + System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
79 }).then().block(); 95 }).then().block();
80 } 96 }
81 97
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java
1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; 1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
2 2
  3 +import cn.hutool.system.SystemUtil;
  4 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot.MoonshotChatModel;
3 import org.junit.jupiter.api.Disabled; 5 import org.junit.jupiter.api.Disabled;
4 import org.junit.jupiter.api.Test; 6 import org.junit.jupiter.api.Test;
5 -import org.springaicommunity.moonshot.MoonshotChatModel;  
6 -import org.springaicommunity.moonshot.MoonshotChatOptions;  
7 -import org.springaicommunity.moonshot.api.MoonshotApi;  
8 import org.springframework.ai.chat.messages.Message; 7 import org.springframework.ai.chat.messages.Message;
9 import org.springframework.ai.chat.messages.SystemMessage; 8 import org.springframework.ai.chat.messages.SystemMessage;
10 import org.springframework.ai.chat.messages.UserMessage; 9 import org.springframework.ai.chat.messages.UserMessage;
11 import org.springframework.ai.chat.model.ChatResponse; 10 import org.springframework.ai.chat.model.ChatResponse;
12 import org.springframework.ai.chat.prompt.Prompt; 11 import org.springframework.ai.chat.prompt.Prompt;
  12 +import org.springframework.ai.deepseek.DeepSeekChatModel;
  13 +import org.springframework.ai.deepseek.DeepSeekChatOptions;
  14 +import org.springframework.ai.deepseek.api.DeepSeekApi;
13 import reactor.core.publisher.Flux; 15 import reactor.core.publisher.Flux;
14 16
15 import java.util.ArrayList; 17 import java.util.ArrayList;
16 import java.util.List; 18 import java.util.List;
  19 +import java.util.Objects;
  20 +
  21 +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
17 22
18 /** 23 /**
19 * {@link MoonshotChatModel} 的集成测试 24 * {@link MoonshotChatModel} 的集成测试
@@ -22,18 +27,27 @@ import java.util.List; @@ -22,18 +27,27 @@ import java.util.List;
22 */ 27 */
23 public class MoonshotChatModelTests { 28 public class MoonshotChatModelTests {
24 29
25 - private final MoonshotChatModel chatModel = MoonshotChatModel.builder()  
26 - .moonshotApi(MoonshotApi.builder()  
27 - .apiKey("sk-aHYYV1SARscItye5QQRRNbXij4fy65Ee7pNZlC9gsSQnUKXA") // 密钥 30 + private static final String API_KEY = SystemUtil.get("MOONSHOT_API_KEY",
  31 + "sk-xxxx"); // 按需改成你的 Moonshot API Key
  32 + private static final String MODEL = SystemUtil.get("MOONSHOT_MODEL",
  33 + MoonshotChatModel.MODEL_DEFAULT);
  34 +
  35 + private final MoonshotChatModel chatModel = new MoonshotChatModel(DeepSeekChatModel.builder()
  36 + .deepSeekApi(DeepSeekApi.builder()
  37 + .baseUrl(MoonshotChatModel.BASE_URL)
  38 + .completionsPath(MoonshotChatModel.COMPLETE_PATH)
  39 + .apiKey(API_KEY)
28 .build()) 40 .build())
29 - .defaultOptions(MoonshotChatOptions.builder()  
30 - .model("kimi-k2-0711-preview") // 模型 41 + .defaultOptions(DeepSeekChatOptions.builder()
  42 + .model(MODEL)
  43 + .temperature(1D)
31 .build()) 44 .build())
32 - .build(); 45 + .build());
33 46
34 @Test 47 @Test
35 @Disabled 48 @Disabled
36 public void testCall() { 49 public void testCall() {
  50 + validateApiKey(API_KEY);
37 // 准备参数 51 // 准备参数
38 List<Message> messages = new ArrayList<>(); 52 List<Message> messages = new ArrayList<>();
39 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); 53 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -43,12 +57,13 @@ public class MoonshotChatModelTests { @@ -43,12 +57,13 @@ public class MoonshotChatModelTests {
43 ChatResponse response = chatModel.call(new Prompt(messages)); 57 ChatResponse response = chatModel.call(new Prompt(messages));
44 // 打印结果 58 // 打印结果
45 System.out.println(response); 59 System.out.println(response);
46 - System.out.println(response.getResult().getOutput()); 60 + System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
47 } 61 }
48 62
49 @Test 63 @Test
50 @Disabled 64 @Disabled
51 public void testStream() { 65 public void testStream() {
  66 + validateApiKey(API_KEY);
52 // 准备参数 67 // 准备参数
53 List<Message> messages = new ArrayList<>(); 68 List<Message> messages = new ArrayList<>();
54 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); 69 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -59,20 +74,19 @@ public class MoonshotChatModelTests { @@ -59,20 +74,19 @@ public class MoonshotChatModelTests {
59 // 打印结果 74 // 打印结果
60 flux.doOnNext(response -> { 75 flux.doOnNext(response -> {
61 // System.out.println(response); 76 // System.out.println(response);
62 - System.out.println(response.getResult().getOutput()); 77 + System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
63 }).then().block(); 78 }).then().block();
64 } 79 }
65 80
66 - // TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复  
67 @Test 81 @Test
68 @Disabled 82 @Disabled
69 public void testStream_thinking() { 83 public void testStream_thinking() {
  84 + validateApiKey(API_KEY);
70 // 准备参数 85 // 准备参数
71 List<Message> messages = new ArrayList<>(); 86 List<Message> messages = new ArrayList<>();
72 messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); 87 messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
73 - MoonshotChatOptions options = MoonshotChatOptions.builder()  
74 -// .model("kimi-k2-0711-preview")  
75 - .model("kimi-thinking-preview") 88 + DeepSeekChatOptions options = DeepSeekChatOptions.builder()
  89 + .model(MODEL)
76 .build(); 90 .build();
77 91
78 // 调用 92 // 调用
@@ -80,7 +94,7 @@ public class MoonshotChatModelTests { @@ -80,7 +94,7 @@ public class MoonshotChatModelTests {
80 // 打印结果 94 // 打印结果
81 flux.doOnNext(response -> { 95 flux.doOnNext(response -> {
82 // System.out.println(response); 96 // System.out.println(response);
83 - System.out.println(response.getResult().getOutput()); 97 + System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
84 }).then().block(); 98 }).then().block();
85 } 99 }
86 100
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/StepFunChatModelTests.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
  2 +
  3 +import cn.hutool.system.SystemUtil;
  4 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun.StepFunChatModel;
  5 +import org.junit.jupiter.api.Disabled;
  6 +import org.junit.jupiter.api.Test;
  7 +import org.springframework.ai.chat.messages.Message;
  8 +import org.springframework.ai.chat.messages.SystemMessage;
  9 +import org.springframework.ai.chat.messages.UserMessage;
  10 +import org.springframework.ai.chat.model.ChatResponse;
  11 +import org.springframework.ai.chat.prompt.Prompt;
  12 +import org.springframework.ai.deepseek.DeepSeekChatModel;
  13 +import org.springframework.ai.deepseek.DeepSeekChatOptions;
  14 +import org.springframework.ai.deepseek.api.DeepSeekApi;
  15 +import reactor.core.publisher.Flux;
  16 +
  17 +import java.util.ArrayList;
  18 +import java.util.List;
  19 +import java.util.Objects;
  20 +
  21 +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
  22 +
  23 +/**
  24 + * {@link StepFunChatModel} 集成测试
  25 + *
  26 + * @author 芋道源码
  27 + */
  28 +public class StepFunChatModelTests {
  29 +
  30 + private static final String API_KEY = SystemUtil.get("STEPFUN_API_KEY",
  31 + "sk-xxxx");
  32 + private static final String MODEL = SystemUtil.get("STEPFUN_MODEL",
  33 + StepFunChatModel.MODEL_DEFAULT);
  34 +
  35 + private final StepFunChatModel chatModel = new StepFunChatModel(DeepSeekChatModel.builder()
  36 + .deepSeekApi(DeepSeekApi.builder()
  37 + .baseUrl(StepFunChatModel.BASE_URL)
  38 + .completionsPath(StepFunChatModel.COMPLETE_PATH)
  39 + .apiKey(API_KEY)
  40 + .build())
  41 + .defaultOptions(DeepSeekChatOptions.builder()
  42 + .model(MODEL)
  43 + .temperature(0.7)
  44 + .build())
  45 + .build());
  46 +
  47 + @Test
  48 + @Disabled
  49 + public void testCall() {
  50 + validateApiKey(API_KEY);
  51 + // 准备参数
  52 + List<Message> messages = new ArrayList<>();
  53 + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
  54 + messages.add(new UserMessage("1 + 1 = ?"));
  55 +
  56 + // 调用
  57 + ChatResponse response = chatModel.call(new Prompt(messages));
  58 + // 打印结果
  59 + System.out.println(response);
  60 + System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
  61 + }
  62 +
  63 + @Test
  64 + @Disabled
  65 + public void testStream() {
  66 + validateApiKey(API_KEY);
  67 + // 准备参数
  68 + List<Message> messages = new ArrayList<>();
  69 + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
  70 + messages.add(new UserMessage("1 + 1 = ?"));
  71 +
  72 + // 调用
  73 + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
  74 + // 打印结果
  75 + flux.doOnNext(response -> {
  76 +// System.out.println(response);
  77 +// System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
  78 + System.out.println(response.getResult() != null ? response.getResult().getOutput() : null);
  79 + }).then().block();
  80 + }
  81 +
  82 +}
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/XingHuoChatModelTests.java
1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; 1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
2 2
  3 +import cn.hutool.system.SystemUtil;
3 import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; 4 import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
4 import org.junit.jupiter.api.Disabled; 5 import org.junit.jupiter.api.Disabled;
5 import org.junit.jupiter.api.Test; 6 import org.junit.jupiter.api.Test;
@@ -8,13 +9,14 @@ import org.springframework.ai.chat.messages.SystemMessage; @@ -8,13 +9,14 @@ import org.springframework.ai.chat.messages.SystemMessage;
8 import org.springframework.ai.chat.messages.UserMessage; 9 import org.springframework.ai.chat.messages.UserMessage;
9 import org.springframework.ai.chat.model.ChatResponse; 10 import org.springframework.ai.chat.model.ChatResponse;
10 import org.springframework.ai.chat.prompt.Prompt; 11 import org.springframework.ai.chat.prompt.Prompt;
11 -import org.springframework.ai.deepseek.DeepSeekChatModel;  
12 import org.springframework.ai.deepseek.DeepSeekChatOptions; 12 import org.springframework.ai.deepseek.DeepSeekChatOptions;
13 -import org.springframework.ai.deepseek.api.DeepSeekApi;  
14 import reactor.core.publisher.Flux; 13 import reactor.core.publisher.Flux;
15 14
16 import java.util.ArrayList; 15 import java.util.ArrayList;
17 import java.util.List; 16 import java.util.List;
  17 +import java.util.Objects;
  18 +
  19 +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
18 20
19 /** 21 /**
20 * {@link XingHuoChatModel} 集成测试 22 * {@link XingHuoChatModel} 集成测试
@@ -23,24 +25,23 @@ import java.util.List; @@ -23,24 +25,23 @@ import java.util.List;
23 */ 25 */
24 public class XingHuoChatModelTests { 26 public class XingHuoChatModelTests {
25 27
26 - private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()  
27 - .deepSeekApi(DeepSeekApi.builder()  
28 - .baseUrl(XingHuoChatModel.BASE_URL_V2)  
29 - .completionsPath(XingHuoChatModel.BASE_COMPLETIONS_PATH_V2)  
30 - .apiKey("75b161ed2aef4719b275d6e7f2a4d4cd:YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz") // appKey:secretKey  
31 - .build())  
32 - .defaultOptions(DeepSeekChatOptions.builder()  
33 -// .model("generalv3.5") // 模型  
34 - .model("x1") // 模型  
35 - .temperature(0.7)  
36 - .build())  
37 - .build(); 28 + private static final String API_KEY = SystemUtil.get("XINGHUO_API_KEY",
  29 + "sk-xxxx"); // 按需改成你的讯飞星火 API Key
  30 + private static final String MODEL = SystemUtil.get("XINGHUO_MODEL",
  31 + XingHuoChatModel.MODEL_DEFAULT);
38 32
39 - private final XingHuoChatModel chatModel = new XingHuoChatModel(openAiChatModel); 33 + private final XingHuoChatModel chatModel = XingHuoChatModel.builder()
  34 + .apiKey(API_KEY)
  35 + .options(DeepSeekChatOptions.builder()
  36 + .model(MODEL)
  37 + .temperature(0.7)
  38 + .build())
  39 + .build();
40 40
41 @Test 41 @Test
42 @Disabled 42 @Disabled
43 public void testCall() { 43 public void testCall() {
  44 + validateApiKey(API_KEY);
44 // 准备参数 45 // 准备参数
45 List<Message> messages = new ArrayList<>(); 46 List<Message> messages = new ArrayList<>();
46 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); 47 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -55,6 +56,7 @@ public class XingHuoChatModelTests { @@ -55,6 +56,7 @@ public class XingHuoChatModelTests {
55 @Test 56 @Test
56 @Disabled 57 @Disabled
57 public void testStream() { 58 public void testStream() {
  59 + validateApiKey(API_KEY);
58 // 准备参数 60 // 准备参数
59 List<Message> messages = new ArrayList<>(); 61 List<Message> messages = new ArrayList<>();
60 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); 62 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -69,11 +71,12 @@ public class XingHuoChatModelTests { @@ -69,11 +71,12 @@ public class XingHuoChatModelTests {
69 @Test 71 @Test
70 @Disabled 72 @Disabled
71 public void testStream_thinking() { 73 public void testStream_thinking() {
  74 + validateApiKey(API_KEY);
72 // 准备参数 75 // 准备参数
73 List<Message> messages = new ArrayList<>(); 76 List<Message> messages = new ArrayList<>();
74 messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); 77 messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
75 DeepSeekChatOptions options = DeepSeekChatOptions.builder() 78 DeepSeekChatOptions options = DeepSeekChatOptions.builder()
76 - .model("x1") 79 + .model(MODEL)
77 .build(); 80 .build();
78 81
79 // 调用 82 // 调用
@@ -81,7 +84,7 @@ public class XingHuoChatModelTests { @@ -81,7 +84,7 @@ public class XingHuoChatModelTests {
81 // 打印结果 84 // 打印结果
82 flux.doOnNext(response -> { 85 flux.doOnNext(response -> {
83 // System.out.println(response); 86 // System.out.println(response);
84 - System.out.println(response.getResult().getOutput()); 87 + System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
85 }).then().block(); 88 }).then().block();
86 } 89 }
87 90
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java
1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; 1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
2 2
  3 +import cn.hutool.system.SystemUtil;
  4 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan.YiYanChatModel;
3 import org.junit.jupiter.api.Disabled; 5 import org.junit.jupiter.api.Disabled;
4 import org.junit.jupiter.api.Test; 6 import org.junit.jupiter.api.Test;
5 -import org.springaicommunity.qianfan.QianFanChatModel;  
6 -import org.springaicommunity.qianfan.QianFanChatOptions;  
7 -import org.springaicommunity.qianfan.api.QianFanApi;  
8 import org.springframework.ai.chat.messages.Message; 7 import org.springframework.ai.chat.messages.Message;
9 import org.springframework.ai.chat.messages.UserMessage; 8 import org.springframework.ai.chat.messages.UserMessage;
10 import org.springframework.ai.chat.model.ChatResponse; 9 import org.springframework.ai.chat.model.ChatResponse;
11 import org.springframework.ai.chat.prompt.Prompt; 10 import org.springframework.ai.chat.prompt.Prompt;
  11 +import org.springframework.ai.deepseek.DeepSeekChatModel;
  12 +import org.springframework.ai.deepseek.DeepSeekChatOptions;
  13 +import org.springframework.ai.deepseek.api.DeepSeekApi;
12 import reactor.core.publisher.Flux; 14 import reactor.core.publisher.Flux;
13 15
14 import java.util.ArrayList; 16 import java.util.ArrayList;
15 import java.util.List; 17 import java.util.List;
  18 +import java.util.Objects;
  19 +
  20 +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
16 21
17 -// TODO @芋艿:百度千帆 API 提供了 V2 版本,目前 Spring AI 不兼容,可关键 <https://github.com/spring-projects/spring-ai/issues/2179> 进展  
18 /** 22 /**
19 - * {@link QianFanChatModel} 的集成测试 23 + * {@link YiYanChatModel} 的集成测试
20 * 24 *
21 * @author fansili 25 * @author fansili
22 */ 26 */
23 public class YiYanChatModelTests { 27 public class YiYanChatModelTests {
24 28
25 - private final QianFanChatModel chatModel = new QianFanChatModel(  
26 - new QianFanApi("DGnyzREuaY7av7c38bOM9Ji2", "9aR8myflEOPDrEeLhoXv0FdqANOAyIZW"), // 密钥  
27 - QianFanChatOptions.builder()  
28 - .model("ERNIE-4.5-8K-Preview")  
29 - .build()  
30 - ); 29 + private static final String API_KEY = SystemUtil.get("YIYAN_API_KEY",
  30 + "sk-xxxx"); // 按需改成你的文心一言 API Key
  31 + private static final String MODEL = SystemUtil.get("YIYAN_MODEL",
  32 + YiYanChatModel.MODEL_DEFAULT);
  33 +
  34 + private final YiYanChatModel chatModel = new YiYanChatModel(DeepSeekChatModel.builder()
  35 + .deepSeekApi(DeepSeekApi.builder()
  36 + .baseUrl(YiYanChatModel.BASE_URL)
  37 + .apiKey(API_KEY)
  38 + .build())
  39 + .defaultOptions(DeepSeekChatOptions.builder()
  40 + .model(MODEL)
  41 + .build())
  42 + .build());
31 43
32 @Test 44 @Test
33 @Disabled 45 @Disabled
34 public void testCall() { 46 public void testCall() {
  47 + validateApiKey(API_KEY);
35 // 准备参数 48 // 准备参数
36 List<Message> messages = new ArrayList<>(); 49 List<Message> messages = new ArrayList<>();
37 - // TODO @芋艿:文心一言,只要带上 system message 就报错,已经各种测试,很莫名!  
38 -// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));  
39 messages.add(new UserMessage("1 + 1 = ?")); 50 messages.add(new UserMessage("1 + 1 = ?"));
40 51
41 // 调用 52 // 调用
42 ChatResponse response = chatModel.call(new Prompt(messages)); 53 ChatResponse response = chatModel.call(new Prompt(messages));
43 // 打印结果 54 // 打印结果
44 System.out.println(response); 55 System.out.println(response);
  56 + System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
45 } 57 }
46 58
47 @Test 59 @Test
48 @Disabled 60 @Disabled
49 public void testStream() { 61 public void testStream() {
  62 + validateApiKey(API_KEY);
50 // 准备参数 63 // 准备参数
51 List<Message> messages = new ArrayList<>(); 64 List<Message> messages = new ArrayList<>();
52 - // TODO @芋艿:文心一言,只要带上 system message 就报错,已经各种测试,很莫名!  
53 -// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));  
54 messages.add(new UserMessage("1 + 1 = ?")); 65 messages.add(new UserMessage("1 + 1 = ?"));
55 66
56 // 调用 67 // 调用
57 Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); 68 Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
58 // 打印结果 69 // 打印结果
59 - flux.doOnNext(System.out::println).then().block(); 70 + flux.doOnNext(response -> {
  71 +// System.out.println(response);
  72 + System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
  73 + }).then().block();
60 } 74 }
61 75
62 } 76 }
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/ZhiPuAiChatModelTests.java
1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; 1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
2 2
  3 +import cn.hutool.system.SystemUtil;
  4 +import cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu.ZhiPuChatModel;
3 import org.junit.jupiter.api.Disabled; 5 import org.junit.jupiter.api.Disabled;
4 import org.junit.jupiter.api.Test; 6 import org.junit.jupiter.api.Test;
5 import org.springframework.ai.chat.messages.Message; 7 import org.springframework.ai.chat.messages.Message;
@@ -7,31 +9,43 @@ import org.springframework.ai.chat.messages.SystemMessage; @@ -7,31 +9,43 @@ import org.springframework.ai.chat.messages.SystemMessage;
7 import org.springframework.ai.chat.messages.UserMessage; 9 import org.springframework.ai.chat.messages.UserMessage;
8 import org.springframework.ai.chat.model.ChatResponse; 10 import org.springframework.ai.chat.model.ChatResponse;
9 import org.springframework.ai.chat.prompt.Prompt; 11 import org.springframework.ai.chat.prompt.Prompt;
10 -import org.springframework.ai.zhipuai.ZhiPuAiChatModel;  
11 -import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;  
12 -import org.springframework.ai.zhipuai.api.ZhiPuAiApi; 12 +import org.springframework.ai.deepseek.DeepSeekChatModel;
  13 +import org.springframework.ai.deepseek.DeepSeekChatOptions;
  14 +import org.springframework.ai.deepseek.api.DeepSeekApi;
13 import reactor.core.publisher.Flux; 15 import reactor.core.publisher.Flux;
14 16
15 import java.util.ArrayList; 17 import java.util.ArrayList;
16 import java.util.List; 18 import java.util.List;
  19 +import java.util.Objects;
  20 +
  21 +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
17 22
18 /** 23 /**
19 - * {@link ZhiPuAiChatModel} 的集成测试 24 + * {@link ZhiPuChatModel} 的集成测试
20 * 25 *
21 * @author 芋道源码 26 * @author 芋道源码
22 */ 27 */
23 public class ZhiPuAiChatModelTests { 28 public class ZhiPuAiChatModelTests {
24 29
25 - private final ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel(  
26 - ZhiPuAiApi.builder().apiKey("2f35fb6ca4ea41fab898729b7fac086c.6ESSfPcCkxaKEUlR").build(), // 密钥  
27 - ZhiPuAiChatOptions.builder()  
28 - .model(ZhiPuAiApi.ChatModel.GLM_4.getName()) // 模型 30 + private static final String API_KEY = SystemUtil.get("ZHIPU_API_KEY",
  31 + "sk-xxxx"); // 按需改成你的智谱 API Key
  32 + private static final String MODEL = SystemUtil.get("ZHIPU_MODEL",
  33 + ZhiPuChatModel.MODEL_DEFAULT);
  34 +
  35 + private final ZhiPuChatModel chatModel = new ZhiPuChatModel(DeepSeekChatModel.builder()
  36 + .deepSeekApi(DeepSeekApi.builder()
  37 + .baseUrl(ZhiPuChatModel.BASE_URL)
  38 + .apiKey(API_KEY)
  39 + .build())
  40 + .defaultOptions(DeepSeekChatOptions.builder()
  41 + .model(MODEL)
29 .build() 42 .build()
30 - ); 43 + ).build());
31 44
32 @Test 45 @Test
33 @Disabled 46 @Disabled
34 public void testCall() { 47 public void testCall() {
  48 + validateApiKey(API_KEY);
35 // 准备参数 49 // 准备参数
36 List<Message> messages = new ArrayList<>(); 50 List<Message> messages = new ArrayList<>();
37 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); 51 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -41,12 +55,13 @@ public class ZhiPuAiChatModelTests { @@ -41,12 +55,13 @@ public class ZhiPuAiChatModelTests {
41 ChatResponse response = chatModel.call(new Prompt(messages)); 55 ChatResponse response = chatModel.call(new Prompt(messages));
42 // 打印结果 56 // 打印结果
43 System.out.println(response); 57 System.out.println(response);
44 - System.out.println(response.getResult().getOutput()); 58 + System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
45 } 59 }
46 60
47 @Test 61 @Test
48 @Disabled 62 @Disabled
49 public void testStream() { 63 public void testStream() {
  64 + validateApiKey(API_KEY);
50 // 准备参数 65 // 准备参数
51 List<Message> messages = new ArrayList<>(); 66 List<Message> messages = new ArrayList<>();
52 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); 67 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -57,19 +72,19 @@ public class ZhiPuAiChatModelTests { @@ -57,19 +72,19 @@ public class ZhiPuAiChatModelTests {
57 // 打印结果 72 // 打印结果
58 flux.doOnNext(response -> { 73 flux.doOnNext(response -> {
59 // System.out.println(response); 74 // System.out.println(response);
60 - System.out.println(response.getResult().getOutput()); 75 + System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
61 }).then().block(); 76 }).then().block();
62 } 77 }
63 78
64 - // TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复  
65 @Test 79 @Test
66 @Disabled 80 @Disabled
67 public void testStream_thinking() { 81 public void testStream_thinking() {
  82 + validateApiKey(API_KEY);
68 // 准备参数 83 // 准备参数
69 List<Message> messages = new ArrayList<>(); 84 List<Message> messages = new ArrayList<>();
70 messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); 85 messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
71 - ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder()  
72 - .model("GLM-4.5") 86 + DeepSeekChatOptions options = DeepSeekChatOptions.builder()
  87 + .model(MODEL)
73 .build(); 88 .build();
74 89
75 // 调用 90 // 调用
@@ -77,7 +92,7 @@ public class ZhiPuAiChatModelTests { @@ -77,7 +92,7 @@ public class ZhiPuAiChatModelTests {
77 // 打印结果 92 // 打印结果
78 flux.doOnNext(response -> { 93 flux.doOnNext(response -> {
79 // System.out.println(response); 94 // System.out.println(response);
80 - System.out.println(response.getResult().getOutput()); 95 + System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
81 }).then().block(); 96 }).then().block();
82 } 97 }
83 98
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java
1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.image; 1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.image;
2 2
  3 +import cn.hutool.system.SystemUtil;
3 import org.junit.jupiter.api.Disabled; 4 import org.junit.jupiter.api.Disabled;
4 import org.junit.jupiter.api.Test; 5 import org.junit.jupiter.api.Test;
5 import org.springframework.ai.image.ImageOptions; 6 import org.springframework.ai.image.ImageOptions;
@@ -9,6 +10,8 @@ import org.springframework.ai.openai.OpenAiImageModel; @@ -9,6 +10,8 @@ import org.springframework.ai.openai.OpenAiImageModel;
9 import org.springframework.ai.openai.OpenAiImageOptions; 10 import org.springframework.ai.openai.OpenAiImageOptions;
10 import org.springframework.ai.openai.api.OpenAiImageApi; 11 import org.springframework.ai.openai.api.OpenAiImageApi;
11 12
  13 +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
  14 +
12 /** 15 /**
13 * {@link OpenAiImageModel} 集成测试类 16 * {@link OpenAiImageModel} 集成测试类
14 * 17 *
@@ -16,17 +19,20 @@ import org.springframework.ai.openai.api.OpenAiImageApi; @@ -16,17 +19,20 @@ import org.springframework.ai.openai.api.OpenAiImageApi;
16 */ 19 */
17 public class OpenAiImageModelTests { 20 public class OpenAiImageModelTests {
18 21
  22 + private static final String API_KEY = SystemUtil.get("OPENAI_API_KEY", "sk-xxxx");
  23 +
19 private final OpenAiImageModel imageModel = new OpenAiImageModel(OpenAiImageApi.builder() 24 private final OpenAiImageModel imageModel = new OpenAiImageModel(OpenAiImageApi.builder()
20 .baseUrl("https://api.holdai.top") // apiKey 25 .baseUrl("https://api.holdai.top") // apiKey
21 - .apiKey("sk-PytRecQlmjEteoa2RRN6cGnwslo72UUPLQVNEMS6K9yjbmpD") 26 + .apiKey(API_KEY)
22 .build()); 27 .build());
23 28
24 @Test 29 @Test
25 @Disabled 30 @Disabled
26 public void testCall() { 31 public void testCall() {
  32 + validateApiKey(API_KEY);
27 // 准备参数 33 // 准备参数
28 ImageOptions options = OpenAiImageOptions.builder() 34 ImageOptions options = OpenAiImageOptions.builder()
29 - .model(OpenAiImageApi.ImageModel.DALL_E_2.getValue()) // 这个模型比较便宜 35 + .model("gpt-image-1")
30 .height(256).width(256) 36 .height(256).width(256)
31 .build(); 37 .build();
32 ImagePrompt prompt = new ImagePrompt("中国长城!", options); 38 ImagePrompt prompt = new ImagePrompt("中国长城!", options);
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/QianFanImageTests.java deleted
1 -package cn.iocoder.yudao.module.ai.framework.ai.core.model.image;  
2 -  
3 -import org.junit.jupiter.api.Disabled;  
4 -import org.junit.jupiter.api.Test;  
5 -import org.springaicommunity.qianfan.QianFanImageModel;  
6 -import org.springaicommunity.qianfan.QianFanImageOptions;  
7 -import org.springaicommunity.qianfan.api.QianFanImageApi;  
8 -import org.springframework.ai.image.ImagePrompt;  
9 -import org.springframework.ai.image.ImageResponse;  
10 -  
11 -import static cn.iocoder.yudao.module.ai.framework.ai.core.model.image.StabilityAiImageModelTests.viewImage;  
12 -  
13 -// TODO @芋艿:百度千帆 API 提供了 V2 版本,目前 Spring AI 不兼容,可关键 <https://github.com/spring-projects/spring-ai/issues/2179> 进展  
14 -  
15 -/**  
16 - * {@link QianFanImageModel} 集成测试类  
17 - */  
18 -public class QianFanImageTests {  
19 -  
20 - private final QianFanImageModel imageModel = new QianFanImageModel(  
21 - new QianFanImageApi("qS8k8dYr2nXunagK4SSU8Xjj", "pHGbx51ql2f0hOyabQvSZezahVC3hh3e")); // 密钥  
22 -  
23 - @Test  
24 - @Disabled  
25 - public void testCall() {  
26 - // 准备参数  
27 - // 只支持 1024x1024、768x768、768x1024、1024x768、576x1024、1024x576  
28 - QianFanImageOptions imageOptions = QianFanImageOptions.builder()  
29 - .model(QianFanImageApi.ImageModel.Stable_Diffusion_XL.getValue())  
30 - .width(1024).height(1024)  
31 - .N(1)  
32 - .build();  
33 - ImagePrompt prompt = new ImagePrompt("good", imageOptions);  
34 -  
35 - // 方法调用  
36 - ImageResponse response = imageModel.call(prompt);  
37 - // 打印结果  
38 - String b64Json = response.getResult().getOutput().getB64Json();  
39 - System.out.println(response);  
40 - viewImage(b64Json);  
41 - }  
42 -  
43 -}  
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/ZhiPuAiImageModelTests.java deleted
1 -package cn.iocoder.yudao.module.ai.framework.ai.core.model.image;  
2 -  
3 -import org.junit.jupiter.api.Disabled;  
4 -import org.junit.jupiter.api.Test;  
5 -import org.springframework.ai.image.ImagePrompt;  
6 -import org.springframework.ai.image.ImageResponse;  
7 -import org.springframework.ai.zhipuai.ZhiPuAiImageModel;  
8 -import org.springframework.ai.zhipuai.ZhiPuAiImageOptions;  
9 -import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi;  
10 -  
11 -/**  
12 - * {@link ZhiPuAiImageModel} 集成测试  
13 - */  
14 -public class ZhiPuAiImageModelTests {  
15 -  
16 - private final ZhiPuAiImageModel imageModel = new ZhiPuAiImageModel(  
17 - new ZhiPuAiImageApi("78d3228c1d9e5e342a3e1ab349e2dd7b.VXLoq5vrwK2ofboy") // 密钥  
18 - );  
19 -  
20 - @Test  
21 - @Disabled  
22 - public void testCall() {  
23 - // 准备参数  
24 - ZhiPuAiImageOptions imageOptions = ZhiPuAiImageOptions.builder()  
25 - .model(ZhiPuAiImageApi.ImageModel.CogView_3.getValue())  
26 - .build();  
27 - ImagePrompt prompt = new ImagePrompt("万里长城", imageOptions);  
28 -  
29 - // 方法调用  
30 - ImageResponse response = imageModel.call(prompt);  
31 - // 打印结果  
32 - System.out.println(response);  
33 - }  
34 -  
35 -}  
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java
1 package cn.iocoder.yudao.module.bpm.controller.admin.definition; 1 package cn.iocoder.yudao.module.bpm.controller.admin.definition;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.StrUtil;
4 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 5 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  6 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  7 +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
5 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.*; 8 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.*;
6 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; 9 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
7 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelUpdateReqVO; 10 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; @@ -18,6 +21,7 @@ import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
18 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; 21 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
19 import io.swagger.v3.oas.annotations.Operation; 22 import io.swagger.v3.oas.annotations.Operation;
20 import io.swagger.v3.oas.annotations.Parameter; 23 import io.swagger.v3.oas.annotations.Parameter;
  24 +import io.swagger.v3.oas.annotations.Parameters;
21 import io.swagger.v3.oas.annotations.tags.Tag; 25 import io.swagger.v3.oas.annotations.tags.Tag;
22 import jakarta.annotation.Resource; 26 import jakarta.annotation.Resource;
23 import jakarta.validation.Valid; 27 import jakarta.validation.Valid;
@@ -27,16 +31,20 @@ import org.flowable.engine.repository.ProcessDefinition; @@ -27,16 +31,20 @@ import org.flowable.engine.repository.ProcessDefinition;
27 import org.springframework.security.access.prepost.PreAuthorize; 31 import org.springframework.security.access.prepost.PreAuthorize;
28 import org.springframework.validation.annotation.Validated; 32 import org.springframework.validation.annotation.Validated;
29 import org.springframework.web.bind.annotation.*; 33 import org.springframework.web.bind.annotation.*;
  34 +import org.springframework.web.multipart.MultipartFile;
30 35
  36 +import java.io.IOException;
31 import java.util.Collections; 37 import java.util.Collections;
32 import java.util.List; 38 import java.util.List;
33 import java.util.Map; 39 import java.util.Map;
34 import java.util.Set; 40 import java.util.Set;
35 import java.util.stream.Stream; 41 import java.util.stream.Stream;
36 42
  43 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
37 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; 44 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
38 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; 45 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
39 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; 46 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
  47 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.MODEL_IMPORT_FAIL_INVALID;
40 48
41 @Tag(name = "管理后台 - 流程模型") 49 @Tag(name = "管理后台 - 流程模型")
42 @RestController 50 @RestController
@@ -112,6 +120,14 @@ public class BpmModelController { @@ -112,6 +120,14 @@ public class BpmModelController {
112 return success(BpmModelConvert.INSTANCE.buildModel(model, bpmnBytes, simpleModel)); 120 return success(BpmModelConvert.INSTANCE.buildModel(model, bpmnBytes, simpleModel));
113 } 121 }
114 122
  123 + @GetMapping("/export")
  124 + @Operation(summary = "导出模型")
  125 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  126 + @PreAuthorize("@ss.hasPermission('bpm:model:export')")
  127 + public CommonResult<BpmModelSaveReqVO> exportModel(@RequestParam("id") String id) {
  128 + return success(modelService.exportModel(id));
  129 + }
  130 +
115 @PostMapping("/create") 131 @PostMapping("/create")
116 @Operation(summary = "新建模型") 132 @Operation(summary = "新建模型")
117 @PreAuthorize("@ss.hasPermission('bpm:model:create')") 133 @PreAuthorize("@ss.hasPermission('bpm:model:create')")
@@ -119,6 +135,36 @@ public class BpmModelController { @@ -119,6 +135,36 @@ public class BpmModelController {
119 return success(modelService.createModel(createRetVO)); 135 return success(modelService.createModel(createRetVO));
120 } 136 }
121 137
  138 + @PostMapping("/import")
  139 + @Operation(summary = "导入模型")
  140 + @Parameters({
  141 + @Parameter(name = "file", description = "流程模型 JSON 文件", required = true),
  142 + @Parameter(name = "key", description = "流程标识,替换导入文件中的标识", example = "process_yudao"),
  143 + @Parameter(name = "name", description = "流程名称,替换导入文件中的名称", example = "芋道")
  144 + })
  145 + @PreAuthorize("@ss.hasPermission('bpm:model:import')")
  146 + public CommonResult<String> importModel(@RequestParam("file") MultipartFile file,
  147 + @RequestParam(value = "key", required = false) String key,
  148 + @RequestParam(value = "name", required = false) String name) throws IOException {
  149 + BpmModelSaveReqVO reqVO;
  150 + try {
  151 + reqVO = JsonUtils.parseObject(file.getBytes(), BpmModelSaveReqVO.class);
  152 + } catch (RuntimeException ex) {
  153 + throw exception(MODEL_IMPORT_FAIL_INVALID);
  154 + }
  155 + if (reqVO == null) {
  156 + throw exception(MODEL_IMPORT_FAIL_INVALID);
  157 + }
  158 + if (StrUtil.isNotEmpty(key)) {
  159 + reqVO.setKey(key);
  160 + }
  161 + if (StrUtil.isNotEmpty(name)) {
  162 + reqVO.setName(name);
  163 + }
  164 + ValidationUtils.validate(reqVO);
  165 + return success(modelService.importModel(reqVO));
  166 + }
  167 +
122 @PutMapping("/update") 168 @PutMapping("/update")
123 @Operation(summary = "修改模型") 169 @Operation(summary = "修改模型")
124 @PreAuthorize("@ss.hasPermission('bpm:model:update')") 170 @PreAuthorize("@ss.hasPermission('bpm:model:update')")
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java
@@ -111,6 +111,18 @@ public interface BpmModelConvert { @@ -111,6 +111,18 @@ public interface BpmModelConvert {
111 model.setMetaInfo(JsonUtils.toJsonString(BeanUtils.toBean(reqVO, BpmModelMetaInfoVO.class))); 111 model.setMetaInfo(JsonUtils.toJsonString(BeanUtils.toBean(reqVO, BpmModelMetaInfoVO.class)));
112 } 112 }
113 113
  114 + default BpmModelSaveReqVO convertToSaveReqVO(Model model) {
  115 + BpmModelMetaInfoVO metaInfo = parseMetaInfo(model);
  116 + BpmModelSaveReqVO reqVO = new BpmModelSaveReqVO();
  117 + reqVO.setKey(model.getKey());
  118 + reqVO.setName(model.getName());
  119 + reqVO.setCategory(model.getCategory());
  120 + if (metaInfo != null) {
  121 + BeanUtils.copyProperties(metaInfo, reqVO);
  122 + }
  123 + return reqVO;
  124 + }
  125 +
114 default BpmModelMetaInfoVO parseMetaInfo(Model model) { 126 default BpmModelMetaInfoVO parseMetaInfo(Model model) {
115 BpmModelMetaInfoVO vo = JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoVO.class); 127 BpmModelMetaInfoVO vo = JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoVO.class);
116 if (vo == null) { 128 if (vo == null) {
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java
@@ -25,6 +25,7 @@ public interface ErrorCodeConstants { @@ -25,6 +25,7 @@ public interface ErrorCodeConstants {
25 ErrorCode MODEL_DEPLOY_FAIL_BPMN_USER_TASK_NAME_NOT_EXISTS = new ErrorCode(1_009_002_006, "部署流程失败,原因:BPMN 流程图中,用户任务({})的名字不存在"); 25 ErrorCode MODEL_DEPLOY_FAIL_BPMN_USER_TASK_NAME_NOT_EXISTS = new ErrorCode(1_009_002_006, "部署流程失败,原因:BPMN 流程图中,用户任务({})的名字不存在");
26 ErrorCode MODEL_UPDATE_FAIL_NOT_MANAGER = new ErrorCode(1_009_002_007, "操作流程失败,原因:你不是该流程({})的管理员"); 26 ErrorCode MODEL_UPDATE_FAIL_NOT_MANAGER = new ErrorCode(1_009_002_007, "操作流程失败,原因:你不是该流程({})的管理员");
27 ErrorCode MODEL_DEPLOY_FAIL_FIRST_USER_TASK_CANDIDATE_STRATEGY_ERROR = new ErrorCode(1_009_002_008, "部署流程失败,原因:首个任务({})的审批人不能是【审批人自选】"); 27 ErrorCode MODEL_DEPLOY_FAIL_FIRST_USER_TASK_CANDIDATE_STRATEGY_ERROR = new ErrorCode(1_009_002_008, "部署流程失败,原因:首个任务({})的审批人不能是【审批人自选】");
  28 + ErrorCode MODEL_IMPORT_FAIL_INVALID = new ErrorCode(1_009_002_009, "导入流程失败,流程模型文件为空或 JSON 格式不正确");
28 29
29 // ========== 流程定义 1-009-003-000 ========== 30 // ========== 流程定义 1-009-003-000 ==========
30 ErrorCode PROCESS_DEFINITION_KEY_NOT_MATCH = new ErrorCode(1_009_003_000, "流程定义的标识期望是({}),当前是({}),请修改 BPMN 流程图"); 31 ErrorCode PROCESS_DEFINITION_KEY_NOT_MATCH = new ErrorCode(1_009_003_000, "流程定义的标识期望是({}),当前是({}),请修改 BPMN 流程图");
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java
@@ -41,6 +41,22 @@ public interface BpmModelService { @@ -41,6 +41,22 @@ public interface BpmModelService {
41 String createModel(@Valid BpmModelSaveReqVO modelVO); 41 String createModel(@Valid BpmModelSaveReqVO modelVO);
42 42
43 /** 43 /**
  44 + * 导入流程模型
  45 + *
  46 + * @param reqVO 导入信息
  47 + * @return 创建的流程模型的编号
  48 + */
  49 + String importModel(@Valid BpmModelSaveReqVO reqVO);
  50 +
  51 + /**
  52 + * 导出流程模型
  53 + *
  54 + * @param id 编号
  55 + * @return 导出的流程模型
  56 + */
  57 + BpmModelSaveReqVO exportModel(String id);
  58 +
  59 + /**
44 * 获得流程模块 60 * 获得流程模块
45 * 61 *
46 * @param id 编号 62 * @param id 编号
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java
@@ -120,6 +120,42 @@ public class BpmModelServiceImpl implements BpmModelService { @@ -120,6 +120,42 @@ public class BpmModelServiceImpl implements BpmModelService {
120 } 120 }
121 121
122 @Override 122 @Override
  123 + @Transactional(rollbackFor = Exception.class)
  124 + public String importModel(@Valid BpmModelSaveReqVO reqVO) {
  125 + if (!ValidationUtils.isXmlNCName(reqVO.getKey())) {
  126 + throw exception(MODEL_KEY_VALID);
  127 + }
  128 + // 1. 校验流程标识已经存在
  129 + Model keyModel = getModelByKey(reqVO.getKey());
  130 + if (keyModel != null) {
  131 + throw exception(MODEL_KEY_EXISTS, reqVO.getKey());
  132 + }
  133 +
  134 + // 2. 创建 Model 对象,并归属到当前租户
  135 + reqVO.setSort(System.currentTimeMillis());
  136 + Model model = repositoryService.newModel();
  137 + BpmModelConvert.INSTANCE.copyToModel(model, reqVO);
  138 + model.setTenantId(FlowableUtils.getTenantId());
  139 +
  140 + // 3. 保存模型
  141 + saveModel(model, reqVO);
  142 + return model.getId();
  143 + }
  144 +
  145 + @Override
  146 + public BpmModelSaveReqVO exportModel(String id) {
  147 + Model model = validateModelExists(id);
  148 + BpmModelSaveReqVO result = BpmModelConvert.INSTANCE.convertToSaveReqVO(model);
  149 + if (Objects.equals(result.getType(), BpmModelTypeEnum.BPMN.getType())) {
  150 + byte[] bpmnBytes = getModelBpmnXML(id);
  151 + result.setBpmnXml(ArrayUtil.isEmpty(bpmnBytes) ? null : StrUtil.utf8Str(bpmnBytes));
  152 + } else if (Objects.equals(result.getType(), BpmModelTypeEnum.SIMPLE.getType())) {
  153 + result.setSimpleModel(JsonUtils.parseObject(getModelSimpleJson(id), BpmSimpleModelNodeVO.class));
  154 + }
  155 + return result;
  156 + }
  157 +
  158 + @Override
123 @Transactional(rollbackFor = Exception.class) // 因为进行多个操作,所以开启事务 159 @Transactional(rollbackFor = Exception.class) // 因为进行多个操作,所以开启事务
124 public void updateModel(Long userId, BpmModelSaveReqVO updateReqVO) { 160 public void updateModel(Long userId, BpmModelSaveReqVO updateReqVO) {
125 // 1. 校验流程模型存在 161 // 1. 校验流程模型存在
@@ -187,7 +223,8 @@ public class BpmModelServiceImpl implements BpmModelService { @@ -187,7 +223,8 @@ public class BpmModelServiceImpl implements BpmModelService {
187 } 223 }
188 224
189 private Model validateModelExists(String id) { 225 private Model validateModelExists(String id) {
190 - Model model = repositoryService.getModel(id); 226 + Model model = repositoryService.createModelQuery()
  227 + .modelId(id).modelTenantId(FlowableUtils.getTenantId()).singleResult();
191 if (model == null) { 228 if (model == null) {
192 throw exception(MODEL_NOT_EXISTS); 229 throw exception(MODEL_NOT_EXISTS);
193 } 230 }
@@ -429,7 +466,8 @@ public class BpmModelServiceImpl implements BpmModelService { @@ -429,7 +466,8 @@ public class BpmModelServiceImpl implements BpmModelService {
429 466
430 @Override 467 @Override
431 public Model getModel(String id) { 468 public Model getModel(String id) {
432 - return repositoryService.getModel(id); 469 + return repositoryService.createModelQuery()
  470 + .modelId(id).modelTenantId(FlowableUtils.getTenantId()).singleResult();
433 } 471 }
434 472
435 @Override 473 @Override
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.service.definition;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  5 +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
  6 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  7 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
  8 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelSaveReqVO;
  9 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
  10 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
  11 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum;
  12 +import org.flowable.engine.RepositoryService;
  13 +import org.flowable.engine.repository.Model;
  14 +import org.flowable.engine.repository.ModelQuery;
  15 +import org.junit.jupiter.api.AfterEach;
  16 +import org.junit.jupiter.api.BeforeEach;
  17 +import org.junit.jupiter.api.Test;
  18 +import org.mockito.ArgumentCaptor;
  19 +import org.mockito.InjectMocks;
  20 +import org.mockito.Mock;
  21 +
  22 +import java.util.Arrays;
  23 +import java.util.Collections;
  24 +import java.util.Date;
  25 +
  26 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
  27 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.MODEL_NOT_EXISTS;
  28 +import static org.junit.jupiter.api.Assertions.*;
  29 +import static org.mockito.AdditionalMatchers.aryEq;
  30 +import static org.mockito.ArgumentMatchers.eq;
  31 +import static org.mockito.Mockito.*;
  32 +
  33 +/**
  34 + * {@link BpmModelServiceImpl} 的单元测试
  35 + *
  36 + * @author 芋道源码
  37 + */
  38 +public class BpmModelServiceImplTest extends BaseMockitoUnitTest {
  39 +
  40 + private static final Long TENANT_ID = 1L;
  41 + private static final String MODEL_ID = "model-id";
  42 +
  43 + @InjectMocks
  44 + private BpmModelServiceImpl modelService;
  45 +
  46 + @Mock
  47 + private RepositoryService repositoryService;
  48 + @Mock
  49 + private ModelQuery modelQuery;
  50 +
  51 + @BeforeEach
  52 + public void setUp() {
  53 + TenantContextHolder.setTenantId(TENANT_ID);
  54 + }
  55 +
  56 + @AfterEach
  57 + public void tearDown() {
  58 + TenantContextHolder.clear();
  59 + }
  60 +
  61 + @Test
  62 + public void testExportModel_bpmn() {
  63 + // 准备参数
  64 + Model model = mockModel(BpmModelTypeEnum.BPMN.getType());
  65 + String bpmnXml = "<definitions />";
  66 + // mock 方法(repositoryService)
  67 + mockGetModel(model);
  68 + when(repositoryService.getModelEditorSource(eq(MODEL_ID))).thenReturn(StrUtil.utf8Bytes(bpmnXml));
  69 +
  70 + // 调用
  71 + BpmModelSaveReqVO result = modelService.exportModel(MODEL_ID);
  72 +
  73 + // 断言
  74 + assertEquals(model.getKey(), result.getKey());
  75 + assertEquals(model.getName(), result.getName());
  76 + assertEquals(model.getCategory(), result.getCategory());
  77 + assertEquals(BpmModelTypeEnum.BPMN.getType(), result.getType());
  78 + assertEquals(bpmnXml, result.getBpmnXml());
  79 + assertNull(result.getSimpleModel());
  80 + }
  81 +
  82 + @Test
  83 + public void testExportModel_simple() {
  84 + // 准备参数
  85 + Model model = mockModel(BpmModelTypeEnum.SIMPLE.getType());
  86 + BpmSimpleModelNodeVO simpleModel = new BpmSimpleModelNodeVO();
  87 + simpleModel.setId("start");
  88 + simpleModel.setType(BpmSimpleModelNodeTypeEnum.START_NODE.getType());
  89 + // mock 方法(repositoryService)
  90 + mockGetModel(model);
  91 + when(repositoryService.getModelEditorSourceExtra(eq(MODEL_ID)))
  92 + .thenReturn(JsonUtils.toJsonByte(simpleModel));
  93 +
  94 + // 调用
  95 + BpmModelSaveReqVO result = modelService.exportModel(MODEL_ID);
  96 +
  97 + // 断言
  98 + assertEquals(BpmModelTypeEnum.SIMPLE.getType(), result.getType());
  99 + assertNotNull(result.getSimpleModel());
  100 + assertEquals(simpleModel.getId(), result.getSimpleModel().getId());
  101 + assertEquals(simpleModel.getType(), result.getSimpleModel().getType());
  102 + assertNull(result.getBpmnXml());
  103 + }
  104 +
  105 + @Test
  106 + public void testExportModel_notExists() {
  107 + // mock 方法(repositoryService)
  108 + when(repositoryService.createModelQuery()).thenReturn(modelQuery);
  109 + when(modelQuery.modelId(eq(MODEL_ID))).thenReturn(modelQuery);
  110 + when(modelQuery.modelTenantId(eq(TENANT_ID.toString()))).thenReturn(modelQuery);
  111 + when(modelQuery.singleResult()).thenReturn(null);
  112 +
  113 + // 调用,并断言异常
  114 + assertServiceException(() -> modelService.exportModel(MODEL_ID), MODEL_NOT_EXISTS);
  115 + }
  116 +
  117 + @Test
  118 + public void testImportModel_bpmn() {
  119 + // 准备参数
  120 + BpmModelSaveReqVO reqVO = new BpmModelSaveReqVO();
  121 + reqVO.setKey("test_process");
  122 + reqVO.setName("测试流程");
  123 + reqVO.setCategory("OA");
  124 + reqVO.setType(BpmModelTypeEnum.BPMN.getType());
  125 + reqVO.setBpmnXml("<definitions />");
  126 + reqVO.setStartUserIds(Arrays.asList(10L, 20L));
  127 + reqVO.setStartDeptIds(Collections.singletonList(30L));
  128 + reqVO.setManagerUserIds(Collections.singletonList(40L));
  129 + Model model = mock(Model.class);
  130 + when(model.getId()).thenReturn(MODEL_ID);
  131 + // mock 方法(repositoryService)
  132 + when(repositoryService.createModelQuery()).thenReturn(modelQuery);
  133 + when(modelQuery.modelTenantId(eq(TENANT_ID.toString()))).thenReturn(modelQuery);
  134 + when(modelQuery.modelKey(eq(reqVO.getKey()))).thenReturn(modelQuery);
  135 + when(modelQuery.singleResult()).thenReturn(null);
  136 + when(repositoryService.newModel()).thenReturn(model);
  137 +
  138 + // 调用
  139 + String result = modelService.importModel(reqVO);
  140 +
  141 + // 断言
  142 + assertEquals(MODEL_ID, result);
  143 + verify(model).setTenantId(TENANT_ID.toString());
  144 + verify(repositoryService).saveModel(same(model));
  145 + verify(repositoryService).addModelEditorSource(eq(MODEL_ID),
  146 + aryEq(StrUtil.utf8Bytes(reqVO.getBpmnXml())));
  147 + ArgumentCaptor<String> metaInfoCaptor = ArgumentCaptor.forClass(String.class);
  148 + verify(model).setMetaInfo(metaInfoCaptor.capture());
  149 + BpmModelMetaInfoVO metaInfo = JsonUtils.parseObject(metaInfoCaptor.getValue(), BpmModelMetaInfoVO.class);
  150 + assertNotNull(metaInfo);
  151 + assertEquals(Arrays.asList(10L, 20L), metaInfo.getStartUserIds());
  152 + assertEquals(Collections.singletonList(30L), metaInfo.getStartDeptIds());
  153 + assertEquals(Collections.singletonList(40L), metaInfo.getManagerUserIds());
  154 + }
  155 +
  156 + private Model mockModel(Integer type) {
  157 + Model model = mock(Model.class);
  158 + when(model.getKey()).thenReturn("test_process");
  159 + when(model.getName()).thenReturn("测试流程");
  160 + when(model.getCategory()).thenReturn("OA");
  161 + when(model.getCreateTime()).thenReturn(new Date(1_000L));
  162 + BpmModelMetaInfoVO metaInfo = new BpmModelMetaInfoVO();
  163 + metaInfo.setType(type);
  164 + when(model.getMetaInfo()).thenReturn(JsonUtils.toJsonString(metaInfo));
  165 + return model;
  166 + }
  167 +
  168 + private void mockGetModel(Model model) {
  169 + when(repositoryService.createModelQuery()).thenReturn(modelQuery);
  170 + when(modelQuery.modelId(eq(MODEL_ID))).thenReturn(modelQuery);
  171 + when(modelQuery.modelTenantId(eq(TENANT_ID.toString()))).thenReturn(modelQuery);
  172 + when(modelQuery.singleResult()).thenReturn(model);
  173 + }
  174 +
  175 +}
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; @@ -23,6 +23,8 @@ import org.springframework.security.access.prepost.PreAuthorize;
23 import org.springframework.validation.annotation.Validated; 23 import org.springframework.validation.annotation.Validated;
24 import org.springframework.web.bind.annotation.*; 24 import org.springframework.web.bind.annotation.*;
25 25
  26 +import java.util.Collections;
  27 +import java.util.List;
26 import java.util.Map; 28 import java.util.Map;
27 29
28 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; 30 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -62,19 +64,42 @@ public class ImChannelMessageManagerController { @@ -62,19 +64,42 @@ public class ImChannelMessageManagerController {
62 @PreAuthorize("@ss.hasPermission('im:manager:channel-message:query')") 64 @PreAuthorize("@ss.hasPermission('im:manager:channel-message:query')")
63 public CommonResult<PageResult<ImChannelMessageRespVO>> getMessagePage(@Valid ImChannelMessagePageReqVO pageReqVO) { 65 public CommonResult<PageResult<ImChannelMessageRespVO>> getMessagePage(@Valid ImChannelMessagePageReqVO pageReqVO) {
64 PageResult<ImChannelMessageDO> pageResult = channelMessageService.getMessagePage(pageReqVO); 66 PageResult<ImChannelMessageDO> pageResult = channelMessageService.getMessagePage(pageReqVO);
65 - if (CollUtil.isEmpty(pageResult.getList())) {  
66 - return success(PageResult.empty(pageResult.getTotal())); 67 + return success(new PageResult<>(buildMessageRespVOList(pageResult.getList()), pageResult.getTotal()));
  68 + }
  69 +
  70 + @GetMapping("/get")
  71 + @Operation(summary = "获得频道消息详情")
  72 + @Parameter(name = "id", description = "消息编号", required = true, example = "1024")
  73 + @PreAuthorize("@ss.hasPermission('im:manager:channel-message:query')")
  74 + public CommonResult<ImChannelMessageRespVO> getMessage(@RequestParam("id") Long id) {
  75 + ImChannelMessageDO message = channelMessageService.getMessage(id);
  76 + return success(buildMessageRespVO(message));
  77 + }
  78 +
  79 + // ==================== 拼接 VO ====================
  80 +
  81 + private ImChannelMessageRespVO buildMessageRespVO(ImChannelMessageDO message) {
  82 + if (message == null) {
  83 + return null;
  84 + }
  85 + return CollUtil.getFirst(buildMessageRespVOList(Collections.singletonList(message)));
  86 + }
  87 +
  88 + private List<ImChannelMessageRespVO> buildMessageRespVOList(List<ImChannelMessageDO> messages) {
  89 + if (CollUtil.isEmpty(messages)) {
  90 + return Collections.emptyList();
67 } 91 }
68 - // 批量查询频道和素材,并回填频道名 / 素材标题 92 + // 1. 查询关联数据
69 Map<Long, ImChannelDO> channelMap = channelService.getChannelMap( 93 Map<Long, ImChannelDO> channelMap = channelService.getChannelMap(
70 - convertSet(pageResult.getList(), ImChannelMessageDO::getChannelId)); 94 + convertSet(messages, ImChannelMessageDO::getChannelId));
71 Map<Long, ImChannelMaterialDO> materialMap = channelMaterialService.getMaterialMap( 95 Map<Long, ImChannelMaterialDO> materialMap = channelMaterialService.getMaterialMap(
72 - convertSet(pageResult.getList(), ImChannelMessageDO::getMaterialId));  
73 - return success(BeanUtils.toBean(pageResult, ImChannelMessageRespVO.class, vo -> {  
74 - MapUtils.findAndThen(channelMap, vo.getChannelId(), c -> vo.setChannelName(c.getName())); 96 + convertSet(messages, ImChannelMessageDO::getMaterialId));
  97 + // 2. 拼接 VO
  98 + return BeanUtils.toBean(messages, ImChannelMessageRespVO.class, vo -> {
  99 + MapUtils.findAndThen(channelMap, vo.getChannelId(), channel -> vo.setChannelName(channel.getName()));
75 MapUtils.findAndThen(materialMap, vo.getMaterialId(), 100 MapUtils.findAndThen(materialMap, vo.getMaterialId(),
76 material -> vo.setMaterialTitle(material.getTitle()).setMaterialCoverUrl(material.getCoverUrl())); 101 material -> vo.setMaterialTitle(material.getTitle()).setMaterialCoverUrl(material.getCoverUrl()));
77 - })); 102 + });
78 } 103 }
79 104
80 } 105 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/framework/rtc/core/LiveKitClient.java
@@ -208,17 +208,21 @@ public class LiveKitClient { @@ -208,17 +208,21 @@ public class LiveKitClient {
208 /** 208 /**
209 * 签发管理 Token;用于调 Server API(DeleteRoom / ListParticipants / RemoveParticipant 等) 209 * 签发管理 Token;用于调 Server API(DeleteRoom / ListParticipants / RemoveParticipant 等)
210 * 210 *
  211 + * @param room 房间名;LiveKit 的 roomAdmin 权限必须限定到具体房间
211 * @return JWT 字符串 212 * @return JWT 字符串
212 */ 213 */
213 - private String signAdminToken() { 214 + private String signAdminToken(String room) {
214 ImProperties.Rtc cfg = imProperties.getRtc(); 215 ImProperties.Rtc cfg = imProperties.getRtc();
215 // roomAdmin claim 给管理类 API 必备 216 // roomAdmin claim 给管理类 API 必备
  217 + Map<String, Object> video = new HashMap<>();
  218 + video.put("roomAdmin", true);
  219 + video.put("room", room);
216 long nowSec = Instant.now().getEpochSecond(); 220 long nowSec = Instant.now().getEpochSecond();
217 return JWT.create() 221 return JWT.create()
218 .setIssuer(cfg.getApiKey()) 222 .setIssuer(cfg.getApiKey())
219 .setNotBefore(new Date(nowSec * 1000)) 223 .setNotBefore(new Date(nowSec * 1000))
220 .setExpiresAt(new Date((nowSec + ADMIN_TOKEN_TTL.getSeconds()) * 1000)) 224 .setExpiresAt(new Date((nowSec + ADMIN_TOKEN_TTL.getSeconds()) * 1000))
221 - .setPayload("video", MapUtil.of("roomAdmin", true)) 225 + .setPayload("video", video)
222 .setSigner(JWTSignerUtil.hs256(cfg.getApiSecret().getBytes(StandardCharsets.UTF_8))) 226 .setSigner(JWTSignerUtil.hs256(cfg.getApiSecret().getBytes(StandardCharsets.UTF_8)))
223 .sign(); 227 .sign();
224 } 228 }
@@ -232,7 +236,7 @@ public class LiveKitClient { @@ -232,7 +236,7 @@ public class LiveKitClient {
232 */ 236 */
233 private HttpResponse postTwirp(String path, String room) { 237 private HttpResponse postTwirp(String path, String room) {
234 Assert.notBlank(room, "room 不可为空"); 238 Assert.notBlank(room, "room 不可为空");
235 - String token = signAdminToken(); 239 + String token = signAdminToken(room);
236 return HttpRequest.post(HttpUtils.wsUrlToHttp(imProperties.getRtc().getLivekitUrl()) + path) 240 return HttpRequest.post(HttpUtils.wsUrlToHttp(imProperties.getRtc().getLivekitUrl()) + path)
237 .header("Authorization", "Bearer " + token) 241 .header("Authorization", "Bearer " + token)
238 .header("Content-Type", "application/json") 242 .header("Content-Type", "application/json")
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImChannelMessageService.java
@@ -66,6 +66,14 @@ public interface ImChannelMessageService { @@ -66,6 +66,14 @@ public interface ImChannelMessageService {
66 PageResult<ImChannelMessageDO> getMessagePage(ImChannelMessagePageReqVO reqVO); 66 PageResult<ImChannelMessageDO> getMessagePage(ImChannelMessagePageReqVO reqVO);
67 67
68 /** 68 /**
  69 + * 获得频道消息
  70 + *
  71 + * @param id 消息编号
  72 + * @return 频道消息
  73 + */
  74 + ImChannelMessageDO getMessage(Long id);
  75 +
  76 + /**
69 * 删除消息 77 * 删除消息
70 * 78 *
71 * @param id 消息编号 79 * @param id 消息编号
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImChannelMessageServiceImpl.java
@@ -138,6 +138,11 @@ public class ImChannelMessageServiceImpl implements ImChannelMessageService { @@ -138,6 +138,11 @@ public class ImChannelMessageServiceImpl implements ImChannelMessageService {
138 } 138 }
139 139
140 @Override 140 @Override
  141 + public ImChannelMessageDO getMessage(Long id) {
  142 + return channelMessageMapper.selectById(id);
  143 + }
  144 +
  145 + @Override
141 public void deleteMessage(Long id) { 146 public void deleteMessage(Long id) {
142 if (channelMessageMapper.selectById(id) == null) { 147 if (channelMessageMapper.selectById(id) == null) {
143 throw exception(IM_CHANNEL_MESSAGE_NOT_EXISTS); 148 throw exception(IM_CHANNEL_MESSAGE_NOT_EXISTS);
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java
@@ -48,6 +48,10 @@ public interface ErrorCodeConstants { @@ -48,6 +48,10 @@ public interface ErrorCodeConstants {
48 ErrorCode CODEGEN_MASTER_TABLE_NOT_EXISTS = new ErrorCode(1_001_004_010, "主表(id={})定义不存在,请检查"); 48 ErrorCode CODEGEN_MASTER_TABLE_NOT_EXISTS = new ErrorCode(1_001_004_010, "主表(id={})定义不存在,请检查");
49 ErrorCode CODEGEN_SUB_COLUMN_NOT_EXISTS = new ErrorCode(1_001_004_011, "子表的字段(id={})不存在,请检查"); 49 ErrorCode CODEGEN_SUB_COLUMN_NOT_EXISTS = new ErrorCode(1_001_004_011, "子表的字段(id={})不存在,请检查");
50 ErrorCode CODEGEN_MASTER_GENERATION_FAIL_NO_SUB_TABLE = new ErrorCode(1_001_004_012, "主表生成代码失败,原因:它没有子表"); 50 ErrorCode CODEGEN_MASTER_GENERATION_FAIL_NO_SUB_TABLE = new ErrorCode(1_001_004_012, "主表生成代码失败,原因:它没有子表");
  51 + ErrorCode CODEGEN_MASTER_TABLE_NAME_DUPLICATE = new ErrorCode(1_001_004_013,
  52 + "主子表规范化类名({})重复,请调整主表或子表类名");
  53 + ErrorCode CODEGEN_MASTER_TABLE_FIELD_DUPLICATE = new ErrorCode(1_001_004_014,
  54 + "主子表属性名({})重复,请调整主表字段、子表类名或关联关系");
51 55
52 // ========== 文件配置 1-001-006-000 ========== 56 // ========== 文件配置 1-001-006-000 ==========
53 ErrorCode FILE_CONFIG_NOT_EXISTS = new ErrorCode(1_001_006_000, "文件配置不存在"); 57 ErrorCode FILE_CONFIG_NOT_EXISTS = new ErrorCode(1_001_006_000, "文件配置不存在");
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java
@@ -48,6 +48,9 @@ import java.util.*; @@ -48,6 +48,9 @@ import java.util.*;
48 48
49 import static cn.hutool.core.map.MapUtil.getStr; 49 import static cn.hutool.core.map.MapUtil.getStr;
50 import static cn.hutool.core.text.CharSequenceUtil.*; 50 import static cn.hutool.core.text.CharSequenceUtil.*;
  51 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  52 +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.CODEGEN_MASTER_TABLE_FIELD_DUPLICATE;
  53 +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.CODEGEN_MASTER_TABLE_NAME_DUPLICATE;
51 54
52 /** 55 /**
53 * 代码生成的引擎,用于具体生成代码 56 * 代码生成的引擎,用于具体生成代码
@@ -152,6 +155,16 @@ public class CodegenEngine { @@ -152,6 +155,16 @@ public class CodegenEngine {
152 vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/form/index.vue")) 155 vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/form/index.vue"))
153 .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/detail/index.vue"), 156 .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/detail/index.vue"),
154 vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/detail/index.vue")) 157 vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/detail/index.vue"))
  158 + .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/index_tree.vue"),
  159 + vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/index.vue"))
  160 + .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("components/breadcrumb_tree.vue"),
  161 + vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/components/breadcrumb.vue"))
  162 + .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/detail_master.vue"),
  163 + vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/detail/index.vue"))
  164 + .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/detail_erp.vue"),
  165 + vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/detail/index.vue"))
  166 + .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/form_sub_erp.vue"),
  167 + vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/${subSimpleClassName_strikeCase}/form/index.vue"))
155 // VUE3_VBEN2_ANTD_SCHEMA 168 // VUE3_VBEN2_ANTD_SCHEMA
156 .put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/data.ts"), 169 .put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/data.ts"),
157 vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts")) 170 vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts"))
@@ -380,12 +393,17 @@ public class CodegenEngine { @@ -380,12 +393,17 @@ public class CodegenEngine {
380 393
381 // 2. 执行生成 394 // 2. 执行生成
382 Map<String, String> result = Maps.newLinkedHashMapWithExpectedSize(templates.size()); // 有序 395 Map<String, String> result = Maps.newLinkedHashMapWithExpectedSize(templates.size()); // 有序
  396 + Map<String, String> generatedSources = Maps.newHashMapWithExpectedSize(templates.size());
383 templates.forEach((vmPath, filePath) -> { 397 templates.forEach((vmPath, filePath) -> {
384 - // 2.1 特殊:主子表专属逻辑 398 + // 2.1 特殊:主子表、Uniapp 树表使用独立页面模版
  399 + if (isVue3AdminUniappTemplate(vmPath) && !matchVue3AdminUniappTemplate(table, vmPath)) {
  400 + return;
  401 + }
  402 + // 2.2 特殊:主子表专属逻辑
385 if (isSubTemplate(vmPath)) { 403 if (isSubTemplate(vmPath)) {
386 - generateSubCode(table, subTables, result, vmPath, filePath, bindingMap); 404 + generateSubCode(table, subTables, result, generatedSources, vmPath, filePath, bindingMap);
387 return; 405 return;
388 - // 2.2 特殊:树表专属逻辑 406 + // 2.3 特殊:树表专属逻辑
389 } else if (isPageReqVOTemplate(vmPath)) { 407 } else if (isPageReqVOTemplate(vmPath)) {
390 // 减少多余的类生成,例如说 PageVO.java 类 408 // 减少多余的类生成,例如说 PageVO.java 类
391 if (CodegenTemplateTypeEnum.isTree(table.getTemplateType())) { 409 if (CodegenTemplateTypeEnum.isTree(table.getTemplateType())) {
@@ -402,23 +420,36 @@ public class CodegenEngine { @@ -402,23 +420,36 @@ public class CodegenEngine {
402 return; 420 return;
403 } 421 }
404 } 422 }
405 - // 2.3 默认生成  
406 - generateCode(result, vmPath, filePath, bindingMap); 423 + // 2.4 默认生成
  424 + generateCode(result, generatedSources, vmPath, filePath, bindingMap);
407 }); 425 });
408 return result; 426 return result;
409 } 427 }
410 428
411 - private void generateCode(Map<String, String> result, String vmPath, 429 + private void generateCode(Map<String, String> result, Map<String, String> generatedSources, String vmPath,
412 String filePath, Map<String, Object> bindingMap) { 430 String filePath, Map<String, Object> bindingMap) {
413 filePath = formatFilePath(filePath, bindingMap); 431 filePath = formatFilePath(filePath, bindingMap);
  432 + registerGeneratedSource(generatedSources, filePath, vmPath, bindingMap);
414 String content = templateEngine.getTemplate(vmPath).render(bindingMap); 433 String content = templateEngine.getTemplate(vmPath).render(bindingMap);
415 // 格式化代码 434 // 格式化代码
416 content = prettyCode(content, vmPath); 435 content = prettyCode(content, vmPath);
417 result.put(filePath, content); 436 result.put(filePath, content);
418 } 437 }
419 438
  439 + @VisibleForTesting
  440 + static void registerGeneratedSource(Map<String, String> generatedSources, String filePath, String vmPath,
  441 + Map<String, Object> bindingMap) {
  442 + String source = vmPath
  443 + + (bindingMap.containsKey("subIndex") ? "[subIndex=" + bindingMap.get("subIndex") + "]" : "");
  444 + String previousSource = generatedSources.putIfAbsent(filePath, source);
  445 + if (previousSource != null) {
  446 + throw new IllegalStateException(String.format("生成文件路径重复:%s,来源模板:%s、%s",
  447 + filePath, previousSource, source));
  448 + }
  449 + }
  450 +
420 private void generateSubCode(CodegenTableDO table, List<CodegenTableDO> subTables, 451 private void generateSubCode(CodegenTableDO table, List<CodegenTableDO> subTables,
421 - Map<String, String> result, String vmPath, 452 + Map<String, String> result, Map<String, String> generatedSources, String vmPath,
422 String filePath, Map<String, Object> bindingMap) { 453 String filePath, Map<String, Object> bindingMap) {
423 // 没有子表,所以不生成 454 // 没有子表,所以不生成
424 if (CollUtil.isEmpty(subTables)) { 455 if (CollUtil.isEmpty(subTables)) {
@@ -441,7 +472,7 @@ public class CodegenEngine { @@ -441,7 +472,7 @@ public class CodegenEngine {
441 // 逐个生成 472 // 逐个生成
442 for (int i = 0; i < subTables.size(); i++) { 473 for (int i = 0; i < subTables.size(); i++) {
443 bindingMap.put("subIndex", i); 474 bindingMap.put("subIndex", i);
444 - generateCode(result, vmPath, filePath, bindingMap); 475 + generateCode(result, generatedSources, vmPath, filePath, bindingMap);
445 } 476 }
446 bindingMap.remove("subIndex"); 477 bindingMap.remove("subIndex");
447 } 478 }
@@ -482,6 +513,13 @@ public class CodegenEngine { @@ -482,6 +513,13 @@ public class CodegenEngine {
482 if (StrUtil.count(content, "DICT_TYPE.") == 0) { 513 if (StrUtil.count(content, "DICT_TYPE.") == 0) {
483 content = StrUtils.removeLineContains(content, "DICT_TYPE"); 514 content = StrUtils.removeLineContains(content, "DICT_TYPE");
484 } 515 }
  516 + // Vue3 Admin UniApp:清理空白行,并补充文件末尾换行,满足 ESLint 格式要求
  517 + if (vmPath.contains("vue3_admin_uniapp")) {
  518 + content = content.replaceAll("(?m)^[\\t ]+$", "");
  519 + if (!content.endsWith("\n")) {
  520 + content += "\n";
  521 + }
  522 + }
485 return content; 523 return content;
486 } 524 }
487 525
@@ -533,6 +571,14 @@ public class CodegenEngine { @@ -533,6 +571,14 @@ public class CodegenEngine {
533 List<String> subClassNameVars = new ArrayList<>(); 571 List<String> subClassNameVars = new ArrayList<>();
534 List<String> simpleClassNameUnderlineCases = new ArrayList<>(); 572 List<String> simpleClassNameUnderlineCases = new ArrayList<>();
535 List<String> subSimpleClassNameStrikeCases = new ArrayList<>(); 573 List<String> subSimpleClassNameStrikeCases = new ArrayList<>();
  574 + Set<String> masterSymbols = new HashSet<>();
  575 + masterSymbols.add(simpleClassName);
  576 + boolean mergeSubTableFields = ObjectUtils.equalsAny(table.getTemplateType(),
  577 + CodegenTemplateTypeEnum.MASTER_NORMAL.getType(), CodegenTemplateTypeEnum.MASTER_INNER.getType());
  578 + Set<String> masterFieldSymbols = new HashSet<>();
  579 + if (mergeSubTableFields) {
  580 + columns.forEach(column -> masterFieldSymbols.add(column.getJavaField()));
  581 + }
536 for (int i = 0; i < subTables.size(); i++) { 582 for (int i = 0; i < subTables.size(); i++) {
537 CodegenTableDO subTable = subTables.get(i); 583 CodegenTableDO subTable = subTables.get(i);
538 List<CodegenColumnDO> subColumns = subColumnsList.get(i); 584 List<CodegenColumnDO> subColumns = subColumnsList.get(i);
@@ -543,9 +589,18 @@ public class CodegenEngine { @@ -543,9 +589,18 @@ public class CodegenEngine {
543 subJoinColumnStrikeCases.add(toSymbolCase(subColumn.getJavaField(), '-')); // 将 DictType 转换成 dict-type 589 subJoinColumnStrikeCases.add(toSymbolCase(subColumn.getJavaField(), '-')); // 将 DictType 转换成 dict-type
544 // className 相关 590 // className 相关
545 String subSimpleClassName = removePrefix(subTable.getClassName(), upperFirst(subTable.getModuleName())); 591 String subSimpleClassName = removePrefix(subTable.getClassName(), upperFirst(subTable.getModuleName()));
  592 + if (!masterSymbols.add(subSimpleClassName)) {
  593 + throw exception(CODEGEN_MASTER_TABLE_NAME_DUPLICATE, subSimpleClassName);
  594 + }
  595 + String subClassNameVar = lowerFirst(subSimpleClassName);
  596 + String subFieldName = subClassNameVar
  597 + + (Boolean.TRUE.equals(subTable.getSubJoinMany()) ? "s" : "");
  598 + if (mergeSubTableFields && !masterFieldSymbols.add(subFieldName)) {
  599 + throw exception(CODEGEN_MASTER_TABLE_FIELD_DUPLICATE, subFieldName);
  600 + }
546 subSimpleClassNames.add(subSimpleClassName); 601 subSimpleClassNames.add(subSimpleClassName);
547 simpleClassNameUnderlineCases.add(toUnderlineCase(subSimpleClassName)); // 将 DictType 转换成 dict_type 602 simpleClassNameUnderlineCases.add(toUnderlineCase(subSimpleClassName)); // 将 DictType 转换成 dict_type
548 - subClassNameVars.add(lowerFirst(subSimpleClassName)); // 将 DictType 转换成 dictType,用于变量 603 + subClassNameVars.add(subClassNameVar); // 将 DictType 转换成 dictType,用于变量
549 subSimpleClassNameStrikeCases.add(toSymbolCase(subSimpleClassName, '-')); // 将 DictType 转换成 dict-type 604 subSimpleClassNameStrikeCases.add(toSymbolCase(subSimpleClassName, '-')); // 将 DictType 转换成 dict-type
550 } 605 }
551 bindingMap.put("subPrimaryColumns", subPrimaryColumns); 606 bindingMap.put("subPrimaryColumns", subPrimaryColumns);
@@ -734,6 +789,33 @@ public class CodegenEngine { @@ -734,6 +789,33 @@ public class CodegenEngine {
734 return path.contains("_sub"); 789 return path.contains("_sub");
735 } 790 }
736 791
  792 + private static boolean isVue3AdminUniappTemplate(String path) {
  793 + return path.contains("codegen/vue3_admin_uniapp/");
  794 + }
  795 +
  796 + /**
  797 + * 匹配 Uniapp 不同表类型的专用页面模版
  798 + */
  799 + private static boolean matchVue3AdminUniappTemplate(CodegenTableDO table, String path) {
  800 + boolean tree = CodegenTemplateTypeEnum.isTree(table.getTemplateType());
  801 + boolean masterSubmit = ObjectUtil.equals(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_NORMAL.getType())
  802 + || ObjectUtil.equals(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_INNER.getType());
  803 + boolean masterErp = ObjectUtil.equals(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_ERP.getType());
  804 + if (path.contains("_tree")) {
  805 + return tree;
  806 + }
  807 + if (path.contains("detail_master")) {
  808 + return masterSubmit;
  809 + }
  810 + if (path.contains("_erp")) {
  811 + return masterErp;
  812 + }
  813 + if (tree && path.endsWith("views/index.vue.vm")) {
  814 + return false;
  815 + }
  816 + return !(masterSubmit || masterErp) || !path.endsWith("views/detail/index.vue.vm");
  817 + }
  818 +
737 private static boolean isPageReqVOTemplate(String path) { 819 private static boolean isPageReqVOTemplate(String path) {
738 return path.contains("pageReqVO"); 820 return path.contains("pageReqVO");
739 } 821 }
yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/api/api.ts.vm
  1 +#if ($table.templateType != 2)
1 import type { PageParam, PageResult } from '@/http/types' 2 import type { PageParam, PageResult } from '@/http/types'
  3 +#end
2 import { http } from '@/http/http' 4 import { http } from '@/http/http'
3 5
4 #set ($primaryJavaType = $primaryColumn.javaType.toLowerCase()) 6 #set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
5 -#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte") 7 +#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "float" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
6 #set ($primaryTsType = "number") 8 #set ($primaryTsType = "number")
7 #else 9 #else
8 #set ($primaryTsType = "string") 10 #set ($primaryTsType = "string")
9 #end 11 #end
  12 +#foreach ($subTable in $subTables)
  13 +#set ($index = $foreach.count - 1)
  14 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  15 +#set ($subColumns = $subColumnsList.get($index))
  16 +#set ($subJoinColumn = $subJoinColumns.get($index))
  17 +/** ${subTable.classComment}信息 */
  18 +export interface ${subSimpleClassName} {
  19 +#foreach ($column in $subColumns)
  20 +#if ($column.primaryKey || $column.createOperation || $column.updateOperation || $column.listOperationResult)
  21 +#set ($javaType = $column.javaType.toLowerCase())
  22 +#set ($optional = $column.nullable || $column.primaryKey || ($table.templateType != 11 && $column.id == $subJoinColumn.id))
  23 +#if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "float" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
  24 + ${column.javaField}#if($optional)?#end: number
  25 +#elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")
  26 + ${column.javaField}#if($optional)?#end: number
  27 +#elseif(${javaType} == "boolean")
  28 + ${column.javaField}#if($optional)?#end: boolean
  29 +#else
  30 + ${column.javaField}#if($optional)?#end: string
  31 +#end
  32 +#end
  33 +#end
  34 +}
  35 +
  36 +#end
10 /** ${table.classComment}信息 */ 37 /** ${table.classComment}信息 */
11 export interface ${simpleClassName} { 38 export interface ${simpleClassName} {
12 #foreach ($column in $columns) 39 #foreach ($column in $columns)
13 - #if ($column.primaryKey || $column.createOperation || $column.updateOperation || $column.listOperationResult)  
14 - #set ($javaType = $column.javaType.toLowerCase())  
15 - #set ($javaFieldLower = $column.javaField.toLowerCase())  
16 - #set ($optional = $column.nullable || $column.primaryKey || $javaFieldLower == "createtime" || $javaFieldLower == "updatetime")  
17 - #if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte") 40 +#if ($column.primaryKey || $column.createOperation || $column.updateOperation || $column.listOperationResult)
  41 +#set ($javaType = $column.javaType.toLowerCase())
  42 +#set ($optional = $column.nullable || $column.primaryKey)
  43 +#if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "float" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
  44 + ${column.javaField}#if($optional)?#end: number
  45 +#elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")
18 ${column.javaField}#if($optional)?#end: number 46 ${column.javaField}#if($optional)?#end: number
19 - #elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")  
20 - ${column.javaField}#if($optional)?#end: Date  
21 - #elseif(${javaType} == "boolean") 47 +#elseif(${javaType} == "boolean")
22 ${column.javaField}#if($optional)?#end: boolean 48 ${column.javaField}#if($optional)?#end: boolean
23 - #else 49 +#else
24 ${column.javaField}#if($optional)?#end: string 50 ${column.javaField}#if($optional)?#end: string
25 - #end  
26 - #end 51 +#end
  52 +#end
  53 +#end
  54 +#if ($table.templateType == 2)
  55 + children?: ${simpleClassName}[]
  56 +#end
  57 +#if ($table.templateType == 10 || $table.templateType == 12)
  58 +#foreach ($subTable in $subTables)
  59 +#set ($index = $foreach.count - 1)
  60 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  61 +#set ($subClassNameVar = $subClassNameVars.get($index))
  62 + ${subClassNameVar}#if($subTable.subJoinMany)s#end?: ${subSimpleClassName}#if($subTable.subJoinMany)[]#else | null#end
  63 +#end
27 #end 64 #end
28 } 65 }
29 66
  67 +#if ($table.templateType == 2)
  68 +/** 获取${table.classComment}列表 */
  69 +export function get${simpleClassName}List(params?: Record<string, any>) {
  70 + return http.get<${simpleClassName}[]>('/${table.moduleName}/${simpleClassName_strikeCase}/list', params)
  71 +}
  72 +#else
30 /** 获取${table.classComment}分页列表 */ 73 /** 获取${table.classComment}分页列表 */
31 export function get${simpleClassName}Page(params: PageParam) { 74 export function get${simpleClassName}Page(params: PageParam) {
32 return http.get<PageResult<${simpleClassName}>>('/${table.moduleName}/${simpleClassName_strikeCase}/page', params) 75 return http.get<PageResult<${simpleClassName}>>('/${table.moduleName}/${simpleClassName_strikeCase}/page', params)
33 } 76 }
  77 +#end
34 78
35 /** 获取${table.classComment}详情 */ 79 /** 获取${table.classComment}详情 */
36 export function get${simpleClassName}(id: ${primaryTsType}) { 80 export function get${simpleClassName}(id: ${primaryTsType}) {
@@ -39,7 +83,7 @@ export function get${simpleClassName}(id: ${primaryTsType}) { @@ -39,7 +83,7 @@ export function get${simpleClassName}(id: ${primaryTsType}) {
39 83
40 /** 创建${table.classComment} */ 84 /** 创建${table.classComment} */
41 export function create${simpleClassName}(data: ${simpleClassName}) { 85 export function create${simpleClassName}(data: ${simpleClassName}) {
42 - return http.post<number>('/${table.moduleName}/${simpleClassName_strikeCase}/create', data) 86 + return http.post<${primaryTsType}>('/${table.moduleName}/${simpleClassName_strikeCase}/create', data)
43 } 87 }
44 88
45 /** 更新${table.classComment} */ 89 /** 更新${table.classComment} */
@@ -51,3 +95,65 @@ export function update${simpleClassName}(data: ${simpleClassName}) { @@ -51,3 +95,65 @@ export function update${simpleClassName}(data: ${simpleClassName}) {
51 export function delete${simpleClassName}(id: ${primaryTsType}) { 95 export function delete${simpleClassName}(id: ${primaryTsType}) {
52 return http.delete<boolean>(`/${table.moduleName}/${simpleClassName_strikeCase}/delete?id=${id}`) 96 return http.delete<boolean>(`/${table.moduleName}/${simpleClassName_strikeCase}/delete?id=${id}`)
53 } 97 }
  98 +#foreach ($subTable in $subTables)
  99 +#set ($index = $foreach.count - 1)
  100 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  101 +#set ($subPrimaryColumn = $subPrimaryColumns.get($index))
  102 +#set ($subJoinColumn = $subJoinColumns.get($index))
  103 +#set ($subPrimaryJavaType = $subPrimaryColumn.javaType.toLowerCase())
  104 +#if(${subPrimaryJavaType} == "long" || ${subPrimaryJavaType} == "integer" || ${subPrimaryJavaType} == "short" || ${subPrimaryJavaType} == "float" || ${subPrimaryJavaType} == "double" || ${subPrimaryJavaType} == "bigdecimal" || ${subPrimaryJavaType} == "byte")
  105 +#set ($subPrimaryTsType = "number")
  106 +#else
  107 +#set ($subPrimaryTsType = "string")
  108 +#end
  109 +#set ($subJoinJavaType = $subJoinColumn.javaType.toLowerCase())
  110 +#if(${subJoinJavaType} == "long" || ${subJoinJavaType} == "integer" || ${subJoinJavaType} == "short" || ${subJoinJavaType} == "float" || ${subJoinJavaType} == "double" || ${subJoinJavaType} == "bigdecimal" || ${subJoinJavaType} == "byte")
  111 +#set ($subJoinTsType = "number")
  112 +#else
  113 +#set ($subJoinTsType = "string")
  114 +#end
  115 +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + $subJoinColumn.javaField.substring(1))
  116 +#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index))
  117 +#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
  118 +
  119 +// ==================== 子表:${subTable.classComment} ====================
  120 +#if ($table.templateType == 11)
  121 +
  122 +/** 获取${subTable.classComment}分页列表 */
  123 +export function get${subSimpleClassName}Page(params: PageParam & { ${subJoinColumn.javaField}: ${subJoinTsType} }) {
  124 + return http.get<PageResult<${subSimpleClassName}>>('/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/page', params)
  125 +}
  126 +
  127 +/** 获取${subTable.classComment}详情 */
  128 +export function get${subSimpleClassName}(id: ${subPrimaryTsType}) {
  129 + return http.get<${subSimpleClassName}>(`/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/get?id=${id}`)
  130 +}
  131 +
  132 +/** 创建${subTable.classComment} */
  133 +export function create${subSimpleClassName}(data: ${subSimpleClassName}) {
  134 + return http.post<${subPrimaryTsType}>('/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/create', data)
  135 +}
  136 +
  137 +/** 更新${subTable.classComment} */
  138 +export function update${subSimpleClassName}(data: ${subSimpleClassName}) {
  139 + return http.put<boolean>('/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/update', data)
  140 +}
  141 +
  142 +/** 删除${subTable.classComment} */
  143 +export function delete${subSimpleClassName}(id: ${subPrimaryTsType}) {
  144 + return http.delete<boolean>(`/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/delete?id=${id}`)
  145 +}
  146 +#elseif ($subTable.subJoinMany)
  147 +
  148 +/** 获取${subTable.classComment}列表 */
  149 +export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}: ${subJoinTsType}) {
  150 + return http.get<${subSimpleClassName}[]>(`/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=#[[${]]#${subJoinColumn.javaField}#[[}]]#`)
  151 +}
  152 +#else
  153 +
  154 +/** 获取${subTable.classComment} */
  155 +export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}: ${subJoinTsType}) {
  156 + return http.get<${subSimpleClassName} | null>(`/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=#[[${]]#${subJoinColumn.javaField}#[[}]]#`)
  157 +}
  158 +#end
  159 +#end
yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/components/breadcrumb_tree.vue.vm 0 → 100644
  1 +<template>
  2 + <view class="bg-white px-24rpx py-16rpx">
  3 + <scroll-view scroll-x class="whitespace-nowrap">
  4 + <view class="inline-flex items-center">
  5 + <view
  6 + class="flex items-center text-28rpx"
  7 + :class="items.length > 0 ? 'text-[#1890ff]' : 'text-[#333]'"
  8 + @click="handleClick(-1)"
  9 + >
  10 + <text>全部${table.classComment}</text>
  11 + </view>
  12 + <template v-for="(item, index) in items" :key="item.id">
  13 + <wd-icon name="arrow-right" size="12px" color="#999" custom-class="mx-8rpx" />
  14 + <view
  15 + class="flex items-center text-28rpx"
  16 + :class="index < items.length - 1 ? 'text-[#1890ff]' : 'text-[#333]'"
  17 + @click="handleClick(index)"
  18 + >
  19 + <text>{{ item.name }}</text>
  20 + </view>
  21 + </template>
  22 + </view>
  23 + </scroll-view>
  24 + </view>
  25 +</template>
  26 +
  27 +<script lang="ts" setup>
  28 +import { ref, watch } from 'vue'
  29 +
  30 +interface BreadcrumbItem {
  31 + id: number
  32 + name: string
  33 +}
  34 +
  35 +const props = defineProps<{
  36 + modelValue: number
  37 +}>()
  38 +
  39 +const emit = defineEmits<{
  40 + 'update:modelValue': [value: number]
  41 +}>()
  42 +
  43 +const items = ref<BreadcrumbItem[]>([]) // 面包屑数据
  44 +
  45 +/** 监听外部值变化 */
  46 +watch(() => props.modelValue, (value) => {
  47 + if (value === 0) {
  48 + items.value = []
  49 + }
  50 +})
  51 +
  52 +/** 点击面包屑 */
  53 +function handleClick(index: number) {
  54 + if (index === -1) {
  55 + reset()
  56 + return
  57 + }
  58 + if (index < items.value.length - 1) {
  59 + items.value = items.value.slice(0, index + 1)
  60 + emit('update:modelValue', items.value[index].id)
  61 + }
  62 +}
  63 +
  64 +/** 进入子层级 */
  65 +function enter(item: BreadcrumbItem) {
  66 + items.value.push(item)
  67 + emit('update:modelValue', item.id)
  68 +}
  69 +
  70 +/** 返回上一层级 */
  71 +function back(): boolean {
  72 + if (items.value.length === 0) {
  73 + return false
  74 + }
  75 + items.value.pop()
  76 + emit('update:modelValue', items.value.at(-1)?.id ?? 0)
  77 + return true
  78 +}
  79 +
  80 +/** 重置面包屑 */
  81 +function reset() {
  82 + items.value = []
  83 + emit('update:modelValue', 0)
  84 +}
  85 +
  86 +defineExpose({ back, enter, reset })
  87 +</script>
yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/components/search-form.vue.vm
@@ -42,23 +42,12 @@ @@ -42,23 +42,12 @@
42 #elseif ($column.htmlType == "datetime" && $listOperationCondition == "BETWEEN") 42 #elseif ($column.htmlType == "datetime" && $listOperationCondition == "BETWEEN")
43 <yd-search-date-range v-model="formData.${javaField}" label="${comment}" /> 43 <yd-search-date-range v-model="formData.${javaField}" label="${comment}" />
44 #elseif (($column.htmlType == "select" || $column.htmlType == "radio") && $dictType && "" != $dictType) 44 #elseif (($column.htmlType == "select" || $column.htmlType == "radio") && $dictType && "" != $dictType)
45 - <view class="yd-search-form-item">  
46 - <view class="yd-search-form-label">  
47 - ${comment}  
48 - </view>  
49 - <wd-radio-group v-model="formData.${javaField}" type="button">  
50 - <wd-radio :value="-1">  
51 - 全部  
52 - </wd-radio>  
53 - <wd-radio  
54 - v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"  
55 - :key="dict.value"  
56 - :value="dict.value"  
57 - >  
58 - {{ dict.label }}  
59 - </wd-radio>  
60 - </wd-radio-group>  
61 - </view> 45 + <yd-search-picker
  46 + v-model="formData.${javaField}"
  47 + label="${comment}"
  48 + :columns="$dictMethod(DICT_TYPE.$dictType.toUpperCase())"
  49 + all-option
  50 + />
62 #else 51 #else
63 <view class="yd-search-form-item"> 52 <view class="yd-search-form-item">
64 <view class="yd-search-form-label"> 53 <view class="yd-search-form-label">
@@ -150,13 +139,13 @@ const formData = reactive({ @@ -150,13 +139,13 @@ const formData = reactive({
150 ${column.javaField}: [undefined, undefined] as [number | undefined, number | undefined], 139 ${column.javaField}: [undefined, undefined] as [number | undefined, number | undefined],
151 #elseif ($column.dictType && "" != $column.dictType) 140 #elseif ($column.dictType && "" != $column.dictType)
152 #if(${javaType} == "string") 141 #if(${javaType} == "string")
153 - ${column.javaField}: -1 as -1 | string, // -1 表示全部 142 + ${column.javaField}: undefined as string | undefined,
154 #elseif(${javaType} == "boolean") 143 #elseif(${javaType} == "boolean")
155 - ${column.javaField}: -1 as -1 | boolean, // -1 表示全部 144 + ${column.javaField}: undefined as boolean | undefined,
156 #else 145 #else
157 - ${column.javaField}: -1, // -1 表示全部 146 + ${column.javaField}: undefined as number | undefined,
158 #end 147 #end
159 - #elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte") 148 + #elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "float" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
160 ${column.javaField}: undefined as number | undefined, 149 ${column.javaField}: undefined as number | undefined,
161 #elseif(${javaType} == "boolean") 150 #elseif(${javaType} == "boolean")
162 ${column.javaField}: undefined as boolean | undefined, 151 ${column.javaField}: undefined as boolean | undefined,
@@ -181,11 +170,11 @@ const placeholder = computed(() =&gt; { @@ -181,11 +170,11 @@ const placeholder = computed(() =&gt; {
181 conditions.push(`${comment}:#[[${]]#formatDate(formData.${javaField}[0])#[[}]]#~#[[${]]#formatDate(formData.${javaField}[1])#[[}]]#`) 170 conditions.push(`${comment}:#[[${]]#formatDate(formData.${javaField}[0])#[[}]]#~#[[${]]#formatDate(formData.${javaField}[1])#[[}]]#`)
182 } 171 }
183 #elseif ($dictType && "" != $dictType) 172 #elseif ($dictType && "" != $dictType)
184 - if (formData.${javaField} !== -1) { 173 + if (formData.${javaField} !== undefined) {
185 conditions.push(`${comment}:#[[${]]#getDictLabel(DICT_TYPE.${dictType.toUpperCase()}, formData.${javaField})#[[}]]#`) 174 conditions.push(`${comment}:#[[${]]#getDictLabel(DICT_TYPE.${dictType.toUpperCase()}, formData.${javaField})#[[}]]#`)
186 } 175 }
187 #else 176 #else
188 - #if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte" || ${javaType} == "boolean") 177 + #if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "float" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte" || ${javaType} == "boolean")
189 if (formData.${javaField} !== undefined) { 178 if (formData.${javaField} !== undefined) {
190 conditions.push(`${comment}:#[[${]]#formData.${javaField}#[[}]]#`) 179 conditions.push(`${comment}:#[[${]]#formData.${javaField}#[[}]]#`)
191 } 180 }
@@ -207,9 +196,7 @@ function handleSearch() { @@ -207,9 +196,7 @@ function handleSearch() {
207 ...formData, 196 ...formData,
208 #foreach($column in $columns) 197 #foreach($column in $columns)
209 #if ($column.listOperation) 198 #if ($column.listOperation)
210 - #if ($column.dictType && "" != $column.dictType)  
211 - ${column.javaField}: formData.${column.javaField} === -1 ? undefined : formData.${column.javaField},  
212 - #elseif ($column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN") 199 + #if ($column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
213 ${column.javaField}: formatDateRange(formData.${column.javaField}), 200 ${column.javaField}: formatDateRange(formData.${column.javaField}),
214 #end 201 #end
215 #end 202 #end
@@ -224,7 +211,7 @@ function handleReset() { @@ -224,7 +211,7 @@ function handleReset() {
224 #if ($column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN") 211 #if ($column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
225 formData.${column.javaField} = [undefined, undefined] 212 formData.${column.javaField} = [undefined, undefined]
226 #elseif ($column.dictType && "" != $column.dictType) 213 #elseif ($column.dictType && "" != $column.dictType)
227 - formData.${column.javaField} = -1 214 + formData.${column.javaField} = undefined
228 #else 215 #else
229 formData.${column.javaField} = undefined 216 formData.${column.javaField} = undefined
230 #end 217 #end
yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/detail/index.vue.vm
@@ -39,7 +39,7 @@ @@ -39,7 +39,7 @@
39 </wd-button> 39 </wd-button>
40 <wd-button 40 <wd-button
41 v-if="hasAccessByCodes(['${permissionPrefix}:delete'])" 41 v-if="hasAccessByCodes(['${permissionPrefix}:delete'])"
42 - class="flex-1" type="error" :loading="deleting" @click="handleDelete" 42 + class="flex-1" type="danger" :loading="deleting" @click="handleDelete"
43 > 43 >
44 删除 44 删除
45 </wd-button> 45 </wd-button>
@@ -49,6 +49,12 @@ @@ -49,6 +49,12 @@
49 </template> 49 </template>
50 50
51 <script lang="ts" setup> 51 <script lang="ts" setup>
  52 +#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
  53 +#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "float" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
  54 +#set ($primaryTsType = "number")
  55 +#else
  56 +#set ($primaryTsType = "string")
  57 +#end
52 #set ($hasDict = 0) 58 #set ($hasDict = 0)
53 #foreach($column in $columns) 59 #foreach($column in $columns)
54 #if ($hasDict == 0 && $column.dictType && "" != $column.dictType) 60 #if ($hasDict == 0 && $column.dictType && "" != $column.dictType)
@@ -64,6 +70,7 @@ @@ -64,6 +70,7 @@
64 import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}' 70 import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
65 import { onUnload } from '@dcloudio/uni-app' 71 import { onUnload } from '@dcloudio/uni-app'
66 import { onMounted, ref } from 'vue' 72 import { onMounted, ref } from 'vue'
  73 +import { useDialog } from '@wot-ui/ui/components/wd-dialog'
67 import { useToast } from '@wot-ui/ui/components/wd-toast' 74 import { useToast } from '@wot-ui/ui/components/wd-toast'
68 import { delete${simpleClassName}, get${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}' 75 import { delete${simpleClassName}, get${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
69 import { useAccess } from '@/hooks/useAccess' 76 import { useAccess } from '@/hooks/useAccess'
@@ -76,7 +83,7 @@ import { formatDateTime } from &#39;@/utils/date&#39; @@ -76,7 +83,7 @@ import { formatDateTime } from &#39;@/utils/date&#39;
76 #end 83 #end
77 84
78 const props = defineProps<{ 85 const props = defineProps<{
79 - id?: number | any 86 + id?: ${primaryTsType}
80 }>() 87 }>()
81 88
82 definePage({ 89 definePage({
@@ -87,6 +94,7 @@ definePage({ @@ -87,6 +94,7 @@ definePage({
87 }) 94 })
88 95
89 const { hasAccessByCodes } = useAccess() 96 const { hasAccessByCodes } = useAccess()
  97 +const dialog = useDialog()
90 const toast = useToast() 98 const toast = useToast()
91 const formData = ref<${simpleClassName}>() // 详情数据 99 const formData = ref<${simpleClassName}>() // 详情数据
92 const deleting = ref(false) // 删除状态 100 const deleting = ref(false) // 删除状态
@@ -117,28 +125,27 @@ function handleEdit() { @@ -117,28 +125,27 @@ function handleEdit() {
117 } 125 }
118 126
119 /** 删除${table.classComment} */ 127 /** 删除${table.classComment} */
120 -function handleDelete() { 128 +async function handleDelete() {
121 if (!props.id) { 129 if (!props.id) {
122 return 130 return
123 } 131 }
124 - uni.showModal({  
125 - title: '提示',  
126 - content: '确定要删除该${table.classComment}吗?',  
127 - success: async (res) => {  
128 - if (!res.confirm) {  
129 - return  
130 - }  
131 - deleting.value = true  
132 - try {  
133 - await delete${simpleClassName}(props.id)  
134 - toast.success('删除成功')  
135 - uni.$emit('${table.moduleName}:${table.businessName}:reload')  
136 - delay(handleBack)  
137 - } finally {  
138 - deleting.value = false  
139 - }  
140 - },  
141 - }) 132 + try {
  133 + await dialog.confirm({
  134 + title: '提示',
  135 + msg: '确定要删除该${table.classComment}吗?',
  136 + })
  137 + } catch {
  138 + return
  139 + }
  140 + deleting.value = true
  141 + try {
  142 + await delete${simpleClassName}(props.id)
  143 + toast.success('删除成功')
  144 + uni.$emit('${table.moduleName}:${table.businessName}:reload')
  145 + delay(handleBack)
  146 + } finally {
  147 + deleting.value = false
  148 + }
142 } 149 }
143 150
144 /** 初始化 */ 151 /** 初始化 */
yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/detail_erp.vue.vm 0 → 100644
  1 +<template>
  2 + <view class="yd-page-container">
  3 + <!-- 顶部导航栏 -->
  4 + <wd-navbar title="${table.classComment}详情" left-arrow placeholder safe-area-inset-top fixed @click-left="handleBack" />
  5 +
  6 + <view class="pb-160rpx">
  7 + <!-- 主表详情 -->
  8 + <wd-cell-group border title="${table.classComment}信息">
  9 +#foreach($column in $columns)
  10 +#if ($column.primaryKey || $column.listOperationResult || $column.createOperation || $column.updateOperation)
  11 +#if ($column.dictType && "" != $column.dictType)
  12 + <wd-cell title="${column.columnComment}">
  13 + <dict-tag :type="DICT_TYPE.${column.dictType.toUpperCase()}" :value="formData?.${column.javaField}" />
  14 + </wd-cell>
  15 +#elseif ($column.javaType == "LocalDateTime")
  16 + <wd-cell title="${column.columnComment}" :value="formatDateTime(formData?.${column.javaField}) || '-'" />
  17 +#else
  18 + <wd-cell title="${column.columnComment}" :value="formData?.${column.javaField} ?? '-'" />
  19 +#end
  20 +#end
  21 +#end
  22 + </wd-cell-group>
  23 +
  24 +#foreach ($subTable in $subTables)
  25 +#set ($index = $foreach.count - 1)
  26 +#set ($subColumns = $subColumnsList.get($index))
  27 +#set ($subClassNameVar = $subClassNameVars.get($index))
  28 +#set ($subPrimaryColumn = $subPrimaryColumns.get($index))
  29 +#set ($pageSize = 10)
  30 +#if (!$subTable.subJoinMany)
  31 +#set ($pageSize = 1)
  32 +#end
  33 + <!-- 子表:${subTable.classComment} -->
  34 + <view class="mt-20rpx flex items-center justify-between px-24rpx py-16rpx">
  35 + <text class="text-28rpx text-[#333] font-semibold">${subTable.classComment}({{ ${subClassNameVar}Total }})</text>
  36 + <wd-button
  37 +#if (!$subTable.subJoinMany)
  38 + v-if="${subClassNameVar}Total === 0 && hasAccessByCodes(['${permissionPrefix}:create'])"
  39 +#else
  40 + v-if="hasAccessByCodes(['${permissionPrefix}:create'])"
  41 +#end
  42 + size="small" type="primary" variant="plain" @click="open${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}()"
  43 + >
  44 + 添加
  45 + </wd-button>
  46 + </view>
  47 + <view class="px-24rpx">
  48 + <z-paging
  49 + ref="${subClassNameVar}PagingRef"
  50 + v-model="${subClassNameVar}List"
  51 + :fixed="false"
  52 + :auto="false"
  53 + height="560rpx"
  54 + :default-page-size="${pageSize}"
  55 + :refresher-enabled="false"
  56 + :inside-more="true"
  57 + :to-bottom-loading-more-enabled="false"
  58 + loading-more-default-text="点击加载更多"
  59 + empty-view-text="暂无${subTable.classComment}"
  60 + @query="query${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}List"
  61 + >
  62 + <view
  63 + v-for="item in ${subClassNameVar}List"
  64 + :key="item.${subPrimaryColumn.javaField}"
  65 + class="mb-16rpx rounded-12rpx bg-white p-24rpx shadow-sm"
  66 + >
  67 +#foreach ($column in $subColumns)
  68 +#if ($column.listOperationResult && !$column.primaryKey)
  69 + <view class="mb-8rpx text-28rpx text-[#666]">
  70 + ${column.columnComment}:{{ item.${column.javaField} ?? '-' }}
  71 + </view>
  72 +#end
  73 +#end
  74 + <view class="mt-16rpx flex justify-end gap-16rpx">
  75 + <wd-button size="small" variant="plain" @click="open${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(item)">
  76 + 编辑
  77 + </wd-button>
  78 + <wd-button size="small" type="danger" variant="plain" @click="remove${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(item)">
  79 + 删除
  80 + </wd-button>
  81 + </view>
  82 + </view>
  83 + </z-paging>
  84 + </view>
  85 +#end
  86 + </view>
  87 +
  88 + <!-- 底部操作按钮 -->
  89 + <view class="yd-detail-footer">
  90 + <view class="yd-detail-footer-actions">
  91 + <wd-button v-if="hasAccessByCodes(['${permissionPrefix}:update'])" class="flex-1" type="warning" @click="handleEdit">
  92 + 编辑
  93 + </wd-button>
  94 + <wd-button v-if="hasAccessByCodes(['${permissionPrefix}:delete'])" class="flex-1" type="danger" :loading="deleting" @click="handleDelete">
  95 + 删除
  96 + </wd-button>
  97 + </view>
  98 + </view>
  99 + </view>
  100 +</template>
  101 +
  102 +<script lang="ts" setup>
  103 +#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
  104 +#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "float" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
  105 +#set ($primaryTsType = "number")
  106 +#else
  107 +#set ($primaryTsType = "string")
  108 +#end
  109 +#set ($hasDict = 0)
  110 +#set ($hasDateTime = 0)
  111 +#foreach($column in $columns)
  112 +#if ($column.dictType && "" != $column.dictType)
  113 +#set ($hasDict = 1)
  114 +#end
  115 +#if ($column.javaType == "LocalDateTime")
  116 +#set ($hasDateTime = 1)
  117 +#end
  118 +#end
  119 +import type {
  120 + ${simpleClassName},
  121 +#foreach ($subSimpleClassName in $subSimpleClassNames)
  122 + ${subSimpleClassName},
  123 +#end
  124 +} from '@/api/${table.moduleName}/${table.businessName}'
  125 +import { onShow } from '@dcloudio/uni-app'
  126 +import { nextTick, ref } from 'vue'
  127 +import { useDialog } from '@wot-ui/ui/components/wd-dialog'
  128 +import { useToast } from '@wot-ui/ui/components/wd-toast'
  129 +import {
  130 + delete${simpleClassName},
  131 +#foreach ($subSimpleClassName in $subSimpleClassNames)
  132 + delete${subSimpleClassName},
  133 +#end
  134 + get${simpleClassName},
  135 +#foreach ($subSimpleClassName in $subSimpleClassNames)
  136 + get${subSimpleClassName}Page,
  137 +#end
  138 +} from '@/api/${table.moduleName}/${table.businessName}'
  139 +import { useAccess } from '@/hooks/useAccess'
  140 +import { delay, navigateBackPlus } from '@/utils'
  141 +#if ($hasDict == 1)
  142 +import { DICT_TYPE } from '@/utils/constants'
  143 +#end
  144 +#if ($hasDateTime == 1)
  145 +import { formatDateTime } from '@/utils/date'
  146 +#end
  147 +
  148 +const props = defineProps<{ id?: ${primaryTsType} }>()
  149 +
  150 +definePage({
  151 + style: { navigationBarTitleText: '', navigationStyle: 'custom' },
  152 +})
  153 +
  154 +const { hasAccessByCodes } = useAccess()
  155 +const dialog = useDialog()
  156 +const toast = useToast()
  157 +const formData = ref<${simpleClassName}>() // 主表数据
  158 +#foreach ($subTable in $subTables)
  159 +#set ($index = $foreach.count - 1)
  160 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  161 +#set ($subClassNameVar = $subClassNameVars.get($index))
  162 +const ${subClassNameVar}List = ref<${subSimpleClassName}[]>([]) // ${subTable.classComment}列表
  163 +const ${subClassNameVar}Total = ref(0) // ${subTable.classComment}总数
  164 +const ${subClassNameVar}PagingRef = ref<any>() // ${subTable.classComment}分页组件引用
  165 +#end
  166 +const deleting = ref(false) // 主表删除状态
  167 +
  168 +/** 返回上一页 */
  169 +function handleBack() {
  170 + navigateBackPlus('/pages-${table.moduleName}/${table.businessName}/index')
  171 +}
  172 +
  173 +/** 加载主表 */
  174 +async function loadMain() {
  175 + if (props.id) {
  176 + formData.value = await get${simpleClassName}(props.id)
  177 + }
  178 +}
  179 +
  180 +#foreach ($subTable in $subTables)
  181 +#set ($index = $foreach.count - 1)
  182 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  183 +#set ($subClassNameVar = $subClassNameVars.get($index))
  184 +#set ($subJoinColumn = $subJoinColumns.get($index))
  185 +#set ($subPrimaryColumn = $subPrimaryColumns.get($index))
  186 +/** 查询${subTable.classComment}列表 */
  187 +async function query${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}List(pageNo: number, pageSize: number) {
  188 + if (!props.id) {
  189 + ${subClassNameVar}PagingRef.value?.completeByTotal([], 0)
  190 + return
  191 + }
  192 + try {
  193 + const data = await get${subSimpleClassName}Page({ ${subJoinColumn.javaField}: props.id, pageNo, pageSize })
  194 + ${subClassNameVar}Total.value = data.total
  195 + ${subClassNameVar}PagingRef.value?.completeByTotal(data.list, data.total)
  196 + } catch {
  197 + ${subClassNameVar}PagingRef.value?.complete(false)
  198 + }
  199 +}
  200 +
  201 +/** 打开${subTable.classComment}表单 */
  202 +function open${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(item?: ${subSimpleClassName}) {
  203 + const query = [
  204 + `${subJoinColumn.javaField}=#[[${]]#props.id#[[}]]#`,
  205 + item?.${subPrimaryColumn.javaField} ? `id=#[[${]]#item.${subPrimaryColumn.javaField}#[[}]]#` : '',
  206 + ].filter(Boolean).join('&')
  207 + uni.navigateTo({ url: `/pages-${table.moduleName}/${table.businessName}/${subSimpleClassName_strikeCases.get($index)}/form/index?#[[${]]#query#[[}]]#` })
  208 +}
  209 +
  210 +/** 删除${subTable.classComment} */
  211 +async function remove${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(item: ${subSimpleClassName}) {
  212 + try {
  213 + await dialog.confirm({ title: '提示', msg: '确定要删除该${subTable.classComment}吗?' })
  214 + } catch {
  215 + return
  216 + }
  217 + await delete${subSimpleClassName}(item.${subPrimaryColumn.javaField}!)
  218 + toast.success('删除成功')
  219 + ${subClassNameVar}PagingRef.value?.reload()
  220 +}
  221 +
  222 +#end
  223 +/** 编辑主表 */
  224 +function handleEdit() {
  225 + uni.navigateTo({ url: `/pages-${table.moduleName}/${table.businessName}/form/index?id=#[[${]]#props.id#[[}]]#` })
  226 +}
  227 +
  228 +/** 删除主表 */
  229 +async function handleDelete() {
  230 + if (!props.id) {
  231 + return
  232 + }
  233 + try {
  234 + await dialog.confirm({ title: '提示', msg: '确定要删除该${table.classComment}吗?' })
  235 + } catch {
  236 + return
  237 + }
  238 + deleting.value = true
  239 + try {
  240 + await delete${simpleClassName}(props.id)
  241 + toast.success('删除成功')
  242 + uni.$emit('${table.moduleName}:${table.businessName}:reload')
  243 + delay(handleBack)
  244 + } finally {
  245 + deleting.value = false
  246 + }
  247 +}
  248 +
  249 +/** 进入页面加载(含编辑和子表返回后刷新) */
  250 +onShow(() => {
  251 + loadMain()
  252 + nextTick(() => {
  253 +#foreach ($subClassNameVar in $subClassNameVars)
  254 + ${subClassNameVar}PagingRef.value?.reload()
  255 +#end
  256 + })
  257 +})
  258 +</script>
yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/detail_master.vue.vm 0 → 100644
  1 +<template>
  2 + <view class="yd-page-container">
  3 + <!-- 顶部导航栏 -->
  4 + <wd-navbar
  5 + title="${table.classComment}详情"
  6 + left-arrow placeholder safe-area-inset-top fixed
  7 + @click-left="handleBack"
  8 + />
  9 +
  10 + <view class="pb-160rpx">
  11 + <!-- 主表详情 -->
  12 + <wd-cell-group border title="${table.classComment}信息">
  13 +#foreach($column in $columns)
  14 +#if ($column.primaryKey || $column.listOperationResult || $column.createOperation || $column.updateOperation)
  15 +#if ($column.dictType && "" != $column.dictType)
  16 + <wd-cell title="${column.columnComment}">
  17 + <dict-tag :type="DICT_TYPE.${column.dictType.toUpperCase()}" :value="formData?.${column.javaField}" />
  18 + </wd-cell>
  19 +#elseif ($column.javaType == "LocalDateTime")
  20 + <wd-cell title="${column.columnComment}" :value="formatDateTime(formData?.${column.javaField}) || '-'" />
  21 +#else
  22 + <wd-cell title="${column.columnComment}" :value="formData?.${column.javaField} ?? '-'" />
  23 +#end
  24 +#end
  25 +#end
  26 + </wd-cell-group>
  27 +
  28 +#foreach ($subTable in $subTables)
  29 +#set ($index = $foreach.count - 1)
  30 +#set ($subColumns = $subColumnsList.get($index))
  31 +#set ($subClassNameVar = $subClassNameVars.get($index))
  32 +#set ($subPrimaryColumn = $subPrimaryColumns.get($index))
  33 + <!-- 子表:${subTable.classComment} -->
  34 + <view class="mt-20rpx px-24rpx py-16rpx text-28rpx text-[#999]">
  35 + ${subTable.classComment}#if($subTable.subJoinMany)({{ ${subClassNameVar}s.length }})#end
  36 + </view>
  37 + <view class="px-24rpx">
  38 +#if ($subTable.subJoinMany)
  39 + <view v-if="!${subClassNameVar}s.length" class="rounded-12rpx bg-white py-40rpx text-center text-26rpx text-[#999]">
  40 + 暂无${subTable.classComment}
  41 + </view>
  42 + <view
  43 + v-for="item in ${subClassNameVar}s"
  44 + :key="item.${subPrimaryColumn.javaField}"
  45 + class="mb-16rpx rounded-12rpx bg-white p-24rpx shadow-sm"
  46 + >
  47 +#foreach ($column in $subColumns)
  48 +#if ($column.listOperationResult && !$column.primaryKey)
  49 + <view class="mb-8rpx text-28rpx text-[#666]">
  50 + ${column.columnComment}:{{ item.${column.javaField} ?? '-' }}
  51 + </view>
  52 +#end
  53 +#end
  54 + </view>
  55 +#else
  56 + <view class="rounded-12rpx bg-white p-24rpx shadow-sm">
  57 + <template v-if="${subClassNameVar}">
  58 +#foreach ($column in $subColumns)
  59 +#if ($column.listOperationResult && !$column.primaryKey)
  60 + <view class="mb-8rpx text-28rpx text-[#666]">
  61 + ${column.columnComment}:{{ ${subClassNameVar}.${column.javaField} ?? '-' }}
  62 + </view>
  63 +#end
  64 +#end
  65 + </template>
  66 + <view v-else class="text-center text-26rpx text-[#999]">
  67 + 暂无${subTable.classComment}
  68 + </view>
  69 + </view>
  70 +#end
  71 + </view>
  72 +#end
  73 + </view>
  74 +
  75 + <!-- 底部操作按钮 -->
  76 + <view class="yd-detail-footer">
  77 + <view class="yd-detail-footer-actions">
  78 + <wd-button
  79 + v-if="hasAccessByCodes(['${permissionPrefix}:update'])"
  80 + class="flex-1" type="warning" @click="handleEdit"
  81 + >
  82 + 编辑
  83 + </wd-button>
  84 + <wd-button
  85 + v-if="hasAccessByCodes(['${permissionPrefix}:delete'])"
  86 + class="flex-1" type="danger" :loading="deleting" @click="handleDelete"
  87 + >
  88 + 删除
  89 + </wd-button>
  90 + </view>
  91 + </view>
  92 + </view>
  93 +</template>
  94 +
  95 +<script lang="ts" setup>
  96 +#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
  97 +#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "float" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
  98 +#set ($primaryTsType = "number")
  99 +#else
  100 +#set ($primaryTsType = "string")
  101 +#end
  102 +#set ($hasDict = 0)
  103 +#set ($hasDateTime = 0)
  104 +#foreach($column in $columns)
  105 +#if ($column.dictType && "" != $column.dictType)
  106 +#set ($hasDict = 1)
  107 +#end
  108 +#if ($column.javaType == "LocalDateTime")
  109 +#set ($hasDateTime = 1)
  110 +#end
  111 +#end
  112 +import type {
  113 + ${simpleClassName},
  114 +#foreach ($subSimpleClassName in $subSimpleClassNames)
  115 + ${subSimpleClassName},
  116 +#end
  117 +} from '@/api/${table.moduleName}/${table.businessName}'
  118 +import { onShow } from '@dcloudio/uni-app'
  119 +import { ref } from 'vue'
  120 +import { useDialog } from '@wot-ui/ui/components/wd-dialog'
  121 +import { useToast } from '@wot-ui/ui/components/wd-toast'
  122 +import {
  123 + delete${simpleClassName},
  124 + get${simpleClassName},
  125 +#foreach ($subTable in $subTables)
  126 +#set ($index = $foreach.count - 1)
  127 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  128 +#set ($subJoinColumn = $subJoinColumns.get($index))
  129 +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + $subJoinColumn.javaField.substring(1))
  130 +#if ($subTable.subJoinMany)
  131 + get${subSimpleClassName}ListBy${SubJoinColumnName},
  132 +#else
  133 + get${subSimpleClassName}By${SubJoinColumnName},
  134 +#end
  135 +#end
  136 +} from '@/api/${table.moduleName}/${table.businessName}'
  137 +import { useAccess } from '@/hooks/useAccess'
  138 +import { delay, navigateBackPlus } from '@/utils'
  139 +#if ($hasDict == 1)
  140 +import { DICT_TYPE } from '@/utils/constants'
  141 +#end
  142 +#if ($hasDateTime == 1)
  143 +import { formatDateTime } from '@/utils/date'
  144 +#end
  145 +
  146 +const props = defineProps<{
  147 + id?: ${primaryTsType}
  148 +}>()
  149 +
  150 +definePage({
  151 + style: {
  152 + navigationBarTitleText: '',
  153 + navigationStyle: 'custom',
  154 + },
  155 +})
  156 +
  157 +const { hasAccessByCodes } = useAccess()
  158 +const dialog = useDialog()
  159 +const toast = useToast()
  160 +const formData = ref<${simpleClassName}>() // 主表数据
  161 +#foreach ($subTable in $subTables)
  162 +#set ($index = $foreach.count - 1)
  163 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  164 +#set ($subClassNameVar = $subClassNameVars.get($index))
  165 +#if ($subTable.subJoinMany)
  166 +const ${subClassNameVar}s = ref<${subSimpleClassName}[]>([]) // ${subTable.classComment}数据
  167 +#else
  168 +const ${subClassNameVar} = ref<${subSimpleClassName} | null>(null) // ${subTable.classComment}数据
  169 +#end
  170 +#end
  171 +const deleting = ref(false) // 删除状态
  172 +
  173 +/** 返回上一页 */
  174 +function handleBack() {
  175 + navigateBackPlus('/pages-${table.moduleName}/${table.businessName}/index')
  176 +}
  177 +
  178 +/** 加载主表和子表详情 */
  179 +async function getDetail() {
  180 + if (!props.id || deleting.value) {
  181 + return
  182 + }
  183 + const [data#foreach ($subTable in $subTables)#set ($index = $foreach.count - 1), ${subClassNameVars.get($index)}Data#end] = await Promise.all([
  184 + get${simpleClassName}(props.id),
  185 +#foreach ($subTable in $subTables)
  186 +#set ($index = $foreach.count - 1)
  187 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  188 +#set ($subJoinColumn = $subJoinColumns.get($index))
  189 +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + $subJoinColumn.javaField.substring(1))
  190 +#if ($subTable.subJoinMany)
  191 + get${subSimpleClassName}ListBy${SubJoinColumnName}(props.id),
  192 +#else
  193 + get${subSimpleClassName}By${SubJoinColumnName}(props.id),
  194 +#end
  195 +#end
  196 + ])
  197 + formData.value = data
  198 +#foreach ($subTable in $subTables)
  199 +#set ($index = $foreach.count - 1)
  200 +#set ($subClassNameVar = $subClassNameVars.get($index))
  201 +#if ($subTable.subJoinMany)
  202 + ${subClassNameVar}s.value = ${subClassNameVar}Data || []
  203 +#else
  204 + ${subClassNameVar}.value = ${subClassNameVar}Data || null
  205 +#end
  206 +#end
  207 +}
  208 +
  209 +/** 编辑${table.classComment} */
  210 +function handleEdit() {
  211 + uni.navigateTo({ url: `/pages-${table.moduleName}/${table.businessName}/form/index?id=#[[${]]#props.id#[[}]]#` })
  212 +}
  213 +
  214 +/** 删除${table.classComment} */
  215 +async function handleDelete() {
  216 + if (!props.id) {
  217 + return
  218 + }
  219 + try {
  220 + await dialog.confirm({ title: '提示', msg: '确定要删除该${table.classComment}吗?' })
  221 + } catch {
  222 + return
  223 + }
  224 + deleting.value = true
  225 + try {
  226 + await delete${simpleClassName}(props.id)
  227 + toast.success('删除成功')
  228 + uni.$emit('${table.moduleName}:${table.businessName}:reload')
  229 + delay(handleBack)
  230 + } finally {
  231 + deleting.value = false
  232 + }
  233 +}
  234 +
  235 +/** 进入页面加载(含编辑返回后刷新) */
  236 +onShow(() => {
  237 + getDetail()
  238 +})
  239 +</script>
yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/form/index.vue.vm
@@ -12,34 +12,48 @@ @@ -12,34 +12,48 @@
12 <wd-form ref="formRef" :model="formData" :schema="formSchema"> 12 <wd-form ref="formRef" :model="formData" :schema="formSchema">
13 <wd-cell-group border> 13 <wd-cell-group border>
14 #foreach($column in $columns) 14 #foreach($column in $columns)
15 - #if (($column.createOperation || $column.updateOperation) && !$column.primaryKey)  
16 - #set ($dictType = $column.dictType)  
17 - #set ($javaField = $column.javaField)  
18 - #set ($javaType = $column.javaType)  
19 - #set ($comment = $column.columnComment)  
20 - #set ($dictMethod = "getDictOptions")  
21 - #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")  
22 - #set ($dictMethod = "getIntDictOptions")  
23 - #elseif ($javaType == "String")  
24 - #set ($dictMethod = "getStrDictOptions")  
25 - #elseif ($javaType == "Boolean")  
26 - #set ($dictMethod = "getBoolDictOptions")  
27 - #end  
28 - ## 下拉选择(字典)→ yd-form-picker,内部按 dictType 解析选项,适合选项较多  
29 - #if ($column.htmlType == "select" && $dictType && "" != $dictType) 15 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey)
  16 +#set ($dictType = $column.dictType)
  17 +#set ($javaField = $column.javaField)
  18 +#set ($javaType = $column.javaType)
  19 +#set ($comment = $column.columnComment)
  20 +#set ($dictMethod = "getDictOptions")
  21 +#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
  22 +#set ($dictMethod = "getIntDictOptions")
  23 +#elseif ($javaType == "String")
  24 +#set ($dictMethod = "getStrDictOptions")
  25 +#elseif ($javaType == "Boolean")
  26 +#set ($dictMethod = "getBoolDictOptions")
  27 +#end
  28 +## 树表的父级选择
  29 +#if ($table.templateType == 2 && $column.id == $treeParentColumn.id)
  30 + <yd-tree-select
  31 + v-model="formData.${javaField}"
  32 + :data="treeData"
  33 + label="${comment}"
  34 + prop="${javaField}"
  35 + :props="{ label: '${treeNameColumn.javaField}', value: '${primaryColumn.javaField}' }"
  36 + placeholder="请选择${comment}"
  37 + />
  38 +## 下拉选择(字典)→ yd-form-picker,内部按 dictType 解析选项,适合选项较多
  39 +#elseif ($column.htmlType == "select" && $dictType && "" != $dictType)
30 <yd-form-picker 40 <yd-form-picker
31 v-model="formData.${javaField}" 41 v-model="formData.${javaField}"
32 label="${comment}" 42 label="${comment}"
33 label-width="180rpx" 43 label-width="180rpx"
34 prop="${javaField}" 44 prop="${javaField}"
  45 +#if ($javaType == "Boolean")
  46 + :columns="getBoolDictOptions(DICT_TYPE.${dictType.toUpperCase()})"
  47 +#else
35 :dict-type="DICT_TYPE.${dictType.toUpperCase()}" 48 :dict-type="DICT_TYPE.${dictType.toUpperCase()}"
36 #if ($javaType == "String") 49 #if ($javaType == "String")
37 dict-kind="str" 50 dict-kind="str"
38 #end 51 #end
  52 +#end
39 placeholder="请选择${comment}" 53 placeholder="请选择${comment}"
40 /> 54 />
41 - ## 单选(字典)→ radio-group,适合选项较少  
42 - #elseif ($column.htmlType == "radio" && $dictType && "" != $dictType) 55 +## 单选(字典)→ radio-group,适合选项较少
  56 +#elseif ($column.htmlType == "radio" && $dictType && "" != $dictType)
43 <wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}" center> 57 <wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}" center>
44 <wd-radio-group v-model="formData.${javaField}" type="button"> 58 <wd-radio-group v-model="formData.${javaField}" type="button">
45 <wd-radio 59 <wd-radio
@@ -51,70 +65,78 @@ @@ -51,70 +65,78 @@
51 </wd-radio> 65 </wd-radio>
52 </wd-radio-group> 66 </wd-radio-group>
53 </wd-form-item> 67 </wd-form-item>
54 - ## 数字类型(无字典)  
55 - #elseif (${javaType.toLowerCase()} == "long" || ${javaType.toLowerCase()} == "integer" || ${javaType.toLowerCase()} == "short" || ${javaType.toLowerCase()} == "double" || ${javaType.toLowerCase()} == "bigdecimal" || ${javaType.toLowerCase()} == "byte") 68 +## 数字类型(无字典)
  69 +#elseif (${javaType.toLowerCase()} == "long" || ${javaType.toLowerCase()} == "integer" || ${javaType.toLowerCase()} == "short" || ${javaType.toLowerCase()} == "float" || ${javaType.toLowerCase()} == "double" || ${javaType.toLowerCase()} == "bigdecimal" || ${javaType.toLowerCase()} == "byte")
56 <wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}"> 70 <wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}">
57 <wd-input-number 71 <wd-input-number
58 v-model="formData.${javaField}" 72 v-model="formData.${javaField}"
59 -#if (${javaType.toLowerCase()} == "double" || ${javaType.toLowerCase()} == "bigdecimal") 73 +#if ($column.nullable)
  74 + allow-null
  75 +#end
  76 +#if (${javaType.toLowerCase()} == "float" || ${javaType.toLowerCase()} == "double" || ${javaType.toLowerCase()} == "bigdecimal")
60 :precision="2" 77 :precision="2"
61 input-type="number" 78 input-type="number"
62 #end 79 #end
63 :min="0" 80 :min="0"
64 /> 81 />
65 </wd-form-item> 82 </wd-form-item>
66 - ## 布尔类型  
67 - #elseif (${javaType.toLowerCase()} == "boolean") 83 +## 布尔类型
  84 +#elseif (${javaType.toLowerCase()} == "boolean")
68 <wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}" center> 85 <wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}" center>
69 <wd-switch v-model="formData.${javaField}" /> 86 <wd-switch v-model="formData.${javaField}" />
70 </wd-form-item> 87 </wd-form-item>
71 - ## 日期时间类型  
72 - #elseif (${javaType.toLowerCase()} == "date" || ${javaType.toLowerCase()} == "localdate" || ${javaType.toLowerCase()} == "localdatetime")  
73 - #set ($pickerType = "date")  
74 - #if (${javaType.toLowerCase()} == "localdatetime")  
75 - #set ($pickerType = "datetime")  
76 - #end 88 +## 日期时间类型
  89 +#elseif (${javaType.toLowerCase()} == "date" || ${javaType.toLowerCase()} == "localdate" || ${javaType.toLowerCase()} == "localdatetime")
  90 +#set ($pickerType = "date")
  91 +#if (${javaType.toLowerCase()} == "localdatetime")
  92 +#set ($pickerType = "datetime")
  93 +#end
77 <wd-form-item 94 <wd-form-item
78 title="${comment}" 95 title="${comment}"
79 title-width="180rpx" 96 title-width="180rpx"
80 prop="${javaField}" 97 prop="${javaField}"
81 is-link 98 is-link
82 #if ($pickerType == "datetime") 99 #if ($pickerType == "datetime")
83 - :value="formatDateTime(formData.${javaField})" 100 + :value="formData.${javaField} ? formatDateTime(formData.${javaField}) : ''"
84 #else 101 #else
85 - :value="formatDate(formData.${javaField})" 102 + :value="formData.${javaField} ? formatDate(formData.${javaField}) : ''"
86 #end 103 #end
87 placeholder="请选择${comment}" 104 placeholder="请选择${comment}"
88 @click="${javaField}Visible = true" 105 @click="${javaField}Visible = true"
89 /> 106 />
90 <wd-datetime-picker 107 <wd-datetime-picker
91 - v-model="formData.${javaField}"  
92 v-model:visible="${javaField}Visible" 108 v-model:visible="${javaField}Visible"
  109 + :model-value="formData.${javaField} || Date.now()"
93 type="${pickerType}" 110 type="${pickerType}"
  111 + @update:model-value="value => formData.${javaField} = value"
94 /> 112 />
95 - ## 文本域  
96 - #elseif ($column.htmlType == "textarea") 113 +## 文本域
  114 +#elseif ($column.htmlType == "textarea" || $column.htmlType == "editor")
97 <wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}"> 115 <wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}">
98 <wd-textarea 116 <wd-textarea
99 v-model="formData.${javaField}" 117 v-model="formData.${javaField}"
100 clearable 118 clearable
101 placeholder="请输入${comment}" 119 placeholder="请输入${comment}"
  120 +#if ($column.htmlType == "editor")
  121 + :maxlength="2000"
  122 +#else
102 :maxlength="200" 123 :maxlength="200"
  124 +#end
103 show-word-limit 125 show-word-limit
104 /> 126 />
105 </wd-form-item> 127 </wd-form-item>
106 - ## 图片上传 → yd-upload-img(包 wd-form-item 才能注册到 wd-form 做校验/错误展示)  
107 - #elseif ($column.htmlType == "imageUpload") 128 +## 图片上传 → yd-upload-img(包 wd-form-item 才能注册到 wd-form 做校验/错误展示)
  129 +#elseif ($column.htmlType == "imageUpload")
108 <wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}"> 130 <wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}">
109 <yd-upload-img v-model="formData.${javaField}" directory="${table.moduleName}/${table.businessName}" /> 131 <yd-upload-img v-model="formData.${javaField}" directory="${table.moduleName}/${table.businessName}" />
110 </wd-form-item> 132 </wd-form-item>
111 - ## 文件上传 → yd-upload-file  
112 - #elseif ($column.htmlType == "fileUpload") 133 +## 文件上传 → yd-upload-file
  134 +#elseif ($column.htmlType == "fileUpload")
113 <wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}"> 135 <wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}">
114 - <yd-upload-file v-model="formData.${javaField}" directory="${table.moduleName}/${table.businessName}" /> 136 + <yd-upload-file v-model="formData.${javaField}" :limit="1" directory="${table.moduleName}/${table.businessName}" />
115 </wd-form-item> 137 </wd-form-item>
116 - ## 默认:文本输入  
117 - #else 138 +## 默认:文本输入
  139 +#else
118 <wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}"> 140 <wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}">
119 <wd-input 141 <wd-input
120 v-model="formData.${javaField}" 142 v-model="formData.${javaField}"
@@ -122,11 +144,277 @@ @@ -122,11 +144,277 @@
122 placeholder="请输入${comment}" 144 placeholder="请输入${comment}"
123 /> 145 />
124 </wd-form-item> 146 </wd-form-item>
125 - #end  
126 - #end 147 +#end
  148 +#end
127 #end 149 #end
128 </wd-cell-group> 150 </wd-cell-group>
129 </wd-form> 151 </wd-form>
  152 +#if ($table.templateType == 10 || $table.templateType == 12)
  153 +#foreach ($subTable in $subTables)
  154 +#set ($index = $foreach.count - 1)
  155 +#set ($subColumns = $subColumnsList.get($index))
  156 +#set ($subJoinColumn = $subJoinColumns.get($index))
  157 +#set ($subClassNameVar = $subClassNameVars.get($index))
  158 +
  159 + <!-- 子表:${subTable.classComment} -->
  160 + <view class="mt-20rpx flex items-center justify-between px-24rpx py-16rpx">
  161 + <text class="text-28rpx text-[#333] font-semibold">${subTable.classComment}</text>
  162 +#if ($subTable.subJoinMany)
  163 + <wd-button size="small" type="primary" variant="plain" @click="add${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}">
  164 + 添加
  165 + </wd-button>
  166 +#else
  167 + <wd-button
  168 + v-if="!${subClassNameVar}"
  169 + size="small"
  170 + type="primary"
  171 + variant="plain"
  172 + @click="add${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}()"
  173 + >
  174 + 添加
  175 + </wd-button>
  176 +#end
  177 + </view>
  178 + <view class="px-24rpx">
  179 +#if ($subTable.subJoinMany)
  180 + <view v-if="!${subClassNameVar}s.length" class="rounded-12rpx bg-white py-40rpx text-center text-26rpx text-[#999]">
  181 + 暂无${subTable.classComment}
  182 + </view>
  183 + <view
  184 + v-for="(item, index) in ${subClassNameVar}s"
  185 + :key="index"
  186 + class="mb-20rpx rounded-12rpx bg-white p-24rpx shadow-sm"
  187 + >
  188 + <view class="mb-16rpx flex justify-end">
  189 + <wd-button size="small" type="danger" variant="plain" @click="remove${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(index)">
  190 + 删除
  191 + </wd-button>
  192 + </view>
  193 +#foreach ($column in $subColumns)
  194 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
  195 +#set ($javaType = $column.javaType.toLowerCase())
  196 +#set ($dictMethod = "getDictOptions")
  197 +#if ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short")
  198 +#set ($dictMethod = "getIntDictOptions")
  199 +#elseif ($column.javaType == "String")
  200 +#set ($dictMethod = "getStrDictOptions")
  201 +#elseif ($column.javaType == "Boolean")
  202 +#set ($dictMethod = "getBoolDictOptions")
  203 +#end
  204 +#if ($column.htmlType == "select" && $column.dictType && "" != $column.dictType)
  205 + <yd-form-picker
  206 + v-model="item.${column.javaField}"
  207 + label="${column.columnComment}"
  208 + label-width="180rpx"
  209 +#if ($column.javaType == "Boolean")
  210 + :columns="getBoolDictOptions(DICT_TYPE.${column.dictType.toUpperCase()})"
  211 +#else
  212 + :dict-type="DICT_TYPE.${column.dictType.toUpperCase()}"
  213 +#if ($column.javaType == "String")
  214 + dict-kind="str"
  215 +#end
  216 +#end
  217 + placeholder="请选择${column.columnComment}"
  218 + />
  219 +#elseif ($column.htmlType == "radio" && $column.dictType && "" != $column.dictType)
  220 + <wd-form-item title="${column.columnComment}" title-width="180rpx" center>
  221 + <wd-radio-group v-model="item.${column.javaField}" type="button">
  222 + <wd-radio
  223 + v-for="dict in $dictMethod(DICT_TYPE.${column.dictType.toUpperCase()})"
  224 + :key="dict.value"
  225 + :value="dict.value"
  226 + >
  227 + {{ dict.label }}
  228 + </wd-radio>
  229 + </wd-radio-group>
  230 + </wd-form-item>
  231 +#elseif ($javaType == "boolean")
  232 + <wd-form-item title="${column.columnComment}" title-width="180rpx" center>
  233 + <wd-switch v-model="item.${column.javaField}" />
  234 + </wd-form-item>
  235 +#elseif ($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte")
  236 + <wd-form-item title="${column.columnComment}" title-width="180rpx">
  237 + <wd-input-number
  238 + v-model="item.${column.javaField}"
  239 +#if($column.nullable)
  240 + allow-null
  241 +#end
  242 +#if ($javaType == "float" || $javaType == "double" || $javaType == "bigdecimal")
  243 + :precision="2"
  244 + input-type="number"
  245 +#end
  246 + :min="0"
  247 + />
  248 + </wd-form-item>
  249 +#elseif ($javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime")
  250 +#set ($subPickerType = "date")
  251 +#if ($javaType == "localdatetime")
  252 +#set ($subPickerType = "datetime")
  253 +#end
  254 + <wd-form-item
  255 + title="${column.columnComment}"
  256 + title-width="180rpx"
  257 + is-link
  258 +#if ($javaType == "localdatetime")
  259 + :value="item.${column.javaField} ? formatDateTime(item.${column.javaField}) : ''"
  260 +#else
  261 + :value="item.${column.javaField} ? formatDate(item.${column.javaField}) : ''"
  262 +#end
  263 + placeholder="请选择${column.columnComment}"
  264 + @click="subDateVisible[`${subClassNameVar}s.#[[${]]#index#[[}]]#.${column.javaField}`] = true"
  265 + />
  266 + <wd-datetime-picker
  267 + v-model:visible="subDateVisible[`${subClassNameVar}s.#[[${]]#index#[[}]]#.${column.javaField}`]"
  268 + :model-value="item.${column.javaField} || Date.now()"
  269 + type="${subPickerType}"
  270 + @update:model-value="value => item.${column.javaField} = value"
  271 + />
  272 +#elseif ($column.htmlType == "textarea" || $column.htmlType == "editor")
  273 + <wd-form-item title="${column.columnComment}" title-width="180rpx">
  274 + <wd-textarea
  275 + v-model="item.${column.javaField}"
  276 + clearable
  277 + placeholder="请输入${column.columnComment}"
  278 +#if ($column.htmlType == "editor")
  279 + :maxlength="2000"
  280 +#else
  281 + :maxlength="200"
  282 +#end
  283 + />
  284 + </wd-form-item>
  285 +#elseif ($column.htmlType == "imageUpload")
  286 + <wd-form-item title="${column.columnComment}" title-width="180rpx">
  287 + <yd-upload-img v-model="item.${column.javaField}" directory="${table.moduleName}/${table.businessName}" />
  288 + </wd-form-item>
  289 +#elseif ($column.htmlType == "fileUpload")
  290 + <wd-form-item title="${column.columnComment}" title-width="180rpx">
  291 + <yd-upload-file v-model="item.${column.javaField}" :limit="1" directory="${table.moduleName}/${table.businessName}" />
  292 + </wd-form-item>
  293 +#else
  294 + <wd-form-item title="${column.columnComment}" title-width="180rpx">
  295 + <wd-input v-model="item.${column.javaField}" clearable placeholder="请输入${column.columnComment}" />
  296 + </wd-form-item>
  297 +#end
  298 +#end
  299 +#end
  300 + </view>
  301 +#else
  302 + <view v-if="!${subClassNameVar}" class="rounded-12rpx bg-white py-40rpx text-center text-26rpx text-[#999]">
  303 + 暂无${subTable.classComment}
  304 + </view>
  305 + <view v-else class="rounded-12rpx bg-white p-24rpx shadow-sm">
  306 +#foreach ($column in $subColumns)
  307 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
  308 +#set ($javaType = $column.javaType.toLowerCase())
  309 +#set ($dictMethod = "getDictOptions")
  310 +#if ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short")
  311 +#set ($dictMethod = "getIntDictOptions")
  312 +#elseif ($column.javaType == "String")
  313 +#set ($dictMethod = "getStrDictOptions")
  314 +#elseif ($column.javaType == "Boolean")
  315 +#set ($dictMethod = "getBoolDictOptions")
  316 +#end
  317 +#if ($column.htmlType == "select" && $column.dictType && "" != $column.dictType)
  318 + <yd-form-picker
  319 + v-model="${subClassNameVar}.${column.javaField}"
  320 + label="${column.columnComment}"
  321 + label-width="180rpx"
  322 +#if ($column.javaType == "Boolean")
  323 + :columns="getBoolDictOptions(DICT_TYPE.${column.dictType.toUpperCase()})"
  324 +#else
  325 + :dict-type="DICT_TYPE.${column.dictType.toUpperCase()}"
  326 +#if ($column.javaType == "String")
  327 + dict-kind="str"
  328 +#end
  329 +#end
  330 + placeholder="请选择${column.columnComment}"
  331 + />
  332 +#elseif ($column.htmlType == "radio" && $column.dictType && "" != $column.dictType)
  333 + <wd-form-item title="${column.columnComment}" title-width="180rpx" center>
  334 + <wd-radio-group v-model="${subClassNameVar}.${column.javaField}" type="button">
  335 + <wd-radio
  336 + v-for="dict in $dictMethod(DICT_TYPE.${column.dictType.toUpperCase()})"
  337 + :key="dict.value"
  338 + :value="dict.value"
  339 + >
  340 + {{ dict.label }}
  341 + </wd-radio>
  342 + </wd-radio-group>
  343 + </wd-form-item>
  344 +#elseif ($javaType == "boolean")
  345 + <wd-form-item title="${column.columnComment}" title-width="180rpx" center>
  346 + <wd-switch v-model="${subClassNameVar}.${column.javaField}" />
  347 + </wd-form-item>
  348 +#elseif ($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte")
  349 + <wd-form-item title="${column.columnComment}" title-width="180rpx">
  350 + <wd-input-number
  351 + v-model="${subClassNameVar}.${column.javaField}"
  352 +#if($column.nullable)
  353 + allow-null
  354 +#end
  355 +#if ($javaType == "float" || $javaType == "double" || $javaType == "bigdecimal")
  356 + :precision="2"
  357 + input-type="number"
  358 +#end
  359 + :min="0"
  360 + />
  361 + </wd-form-item>
  362 +#elseif ($javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime")
  363 +#set ($subPickerType = "date")
  364 +#if ($javaType == "localdatetime")
  365 +#set ($subPickerType = "datetime")
  366 +#end
  367 + <wd-form-item
  368 + title="${column.columnComment}"
  369 + title-width="180rpx"
  370 + is-link
  371 +#if ($javaType == "localdatetime")
  372 + :value="${subClassNameVar}.${column.javaField} ? formatDateTime(${subClassNameVar}.${column.javaField}) : ''"
  373 +#else
  374 + :value="${subClassNameVar}.${column.javaField} ? formatDate(${subClassNameVar}.${column.javaField}) : ''"
  375 +#end
  376 + placeholder="请选择${column.columnComment}"
  377 + @click="subDateVisible['${subClassNameVar}.${column.javaField}'] = true"
  378 + />
  379 + <wd-datetime-picker
  380 + v-model:visible="subDateVisible['${subClassNameVar}.${column.javaField}']"
  381 + :model-value="${subClassNameVar}.${column.javaField} || Date.now()"
  382 + type="${subPickerType}"
  383 + @update:model-value="value => ${subClassNameVar}.${column.javaField} = value"
  384 + />
  385 +#elseif ($column.htmlType == "textarea" || $column.htmlType == "editor")
  386 + <wd-form-item title="${column.columnComment}" title-width="180rpx">
  387 + <wd-textarea
  388 + v-model="${subClassNameVar}.${column.javaField}"
  389 + clearable
  390 + placeholder="请输入${column.columnComment}"
  391 +#if ($column.htmlType == "editor")
  392 + :maxlength="2000"
  393 +#else
  394 + :maxlength="200"
  395 +#end
  396 + />
  397 + </wd-form-item>
  398 +#elseif ($column.htmlType == "imageUpload")
  399 + <wd-form-item title="${column.columnComment}" title-width="180rpx">
  400 + <yd-upload-img v-model="${subClassNameVar}.${column.javaField}" directory="${table.moduleName}/${table.businessName}" />
  401 + </wd-form-item>
  402 +#elseif ($column.htmlType == "fileUpload")
  403 + <wd-form-item title="${column.columnComment}" title-width="180rpx">
  404 + <yd-upload-file v-model="${subClassNameVar}.${column.javaField}" :limit="1" directory="${table.moduleName}/${table.businessName}" />
  405 + </wd-form-item>
  406 +#else
  407 + <wd-form-item title="${column.columnComment}" title-width="180rpx">
  408 + <wd-input v-model="${subClassNameVar}.${column.javaField}" clearable placeholder="请输入${column.columnComment}" />
  409 + </wd-form-item>
  410 +#end
  411 +#end
  412 +#end
  413 + </view>
  414 +#end
  415 + </view>
  416 +#end
  417 +#end
130 </view> 418 </view>
131 419
132 <!-- 底部保存按钮 --> 420 <!-- 底部保存按钮 -->
@@ -146,7 +434,7 @@ @@ -146,7 +434,7 @@
146 <script lang="ts" setup> 434 <script lang="ts" setup>
147 import type { FormInstance } from '@wot-ui/ui/components/wd-form/types' 435 import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
148 #set ($primaryJavaType = $primaryColumn.javaType.toLowerCase()) 436 #set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
149 -#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte") 437 +#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "float" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
150 #set ($primaryTsType = "number") 438 #set ($primaryTsType = "number")
151 #else 439 #else
152 #set ($primaryTsType = "string") 440 #set ($primaryTsType = "string")
@@ -154,54 +442,113 @@ import type { FormInstance } from &#39;@wot-ui/ui/components/wd-form/types&#39; @@ -154,54 +442,113 @@ import type { FormInstance } from &#39;@wot-ui/ui/components/wd-form/types&#39;
154 #set ($hasDict = 0) 442 #set ($hasDict = 0)
155 #set ($hasDate = 0) 443 #set ($hasDate = 0)
156 #set ($hasDateTime = 0) 444 #set ($hasDateTime = 0)
  445 +#set ($hasSubDate = 0)
157 #set ($hasGetDictOptions = 0) 446 #set ($hasGetDictOptions = 0)
158 #set ($hasGetIntDictOptions = 0) 447 #set ($hasGetIntDictOptions = 0)
159 #set ($hasGetStrDictOptions = 0) 448 #set ($hasGetStrDictOptions = 0)
160 #set ($hasGetBoolDictOptions = 0) 449 #set ($hasGetBoolDictOptions = 0)
161 #foreach ($column in $columns) 450 #foreach ($column in $columns)
162 - #if (($column.createOperation || $column.updateOperation) && !$column.primaryKey 451 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
163 && ($column.htmlType == "select" || $column.htmlType == "radio") 452 && ($column.htmlType == "select" || $column.htmlType == "radio")
164 && $column.dictType && "" != $column.dictType) 453 && $column.dictType && "" != $column.dictType)
165 - #set ($hasDict = 1)  
166 - #if ($column.htmlType == "radio" && ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short"))  
167 - #set ($hasGetIntDictOptions = 1)  
168 - #elseif ($column.htmlType == "radio" && $column.javaType == "String")  
169 - #set ($hasGetStrDictOptions = 1)  
170 - #elseif ($column.htmlType == "radio" && $column.javaType == "Boolean")  
171 - #set ($hasGetBoolDictOptions = 1)  
172 - #elseif ($column.htmlType == "radio")  
173 - #set ($hasGetDictOptions = 1)  
174 - #end  
175 - #end  
176 - #if (($column.createOperation || $column.updateOperation) && !$column.primaryKey 454 +#set ($hasDict = 1)
  455 +#if ($column.htmlType == "radio" && ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short"))
  456 +#set ($hasGetIntDictOptions = 1)
  457 +#elseif ($column.htmlType == "radio" && $column.javaType == "String")
  458 +#set ($hasGetStrDictOptions = 1)
  459 +#elseif (($column.htmlType == "select" || $column.htmlType == "radio") && $column.javaType == "Boolean")
  460 +#set ($hasGetBoolDictOptions = 1)
  461 +#elseif ($column.htmlType == "radio")
  462 +#set ($hasGetDictOptions = 1)
  463 +#end
  464 +#end
  465 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
177 && (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate")) 466 && (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate"))
178 - #set ($hasDate = 1)  
179 - #end  
180 - #if (($column.createOperation || $column.updateOperation) && !$column.primaryKey 467 +#set ($hasDate = 1)
  468 +#end
  469 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
181 && ${column.javaType.toLowerCase()} == "localdatetime") 470 && ${column.javaType.toLowerCase()} == "localdatetime")
182 - #set ($hasDateTime = 1)  
183 - #end 471 +#set ($hasDateTime = 1)
  472 +#end
  473 +#end
  474 +#if ($table.templateType == 10 || $table.templateType == 12)
  475 +#foreach ($subColumns in $subColumnsList)
  476 +#foreach ($column in $subColumns)
  477 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
  478 + && ($column.htmlType == "select" || $column.htmlType == "radio")
  479 + && $column.dictType && "" != $column.dictType)
  480 +#set ($hasDict = 1)
  481 +#if ($column.htmlType == "radio" && ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short"))
  482 +#set ($hasGetIntDictOptions = 1)
  483 +#elseif ($column.htmlType == "radio" && $column.javaType == "String")
  484 +#set ($hasGetStrDictOptions = 1)
  485 +#elseif (($column.htmlType == "select" || $column.htmlType == "radio") && $column.javaType == "Boolean")
  486 +#set ($hasGetBoolDictOptions = 1)
  487 +#elseif ($column.htmlType == "radio")
  488 +#set ($hasGetDictOptions = 1)
  489 +#end
  490 +#end
  491 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
  492 + && (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate"))
  493 +#set ($hasDate = 1)
  494 +#set ($hasSubDate = 1)
  495 +#end
  496 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
  497 + && ${column.javaType.toLowerCase()} == "localdatetime")
  498 +#set ($hasDateTime = 1)
  499 +#set ($hasSubDate = 1)
  500 +#end
  501 +#end
  502 +#end
184 #end 503 #end
185 -import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}' 504 +import type {
  505 + ${simpleClassName},
  506 +#if ($table.templateType == 10 || $table.templateType == 12)
  507 +#foreach ($subSimpleClassName in $subSimpleClassNames)
  508 + ${subSimpleClassName},
  509 +#end
  510 +#end
  511 +} from '@/api/${table.moduleName}/${table.businessName}'
186 import { computed, onMounted, ref } from 'vue' 512 import { computed, onMounted, ref } from 'vue'
187 import { useToast } from '@wot-ui/ui/components/wd-toast' 513 import { useToast } from '@wot-ui/ui/components/wd-toast'
  514 +#if ($table.templateType == 10 || $table.templateType == 12)
  515 +import {
  516 + create${simpleClassName},
  517 + get${simpleClassName},
  518 +#foreach ($subTable in $subTables)
  519 +#set ($index = $foreach.count - 1)
  520 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  521 +#set ($subJoinColumn = $subJoinColumns.get($index))
  522 +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + $subJoinColumn.javaField.substring(1))
  523 +#if ($subTable.subJoinMany)
  524 + get${subSimpleClassName}ListBy${SubJoinColumnName},
  525 +#else
  526 + get${subSimpleClassName}By${SubJoinColumnName},
  527 +#end
  528 +#end
  529 + update${simpleClassName},
  530 +} from '@/api/${table.moduleName}/${table.businessName}'
  531 +#elseif ($table.templateType == 2)
  532 +import { create${simpleClassName}, get${simpleClassName}, get${simpleClassName}List, update${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
  533 +#else
188 import { create${simpleClassName}, get${simpleClassName}, update${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}' 534 import { create${simpleClassName}, get${simpleClassName}, update${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
  535 +#end
189 #if ($hasGetDictOptions == 1 || $hasGetIntDictOptions == 1 || $hasGetStrDictOptions == 1 || $hasGetBoolDictOptions == 1) 536 #if ($hasGetDictOptions == 1 || $hasGetIntDictOptions == 1 || $hasGetStrDictOptions == 1 || $hasGetBoolDictOptions == 1)
190 - #set ($dictImportNames = "")  
191 - #if ($hasGetDictOptions == 1)  
192 - #set ($dictImportNames = "${dictImportNames}getDictOptions, ")  
193 - #end  
194 - #if ($hasGetIntDictOptions == 1)  
195 - #set ($dictImportNames = "${dictImportNames}getIntDictOptions, ")  
196 - #end  
197 - #if ($hasGetStrDictOptions == 1)  
198 - #set ($dictImportNames = "${dictImportNames}getStrDictOptions, ")  
199 - #end  
200 - #if ($hasGetBoolDictOptions == 1)  
201 - #set ($dictImportNames = "${dictImportNames}getBoolDictOptions, ")  
202 - #end  
203 - #set ($dictImportNames = $dictImportNames.trim())  
204 - #set ($dictImportNames = $dictImportNames.substring(0, $dictImportNames.length() - 1)) 537 +#set ($dictImportNames = "")
  538 +#if ($hasGetDictOptions == 1)
  539 +#set ($dictImportNames = "${dictImportNames}getDictOptions, ")
  540 +#end
  541 +#if ($hasGetIntDictOptions == 1)
  542 +#set ($dictImportNames = "${dictImportNames}getIntDictOptions, ")
  543 +#end
  544 +#if ($hasGetStrDictOptions == 1)
  545 +#set ($dictImportNames = "${dictImportNames}getStrDictOptions, ")
  546 +#end
  547 +#if ($hasGetBoolDictOptions == 1)
  548 +#set ($dictImportNames = "${dictImportNames}getBoolDictOptions, ")
  549 +#end
  550 +#set ($dictImportNames = $dictImportNames.trim())
  551 +#set ($dictImportNames = $dictImportNames.substring(0, $dictImportNames.length() - 1))
205 import { $dictImportNames } from '@/hooks/useDict' 552 import { $dictImportNames } from '@/hooks/useDict'
206 #end 553 #end
207 import { delay, navigateBackPlus } from '@/utils' 554 import { delay, navigateBackPlus } from '@/utils'
@@ -209,21 +556,27 @@ import { delay, navigateBackPlus } from &#39;@/utils&#39; @@ -209,21 +556,27 @@ import { delay, navigateBackPlus } from &#39;@/utils&#39;
209 import { DICT_TYPE } from '@/utils/constants' 556 import { DICT_TYPE } from '@/utils/constants'
210 #end 557 #end
211 #if ($hasDate == 1 || $hasDateTime == 1) 558 #if ($hasDate == 1 || $hasDateTime == 1)
212 - #set ($dateImportNames = "")  
213 - #if ($hasDate == 1)  
214 - #set ($dateImportNames = "${dateImportNames}formatDate, ")  
215 - #end  
216 - #if ($hasDateTime == 1)  
217 - #set ($dateImportNames = "${dateImportNames}formatDateTime, ")  
218 - #end  
219 - #set ($dateImportNames = $dateImportNames.trim())  
220 - #set ($dateImportNames = $dateImportNames.substring(0, $dateImportNames.length() - 1)) 559 +#set ($dateImportNames = "")
  560 +#if ($hasDate == 1)
  561 +#set ($dateImportNames = "${dateImportNames}formatDate, ")
  562 +#end
  563 +#if ($hasDateTime == 1)
  564 +#set ($dateImportNames = "${dateImportNames}formatDateTime, ")
  565 +#end
  566 +#set ($dateImportNames = $dateImportNames.trim())
  567 +#set ($dateImportNames = $dateImportNames.substring(0, $dateImportNames.length() - 1))
221 import { $dateImportNames } from '@/utils/date' 568 import { $dateImportNames } from '@/utils/date'
222 #end 569 #end
223 import { createFormSchema } from '@/utils/wot' 570 import { createFormSchema } from '@/utils/wot'
  571 +#if ($table.templateType == 2)
  572 +import { handleTree } from '@/utils/tree'
  573 +#end
224 574
225 const props = defineProps<{ 575 const props = defineProps<{
226 - id?: ${primaryTsType} | any 576 + id?: ${primaryTsType}
  577 +#if ($table.templateType == 2)
  578 + ${treeParentColumn.javaField}?: ${primaryTsType}
  579 +#end
227 }>() 580 }>()
228 581
229 definePage({ 582 definePage({
@@ -236,42 +589,165 @@ definePage({ @@ -236,42 +589,165 @@ definePage({
236 const toast = useToast() 589 const toast = useToast()
237 const getTitle = computed(() => props.id ? '编辑${table.classComment}' : '新增${table.classComment}') 590 const getTitle = computed(() => props.id ? '编辑${table.classComment}' : '新增${table.classComment}')
238 const formLoading = ref(false) // 表单提交状态 591 const formLoading = ref(false) // 表单提交状态
239 -const formData = ref<${simpleClassName}>({ 592 +const formData = ref<Partial<${simpleClassName}>>({
240 #foreach($column in $columns) 593 #foreach($column in $columns)
241 - #if (($column.createOperation || $column.updateOperation) || $column.primaryKey)  
242 - #set ($javaType = $column.javaType.toLowerCase())  
243 - #set ($javaFieldLower = $column.javaField.toLowerCase())  
244 - #set ($optional = $column.nullable || $column.primaryKey || $javaFieldLower == "createtime" || $javaFieldLower == "updatetime")  
245 - #if ($column.primaryKey) 594 +#if (($column.createOperation || $column.updateOperation) || $column.primaryKey)
  595 +#set ($javaType = $column.javaType.toLowerCase())
  596 +#set ($javaFieldLower = $column.javaField.toLowerCase())
  597 +#set ($optional = $column.nullable || $column.primaryKey || $javaFieldLower == "createtime" || $javaFieldLower == "updatetime")
  598 +#if ($column.primaryKey)
  599 + ${column.javaField}: undefined,
  600 +#elseif($table.templateType == 2 && $column.id == $treeParentColumn.id)
  601 + ${column.javaField}: props.${column.javaField} ?? 0,
  602 +#elseif(($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType)
  603 + ${column.javaField}: undefined,
  604 +#elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "float" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
  605 +#if($column.nullable)
246 ${column.javaField}: undefined, 606 ${column.javaField}: undefined,
247 - #elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte") 607 +#else
248 ${column.javaField}: 0, 608 ${column.javaField}: 0,
249 - #elseif(${javaType} == "boolean") 609 +#end
  610 +#elseif(${javaType} == "boolean")
250 ${column.javaField}: false, 611 ${column.javaField}: false,
251 - #elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime") 612 +#elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")
252 ${column.javaField}: undefined, 613 ${column.javaField}: undefined,
253 - #else 614 +#else
254 ${column.javaField}: '', 615 ${column.javaField}: '',
255 - #end  
256 - #end 616 +#end
  617 +#end
257 #end 618 #end
258 }) // 表单数据 619 }) // 表单数据
  620 +#if ($table.templateType == 10 || $table.templateType == 12)
  621 +#foreach ($subTable in $subTables)
  622 +#set ($index = $foreach.count - 1)
  623 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  624 +#set ($subClassNameVar = $subClassNameVars.get($index))
  625 +#set ($subColumns = $subColumnsList.get($index))
  626 +#set ($subJoinColumn = $subJoinColumns.get($index))
  627 +#if ($subTable.subJoinMany)
  628 +const ${subClassNameVar}s = ref<Partial<${subSimpleClassName}>[]>([]) // ${subTable.classComment}数据
  629 +#else
  630 +const ${subClassNameVar} = ref<Partial<${subSimpleClassName}> | null>(null) // ${subTable.classComment}数据
  631 +#end
  632 +#end
  633 +#end
259 #foreach($column in $columns) 634 #foreach($column in $columns)
260 - #if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime")) 635 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime"))
261 const ${column.javaField}Visible = ref(false) // ${column.columnComment}选择器状态 636 const ${column.javaField}Visible = ref(false) // ${column.columnComment}选择器状态
262 - #end 637 +#end
  638 +#end
  639 +#if ($hasSubDate == 1)
  640 +const subDateVisible = ref<Record<string, boolean>>({}) // 子表日期选择器状态
263 #end 641 #end
264 const formSchema = createFormSchema({ 642 const formSchema = createFormSchema({
265 #foreach($column in $columns) 643 #foreach($column in $columns)
266 - #set ($javaFieldLower = $column.javaField.toLowerCase())  
267 - #if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey 644 +#set ($javaFieldLower = $column.javaField.toLowerCase())
  645 +#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey
268 && $javaFieldLower != "createtime" && $javaFieldLower != "updatetime") 646 && $javaFieldLower != "createtime" && $javaFieldLower != "updatetime")
269 ${column.javaField}: [{ required: true, message: '${column.columnComment}不能为空' }], 647 ${column.javaField}: [{ required: true, message: '${column.columnComment}不能为空' }],
270 - #end 648 +#end
271 #end 649 #end
272 }) // 表单校验规则 650 }) // 表单校验规则
273 const formRef = ref<FormInstance>() // 表单组件引用 651 const formRef = ref<FormInstance>() // 表单组件引用
  652 +#if ($table.templateType == 2)
  653 +const treeData = ref<${simpleClassName}[]>([]) // 父级树形选项
  654 +#end
  655 +
  656 +#if ($table.templateType == 10 || $table.templateType == 12)
  657 +#foreach ($subTable in $subTables)
  658 +#set ($index = $foreach.count - 1)
  659 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  660 +#set ($subClassNameVar = $subClassNameVars.get($index))
  661 +#set ($subColumns = $subColumnsList.get($index))
  662 +#set ($subJoinColumn = $subJoinColumns.get($index))
  663 +#if ($subTable.subJoinMany)
  664 +#set ($subClassNameVar = $subClassNameVars.get($index))
  665 +#set ($subColumns = $subColumnsList.get($index))
  666 +#set ($subJoinColumn = $subJoinColumns.get($index))
  667 +/** 添加${subTable.classComment} */
  668 +function add${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}() {
  669 + ${subClassNameVar}s.value.push({
  670 +#foreach ($column in $subColumns)
  671 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
  672 +#set ($javaType = $column.javaType.toLowerCase())
  673 +#if((($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType) || $javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime")
  674 + ${column.javaField}: undefined,
  675 +#else
  676 + ${column.javaField}:#if($column.nullable) undefined#elseif($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte") 0#elseif($javaType == "boolean") false#else ''#end,
  677 +#end
  678 +#end
  679 +#end
  680 + })
  681 +}
274 682
  683 +/** 删除${subTable.classComment} */
  684 +function remove${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(index: number) {
  685 + ${subClassNameVar}s.value.splice(index, 1)
  686 +}
  687 +
  688 +#else
  689 +/** 添加${subTable.classComment} */
  690 +function add${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}() {
  691 + ${subClassNameVar}.value = {
  692 +#foreach ($column in $subColumns)
  693 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
  694 +#set ($javaType = $column.javaType.toLowerCase())
  695 +#if((($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType) || $javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime")
  696 + ${column.javaField}: undefined,
  697 +#else
  698 + ${column.javaField}:#if($column.nullable) undefined#elseif($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte") 0#elseif($javaType == "boolean") false#else ''#end,
  699 +#end
  700 +#end
  701 +#end
  702 + }
  703 +}
  704 +
  705 +#end
  706 +#end
  707 +/** 校验主子表数据 */
  708 +function isEmptySubValue(value: unknown) {
  709 + return value === undefined || value === null || value === ''
  710 +}
  711 +
  712 +function validateSubTables() {
  713 +#foreach ($subTable in $subTables)
  714 +#set ($index = $foreach.count - 1)
  715 +#set ($subClassNameVar = $subClassNameVars.get($index))
  716 +#set ($subColumns = $subColumnsList.get($index))
  717 +#set ($subJoinColumn = $subJoinColumns.get($index))
  718 +#if ($subTable.subJoinMany)
  719 + for (let index = 0; index < ${subClassNameVar}s.value.length; index++) {
  720 + const item = ${subClassNameVar}s.value[index]!
  721 +#foreach ($column in $subColumns)
  722 +#set ($javaFieldLower = $column.javaField.toLowerCase())
  723 +#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey
  724 + && $column.id != $subJoinColumn.id && $javaFieldLower != "createtime" && $javaFieldLower != "updatetime")
  725 + if (isEmptySubValue(item.${column.javaField})) {
  726 + toast.warning(`${subTable.classComment}第#[[${]]#index + 1#[[}]]#项的${column.columnComment}不能为空`)
  727 + return false
  728 + }
  729 +#end
  730 +#end
  731 + }
  732 +#else
  733 + if (${subClassNameVar}.value) {
  734 +#foreach ($column in $subColumns)
  735 +#set ($javaFieldLower = $column.javaField.toLowerCase())
  736 +#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey
  737 + && $column.id != $subJoinColumn.id && $javaFieldLower != "createtime" && $javaFieldLower != "updatetime")
  738 + if (isEmptySubValue(${subClassNameVar}.value.${column.javaField})) {
  739 + toast.warning('${subTable.classComment}的${column.columnComment}不能为空')
  740 + return false
  741 + }
  742 +#end
  743 +#end
  744 + }
  745 +#end
  746 +#end
  747 + return true
  748 +}
  749 +
  750 +#end
275 /** 返回上一页 */ 751 /** 返回上一页 */
276 function handleBack() { 752 function handleBack() {
277 navigateBackPlus('/pages-${table.moduleName}/${table.businessName}/index') 753 navigateBackPlus('/pages-${table.moduleName}/${table.businessName}/index')
@@ -282,7 +758,34 @@ async function getDetail() { @@ -282,7 +758,34 @@ async function getDetail() {
282 if (!props.id) { 758 if (!props.id) {
283 return 759 return
284 } 760 }
  761 +#if ($table.templateType == 10 || $table.templateType == 12)
  762 + const [data#foreach ($subTable in $subTables)#set ($index = $foreach.count - 1), ${subClassNameVars.get($index)}Data#end] = await Promise.all([
  763 + get${simpleClassName}(props.id),
  764 +#foreach ($subTable in $subTables)
  765 +#set ($index = $foreach.count - 1)
  766 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  767 +#set ($subJoinColumn = $subJoinColumns.get($index))
  768 +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + $subJoinColumn.javaField.substring(1))
  769 +#if ($subTable.subJoinMany)
  770 + get${subSimpleClassName}ListBy${SubJoinColumnName}(props.id),
  771 +#else
  772 + get${subSimpleClassName}By${SubJoinColumnName}(props.id),
  773 +#end
  774 +#end
  775 + ])
  776 + formData.value = data
  777 +#foreach ($subTable in $subTables)
  778 +#set ($index = $foreach.count - 1)
  779 +#set ($subClassNameVar = $subClassNameVars.get($index))
  780 +#if ($subTable.subJoinMany)
  781 + ${subClassNameVar}s.value = ${subClassNameVar}Data || []
  782 +#else
  783 + ${subClassNameVar}.value = ${subClassNameVar}Data || null
  784 +#end
  785 +#end
  786 +#else
285 formData.value = await get${simpleClassName}(props.id) 787 formData.value = await get${simpleClassName}(props.id)
  788 +#end
286 } 789 }
287 790
288 /** 提交表单 */ 791 /** 提交表单 */
@@ -291,14 +794,31 @@ async function handleSubmit() { @@ -291,14 +794,31 @@ async function handleSubmit() {
291 if (!valid) { 794 if (!valid) {
292 return 795 return
293 } 796 }
  797 +#if ($table.templateType == 10 || $table.templateType == 12)
  798 + if (!validateSubTables()) {
  799 + return
  800 + }
  801 +#end
294 802
295 formLoading.value = true 803 formLoading.value = true
296 try { 804 try {
  805 +#if ($table.templateType == 10 || $table.templateType == 12)
  806 + const data = {
  807 + ...formData.value,
  808 +#foreach ($subTable in $subTables)
  809 +#set ($index = $foreach.count - 1)
  810 +#set ($subClassNameVar = $subClassNameVars.get($index))
  811 + ${subClassNameVar}#if($subTable.subJoinMany)s#end: ${subClassNameVar}#if($subTable.subJoinMany)s#end.value,
  812 +#end
  813 + } as ${simpleClassName}
  814 +#else
  815 + const data = formData.value as ${simpleClassName}
  816 +#end
297 if (props.id) { 817 if (props.id) {
298 - await update${simpleClassName}(formData.value) 818 + await update${simpleClassName}(data)
299 toast.success('修改成功') 819 toast.success('修改成功')
300 } else { 820 } else {
301 - await create${simpleClassName}(formData.value) 821 + await create${simpleClassName}(data)
302 toast.success('新增成功') 822 toast.success('新增成功')
303 } 823 }
304 uni.$emit('${table.moduleName}:${table.businessName}:reload') 824 uni.$emit('${table.moduleName}:${table.businessName}:reload')
@@ -309,7 +829,16 @@ async function handleSubmit() { @@ -309,7 +829,16 @@ async function handleSubmit() {
309 } 829 }
310 830
311 /** 初始化 */ 831 /** 初始化 */
312 -onMounted(() => { 832 +onMounted(async () => {
  833 +#if ($table.templateType == 2)
  834 + const data = await get${simpleClassName}List()
  835 + treeData.value = [{
  836 + ${primaryColumn.javaField}: 0,
  837 + ${treeNameColumn.javaField}: '顶级${table.classComment}',
  838 + ${treeParentColumn.javaField}: 0,
  839 + children: handleTree(data, '${primaryColumn.javaField}', '${treeParentColumn.javaField}'),
  840 + } as ${simpleClassName}]
  841 +#end
313 getDetail() 842 getDetail()
314 }) 843 })
315 </script> 844 </script>
yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/form_sub_erp.vue.vm 0 → 100644
  1 +#set ($subTable = $subTables.get($subIndex))
  2 +#set ($subColumns = $subColumnsList.get($subIndex))
  3 +#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
  4 +#set ($subPrimaryColumn = $subPrimaryColumns.get($subIndex))
  5 +#set ($subJoinColumn = $subJoinColumns.get($subIndex))
  6 +#set ($subPrimaryJavaType = $subPrimaryColumn.javaType.toLowerCase())
  7 +#if(${subPrimaryJavaType} == "long" || ${subPrimaryJavaType} == "integer" || ${subPrimaryJavaType} == "short" || ${subPrimaryJavaType} == "float" || ${subPrimaryJavaType} == "double" || ${subPrimaryJavaType} == "bigdecimal" || ${subPrimaryJavaType} == "byte")
  8 +#set ($subPrimaryTsType = "number")
  9 +#else
  10 +#set ($subPrimaryTsType = "string")
  11 +#end
  12 +#set ($subJoinJavaType = $subJoinColumn.javaType.toLowerCase())
  13 +#if(${subJoinJavaType} == "long" || ${subJoinJavaType} == "integer" || ${subJoinJavaType} == "short" || ${subJoinJavaType} == "float" || ${subJoinJavaType} == "double" || ${subJoinJavaType} == "bigdecimal" || ${subJoinJavaType} == "byte")
  14 +#set ($subJoinTsType = "number")
  15 +#else
  16 +#set ($subJoinTsType = "string")
  17 +#end
  18 +#set ($hasDict = 0)
  19 +#set ($hasBoolDict = 0)
  20 +#set ($hasDate = 0)
  21 +#set ($hasDateTime = 0)
  22 +#foreach ($column in $subColumns)
  23 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
  24 +#if (($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType)
  25 +#set ($hasDict = 1)
  26 +#if ($column.javaType == "Boolean")
  27 +#set ($hasBoolDict = 1)
  28 +#end
  29 +#end
  30 +#if (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate")
  31 +#set ($hasDate = 1)
  32 +#end
  33 +#if (${column.javaType.toLowerCase()} == "localdatetime")
  34 +#set ($hasDateTime = 1)
  35 +#end
  36 +#end
  37 +#end
  38 +<template>
  39 + <view class="yd-page-container">
  40 + <!-- 顶部导航栏 -->
  41 + <wd-navbar :title="getTitle" left-arrow placeholder safe-area-inset-top fixed @click-left="handleBack" />
  42 +
  43 + <!-- 表单区域 -->
  44 + <wd-form ref="formRef" :model="formData" :schema="formSchema">
  45 + <wd-cell-group border>
  46 +#foreach ($column in $subColumns)
  47 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
  48 +#set ($javaType = $column.javaType.toLowerCase())
  49 +#if (($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType)
  50 + <yd-form-picker
  51 + v-model="formData.${column.javaField}"
  52 + label="${column.columnComment}"
  53 + label-width="180rpx"
  54 + prop="${column.javaField}"
  55 +#if ($column.javaType == "Boolean")
  56 + :columns="getBoolDictOptions(DICT_TYPE.${column.dictType.toUpperCase()})"
  57 +#else
  58 + :dict-type="DICT_TYPE.${column.dictType.toUpperCase()}"
  59 +#if ($column.javaType == "String")
  60 + dict-kind="str"
  61 +#end
  62 +#end
  63 + placeholder="请选择${column.columnComment}"
  64 + />
  65 +#elseif ($javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime")
  66 +#set ($pickerType = "date")
  67 +#if ($javaType == "localdatetime")
  68 +#set ($pickerType = "datetime")
  69 +#end
  70 + <wd-form-item
  71 + title="${column.columnComment}"
  72 + title-width="180rpx"
  73 + prop="${column.javaField}"
  74 + is-link
  75 +#if ($javaType == "localdatetime")
  76 + :value="formData.${column.javaField} ? formatDateTime(formData.${column.javaField}) : ''"
  77 +#else
  78 + :value="formData.${column.javaField} ? formatDate(formData.${column.javaField}) : ''"
  79 +#end
  80 + placeholder="请选择${column.columnComment}"
  81 + @click="${column.javaField}Visible = true"
  82 + />
  83 + <wd-datetime-picker
  84 + v-model:visible="${column.javaField}Visible"
  85 + :model-value="formData.${column.javaField} || Date.now()"
  86 + type="${pickerType}"
  87 + @update:model-value="value => formData.${column.javaField} = value"
  88 + />
  89 +#else
  90 + <wd-form-item title="${column.columnComment}" title-width="180rpx" prop="${column.javaField}">
  91 +#if ($javaType == "boolean")
  92 + <wd-switch v-model="formData.${column.javaField}" />
  93 +#elseif ($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte")
  94 + <wd-input-number
  95 + v-model="formData.${column.javaField}"
  96 +#if($column.nullable)
  97 + allow-null
  98 +#end
  99 +#if ($javaType == "float" || $javaType == "double" || $javaType == "bigdecimal")
  100 + :precision="2"
  101 + input-type="number"
  102 +#end
  103 + :min="0"
  104 + />
  105 +#elseif ($column.htmlType == "textarea" || $column.htmlType == "editor")
  106 + <wd-textarea
  107 + v-model="formData.${column.javaField}"
  108 + clearable
  109 + placeholder="请输入${column.columnComment}"
  110 +#if ($column.htmlType == "editor")
  111 + :maxlength="2000"
  112 +#else
  113 + :maxlength="200"
  114 +#end
  115 + />
  116 +#elseif ($column.htmlType == "imageUpload")
  117 + <yd-upload-img v-model="formData.${column.javaField}" directory="${table.moduleName}/${table.businessName}" />
  118 +#elseif ($column.htmlType == "fileUpload")
  119 + <yd-upload-file v-model="formData.${column.javaField}" :limit="1" directory="${table.moduleName}/${table.businessName}" />
  120 +#else
  121 + <wd-input v-model="formData.${column.javaField}" clearable placeholder="请输入${column.columnComment}" />
  122 +#end
  123 + </wd-form-item>
  124 +#end
  125 +#end
  126 +#end
  127 + </wd-cell-group>
  128 + </wd-form>
  129 +
  130 + <!-- 底部保存按钮 -->
  131 + <view class="yd-detail-footer">
  132 + <wd-button type="primary" block :loading="formLoading" @click="handleSubmit">
  133 + 保存
  134 + </wd-button>
  135 + </view>
  136 + </view>
  137 +</template>
  138 +
  139 +<script lang="ts" setup>
  140 +import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
  141 +import type { ${subSimpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
  142 +import { computed, onMounted, ref } from 'vue'
  143 +import { useToast } from '@wot-ui/ui/components/wd-toast'
  144 +import { create${subSimpleClassName}, get${subSimpleClassName}, update${subSimpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
  145 +import { delay, navigateBackPlus } from '@/utils'
  146 +#if ($hasBoolDict == 1)
  147 +import { getBoolDictOptions } from '@/hooks/useDict'
  148 +#end
  149 +#if ($hasDict == 1)
  150 +import { DICT_TYPE } from '@/utils/constants'
  151 +#end
  152 +#if ($hasDate == 1 || $hasDateTime == 1)
  153 +#set ($dateImportNames = "")
  154 +#if ($hasDate == 1)
  155 +#set ($dateImportNames = "${dateImportNames}formatDate, ")
  156 +#end
  157 +#if ($hasDateTime == 1)
  158 +#set ($dateImportNames = "${dateImportNames}formatDateTime, ")
  159 +#end
  160 +#set ($dateImportNames = $dateImportNames.trim())
  161 +#set ($dateImportNames = $dateImportNames.substring(0, $dateImportNames.length() - 1))
  162 +import { $dateImportNames } from '@/utils/date'
  163 +#end
  164 +import { createFormSchema } from '@/utils/wot'
  165 +
  166 +const props = defineProps<{
  167 + id?: ${subPrimaryTsType}
  168 + ${subJoinColumn.javaField}: ${subJoinTsType}
  169 +}>()
  170 +
  171 +definePage({
  172 + style: { navigationBarTitleText: '', navigationStyle: 'custom' },
  173 +})
  174 +
  175 +const toast = useToast()
  176 +const getTitle = computed(() => props.id ? '编辑${subTable.classComment}' : '新增${subTable.classComment}')
  177 +const formLoading = ref(false) // 表单提交状态
  178 +const formData = ref<Partial<${subSimpleClassName}>>({
  179 +#foreach ($column in $subColumns)
  180 +#if ($column.primaryKey)
  181 + ${column.javaField}: undefined,
  182 +#elseif ($column.id == $subJoinColumn.id)
  183 + ${column.javaField}: props.${column.javaField},
  184 +#elseif ($column.createOperation || $column.updateOperation)
  185 +#set ($javaType = $column.javaType.toLowerCase())
  186 +#if((($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType) || $javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime")
  187 + ${column.javaField}: undefined,
  188 +#else
  189 + ${column.javaField}:#if($column.nullable) undefined#elseif($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte") 0#elseif($javaType == "boolean") false#else ''#end,
  190 +#end
  191 +#end
  192 +#end
  193 +}) // 表单数据
  194 +#foreach ($column in $subColumns)
  195 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id
  196 + && (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime"))
  197 +const ${column.javaField}Visible = ref(false) // ${column.columnComment}选择器状态
  198 +#end
  199 +#end
  200 +const formSchema = createFormSchema({
  201 +#foreach ($column in $subColumns)
  202 +#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey)
  203 + ${column.javaField}: [{ required: true, message: '${column.columnComment}不能为空' }],
  204 +#end
  205 +#end
  206 +}) // 表单校验规则
  207 +const formRef = ref<FormInstance>() // 表单组件引用
  208 +
  209 +/** 返回上一页 */
  210 +function handleBack() {
  211 + navigateBackPlus()
  212 +}
  213 +
  214 +/** 加载${subTable.classComment}详情 */
  215 +async function getDetail() {
  216 + if (props.id) {
  217 + formData.value = await get${subSimpleClassName}(props.id)
  218 + }
  219 +}
  220 +
  221 +/** 提交表单 */
  222 +async function handleSubmit() {
  223 + const { valid } = await formRef.value.validate()
  224 + if (!valid) {
  225 + return
  226 + }
  227 + formLoading.value = true
  228 + try {
  229 + const data = {
  230 + ...formData.value,
  231 + ${subJoinColumn.javaField}: props.${subJoinColumn.javaField} ?? formData.value.${subJoinColumn.javaField},
  232 + } as ${subSimpleClassName}
  233 + if (props.id) {
  234 + await update${subSimpleClassName}(data)
  235 + toast.success('修改成功')
  236 + } else {
  237 + await create${subSimpleClassName}(data)
  238 + toast.success('新增成功')
  239 + }
  240 + delay(handleBack)
  241 + } finally {
  242 + formLoading.value = false
  243 + }
  244 +}
  245 +
  246 +/** 初始化 */
  247 +onMounted(() => {
  248 + getDetail()
  249 +})
  250 +</script>
yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/index_tree.vue.vm 0 → 100644
  1 +<template>
  2 + <view class="yd-page-container">
  3 + <!-- 顶部导航栏 -->
  4 + <wd-navbar
  5 + title="${table.classComment}管理"
  6 + left-arrow placeholder safe-area-inset-top fixed
  7 + @click-left="handleBack"
  8 + />
  9 +
  10 + <!-- 搜索组件 -->
  11 + <SearchForm @search="handleQuery" @reset="handleReset" />
  12 +
  13 + <!-- 面包屑导航 -->
  14 + <Breadcrumb ref="breadcrumbRef" v-model="currentParentId" />
  15 +
  16 + <!-- ${table.classComment}列表 -->
  17 + <view class="p-24rpx">
  18 + <view
  19 + v-for="item in currentList"
  20 + :key="item.${primaryColumn.javaField}"
  21 + class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
  22 + >
  23 + <view class="p-24rpx" @click="handleDetail(item)">
  24 + <view class="flex items-center justify-between">
  25 + <view class="flex min-w-0 items-center">
  26 + <view class="mr-16rpx h-48rpx w-48rpx shrink-0 flex items-center justify-center rounded-8rpx bg-[#1890ff]">
  27 + <wd-icon name="folder" size="20px" color="#fff" />
  28 + </view>
  29 + <view class="truncate text-32rpx text-[#333] font-semibold">
  30 + {{ item.${treeNameColumn.javaField} }}
  31 + </view>
  32 + </view>
  33 + <view
  34 + v-if="item.children?.length"
  35 + class="ml-16rpx shrink-0 flex items-center"
  36 + @click.stop="handleEnterChildren(item)"
  37 + >
  38 + <text class="text-24rpx text-[#1890ff]">子级 ({{ item.children.length }})</text>
  39 + <wd-icon name="arrow-right" size="12px" color="#1890ff" />
  40 + </view>
  41 + </view>
  42 + </view>
  43 + </view>
  44 +
  45 + <!-- 空状态 -->
  46 + <view v-if="!loading && currentList.length === 0" class="py-100rpx text-center">
  47 + <wd-empty icon="content" tip="暂无${table.classComment}数据" />
  48 + </view>
  49 + </view>
  50 +
  51 + <!-- 新增按钮 -->
  52 + <wd-fab
  53 + v-if="hasAccessByCodes(['${permissionPrefix}:create'])"
  54 + position="right-bottom"
  55 + type="primary"
  56 + :expandable="false"
  57 + @click="handleAdd"
  58 + />
  59 + </view>
  60 +</template>
  61 +
  62 +<script lang="ts" setup>
  63 +#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
  64 +#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "float" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
  65 +#set ($primaryTsType = "number")
  66 +#else
  67 +#set ($primaryTsType = "string")
  68 +#end
  69 +import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
  70 +import { onUnload } from '@dcloudio/uni-app'
  71 +import { computed, onMounted, ref } from 'vue'
  72 +import { get${simpleClassName}List } from '@/api/${table.moduleName}/${table.businessName}'
  73 +import { useAccess } from '@/hooks/useAccess'
  74 +import { navigateBackPlus } from '@/utils'
  75 +import { findChildren, handleTree } from '@/utils/tree'
  76 +import Breadcrumb from './components/breadcrumb.vue'
  77 +import SearchForm from './components/search-form.vue'
  78 +
  79 +definePage({
  80 + style: {
  81 + navigationBarTitleText: '',
  82 + navigationStyle: 'custom',
  83 + },
  84 +})
  85 +
  86 +const { hasAccessByCodes } = useAccess()
  87 +const loading = ref(false) // 列表加载状态
  88 +const list = ref<${simpleClassName}[]>([]) // 完整树形列表
  89 +const currentParentId = ref<${primaryTsType}>(0) // 当前层级的父节点编号
  90 +const currentList = computed(() => currentParentId.value === 0
  91 + ? list.value
  92 + : findChildren(list.value, currentParentId.value, '${primaryColumn.javaField}')) // 当前层级列表
  93 +const breadcrumbRef = ref<InstanceType<typeof Breadcrumb>>() // 面包屑组件引用
  94 +const queryParams = ref<Record<string, any>>({}) // 查询参数
  95 +
  96 +/** 返回上一页或上一层级 */
  97 +function handleBack() {
  98 + if (!breadcrumbRef.value?.back()) {
  99 + navigateBackPlus()
  100 + }
  101 +}
  102 +
  103 +/** 进入子层级 */
  104 +function handleEnterChildren(item: ${simpleClassName}) {
  105 + breadcrumbRef.value?.enter({ id: item.${primaryColumn.javaField}!, name: item.${treeNameColumn.javaField} })
  106 +}
  107 +
  108 +/** 查询${table.classComment}列表 */
  109 +async function getList() {
  110 + loading.value = true
  111 + try {
  112 + list.value = handleTree(await get${simpleClassName}List(queryParams.value), '${primaryColumn.javaField}', '${treeParentColumn.javaField}')
  113 + } finally {
  114 + loading.value = false
  115 + }
  116 +}
  117 +
  118 +/** 搜索按钮操作 */
  119 +function handleQuery(data?: Record<string, any>) {
  120 + queryParams.value = { ...data }
  121 + currentParentId.value = 0
  122 + breadcrumbRef.value?.reset()
  123 + getList()
  124 +}
  125 +
  126 +/** 重置按钮操作 */
  127 +function handleReset() {
  128 + handleQuery()
  129 +}
  130 +
  131 +/** 新增${table.classComment} */
  132 +function handleAdd() {
  133 + uni.navigateTo({ url: `/pages-${table.moduleName}/${table.businessName}/form/index?${treeParentColumn.javaField}=#[[${]]#currentParentId.value#[[}]]#` })
  134 +}
  135 +
  136 +/** 查看详情 */
  137 +function handleDetail(item: ${simpleClassName}) {
  138 + uni.navigateTo({ url: `/pages-${table.moduleName}/${table.businessName}/detail/index?id=#[[${]]#item.${primaryColumn.javaField}#[[}]]#` })
  139 +}
  140 +
  141 +/** 初始化 */
  142 +onMounted(() => {
  143 + getList()
  144 + uni.$on('${table.moduleName}:${table.businessName}:reload', getList)
  145 +})
  146 +
  147 +/** 卸载 */
  148 +onUnload(() => {
  149 + uni.$off('${table.moduleName}:${table.businessName}:reload', getList)
  150 +})
  151 +</script>
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antdv_next/general/views/form.vue.vm
  1 +#set ($selectOptionEnable = false)
  2 +#foreach ($column in $columns)
  3 + #if (($column.createOperation || $column.updateOperation) && $column.htmlType == "select")
  4 + #set ($selectOptionEnable = true)
  5 + #end
  6 +#end
1 <script lang="ts" setup> 7 <script lang="ts" setup>
2 -import type { Rule } from 'antdv-next/es/form'; 8 +import type { Rule } from 'antdv-next';
3 import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}'; 9 import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
4 10
5 import { computed, ref } from 'vue'; 11 import { computed, ref } from 'vue';
@@ -9,7 +15,7 @@ import { DICT_TYPE } from &#39;@vben/constants&#39;; @@ -9,7 +15,7 @@ import { DICT_TYPE } from &#39;@vben/constants&#39;;
9 import { getDictOptions } from '@vben/hooks'; 15 import { getDictOptions } from '@vben/hooks';
10 import { Tinymce as RichTextarea } from '#/components/tinymce'; 16 import { Tinymce as RichTextarea } from '#/components/tinymce';
11 import { ImageUpload, FileUpload } from "#/components/upload"; 17 import { ImageUpload, FileUpload } from "#/components/upload";
12 -import { message, Tabs, Form, Input, TextArea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'antdv-next'; 18 +import { Checkbox, CheckboxGroup, DatePicker, Form, FormItem, Input, message, Radio, RadioGroup, Select#if ($selectOptionEnable), SelectOption#end#if ( $table.templateType == 10 || $table.templateType == 12 ), TabPane, Tabs#end, TextArea, TreeSelect } from 'antdv-next';
13 #if($table.templateType == 2)## 树表需要导入这些 19 #if($table.templateType == 2)## 树表需要导入这些
14 import { get${simpleClassName}List } from '#/api/${table.moduleName}/${table.businessName}'; 20 import { get${simpleClassName}List } from '#/api/${table.moduleName}/${table.businessName}';
15 import { handleTree } from '@vben/utils' 21 import { handleTree } from '@vben/utils'
@@ -204,7 +210,7 @@ const [Modal, modalApi] = useVbenModal({ @@ -204,7 +210,7 @@ const [Modal, modalApi] = useVbenModal({
204 #set ($dictMethod = "boolean") 210 #set ($dictMethod = "boolean")
205 #end 211 #end
206 #if ( $table.templateType == 2 && $column.id == $treeParentColumn.id ) 212 #if ( $table.templateType == 2 && $column.id == $treeParentColumn.id )
207 - <Form.Item label="${comment}" name="${javaField}"> 213 + <FormItem label="${comment}" name="${javaField}">
208 <TreeSelect 214 <TreeSelect
209 v-model:value="formData.${javaField}" 215 v-model:value="formData.${javaField}"
210 :treeData="${classNameVar}Tree" 216 :treeData="${classNameVar}Tree"
@@ -225,25 +231,25 @@ const [Modal, modalApi] = useVbenModal({ @@ -225,25 +231,25 @@ const [Modal, modalApi] = useVbenModal({
225 treeDefaultExpandAll 231 treeDefaultExpandAll
226 placeholder="请选择${comment}" 232 placeholder="请选择${comment}"
227 /> 233 />
228 - </Form.Item> 234 + </FormItem>
229 #elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里 235 #elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
230 - <Form.Item label="${comment}" name="${javaField}"> 236 + <FormItem label="${comment}" name="${javaField}">
231 <Input v-model:value="formData.${javaField}" placeholder="请输入${comment}" /> 237 <Input v-model:value="formData.${javaField}" placeholder="请输入${comment}" />
232 - </Form.Item> 238 + </FormItem>
233 #elseif($column.htmlType == "imageUpload")## 图片上传 239 #elseif($column.htmlType == "imageUpload")## 图片上传
234 - <Form.Item label="${comment}" name="${javaField}"> 240 + <FormItem label="${comment}" name="${javaField}">
235 <ImageUpload v-model:value="formData.${javaField}" /> 241 <ImageUpload v-model:value="formData.${javaField}" />
236 - </Form.Item> 242 + </FormItem>
237 #elseif($column.htmlType == "fileUpload")## 文件上传 243 #elseif($column.htmlType == "fileUpload")## 文件上传
238 - <Form.Item label="${comment}" name="${javaField}"> 244 + <FormItem label="${comment}" name="${javaField}">
239 <FileUpload v-model:value="formData.${javaField}" /> 245 <FileUpload v-model:value="formData.${javaField}" />
240 - </Form.Item> 246 + </FormItem>
241 #elseif($column.htmlType == "editor")## 文本编辑器 247 #elseif($column.htmlType == "editor")## 文本编辑器
242 - <Form.Item label="${comment}" name="${javaField}"> 248 + <FormItem label="${comment}" name="${javaField}">
243 <RichTextarea v-model="formData.${javaField}" height="500px" /> 249 <RichTextarea v-model="formData.${javaField}" height="500px" />
244 - </Form.Item> 250 + </FormItem>
245 #elseif($column.htmlType == "select")## 下拉框 251 #elseif($column.htmlType == "select")## 下拉框
246 - <Form.Item label="${comment}" name="${javaField}"> 252 + <FormItem label="${comment}" name="${javaField}">
247 <Select v-model:value="formData.${javaField}" placeholder="请选择${comment}"> 253 <Select v-model:value="formData.${javaField}" placeholder="请选择${comment}">
248 #if ("" != $dictType)## 有数据字典 254 #if ("" != $dictType)## 有数据字典
249 <SelectOption 255 <SelectOption
@@ -257,9 +263,9 @@ const [Modal, modalApi] = useVbenModal({ @@ -257,9 +263,9 @@ const [Modal, modalApi] = useVbenModal({
257 <SelectOption label="请选择字典生成" value="" /> 263 <SelectOption label="请选择字典生成" value="" />
258 #end 264 #end
259 </Select> 265 </Select>
260 - </Form.Item> 266 + </FormItem>
261 #elseif($column.htmlType == "checkbox")## 多选框 267 #elseif($column.htmlType == "checkbox")## 多选框
262 - <Form.Item label="${comment}" name="${javaField}"> 268 + <FormItem label="${comment}" name="${javaField}">
263 <CheckboxGroup v-model:value="formData.${javaField}"> 269 <CheckboxGroup v-model:value="formData.${javaField}">
264 #if ("" != $dictType)## 有数据字典 270 #if ("" != $dictType)## 有数据字典
265 <Checkbox 271 <Checkbox
@@ -273,9 +279,9 @@ const [Modal, modalApi] = useVbenModal({ @@ -273,9 +279,9 @@ const [Modal, modalApi] = useVbenModal({
273 <Checkbox label="请选择字典生成" /> 279 <Checkbox label="请选择字典生成" />
274 #end 280 #end
275 </CheckboxGroup> 281 </CheckboxGroup>
276 - </Form.Item> 282 + </FormItem>
277 #elseif($column.htmlType == "radio")## 单选框 283 #elseif($column.htmlType == "radio")## 单选框
278 - <Form.Item label="${comment}" name="${javaField}"> 284 + <FormItem label="${comment}" name="${javaField}">
279 <RadioGroup v-model:value="formData.${javaField}"> 285 <RadioGroup v-model:value="formData.${javaField}">
280 #if ("" != $dictType)## 有数据字典 286 #if ("" != $dictType)## 有数据字典
281 <Radio 287 <Radio
@@ -289,19 +295,19 @@ const [Modal, modalApi] = useVbenModal({ @@ -289,19 +295,19 @@ const [Modal, modalApi] = useVbenModal({
289 <Radio value="1">请选择字典生成</Radio> 295 <Radio value="1">请选择字典生成</Radio>
290 #end 296 #end
291 </RadioGroup> 297 </RadioGroup>
292 - </Form.Item> 298 + </FormItem>
293 #elseif($column.htmlType == "datetime")## 时间框 299 #elseif($column.htmlType == "datetime")## 时间框
294 - <Form.Item label="${comment}" name="${javaField}"> 300 + <FormItem label="${comment}" name="${javaField}">
295 <DatePicker 301 <DatePicker
296 v-model:value="formData.${javaField}" 302 v-model:value="formData.${javaField}"
297 valueFormat="x" 303 valueFormat="x"
298 placeholder="选择${comment}" 304 placeholder="选择${comment}"
299 /> 305 />
300 - </Form.Item> 306 + </FormItem>
301 #elseif($column.htmlType == "textarea")## 文本框 307 #elseif($column.htmlType == "textarea")## 文本框
302 - <Form.Item label="${comment}" name="${javaField}"> 308 + <FormItem label="${comment}" name="${javaField}">
303 <TextArea v-model:value="formData.${javaField}" placeholder="请输入${comment}" /> 309 <TextArea v-model:value="formData.${javaField}" placeholder="请输入${comment}" />
304 - </Form.Item> 310 + </FormItem>
305 #end 311 #end
306 #end 312 #end
307 #end 313 #end
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antdv_next/general/views/import.vue.vm
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 -import type { FileType } from 'antdv-next/es/upload/interface';  
3 -  
4 import { ref } from 'vue'; 2 import { ref } from 'vue';
5 3
6 import { useVbenModal } from '@vben/common-ui'; 4 import { useVbenModal } from '@vben/common-ui';
@@ -43,8 +41,8 @@ const [Modal, modalApi] = useVbenModal({ @@ -43,8 +41,8 @@ const [Modal, modalApi] = useVbenModal({
43 }); 41 });
44 42
45 /** 上传前:拦截 antd Upload 的自动上传,文件存到 ref */ 43 /** 上传前:拦截 antd Upload 的自动上传,文件存到 ref */
46 -function beforeUpload(file: FileType) {  
47 - fileRef.value = file as unknown as File; 44 +function beforeUpload(file: File) {
  45 + fileRef.value = file;
48 return false; 46 return false;
49 } 47 }
50 48
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antdv_next/general/views/index.vue.vm
  1 +#set ($rangePickerEnable = false)
  2 +#set ($selectOptionEnable = false)
  3 +#foreach ($column in $columns)
  4 + #if ($column.listOperation && $column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
  5 + #set ($rangePickerEnable = true)
  6 + #end
  7 + #if ($column.listOperation && ($column.htmlType == "select" || $column.htmlType == "radio" || $column.htmlType == "checkbox"))
  8 + #set ($selectOptionEnable = true)
  9 + #end
  10 +#end
1 <script lang="ts" setup> 11 <script lang="ts" setup>
2 import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}'; 12 import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
3 13
@@ -8,12 +18,14 @@ import { DICT_TYPE } from &#39;@vben/constants&#39;; @@ -8,12 +18,14 @@ import { DICT_TYPE } from &#39;@vben/constants&#39;;
8 import { getDictOptions } from '@vben/hooks'; 18 import { getDictOptions } from '@vben/hooks';
9 import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table'; 19 import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
10 import { cloneDeep, downloadFileFromBlobPart, formatDateTime, isEmpty } from '@vben/utils'; 20 import { cloneDeep, downloadFileFromBlobPart, formatDateTime, isEmpty } from '@vben/utils';
11 -import { Button, Card, message, Tabs, Pagination, Form, RangePicker, DatePicker, Select, Input } from 'antdv-next'; 21 +import { Button, Card, DatePicker, Form, FormItem, Input, message, Pagination#if ($rangePickerEnable), DateRangePicker as RangePicker#end, Select#if ($selectOptionEnable), SelectOption#end#if ( $table.templateType == 11 || $table.templateType == 12 ), TabPane, Tabs#end } from 'antdv-next';
12 import ${simpleClassName}Form from './modules/form.vue'; 22 import ${simpleClassName}Form from './modules/form.vue';
13 import { Download, Plus, RefreshCw, Search, Trash2 } from '@vben/icons'; 23 import { Download, Plus, RefreshCw, Search, Trash2 } from '@vben/icons';
14 import { DictTag } from '#/components/dict-tag'; 24 import { DictTag } from '#/components/dict-tag';
15 import { VxeColumn, VxeTable } from '#/adapter/vxe-table'; 25 import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
  26 +#if ($rangePickerEnable)
16 import { getRangePickerDefaultProps } from '#/utils/rangePickerProps'; 27 import { getRangePickerDefaultProps } from '#/utils/rangePickerProps';
  28 +#end
17 29
18 ## 特殊:主子表专属逻辑 30 ## 特殊:主子表专属逻辑
19 #if ( $table.templateType == 11 || $table.templateType == 12 ) 31 #if ( $table.templateType == 11 || $table.templateType == 12 )
@@ -249,7 +261,7 @@ onMounted(() =&gt; { @@ -249,7 +261,7 @@ onMounted(() =&gt; {
249 #set ($dictMethod = "boolean") 261 #set ($dictMethod = "boolean")
250 #end 262 #end
251 #if ($column.htmlType == "input") 263 #if ($column.htmlType == "input")
252 - <Form.Item label="${comment}" name="${javaField}"> 264 + <FormItem label="${comment}" name="${javaField}">
253 <Input 265 <Input
254 v-model:value="queryParams.${javaField}" 266 v-model:value="queryParams.${javaField}"
255 placeholder="请输入${comment}" 267 placeholder="请输入${comment}"
@@ -257,9 +269,9 @@ onMounted(() =&gt; { @@ -257,9 +269,9 @@ onMounted(() =&gt; {
257 @pressEnter="handleQuery" 269 @pressEnter="handleQuery"
258 class="w-full" 270 class="w-full"
259 /> 271 />
260 - </Form.Item> 272 + </FormItem>
261 #elseif ($column.htmlType == "select" || $column.htmlType == "radio" || $column.htmlType == "checkbox") 273 #elseif ($column.htmlType == "select" || $column.htmlType == "radio" || $column.htmlType == "checkbox")
262 - <Form.Item label="${comment}" name="${javaField}"> 274 + <FormItem label="${comment}" name="${javaField}">
263 <Select 275 <Select
264 v-model:value="queryParams.${javaField}" 276 v-model:value="queryParams.${javaField}"
265 placeholder="请选择${comment}" 277 placeholder="请选择${comment}"
@@ -278,10 +290,10 @@ onMounted(() =&gt; { @@ -278,10 +290,10 @@ onMounted(() =&gt; {
278 <SelectOption label="请选择字典生成" value="" /> 290 <SelectOption label="请选择字典生成" value="" />
279 #end 291 #end
280 </Select> 292 </Select>
281 - </Form.Item> 293 + </FormItem>
282 #elseif($column.htmlType == "datetime") 294 #elseif($column.htmlType == "datetime")
283 #if ($column.listOperationCondition != "BETWEEN")## 非范围 295 #if ($column.listOperationCondition != "BETWEEN")## 非范围
284 - <Form.Item label="${comment}" name="${javaField}"> 296 + <FormItem label="${comment}" name="${javaField}">
285 <DatePicker 297 <DatePicker
286 v-model:value="queryParams.${javaField}" 298 v-model:value="queryParams.${javaField}"
287 valueFormat="YYYY-MM-DD" 299 valueFormat="YYYY-MM-DD"
@@ -289,25 +301,25 @@ onMounted(() =&gt; { @@ -289,25 +301,25 @@ onMounted(() =&gt; {
289 allowClear 301 allowClear
290 class="w-full" 302 class="w-full"
291 /> 303 />
292 - </Form.Item> 304 + </FormItem>
293 #else## 范围 305 #else## 范围
294 - <Form.Item label="${comment}" name="${javaField}"> 306 + <FormItem label="${comment}" name="${javaField}">
295 <RangePicker 307 <RangePicker
296 v-model:value="queryParams.${javaField}" 308 v-model:value="queryParams.${javaField}"
297 v-bind="getRangePickerDefaultProps()" 309 v-bind="getRangePickerDefaultProps()"
298 class="w-full" 310 class="w-full"
299 /> 311 />
300 - </Form.Item> 312 + </FormItem>
301 #end 313 #end
302 #end 314 #end
303 #end 315 #end
304 #end 316 #end
305 - <Form.Item> 317 + <FormItem>
306 <Button class="ml-2" @click="resetQuery"> 重置 </Button> 318 <Button class="ml-2" @click="resetQuery"> 重置 </Button>
307 <Button class="ml-2" @click="handleQuery" type="primary"> 319 <Button class="ml-2" @click="handleQuery" type="primary">
308 搜索 320 搜索
309 </Button> 321 </Button>
310 - </Form.Item> 322 + </FormItem>
311 </Form> 323 </Form>
312 </Card> 324 </Card>
313 325
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antdv_next/general/views/modules/form_sub_erp.vue.vm
@@ -2,8 +2,14 @@ @@ -2,8 +2,14 @@
2 #set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 2 #set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组
3 #set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 3 #set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
4 #set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) 4 #set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
  5 +#set ($selectOptionEnable = false)
  6 +#foreach ($column in $subColumns)
  7 + #if (($column.createOperation || $column.updateOperation) && $column.htmlType == "select")
  8 + #set ($selectOptionEnable = true)
  9 + #end
  10 +#end
5 <script lang="ts" setup> 11 <script lang="ts" setup>
6 -import type { Rule } from 'antdv-next/es/form'; 12 +import type { Rule } from 'antdv-next';
7 import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}'; 13 import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
8 14
9 import { computed, ref } from 'vue'; 15 import { computed, ref } from 'vue';
@@ -13,7 +19,7 @@ import { DICT_TYPE } from &#39;@vben/constants&#39;; @@ -13,7 +19,7 @@ import { DICT_TYPE } from &#39;@vben/constants&#39;;
13 import { getDictOptions } from '@vben/hooks'; 19 import { getDictOptions } from '@vben/hooks';
14 import { Tinymce as RichTextarea } from '#/components/tinymce'; 20 import { Tinymce as RichTextarea } from '#/components/tinymce';
15 import { ImageUpload, FileUpload } from "#/components/upload"; 21 import { ImageUpload, FileUpload } from "#/components/upload";
16 -import { message, Tabs, Form, Input, TextArea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'antdv-next'; 22 +import { Checkbox, CheckboxGroup, DatePicker, Form, FormItem, Input, message, Radio, RadioGroup, Select#if ($selectOptionEnable), SelectOption#end, TextArea, TreeSelect } from 'antdv-next';
17 23
18 import { $t } from '#/locales'; 24 import { $t } from '#/locales';
19 25
@@ -130,23 +136,23 @@ function resetForm(){ @@ -130,23 +136,23 @@ function resetForm(){
130 #set ($dictMethod = "boolean") 136 #set ($dictMethod = "boolean")
131 #end 137 #end
132 #if ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里 138 #if ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
133 - <Form.Item label="${comment}" name="${javaField}"> 139 + <FormItem label="${comment}" name="${javaField}">
134 <Input v-model:value="formData.${javaField}" placeholder="请输入${comment}" /> 140 <Input v-model:value="formData.${javaField}" placeholder="请输入${comment}" />
135 - </Form.Item> 141 + </FormItem>
136 #elseif($column.htmlType == "imageUpload")## 图片上传 142 #elseif($column.htmlType == "imageUpload")## 图片上传
137 - <Form.Item label="${comment}" name="${javaField}"> 143 + <FormItem label="${comment}" name="${javaField}">
138 <ImageUpload v-model:value="formData.${javaField}" /> 144 <ImageUpload v-model:value="formData.${javaField}" />
139 - </Form.Item> 145 + </FormItem>
140 #elseif($column.htmlType == "fileUpload")## 文件上传 146 #elseif($column.htmlType == "fileUpload")## 文件上传
141 - <Form.Item label="${comment}" name="${javaField}"> 147 + <FormItem label="${comment}" name="${javaField}">
142 <FileUpload v-model:value="formData.${javaField}" /> 148 <FileUpload v-model:value="formData.${javaField}" />
143 - </Form.Item> 149 + </FormItem>
144 #elseif($column.htmlType == "editor")## 文本编辑器 150 #elseif($column.htmlType == "editor")## 文本编辑器
145 - <Form.Item label="${comment}" name="${javaField}"> 151 + <FormItem label="${comment}" name="${javaField}">
146 <RichTextarea v-model="formData.${javaField}" height="500px" /> 152 <RichTextarea v-model="formData.${javaField}" height="500px" />
147 - </Form.Item> 153 + </FormItem>
148 #elseif($column.htmlType == "select")## 下拉框 154 #elseif($column.htmlType == "select")## 下拉框
149 - <Form.Item label="${comment}" name="${javaField}"> 155 + <FormItem label="${comment}" name="${javaField}">
150 <Select v-model:value="formData.${javaField}" placeholder="请选择${comment}"> 156 <Select v-model:value="formData.${javaField}" placeholder="请选择${comment}">
151 #if ("" != $dictType)## 有数据字典 157 #if ("" != $dictType)## 有数据字典
152 <SelectOption 158 <SelectOption
@@ -160,9 +166,9 @@ function resetForm(){ @@ -160,9 +166,9 @@ function resetForm(){
160 <SelectOption label="请选择字典生成" value="" /> 166 <SelectOption label="请选择字典生成" value="" />
161 #end 167 #end
162 </Select> 168 </Select>
163 - </Form.Item> 169 + </FormItem>
164 #elseif($column.htmlType == "checkbox")## 多选框 170 #elseif($column.htmlType == "checkbox")## 多选框
165 - <Form.Item label="${comment}" name="${javaField}"> 171 + <FormItem label="${comment}" name="${javaField}">
166 <CheckboxGroup v-model:value="formData.${javaField}"> 172 <CheckboxGroup v-model:value="formData.${javaField}">
167 #if ("" != $dictType)## 有数据字典 173 #if ("" != $dictType)## 有数据字典
168 <Checkbox 174 <Checkbox
@@ -176,9 +182,9 @@ function resetForm(){ @@ -176,9 +182,9 @@ function resetForm(){
176 <Checkbox label="请选择字典生成" /> 182 <Checkbox label="请选择字典生成" />
177 #end 183 #end
178 </CheckboxGroup> 184 </CheckboxGroup>
179 - </Form.Item> 185 + </FormItem>
180 #elseif($column.htmlType == "radio")## 单选框 186 #elseif($column.htmlType == "radio")## 单选框
181 - <Form.Item label="${comment}" name="${javaField}"> 187 + <FormItem label="${comment}" name="${javaField}">
182 <RadioGroup v-model:value="formData.${javaField}"> 188 <RadioGroup v-model:value="formData.${javaField}">
183 #if ("" != $dictType)## 有数据字典 189 #if ("" != $dictType)## 有数据字典
184 <Radio 190 <Radio
@@ -192,19 +198,19 @@ function resetForm(){ @@ -192,19 +198,19 @@ function resetForm(){
192 <Radio value="1">请选择字典生成</Radio> 198 <Radio value="1">请选择字典生成</Radio>
193 #end 199 #end
194 </RadioGroup> 200 </RadioGroup>
195 - </Form.Item> 201 + </FormItem>
196 #elseif($column.htmlType == "datetime")## 时间框 202 #elseif($column.htmlType == "datetime")## 时间框
197 - <Form.Item label="${comment}" name="${javaField}"> 203 + <FormItem label="${comment}" name="${javaField}">
198 <DatePicker 204 <DatePicker
199 v-model:value="formData.${javaField}" 205 v-model:value="formData.${javaField}"
200 valueFormat="x" 206 valueFormat="x"
201 placeholder="选择${comment}" 207 placeholder="选择${comment}"
202 /> 208 />
203 - </Form.Item> 209 + </FormItem>
204 #elseif($column.htmlType == "textarea")## 文本框 210 #elseif($column.htmlType == "textarea")## 文本框
205 - <Form.Item label="${comment}" name="${javaField}"> 211 + <FormItem label="${comment}" name="${javaField}">
206 <TextArea v-model:value="formData.${javaField}" placeholder="请输入${comment}" /> 212 <TextArea v-model:value="formData.${javaField}" placeholder="请输入${comment}" />
207 - </Form.Item> 213 + </FormItem>
208 #end 214 #end
209 #end 215 #end
210 #end 216 #end
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antdv_next/general/views/modules/form_sub_inner.vue.vm
1 ## 主表的 normal 和 inner 使用相同的 form 表单 1 ## 主表的 normal 和 inner 使用相同的 form 表单
2 -#parse("codegen/vue3_vben5_antd/general/views/modules/form_sub_normal.vue.vm")  
3 \ No newline at end of file 2 \ No newline at end of file
  3 +#parse("codegen/vue3_vben5_antdv_next/general/views/modules/form_sub_normal.vue.vm")
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antdv_next/general/views/modules/form_sub_normal.vue.vm
@@ -4,6 +4,12 @@ @@ -4,6 +4,12 @@
4 #set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) 4 #set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
5 #set ($subClassNameVar = $subClassNameVars.get($subIndex)) 5 #set ($subClassNameVar = $subClassNameVars.get($subIndex))
6 #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 6 #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
  7 +#set ($selectOptionEnable = false)
  8 +#foreach ($column in $subColumns)
  9 + #if (($column.createOperation || $column.updateOperation) && (!$subTable.subJoinMany || $column.id != $subJoinColumn.id) && $column.htmlType == "select")
  10 + #set ($selectOptionEnable = true)
  11 + #end
  12 +#end
7 <script lang="ts" setup> 13 <script lang="ts" setup>
8 import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}'; 14 import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
9 15
@@ -12,7 +18,7 @@ import { computed, ref, h, onMounted, watch, nextTick } from &#39;vue&#39;; @@ -12,7 +18,7 @@ import { computed, ref, h, onMounted, watch, nextTick } from &#39;vue&#39;;
12 import { DICT_TYPE } from '@vben/constants'; 18 import { DICT_TYPE } from '@vben/constants';
13 import { getDictOptions } from '@vben/hooks'; 19 import { getDictOptions } from '@vben/hooks';
14 20
15 -import { message, Tabs, Form, Input, TextArea, Button, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker } from 'antdv-next'; 21 +import { Button, Checkbox, CheckboxGroup, DatePicker#if (!$subTable.subJoinMany), Form, FormItem#end, Input, Radio, RadioGroup, Select#if ($selectOptionEnable), SelectOption#end, TextArea } from 'antdv-next';
16 import { $t } from '#/locales'; 22 import { $t } from '#/locales';
17 23
18 #if ($subTable.subJoinMany) ## 一对多 24 #if ($subTable.subJoinMany) ## 一对多
@@ -22,7 +28,7 @@ import { VxeColumn, VxeTable } from &#39;#/adapter/vxe-table&#39;; @@ -22,7 +28,7 @@ import { VxeColumn, VxeTable } from &#39;#/adapter/vxe-table&#39;;
22 import { ImageUpload, FileUpload } from "#/components/upload"; 28 import { ImageUpload, FileUpload } from "#/components/upload";
23 import { get${subSimpleClassName}ListBy${SubJoinColumnName} } from '#/api/${table.moduleName}/${table.businessName}'; 29 import { get${subSimpleClassName}ListBy${SubJoinColumnName} } from '#/api/${table.moduleName}/${table.businessName}';
24 #else 30 #else
25 -import type { Rule } from 'antdv-next/es/form'; 31 +import type { Rule } from 'antdv-next';
26 import { Tinymce as RichTextarea } from '#/components/tinymce'; 32 import { Tinymce as RichTextarea } from '#/components/tinymce';
27 import { ImageUpload, FileUpload } from "#/components/upload"; 33 import { ImageUpload, FileUpload } from "#/components/upload";
28 import { get${subSimpleClassName}By${SubJoinColumnName} } from '#/api/${table.moduleName}/${table.businessName}'; 34 import { get${subSimpleClassName}By${SubJoinColumnName} } from '#/api/${table.moduleName}/${table.businessName}';
@@ -264,23 +270,23 @@ watch( @@ -264,23 +270,23 @@ watch(
264 #set ($dictMethod = "boolean") 270 #set ($dictMethod = "boolean")
265 #end 271 #end
266 #if ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里 272 #if ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
267 - <Form.Item label="${comment}" name="${javaField}"> 273 + <FormItem label="${comment}" name="${javaField}">
268 <Input v-model:value="formData.${javaField}" placeholder="请输入${comment}" /> 274 <Input v-model:value="formData.${javaField}" placeholder="请输入${comment}" />
269 - </Form.Item> 275 + </FormItem>
270 #elseif($column.htmlType == "imageUpload")## 图片上传 276 #elseif($column.htmlType == "imageUpload")## 图片上传
271 - <Form.Item label="${comment}" name="${javaField}"> 277 + <FormItem label="${comment}" name="${javaField}">
272 <ImageUpload v-model:value="formData.${javaField}" /> 278 <ImageUpload v-model:value="formData.${javaField}" />
273 - </Form.Item> 279 + </FormItem>
274 #elseif($column.htmlType == "fileUpload")## 文件上传 280 #elseif($column.htmlType == "fileUpload")## 文件上传
275 - <Form.Item label="${comment}" name="${javaField}"> 281 + <FormItem label="${comment}" name="${javaField}">
276 <FileUpload v-model:value="formData.${javaField}" /> 282 <FileUpload v-model:value="formData.${javaField}" />
277 - </Form.Item> 283 + </FormItem>
278 #elseif($column.htmlType == "editor")## 文本编辑器 284 #elseif($column.htmlType == "editor")## 文本编辑器
279 - <Form.Item label="${comment}" name="${javaField}"> 285 + <FormItem label="${comment}" name="${javaField}">
280 <RichTextarea v-model="formData.${javaField}" height="500px" /> 286 <RichTextarea v-model="formData.${javaField}" height="500px" />
281 - </Form.Item> 287 + </FormItem>
282 #elseif($column.htmlType == "select")## 下拉框 288 #elseif($column.htmlType == "select")## 下拉框
283 - <Form.Item label="${comment}" name="${javaField}"> 289 + <FormItem label="${comment}" name="${javaField}">
284 <Select v-model:value="formData.${javaField}" placeholder="请选择${comment}"> 290 <Select v-model:value="formData.${javaField}" placeholder="请选择${comment}">
285 #if ("" != $dictType)## 有数据字典 291 #if ("" != $dictType)## 有数据字典
286 <SelectOption 292 <SelectOption
@@ -294,9 +300,9 @@ watch( @@ -294,9 +300,9 @@ watch(
294 <SelectOption label="请选择字典生成" value="" /> 300 <SelectOption label="请选择字典生成" value="" />
295 #end 301 #end
296 </Select> 302 </Select>
297 - </Form.Item> 303 + </FormItem>
298 #elseif($column.htmlType == "checkbox")## 多选框 304 #elseif($column.htmlType == "checkbox")## 多选框
299 - <Form.Item label="${comment}" name="${javaField}"> 305 + <FormItem label="${comment}" name="${javaField}">
300 <CheckboxGroup v-model:value="formData.${javaField}"> 306 <CheckboxGroup v-model:value="formData.${javaField}">
301 #if ("" != $dictType)## 有数据字典 307 #if ("" != $dictType)## 有数据字典
302 <Checkbox 308 <Checkbox
@@ -310,9 +316,9 @@ watch( @@ -310,9 +316,9 @@ watch(
310 <Checkbox label="请选择字典生成" /> 316 <Checkbox label="请选择字典生成" />
311 #end 317 #end
312 </CheckboxGroup> 318 </CheckboxGroup>
313 - </Form.Item> 319 + </FormItem>
314 #elseif($column.htmlType == "radio")## 单选框 320 #elseif($column.htmlType == "radio")## 单选框
315 - <Form.Item label="${comment}" name="${javaField}"> 321 + <FormItem label="${comment}" name="${javaField}">
316 <RadioGroup v-model:value="formData.${javaField}"> 322 <RadioGroup v-model:value="formData.${javaField}">
317 #if ("" != $dictType)## 有数据字典 323 #if ("" != $dictType)## 有数据字典
318 <Radio 324 <Radio
@@ -326,19 +332,19 @@ watch( @@ -326,19 +332,19 @@ watch(
326 <Radio value="1">请选择字典生成</Radio> 332 <Radio value="1">请选择字典生成</Radio>
327 #end 333 #end
328 </RadioGroup> 334 </RadioGroup>
329 - </Form.Item> 335 + </FormItem>
330 #elseif($column.htmlType == "datetime")## 时间框 336 #elseif($column.htmlType == "datetime")## 时间框
331 - <Form.Item label="${comment}" name="${javaField}"> 337 + <FormItem label="${comment}" name="${javaField}">
332 <DatePicker 338 <DatePicker
333 v-model:value="formData.${javaField}" 339 v-model:value="formData.${javaField}"
334 valueFormat="x" 340 valueFormat="x"
335 placeholder="选择${comment}" 341 placeholder="选择${comment}"
336 /> 342 />
337 - </Form.Item> 343 + </FormItem>
338 #elseif($column.htmlType == "textarea")## 文本框 344 #elseif($column.htmlType == "textarea")## 文本框
339 - <Form.Item label="${comment}" name="${javaField}"> 345 + <FormItem label="${comment}" name="${javaField}">
340 <TextArea v-model:value="formData.${javaField}" placeholder="请输入${comment}" /> 346 <TextArea v-model:value="formData.${javaField}" placeholder="请输入${comment}" />
341 - </Form.Item> 347 + </FormItem>
342 #end 348 #end
343 #end 349 #end
344 #end 350 #end
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antdv_next/general/views/modules/list_sub_erp.vue.vm
@@ -5,6 +5,16 @@ @@ -5,6 +5,16 @@
5 #set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 5 #set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
6 #set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($subIndex)) 6 #set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($subIndex))
7 #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 7 #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
  8 +#set ($rangePickerEnable = false)
  9 +#set ($selectOptionEnable = false)
  10 +#foreach ($column in $subColumns)
  11 + #if ($column.listOperation && $column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
  12 + #set ($rangePickerEnable = true)
  13 + #end
  14 + #if ($column.listOperation && ($column.htmlType == "select" || $column.htmlType == "radio" || $column.htmlType == "checkbox"))
  15 + #set ($selectOptionEnable = true)
  16 + #end
  17 +#end
8 <script lang="ts" setup> 18 <script lang="ts" setup>
9 import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}'; 19 import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
10 import type { VxeTableInstance } from '#/adapter/vxe-table'; 20 import type { VxeTableInstance } from '#/adapter/vxe-table';
@@ -15,7 +25,9 @@ import { DICT_TYPE } from &#39;@vben/constants&#39;; @@ -15,7 +25,9 @@ import { DICT_TYPE } from &#39;@vben/constants&#39;;
15 import { getDictOptions } from '@vben/hooks'; 25 import { getDictOptions } from '@vben/hooks';
16 26
17 import { DictTag } from '#/components/dict-tag'; 27 import { DictTag } from '#/components/dict-tag';
  28 +#if ($table.templateType == 11 && $rangePickerEnable)
18 import { getRangePickerDefaultProps } from '#/utils/rangePickerProps'; 29 import { getRangePickerDefaultProps } from '#/utils/rangePickerProps';
  30 +#end
19 import { VxeColumn, VxeTable } from '#/adapter/vxe-table'; 31 import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
20 import { formatDateTime } from '@vben/utils'; 32 import { formatDateTime } from '@vben/utils';
21 33
@@ -25,7 +37,7 @@ import { useTableToolbar, VbenVxeTableToolbar } from &#39;@vben/plugins/vxe-table&#39;; @@ -25,7 +37,7 @@ import { useTableToolbar, VbenVxeTableToolbar } from &#39;@vben/plugins/vxe-table&#39;;
25 import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue' 37 import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue'
26 import { Tinymce as RichTextarea } from '#/components/tinymce'; 38 import { Tinymce as RichTextarea } from '#/components/tinymce';
27 import { ImageUpload, FileUpload } from "#/components/upload"; 39 import { ImageUpload, FileUpload } from "#/components/upload";
28 -import { message, Button, Card, Tabs, Pagination, Form, Input, TextArea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, RangePicker, DatePicker, TreeSelect } from 'antdv-next'; 40 +import { Button, Card, Checkbox, CheckboxGroup, DatePicker, Form, FormItem, Input, message, Pagination, Radio, RadioGroup#if ($rangePickerEnable), DateRangePicker as RangePicker#end, Select#if ($selectOptionEnable), SelectOption#end, TextArea, TreeSelect } from 'antdv-next';
29 import { Plus, Trash2 } from '@vben/icons'; 41 import { Plus, Trash2 } from '@vben/icons';
30 import { $t } from '#/locales'; 42 import { $t } from '#/locales';
31 #end 43 #end
@@ -224,7 +236,7 @@ onMounted(() =&gt; { @@ -224,7 +236,7 @@ onMounted(() =&gt; {
224 #set ($dictMethod = "boolean") 236 #set ($dictMethod = "boolean")
225 #end 237 #end
226 #if ($column.htmlType == "input") 238 #if ($column.htmlType == "input")
227 - <Form.Item label="${comment}" name="${javaField}"> 239 + <FormItem label="${comment}" name="${javaField}">
228 <Input 240 <Input
229 v-model:value="queryParams.${javaField}" 241 v-model:value="queryParams.${javaField}"
230 placeholder="请输入${comment}" 242 placeholder="请输入${comment}"
@@ -232,9 +244,9 @@ onMounted(() =&gt; { @@ -232,9 +244,9 @@ onMounted(() =&gt; {
232 @pressEnter="handleQuery" 244 @pressEnter="handleQuery"
233 class="w-full" 245 class="w-full"
234 /> 246 />
235 - </Form.Item> 247 + </FormItem>
236 #elseif ($column.htmlType == "select" || $column.htmlType == "radio" || $column.htmlType == "checkbox") 248 #elseif ($column.htmlType == "select" || $column.htmlType == "radio" || $column.htmlType == "checkbox")
237 - <Form.Item label="${comment}" name="${javaField}"> 249 + <FormItem label="${comment}" name="${javaField}">
238 <Select 250 <Select
239 v-model:value="queryParams.${javaField}" 251 v-model:value="queryParams.${javaField}"
240 placeholder="请选择${comment}" 252 placeholder="请选择${comment}"
@@ -253,10 +265,10 @@ onMounted(() =&gt; { @@ -253,10 +265,10 @@ onMounted(() =&gt; {
253 <SelectOption label="请选择字典生成" value="" /> 265 <SelectOption label="请选择字典生成" value="" />
254 #end 266 #end
255 </Select> 267 </Select>
256 - </Form.Item> 268 + </FormItem>
257 #elseif($column.htmlType == "datetime") 269 #elseif($column.htmlType == "datetime")
258 #if ($column.listOperationCondition != "BETWEEN")## 非范围 270 #if ($column.listOperationCondition != "BETWEEN")## 非范围
259 - <Form.Item label="${comment}" name="${javaField}"> 271 + <FormItem label="${comment}" name="${javaField}">
260 <DatePicker 272 <DatePicker
261 v-model:value="queryParams.${javaField}" 273 v-model:value="queryParams.${javaField}"
262 valueFormat="YYYY-MM-DD" 274 valueFormat="YYYY-MM-DD"
@@ -264,25 +276,25 @@ onMounted(() =&gt; { @@ -264,25 +276,25 @@ onMounted(() =&gt; {
264 allowClear 276 allowClear
265 class="w-full" 277 class="w-full"
266 /> 278 />
267 - </Form.Item> 279 + </FormItem>
268 #else## 范围 280 #else## 范围
269 - <Form.Item label="${comment}" name="${javaField}"> 281 + <FormItem label="${comment}" name="${javaField}">
270 <RangePicker 282 <RangePicker
271 v-model:value="queryParams.${javaField}" 283 v-model:value="queryParams.${javaField}"
272 v-bind="getRangePickerDefaultProps()" 284 v-bind="getRangePickerDefaultProps()"
273 class="w-full" 285 class="w-full"
274 /> 286 />
275 - </Form.Item> 287 + </FormItem>
276 #end 288 #end
277 #end 289 #end
278 #end 290 #end
279 #end 291 #end
280 - <Form.Item> 292 + <FormItem>
281 <Button class="ml-2" @click="resetQuery"> 重置 </Button> 293 <Button class="ml-2" @click="resetQuery"> 重置 </Button>
282 <Button class="ml-2" @click="handleQuery" type="primary"> 294 <Button class="ml-2" @click="handleQuery" type="primary">
283 搜索 295 搜索
284 </Button> 296 </Button>
285 - </Form.Item> 297 + </FormItem>
286 </Form> 298 </Form>
287 </Card> 299 </Card>
288 300
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antdv_next/general/views/modules/list_sub_inner.vue.vm
1 ## 子表的 erp 和 inner 使用相似的 list 列表,差异主要两点: 1 ## 子表的 erp 和 inner 使用相似的 list 列表,差异主要两点:
2 ## 1)inner 使用 list 不分页,erp 使用 page 分页 2 ## 1)inner 使用 list 不分页,erp 使用 page 分页
3 ## 2)erp 支持单个子表的新增、修改、删除,inner 不支持 3 ## 2)erp 支持单个子表的新增、修改、删除,inner 不支持
4 -#parse("codegen/vue3_vben5_antd/general/views/modules/list_sub_erp.vue.vm")  
5 \ No newline at end of file 4 \ No newline at end of file
  5 +#parse("codegen/vue3_vben5_antdv_next/general/views/modules/list_sub_erp.vue.vm")
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antdv_next/schema/views/form.vue.vm
@@ -6,7 +6,7 @@ import { computed, ref } from &#39;vue&#39;; @@ -6,7 +6,7 @@ import { computed, ref } from &#39;vue&#39;;
6 6
7 import { useVbenModal } from '@vben/common-ui'; 7 import { useVbenModal } from '@vben/common-ui';
8 8
9 -import { message#if ($table.templateType == 11), Tabs#end } from 'antdv-next'; 9 +import { message#if ($table.templateType == 10 || $table.templateType == 12), TabPane, Tabs#end } from 'antdv-next';
10 10
11 import { useVbenForm } from '#/adapter/form'; 11 import { useVbenForm } from '#/adapter/form';
12 import { create${simpleClassName}, get${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}'; 12 import { create${simpleClassName}, get${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antdv_next/schema/views/import.vue.vm
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 -import type { FileType } from 'antdv-next/es/upload/interface';  
3 -  
4 import { useVbenModal } from '@vben/common-ui'; 2 import { useVbenModal } from '@vben/common-ui';
5 import { downloadFileFromBlobPart } from '@vben/utils'; 3 import { downloadFileFromBlobPart } from '@vben/utils';
6 4
@@ -46,7 +44,7 @@ const [Modal, modalApi] = useVbenModal({ @@ -46,7 +44,7 @@ const [Modal, modalApi] = useVbenModal({
46 }); 44 });
47 45
48 /** 上传前 */ 46 /** 上传前 */
49 -function beforeUpload(file: FileType) { 47 +function beforeUpload(file: File) {
50 formApi.setFieldValue('file', file); 48 formApi.setFieldValue('file', file);
51 return false; 49 return false;
52 } 50 }
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antdv_next/schema/views/index.vue.vm
@@ -8,7 +8,7 @@ import { ref } from &#39;vue&#39;; @@ -8,7 +8,7 @@ import { ref } from &#39;vue&#39;;
8 import {#if ($table.templateType != 2 && $deleteBatchEnable) confirm,#end Page, useVbenModal } from '@vben/common-ui'; 8 import {#if ($table.templateType != 2 && $deleteBatchEnable) confirm,#end Page, useVbenModal } from '@vben/common-ui';
9 import { downloadFileFromBlobPart#if ($table.templateType != 2 && $deleteBatchEnable), isEmpty#end } from '@vben/utils'; 9 import { downloadFileFromBlobPart#if ($table.templateType != 2 && $deleteBatchEnable), isEmpty#end } from '@vben/utils';
10 10
11 -import { message#if ($table.templateType == 11), Tabs#end } from 'antdv-next'; 11 +import { message#if ($table.templateType == 11 || $table.templateType == 12), TabPane, Tabs#end } from 'antdv-next';
12 12
13 import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; 13 import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
14 #if (${table.templateType} == 2)## 树表接口 14 #if (${table.templateType} == 2)## 树表接口
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antdv_next/schema/views/modules/form_sub_inner.vue.vm
1 ## 主表的 normal 和 inner 使用相同的 form 表单 1 ## 主表的 normal 和 inner 使用相同的 form 表单
2 -#parse("codegen/vue3_vben5_antd/schema/views/modules/form_sub_normal.vue.vm")  
3 \ No newline at end of file 2 \ No newline at end of file
  3 +#parse("codegen/vue3_vben5_antdv_next/schema/views/modules/form_sub_normal.vue.vm")
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antdv_next/schema/views/modules/form_sub_normal.vue.vm
@@ -5,6 +5,12 @@ @@ -5,6 +5,12 @@
5 #set ($subClassNameVar = $subClassNameVars.get($subIndex)) 5 #set ($subClassNameVar = $subClassNameVars.get($subIndex))
6 #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 6 #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
7 #set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api") 7 #set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
  8 +#set ($selectOptionEnable = false)
  9 +#foreach ($column in $subColumns)
  10 + #if (($column.createOperation || $column.updateOperation) && $column.id != $subJoinColumn.id && $column.htmlType == "select")
  11 + #set ($selectOptionEnable = true)
  12 + #end
  13 +#end
8 <script lang="ts" setup> 14 <script lang="ts" setup>
9 import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}'; 15 import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
10 16
@@ -14,7 +20,19 @@ @@ -14,7 +20,19 @@
14 20
15 #if ($subTable.subJoinMany) ## 一对多 21 #if ($subTable.subJoinMany) ## 一对多
16 import { Plus } from "@vben/icons"; 22 import { Plus } from "@vben/icons";
17 -import { Button, Tabs, Checkbox, Input, TextArea, Select,RadioGroup,CheckboxGroup, DatePicker } from 'antdv-next'; 23 +import {
  24 + Button,
  25 + Checkbox,
  26 + CheckboxGroup,
  27 + DatePicker,
  28 + Input,
  29 + RadioGroup,
  30 + Select,
  31 +#if ($selectOptionEnable)
  32 + SelectOption,
  33 +#end
  34 + TextArea,
  35 +} from 'antdv-next';
18 import { ImageUpload, FileUpload } from "#/components/upload"; 36 import { ImageUpload, FileUpload } from "#/components/upload";
19 import { useVbenVxeGrid } from '#/adapter/vxe-table'; 37 import { useVbenVxeGrid } from '#/adapter/vxe-table';
20 import { use${subSimpleClassName}GridEditColumns } from '../data'; 38 import { use${subSimpleClassName}GridEditColumns } from '../data';
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antdv_next/schema/views/modules/list_sub_inner.vue.vm
1 ## 子表的 erp 和 inner 使用相似的 list 列表,差异主要两点: 1 ## 子表的 erp 和 inner 使用相似的 list 列表,差异主要两点:
2 ## 1)inner 使用 list 不分页,erp 使用 page 分页 2 ## 1)inner 使用 list 不分页,erp 使用 page 分页
3 ## 2)erp 支持单个子表的新增、修改、删除,inner 不支持 3 ## 2)erp 支持单个子表的新增、修改、删除,inner 不支持
4 -#parse("codegen/vue3_vben5_antd/schema/views/modules/list_sub_erp.vue.vm")  
5 \ No newline at end of file 4 \ No newline at end of file
  5 +#parse("codegen/vue3_vben5_antdv_next/schema/views/modules/list_sub_erp.vue.vm")
yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineAbstractTest.java
@@ -157,13 +157,17 @@ public abstract class CodegenEngineAbstractTest extends BaseMockitoUnitTest { @@ -157,13 +157,17 @@ public abstract class CodegenEngineAbstractTest extends BaseMockitoUnitTest {
157 String lastFilePath = StrUtil.subAfter(filePath, '/', true); 157 String lastFilePath = StrUtil.subAfter(filePath, '/', true);
158 String ext = StrUtil.subAfter(lastFilePath, '.', true); 158 String ext = StrUtil.subAfter(lastFilePath, '.', true);
159 String name = StrUtil.subBefore(lastFilePath, '.', true); 159 String name = StrUtil.subBefore(lastFilePath, '.', true);
160 - String contentPath = ext + '/' + name;  
161 - // 同名文件(如 index.vue 同时出现在 列表/form/detail)会撞名,撞名时前缀补上级目录区分;仍撞则加序号兜底,避免快照互相覆盖 160 + String parentPath = StrUtil.subBefore(filePath, '/' + lastFilePath, true);
  161 + String parentDir = StrUtil.subAfter(parentPath, '/', true);
  162 + // index.vue 按页面语义稳定归档,避免结果迭代顺序导致列表页和表单页的快照路径互换
  163 + String contentPath = "index".equals(name) && ("form".equals(parentDir) || "detail".equals(parentDir))
  164 + ? ext + '/' + parentDir + '/' + name : ext + '/' + name;
  165 + // ERP 多个子表的 form/index.vue 仍可能撞名,继续补充祖父目录区分
162 if (usedContentPaths.contains(contentPath)) { 166 if (usedContentPaths.contains(contentPath)) {
163 - String parentDir = StrUtil.subAfter(StrUtil.subBefore(filePath, '/' + lastFilePath, true), '/', true);  
164 - contentPath = ext + '/' + parentDir + '/' + name; 167 + String grandParentDir = StrUtil.subAfter(StrUtil.subBefore(parentPath, '/' + parentDir, true), '/', true);
  168 + contentPath = ext + '/' + grandParentDir + '/' + parentDir + '/' + name;
165 for (int i = 2; usedContentPaths.contains(contentPath); i++) { 169 for (int i = 2; usedContentPaths.contains(contentPath); i++) {
166 - contentPath = ext + '/' + parentDir + '/' + name + '-' + i; 170 + contentPath = ext + '/' + grandParentDir + '/' + parentDir + '/' + name + '-' + i;
167 } 171 }
168 } 172 }
169 usedContentPaths.add(contentPath); 173 usedContentPaths.add(contentPath);
yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineUniappTest.java
1 package cn.iocoder.yudao.module.infra.service.codegen.inner; 1 package cn.iocoder.yudao.module.infra.service.codegen.inner;
2 2
  3 +import cn.iocoder.yudao.framework.common.exception.ServiceException;
3 import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO; 4 import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
4 import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO; 5 import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
5 import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum; 6 import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
@@ -7,8 +8,15 @@ import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum; @@ -7,8 +8,15 @@ import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
7 import com.baomidou.mybatisplus.annotation.DbType; 8 import com.baomidou.mybatisplus.annotation.DbType;
8 import org.junit.jupiter.api.Test; 9 import org.junit.jupiter.api.Test;
9 10
  11 +import java.util.Arrays;
  12 +import java.util.HashMap;
10 import java.util.List; 13 import java.util.List;
11 import java.util.Map; 14 import java.util.Map;
  15 +import java.util.Objects;
  16 +
  17 +import static org.junit.jupiter.api.Assertions.assertFalse;
  18 +import static org.junit.jupiter.api.Assertions.assertThrows;
  19 +import static org.junit.jupiter.api.Assertions.assertTrue;
12 20
13 /** 21 /**
14 * {@link CodegenEngine} 的 Vue3 Admin Uniapp + Wot UI 单元测试 22 * {@link CodegenEngine} 的 Vue3 Admin Uniapp + Wot UI 单元测试
@@ -31,4 +39,340 @@ public class CodegenEngineUniappTest extends CodegenEngineAbstractTest { @@ -31,4 +39,340 @@ public class CodegenEngineUniappTest extends CodegenEngineAbstractTest {
31 assertResult(result, "/vue3_admin_uniapp_one"); 39 assertResult(result, "/vue3_admin_uniapp_one");
32 } 40 }
33 41
  42 + @Test
  43 + public void testExecute_dictSearch() {
  44 + // 准备参数
  45 + CodegenTableDO table = getTable("student")
  46 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  47 + .setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
  48 + List<CodegenColumnDO> columns = getColumnList("student");
  49 +
  50 + // 调用并断言数字、布尔字典
  51 + Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
  52 + String searchForm = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/components/search-form.vue");
  53 + assertFalse(searchForm.contains("<wd-radio"));
  54 + assertTrue(searchForm.contains("all-option"));
  55 + assertTrue(searchForm.contains(":columns=\"getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)\""));
  56 + assertTrue(searchForm.contains(":columns=\"getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)\""));
  57 +
  58 + // 调用并断言字符串字典
  59 + columns.stream().filter(column -> "sex".equals(column.getJavaField())).findFirst().orElseThrow()
  60 + .setJavaType("String");
  61 + result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
  62 + searchForm = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/components/search-form.vue");
  63 + assertTrue(searchForm.contains(":columns=\"getStrDictOptions(DICT_TYPE.SYSTEM_USER_SEX)\""));
  64 + assertTrue(searchForm.contains("sex: undefined as string | undefined,"));
  65 + }
  66 +
  67 + @Test
  68 + public void testExecute_tree() {
  69 + // 准备参数
  70 + CodegenTableDO table = getTable("category")
  71 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  72 + .setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
  73 + List<CodegenColumnDO> columns = getColumnList("category");
  74 +
  75 + // 调用
  76 + Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
  77 + // 断言
  78 + assertResult(result, "/vue3_admin_uniapp_tree");
  79 + }
  80 +
  81 + @Test
  82 + public void testExecute_semantics() {
  83 + // 准备参数
  84 + CodegenTableDO table = getTable("student")
  85 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  86 + .setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
  87 + List<CodegenColumnDO> columns = getColumnList("student");
  88 + columns.stream().filter(column -> "memo".equals(column.getJavaField())).findFirst().orElseThrow()
  89 + .setJavaType("Float").setHtmlType("input");
  90 +
  91 + // 调用
  92 + Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
  93 + // 断言
  94 + String api = result.get("yudao-ui-admin-uniapp/src/api/infra/demo/index.ts");
  95 + assertTrue(api.contains("birthday: number"));
  96 + assertTrue(api.contains("createTime?: number"));
  97 + assertTrue(api.contains("memo: number"));
  98 + assertFalse(api.contains("createTime?: Date"));
  99 + String form = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue");
  100 + assertTrue(form.contains("v-model=\"formData.memo\""));
  101 + assertTrue(form.contains(":precision=\"2\""));
  102 + String detail = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue");
  103 + assertTrue(detail.contains("type=\"danger\""));
  104 + assertFalse(detail.contains("type=\"error\""));
  105 + }
  106 +
  107 + @Test
  108 + public void testExecute_stringPrimaryKey() {
  109 + // 准备主表
  110 + CodegenTableDO table = getTable("student")
  111 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  112 + .setTemplateType(CodegenTemplateTypeEnum.MASTER_ERP.getType());
  113 + List<CodegenColumnDO> columns = getColumnList("student");
  114 + columns.stream().filter(CodegenColumnDO::getPrimaryKey).findFirst().orElseThrow().setJavaType("String");
  115 + // 准备子表
  116 + CodegenTableDO contactTable = getTable("contact")
  117 + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
  118 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  119 + .setSubJoinColumnId(100L).setSubJoinMany(true);
  120 + List<CodegenColumnDO> contactColumns = getColumnList("contact");
  121 + contactColumns.stream().filter(CodegenColumnDO::getPrimaryKey).findFirst().orElseThrow().setJavaType("String");
  122 + contactColumns.stream().filter(column -> Objects.equals(column.getId(), 100L))
  123 + .findFirst().orElseThrow().setJavaType("String");
  124 +
  125 + // 调用
  126 + Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
  127 + List.of(contactTable), List.of(contactColumns));
  128 + // 断言
  129 + String api = result.get("yudao-ui-admin-uniapp/src/api/infra/demo/index.ts");
  130 + assertTrue(api.contains("return http.post<string>('/infra/student/create', data)"));
  131 + assertTrue(api.contains("studentId: string"));
  132 + assertTrue(api.contains("getStudentContact(id: string)"));
  133 + assertTrue(api.contains("deleteStudentContact(id: string)"));
  134 + String detail = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue");
  135 + assertFalse(detail.contains("Number(props.id)"));
  136 + String subForm = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/student-contact/form/index.vue");
  137 + assertTrue(subForm.contains("id?: string"));
  138 + assertTrue(subForm.contains("studentId: string"));
  139 + }
  140 +
  141 + @Test
  142 + public void testExecute_treeSearch() {
  143 + // 准备参数
  144 + CodegenTableDO table = getTable("category")
  145 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  146 + .setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
  147 + List<CodegenColumnDO> columns = getColumnList("category");
  148 +
  149 + // 调用
  150 + Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
  151 + // 断言
  152 + String index = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/index.vue");
  153 + assertTrue(index.contains("currentParentId.value === 0\n ? list.value"));
  154 + assertFalse(index.contains("list.value.filter"));
  155 + String breadcrumb = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/components/breadcrumb.vue");
  156 + assertTrue(breadcrumb.contains("modelValue: number"));
  157 + assertFalse(breadcrumb.contains("TreeValue"));
  158 + }
  159 +
  160 + @Test
  161 + public void testExecute_duplicateErpSymbol() {
  162 + // 准备主表
  163 + CodegenTableDO table = getTable("student")
  164 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  165 + .setTemplateType(CodegenTemplateTypeEnum.MASTER_ERP.getType());
  166 + List<CodegenColumnDO> columns = getColumnList("student");
  167 + // 准备两个归一化名称相同的子表
  168 + CodegenTableDO crmItemTable = getTable("contact").setModuleName("crm").setClassName("CrmOrderItem")
  169 + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
  170 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  171 + .setSubJoinColumnId(100L).setSubJoinMany(true);
  172 + CodegenTableDO erpItemTable = getTable("contact").setModuleName("erp").setClassName("ErpOrderItem")
  173 + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
  174 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  175 + .setSubJoinColumnId(100L).setSubJoinMany(true);
  176 + List<CodegenColumnDO> crmItemColumns = getColumnList("contact");
  177 + List<CodegenColumnDO> erpItemColumns = getColumnList("contact");
  178 +
  179 + // 调用
  180 + ServiceException exception = assertThrows(ServiceException.class,
  181 + () -> codegenEngine.execute(DbType.MYSQL, table, columns,
  182 + Arrays.asList(crmItemTable, erpItemTable), Arrays.asList(crmItemColumns, erpItemColumns)));
  183 + // 断言
  184 + assertTrue(exception.getMessage().contains("主子表规范化类名(OrderItem)重复"));
  185 + }
  186 +
  187 + @Test
  188 + public void testExecute_duplicateMasterSymbol() {
  189 + // 准备主表
  190 + CodegenTableDO table = getTable("student")
  191 + .setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType())
  192 + .setTemplateType(CodegenTemplateTypeEnum.MASTER_NORMAL.getType());
  193 + List<CodegenColumnDO> columns = getColumnList("student");
  194 + // 准备两个规范化名称相同的子表
  195 + CodegenTableDO crmItemTable = getTable("contact").setModuleName("crm").setClassName("CrmOrderItem")
  196 + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(true);
  197 + CodegenTableDO erpItemTable = getTable("contact").setModuleName("erp").setClassName("ErpOrderItem")
  198 + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(true);
  199 + List<CodegenColumnDO> crmItemColumns = getColumnList("contact");
  200 + List<CodegenColumnDO> erpItemColumns = getColumnList("contact");
  201 +
  202 + // 调用并断言
  203 + ServiceException exception = assertThrows(ServiceException.class,
  204 + () -> codegenEngine.execute(DbType.MYSQL, table, columns,
  205 + Arrays.asList(crmItemTable, erpItemTable), Arrays.asList(crmItemColumns, erpItemColumns)));
  206 + assertTrue(exception.getMessage().contains("主子表规范化类名(OrderItem)重复"));
  207 + }
  208 +
  209 + @Test
  210 + public void testExecute_duplicateMasterFieldNormal() {
  211 + testExecute_duplicateMasterField(CodegenTemplateTypeEnum.MASTER_NORMAL);
  212 + }
  213 +
  214 + @Test
  215 + public void testExecute_duplicateMasterFieldInner() {
  216 + testExecute_duplicateMasterField(CodegenTemplateTypeEnum.MASTER_INNER);
  217 + }
  218 +
  219 + @Test
  220 + public void testExecute_duplicateMasterFieldWithMainNormal() {
  221 + testExecute_duplicateMasterFieldWithMain(CodegenTemplateTypeEnum.MASTER_NORMAL);
  222 + }
  223 +
  224 + @Test
  225 + public void testExecute_duplicateMasterFieldWithMainInner() {
  226 + testExecute_duplicateMasterFieldWithMain(CodegenTemplateTypeEnum.MASTER_INNER);
  227 + }
  228 +
  229 + @Test
  230 + public void testExecute_duplicateMasterFieldErp() {
  231 + // 准备主表
  232 + CodegenTableDO table = getTable("student")
  233 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  234 + .setTemplateType(CodegenTemplateTypeEnum.MASTER_ERP.getType());
  235 + List<CodegenColumnDO> columns = getColumnList("student");
  236 + // 准备属性名同为 items 的子表:Item 一对多、Items 一对一
  237 + CodegenTableDO crmItemTable = getTable("contact").setModuleName("crm").setClassName("CrmItem")
  238 + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(true);
  239 + CodegenTableDO erpItemsTable = getTable("contact").setModuleName("erp").setClassName("ErpItems")
  240 + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(false);
  241 +
  242 + // 调用
  243 + Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
  244 + Arrays.asList(crmItemTable, erpItemsTable),
  245 + Arrays.asList(getColumnList("contact"), getColumnList("contact")));
  246 + // 断言
  247 + assertTrue(result.containsKey("yudao-ui-admin-uniapp/src/pages-infra/demo/item/form/index.vue"));
  248 + assertTrue(result.containsKey("yudao-ui-admin-uniapp/src/pages-infra/demo/items/form/index.vue"));
  249 + }
  250 +
  251 + @Test
  252 + public void testRegisterGeneratedSource_duplicatePath() {
  253 + // 准备参数
  254 + Map<String, String> generatedSources = new HashMap<>();
  255 + Map<String, Object> bindingMap = new HashMap<>();
  256 + bindingMap.put("subIndex", 0);
  257 + CodegenEngine.registerGeneratedSource(generatedSources, "same.vue", "form_sub.vm", bindingMap);
  258 + bindingMap.put("subIndex", 1);
  259 +
  260 + // 调用
  261 + IllegalStateException exception = assertThrows(IllegalStateException.class,
  262 + () -> CodegenEngine.registerGeneratedSource(generatedSources, "same.vue", "form_sub.vm", bindingMap));
  263 + // 断言
  264 + assertTrue(exception.getMessage().contains("生成文件路径重复:same.vue"));
  265 + assertTrue(exception.getMessage().contains("form_sub.vm[subIndex=0]"));
  266 + assertTrue(exception.getMessage().contains("form_sub.vm[subIndex=1]"));
  267 + }
  268 +
  269 + @Test
  270 + public void testExecute_masterNormal() {
  271 + testExecute_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "/vue3_admin_uniapp_master_normal");
  272 + }
  273 +
  274 + @Test
  275 + public void testExecute_masterInner() {
  276 + testExecute_master(CodegenTemplateTypeEnum.MASTER_INNER, "/vue3_admin_uniapp_master_inner");
  277 + }
  278 +
  279 + @Test
  280 + public void testExecute_masterErp() {
  281 + testExecute_master(CodegenTemplateTypeEnum.MASTER_ERP, "/vue3_admin_uniapp_master_erp");
  282 + }
  283 +
  284 + private void testExecute_duplicateMasterField(CodegenTemplateTypeEnum templateType) {
  285 + // 准备主表
  286 + CodegenTableDO table = getTable("student")
  287 + .setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType())
  288 + .setTemplateType(templateType.getType());
  289 + List<CodegenColumnDO> columns = getColumnList("student");
  290 + // 准备属性名同为 items 的子表:Item 一对多、Items 一对一
  291 + CodegenTableDO crmItemTable = getTable("contact").setModuleName("crm").setClassName("CrmItem")
  292 + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(true);
  293 + CodegenTableDO erpItemsTable = getTable("contact").setModuleName("erp").setClassName("ErpItems")
  294 + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(false);
  295 + List<CodegenColumnDO> crmItemColumns = getColumnList("contact");
  296 + List<CodegenColumnDO> erpItemsColumns = getColumnList("contact");
  297 +
  298 + // 调用并断言
  299 + ServiceException exception = assertThrows(ServiceException.class,
  300 + () -> codegenEngine.execute(DbType.MYSQL, table, columns,
  301 + Arrays.asList(crmItemTable, erpItemsTable), Arrays.asList(crmItemColumns, erpItemsColumns)));
  302 + assertTrue(exception.getMessage().contains("主子表属性名(items)重复"));
  303 + }
  304 +
  305 + private void testExecute_duplicateMasterFieldWithMain(CodegenTemplateTypeEnum templateType) {
  306 + // 准备主表
  307 + CodegenTableDO table = getTable("student")
  308 + .setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType())
  309 + .setTemplateType(templateType.getType());
  310 + List<CodegenColumnDO> columns = getColumnList("student");
  311 + columns.stream().filter(column -> "memo".equals(column.getJavaField())).findFirst().orElseThrow()
  312 + .setJavaField("items");
  313 + // 准备一对多子表 Item,生成属性名 items
  314 + CodegenTableDO itemTable = getTable("contact").setModuleName("crm").setClassName("CrmItem")
  315 + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(true);
  316 +
  317 + // 调用并断言
  318 + ServiceException exception = assertThrows(ServiceException.class,
  319 + () -> codegenEngine.execute(DbType.MYSQL, table, columns,
  320 + List.of(itemTable), List.of(getColumnList("contact"))));
  321 + assertTrue(exception.getMessage().contains("主子表属性名(items)重复"));
  322 + }
  323 +
  324 + private void testExecute_master(CodegenTemplateTypeEnum templateType, String path) {
  325 + // 准备主表
  326 + CodegenTableDO table = getTable("student")
  327 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  328 + .setTemplateType(templateType.getType());
  329 + List<CodegenColumnDO> columns = getColumnList("student");
  330 + // 准备一对多子表
  331 + CodegenTableDO contactTable = getTable("contact")
  332 + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
  333 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  334 + .setSubJoinColumnId(100L).setSubJoinMany(true);
  335 + List<CodegenColumnDO> contactColumns = getColumnList("contact");
  336 + // 准备一对一子表
  337 + CodegenTableDO teacherTable = getTable("teacher")
  338 + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
  339 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  340 + .setSubJoinColumnId(200L).setSubJoinMany(false);
  341 + List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
  342 +
  343 + // 调用
  344 + Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
  345 + Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
  346 + // 断言
  347 + assertResult(result, path);
  348 + if (templateType != CodegenTemplateTypeEnum.MASTER_ERP) {
  349 + String api = result.get("yudao-ui-admin-uniapp/src/api/infra/demo/index.ts");
  350 + assertTrue(api.contains("studentTeacher?: StudentTeacher | null"));
  351 + String form = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue");
  352 + assertTrue(form.contains("ref<Partial<StudentTeacher> | null>(null)"));
  353 + assertTrue(form.contains("studentTeacher.value = studentTeacherData || null"));
  354 + assertTrue(form.contains(":model-value=\"item.birthday || Date.now()\""));
  355 + assertTrue(form.contains("v-model=\"item.sex\""));
  356 + assertTrue(form.contains("<yd-upload-img v-model=\"item.avatar\""));
  357 + assertTrue(form.contains("<yd-upload-file v-model=\"item.video\""));
  358 + assertTrue(form.contains("<yd-upload-file v-model=\"item.video\" :limit=\"1\""));
  359 + assertTrue(form.contains("v-model=\"item.description\""));
  360 + assertTrue(form.contains("v-model=\"item.memo\""));
  361 + assertTrue(form.contains("birthday: undefined"));
  362 + assertTrue(form.contains("function validateSubTables()"));
  363 + } else {
  364 + String contactForm = result.get(
  365 + "yudao-ui-admin-uniapp/src/pages-infra/demo/student-contact/form/index.vue");
  366 + assertTrue(contactForm.contains(":model-value=\"formData.birthday || Date.now()\""));
  367 + assertTrue(contactForm.contains("v-model=\"formData.sex\""));
  368 + assertTrue(contactForm.contains("<yd-upload-img v-model=\"formData.avatar\""));
  369 + assertTrue(contactForm.contains("<yd-upload-file v-model=\"formData.video\""));
  370 + assertTrue(contactForm.contains("<yd-upload-file v-model=\"formData.video\" :limit=\"1\""));
  371 + assertTrue(contactForm.contains("v-model=\"formData.description\""));
  372 + assertTrue(contactForm.contains("v-model=\"formData.memo\""));
  373 + assertTrue(contactForm.contains("birthday: undefined"));
  374 + assertTrue(contactForm.contains(":columns=\"getBoolDictOptions"));
  375 + }
  376 + }
  377 +
34 } 378 }
yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineVben5AntdvNextTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.service.codegen.inner;
  2 +
  3 +import cn.hutool.core.io.resource.ResourceUtil;
  4 +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
  5 +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
  6 +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
  7 +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
  8 +import com.baomidou.mybatisplus.annotation.DbType;
  9 +import org.junit.jupiter.api.Test;
  10 +
  11 +import java.util.Arrays;
  12 +import java.util.List;
  13 +import java.util.Map;
  14 +import java.util.regex.Pattern;
  15 +
  16 +import static org.junit.jupiter.api.Assertions.assertEquals;
  17 +import static org.junit.jupiter.api.Assertions.assertFalse;
  18 +import static org.junit.jupiter.api.Assertions.assertNotNull;
  19 +import static org.junit.jupiter.api.Assertions.assertTrue;
  20 +
  21 +/**
  22 + * {@link CodegenEngine} 的 Vue3 + Vben5 + Antdv Next 单元测试
  23 + *
  24 + * @author 芋道源码
  25 + */
  26 +public class CodegenEngineVben5AntdvNextTest extends CodegenEngineAbstractTest {
  27 +
  28 + private static final Pattern ANTDV_NEXT_VALUE_IMPORT_PATTERN = Pattern.compile(
  29 + "import\\s*\\{([^}]*)}\\s*from\\s*'antdv-next';?", Pattern.DOTALL);
  30 + private static final Pattern RANGE_PICKER_COMPONENT_PATTERN = Pattern.compile("<RangePicker\\b");
  31 + private static final Pattern RANGE_PICKER_HELPER_BINDING_PATTERN = Pattern.compile(
  32 + "\\bv-bind\\s*=\\s*([\"'])\\s*getRangePickerDefaultProps\\s*\\(\\s*\\)\\s*\\1");
  33 + private static final Pattern RANGE_PICKER_HELPER_CALL_PATTERN = Pattern.compile(
  34 + "\\bgetRangePickerDefaultProps\\s*\\(\\s*\\)");
  35 + private static final Pattern RANGE_PICKER_HELPER_IMPORT_PATTERN = Pattern.compile(
  36 + "import\\s*\\{[^}]*\\bgetRangePickerDefaultProps\\b[^}]*}"
  37 + + "\\s*from\\s*'#/utils(?:/rangePickerProps)?';?", Pattern.DOTALL);
  38 + private static final Pattern SCHEMA_RANGE_PICKER_COMPONENT_PATTERN = Pattern.compile(
  39 + "\\bcomponent\\s*:\\s*([\"'])RangePicker\\1");
  40 + private static final Pattern SCHEMA_RANGE_PICKER_HELPER_SPREAD_PATTERN = Pattern.compile(
  41 + "\\.\\.\\.\\s*getRangePickerDefaultProps\\s*\\(\\s*\\)");
  42 +
  43 + private static final String IMPORT_FORM_PATH =
  44 + "yudao-ui-admin-vben/src/views/infra/demo/modules/import-form.vue";
  45 + private static final String SCHEMA_DATA_PATH =
  46 + "yudao-ui-admin-vben/src/views/infra/demo/data.ts";
  47 + private static final String STUDENT_CONTACT_FORM_PATH =
  48 + "yudao-ui-admin-vben/src/views/infra/demo/modules/student-contact-form.vue";
  49 + private static final String STUDENT_TEACHER_FORM_PATH =
  50 + "yudao-ui-admin-vben/src/views/infra/demo/modules/student-teacher-form.vue";
  51 +
  52 + private static final List<String> INNER_TEMPLATE_PATHS = Arrays.asList(
  53 + "codegen/vue3_vben5_antdv_next/general/views/modules/form_sub_inner.vue.vm",
  54 + "codegen/vue3_vben5_antdv_next/general/views/modules/list_sub_inner.vue.vm",
  55 + "codegen/vue3_vben5_antdv_next/schema/views/modules/form_sub_inner.vue.vm",
  56 + "codegen/vue3_vben5_antdv_next/schema/views/modules/list_sub_inner.vue.vm");
  57 +
  58 + @Test
  59 + public void testExecute_schemaMasterNormal() {
  60 + // 调用
  61 + Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_SCHEMA.getType(),
  62 + CodegenTemplateTypeEnum.MASTER_NORMAL);
  63 +
  64 + // 断言
  65 + assertGeneratedCodeCompatibleWithAntdvNext(result);
  66 + assertSchemaRangePickerGenerated(result);
  67 + assertTabsGenerated(result);
  68 + }
  69 +
  70 + @Test
  71 + public void testExecute_schemaMasterErp() {
  72 + // 调用
  73 + Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_SCHEMA.getType(),
  74 + CodegenTemplateTypeEnum.MASTER_ERP);
  75 +
  76 + // 断言
  77 + assertGeneratedCodeCompatibleWithAntdvNext(result);
  78 + assertSchemaRangePickerGenerated(result);
  79 + assertTabsGenerated(result);
  80 + }
  81 +
  82 + @Test
  83 + public void testExecute_schemaMasterInner() {
  84 + // 调用
  85 + Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_SCHEMA.getType(),
  86 + CodegenTemplateTypeEnum.MASTER_INNER);
  87 +
  88 + // 断言
  89 + assertGeneratedCodeCompatibleWithAntdvNext(result);
  90 + assertSchemaRangePickerGenerated(result);
  91 + assertTabsGenerated(result);
  92 + }
  93 +
  94 + @Test
  95 + public void testExecute_generalTree() {
  96 + // 调用
  97 + Map<String, String> result = executeTree(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType());
  98 +
  99 + // 断言
  100 + assertGeneratedCodeCompatibleWithAntdvNext(result);
  101 + assertRuleGenerated(result);
  102 + assertComponentGenerated(result, "FormItem");
  103 + assertComponentGenerated(result, "TreeSelect");
  104 + }
  105 +
  106 + @Test
  107 + public void testExecute_generalMasterNormal() {
  108 + // 调用
  109 + Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType(),
  110 + CodegenTemplateTypeEnum.MASTER_NORMAL);
  111 +
  112 + // 断言
  113 + assertGeneratedCodeCompatibleWithAntdvNext(result);
  114 + assertRuleGenerated(result);
  115 + assertGeneralCrudComponentsGenerated(result);
  116 + assertTabsGenerated(result);
  117 + }
  118 +
  119 + @Test
  120 + public void testExecute_generalMasterNormal_oneToOneJoinColumnOnlySelect() {
  121 + // 调用
  122 + Map<String, String> result = executeGeneralMasterNormalWithOneToOneJoinSelect();
  123 +
  124 + // 断言
  125 + assertGeneratedCodeCompatibleWithAntdvNext(result);
  126 + String content = result.get(STUDENT_TEACHER_FORM_PATH);
  127 + assertNotNull(content, STUDENT_TEACHER_FORM_PATH + ":未生成一对一子表表单");
  128 + assertTrue(content.contains("<Select v-model:value=\"formData.studentId\""),
  129 + STUDENT_TEACHER_FORM_PATH + ":应渲染 join 列的 Select 组件");
  130 + assertTrue(content.contains("<SelectOption"), STUDENT_TEACHER_FORM_PATH + ":应渲染 SelectOption 组件");
  131 + assertRootValueImport(STUDENT_TEACHER_FORM_PATH, content, "\\bSelectOption\\b");
  132 + }
  133 +
  134 + @Test
  135 + public void testExecute_generalMasterNormal_oneToManyJoinColumnOnlySelect() {
  136 + // 调用
  137 + Map<String, String> result = executeGeneralMasterNormalWithOneToManyJoinSelect();
  138 +
  139 + // 断言
  140 + assertGeneratedCodeCompatibleWithAntdvNext(result);
  141 + String content = result.get(STUDENT_CONTACT_FORM_PATH);
  142 + assertNotNull(content, STUDENT_CONTACT_FORM_PATH + ":未生成一对多子表表单");
  143 + assertTrue(content.contains("<VxeTable"), STUDENT_CONTACT_FORM_PATH + ":应生成一对多子表表格");
  144 + assertFalse(content.contains("<VxeColumn field=\"studentId\""),
  145 + STUDENT_CONTACT_FORM_PATH + ":不应生成 join 列");
  146 + assertFalse(content.contains("<Select v-model:value=\"row.studentId\""),
  147 + STUDENT_CONTACT_FORM_PATH + ":不应渲染 join 列的 Select 组件");
  148 + assertFalse(content.contains("<SelectOption"), STUDENT_CONTACT_FORM_PATH + ":不应渲染 SelectOption 组件");
  149 + assertFalse(createRootImportPattern("import\\s*\\{", "\\bSelectOption\\b").matcher(content).find(),
  150 + STUDENT_CONTACT_FORM_PATH + ":不应导入 SelectOption 组件");
  151 + }
  152 +
  153 + @Test
  154 + public void testExecute_generalMasterInner() {
  155 + // 调用
  156 + Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType(),
  157 + CodegenTemplateTypeEnum.MASTER_INNER);
  158 +
  159 + // 断言
  160 + assertGeneratedCodeCompatibleWithAntdvNext(result);
  161 + assertRuleGenerated(result);
  162 + assertGeneralCrudComponentsGenerated(result);
  163 + assertTabsGenerated(result);
  164 + }
  165 +
  166 + @Test
  167 + public void testExecute_generalMasterErp() {
  168 + // 调用
  169 + Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType(),
  170 + CodegenTemplateTypeEnum.MASTER_ERP);
  171 +
  172 + // 断言
  173 + assertGeneratedCodeCompatibleWithAntdvNext(result);
  174 + assertRuleGenerated(result);
  175 + assertGeneralCrudComponentsGenerated(result);
  176 + assertTabsGenerated(result);
  177 + }
  178 +
  179 + @Test
  180 + public void testExecute_oneImportEnable() {
  181 + // 开启 import 开关
  182 + codegenProperties.setImportEnable(true);
  183 + codegenEngine.initGlobalBindingMap();
  184 +
  185 + // 调用并断言
  186 + assertImportFormCompatibleWithAntdvNext(
  187 + executeOne(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_SCHEMA.getType()));
  188 + assertImportFormCompatibleWithAntdvNext(
  189 + executeOne(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType()));
  190 + }
  191 +
  192 + @Test
  193 + public void testInnerTemplatesParseAntdvNext() {
  194 + INNER_TEMPLATE_PATHS.forEach(path -> {
  195 + String template = ResourceUtil.readUtf8Str(path);
  196 + assertFalse(template.contains("codegen/vue3_vben5_antd/"), path + ":不应引用 Ant Design Vue 模板");
  197 + assertTrue(template.contains("codegen/vue3_vben5_antdv_next/"), path + ":应引用 Antdv Next 模板");
  198 + });
  199 + }
  200 +
  201 + private Map<String, String> executeOne(Integer frontType) {
  202 + CodegenTableDO table = getTable("student")
  203 + .setFrontType(frontType)
  204 + .setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
  205 + return codegenEngine.execute(DbType.MYSQL, table, getColumnList("student"), null, null);
  206 + }
  207 +
  208 + private Map<String, String> executeTree(Integer frontType) {
  209 + CodegenTableDO table = getTable("category")
  210 + .setFrontType(frontType)
  211 + .setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
  212 + return codegenEngine.execute(DbType.MYSQL, table, getColumnList("category"), null, null);
  213 + }
  214 +
  215 + private Map<String, String> executeGeneralMasterNormalWithOneToOneJoinSelect() {
  216 + return executeGeneralMasterNormalWithJoinSelect("teacher", 200L, false);
  217 + }
  218 +
  219 + private Map<String, String> executeGeneralMasterNormalWithOneToManyJoinSelect() {
  220 + return executeGeneralMasterNormalWithJoinSelect("contact", 100L, true);
  221 + }
  222 +
  223 + private Map<String, String> executeGeneralMasterNormalWithJoinSelect(String subTableName,
  224 + Long subJoinColumnId,
  225 + boolean subJoinMany) {
  226 + CodegenTableDO table = getTable("student")
  227 + .setFrontType(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType())
  228 + .setTemplateType(CodegenTemplateTypeEnum.MASTER_NORMAL.getType());
  229 + CodegenTableDO subTable = getTable(subTableName)
  230 + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
  231 + .setFrontType(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType())
  232 + .setSubJoinColumnId(subJoinColumnId).setSubJoinMany(subJoinMany);
  233 + List<CodegenColumnDO> subColumns = getColumnList(subTableName);
  234 + // 稀疏场景:join 列是唯一的 select,避免其它 select 掩盖 join 列对导入判定的影响。
  235 + subColumns.forEach(column -> {
  236 + if (subJoinColumnId.equals(column.getId())) {
  237 + column.setHtmlType("select");
  238 + column.setDictType("");
  239 + } else if ("select".equals(column.getHtmlType())) {
  240 + column.setHtmlType("input");
  241 + }
  242 + });
  243 + return codegenEngine.execute(DbType.MYSQL, table, getColumnList("student"),
  244 + List.of(subTable), List.of(subColumns));
  245 + }
  246 +
  247 + private Map<String, String> executeMaster(Integer frontType, CodegenTemplateTypeEnum templateType) {
  248 + // 准备参数
  249 + CodegenTableDO table = getTable("student")
  250 + .setFrontType(frontType)
  251 + .setTemplateType(templateType.getType());
  252 + List<CodegenColumnDO> columns = getColumnList("student");
  253 + // 准备参数(子表)
  254 + CodegenTableDO contactTable = getTable("contact")
  255 + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
  256 + .setFrontType(frontType)
  257 + .setSubJoinColumnId(100L).setSubJoinMany(true);
  258 + List<CodegenColumnDO> contactColumns = getColumnList("contact");
  259 + // 准备参数(班主任)
  260 + CodegenTableDO teacherTable = getTable("teacher")
  261 + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
  262 + .setFrontType(frontType)
  263 + .setSubJoinColumnId(200L).setSubJoinMany(false);
  264 + List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
  265 +
  266 + // 调用
  267 + return codegenEngine.execute(DbType.MYSQL, table, columns,
  268 + Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
  269 + }
  270 +
  271 + private static void assertGeneratedCodeCompatibleWithAntdvNext(Map<String, String> result) {
  272 + assertTrue(result.values().stream().anyMatch(content -> content.contains("from 'antdv-next")),
  273 + "生成结果应使用 antdv-next");
  274 + result.forEach((filePath, content) -> {
  275 + assertFalse(content.contains("ant-design-vue"), filePath + ":不应使用 ant-design-vue");
  276 + assertFalse(content.contains("antdv-next/es/"), filePath + ":不应使用未导出的 antdv-next 深层路径");
  277 + assertFalse(content.contains("<Form.Item"), filePath + ":不应使用 Form.Item");
  278 + assertFalse(content.contains("</Form.Item>"), filePath + ":不应使用 Form.Item");
  279 + assertRootValueImportsSorted(filePath, content);
  280 + assertRootValueImportMatchesUsage(filePath, content,
  281 + "\\bDateRangePicker\\s+as\\s+RangePicker\\b", "<RangePicker\\b", "RangePicker");
  282 + assertRootValueImportMatchesUsage(filePath, content,
  283 + "\\bFormItem\\b", "<FormItem\\b", "FormItem");
  284 + assertRootValueImportMatchesUsage(filePath, content,
  285 + "\\bSelectOption\\b", "<SelectOption\\b", "SelectOption");
  286 + assertRootValueImportMatchesUsage(filePath, content,
  287 + "\\bTabPane\\b", "<TabPane\\b", "TabPane");
  288 + assertRootValueImportMatchesUsage(filePath, content,
  289 + "\\bTabs\\b", "<Tabs\\b", "Tabs");
  290 + assertRootTypeImportMatchesUsage(filePath, content,
  291 + "\\bRule\\b", "\\bRule\\b", "Rule");
  292 + assertRangePickerHelperImportMatchesUsage(filePath, content);
  293 + if (filePath.endsWith(".vue")) {
  294 + assertRangePickerHelperBindingMatchesComponent(filePath, content);
  295 + } else if (filePath.endsWith(".ts")) {
  296 + assertSchemaRangePickerHelperMatchesComponent(filePath, content);
  297 + }
  298 + });
  299 + }
  300 +
  301 + private static void assertImportFormCompatibleWithAntdvNext(Map<String, String> result) {
  302 + assertGeneratedCodeCompatibleWithAntdvNext(result);
  303 + String content = result.get(IMPORT_FORM_PATH);
  304 + assertNotNull(content, IMPORT_FORM_PATH + ":未生成导入表单");
  305 + assertTrue(content.contains("<Upload"), IMPORT_FORM_PATH + ":应生成 Upload 组件");
  306 + assertRootValueImport(IMPORT_FORM_PATH, content, "\\bUpload\\b");
  307 + assertTrue(content.contains("beforeUpload(file: File)"), IMPORT_FORM_PATH + ":上传参数应使用原生 File 类型");
  308 + assertFalse(content.contains("UploadFile"), IMPORT_FORM_PATH + ":不应使用上传列表项类型 UploadFile");
  309 + assertFalse(content.contains("as unknown as File"), IMPORT_FORM_PATH + ":不应通过双重断言转换上传文件");
  310 + assertFalse(content.contains("FileType"), IMPORT_FORM_PATH + ":不应继续使用旧 FileType");
  311 + }
  312 +
  313 + private static void assertTabsGenerated(Map<String, String> result) {
  314 + assertComponentGenerated(result, "Tabs");
  315 + assertComponentGenerated(result, "TabPane");
  316 + }
  317 +
  318 + private static void assertGeneralCrudComponentsGenerated(Map<String, String> result) {
  319 + assertComponentGenerated(result, "FormItem");
  320 + assertComponentGenerated(result, "RangePicker");
  321 + assertComponentGenerated(result, "SelectOption");
  322 + }
  323 +
  324 + private static void assertRuleGenerated(Map<String, String> result) {
  325 + assertTrue(result.values().stream().map(CodegenEngineVben5AntdvNextTest::removeAntdvNextRootImports)
  326 + .anyMatch(content -> Pattern.compile("\\bRule\\b").matcher(content).find()),
  327 + "生成结果应包含 Rule 类型校验规则");
  328 + }
  329 +
  330 + private static void assertSchemaRangePickerGenerated(Map<String, String> result) {
  331 + String content = result.get(SCHEMA_DATA_PATH);
  332 + assertNotNull(content, SCHEMA_DATA_PATH + ":未生成 Schema 配置");
  333 + long componentCount = countMatches(SCHEMA_RANGE_PICKER_COMPONENT_PATTERN, content);
  334 + assertTrue(componentCount > 0, SCHEMA_DATA_PATH + ":应生成 RangePicker 组件配置");
  335 + }
  336 +
  337 + private static void assertComponentGenerated(Map<String, String> result, String component) {
  338 + assertTrue(result.values().stream().anyMatch(content -> content.contains("<" + component)),
  339 + "生成结果应包含 " + component + " 组件");
  340 + }
  341 +
  342 + private static void assertRootValueImportMatchesUsage(String filePath, String content,
  343 + String expectedImportRegex, String usageRegex,
  344 + String bindingName) {
  345 + assertRootImportMatchesUsage(filePath, content, "import\\s*\\{", expectedImportRegex,
  346 + usageRegex, bindingName);
  347 + }
  348 +
  349 + private static void assertRootTypeImportMatchesUsage(String filePath, String content,
  350 + String expectedImportRegex, String usageRegex,
  351 + String bindingName) {
  352 + assertRootImportMatchesUsage(filePath, content, "import\\s+type\\s*\\{", expectedImportRegex,
  353 + usageRegex, bindingName);
  354 + }
  355 +
  356 + private static void assertRootImportMatchesUsage(String filePath, String content, String importPrefix,
  357 + String expectedImportRegex, String usageRegex,
  358 + String bindingName) {
  359 + boolean imported = createRootImportPattern(importPrefix, expectedImportRegex).matcher(content).find();
  360 + boolean used = Pattern.compile(usageRegex).matcher(removeAntdvNextRootImports(content)).find();
  361 + assertEquals(used, imported, filePath + ":" + bindingName + (used ? " 缺少合法根导入" : " 存在孤立根导入"));
  362 + }
  363 +
  364 + private static String removeAntdvNextRootImports(String content) {
  365 + Pattern pattern = Pattern.compile("import\\s+(?:type\\s+)?\\{[^}]*}\\s*from\\s*'antdv-next';?",
  366 + Pattern.DOTALL);
  367 + return pattern.matcher(content).replaceAll("");
  368 + }
  369 +
  370 + private static void assertRootValueImport(String filePath, String content, String expectedRegex) {
  371 + assertRootImport(filePath, content, "import\\s*\\{", expectedRegex);
  372 + }
  373 +
  374 + private static void assertRootValueImportsSorted(String filePath, String content) {
  375 + var matcher = ANTDV_NEXT_VALUE_IMPORT_PATTERN.matcher(content);
  376 + while (matcher.find()) {
  377 + List<String> actual = Arrays.stream(matcher.group(1).split(","))
  378 + .map(String::trim)
  379 + .filter(item -> !item.isEmpty())
  380 + .map(item -> item.replaceFirst("^.*\\s+as\\s+", ""))
  381 + .toList();
  382 + List<String> expected = actual.stream().sorted(String.CASE_INSENSITIVE_ORDER).toList();
  383 + assertEquals(expected, actual, filePath + ":antdv-next 命名导入未按 natural 顺序排列");
  384 + }
  385 + }
  386 +
  387 + private static void assertRangePickerHelperImportMatchesUsage(String filePath, String content) {
  388 + boolean imported = RANGE_PICKER_HELPER_IMPORT_PATTERN.matcher(content).find();
  389 + boolean called = RANGE_PICKER_HELPER_CALL_PATTERN.matcher(content).find();
  390 + assertEquals(called, imported,
  391 + filePath + ":getRangePickerDefaultProps() 调用与导入不一致");
  392 + }
  393 +
  394 + private static void assertRangePickerHelperBindingMatchesComponent(String filePath, String content) {
  395 + long componentCount = countMatches(RANGE_PICKER_COMPONENT_PATTERN, content);
  396 + long bindingCount = countMatches(RANGE_PICKER_HELPER_BINDING_PATTERN, content);
  397 + assertEquals(componentCount, bindingCount,
  398 + filePath + ":RangePicker 与 getRangePickerDefaultProps() 绑定数量不一致");
  399 + }
  400 +
  401 + private static void assertSchemaRangePickerHelperMatchesComponent(String filePath, String content) {
  402 + long componentCount = countMatches(SCHEMA_RANGE_PICKER_COMPONENT_PATTERN, content);
  403 + long helperSpreadCount = countMatches(SCHEMA_RANGE_PICKER_HELPER_SPREAD_PATTERN, content);
  404 + assertEquals(componentCount, helperSpreadCount,
  405 + filePath + ":Schema RangePicker 与 getRangePickerDefaultProps() 展开数量不一致");
  406 + }
  407 +
  408 + private static long countMatches(Pattern pattern, String content) {
  409 + return pattern.matcher(content).results().count();
  410 + }
  411 +
  412 + private static void assertRootImport(String filePath, String content, String importPrefix, String expectedRegex) {
  413 + Pattern pattern = createRootImportPattern(importPrefix, expectedRegex);
  414 + assertTrue(pattern.matcher(content).find(), filePath + ":缺少合法的 antdv-next 根导入 " + expectedRegex);
  415 + }
  416 +
  417 + private static Pattern createRootImportPattern(String importPrefix, String expectedRegex) {
  418 + return Pattern.compile(importPrefix + "[^}]*" + expectedRegex
  419 + + "[^}]*}\\s*from\\s*'antdv-next';?", Pattern.DOTALL);
  420 + }
  421 +
  422 +}
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/assert.json 0 → 100644
  1 +[ {
  2 + "contentPath" : "java/InfraStudentPageReqVO",
  3 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
  4 +}, {
  5 + "contentPath" : "java/InfraStudentRespVO",
  6 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
  7 +}, {
  8 + "contentPath" : "java/InfraStudentSaveReqVO",
  9 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
  10 +}, {
  11 + "contentPath" : "java/InfraStudentController",
  12 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
  13 +}, {
  14 + "contentPath" : "java/InfraStudentDO",
  15 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
  16 +}, {
  17 + "contentPath" : "java/InfraStudentContactDO",
  18 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java"
  19 +}, {
  20 + "contentPath" : "java/InfraStudentTeacherDO",
  21 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java"
  22 +}, {
  23 + "contentPath" : "java/InfraStudentMapper",
  24 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
  25 +}, {
  26 + "contentPath" : "java/InfraStudentContactMapper",
  27 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java"
  28 +}, {
  29 + "contentPath" : "java/InfraStudentTeacherMapper",
  30 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java"
  31 +}, {
  32 + "contentPath" : "xml/InfraStudentMapper",
  33 + "filePath" : "yudao-module-infra/src/main/resources/mapper/demo/InfraStudentMapper.xml"
  34 +}, {
  35 + "contentPath" : "java/InfraStudentServiceImpl",
  36 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
  37 +}, {
  38 + "contentPath" : "java/InfraStudentService",
  39 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
  40 +}, {
  41 + "contentPath" : "java/InfraStudentServiceImplTest",
  42 + "filePath" : "yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
  43 +}, {
  44 + "contentPath" : "java/ErrorCodeConstants_手动操作",
  45 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
  46 +}, {
  47 + "contentPath" : "sql/sql",
  48 + "filePath" : "sql/sql.sql"
  49 +}, {
  50 + "contentPath" : "sql/h2",
  51 + "filePath" : "sql/h2.sql"
  52 +}, {
  53 + "contentPath" : "ts/index",
  54 + "filePath" : "yudao-ui-admin-uniapp/src/api/infra/demo/index.ts"
  55 +}, {
  56 + "contentPath" : "vue/index",
  57 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/index.vue"
  58 +}, {
  59 + "contentPath" : "vue/search-form",
  60 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/components/search-form.vue"
  61 +}, {
  62 + "contentPath" : "vue/form/index",
  63 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue"
  64 +}, {
  65 + "contentPath" : "vue/detail/index",
  66 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue"
  67 +}, {
  68 + "contentPath" : "vue/student-contact/form/index",
  69 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/student-contact/form/index.vue"
  70 +}, {
  71 + "contentPath" : "vue/student-teacher/form/index",
  72 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/student-teacher/form/index.vue"
  73 +} ]
0 \ No newline at end of file 74 \ No newline at end of file
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/ErrorCodeConstants_手动操作 0 → 100644
  1 +// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
  2 +// ========== 学生 TODO 补充编号 ==========
  3 +ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
  4 +ErrorCode STUDENT_CONTACT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生联系人不存在");
  5 +ErrorCode STUDENT_TEACHER_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生班主任不存在");
  6 +ErrorCode STUDENT_TEACHER_EXISTS = new ErrorCode(TODO 补充编号, "学生班主任已存在");
0 \ No newline at end of file 7 \ No newline at end of file
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentContactDO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
  2 +
  3 +import lombok.*;
  4 +import java.util.*;
  5 +import java.time.LocalDateTime;
  6 +import java.time.LocalDateTime;
  7 +import com.baomidou.mybatisplus.annotation.*;
  8 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  9 +
  10 +/**
  11 + * 学生联系人 DO
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@TableName("infra_student_contact")
  16 +@KeySequence("infra_student_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  17 +@Data
  18 +@EqualsAndHashCode(callSuper = true)
  19 +@ToString(callSuper = true)
  20 +@Builder
  21 +@NoArgsConstructor
  22 +@AllArgsConstructor
  23 +public class InfraStudentContactDO extends BaseDO {
  24 +
  25 + /**
  26 + * 编号
  27 + */
  28 + @TableId
  29 + private Long id;
  30 + /**
  31 + * 学生编号
  32 + */
  33 + private Long studentId;
  34 + /**
  35 + * 名字
  36 + */
  37 + private String name;
  38 + /**
  39 + * 简介
  40 + */
  41 + private String description;
  42 + /**
  43 + * 出生日期
  44 + */
  45 + private LocalDateTime birthday;
  46 + /**
  47 + * 性别
  48 + *
  49 + * 枚举 {@link TODO system_user_sex 对应的类}
  50 + */
  51 + private Integer sex;
  52 + /**
  53 + * 是否有效
  54 + *
  55 + * 枚举 {@link TODO infra_boolean_string 对应的类}
  56 + */
  57 + private Boolean enabled;
  58 + /**
  59 + * 头像
  60 + */
  61 + private String avatar;
  62 + /**
  63 + * 附件
  64 + */
  65 + private String video;
  66 + /**
  67 + * 备注
  68 + */
  69 + private String memo;
  70 +
  71 +}
0 \ No newline at end of file 72 \ No newline at end of file
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentContactMapper 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.mysql.demo;
  2 +
  3 +import java.util.*;
  4 +
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  7 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  8 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  9 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
  10 +import org.apache.ibatis.annotations.Mapper;
  11 +
  12 +/**
  13 + * 学生联系人 Mapper
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Mapper
  18 +public interface InfraStudentContactMapper extends BaseMapperX<InfraStudentContactDO> {
  19 +
  20 + default PageResult<InfraStudentContactDO> selectPage(PageParam reqVO, Long studentId) {
  21 + return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentContactDO>()
  22 + .eq(InfraStudentContactDO::getStudentId, studentId)
  23 + .orderByDesc(InfraStudentContactDO::getId));
  24 + }
  25 +
  26 + default int deleteByStudentId(Long studentId) {
  27 + return delete(InfraStudentContactDO::getStudentId, studentId);
  28 + }
  29 +
  30 + default int deleteByStudentIds(List<Long> studentIds) {
  31 + return deleteBatch(InfraStudentContactDO::getStudentId, studentIds);
  32 + }
  33 +
  34 +}
0 \ No newline at end of file 35 \ No newline at end of file
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentController 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.controller.admin.demo;
  2 +
  3 +import org.springframework.web.bind.annotation.*;
  4 +import jakarta.annotation.Resource;
  5 +import org.springframework.validation.annotation.Validated;
  6 +import org.springframework.security.access.prepost.PreAuthorize;
  7 +import io.swagger.v3.oas.annotations.tags.Tag;
  8 +import io.swagger.v3.oas.annotations.Parameter;
  9 +import io.swagger.v3.oas.annotations.Operation;
  10 +
  11 +import jakarta.validation.constraints.*;
  12 +import jakarta.validation.*;
  13 +import jakarta.servlet.http.*;
  14 +import java.util.*;
  15 +import java.io.IOException;
  16 +
  17 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  18 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  19 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  20 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  21 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  22 +
  23 +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
  24 +
  25 +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
  26 +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
  27 +
  28 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  29 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
  30 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
  31 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
  32 +import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService;
  33 +
  34 +@Tag(name = "管理后台 - 学生")
  35 +@RestController
  36 +@RequestMapping("/infra/student")
  37 +@Validated
  38 +public class InfraStudentController {
  39 +
  40 + @Resource
  41 + private InfraStudentService studentService;
  42 +
  43 + @PostMapping("/create")
  44 + @Operation(summary = "创建学生")
  45 + @PreAuthorize("@ss.hasPermission('infra:student:create')")
  46 + public CommonResult<Long> createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) {
  47 + return success(studentService.createStudent(createReqVO));
  48 + }
  49 +
  50 + @PutMapping("/update")
  51 + @Operation(summary = "更新学生")
  52 + @PreAuthorize("@ss.hasPermission('infra:student:update')")
  53 + public CommonResult<Boolean> updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) {
  54 + studentService.updateStudent(updateReqVO);
  55 + return success(true);
  56 + }
  57 +
  58 + @DeleteMapping("/delete")
  59 + @Operation(summary = "删除学生")
  60 + @Parameter(name = "id", description = "编号", required = true)
  61 + @PreAuthorize("@ss.hasPermission('infra:student:delete')")
  62 + public CommonResult<Boolean> deleteStudent(@RequestParam("id") Long id) {
  63 + studentService.deleteStudent(id);
  64 + return success(true);
  65 + }
  66 +
  67 + @DeleteMapping("/delete-list")
  68 + @Parameter(name = "ids", description = "编号", required = true)
  69 + @Operation(summary = "批量删除学生")
  70 + @PreAuthorize("@ss.hasPermission('infra:student:delete')")
  71 + public CommonResult<Boolean> deleteStudentList(@RequestParam("ids") List<Long> ids) {
  72 + studentService.deleteStudentListByIds(ids);
  73 + return success(true);
  74 + }
  75 +
  76 + @GetMapping("/get")
  77 + @Operation(summary = "获得学生")
  78 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  79 + @PreAuthorize("@ss.hasPermission('infra:student:query')")
  80 + public CommonResult<InfraStudentRespVO> getStudent(@RequestParam("id") Long id) {
  81 + InfraStudentDO student = studentService.getStudent(id);
  82 + return success(BeanUtils.toBean(student, InfraStudentRespVO.class));
  83 + }
  84 +
  85 + @GetMapping("/page")
  86 + @Operation(summary = "获得学生分页")
  87 + @PreAuthorize("@ss.hasPermission('infra:student:query')")
  88 + public CommonResult<PageResult<InfraStudentRespVO>> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) {
  89 + PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(pageReqVO);
  90 + return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class));
  91 + }
  92 +
  93 + @GetMapping("/export-excel")
  94 + @Operation(summary = "导出学生 Excel")
  95 + @PreAuthorize("@ss.hasPermission('infra:student:export')")
  96 + @ApiAccessLog(operateType = EXPORT)
  97 + public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
  98 + HttpServletResponse response) throws IOException {
  99 + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
  100 + List<InfraStudentDO> list = studentService.getStudentPage(pageReqVO).getList();
  101 + // 导出 Excel
  102 + ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class,
  103 + BeanUtils.toBean(list, InfraStudentRespVO.class));
  104 + }
  105 +
  106 + // ==================== 子表(学生联系人) ====================
  107 +
  108 + @GetMapping("/student-contact/page")
  109 + @Operation(summary = "获得学生联系人分页")
  110 + @Parameter(name = "studentId", description = "学生编号")
  111 + @PreAuthorize("@ss.hasPermission('infra:student:query')")
  112 + public CommonResult<PageResult<InfraStudentContactDO>> getStudentContactPage(PageParam pageReqVO,
  113 + @RequestParam("studentId") Long studentId) {
  114 + return success(studentService.getStudentContactPage(pageReqVO, studentId));
  115 + }
  116 +
  117 + @PostMapping("/student-contact/create")
  118 + @Operation(summary = "创建学生联系人")
  119 + @PreAuthorize("@ss.hasPermission('infra:student:create')")
  120 + public CommonResult<Long> createStudentContact(@Valid @RequestBody InfraStudentContactDO studentContact) {
  121 + return success(studentService.createStudentContact(studentContact));
  122 + }
  123 +
  124 + @PutMapping("/student-contact/update")
  125 + @Operation(summary = "更新学生联系人")
  126 + @PreAuthorize("@ss.hasPermission('infra:student:update')")
  127 + public CommonResult<Boolean> updateStudentContact(@Valid @RequestBody InfraStudentContactDO studentContact) {
  128 + studentService.updateStudentContact(studentContact);
  129 + return success(true);
  130 + }
  131 +
  132 + @DeleteMapping("/student-contact/delete")
  133 + @Parameter(name = "id", description = "编号", required = true)
  134 + @Operation(summary = "删除学生联系人")
  135 + @PreAuthorize("@ss.hasPermission('infra:student:delete')")
  136 + public CommonResult<Boolean> deleteStudentContact(@RequestParam("id") Long id) {
  137 + studentService.deleteStudentContact(id);
  138 + return success(true);
  139 + }
  140 +
  141 + @DeleteMapping("/student-contact/delete-list")
  142 + @Parameter(name = "ids", description = "编号", required = true)
  143 + @Operation(summary = "批量删除学生联系人")
  144 + @PreAuthorize("@ss.hasPermission('infra:student:delete')")
  145 + public CommonResult<Boolean> deleteStudentContactList(@RequestParam("ids") List<Long> ids) {
  146 + studentService.deleteStudentContactListByIds(ids);
  147 + return success(true);
  148 + }
  149 +
  150 + @GetMapping("/student-contact/get")
  151 + @Operation(summary = "获得学生联系人")
  152 + @Parameter(name = "id", description = "编号", required = true)
  153 + @PreAuthorize("@ss.hasPermission('infra:student:query')")
  154 + public CommonResult<InfraStudentContactDO> getStudentContact(@RequestParam("id") Long id) {
  155 + return success(studentService.getStudentContact(id));
  156 + }
  157 +
  158 + // ==================== 子表(学生班主任) ====================
  159 +
  160 + @GetMapping("/student-teacher/page")
  161 + @Operation(summary = "获得学生班主任分页")
  162 + @Parameter(name = "studentId", description = "学生编号")
  163 + @PreAuthorize("@ss.hasPermission('infra:student:query')")
  164 + public CommonResult<PageResult<InfraStudentTeacherDO>> getStudentTeacherPage(PageParam pageReqVO,
  165 + @RequestParam("studentId") Long studentId) {
  166 + return success(studentService.getStudentTeacherPage(pageReqVO, studentId));
  167 + }
  168 +
  169 + @PostMapping("/student-teacher/create")
  170 + @Operation(summary = "创建学生班主任")
  171 + @PreAuthorize("@ss.hasPermission('infra:student:create')")
  172 + public CommonResult<Long> createStudentTeacher(@Valid @RequestBody InfraStudentTeacherDO studentTeacher) {
  173 + return success(studentService.createStudentTeacher(studentTeacher));
  174 + }
  175 +
  176 + @PutMapping("/student-teacher/update")
  177 + @Operation(summary = "更新学生班主任")
  178 + @PreAuthorize("@ss.hasPermission('infra:student:update')")
  179 + public CommonResult<Boolean> updateStudentTeacher(@Valid @RequestBody InfraStudentTeacherDO studentTeacher) {
  180 + studentService.updateStudentTeacher(studentTeacher);
  181 + return success(true);
  182 + }
  183 +
  184 + @DeleteMapping("/student-teacher/delete")
  185 + @Parameter(name = "id", description = "编号", required = true)
  186 + @Operation(summary = "删除学生班主任")
  187 + @PreAuthorize("@ss.hasPermission('infra:student:delete')")
  188 + public CommonResult<Boolean> deleteStudentTeacher(@RequestParam("id") Long id) {
  189 + studentService.deleteStudentTeacher(id);
  190 + return success(true);
  191 + }
  192 +
  193 + @DeleteMapping("/student-teacher/delete-list")
  194 + @Parameter(name = "ids", description = "编号", required = true)
  195 + @Operation(summary = "批量删除学生班主任")
  196 + @PreAuthorize("@ss.hasPermission('infra:student:delete')")
  197 + public CommonResult<Boolean> deleteStudentTeacherList(@RequestParam("ids") List<Long> ids) {
  198 + studentService.deleteStudentTeacherListByIds(ids);
  199 + return success(true);
  200 + }
  201 +
  202 + @GetMapping("/student-teacher/get")
  203 + @Operation(summary = "获得学生班主任")
  204 + @Parameter(name = "id", description = "编号", required = true)
  205 + @PreAuthorize("@ss.hasPermission('infra:student:query')")
  206 + public CommonResult<InfraStudentTeacherDO> getStudentTeacher(@RequestParam("id") Long id) {
  207 + return success(studentService.getStudentTeacher(id));
  208 + }
  209 +
  210 +}
0 \ No newline at end of file 211 \ No newline at end of file
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentDO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
  2 +
  3 +import lombok.*;
  4 +import java.util.*;
  5 +import java.time.LocalDateTime;
  6 +import java.time.LocalDateTime;
  7 +import com.baomidou.mybatisplus.annotation.*;
  8 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  9 +
  10 +/**
  11 + * 学生 DO
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@TableName("infra_student")
  16 +@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  17 +@Data
  18 +@EqualsAndHashCode(callSuper = true)
  19 +@ToString(callSuper = true)
  20 +@Builder
  21 +@NoArgsConstructor
  22 +@AllArgsConstructor
  23 +public class InfraStudentDO extends BaseDO {
  24 +
  25 + /**
  26 + * 编号
  27 + */
  28 + @TableId
  29 + private Long id;
  30 + /**
  31 + * 名字
  32 + */
  33 + private String name;
  34 + /**
  35 + * 简介
  36 + */
  37 + private String description;
  38 + /**
  39 + * 出生日期
  40 + */
  41 + private LocalDateTime birthday;
  42 + /**
  43 + * 性别
  44 + *
  45 + * 枚举 {@link TODO system_user_sex 对应的类}
  46 + */
  47 + private Integer sex;
  48 + /**
  49 + * 是否有效
  50 + *
  51 + * 枚举 {@link TODO infra_boolean_string 对应的类}
  52 + */
  53 + private Boolean enabled;
  54 + /**
  55 + * 头像
  56 + */
  57 + private String avatar;
  58 + /**
  59 + * 附件
  60 + */
  61 + private String video;
  62 + /**
  63 + * 备注
  64 + */
  65 + private String memo;
  66 +
  67 +
  68 +}
0 \ No newline at end of file 69 \ No newline at end of file
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentMapper 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.mysql.demo;
  2 +
  3 +import java.util.*;
  4 +
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  7 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  8 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
  9 +import org.apache.ibatis.annotations.Mapper;
  10 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  11 +
  12 +/**
  13 + * 学生 Mapper
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Mapper
  18 +public interface InfraStudentMapper extends BaseMapperX<InfraStudentDO> {
  19 +
  20 + default PageResult<InfraStudentDO> selectPage(InfraStudentPageReqVO reqVO) {
  21 + return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentDO>()
  22 + .likeIfPresent(InfraStudentDO::getName, reqVO.getName())
  23 + .eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday())
  24 + .eqIfPresent(InfraStudentDO::getSex, reqVO.getSex())
  25 + .eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled())
  26 + .betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime())
  27 + .orderByDesc(InfraStudentDO::getId));
  28 + }
  29 +
  30 +}
0 \ No newline at end of file 31 \ No newline at end of file
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentPageReqVO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
  2 +
  3 +import lombok.*;
  4 +import java.util.*;
  5 +import io.swagger.v3.oas.annotations.media.Schema;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  7 +import org.springframework.format.annotation.DateTimeFormat;
  8 +import java.time.LocalDateTime;
  9 +
  10 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  11 +
  12 +@Schema(description = "管理后台 - 学生分页 Request VO")
  13 +@Data
  14 +public class InfraStudentPageReqVO extends PageParam {
  15 +
  16 + @Schema(description = "名字", example = "芋头")
  17 + private String name;
  18 +
  19 + @Schema(description = "出生日期")
  20 + private LocalDateTime birthday;
  21 +
  22 + @Schema(description = "性别", example = "1")
  23 + private Integer sex;
  24 +
  25 + @Schema(description = "是否有效", example = "true")
  26 + private Boolean enabled;
  27 +
  28 + @Schema(description = "创建时间")
  29 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  30 + private LocalDateTime[] createTime;
  31 +
  32 +}
0 \ No newline at end of file 33 \ No newline at end of file
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentRespVO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.*;
  5 +import java.util.*;
  6 +import org.springframework.format.annotation.DateTimeFormat;
  7 +import java.time.LocalDateTime;
  8 +import cn.idev.excel.annotation.*;
  9 +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
  10 +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
  11 +
  12 +@Schema(description = "管理后台 - 学生 Response VO")
  13 +@Data
  14 +@ExcelIgnoreUnannotated
  15 +public class InfraStudentRespVO {
  16 +
  17 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  18 + @ExcelProperty("编号")
  19 + private Long id;
  20 +
  21 + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
  22 + @ExcelProperty("名字")
  23 + private String name;
  24 +
  25 + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
  26 + @ExcelProperty("简介")
  27 + private String description;
  28 +
  29 + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
  30 + @ExcelProperty("出生日期")
  31 + private LocalDateTime birthday;
  32 +
  33 + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  34 + @ExcelProperty(value = "性别", converter = DictConvert.class)
  35 + @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
  36 + private Integer sex;
  37 +
  38 + @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  39 + @ExcelProperty(value = "是否有效", converter = DictConvert.class)
  40 + @DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
  41 + private Boolean enabled;
  42 +
  43 + @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
  44 + @ExcelProperty("头像")
  45 + private String avatar;
  46 +
  47 + @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
  48 + @ExcelProperty("附件")
  49 + private String video;
  50 +
  51 + @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
  52 + @ExcelProperty("备注")
  53 + private String memo;
  54 +
  55 + @Schema(description = "创建时间")
  56 + @ExcelProperty("创建时间")
  57 + private LocalDateTime createTime;
  58 +
  59 +}
0 \ No newline at end of file 60 \ No newline at end of file
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentSaveReqVO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.*;
  5 +import java.util.*;
  6 +import jakarta.validation.constraints.*;
  7 +import org.springframework.format.annotation.DateTimeFormat;
  8 +import java.time.LocalDateTime;
  9 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
  10 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
  11 +
  12 +@Schema(description = "管理后台 - 学生新增/修改 Request VO")
  13 +@Data
  14 +public class InfraStudentSaveReqVO {
  15 +
  16 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  17 + private Long id;
  18 +
  19 + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
  20 + @NotEmpty(message = "名字不能为空")
  21 + private String name;
  22 +
  23 + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
  24 + @NotEmpty(message = "简介不能为空")
  25 + private String description;
  26 +
  27 + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
  28 + @NotNull(message = "出生日期不能为空")
  29 + private LocalDateTime birthday;
  30 +
  31 + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  32 + @NotNull(message = "性别不能为空")
  33 + private Integer sex;
  34 +
  35 + @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  36 + @NotNull(message = "是否有效不能为空")
  37 + private Boolean enabled;
  38 +
  39 + @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
  40 + @NotEmpty(message = "头像不能为空")
  41 + private String avatar;
  42 +
  43 + @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
  44 + @NotEmpty(message = "附件不能为空")
  45 + private String video;
  46 +
  47 + @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
  48 + @NotEmpty(message = "备注不能为空")
  49 + private String memo;
  50 +
  51 +}
0 \ No newline at end of file 52 \ No newline at end of file
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentService 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.service.demo;
  2 +
  3 +import java.util.*;
  4 +import jakarta.validation.*;
  5 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  6 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
  7 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
  8 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
  9 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  10 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  11 +
  12 +/**
  13 + * 学生 Service 接口
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +public interface InfraStudentService {
  18 +
  19 + /**
  20 + * 创建学生
  21 + *
  22 + * @param createReqVO 创建信息
  23 + * @return 编号
  24 + */
  25 + Long createStudent(@Valid InfraStudentSaveReqVO createReqVO);
  26 +
  27 + /**
  28 + * 更新学生
  29 + *
  30 + * @param updateReqVO 更新信息
  31 + */
  32 + void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO);
  33 +
  34 + /**
  35 + * 删除学生
  36 + *
  37 + * @param id 编号
  38 + */
  39 + void deleteStudent(Long id);
  40 +
  41 + /**
  42 + * 批量删除学生
  43 + *
  44 + * @param ids 编号
  45 + */
  46 + void deleteStudentListByIds(List<Long> ids);
  47 +
  48 + /**
  49 + * 获得学生
  50 + *
  51 + * @param id 编号
  52 + * @return 学生
  53 + */
  54 + InfraStudentDO getStudent(Long id);
  55 +
  56 + /**
  57 + * 获得学生分页
  58 + *
  59 + * @param pageReqVO 分页查询
  60 + * @return 学生分页
  61 + */
  62 + PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO);
  63 +
  64 + // ==================== 子表(学生联系人) ====================
  65 +
  66 + /**
  67 + * 获得学生联系人分页
  68 + *
  69 + * @param pageReqVO 分页查询
  70 + * @param studentId 学生编号
  71 + * @return 学生联系人分页
  72 + */
  73 + PageResult<InfraStudentContactDO> getStudentContactPage(PageParam pageReqVO, Long studentId);
  74 +
  75 + /**
  76 + * 创建学生联系人
  77 + *
  78 + * @param studentContact 创建信息
  79 + * @return 编号
  80 + */
  81 + Long createStudentContact(@Valid InfraStudentContactDO studentContact);
  82 +
  83 + /**
  84 + * 更新学生联系人
  85 + *
  86 + * @param studentContact 更新信息
  87 + */
  88 + void updateStudentContact(@Valid InfraStudentContactDO studentContact);
  89 +
  90 + /**
  91 + * 删除学生联系人
  92 + *
  93 + * @param id 编号
  94 + */
  95 + void deleteStudentContact(Long id);
  96 +
  97 + /**
  98 + * 批量删除学生联系人
  99 + *
  100 + * @param ids 编号
  101 + */
  102 + void deleteStudentContactListByIds(List<Long> ids);
  103 +
  104 + /**
  105 + * 获得学生联系人
  106 + *
  107 + * @param id 编号
  108 + * @return 学生联系人
  109 + */
  110 + InfraStudentContactDO getStudentContact(Long id);
  111 +
  112 + // ==================== 子表(学生班主任) ====================
  113 +
  114 + /**
  115 + * 获得学生班主任分页
  116 + *
  117 + * @param pageReqVO 分页查询
  118 + * @param studentId 学生编号
  119 + * @return 学生班主任分页
  120 + */
  121 + PageResult<InfraStudentTeacherDO> getStudentTeacherPage(PageParam pageReqVO, Long studentId);
  122 +
  123 + /**
  124 + * 创建学生班主任
  125 + *
  126 + * @param studentTeacher 创建信息
  127 + * @return 编号
  128 + */
  129 + Long createStudentTeacher(@Valid InfraStudentTeacherDO studentTeacher);
  130 +
  131 + /**
  132 + * 更新学生班主任
  133 + *
  134 + * @param studentTeacher 更新信息
  135 + */
  136 + void updateStudentTeacher(@Valid InfraStudentTeacherDO studentTeacher);
  137 +
  138 + /**
  139 + * 删除学生班主任
  140 + *
  141 + * @param id 编号
  142 + */
  143 + void deleteStudentTeacher(Long id);
  144 +
  145 + /**
  146 + * 批量删除学生班主任
  147 + *
  148 + * @param ids 编号
  149 + */
  150 + void deleteStudentTeacherListByIds(List<Long> ids);
  151 +
  152 + /**
  153 + * 获得学生班主任
  154 + *
  155 + * @param id 编号
  156 + * @return 学生班主任
  157 + */
  158 + InfraStudentTeacherDO getStudentTeacher(Long id);
  159 +
  160 +}
0 \ No newline at end of file 161 \ No newline at end of file