Commit 521a24b382d95dc9f80b9be1ffe6abd96d8017d8

Authored by Leslie
Committed by GitHub
2 parents 8402441a 1a2639e4

Merge branch 'YunaiV:master-jdk17' into master-jdk17

Showing 111 changed files with 3021 additions and 1278 deletions
sql/tools/convertor.py
@@ -6,12 +6,12 @@ Author: dhb52 (https://gitee.com/dhb52) @@ -6,12 +6,12 @@ Author: dhb52 (https://gitee.com/dhb52)
6 pip install simple-ddl-parser 6 pip install simple-ddl-parser
7 7
8 or with uv 8 or with uv
9 -uv run --with simple-ddl-parser convertor.py postgres > ../postgresql/ruoyi-vue-pro.sql 239ms  四 5/22 21:03:16 2025  
10 -uv run --with simple-ddl-parser convertor.py sqlserver > ../sqlserver/ruoyi-vue-pro.sql  
11 -uv run --with simple-ddl-parser convertor.py kingbase > ../kingbase/ruoyi-vue-pro.sql  
12 -uv run --with simple-ddl-parser convertor.py opengauss > ../opengauss/ruoyi-vue-pro.sql  
13 -uv run --with simple-ddl-parser convertor.py oracle > ../oracle/ruoyi-vue-pro.sql  
14 -uv run --with simple-ddl-parser convertor.py dm8 > ../dm/ruoyi-vue-pro-dm8.sql 9 +uv run --with simple-ddl-parser convertor.py postgres ../mysql/ruoyi-vue-pro.sql > ../postgresql/ruoyi-vue-pro.sql
  10 +uv run --with simple-ddl-parser convertor.py sqlserver ../mysql/ruoyi-vue-pro.sql > ../sqlserver/ruoyi-vue-pro.sql
  11 +uv run --with simple-ddl-parser convertor.py kingbase ../mysql/ruoyi-vue-pro.sql > ../kingbase/ruoyi-vue-pro.sql
  12 +uv run --with simple-ddl-parser convertor.py opengauss ../mysql/ruoyi-vue-pro.sql > ../opengauss/ruoyi-vue-pro.sql
  13 +uv run --with simple-ddl-parser convertor.py oracle ../mysql/ruoyi-vue-pro.sql > ../oracle/ruoyi-vue-pro.sql
  14 +uv run --with simple-ddl-parser convertor.py dm8 ../mysql/ruoyi-vue-pro.sql > ../dm/ruoyi-vue-pro-dm8.sql
15 """ 15 """
16 16
17 import argparse 17 import argparse
@@ -24,6 +24,9 @@ from typing import Dict, Generator, Optional, Tuple, Union @@ -24,6 +24,9 @@ from typing import Dict, Generator, Optional, Tuple, Union
24 24
25 from simple_ddl_parser import DDLParser 25 from simple_ddl_parser import DDLParser
26 26
  27 +# 避免 Windows 系统使用默认的 GBK 编码
  28 +sys.stdout = open(sys.stdout.fileno(), mode='w', encoding='utf-8', buffering=1)
  29 +
27 PREAMBLE = """/* 30 PREAMBLE = """/*
28 Yudao Database Transfer Tool 31 Yudao Database Transfer Tool
29 32
@@ -919,9 +922,15 @@ def main(): @@ -919,9 +922,15 @@ def main():
919 help="目标数据库类型", 922 help="目标数据库类型",
920 choices=["postgres", "oracle", "sqlserver", "dm8", "kingbase", "opengauss"], 923 choices=["postgres", "oracle", "sqlserver", "dm8", "kingbase", "opengauss"],
921 ) 924 )
  925 + parser.add_argument(
  926 + "path",
  927 + type=str,
  928 + help="源数据库脚本路径",
  929 + default="../mysql/ruoyi-vue-pro.sql"
  930 + )
922 args = parser.parse_args() 931 args = parser.parse_args()
923 932
924 - sql_file = pathlib.Path("../mysql/ruoyi-vue-pro.sql").resolve().as_posix() 933 + sql_file = pathlib.Path(args.path).resolve().as_posix()
925 convertor = None 934 convertor = None
926 if args.type == "postgres": 935 if args.type == "postgres":
927 convertor = PostgreSQLConvertor(sql_file) 936 convertor = PostgreSQLConvertor(sql_file)
yudao-dependencies/pom.xml
@@ -24,7 +24,7 @@ @@ -24,7 +24,7 @@
24 <!-- DB 相关 --> 24 <!-- DB 相关 -->
25 <druid.version>1.2.27</druid.version> 25 <druid.version>1.2.27</druid.version>
26 <mybatis.version>3.5.19</mybatis.version> 26 <mybatis.version>3.5.19</mybatis.version>
27 - <mybatis-plus.version>3.5.12</mybatis-plus.version> 27 + <mybatis-plus.version>3.5.14</mybatis-plus.version>
28 <mybatis-plus-join.version>1.5.4</mybatis-plus-join.version> 28 <mybatis-plus-join.version>1.5.4</mybatis-plus-join.version>
29 <dynamic-datasource.version>4.3.1</dynamic-datasource.version> 29 <dynamic-datasource.version>4.3.1</dynamic-datasource.version>
30 <easy-trans.version>3.0.6</easy-trans.version> 30 <easy-trans.version>3.0.6</easy-trans.version>
@@ -61,7 +61,7 @@ @@ -61,7 +61,7 @@
61 <transmittable-thread-local.version>2.14.5</transmittable-thread-local.version> 61 <transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
62 <commons-net.version>3.11.1</commons-net.version> 62 <commons-net.version>3.11.1</commons-net.version>
63 <commons-lang3.version>3.18.0</commons-lang3.version> 63 <commons-lang3.version>3.18.0</commons-lang3.version>
64 - <jsch.version>0.1.55</jsch.version> 64 + <jsch.version>2.27.3</jsch.version>
65 <tika-core.version>3.2.2</tika-core.version> 65 <tika-core.version>3.2.2</tika-core.version>
66 <ip2region.version>2.7.0</ip2region.version> 66 <ip2region.version>2.7.0</ip2region.version>
67 <bizlog-sdk.version>3.0.6</bizlog-sdk.version> 67 <bizlog-sdk.version>3.0.6</bizlog-sdk.version>
@@ -526,7 +526,7 @@ @@ -526,7 +526,7 @@
526 <version>${commons-net.version}</version> 526 <version>${commons-net.version}</version>
527 </dependency> 527 </dependency>
528 <dependency> 528 <dependency>
529 - <groupId>com.jcraft</groupId> 529 + <groupId>com.github.mwiede</groupId>
530 <artifactId>jsch</artifactId> <!-- 解决 sftp 连接 --> 530 <artifactId>jsch</artifactId> <!-- 解决 sftp 连接 -->
531 <version>${jsch.version}</version> 531 <version>${jsch.version}</version>
532 </dependency> 532 </dependency>
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java
@@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils; @@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
6 import cn.iocoder.yudao.framework.mybatis.core.handler.DefaultDBFieldHandler; 6 import cn.iocoder.yudao.framework.mybatis.core.handler.DefaultDBFieldHandler;
7 import com.baomidou.mybatisplus.annotation.DbType; 7 import com.baomidou.mybatisplus.annotation.DbType;
8 import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; 8 import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
  9 +import com.baomidou.mybatisplus.core.handlers.IJsonTypeHandler;
9 import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; 10 import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
10 import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator; 11 import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator;
11 import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; 12 import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
@@ -80,8 +81,8 @@ public class YudaoMybatisAutoConfiguration { @@ -80,8 +81,8 @@ public class YudaoMybatisAutoConfiguration {
80 throw new IllegalArgumentException(StrUtil.format("DbType{} 找不到合适的 IKeyGenerator 实现类", dbType)); 81 throw new IllegalArgumentException(StrUtil.format("DbType{} 找不到合适的 IKeyGenerator 实现类", dbType));
81 } 82 }
82 83
83 - @Bean  
84 - public JacksonTypeHandler jacksonTypeHandler(List<ObjectMapper> objectMappers) { 84 + @Bean // 特殊:返回结果使用 Object 而不用 JacksonTypeHandler 的原因,避免因为 JacksonTypeHandler 被 mybatis 全局使用!
  85 + public Object jacksonTypeHandler(List<ObjectMapper> objectMappers) {
85 // 特殊:设置 JacksonTypeHandler 的 ObjectMapper! 86 // 特殊:设置 JacksonTypeHandler 的 ObjectMapper!
86 ObjectMapper objectMapper = CollUtil.getFirst(objectMappers); 87 ObjectMapper objectMapper = CollUtil.getFirst(objectMappers);
87 if (objectMapper == null) { 88 if (objectMapper == null) {
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/enums/DbTypeEnum.java
@@ -60,6 +60,12 @@ public enum DbTypeEnum { @@ -60,6 +60,12 @@ public enum DbTypeEnum {
60 * 人大金仓 60 * 人大金仓
61 */ 61 */
62 KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION('#{value}' IN #{column}) <> 0"), 62 KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION('#{value}' IN #{column}) <> 0"),
  63 +
  64 + /**
  65 + * OceanBase
  66 + */
  67 + OCEAN_BASE(DbType.OCEAN_BASE, "OceanBase", "FIND_IN_SET('#{value}', #{column}) <> 0")
  68 +
63 ; 69 ;
64 70
65 public static final Map<String, DbTypeEnum> MAP_BY_NAME = Arrays.stream(values()) 71 public static final Map<String, DbTypeEnum> MAP_BY_NAME = Arrays.stream(values())
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/config/YudaoSwaggerAutoConfiguration.java
1 package cn.iocoder.yudao.framework.swagger.config; 1 package cn.iocoder.yudao.framework.swagger.config;
2 2
  3 +import com.github.xiaoymin.knife4j.spring.configuration.Knife4jAutoConfiguration;
3 import io.swagger.v3.oas.models.Components; 4 import io.swagger.v3.oas.models.Components;
4 import io.swagger.v3.oas.models.OpenAPI; 5 import io.swagger.v3.oas.models.OpenAPI;
5 import io.swagger.v3.oas.models.info.Contact; 6 import io.swagger.v3.oas.models.info.Contact;
@@ -11,6 +12,7 @@ import io.swagger.v3.oas.models.parameters.Parameter; @@ -11,6 +12,7 @@ import io.swagger.v3.oas.models.parameters.Parameter;
11 import io.swagger.v3.oas.models.security.SecurityRequirement; 12 import io.swagger.v3.oas.models.security.SecurityRequirement;
12 import io.swagger.v3.oas.models.security.SecurityScheme; 13 import io.swagger.v3.oas.models.security.SecurityScheme;
13 import org.springdoc.core.customizers.OpenApiBuilderCustomizer; 14 import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
  15 +import org.springdoc.core.customizers.OperationCustomizer;
14 import org.springdoc.core.customizers.ServerBaseUrlCustomizer; 16 import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
15 import org.springdoc.core.models.GroupedOpenApi; 17 import org.springdoc.core.models.GroupedOpenApi;
16 import org.springdoc.core.properties.SpringDocConfigProperties; 18 import org.springdoc.core.properties.SpringDocConfigProperties;
@@ -43,7 +45,7 @@ import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_ @@ -43,7 +45,7 @@ import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_
43 * 45 *
44 * @author 芋道源码 46 * @author 芋道源码
45 */ 47 */
46 -@AutoConfiguration 48 +@AutoConfiguration(before = Knife4jAutoConfiguration.class) // before 原因,保证覆写的 Knife4jOpenApiCustomizer 先生效!相关 https://github.com/YunaiV/ruoyi-vue-pro/issues/954 讨论
47 @ConditionalOnClass({OpenAPI.class}) 49 @ConditionalOnClass({OpenAPI.class})
48 @EnableConfigurationProperties(SwaggerProperties.class) 50 @EnableConfigurationProperties(SwaggerProperties.class)
49 @ConditionalOnProperty(prefix = "springdoc.api-docs", name = "enabled", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用 51 @ConditionalOnProperty(prefix = "springdoc.api-docs", name = "enabled", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用
@@ -127,6 +129,7 @@ public class YudaoSwaggerAutoConfiguration { @@ -127,6 +129,7 @@ public class YudaoSwaggerAutoConfiguration {
127 .addOperationCustomizer((operation, handlerMethod) -> operation 129 .addOperationCustomizer((operation, handlerMethod) -> operation
128 .addParametersItem(buildTenantHeaderParameter()) 130 .addParametersItem(buildTenantHeaderParameter())
129 .addParametersItem(buildSecurityHeaderParameter())) 131 .addParametersItem(buildSecurityHeaderParameter()))
  132 + .addOperationCustomizer(buildOperationIdCustomizer())
130 .build(); 133 .build();
131 } 134 }
132 135
@@ -158,5 +161,26 @@ public class YudaoSwaggerAutoConfiguration { @@ -158,5 +161,26 @@ public class YudaoSwaggerAutoConfiguration {
158 .schema(new StringSchema()._default("Bearer test1").name(HEADER_TENANT_ID).description("认证 Token")); // 默认:使用用户编号为 1 161 .schema(new StringSchema()._default("Bearer test1").name(HEADER_TENANT_ID).description("认证 Token")); // 默认:使用用户编号为 1
159 } 162 }
160 163
  164 + /**
  165 + * 核心:自定义OperationId生成规则,组合「类名前缀 + 方法名」
  166 + *
  167 + * @see <a href="https://github.com/YunaiV/ruoyi-vue-pro/issues/957">app-api 前缀不生效,都是使用 admin-api</a>
  168 + */
  169 + private static OperationCustomizer buildOperationIdCustomizer() {
  170 + return (operation, handlerMethod) -> {
  171 + // 1. 获取控制器类名(如 UserController)
  172 + String className = handlerMethod.getBeanType().getSimpleName();
  173 + // 2. 提取类名前缀(去除 Controller 后缀,如 UserController -> User)
  174 + String classPrefix = className.replaceAll("Controller$", "");
  175 + // 3. 获取方法名(如 list)
  176 + String methodName = handlerMethod.getMethod().getName();
  177 + // 4. 组合生成 operationId(如 User_list)
  178 + String operationId = classPrefix + "_" + methodName;
  179 + // 5. 设置自定义 operationId
  180 + operation.setOperationId(operationId);
  181 + return operation;
  182 + };
  183 + }
  184 +
161 } 185 }
162 186
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java
1 package cn.iocoder.yudao.framework.web.config; 1 package cn.iocoder.yudao.framework.web.config;
2 2
  3 +import cn.hutool.core.util.StrUtil;
3 import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiErrorLogCommonApi; 4 import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiErrorLogCommonApi;
4 import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; 5 import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
5 import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyFilter; 6 import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyFilter;
@@ -7,6 +8,7 @@ import cn.iocoder.yudao.framework.web.core.filter.DemoFilter; @@ -7,6 +8,7 @@ import cn.iocoder.yudao.framework.web.core.filter.DemoFilter;
7 import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; 8 import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
8 import cn.iocoder.yudao.framework.web.core.handler.GlobalResponseBodyHandler; 9 import cn.iocoder.yudao.framework.web.core.handler.GlobalResponseBodyHandler;
9 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; 10 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
  11 +import com.google.common.collect.Maps;
10 import jakarta.annotation.Resource; 12 import jakarta.annotation.Resource;
11 import jakarta.servlet.Filter; 13 import jakarta.servlet.Filter;
12 import org.springframework.beans.factory.annotation.Value; 14 import org.springframework.beans.factory.annotation.Value;
@@ -14,6 +16,7 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -14,6 +16,7 @@ import org.springframework.boot.autoconfigure.AutoConfiguration;
14 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 16 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
15 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 17 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
16 import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; 18 import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
  19 +import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
17 import org.springframework.boot.context.properties.EnableConfigurationProperties; 20 import org.springframework.boot.context.properties.EnableConfigurationProperties;
18 import org.springframework.boot.web.client.RestTemplateBuilder; 21 import org.springframework.boot.web.client.RestTemplateBuilder;
19 import org.springframework.boot.web.servlet.FilterRegistrationBean; 22 import org.springframework.boot.web.servlet.FilterRegistrationBean;
@@ -27,35 +30,57 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @@ -27,35 +30,57 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
27 import org.springframework.web.filter.CorsFilter; 30 import org.springframework.web.filter.CorsFilter;
28 import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; 31 import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
29 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 32 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  33 +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
  34 +
  35 +import java.util.Map;
  36 +import java.util.function.Predicate;
30 37
31 @AutoConfiguration 38 @AutoConfiguration
32 @EnableConfigurationProperties(WebProperties.class) 39 @EnableConfigurationProperties(WebProperties.class)
33 -public class YudaoWebAutoConfiguration implements WebMvcConfigurer { 40 +public class YudaoWebAutoConfiguration {
34 41
35 - @Resource  
36 - private WebProperties webProperties;  
37 /** 42 /**
38 * 应用名 43 * 应用名
39 */ 44 */
40 @Value("${spring.application.name}") 45 @Value("${spring.application.name}")
41 private String applicationName; 46 private String applicationName;
42 47
43 - @Override  
44 - public void configurePathMatch(PathMatchConfigurer configurer) {  
45 - configurePathMatch(configurer, webProperties.getAdminApi());  
46 - configurePathMatch(configurer, webProperties.getAppApi());  
47 - } 48 + @Bean
  49 + public WebMvcRegistrations webMvcRegistrations(WebProperties webProperties) {
  50 + return new WebMvcRegistrations() {
48 51
49 - /**  
50 - * 设置 API 前缀,仅仅匹配 controller 包下的  
51 - *  
52 - * @param configurer 配置  
53 - * @param api API 配置  
54 - */  
55 - private void configurePathMatch(PathMatchConfigurer configurer, WebProperties.Api api) {  
56 - AntPathMatcher antPathMatcher = new AntPathMatcher(".");  
57 - configurer.addPathPrefix(api.getPrefix(), clazz -> clazz.isAnnotationPresent(RestController.class)  
58 - && antPathMatcher.match(api.getController(), clazz.getPackage().getName())); // 仅仅匹配 controller 包 52 + @Override
  53 + public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
  54 + var mapping = new RequestMappingHandlerMapping();
  55 + // 实例化时就带上前缀
  56 + mapping.setPathPrefixes(buildPathPrefixes(webProperties));
  57 + return mapping;
  58 + }
  59 +
  60 + /**
  61 + * 构建 prefix → 匹配条件的映射
  62 + */
  63 + private Map<String, Predicate<Class<?>>> buildPathPrefixes(WebProperties webProperties) {
  64 + AntPathMatcher antPathMatcher = new AntPathMatcher(".");
  65 + Map<String, Predicate<Class<?>>> pathPrefixes = Maps.newLinkedHashMapWithExpectedSize(2);
  66 + putPathPrefix(pathPrefixes, webProperties.getAdminApi(), antPathMatcher);
  67 + putPathPrefix(pathPrefixes, webProperties.getAppApi(), antPathMatcher);
  68 + return pathPrefixes;
  69 + }
  70 +
  71 + /**
  72 + * 设置 API 前缀,仅仅匹配 controller 包下的
  73 + */
  74 + private void putPathPrefix(Map<String, Predicate<Class<?>>> pathPrefixes, WebProperties.Api api, AntPathMatcher matcher) {
  75 + if (api == null || StrUtil.isEmpty(api.getPrefix())) {
  76 + return;
  77 + }
  78 + pathPrefixes.put(api.getPrefix(), // api 前缀
  79 + clazz -> clazz.isAnnotationPresent(RestController.class)
  80 + && matcher.match(api.getController(), clazz.getPackage().getName()));
  81 + }
  82 +
  83 + };
59 } 84 }
60 85
61 @Bean 86 @Bean
yudao-module-ai/pom.xml
@@ -78,6 +78,12 @@ @@ -78,6 +78,12 @@
78 <groupId>org.springframework.ai</groupId> 78 <groupId>org.springframework.ai</groupId>
79 <artifactId>spring-ai-starter-model-openai</artifactId> 79 <artifactId>spring-ai-starter-model-openai</artifactId>
80 <version>${spring-ai.version}</version> 80 <version>${spring-ai.version}</version>
  81 + <exclusions>
  82 + <exclusion>
  83 + <groupId>io.swagger.core.v3</groupId>
  84 + <artifactId>swagger-annotations-jakarta</artifactId>
  85 + </exclusion>
  86 + </exclusions>
81 </dependency> 87 </dependency>
82 <dependency> 88 <dependency>
83 <groupId>org.springframework.ai</groupId> 89 <groupId>org.springframework.ai</groupId>
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java
@@ -100,6 +100,10 @@ public class BpmModelMetaInfoVO { @@ -100,6 +100,10 @@ public class BpmModelMetaInfoVO {
100 @Schema(description = "任务后置通知设置", example = "{}") 100 @Schema(description = "任务后置通知设置", example = "{}")
101 private HttpRequestSetting taskAfterTriggerSetting; 101 private HttpRequestSetting taskAfterTriggerSetting;
102 102
  103 + @Schema(description = "自定义打印模板设置", example = "{}")
  104 + @Valid
  105 + private PrintTemplateSetting printTemplateSetting;
  106 +
103 @Schema(description = "流程 ID 规则") 107 @Schema(description = "流程 ID 规则")
104 @Data 108 @Data
105 @Valid 109 @Valid
@@ -180,4 +184,17 @@ public class BpmModelMetaInfoVO { @@ -180,4 +184,17 @@ public class BpmModelMetaInfoVO {
180 184
181 } 185 }
182 186
  187 + @Schema(description = "自定义打印模板设置")
  188 + @Data
  189 + public static class PrintTemplateSetting {
  190 +
  191 + @Schema(description = "是否自定义打印模板", example = "false")
  192 + @NotNull(message = "是否自定义打印模板不能为空")
  193 + private Boolean enable;
  194 +
  195 + @Schema(description = "打印模板", example = "<p></p>")
  196 + private String template;
  197 +
  198 + }
  199 +
183 } 200 }
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java
@@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; @@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
6 import cn.iocoder.yudao.framework.common.pojo.PageResult; 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
7 import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 7 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
8 import cn.iocoder.yudao.framework.common.util.number.NumberUtils; 8 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
  9 +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
9 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*; 10 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
10 import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert; 11 import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert;
11 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; 12 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
@@ -26,6 +27,7 @@ import jakarta.validation.Valid; @@ -26,6 +27,7 @@ import jakarta.validation.Valid;
26 import org.flowable.engine.history.HistoricProcessInstance; 27 import org.flowable.engine.history.HistoricProcessInstance;
27 import org.flowable.engine.repository.ProcessDefinition; 28 import org.flowable.engine.repository.ProcessDefinition;
28 import org.flowable.task.api.Task; 29 import org.flowable.task.api.Task;
  30 +import org.flowable.task.api.history.HistoricTaskInstance;
29 import org.springframework.security.access.prepost.PreAuthorize; 31 import org.springframework.security.access.prepost.PreAuthorize;
30 import org.springframework.validation.annotation.Validated; 32 import org.springframework.validation.annotation.Validated;
31 import org.springframework.web.bind.annotation.*; 33 import org.springframework.web.bind.annotation.*;
@@ -34,9 +36,11 @@ import java.util.List; @@ -34,9 +36,11 @@ import java.util.List;
34 import java.util.Map; 36 import java.util.Map;
35 import java.util.Set; 37 import java.util.Set;
36 38
  39 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
37 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; 40 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
38 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; 41 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
39 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; 42 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
  43 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS;
40 44
41 @Tag(name = "管理后台 - 流程实例") // 流程实例,通过流程定义创建的一次“申请” 45 @Tag(name = "管理后台 - 流程实例") // 流程实例,通过流程定义创建的一次“申请”
42 @RestController 46 @RestController
@@ -192,8 +196,30 @@ public class BpmProcessInstanceController { @@ -192,8 +196,30 @@ public class BpmProcessInstanceController {
192 @GetMapping("/get-bpmn-model-view") 196 @GetMapping("/get-bpmn-model-view")
193 @Operation(summary = "获取流程实例的 BPMN 模型视图", description = "在【流程详细】界面中,进行调用") 197 @Operation(summary = "获取流程实例的 BPMN 模型视图", description = "在【流程详细】界面中,进行调用")
194 @Parameter(name = "id", description = "流程实例的编号", required = true) 198 @Parameter(name = "id", description = "流程实例的编号", required = true)
195 - public CommonResult<BpmProcessInstanceBpmnModelViewRespVO> getProcessInstanceBpmnModelView(@RequestParam(value = "id") String id) { 199 + public CommonResult<BpmProcessInstanceBpmnModelViewRespVO> getProcessInstanceBpmnModelView(
  200 + @RequestParam(value = "id") String id) {
196 return success(processInstanceService.getProcessInstanceBpmnModelView(id)); 201 return success(processInstanceService.getProcessInstanceBpmnModelView(id));
197 } 202 }
198 203
  204 + @GetMapping("/get-print-data")
  205 + @Operation(summary = "获得流程实例的打印数据")
  206 + @Parameter(name = "id", description = "流程实例的编号", required = true)
  207 + @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
  208 + public CommonResult<BpmProcessPrintDataRespVO> getProcessInstancePrintData(
  209 + @RequestParam("processInstanceId") String processInstanceId) {
  210 + HistoricProcessInstance historicProcessInstance = processInstanceService.getHistoricProcessInstance(processInstanceId);
  211 + if (historicProcessInstance == null) {
  212 + throw exception(PROCESS_INSTANCE_NOT_EXISTS);
  213 + }
  214 + AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(historicProcessInstance.getStartUserId()));
  215 + DeptRespDTO dept = deptApi.getDept(startUser.getDeptId());
  216 + List<HistoricTaskInstance> tasks = taskService.getFinishedTaskListByProcessInstanceIdWithoutCancel(processInstanceId);
  217 + Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
  218 + convertSet(tasks, item -> Long.valueOf(item.getAssignee())));
  219 + return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstancePrintData(historicProcessInstance,
  220 + processDefinitionService.getProcessDefinitionInfo(historicProcessInstance.getProcessDefinitionId()),
  221 + tasks, userMap,
  222 + new UserSimpleBaseVO().setNickname(startUser.getNickname()).setDeptName(dept.getName())));
  223 + }
  224 +
199 } 225 }
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessPrintDataRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.util.List;
  7 +
  8 +@Schema(description = "管理后台 - 流程实例的打印数据 Response VO")
  9 +@Data
  10 +public class BpmProcessPrintDataRespVO {
  11 +
  12 + @Schema(description = "流程实例数据")
  13 + private BpmProcessInstanceRespVO processInstance;
  14 +
  15 + @Schema(description = "是否开启自定义打印模板", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  16 + private Boolean printTemplateEnable;
  17 +
  18 + @Schema(description = "自定义打印模板 HTML")
  19 + private String printTemplateHtml;
  20 +
  21 + @Schema(description = "审批任务列表")
  22 + private List<Task> tasks;
  23 +
  24 + @Schema(description = "流程任务")
  25 + @Data
  26 + public static class Task {
  27 +
  28 + @Schema(description = "流程任务的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  29 + private String id;
  30 +
  31 + @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
  32 + private String name;
  33 +
  34 + @Schema(description = "签名 URL", example = "https://www.iocoder.cn/sign.png")
  35 + private String signPicUrl;
  36 +
  37 + @Schema(description = "任务描述", requiredMode = Schema.RequiredMode.REQUIRED)
  38 + private String description; // 该字段由后端拼接
  39 +
  40 + }
  41 +
  42 +}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskPageReqVO.java
@@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task; @@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task;
2 2
3 import cn.iocoder.yudao.framework.common.pojo.PageParam; 3 import cn.iocoder.yudao.framework.common.pojo.PageParam;
4 import cn.iocoder.yudao.framework.common.util.date.DateUtils; 4 import cn.iocoder.yudao.framework.common.util.date.DateUtils;
  5 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  6 +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
5 import io.swagger.v3.oas.annotations.media.Schema; 7 import io.swagger.v3.oas.annotations.media.Schema;
6 import lombok.Data; 8 import lombok.Data;
7 import org.springframework.format.annotation.DateTimeFormat; 9 import org.springframework.format.annotation.DateTimeFormat;
@@ -21,6 +23,10 @@ public class BpmTaskPageReqVO extends PageParam { @@ -21,6 +23,10 @@ public class BpmTaskPageReqVO extends PageParam {
21 @Schema(description = "流程定义的标识", example = "2048") 23 @Schema(description = "流程定义的标识", example = "2048")
22 private String processDefinitionKey; // 精准匹配 24 private String processDefinitionKey; // 精准匹配
23 25
  26 + @Schema(description = "审批状态", example = "1")
  27 + @InEnum(BpmTaskStatusEnum.class)
  28 + private Integer status; // 仅【已办】使用
  29 +
24 @Schema(description = "创建时间") 30 @Schema(description = "创建时间")
25 @DateTimeFormat(pattern = DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) 31 @DateTimeFormat(pattern = DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
26 private LocalDateTime[] createTime; 32 private LocalDateTime[] createTime;
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java
1 package cn.iocoder.yudao.module.bpm.convert.task; 1 package cn.iocoder.yudao.module.bpm.convert.task;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.date.DateUtil;
4 import cn.hutool.core.util.StrUtil; 5 import cn.hutool.core.util.StrUtil;
5 import cn.iocoder.yudao.framework.common.pojo.PageResult; 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 import cn.iocoder.yudao.framework.common.util.collection.MapUtils; 7 import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
7 import cn.iocoder.yudao.framework.common.util.collection.SetUtils; 8 import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
  9 +import cn.iocoder.yudao.framework.common.util.date.DateUtils;
8 import cn.iocoder.yudao.framework.common.util.number.NumberUtils; 10 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
9 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 11 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
10 import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; 12 import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
  13 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
11 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; 14 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
12 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO; 15 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO;
13 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO; 16 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO;
14 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceBpmnModelViewRespVO; 17 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceBpmnModelViewRespVO;
15 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceRespVO; 18 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceRespVO;
  19 +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessPrintDataRespVO;
16 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO; 20 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO;
17 import cn.iocoder.yudao.module.bpm.convert.definition.BpmProcessDefinitionConvert; 21 import cn.iocoder.yudao.module.bpm.convert.definition.BpmProcessDefinitionConvert;
18 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; 22 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
19 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; 23 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
20 import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent; 24 import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
  25 +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
  26 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
21 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; 27 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
22 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; 28 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
23 import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO; 29 import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO;
@@ -35,10 +41,7 @@ import org.mapstruct.Mapping; @@ -35,10 +41,7 @@ import org.mapstruct.Mapping;
35 import org.mapstruct.MappingTarget; 41 import org.mapstruct.MappingTarget;
36 import org.mapstruct.factory.Mappers; 42 import org.mapstruct.factory.Mappers;
37 43
38 -import java.util.HashSet;  
39 -import java.util.List;  
40 -import java.util.Map;  
41 -import java.util.Set; 44 +import java.util.*;
42 45
43 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; 46 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
44 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; 47 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@@ -293,4 +296,47 @@ public interface BpmProcessInstanceConvert { @@ -293,4 +296,47 @@ public interface BpmProcessInstanceConvert {
293 .setActivityNodes(activityNodes); 296 .setActivityNodes(activityNodes);
294 } 297 }
295 298
  299 + default BpmProcessPrintDataRespVO buildProcessInstancePrintData(HistoricProcessInstance historicProcessInstance,
  300 + BpmProcessDefinitionInfoDO processDefinitionInfo,
  301 + List<HistoricTaskInstance> tasks,
  302 + Map<Long, AdminUserRespDTO> userMap,
  303 + UserSimpleBaseVO startUser) {
  304 + BpmModelMetaInfoVO.PrintTemplateSetting printTemplateSetting = processDefinitionInfo.getPrintTemplateSetting();
  305 + BpmProcessPrintDataRespVO printData = new BpmProcessPrintDataRespVO();
  306 + // 打印模板是否开启
  307 + printData.setPrintTemplateEnable(printTemplateSetting != null && Boolean.TRUE.equals(printTemplateSetting.getEnable()));
  308 + // 流程相关数据
  309 + BpmProcessInstanceRespVO processInstance = new BpmProcessInstanceRespVO()
  310 + .setId(historicProcessInstance.getId()).setName(historicProcessInstance.getName())
  311 + .setBusinessKey(historicProcessInstance.getBusinessKey())
  312 + .setStartTime(DateUtils.of(historicProcessInstance.getStartTime()))
  313 + .setEndTime(DateUtils.of(historicProcessInstance.getEndTime()))
  314 + .setStartUser(startUser).setStatus(FlowableUtils.getProcessInstanceStatus(historicProcessInstance))
  315 + .setFormVariables(historicProcessInstance.getProcessVariables())
  316 + .setProcessDefinition(BeanUtils.toBean(processDefinitionInfo, BpmProcessDefinitionRespVO.class));
  317 + printData.setProcessInstance(processInstance);
  318 + // 审批历史
  319 + List<BpmProcessPrintDataRespVO.Task> approveTasks = new ArrayList<>(tasks.size());
  320 + tasks.forEach(item -> {
  321 + Map<String, Object> taskLocalVariables = item.getTaskLocalVariables();
  322 + BpmProcessPrintDataRespVO.Task approveTask = new BpmProcessPrintDataRespVO.Task();
  323 + approveTask.setName(item.getName());
  324 + approveTask.setId(item.getId());
  325 + approveTask.setSignPicUrl((String) taskLocalVariables.get(BpmnVariableConstants.TASK_SIGN_PIC_URL));
  326 + approveTask.setDescription(StrUtil.format("{} / {} / {} / {} / {}",
  327 + userMap.get(Long.valueOf(item.getAssignee())).getNickname(),
  328 + item.getName(),
  329 + DateUtil.formatDateTime(item.getEndTime()),
  330 + BpmTaskStatusEnum.valueOf((Integer) taskLocalVariables.get(BpmnVariableConstants.TASK_VARIABLE_STATUS)).getName(),
  331 + taskLocalVariables.get(BpmnVariableConstants.TASK_VARIABLE_REASON)));
  332 + approveTasks.add(approveTask);
  333 + });
  334 + printData.setTasks(approveTasks);
  335 + // 自定义模板
  336 + if (printData.getPrintTemplateEnable() && printTemplateSetting != null) {
  337 + printData.setPrintTemplateHtml(printTemplateSetting.getTemplate());
  338 + }
  339 + return printData;
  340 + }
  341 +
296 } 342 }
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java
@@ -224,4 +224,10 @@ public class BpmProcessDefinitionInfoDO extends BaseDO { @@ -224,4 +224,10 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
224 @TableField(typeHandler = JacksonTypeHandler.class) 224 @TableField(typeHandler = JacksonTypeHandler.class)
225 private BpmModelMetaInfoVO.HttpRequestSetting taskAfterTriggerSetting; 225 private BpmModelMetaInfoVO.HttpRequestSetting taskAfterTriggerSetting;
226 226
  227 + /**
  228 + * 自定义打印模板设置
  229 + */
  230 + @TableField(typeHandler = JacksonTypeHandler.class)
  231 + private BpmModelMetaInfoVO.PrintTemplateSetting printTemplateSetting;
  232 +
227 } 233 }
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/BpmProcessIdRedisDAO.java
@@ -51,8 +51,8 @@ public class BpmProcessIdRedisDAO { @@ -51,8 +51,8 @@ public class BpmProcessIdRedisDAO {
51 String noPrefix = processIdRule.getPrefix() + infix + processIdRule.getPostfix(); 51 String noPrefix = processIdRule.getPrefix() + infix + processIdRule.getPostfix();
52 String key = RedisKeyConstants.BPM_PROCESS_ID + noPrefix; 52 String key = RedisKeyConstants.BPM_PROCESS_ID + noPrefix;
53 Long no = stringRedisTemplate.opsForValue().increment(key); 53 Long no = stringRedisTemplate.opsForValue().increment(key);
54 - if (StrUtil.isNotEmpty(infix)) {  
55 - // 特殊:没有前缀,则不能过期,不能每次都是从 0 开始 54 + if (StrUtil.isEmpty(infix)) {
  55 + // 特殊:没有前缀,则不能过期,不能每次都是从 0 开始。可见 https://t.zsxq.com/MU1E2 讨论
56 stringRedisTemplate.expire(key, Duration.ofDays(1L)); 56 stringRedisTemplate.expire(key, Duration.ofDays(1L));
57 } 57 }
58 return noPrefix + String.format("%0" + processIdRule.getLength() + "d", no); 58 return noPrefix + String.format("%0" + processIdRule.getLength() + "d", no);
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java
@@ -63,7 +63,7 @@ public interface ErrorCodeConstants { @@ -63,7 +63,7 @@ public interface ErrorCodeConstants {
63 ErrorCode TASK_WITHDRAW_FAIL_PROCESS_NOT_RUNNING = new ErrorCode(1_009_005_017, "撤回失败,流程实例未运行!"); 63 ErrorCode TASK_WITHDRAW_FAIL_PROCESS_NOT_RUNNING = new ErrorCode(1_009_005_017, "撤回失败,流程实例未运行!");
64 ErrorCode TASK_WITHDRAW_FAIL_TASK_NOT_EXISTS = new ErrorCode(1_009_005_018, "撤回失败,未查询到用户已办任务!"); 64 ErrorCode TASK_WITHDRAW_FAIL_TASK_NOT_EXISTS = new ErrorCode(1_009_005_018, "撤回失败,未查询到用户已办任务!");
65 ErrorCode TASK_WITHDRAW_FAIL_NOT_ALLOW = new ErrorCode(1_009_005_019, "撤回失败,此流程不允许撤回操作!"); 65 ErrorCode TASK_WITHDRAW_FAIL_NOT_ALLOW = new ErrorCode(1_009_005_019, "撤回失败,此流程不允许撤回操作!");
66 - ErrorCode TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW = new ErrorCode(1_009_005_019, "撤回失败,下一节点不满足撤回条件!"); 66 + ErrorCode TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW = new ErrorCode(1_009_005_020, "撤回失败,下一节点不满足撤回条件!");
67 67
68 // ========== 动态表单模块 1-009-010-000 ========== 68 // ========== 动态表单模块 1-009-010-000 ==========
69 ErrorCode FORM_NOT_EXISTS = new ErrorCode(1_009_010_000, "动态表单不存在"); 69 ErrorCode FORM_NOT_EXISTS = new ErrorCode(1_009_010_000, "动态表单不存在");
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java
1 package cn.iocoder.yudao.module.bpm.enums.task; 1 package cn.iocoder.yudao.module.bpm.enums.task;
2 2
  3 +import cn.hutool.core.util.ArrayUtil;
3 import cn.iocoder.yudao.framework.common.core.ArrayValuable; 4 import cn.iocoder.yudao.framework.common.core.ArrayValuable;
4 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; 5 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
5 import lombok.AllArgsConstructor; 6 import lombok.AllArgsConstructor;
@@ -47,4 +48,8 @@ public enum BpmProcessInstanceStatusEnum implements ArrayValuable&lt;Integer&gt; { @@ -47,4 +48,8 @@ public enum BpmProcessInstanceStatusEnum implements ArrayValuable&lt;Integer&gt; {
47 APPROVE.getStatus(), REJECT.getStatus(), CANCEL.getStatus()); 48 APPROVE.getStatus(), REJECT.getStatus(), CANCEL.getStatus());
48 } 49 }
49 50
  51 + public static BpmProcessInstanceStatusEnum valueOf(Integer status) {
  52 + return ArrayUtil.firstMatch(item -> item.getStatus().equals(status), values());
  53 + }
  54 +
50 } 55 }
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java
1 package cn.iocoder.yudao.module.bpm.enums.task; 1 package cn.iocoder.yudao.module.bpm.enums.task;
2 2
  3 +import cn.hutool.core.util.ArrayUtil;
3 import cn.hutool.core.util.ObjUtil; 4 import cn.hutool.core.util.ObjUtil;
  5 +import cn.iocoder.yudao.framework.common.core.ArrayValuable;
4 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; 6 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
5 import lombok.AllArgsConstructor; 7 import lombok.AllArgsConstructor;
6 import lombok.Getter; 8 import lombok.Getter;
7 9
  10 +import java.util.Arrays;
  11 +
8 /** 12 /**
9 * 流程任务 Task 的状态枚举 13 * 流程任务 Task 的状态枚举
10 * 14 *
@@ -12,7 +16,7 @@ import lombok.Getter; @@ -12,7 +16,7 @@ import lombok.Getter;
12 */ 16 */
13 @Getter 17 @Getter
14 @AllArgsConstructor 18 @AllArgsConstructor
15 -public enum BpmTaskStatusEnum { 19 +public enum BpmTaskStatusEnum implements ArrayValuable<Integer> {
16 20
17 SKIP(-2, "跳过"), 21 SKIP(-2, "跳过"),
18 NOT_START(-1, "未开始"), 22 NOT_START(-1, "未开始"),
@@ -35,6 +39,8 @@ public enum BpmTaskStatusEnum { @@ -35,6 +39,8 @@ public enum BpmTaskStatusEnum {
35 */ 39 */
36 WAIT(0, "待审批"); 40 WAIT(0, "待审批");
37 41
  42 + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmTaskStatusEnum::getStatus).toArray(Integer[]::new);
  43 +
38 /** 44 /**
39 * 状态 45 * 状态
40 * <p> 46 * <p>
@@ -46,6 +52,11 @@ public enum BpmTaskStatusEnum { @@ -46,6 +52,11 @@ public enum BpmTaskStatusEnum {
46 */ 52 */
47 private final String name; 53 private final String name;
48 54
  55 + @Override
  56 + public Integer[] array() {
  57 + return ARRAYS;
  58 + }
  59 +
49 public static boolean isRejectStatus(Integer status) { 60 public static boolean isRejectStatus(Integer status) {
50 return REJECT.getStatus().equals(status); 61 return REJECT.getStatus().equals(status);
51 } 62 }
@@ -68,4 +79,8 @@ public enum BpmTaskStatusEnum { @@ -68,4 +79,8 @@ public enum BpmTaskStatusEnum {
68 return ObjUtil.equal(status, CANCEL.getStatus()); 79 return ObjUtil.equal(status, CANCEL.getStatus());
69 } 80 }
70 81
  82 + public static BpmTaskStatusEnum valueOf(Integer status) {
  83 + return ArrayUtil.firstMatch(item -> item.getStatus().equals(status), values());
  84 + }
  85 +
71 } 86 }
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java
@@ -43,14 +43,23 @@ public class BpmnVariableConstants { @@ -43,14 +43,23 @@ public class BpmnVariableConstants {
43 * @see ProcessInstance#getProcessVariables() 43 * @see ProcessInstance#getProcessVariables()
44 */ 44 */
45 public static final String PROCESS_INSTANCE_VARIABLE_START_USER_ID = "PROCESS_START_USER_ID"; 45 public static final String PROCESS_INSTANCE_VARIABLE_START_USER_ID = "PROCESS_START_USER_ID";
  46 +
46 /** 47 /**
47 - * 流程实例的变量 - 用于判断流程实例变量节点是否驳回. 格式 RETURN_FLAG_{节点 id} 48 + * 流程实例的变量 - 用于判断流程实例变量节点是否驳回:格式 RETURN_FLAG_{节点 id}
48 * 49 *
49 - * 目的是:驳回到发起节点时,因为审批人与发起人相同,所以被自动通过。但是,此时还是希望不要自动通过 50 + * 目的是:退回到发起节点时,因为审批人与发起人相同,所以被自动通过。但是,此时还是希望不要自动通过
50 * 51 *
51 * @see ProcessInstance#getProcessVariables() 52 * @see ProcessInstance#getProcessVariables()
52 */ 53 */
53 public static final String PROCESS_INSTANCE_VARIABLE_RETURN_FLAG = "RETURN_FLAG_%s"; 54 public static final String PROCESS_INSTANCE_VARIABLE_RETURN_FLAG = "RETURN_FLAG_%s";
  55 +
  56 + /**
  57 + * 流程实例的变量前缀 - 用于退回操作,记录需要预测的节点:格式 NEED_SIMULATE_TASK_{节点定义 id}
  58 + *
  59 + * 目的是:退回操作,预测节点会不准,在流程变量中记录需要预测的节点,来辅助预测
  60 + */
  61 + public static final String PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX = "NEED_SIMULATE_TASK_";
  62 +
54 /** 63 /**
55 * 流程实例的变量 - 是否跳过表达式 64 * 流程实例的变量 - 是否跳过表达式
56 * 65 *
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java
@@ -658,10 +658,11 @@ public class BpmnModelUtils { @@ -658,10 +658,11 @@ public class BpmnModelUtils {
658 658
659 // 根据类型,获取入口连线 659 // 根据类型,获取入口连线
660 List<SequenceFlow> sequenceFlows = getElementIncomingFlows(source); 660 List<SequenceFlow> sequenceFlows = getElementIncomingFlows(source);
  661 + // 1. 没有入口连线,则返回 false
661 if (CollUtil.isEmpty(sequenceFlows)) { 662 if (CollUtil.isEmpty(sequenceFlows)) {
662 - return true; 663 + return false;
663 } 664 }
664 - // 循环找到目标元素 665 + // 2. 循环找目标元素, 找到目标节点
665 for (SequenceFlow sequenceFlow : sequenceFlows) { 666 for (SequenceFlow sequenceFlow : sequenceFlows) {
666 // 如果发现连线重复,说明循环了,跳过这个循环 667 // 如果发现连线重复,说明循环了,跳过这个循环
667 if (visitedElements.contains(sequenceFlow.getId())) { 668 if (visitedElements.contains(sequenceFlow.getId())) {
@@ -669,21 +670,22 @@ public class BpmnModelUtils { @@ -669,21 +670,22 @@ public class BpmnModelUtils {
669 } 670 }
670 // 添加已经走过的连线 671 // 添加已经走过的连线
671 visitedElements.add(sequenceFlow.getId()); 672 visitedElements.add(sequenceFlow.getId());
672 - // 这条线路存在目标节点,这条线路完成,进入下个线路 673 + // 这条线路存在目标节点,直接返回 true
673 FlowElement sourceFlowElement = sequenceFlow.getSourceFlowElement(); 674 FlowElement sourceFlowElement = sequenceFlow.getSourceFlowElement();
674 if (target.getId().equals(sourceFlowElement.getId())) { 675 if (target.getId().equals(sourceFlowElement.getId())) {
675 - continue; 676 + return true;
676 } 677 }
677 - // 如果目标节点为并行网关,则不继续 678 + // 如果目标节点为并行网关,跳过这个循环 (TODO 疑问:这个判断作用是防止回退到并行网关分支上的节点吗?)
678 if (sourceFlowElement instanceof ParallelGateway) { 679 if (sourceFlowElement instanceof ParallelGateway) {
679 - return false; 680 + continue;
680 } 681 }
681 - // 否则就继续迭代  
682 - if (!isSequentialReachable(sourceFlowElement, target, visitedElements)) {  
683 - return false; 682 + // 继续迭代,如果找到目标节点直接返回 true
  683 + if (isSequentialReachable(sourceFlowElement, target, visitedElements)) {
  684 + return true;
684 } 685 }
685 } 686 }
686 - return true; 687 + // 未找到返回 false
  688 + return false;
687 } 689 }
688 690
689 /** 691 /**
@@ -783,7 +785,6 @@ public class BpmnModelUtils { @@ -783,7 +785,6 @@ public class BpmnModelUtils {
783 return resultElements; 785 return resultElements;
784 } 786 }
785 787
786 - @SuppressWarnings("PatternVariableCanBeUsed")  
787 private static void simulateNextFlowElements(FlowElement currentElement, Map<String, Object> variables, 788 private static void simulateNextFlowElements(FlowElement currentElement, Map<String, Object> variables,
788 List<FlowElement> resultElements, Set<FlowElement> visitElements) { 789 List<FlowElement> resultElements, Set<FlowElement> visitElements) {
789 // 如果为空,或者已经遍历过,则直接结束 790 // 如果为空,或者已经遍历过,则直接结束
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java
@@ -737,10 +737,10 @@ public class SimpleModelUtils { @@ -737,10 +737,10 @@ public class SimpleModelUtils {
737 BoundaryEvent boundaryEvent = null; 737 BoundaryEvent boundaryEvent = null;
738 if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())) { 738 if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())) {
739 boundaryEvent = buildTimeoutBoundaryEvent(receiveTask, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(), 739 boundaryEvent = buildTimeoutBoundaryEvent(receiveTask, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(),
740 - node.getDelaySetting().getDelayTime(), null, null); 740 + null, null, node.getDelaySetting().getDelayTime());
741 } else if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())) { 741 } else if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())) {
742 boundaryEvent = buildTimeoutBoundaryEvent(receiveTask, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(), 742 boundaryEvent = buildTimeoutBoundaryEvent(receiveTask, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(),
743 - null, null, node.getDelaySetting().getDelayTime()); 743 + node.getDelaySetting().getDelayTime(), null, null);
744 } else { 744 } else {
745 throw new UnsupportedOperationException("不支持的延迟类型:" + node.getDelaySetting()); 745 throw new UnsupportedOperationException("不支持的延迟类型:" + node.getDelaySetting());
746 } 746 }
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
@@ -2,8 +2,10 @@ package cn.iocoder.yudao.module.bpm.service.task; @@ -2,8 +2,10 @@ package cn.iocoder.yudao.module.bpm.service.task;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.collection.ListUtil; 4 import cn.hutool.core.collection.ListUtil;
  5 +import cn.hutool.core.date.DatePattern;
5 import cn.hutool.core.date.DateUtil; 6 import cn.hutool.core.date.DateUtil;
6 import cn.hutool.core.lang.Assert; 7 import cn.hutool.core.lang.Assert;
  8 +import cn.hutool.core.map.MapUtil;
7 import cn.hutool.core.util.*; 9 import cn.hutool.core.util.*;
8 import cn.iocoder.yudao.framework.common.pojo.PageResult; 10 import cn.iocoder.yudao.framework.common.pojo.PageResult;
9 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; 11 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
@@ -12,6 +14,7 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils; @@ -12,6 +14,7 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
12 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; 14 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
13 import cn.iocoder.yudao.framework.common.util.object.PageUtils; 15 import cn.iocoder.yudao.framework.common.util.object.PageUtils;
14 import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; 16 import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
  17 +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
15 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; 18 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
16 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; 19 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
17 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*; 20 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
@@ -72,6 +75,7 @@ import static cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmA @@ -72,6 +75,7 @@ import static cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmA
72 import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; 75 import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
73 import static cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum.REJECT_CHILD_PROCESS; 76 import static cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum.REJECT_CHILD_PROCESS;
74 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID; 77 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID;
  78 +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX;
75 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseNodeType; 79 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseNodeType;
76 import static java.util.Arrays.asList; 80 import static java.util.Arrays.asList;
77 import static java.util.Collections.singletonList; 81 import static java.util.Collections.singletonList;
@@ -187,6 +191,10 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService @@ -187,6 +191,10 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
187 if (CollUtil.isNotEmpty(reqVO.getProcessVariables())) { 191 if (CollUtil.isNotEmpty(reqVO.getProcessVariables())) {
188 processVariables.putAll(reqVO.getProcessVariables()); 192 processVariables.putAll(reqVO.getProcessVariables());
189 } 193 }
  194 + // 特殊:如果是未发起的场景,则设置发起用户,解决“发起流程”时,需要使用到该变量的问题。例如说:https://t.zsxq.com/fMw5g
  195 + if (historicProcessInstance == null) {
  196 + processVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, loginUserId);
  197 + }
190 // 1.3 读取其它相关数据 198 // 1.3 读取其它相关数据
191 ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition( 199 ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition(
192 historicProcessInstance != null ? historicProcessInstance.getProcessDefinitionId() 200 historicProcessInstance != null ? historicProcessInstance.getProcessDefinitionId()
@@ -218,10 +226,24 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService @@ -218,10 +226,24 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
218 226
219 // 3.1 计算当前登录用户的待办任务 227 // 3.1 计算当前登录用户的待办任务
220 BpmTaskRespVO todoTask = taskService.getTodoTask(loginUserId, reqVO.getTaskId(), reqVO.getProcessInstanceId()); 228 BpmTaskRespVO todoTask = taskService.getTodoTask(loginUserId, reqVO.getTaskId(), reqVO.getProcessInstanceId());
221 - // 3.2 预测未运行节点的审批信息 229 +
  230 + // 3.2 获取由于退回操作,需要预测的节点。从流程变量中获取,回退操作会设置这些变量
  231 + Set<String> needSimulateTaskDefKeysByReturn = new HashSet<>();
  232 + if (StrUtil.isNotEmpty(reqVO.getProcessInstanceId())) {
  233 + Map<String, Object> variables = runtimeService.getVariables(reqVO.getProcessInstanceId());
  234 + Map<String, Object> simulateTaskVariables = MapUtil.filter(variables,
  235 + item -> item.getKey().startsWith(PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX));
  236 + simulateTaskVariables.forEach((key, value) ->
  237 + needSimulateTaskDefKeysByReturn.add(StrUtil.removePrefix(key, PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX)));
  238 + }
  239 + // 移除运行中的节点,运行中的节点无需预测
  240 + // TODO @jason:是不是 foreach runActivityNodes,然后移除 needSimulateTaskDefKeysByReturn 更好?(理解成本低一点)
  241 + CollectionUtils.convertList(runActivityNodes, ActivityNode::getId).forEach(needSimulateTaskDefKeysByReturn::remove);
  242 +
  243 + // 3.3 预测未运行节点的审批信息
222 List<ActivityNode> simulateActivityNodes = getSimulateApproveNodeList(startUserId, bpmnModel, 244 List<ActivityNode> simulateActivityNodes = getSimulateApproveNodeList(startUserId, bpmnModel,
223 processDefinitionInfo, 245 processDefinitionInfo,
224 - processVariables, activities); 246 + processVariables, activities, needSimulateTaskDefKeysByReturn);
225 247
226 // 4. 拼接最终数据 248 // 4. 拼接最终数据
227 return buildApprovalDetail(reqVO, bpmnModel, processDefinition, processDefinitionInfo, historicProcessInstance, 249 return buildApprovalDetail(reqVO, bpmnModel, processDefinition, processDefinitionInfo, historicProcessInstance,
@@ -461,7 +483,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService @@ -461,7 +483,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
461 } 483 }
462 484
463 /** 485 /**
464 - * 获取结束节点的状态 486 + * 获取结束节点的状态
465 */ 487 */
466 private Integer getEndActivityNodeStatus(HistoricTaskInstance task) { 488 private Integer getEndActivityNodeStatus(HistoricTaskInstance task) {
467 Integer status = FlowableUtils.getTaskStatus(task); 489 Integer status = FlowableUtils.getTaskStatus(task);
@@ -546,7 +568,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService @@ -546,7 +568,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
546 private List<ActivityNode> getSimulateApproveNodeList(Long startUserId, BpmnModel bpmnModel, 568 private List<ActivityNode> getSimulateApproveNodeList(Long startUserId, BpmnModel bpmnModel,
547 BpmProcessDefinitionInfoDO processDefinitionInfo, 569 BpmProcessDefinitionInfoDO processDefinitionInfo,
548 Map<String, Object> processVariables, 570 Map<String, Object> processVariables,
549 - List<HistoricActivityInstance> activities) { 571 + List<HistoricActivityInstance> activities,
  572 + Set<String> needSimulateTaskDefKeysByReturn) {
550 // TODO @芋艿:【可优化】在驳回场景下,未来的预测准确性不高。原因是,驳回后,HistoricActivityInstance 573 // TODO @芋艿:【可优化】在驳回场景下,未来的预测准确性不高。原因是,驳回后,HistoricActivityInstance
551 // 包括了历史的操作,不是只有 startEvent 到当前节点的记录 574 // 包括了历史的操作,不是只有 startEvent 到当前节点的记录
552 Set<String> runActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId); 575 Set<String> runActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId);
@@ -555,7 +578,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService @@ -555,7 +578,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
555 List<FlowElement> flowElements = BpmnModelUtils.simulateProcess(bpmnModel, processVariables); 578 List<FlowElement> flowElements = BpmnModelUtils.simulateProcess(bpmnModel, processVariables);
556 return convertList(flowElements, flowElement -> buildNotRunApproveNodeForBpmn( 579 return convertList(flowElements, flowElement -> buildNotRunApproveNodeForBpmn(
557 startUserId, bpmnModel, flowElements, 580 startUserId, bpmnModel, flowElements,
558 - processDefinitionInfo, processVariables, flowElement, runActivityIds)); 581 + processDefinitionInfo, processVariables, flowElement, runActivityIds, needSimulateTaskDefKeysByReturn));
559 } 582 }
560 // 情况二:SIMPLE 设计器 583 // 情况二:SIMPLE 设计器
561 if (Objects.equals(BpmModelTypeEnum.SIMPLE.getType(), processDefinitionInfo.getModelType())) { 584 if (Objects.equals(BpmModelTypeEnum.SIMPLE.getType(), processDefinitionInfo.getModelType())) {
@@ -564,17 +587,19 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService @@ -564,17 +587,19 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
564 List<BpmSimpleModelNodeVO> simpleNodes = SimpleModelUtils.simulateProcess(simpleModel, processVariables); 587 List<BpmSimpleModelNodeVO> simpleNodes = SimpleModelUtils.simulateProcess(simpleModel, processVariables);
565 return convertList(simpleNodes, simpleNode -> buildNotRunApproveNodeForSimple( 588 return convertList(simpleNodes, simpleNode -> buildNotRunApproveNodeForSimple(
566 startUserId, bpmnModel, 589 startUserId, bpmnModel,
567 - processDefinitionInfo, processVariables, simpleNode, runActivityIds)); 590 + processDefinitionInfo, processVariables, simpleNode, runActivityIds, needSimulateTaskDefKeysByReturn));
568 } 591 }
569 throw new IllegalArgumentException("未知设计器类型:" + processDefinitionInfo.getModelType()); 592 throw new IllegalArgumentException("未知设计器类型:" + processDefinitionInfo.getModelType());
570 } 593 }
571 594
572 private ActivityNode buildNotRunApproveNodeForSimple(Long startUserId, BpmnModel bpmnModel, 595 private ActivityNode buildNotRunApproveNodeForSimple(Long startUserId, BpmnModel bpmnModel,
573 BpmProcessDefinitionInfoDO processDefinitionInfo, Map<String, Object> processVariables, 596 BpmProcessDefinitionInfoDO processDefinitionInfo, Map<String, Object> processVariables,
574 - BpmSimpleModelNodeVO node, Set<String> runActivityIds) { 597 + BpmSimpleModelNodeVO node, Set<String> runActivityIds,
  598 + Set<String> needSimulateTaskDefKeysByReturn) {
575 // TODO @芋艿:【可优化】在驳回场景下,未来的预测准确性不高。原因是,驳回后,HistoricActivityInstance 599 // TODO @芋艿:【可优化】在驳回场景下,未来的预测准确性不高。原因是,驳回后,HistoricActivityInstance
576 // 包括了历史的操作,不是只有 startEvent 到当前节点的记录 600 // 包括了历史的操作,不是只有 startEvent 到当前节点的记录
577 - if (runActivityIds.contains(node.getId())) { 601 + if (runActivityIds.contains(node.getId())
  602 + && !needSimulateTaskDefKeysByReturn.contains(node.getId())) { // 特殊:回退操作时候,会记录需要预测的节点到流程变量中。即使在历史操作中,也需要预测
578 return null; 603 return null;
579 } 604 }
580 Integer status = BpmTaskStatusEnum.NOT_START.getStatus(); 605 Integer status = BpmTaskStatusEnum.NOT_START.getStatus();
@@ -621,13 +646,16 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService @@ -621,13 +646,16 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
621 private ActivityNode buildNotRunApproveNodeForBpmn(Long startUserId, BpmnModel bpmnModel, List<FlowElement> flowElements, 646 private ActivityNode buildNotRunApproveNodeForBpmn(Long startUserId, BpmnModel bpmnModel, List<FlowElement> flowElements,
622 BpmProcessDefinitionInfoDO processDefinitionInfo, 647 BpmProcessDefinitionInfoDO processDefinitionInfo,
623 Map<String, Object> processVariables, 648 Map<String, Object> processVariables,
624 - FlowElement node, Set<String> runActivityIds) {  
625 - if (runActivityIds.contains(node.getId())) { 649 + FlowElement node, Set<String> runActivityIds,
  650 + Set<String> needSimulateTaskDefKeysByReturn) {
  651 + // 回退操作时候,会记录需要预测的节点到流程变量中。即使节点在历史操作中,也需要预测。
  652 + if (!needSimulateTaskDefKeysByReturn.contains(node.getId()) && runActivityIds.contains(node.getId())) {
626 return null; 653 return null;
627 } 654 }
  655 +
628 Integer status = BpmTaskStatusEnum.NOT_START.getStatus(); 656 Integer status = BpmTaskStatusEnum.NOT_START.getStatus();
629 // 如果节点被跳过,状态设置为跳过 657 // 如果节点被跳过,状态设置为跳过
630 - if(BpmnModelUtils.isSkipNode(node, processVariables)){ 658 + if (BpmnModelUtils.isSkipNode(node, processVariables)) {
631 status = BpmTaskStatusEnum.SKIP.getStatus(); 659 status = BpmTaskStatusEnum.SKIP.getStatus();
632 } 660 }
633 ActivityNode activityNode = new ActivityNode().setId(node.getId()) 661 ActivityNode activityNode = new ActivityNode().setId(node.getId())
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java
@@ -176,6 +176,14 @@ public interface BpmTaskService { @@ -176,6 +176,14 @@ public interface BpmTaskService {
176 */ 176 */
177 List<HistoricActivityInstance> getHistoricActivityListByExecutionId(String executionId); 177 List<HistoricActivityInstance> getHistoricActivityListByExecutionId(String executionId);
178 178
  179 + /**
  180 + * 获得指定流程实例的已完成的流程任务列表,不包含取消状态
  181 + *
  182 + * @param processInstanceId 流程实例的编号
  183 + * @return 流程任务列表
  184 + */
  185 + List<HistoricTaskInstance> getFinishedTaskListByProcessInstanceIdWithoutCancel(String processInstanceId);
  186 +
179 // ========== Update 写入相关方法 ========== 187 // ========== Update 写入相关方法 ==========
180 188
181 /** 189 /**
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
@@ -2,10 +2,12 @@ package cn.iocoder.yudao.module.bpm.service.task; @@ -2,10 +2,12 @@ package cn.iocoder.yudao.module.bpm.service.task;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.convert.Convert; 4 import cn.hutool.core.convert.Convert;
  5 +import cn.hutool.core.date.DateUtil;
5 import cn.hutool.core.lang.Assert; 6 import cn.hutool.core.lang.Assert;
6 import cn.hutool.core.util.*; 7 import cn.hutool.core.util.*;
7 import cn.hutool.extra.spring.SpringUtil; 8 import cn.hutool.extra.spring.SpringUtil;
8 import cn.iocoder.yudao.framework.common.pojo.PageResult; 9 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  10 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
9 import cn.iocoder.yudao.framework.common.util.date.DateUtils; 11 import cn.iocoder.yudao.framework.common.util.date.DateUtils;
10 import cn.iocoder.yudao.framework.common.util.number.NumberUtils; 12 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
11 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; 13 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
@@ -68,8 +70,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU @@ -68,8 +70,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
68 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; 70 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
69 import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; 71 import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
70 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID; 72 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID;
71 -import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG;  
72 -import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE; 73 +//import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.*;
73 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*; 74 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*;
74 75
75 /** 76 /**
@@ -230,6 +231,9 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -230,6 +231,9 @@ public class BpmTaskServiceImpl implements BpmTaskService {
230 if (StrUtil.isNotBlank(pageVO.getName())) { 231 if (StrUtil.isNotBlank(pageVO.getName())) {
231 taskQuery.taskNameLike("%" + pageVO.getName() + "%"); 232 taskQuery.taskNameLike("%" + pageVO.getName() + "%");
232 } 233 }
  234 + if (pageVO.getStatus() != null) {
  235 + taskQuery.taskVariableValueEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS, pageVO.getStatus());
  236 + }
233 // if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { 237 // if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) {
234 // taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); 238 // taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0]));
235 // taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); 239 // taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1]));
@@ -491,6 +495,17 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -491,6 +495,17 @@ public class BpmTaskServiceImpl implements BpmTaskService {
491 return historyService.createHistoricActivityInstanceQuery().executionId(executionId).list(); 495 return historyService.createHistoricActivityInstanceQuery().executionId(executionId).list();
492 } 496 }
493 497
  498 + @Override
  499 + public List<HistoricTaskInstance> getFinishedTaskListByProcessInstanceIdWithoutCancel(String processInstanceId) {
  500 + return historyService.createHistoricTaskInstanceQuery()
  501 + .finished()
  502 + .includeTaskLocalVariables()
  503 + .processInstanceId(processInstanceId)
  504 + .taskVariableValueNotEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS,
  505 + BpmTaskStatusEnum.CANCEL.getStatus())
  506 + .orderByHistoricTaskInstanceStartTime().asc().list();
  507 + }
  508 +
494 /** 509 /**
495 * 判断指定用户,是否是当前任务的审批人 510 * 判断指定用户,是否是当前任务的审批人
496 * 511 *
@@ -590,7 +605,13 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -590,7 +605,13 @@ public class BpmTaskServiceImpl implements BpmTaskService {
590 bpmnModel, reqVO.getNextAssignees(), instance); 605 bpmnModel, reqVO.getNextAssignees(), instance);
591 runtimeService.setVariables(task.getProcessInstanceId(), variables); 606 runtimeService.setVariables(task.getProcessInstanceId(), variables);
592 607
593 - // 5. 调用 BPM complete 去完成任务 608 + // 5. 移除辅助预测的流程变量,这些变量在回退操作中设置
  609 + // todo @jason:可以直接 + 拼接哈
  610 + String simulateVariableName = StrUtil.concat(false,
  611 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX, task.getTaskDefinitionKey());
  612 + runtimeService.removeVariable(task.getProcessInstanceId(), simulateVariableName);
  613 +
  614 + // 6. 调用 BPM complete 去完成任务
594 taskService.complete(task.getId(), variables, true); 615 taskService.complete(task.getId(), variables, true);
595 616
596 // 【加签专属】处理加签任务 617 // 【加签专属】处理加签任务
@@ -840,34 +861,34 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -840,34 +861,34 @@ public class BpmTaskServiceImpl implements BpmTaskService {
840 if (task.isSuspended()) { 861 if (task.isSuspended()) {
841 throw exception(TASK_IS_PENDING); 862 throw exception(TASK_IS_PENDING);
842 } 863 }
843 - // 1.2 校验源头和目标节点的关系,并返回目标元素  
844 - FlowElement targetElement = validateTargetTaskCanReturn(task.getTaskDefinitionKey(),  
845 - reqVO.getTargetTaskDefinitionKey(), task.getProcessDefinitionId()); 864 + // 1.2 获取流程模型信息
  865 + BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId());
  866 + // 1.3 校验源头和目标节点的关系,并返回目标元素
  867 + FlowElement targetElement = validateTargetTaskCanReturn(bpmnModel, task.getTaskDefinitionKey(),
  868 + reqVO.getTargetTaskDefinitionKey());
846 869
847 // 2. 调用 Flowable 框架的退回逻辑 870 // 2. 调用 Flowable 框架的退回逻辑
848 - returnTask(userId, task, targetElement, reqVO); 871 + returnTask(userId, bpmnModel, task, targetElement, reqVO);
849 } 872 }
850 873
851 /** 874 /**
852 * 退回流程节点时,校验目标任务节点是否可退回 875 * 退回流程节点时,校验目标任务节点是否可退回
853 * 876 *
854 - * @param sourceKey 当前任务节点 Key  
855 - * @param targetKey 目标任务节点 key  
856 - * @param processDefinitionId 当前流程定义 ID 877 + * @param bpmnModel 流程模型
  878 + * @param sourceKey 当前任务节点 Key
  879 + * @param targetKey 目标任务节点 key
857 * @return 目标任务节点元素 880 * @return 目标任务节点元素
858 */ 881 */
859 - private FlowElement validateTargetTaskCanReturn(String sourceKey, String targetKey, String processDefinitionId) {  
860 - // 1.1 获取流程模型信息  
861 - BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processDefinitionId);  
862 - // 1.3 获取当前任务节点元素 882 + private FlowElement validateTargetTaskCanReturn(BpmnModel bpmnModel, String sourceKey, String targetKey) {
  883 + // 1.1 获取当前任务节点元素
863 FlowElement source = BpmnModelUtils.getFlowElementById(bpmnModel, sourceKey); 884 FlowElement source = BpmnModelUtils.getFlowElementById(bpmnModel, sourceKey);
864 - // 1.3 获取跳转的节点元素 885 + // 1.2 获取跳转的节点元素
865 FlowElement target = BpmnModelUtils.getFlowElementById(bpmnModel, targetKey); 886 FlowElement target = BpmnModelUtils.getFlowElementById(bpmnModel, targetKey);
866 if (target == null) { 887 if (target == null) {
867 throw exception(TASK_TARGET_NODE_NOT_EXISTS); 888 throw exception(TASK_TARGET_NODE_NOT_EXISTS);
868 } 889 }
869 890
870 - // 2.2 只有串行可到达的节点,才可以退回。类似非串行、子流程无法退回 891 + // 2. 只有串行可到达的节点,才可以退回。类似非串行、子流程无法退回
871 if (!BpmnModelUtils.isSequentialReachable(source, target, null)) { 892 if (!BpmnModelUtils.isSequentialReachable(source, target, null)) {
872 throw exception(TASK_RETURN_FAIL_SOURCE_TARGET_ERROR); 893 throw exception(TASK_RETURN_FAIL_SOURCE_TARGET_ERROR);
873 } 894 }
@@ -878,11 +899,12 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -878,11 +899,12 @@ public class BpmTaskServiceImpl implements BpmTaskService {
878 * 执行退回逻辑 899 * 执行退回逻辑
879 * 900 *
880 * @param userId 用户编号 901 * @param userId 用户编号
  902 + * @param bpmnModel 流程模型
881 * @param currentTask 当前退回的任务 903 * @param currentTask 当前退回的任务
882 * @param targetElement 需要退回到的目标任务 904 * @param targetElement 需要退回到的目标任务
883 * @param reqVO 前端参数封装 905 * @param reqVO 前端参数封装
884 */ 906 */
885 - public void returnTask(Long userId, Task currentTask, FlowElement targetElement, BpmTaskReturnReqVO reqVO) { 907 + public void returnTask(Long userId, BpmnModel bpmnModel, Task currentTask, FlowElement targetElement, BpmTaskReturnReqVO reqVO) {
886 // 1. 获得所有需要回撤的任务 taskDefinitionKey,用于稍后的 moveActivityIdsToSingleActivityId 回撤 908 // 1. 获得所有需要回撤的任务 taskDefinitionKey,用于稍后的 moveActivityIdsToSingleActivityId 回撤
887 // 1.1 获取所有正常进行的任务节点 Key 909 // 1.1 获取所有正常进行的任务节点 Key
888 List<Task> taskList = taskService.createTaskQuery().processInstanceId(currentTask.getProcessInstanceId()).list(); 910 List<Task> taskList = taskService.createTaskQuery().processInstanceId(currentTask.getProcessInstanceId()).list();
@@ -915,18 +937,54 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -915,18 +937,54 @@ public class BpmTaskServiceImpl implements BpmTaskService {
915 } 937 }
916 }); 938 });
917 939
918 - // 3. 设置流程变量节点驳回标记:用于驳回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略。导致自动通过  
919 - runtimeService.setVariable(currentTask.getProcessInstanceId(),  
920 - String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, reqVO.getTargetTaskDefinitionKey()), Boolean.TRUE); 940 + // 3. 构建需要预测的任务流程变量
  941 + // TODO @jason:【驳回预测相关】是不是搞成一个变量,里面是 set 更简洁一点呀?
  942 + Set<String> taskDefinitionKeyList = getNeedSimulateTaskDefinitionKeys(bpmnModel, currentTask, targetElement);
  943 + Map<String, Object> needSimulateVariables = convertMap(taskDefinitionKeyList,
  944 + taskId -> StrUtil.concat(false, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX, taskId), item -> Boolean.TRUE);
  945 +
921 // 4. 执行驳回 946 // 4. 执行驳回
922 // 使用 moveExecutionsToSingleActivityId 替换 moveActivityIdsToSingleActivityId 原因: 947 // 使用 moveExecutionsToSingleActivityId 替换 moveActivityIdsToSingleActivityId 原因:
923 // 当多实例任务回退的时候有问题。相关 issue: https://github.com/flowable/flowable-engine/issues/3944 948 // 当多实例任务回退的时候有问题。相关 issue: https://github.com/flowable/flowable-engine/issues/3944
924 runtimeService.createChangeActivityStateBuilder() 949 runtimeService.createChangeActivityStateBuilder()
925 .processInstanceId(currentTask.getProcessInstanceId()) 950 .processInstanceId(currentTask.getProcessInstanceId())
926 .moveExecutionsToSingleActivityId(runExecutionIds, reqVO.getTargetTaskDefinitionKey()) 951 .moveExecutionsToSingleActivityId(runExecutionIds, reqVO.getTargetTaskDefinitionKey())
  952 + // 设置需要预测的任务流程变量,用于辅助预测
  953 + .processVariables(needSimulateVariables)
  954 + // 设置流程变量(local)节点退回标记, 用于退回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略,导致自动通过
  955 + .localVariable(reqVO.getTargetTaskDefinitionKey(),
  956 + String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, reqVO.getTargetTaskDefinitionKey()),
  957 + Boolean.TRUE)
927 .changeState(); 958 .changeState();
928 } 959 }
929 960
  961 + private Set<String> getNeedSimulateTaskDefinitionKeys(BpmnModel bpmnModel, Task currentTask, FlowElement targetElement) {
  962 + // 1. 获取需要预测的任务的 definition key。因为当前任务还没完成,也需要预测
  963 + Set<String> taskDefinitionKeys = CollUtil.newHashSet(currentTask.getTaskDefinitionKey());
  964 +
  965 + // 2.1 从已结束任务中找到要回退的目标任务,按时间倒序最近的一个目标任务
  966 + List<HistoricTaskInstance> endTaskList = CollectionUtils.filterList(
  967 + getTaskListByProcessInstanceId(currentTask.getProcessInstanceId(), Boolean.FALSE),
  968 + item -> item.getEndTime() != null);
  969 + // 2.2 遍历已结束的任务,找到在 targetTask 之后生成的任务,且串行可达的任务
  970 + HistoricTaskInstance targetTask = findFirst(endTaskList,
  971 + item -> item.getTaskDefinitionKey().equals(targetElement.getId()));
  972 + // TODO @jason:【驳回预测相关】是不是 if targetTask 先判空?
  973 + endTaskList.forEach(item -> {
  974 + FlowElement element = getFlowElementById(bpmnModel, item.getTaskDefinitionKey());
  975 + // 如果已结束的任务在回退目标节点之后生成,且串行可达,则标记为需要预算节点
  976 + // TODO 串行可达的方法需要和判断可回退节点 validateTargetTaskCanReturn 分开吗? 并行网关可能会有问题。
  977 + // TODO @jason:【驳回预测相关】这里是不是判断 element 哈?
  978 + if (targetTask != null
  979 + // TODO @jason:【驳回预测相关】这里直接 createTime 的 compare 更简单?因为不太会出现空哈。
  980 + && DateUtil.compare(item.getCreateTime(), targetTask.getCreateTime()) > 0
  981 + && BpmnModelUtils.isSequentialReachable(element, targetElement, null)) {
  982 + taskDefinitionKeys.add(item.getTaskDefinitionKey());
  983 + }
  984 + });
  985 + return taskDefinitionKeys;
  986 + }
  987 +
930 @Override 988 @Override
931 @Transactional(rollbackFor = Exception.class) 989 @Transactional(rollbackFor = Exception.class)
932 public void delegateTask(Long userId, BpmTaskDelegateReqVO reqVO) { 990 public void delegateTask(Long userId, BpmTaskDelegateReqVO reqVO) {
@@ -1438,12 +1496,11 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -1438,12 +1496,11 @@ public class BpmTaskServiceImpl implements BpmTaskService {
1438 return; 1496 return;
1439 } 1497 }
1440 FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey()); 1498 FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
1441 - // 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略  
1442 - // TODO 芋艿:【优化】未来有没更好的判断方式?!另外,还要考虑清理机制。就是说,下次处理了之后,就移除这个标识  
1443 - Boolean returnTaskFlag = runtimeService.getVariable(processInstance.getProcessInstanceId(),  
1444 - String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class); 1499 + // 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略(使用 local variable)
  1500 + Boolean returnTaskFlag = runtimeService.getVariableLocal(task.getExecutionId(),
  1501 + String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class);
1445 Boolean skipStartUserNodeFlag = Convert.toBool(runtimeService.getVariable(processInstance.getProcessInstanceId(), 1502 Boolean skipStartUserNodeFlag = Convert.toBool(runtimeService.getVariable(processInstance.getProcessInstanceId(),
1446 - PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class)); 1503 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class));
1447 if (userTaskElement.getId().equals(START_USER_NODE_ID) 1504 if (userTaskElement.getId().equals(START_USER_NODE_ID)
1448 && (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核 1505 && (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核
1449 || BooleanUtil.isTrue(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核 1506 || BooleanUtil.isTrue(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java
@@ -7,6 +7,11 @@ import io.swagger.v3.oas.annotations.media.Schema; @@ -7,6 +7,11 @@ import io.swagger.v3.oas.annotations.media.Schema;
7 import lombok.Data; 7 import lombok.Data;
8 import lombok.EqualsAndHashCode; 8 import lombok.EqualsAndHashCode;
9 import lombok.ToString; 9 import lombok.ToString;
  10 +import org.springframework.format.annotation.DateTimeFormat;
  11 +
  12 +import java.time.LocalDateTime;
  13 +
  14 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
10 15
11 @Schema(description = "管理后台 - 线索分页 Request VO") 16 @Schema(description = "管理后台 - 线索分页 Request VO")
12 @Data 17 @Data
@@ -42,4 +47,8 @@ public class CrmCluePageReqVO extends PageParam { @@ -42,4 +47,8 @@ public class CrmCluePageReqVO extends PageParam {
42 @Schema(description = "跟进状态", example = "true") 47 @Schema(description = "跟进状态", example = "true")
43 private Boolean followUpStatus; 48 private Boolean followUpStatus;
44 49
  50 + @Schema(description = "创建时间", example = "[2023-01-01 00:00:00, 2023-01-31 23:59:59]")
  51 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  52 + private LocalDateTime[] createTime;
  53 +
45 } 54 }
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java
@@ -33,6 +33,7 @@ public interface CrmClueMapper extends BaseMapperX&lt;CrmClueDO&gt; { @@ -33,6 +33,7 @@ public interface CrmClueMapper extends BaseMapperX&lt;CrmClueDO&gt; {
33 .eqIfPresent(CrmClueDO::getLevel, pageReqVO.getLevel()) 33 .eqIfPresent(CrmClueDO::getLevel, pageReqVO.getLevel())
34 .eqIfPresent(CrmClueDO::getSource, pageReqVO.getSource()) 34 .eqIfPresent(CrmClueDO::getSource, pageReqVO.getSource())
35 .eqIfPresent(CrmClueDO::getFollowUpStatus, pageReqVO.getFollowUpStatus()) 35 .eqIfPresent(CrmClueDO::getFollowUpStatus, pageReqVO.getFollowUpStatus())
  36 + .betweenIfPresent(CrmClueDO::getCreateTime, pageReqVO.getCreateTime())
36 .orderByDesc(CrmClueDO::getId); 37 .orderByDesc(CrmClueDO::getId);
37 return selectJoinPage(pageReqVO, CrmClueDO.class, query); 38 return selectJoinPage(pageReqVO, CrmClueDO.class, query);
38 } 39 }
yudao-module-infra/pom.xml
@@ -101,7 +101,7 @@ @@ -101,7 +101,7 @@
101 <artifactId>commons-net</artifactId> <!-- 文件客户端:解决 ftp 连接 --> 101 <artifactId>commons-net</artifactId> <!-- 文件客户端:解决 ftp 连接 -->
102 </dependency> 102 </dependency>
103 <dependency> 103 <dependency>
104 - <groupId>com.jcraft</groupId> 104 + <groupId>com.github.mwiede</groupId>
105 <artifactId>jsch</artifactId> <!-- 文件客户端:解决 sftp 连接 --> 105 <artifactId>jsch</artifactId> <!-- 文件客户端:解决 sftp 连接 -->
106 </dependency> 106 </dependency>
107 <!-- 文件客户端:解决阿里云、腾讯云、minio 等 S3 连接 --> 107 <!-- 文件客户端:解决阿里云、腾讯云、minio 等 S3 连接 -->
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java
@@ -26,6 +26,7 @@ import org.springframework.validation.annotation.Validated; @@ -26,6 +26,7 @@ import org.springframework.validation.annotation.Validated;
26 import org.springframework.web.bind.annotation.*; 26 import org.springframework.web.bind.annotation.*;
27 import org.springframework.web.multipart.MultipartFile; 27 import org.springframework.web.multipart.MultipartFile;
28 28
  29 +import java.nio.charset.StandardCharsets;
29 import java.util.List; 30 import java.util.List;
30 31
31 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; 32 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -99,8 +100,10 @@ public class FileController { @@ -99,8 +100,10 @@ public class FileController {
99 if (StrUtil.isEmpty(path)) { 100 if (StrUtil.isEmpty(path)) {
100 throw new IllegalArgumentException("结尾的 path 路径必须传递"); 101 throw new IllegalArgumentException("结尾的 path 路径必须传递");
101 } 102 }
102 - // 解码,解决中文路径的问题 https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/807/  
103 - path = URLUtil.decode(path); 103 + // 解码,解决中文路径的问题
  104 + // https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/807/
  105 + // https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1432/
  106 + path = URLUtil.decode(path, StandardCharsets.UTF_8, false);
104 107
105 // 读取内容 108 // 读取内容
106 byte[] content = fileService.getFileContent(configId, path); 109 byte[] content = fileService.getFileContent(configId, path);
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java
@@ -21,9 +21,8 @@ public enum CodegenFrontTypeEnum { @@ -21,9 +21,8 @@ public enum CodegenFrontTypeEnum {
21 VUE3_VBEN5_ANTD_SCHEMA(40), // Vue3 VBEN5 + ANTD + schema 模版 21 VUE3_VBEN5_ANTD_SCHEMA(40), // Vue3 VBEN5 + ANTD + schema 模版
22 VUE3_VBEN5_ANTD_GENERAL(41), // Vue3 VBEN5 + ANTD 标准模版 22 VUE3_VBEN5_ANTD_GENERAL(41), // Vue3 VBEN5 + ANTD 标准模版
23 23
24 - // TODO @puhui999::50、51 会好点;  
25 - VUE3_VBEN5_EP_SCHEMA(42), // Vue3 VBEN5 + EP + schema 模版  
26 - VUE3_VBEN5_EP_GENERAL(43), // Vue3 VBEN5 + EP 标准模版 24 + VUE3_VBEN5_EP_SCHEMA(50), // Vue3 VBEN5 + EP + schema 模版
  25 + VUE3_VBEN5_EP_GENERAL(51), // Vue3 VBEN5 + EP 标准模版
27 ; 26 ;
28 27
29 /** 28 /**
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/ftp/FtpFileClient.java
@@ -4,6 +4,7 @@ import cn.hutool.core.io.FileUtil; @@ -4,6 +4,7 @@ import cn.hutool.core.io.FileUtil;
4 import cn.hutool.core.util.CharsetUtil; 4 import cn.hutool.core.util.CharsetUtil;
5 import cn.hutool.core.util.StrUtil; 5 import cn.hutool.core.util.StrUtil;
6 import cn.hutool.extra.ftp.Ftp; 6 import cn.hutool.extra.ftp.Ftp;
  7 +import cn.hutool.extra.ftp.FtpConfig;
7 import cn.hutool.extra.ftp.FtpException; 8 import cn.hutool.extra.ftp.FtpException;
8 import cn.hutool.extra.ftp.FtpMode; 9 import cn.hutool.extra.ftp.FtpMode;
9 import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient; 10 import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient;
@@ -18,6 +19,15 @@ import java.io.ByteArrayOutputStream; @@ -18,6 +19,15 @@ import java.io.ByteArrayOutputStream;
18 */ 19 */
19 public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> { 20 public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> {
20 21
  22 + /**
  23 + * 连接超时时间,单位:毫秒
  24 + */
  25 + private static final Long CONNECTION_TIMEOUT = 3000L;
  26 + /**
  27 + * 读写超时时间,单位:毫秒
  28 + */
  29 + private static final Long SO_TIMEOUT = 10000L;
  30 +
21 private Ftp ftp; 31 private Ftp ftp;
22 32
23 public FtpFileClient(Long id, FtpFileClientConfig config) { 33 public FtpFileClient(Long id, FtpFileClientConfig config) {
@@ -26,9 +36,12 @@ public class FtpFileClient extends AbstractFileClient&lt;FtpFileClientConfig&gt; { @@ -26,9 +36,12 @@ public class FtpFileClient extends AbstractFileClient&lt;FtpFileClientConfig&gt; {
26 36
27 @Override 37 @Override
28 protected void doInit() { 38 protected void doInit() {
29 - // 初始化 Ftp 对象  
30 - this.ftp = new Ftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword(),  
31 - CharsetUtil.CHARSET_UTF_8, null, null, FtpMode.valueOf(config.getMode())); 39 + // 初始化 Ftp 对象:https://gitee.com/zhijiantianya/yudao-cloud/pulls/207/
  40 + FtpConfig ftpConfig = new FtpConfig(config.getHost(), config.getPort(), config.getUsername(), config.getPassword(),
  41 + CharsetUtil.CHARSET_UTF_8, null, null);
  42 + ftpConfig.setConnectionTimeout(CONNECTION_TIMEOUT);
  43 + ftpConfig.setSoTimeout(SO_TIMEOUT);
  44 + this.ftp = new Ftp(ftpConfig, FtpMode.valueOf(config.getMode()));
32 } 45 }
33 46
34 @Override 47 @Override
@@ -72,4 +85,4 @@ public class FtpFileClient extends AbstractFileClient&lt;FtpFileClientConfig&gt; { @@ -72,4 +85,4 @@ public class FtpFileClient extends AbstractFileClient&lt;FtpFileClientConfig&gt; {
72 ftp.reconnectIfTimeout(); 85 ftp.reconnectIfTimeout();
73 } 86 }
74 87
75 -}  
76 \ No newline at end of file 88 \ No newline at end of file
  89 +}
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/ftp/FtpFileClientConfig.java
1 package cn.iocoder.yudao.module.infra.framework.file.core.client.ftp; 1 package cn.iocoder.yudao.module.infra.framework.file.core.client.ftp;
2 2
3 import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClientConfig; 3 import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClientConfig;
4 -import lombok.Data;  
5 -import org.hibernate.validator.constraints.URL;  
6 -  
7 import jakarta.validation.constraints.NotEmpty; 4 import jakarta.validation.constraints.NotEmpty;
8 import jakarta.validation.constraints.NotNull; 5 import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +import org.hibernate.validator.constraints.URL;
9 8
10 /** 9 /**
11 * Ftp 文件客户端的配置类 10 * Ftp 文件客户端的配置类
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/sftp/SftpFileClient.java
1 package cn.iocoder.yudao.module.infra.framework.file.core.client.sftp; 1 package cn.iocoder.yudao.module.infra.framework.file.core.client.sftp;
2 2
3 import cn.hutool.core.io.FileUtil; 3 import cn.hutool.core.io.FileUtil;
  4 +import cn.hutool.core.util.CharsetUtil;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.hutool.extra.ftp.FtpConfig;
  7 +import cn.hutool.extra.ssh.JschRuntimeException;
4 import cn.hutool.extra.ssh.Sftp; 8 import cn.hutool.extra.ssh.Sftp;
5 import cn.iocoder.yudao.framework.common.util.io.FileUtils; 9 import cn.iocoder.yudao.framework.common.util.io.FileUtils;
6 import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient; 10 import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient;
  11 +import com.jcraft.jsch.JSch;
7 12
8 import java.io.File; 13 import java.io.File;
9 14
@@ -14,6 +19,20 @@ import java.io.File; @@ -14,6 +19,20 @@ import java.io.File;
14 */ 19 */
15 public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> { 20 public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> {
16 21
  22 + /**
  23 + * 连接超时时间,单位:毫秒
  24 + */
  25 + private static final Long CONNECTION_TIMEOUT = 3000L;
  26 + /**
  27 + * 读写超时时间,单位:毫秒
  28 + */
  29 + private static final Long SO_TIMEOUT = 10000L;
  30 +
  31 + static {
  32 + // 某些旧的 sftp 服务器仅支持 ssh-dss 协议,该协议并不安全,默认不支持该协议,按需添加
  33 + JSch.setConfig("server_host_key", JSch.getConfig("server_host_key") + ",ssh-dss");
  34 + }
  35 +
17 private Sftp sftp; 36 private Sftp sftp;
18 37
19 public SftpFileClient(Long id, SftpFileClientConfig config) { 38 public SftpFileClient(Long id, SftpFileClientConfig config) {
@@ -22,18 +41,27 @@ public class SftpFileClient extends AbstractFileClient&lt;SftpFileClientConfig&gt; { @@ -22,18 +41,27 @@ public class SftpFileClient extends AbstractFileClient&lt;SftpFileClientConfig&gt; {
22 41
23 @Override 42 @Override
24 protected void doInit() { 43 protected void doInit() {
25 - // 初始化 Ftp 对象  
26 - this.sftp = new Sftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword()); 44 + // 初始化 Sftp 对象
  45 + FtpConfig ftpConfig = new FtpConfig(config.getHost(), config.getPort(), config.getUsername(), config.getPassword(),
  46 + CharsetUtil.CHARSET_UTF_8, null, null);
  47 + ftpConfig.setConnectionTimeout(CONNECTION_TIMEOUT);
  48 + ftpConfig.setSoTimeout(SO_TIMEOUT);
  49 + this.sftp = new Sftp(ftpConfig);
27 } 50 }
28 51
29 @Override 52 @Override
30 public String upload(byte[] content, String path, String type) { 53 public String upload(byte[] content, String path, String type) {
31 // 执行写入 54 // 执行写入
32 String filePath = getFilePath(path); 55 String filePath = getFilePath(path);
  56 + String fileName = FileUtil.getName(filePath);
  57 + String dir = StrUtil.removeSuffix(filePath, fileName);
33 File file = FileUtils.createTempFile(content); 58 File file = FileUtils.createTempFile(content);
34 reconnectIfTimeout(); 59 reconnectIfTimeout();
35 - sftp.mkDirs(FileUtil.getParent(filePath, 1)); // 需要创建父目录,不然会报错  
36 - sftp.upload(filePath, file); 60 + sftp.mkDirs(dir); // 需要创建父目录,不然会报错
  61 + boolean success = sftp.upload(filePath, file);
  62 + if (!success) {
  63 + throw new JschRuntimeException(StrUtil.format("上传文件到目标目录 ({}) 失败", filePath));
  64 + }
37 // 拼接返回路径 65 // 拼接返回路径
38 return super.formatFileUrl(config.getDomain(), path); 66 return super.formatFileUrl(config.getDomain(), path);
39 } 67 }
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/sftp/SftpFileClientConfig.java
1 package cn.iocoder.yudao.module.infra.framework.file.core.client.sftp; 1 package cn.iocoder.yudao.module.infra.framework.file.core.client.sftp;
2 2
3 import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClientConfig; 3 import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClientConfig;
4 -import lombok.Data;  
5 -import org.hibernate.validator.constraints.URL;  
6 -  
7 import jakarta.validation.constraints.NotEmpty; 4 import jakarta.validation.constraints.NotEmpty;
8 import jakarta.validation.constraints.NotNull; 5 import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +import org.hibernate.validator.constraints.URL;
9 8
10 /** 9 /**
11 * Sftp 文件客户端的配置类 10 * Sftp 文件客户端的配置类
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java
@@ -343,7 +343,7 @@ public class CodegenEngine { @@ -343,7 +343,7 @@ public class CodegenEngine {
343 filePath = formatFilePath(filePath, bindingMap); 343 filePath = formatFilePath(filePath, bindingMap);
344 String content = templateEngine.getTemplate(vmPath).render(bindingMap); 344 String content = templateEngine.getTemplate(vmPath).render(bindingMap);
345 // 格式化代码 345 // 格式化代码
346 - content = prettyCode(content); 346 + content = prettyCode(content, vmPath);
347 result.put(filePath, content); 347 result.put(filePath, content);
348 } 348 }
349 349
@@ -383,11 +383,14 @@ public class CodegenEngine { @@ -383,11 +383,14 @@ public class CodegenEngine {
383 * 如果不处理,Vue 的 Pretty 格式校验可能会报错 383 * 如果不处理,Vue 的 Pretty 格式校验可能会报错
384 * 384 *
385 * @param content 格式化前的代码 385 * @param content 格式化前的代码
  386 + * @param vmPath 模板路径
386 * @return 格式化后的代码 387 * @return 格式化后的代码
387 */ 388 */
388 - private String prettyCode(String content) {  
389 - // Vue 界面:去除字段后面多余的 , 逗号,解决前端的 Pretty 代码格式检查的报错  
390 - content = content.replaceAll(",\n}", "\n}").replaceAll(",\n }", "\n }"); 389 + private String prettyCode(String content, String vmPath) {
  390 + // Vue 界面:去除字段后面多余的 , 逗号,解决前端的 Pretty 代码格式检查的报错(需要排除 vben5)
  391 + if (!StrUtil.contains(vmPath, "vben5")) {
  392 + content = content.replaceAll(",\n}", "\n}").replaceAll(",\n }", "\n }");
  393 + }
391 // Vue 界面:去除多的 dateFormatter,只有一个的情况下,说明没使用到 394 // Vue 界面:去除多的 dateFormatter,只有一个的情况下,说明没使用到
392 if (StrUtil.count(content, "dateFormatter") == 1) { 395 if (StrUtil.count(content, "dateFormatter") == 1) {
393 content = StrUtils.removeLineContains(content, "dateFormatter"); 396 content = StrUtils.removeLineContains(content, "dateFormatter");
yudao-module-infra/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm
@@ -217,7 +217,7 @@ const handleDeleteBatch = async () =&gt; { @@ -217,7 +217,7 @@ const handleDeleteBatch = async () =&gt; {
217 217
218 const checkedIds = ref<number[]>([]) 218 const checkedIds = ref<number[]>([])
219 const handleRowCheckboxChange = (records: ${subSimpleClassName}[]) => { 219 const handleRowCheckboxChange = (records: ${subSimpleClassName}[]) => {
220 - checkedIds.value = records.map((item) => item.id); 220 + checkedIds.value = records.map((item) => item.id!);
221 } 221 }
222 #end 222 #end
223 #end 223 #end
yudao-module-infra/src/main/resources/codegen/vue3/views/index.vue.vm
@@ -374,7 +374,7 @@ const handleDeleteBatch = async () =&gt; { @@ -374,7 +374,7 @@ const handleDeleteBatch = async () =&gt; {
374 374
375 const checkedIds = ref<number[]>([]) 375 const checkedIds = ref<number[]>([])
376 const handleRowCheckboxChange = (records: ${simpleClassName}[]) => { 376 const handleRowCheckboxChange = (records: ${simpleClassName}[]) => {
377 - checkedIds.value = records.map((item) => item.id); 377 + checkedIds.value = records.map((item) => item.id!);
378 } 378 }
379 #end 379 #end
380 380
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/general/api/api.ts.vm
@@ -98,7 +98,7 @@ export function delete${simpleClassName}List(ids: number[]) { @@ -98,7 +98,7 @@ export function delete${simpleClassName}List(ids: number[]) {
98 98
99 /** 导出${table.classComment} */ 99 /** 导出${table.classComment} */
100 export function export${simpleClassName}(params: any) { 100 export function export${simpleClassName}(params: any) {
101 - return requestClient.download('${baseURL}/export-excel', params); 101 + return requestClient.download('${baseURL}/export-excel', { params });
102 } 102 }
103 103
104 ## 特殊:主子表专属逻辑 104 ## 特殊:主子表专属逻辑
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/general/views/form.vue.vm
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 -import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';  
3 import type { Rule } from 'ant-design-vue/es/form'; 2 import type { Rule } from 'ant-design-vue/es/form';
  3 +import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
  4 +
  5 +import { computed, ref } from 'vue';
4 6
5 import { useVbenModal } from '@vben/common-ui'; 7 import { useVbenModal } from '@vben/common-ui';
  8 +import { DICT_TYPE } from '@vben/constants';
  9 +import { getDictOptions } from '@vben/hooks';
6 import { Tinymce as RichTextarea } from '#/components/tinymce'; 10 import { Tinymce as RichTextarea } from '#/components/tinymce';
7 import { ImageUpload, FileUpload } from "#/components/upload"; 11 import { ImageUpload, FileUpload } from "#/components/upload";
8 import { message, Tabs, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'ant-design-vue'; 12 import { message, Tabs, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'ant-design-vue';
9 -import { DICT_TYPE, getDictOptions } from '#/utils';  
10 #if($table.templateType == 2)## 树表需要导入这些 13 #if($table.templateType == 2)## 树表需要导入这些
11 import { get${simpleClassName}List } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}'; 14 import { get${simpleClassName}List } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
12 import { handleTree } from '@vben/utils' 15 import { handleTree } from '@vben/utils'
@@ -20,7 +23,6 @@ import { handleTree } from &#39;@vben/utils&#39; @@ -20,7 +23,6 @@ import { handleTree } from &#39;@vben/utils&#39;
20 #end 23 #end
21 #end 24 #end
22 25
23 -import { computed, ref } from 'vue';  
24 import { $t } from '#/locales'; 26 import { $t } from '#/locales';
25 import { get${simpleClassName}, create${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}'; 27 import { get${simpleClassName}, create${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
26 28
@@ -70,7 +72,7 @@ const getTitle = computed(() =&gt; { @@ -70,7 +72,7 @@ const getTitle = computed(() =&gt; {
70 #end 72 #end
71 73
72 /** 重置表单 */ 74 /** 重置表单 */
73 -const resetForm = () => { 75 +function resetForm() {
74 formData.value = { 76 formData.value = {
75 #foreach ($column in $columns) 77 #foreach ($column in $columns)
76 #if ($column.createOperation || $column.updateOperation) 78 #if ($column.createOperation || $column.updateOperation)
@@ -88,7 +90,7 @@ const resetForm = () =&gt; { @@ -88,7 +90,7 @@ const resetForm = () =&gt; {
88 ## 特殊:树表专属逻辑 90 ## 特殊:树表专属逻辑
89 #if ( $table.templateType == 2 ) 91 #if ( $table.templateType == 2 )
90 /** 获得${table.classComment}树 */ 92 /** 获得${table.classComment}树 */
91 -const get${simpleClassName}Tree = async () => { 93 +async function get${simpleClassName}Tree() {
92 ${classNameVar}Tree.value = [] 94 ${classNameVar}Tree.value = []
93 const data = await get${simpleClassName}List({}); 95 const data = await get${simpleClassName}List({});
94 data.unshift({ 96 data.unshift({
@@ -147,7 +149,6 @@ const [Modal, modalApi] = useVbenModal({ @@ -147,7 +149,6 @@ const [Modal, modalApi] = useVbenModal({
147 emit('success'); 149 emit('success');
148 message.success({ 150 message.success({
149 content: $t('ui.actionMessage.operationSuccess'), 151 content: $t('ui.actionMessage.operationSuccess'),
150 - key: 'action_process_msg',  
151 }); 152 });
152 } finally { 153 } finally {
153 modalApi.unlock(); 154 modalApi.unlock();
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/general/views/index.vue.vm
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}'; 2 import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
3 -import type { VxeTableInstance } from '#/adapter/vxe-table'; 3 +
  4 +import { ref, h, reactive, onMounted, nextTick } from 'vue';
4 5
5 import { Page, useVbenModal } from '@vben/common-ui'; 6 import { Page, useVbenModal } from '@vben/common-ui';
6 -import { cloneDeep, formatDateTime } from '@vben/utils'; 7 +import { DICT_TYPE } from '@vben/constants';
  8 +import { getDictOptions } from '@vben/hooks';
  9 +import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
  10 +import { cloneDeep, downloadFileFromBlobPart, formatDateTime } from '@vben/utils';
7 import { Button, message,Tabs,Pagination,Form,RangePicker,DatePicker,Select,Input } from 'ant-design-vue'; 11 import { Button, message,Tabs,Pagination,Form,RangePicker,DatePicker,Select,Input } from 'ant-design-vue';
8 -import { DictTag } from '#/components/dict-tag';  
9 -import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';  
10 import ${simpleClassName}Form from './modules/form.vue'; 12 import ${simpleClassName}Form from './modules/form.vue';
11 import { Download, Plus, RefreshCw, Search, Trash2 } from '@vben/icons'; 13 import { Download, Plus, RefreshCw, Search, Trash2 } from '@vben/icons';
12 import { ContentWrap } from '#/components/content-wrap'; 14 import { ContentWrap } from '#/components/content-wrap';
13 import { VxeColumn, VxeTable } from '#/adapter/vxe-table'; 15 import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
14 -import { TableToolbar } from '#/components/table-toolbar';  
15 -import { useTableToolbar } from '#/hooks';  
16 16
17 ## 特殊:主子表专属逻辑 17 ## 特殊:主子表专属逻辑
18 #if ( $table.templateType == 11 || $table.templateType == 12 ) 18 #if ( $table.templateType == 11 || $table.templateType == 12 )
@@ -23,7 +23,6 @@ import { useTableToolbar } from &#39;#/hooks&#39;; @@ -23,7 +23,6 @@ import { useTableToolbar } from &#39;#/hooks&#39;;
23 #end 23 #end
24 #end 24 #end
25 25
26 -import { ref, h, reactive,onMounted,nextTick } from 'vue';  
27 import { $t } from '#/locales'; 26 import { $t } from '#/locales';
28 #if (${table.templateType} == 2)## 树表接口 27 #if (${table.templateType} == 2)## 树表接口
29 import { handleTree,isEmpty } from '@vben/utils' 28 import { handleTree,isEmpty } from '@vben/utils'
@@ -77,7 +76,7 @@ const queryFormRef = ref() // 搜索的表单 @@ -77,7 +76,7 @@ const queryFormRef = ref() // 搜索的表单
77 const exportLoading = ref(false) // 导出的加载中 76 const exportLoading = ref(false) // 导出的加载中
78 77
79 /** 查询列表 */ 78 /** 查询列表 */
80 -const getList = async () => { 79 +async function getList() {
81 loading.value = true 80 loading.value = true
82 try { 81 try {
83 const params = cloneDeep(queryParams) as any; 82 const params = cloneDeep(queryParams) as any;
@@ -104,7 +103,7 @@ const getList = async () =&gt; { @@ -104,7 +103,7 @@ const getList = async () =&gt; {
104 } 103 }
105 104
106 /** 搜索按钮操作 */ 105 /** 搜索按钮操作 */
107 -const handleQuery = () => { 106 +function handleQuery() {
108 #if ( $table.templateType != 2 ) 107 #if ( $table.templateType != 2 )
109 queryParams.pageNo = 1 108 queryParams.pageNo = 1
110 #end 109 #end
@@ -112,7 +111,7 @@ const handleQuery = () =&gt; { @@ -112,7 +111,7 @@ const handleQuery = () =&gt; {
112 } 111 }
113 112
114 /** 重置按钮操作 */ 113 /** 重置按钮操作 */
115 -const resetQuery = () => { 114 +function resetQuery() {
116 queryFormRef.value.resetFields() 115 queryFormRef.value.resetFields()
117 handleQuery() 116 handleQuery()
118 } 117 }
@@ -124,7 +123,7 @@ const [FormModal, formModalApi] = useVbenModal({ @@ -124,7 +123,7 @@ const [FormModal, formModalApi] = useVbenModal({
124 123
125 /** 创建${table.classComment} */ 124 /** 创建${table.classComment} */
126 function handleCreate() { 125 function handleCreate() {
127 - formModalApi.setData({}).open(); 126 + formModalApi.setData(null).open();
128 } 127 }
129 128
130 /** 编辑${table.classComment} */ 129 /** 编辑${table.classComment} */
@@ -144,14 +143,10 @@ async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) { @@ -144,14 +143,10 @@ async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) {
144 const hideLoading = message.loading({ 143 const hideLoading = message.loading({
145 content: $t('ui.actionMessage.deleting', [row.id]), 144 content: $t('ui.actionMessage.deleting', [row.id]),
146 duration: 0, 145 duration: 0,
147 - key: 'action_process_msg',  
148 }); 146 });
149 try { 147 try {
150 - await delete${simpleClassName}(row.id as number);  
151 - message.success({  
152 - content: $t('ui.actionMessage.deleteSuccess', [row.id]),  
153 - key: 'action_process_msg',  
154 - }); 148 + await delete${simpleClassName}(row.id!);
  149 + message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
155 await getList(); 150 await getList();
156 } finally { 151 } finally {
157 hideLoading(); 152 hideLoading();
@@ -164,12 +159,11 @@ async function handleDeleteBatch() { @@ -164,12 +159,11 @@ async function handleDeleteBatch() {
164 const hideLoading = message.loading({ 159 const hideLoading = message.loading({
165 content: $t('ui.actionMessage.deleting'), 160 content: $t('ui.actionMessage.deleting'),
166 duration: 0, 161 duration: 0,
167 - key: 'action_process_msg',  
168 }); 162 });
169 try { 163 try {
170 await delete${simpleClassName}List(checkedIds.value); 164 await delete${simpleClassName}List(checkedIds.value);
171 checkedIds.value = []; 165 checkedIds.value = [];
172 - message.success( $t('ui.actionMessage.deleteSuccess') ); 166 + message.success($t('ui.actionMessage.deleteSuccess'));
173 await getList(); 167 await getList();
174 } finally { 168 } finally {
175 hideLoading(); 169 hideLoading();
@@ -182,12 +176,12 @@ function handleRowCheckboxChange({ @@ -182,12 +176,12 @@ function handleRowCheckboxChange({
182 }: { 176 }: {
183 records: ${simpleClassName}Api.${simpleClassName}[]; 177 records: ${simpleClassName}Api.${simpleClassName}[];
184 }) { 178 }) {
185 - checkedIds.value = records.map((item) => item.id); 179 + checkedIds.value = records.map((item) => item.id!);
186 } 180 }
187 #end 181 #end
188 182
189 /** 导出表格 */ 183 /** 导出表格 */
190 -async function onExport() { 184 +async function handleExport() {
191 try { 185 try {
192 exportLoading.value = true; 186 exportLoading.value = true;
193 const data = await export${simpleClassName}(queryParams); 187 const data = await export${simpleClassName}(queryParams);
@@ -200,7 +194,7 @@ try { @@ -200,7 +194,7 @@ try {
200 #if (${table.templateType} == 2) 194 #if (${table.templateType} == 2)
201 /** 切换树形展开/收缩状态 */ 195 /** 切换树形展开/收缩状态 */
202 const isExpanded = ref(true); 196 const isExpanded = ref(true);
203 -function toggleExpand() { 197 +function handleExpand() {
204 isExpanded.value = !isExpanded.value; 198 isExpanded.value = !isExpanded.value;
205 tableRef.value?.setAllTreeExpand(isExpanded.value); 199 tableRef.value?.setAllTreeExpand(isExpanded.value);
206 } 200 }
@@ -303,12 +297,12 @@ onMounted(() =&gt; { @@ -303,12 +297,12 @@ onMounted(() =&gt; {
303 <!-- 列表 --> 297 <!-- 列表 -->
304 <ContentWrap title="${table.classComment}"> 298 <ContentWrap title="${table.classComment}">
305 <template #extra> 299 <template #extra>
306 - <TableToolbar 300 + <VbenVxeTableToolbar
307 ref="tableToolbarRef" 301 ref="tableToolbarRef"
308 v-model:hidden-search="hiddenSearchBar" 302 v-model:hidden-search="hiddenSearchBar"
309 > 303 >
310 #if (${table.templateType} == 2) 304 #if (${table.templateType} == 2)
311 - <Button @click="toggleExpand" class="mr-2"> 305 + <Button @click="handleExpand" class="mr-2">
312 {{ isExpanded ? '收缩' : '展开' }} 306 {{ isExpanded ? '收缩' : '展开' }}
313 </Button> 307 </Button>
314 #end 308 #end
@@ -326,7 +320,7 @@ onMounted(() =&gt; { @@ -326,7 +320,7 @@ onMounted(() =&gt; {
326 type="primary" 320 type="primary"
327 class="ml-2" 321 class="ml-2"
328 :loading="exportLoading" 322 :loading="exportLoading"
329 - @click="onExport" 323 + @click="handleExport"
330 v-access:code="['${permissionPrefix}:export']" 324 v-access:code="['${permissionPrefix}:export']"
331 > 325 >
332 {{ $t('ui.actionTitle.export') }} 326 {{ $t('ui.actionTitle.export') }}
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/form_sub_erp.vue.vm
@@ -3,107 +3,109 @@ @@ -3,107 +3,109 @@
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 <script lang="ts" setup> 5 <script lang="ts" setup>
6 - import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';  
7 - import type { Rule } from 'ant-design-vue/es/form'; 6 +import type { Rule } from 'ant-design-vue/es/form';
  7 +import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
8 8
9 - import { useVbenModal } from '@vben/common-ui';  
10 - import { Tinymce as RichTextarea } from '#/components/tinymce';  
11 - import { ImageUpload, FileUpload } from "#/components/upload";  
12 - import { message, Tabs, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'ant-design-vue';  
13 - import { DICT_TYPE, getDictOptions } from '#/utils'; 9 +import { computed, ref } from 'vue';
14 10
15 - import { computed, ref } from 'vue';  
16 - import { $t } from '#/locales'; 11 +import { useVbenModal } from '@vben/common-ui';
  12 +import { DICT_TYPE } from '@vben/constants';
  13 +import { getDictOptions } from '@vben/hooks';
  14 +import { cloneDeep, formatDateTime } from '@vben/utils';
  15 +import { Tinymce as RichTextarea } from '#/components/tinymce';
  16 +import { ImageUpload, FileUpload } from "#/components/upload";
  17 +import { message, Tabs, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'ant-design-vue';
17 18
18 - import { get${subSimpleClassName}, create${subSimpleClassName}, update${subSimpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}'; 19 +import { $t } from '#/locales';
19 20
20 - const emit = defineEmits(['success']);  
21 - const getTitle = computed(() => {  
22 - return formData.value?.id  
23 - ? $t('ui.actionTitle.edit', ['${subTable.classComment}'])  
24 - : $t('ui.actionTitle.create', ['${subTable.classComment}']);  
25 - }); 21 +import { get${subSimpleClassName}, create${subSimpleClassName}, update${subSimpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
26 22
27 - const formRef = ref();  
28 - const formData = ref<Partial<${simpleClassName}Api.${subSimpleClassName}>>({  
29 - #foreach ($column in $subColumns)  
30 - #if ($column.createOperation || $column.updateOperation)  
31 - #if ($column.htmlType == "checkbox")  
32 - $column.javaField: [],  
33 - #else  
34 - $column.javaField: undefined,  
35 - #end 23 +const emit = defineEmits(['success']);
  24 +const getTitle = computed(() => {
  25 + return formData.value?.id
  26 + ? $t('ui.actionTitle.edit', ['${subTable.classComment}'])
  27 + : $t('ui.actionTitle.create', ['${subTable.classComment}']);
  28 +});
  29 +
  30 +const formRef = ref();
  31 +const formData = ref<Partial<${simpleClassName}Api.${subSimpleClassName}>>({
  32 + #foreach ($column in $subColumns)
  33 + #if ($column.createOperation || $column.updateOperation)
  34 + #if ($column.htmlType == "checkbox")
  35 + $column.javaField: [],
  36 + #else
  37 + $column.javaField: undefined,
36 #end 38 #end
37 #end 39 #end
38 - });  
39 - const rules: Record<string, Rule[]> = {  
40 - #foreach ($column in $subColumns)  
41 - #if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键  
42 - #set($comment=$column.columnComment)  
43 - $column.javaField: [{ required: true, message: '${comment}不能为空', trigger: #if($column.htmlType == 'select')'change'#else'blur'#end }],  
44 - #end 40 + #end
  41 +});
  42 +const rules: Record<string, Rule[]> = {
  43 + #foreach ($column in $subColumns)
  44 + #if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键
  45 + #set($comment=$column.columnComment)
  46 + $column.javaField: [{ required: true, message: '${comment}不能为空', trigger: #if($column.htmlType == 'select')'change'#else'blur'#end }],
45 #end 47 #end
46 - }; 48 + #end
  49 +};
  50 +
  51 +const [Modal, modalApi] = useVbenModal({
  52 + async onConfirm() {
  53 + await formRef.value?.validate();
47 54
48 - const [Modal, modalApi] = useVbenModal({  
49 - async onConfirm() {  
50 - await formRef.value?.validate(); 55 + modalApi.lock();
  56 + // 提交表单
  57 + const data = formData.value as ${simpleClassName}Api.${subSimpleClassName};
  58 + try {
  59 + await (formData.value?.id ? update${subSimpleClassName}(data) : create${subSimpleClassName}(data));
  60 + // 关闭并提示
  61 + await modalApi.close();
  62 + emit('success');
  63 + message.success({
  64 + content: $t('ui.actionMessage.operationSuccess'),
  65 + });
  66 + } finally {
  67 + modalApi.unlock();
  68 + }
  69 + },
  70 + async onOpenChange(isOpen: boolean) {
  71 + if (!isOpen) {
  72 + resetForm()
  73 + return;
  74 + }
51 75
  76 + // 加载数据
  77 + let data = modalApi.getData<${simpleClassName}Api.${subSimpleClassName}>();
  78 + if (!data) {
  79 + return;
  80 + }
  81 + if (data.id) {
52 modalApi.lock(); 82 modalApi.lock();
53 - // 提交表单  
54 - const data = formData.value as ${simpleClassName}Api.${subSimpleClassName};  
55 try { 83 try {
56 - await (formData.value?.id ? update${subSimpleClassName}(data) : create${subSimpleClassName}(data));  
57 - // 关闭并提示  
58 - await modalApi.close();  
59 - emit('success');  
60 - message.success({  
61 - content: $t('ui.actionMessage.operationSuccess'),  
62 - key: 'action_process_msg',  
63 - }); 84 + data = await get${subSimpleClassName}(data.id);
64 } finally { 85 } finally {
65 modalApi.unlock(); 86 modalApi.unlock();
66 } 87 }
67 - },  
68 - async onOpenChange(isOpen: boolean) {  
69 - if (!isOpen) {  
70 - resetForm()  
71 - return;  
72 - } 88 + }
  89 + // 设置到 values
  90 + formData.value = data;
  91 + },
  92 +});
73 93
74 - // 加载数据  
75 - let data = modalApi.getData<${simpleClassName}Api.${subSimpleClassName}>();  
76 - if (!data) {  
77 - return;  
78 - }  
79 - if (data.id) {  
80 - modalApi.lock();  
81 - try {  
82 - data = await get${subSimpleClassName}(data.id);  
83 - } finally {  
84 - modalApi.unlock();  
85 - }  
86 - }  
87 - // 设置到 values  
88 - formData.value = data;  
89 - },  
90 - });  
91 -  
92 - /** 重置表单 */  
93 - const resetForm = () => {  
94 - formData.value = {  
95 - #foreach ($column in $subColumns)  
96 - #if ($column.createOperation || $column.updateOperation)  
97 - #if ($column.htmlType == "checkbox")  
98 - $column.javaField: [],  
99 - #else  
100 - $column.javaField: undefined,  
101 - #end 94 +/** 重置表单 */
  95 +function resetForm(){
  96 + formData.value = {
  97 + #foreach ($column in $subColumns)
  98 + #if ($column.createOperation || $column.updateOperation)
  99 + #if ($column.htmlType == "checkbox")
  100 + $column.javaField: [],
  101 + #else
  102 + $column.javaField: undefined,
102 #end 103 #end
103 #end 104 #end
104 - };  
105 - formRef.value?.resetFields();  
106 - } 105 + #end
  106 + };
  107 + formRef.value?.resetFields();
  108 +}
107 </script> 109 </script>
108 110
109 <template> 111 <template>
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/form_sub_normal.vue.vm
@@ -5,12 +5,15 @@ @@ -5,12 +5,15 @@
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 <script lang="ts" setup> 7 <script lang="ts" setup>
8 - import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}'; 8 +import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
9 9
10 - import { message, Tabs, Form, Input, Textarea,Button, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker } from 'ant-design-vue';  
11 - import { computed, ref, h, onMounted,watch,nextTick } from 'vue';  
12 - import { $t } from '#/locales';  
13 - import { DICT_TYPE, getDictOptions } from '#/utils'; 10 +import { computed, ref, h, onMounted,watch,nextTick } from 'vue';
  11 +
  12 +import { DICT_TYPE } from '@vben/constants';
  13 +import { getDictOptions } from '@vben/hooks';
  14 +
  15 +import { message, Tabs, Form, Input, Textarea,Button, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker } from 'ant-design-vue';
  16 +import { $t } from '#/locales';
14 17
15 #if ($subTable.subJoinMany) ## 一对多 18 #if ($subTable.subJoinMany) ## 一对多
16 import type { VxeTableInstance } from '#/adapter/vxe-table'; 19 import type { VxeTableInstance } from '#/adapter/vxe-table';
@@ -30,13 +33,14 @@ const props = defineProps&lt;{ @@ -30,13 +33,14 @@ const props = defineProps&lt;{
30 #if ($subTable.subJoinMany) ## 一对多 33 #if ($subTable.subJoinMany) ## 一对多
31 const list = ref<${simpleClassName}Api.${subSimpleClassName}[]>([]) // 列表的数据 34 const list = ref<${simpleClassName}Api.${subSimpleClassName}[]>([]) // 列表的数据
32 const tableRef = ref<VxeTableInstance>(); 35 const tableRef = ref<VxeTableInstance>();
  36 +
33 /** 添加${subTable.classComment} */ 37 /** 添加${subTable.classComment} */
34 -const onAdd = async () => { 38 +async function handleAdd() {
35 await tableRef.value?.insertAt({} as ${simpleClassName}Api.${subSimpleClassName}, -1); 39 await tableRef.value?.insertAt({} as ${simpleClassName}Api.${subSimpleClassName}, -1);
36 } 40 }
37 41
38 /** 删除${subTable.classComment} */ 42 /** 删除${subTable.classComment} */
39 -const onDelete = async (row: ${simpleClassName}Api.${subSimpleClassName}) => { 43 +async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
40 await tableRef.value?.remove(row); 44 await tableRef.value?.remove(row);
41 } 45 }
42 46
@@ -46,9 +50,11 @@ defineExpose({ @@ -46,9 +50,11 @@ defineExpose({
46 const data = list.value as ${simpleClassName}Api.${subSimpleClassName}[]; 50 const data = list.value as ${simpleClassName}Api.${subSimpleClassName}[];
47 const removeRecords = tableRef.value?.getRemoveRecords() as ${simpleClassName}Api.${subSimpleClassName}[]; 51 const removeRecords = tableRef.value?.getRemoveRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
48 const insertRecords = tableRef.value?.getInsertRecords() as ${simpleClassName}Api.${subSimpleClassName}[]; 52 const insertRecords = tableRef.value?.getInsertRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
49 - return data  
50 - .filter((row) => !removeRecords.some((removed) => removed.id === row.id))  
51 - ?.concat(insertRecords.map((row: any) => ({ ...row, id: undefined }))); 53 + return [
  54 + ...data.filter(
  55 + (row) => !removeRecords.some((removed) => removed.id === row.id),
  56 + ),
  57 + ...insertRecords.map((row: any) => ({ ...row, id: undefined })),
52 }, 58 },
53 }); 59 });
54 60
@@ -123,25 +129,25 @@ watch( @@ -123,25 +129,25 @@ watch(
123 #if ( $column.id == $subJoinColumn.id) ## 特殊:忽略主子表的 join 字段,不用填写 129 #if ( $column.id == $subJoinColumn.id) ## 特殊:忽略主子表的 join 字段,不用填写
124 #elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里 130 #elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
125 <vxe-column field="${javaField}" title="${comment}" align="center"> 131 <vxe-column field="${javaField}" title="${comment}" align="center">
126 - <template #default="{row}"> 132 + <template #default="{ row }">
127 <Input v-model:value="row.${javaField}" /> 133 <Input v-model:value="row.${javaField}" />
128 </template> 134 </template>
129 </vxe-column> 135 </vxe-column>
130 #elseif($column.htmlType == "imageUpload")## 图片上传 136 #elseif($column.htmlType == "imageUpload")## 图片上传
131 <vxe-column field="${javaField}" title="${comment}" align="center"> 137 <vxe-column field="${javaField}" title="${comment}" align="center">
132 - <template #default="{row}"> 138 + <template #default="{ row }">
133 <ImageUpload v-model:value="row.${javaField}" /> 139 <ImageUpload v-model:value="row.${javaField}" />
134 </template> 140 </template>
135 </vxe-column> 141 </vxe-column>
136 #elseif($column.htmlType == "fileUpload")## 文件上传 142 #elseif($column.htmlType == "fileUpload")## 文件上传
137 <vxe-column field="${javaField}" title="${comment}" align="center"> 143 <vxe-column field="${javaField}" title="${comment}" align="center">
138 - <template #default="{row}"> 144 + <template #default="{ row }">
139 <FileUpload v-model:value="row.${javaField}" /> 145 <FileUpload v-model:value="row.${javaField}" />
140 </template> 146 </template>
141 </vxe-column> 147 </vxe-column>
142 #elseif($column.htmlType == "select")## 下拉框 148 #elseif($column.htmlType == "select")## 下拉框
143 <vxe-column field="${javaField}" title="${comment}" align="center"> 149 <vxe-column field="${javaField}" title="${comment}" align="center">
144 - <template #default="{row}"> 150 + <template #default="{ row }">
145 <Select v-model:value="row.${javaField}" placeholder="请选择${comment}"> 151 <Select v-model:value="row.${javaField}" placeholder="请选择${comment}">
146 #if ("" != $dictType)## 有数据字典 152 #if ("" != $dictType)## 有数据字典
147 <Select.Option 153 <Select.Option
@@ -159,7 +165,7 @@ watch( @@ -159,7 +165,7 @@ watch(
159 </vxe-column> 165 </vxe-column>
160 #elseif($column.htmlType == "checkbox")## 多选框 166 #elseif($column.htmlType == "checkbox")## 多选框
161 <vxe-column field="${javaField}" title="${comment}" align="center"> 167 <vxe-column field="${javaField}" title="${comment}" align="center">
162 - <template #default="{row}"> 168 + <template #default="{ row }">
163 <CheckboxGroup v-model:value="row.${javaField}"> 169 <CheckboxGroup v-model:value="row.${javaField}">
164 #if ("" != $dictType)## 有数据字典 170 #if ("" != $dictType)## 有数据字典
165 <Checkbox 171 <Checkbox
@@ -177,7 +183,7 @@ watch( @@ -177,7 +183,7 @@ watch(
177 </vxe-column> 183 </vxe-column>
178 #elseif($column.htmlType == "radio")## 单选框 184 #elseif($column.htmlType == "radio")## 单选框
179 <vxe-column field="${javaField}" title="${comment}" align="center"> 185 <vxe-column field="${javaField}" title="${comment}" align="center">
180 - <template #default="{row}"> 186 + <template #default="{ row }">
181 <RadioGroup v-model:value="row.${javaField}"> 187 <RadioGroup v-model:value="row.${javaField}">
182 #if ("" != $dictType)## 有数据字典 188 #if ("" != $dictType)## 有数据字典
183 <Radio 189 <Radio
@@ -195,7 +201,7 @@ watch( @@ -195,7 +201,7 @@ watch(
195 </vxe-column> 201 </vxe-column>
196 #elseif($column.htmlType == "datetime")## 时间框 202 #elseif($column.htmlType == "datetime")## 时间框
197 <vxe-column field="${javaField}" title="${comment}" align="center"> 203 <vxe-column field="${javaField}" title="${comment}" align="center">
198 - <template #default="{row}"> 204 + <template #default="{ row }">
199 <DatePicker 205 <DatePicker
200 v-model:value="row.${javaField}" 206 v-model:value="row.${javaField}"
201 :showTime="true" 207 :showTime="true"
@@ -206,7 +212,7 @@ watch( @@ -206,7 +212,7 @@ watch(
206 </vxe-column> 212 </vxe-column>
207 #elseif($column.htmlType == "textarea" || $column.htmlType == "editor")## 文本框 213 #elseif($column.htmlType == "textarea" || $column.htmlType == "editor")## 文本框
208 <vxe-column field="${javaField}" title="${comment}" align="center"> 214 <vxe-column field="${javaField}" title="${comment}" align="center">
209 - <template #default="{row}"> 215 + <template #default="{ row }">
210 <Textarea v-model:value="row.${javaField}" /> 216 <Textarea v-model:value="row.${javaField}" />
211 </template> 217 </template>
212 </vxe-column> 218 </vxe-column>
@@ -214,12 +220,12 @@ watch( @@ -214,12 +220,12 @@ watch(
214 #end 220 #end
215 #end 221 #end
216 <vxe-column field="operation" title="操作" align="center"> 222 <vxe-column field="operation" title="操作" align="center">
217 - <template #default="{row}"> 223 + <template #default="{ row }">
218 <Button 224 <Button
219 size="small" 225 size="small"
220 type="link" 226 type="link"
221 danger 227 danger
222 - @click="onDelete(row as any)" 228 + @click="handleDelect(row)"
223 v-access:code="['${permissionPrefix}:delete']" 229 v-access:code="['${permissionPrefix}:delete']"
224 > 230 >
225 {{ $t('ui.actionTitle.delete') }} 231 {{ $t('ui.actionTitle.delete') }}
@@ -228,7 +234,7 @@ watch( @@ -228,7 +234,7 @@ watch(
228 </vxe-column> 234 </vxe-column>
229 </vxe-table> 235 </vxe-table>
230 <div class="flex justify-center mt-4"> 236 <div class="flex justify-center mt-4">
231 - <Button :icon="h(Plus)" type="primary" ghost @click="onAdd" v-access:code="['${permissionPrefix}:create']"> 237 + <Button :icon="h(Plus)" type="primary" ghost @click="handleAdd" v-access:code="['${permissionPrefix}:create']">
232 {{ $t('ui.actionTitle.create', ['${subTable.classComment}']) }} 238 {{ $t('ui.actionTitle.create', ['${subTable.classComment}']) }}
233 </Button> 239 </Button>
234 </div> 240 </div>
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/list_sub_erp.vue.vm
@@ -6,48 +6,50 @@ @@ -6,48 +6,50 @@
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 <script lang="ts" setup> 8 <script lang="ts" setup>
9 - import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';  
10 - import type { VxeTableInstance } from '#/adapter/vxe-table'; 9 +import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
  10 +import type { VxeTableInstance } from '#/adapter/vxe-table';
11 11
12 - import { DictTag } from '#/components/dict-tag';  
13 - import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';  
14 - import { VxeColumn, VxeTable } from '#/adapter/vxe-table';  
15 - import { reactive,ref, h, nextTick,watch,onMounted } from 'vue';  
16 - import { cloneDeep, formatDateTime } from '@vben/utils';  
17 - import { ContentWrap } from '#/components/content-wrap'; 12 +import { reactive,ref, h, nextTick,watch,onMounted } from 'vue';
  13 +
  14 +import { DICT_TYPE } from '@vben/constants';
  15 +import { getDictOptions } from '@vben/hooks';
  16 +
  17 +import { DictTag } from '#/components/dict-tag';
  18 +import { getRangePickerDefaultProps } from '#/utils';
  19 +import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
  20 +import { ContentWrap } from '#/components/content-wrap';
18 21
19 #if ($table.templateType == 11) ## erp 22 #if ($table.templateType == 11) ## erp
20 - import { useVbenModal } from '@vben/common-ui';  
21 - import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue'  
22 - import { Tinymce as RichTextarea } from '#/components/tinymce';  
23 - import { ImageUpload, FileUpload } from "#/components/upload";  
24 - import { message,Button, Tabs,Pagination, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox,RangePicker, DatePicker, TreeSelect } from 'ant-design-vue';  
25 - import { Plus, Trash2 } from '@vben/icons';  
26 - import { $t } from '#/locales';  
27 - import { TableToolbar } from '#/components/table-toolbar';  
28 - import { useTableToolbar } from '#/hooks'; 23 +import { useVbenModal } from '@vben/common-ui';
  24 +import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
  25 +import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue'
  26 +import { Tinymce as RichTextarea } from '#/components/tinymce';
  27 +import { ImageUpload, FileUpload } from "#/components/upload";
  28 +import { message,Button, Tabs,Pagination, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox,RangePicker, DatePicker, TreeSelect } from 'ant-design-vue';
  29 +import { Plus, Trash2 } from '@vben/icons';
  30 +import { $t } from '#/locales';
29 #end 31 #end
30 32
31 #if ($table.templateType == 11) ## erp 33 #if ($table.templateType == 11) ## erp
32 - import { delete${subSimpleClassName},#if ($deleteBatchEnable) delete${subSimpleClassName}List,#end get${subSimpleClassName}Page } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';  
33 - import { isEmpty } from '@vben/utils';  
34 - #else  
35 - #if ($subTable.subJoinMany) ## 一对多  
36 - import { get${subSimpleClassName}ListBy${SubJoinColumnName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';  
37 - #else  
38 - import { get${subSimpleClassName}By${SubJoinColumnName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';  
39 - #end 34 +import { delete${subSimpleClassName},#if ($deleteBatchEnable) delete${subSimpleClassName}List,#end get${subSimpleClassName}Page } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
  35 +import { isEmpty } from '@vben/utils';
  36 +#else
  37 +#if ($subTable.subJoinMany) ## 一对多
  38 +import { get${subSimpleClassName}ListBy${SubJoinColumnName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
  39 +#else
  40 +import { get${subSimpleClassName}By${SubJoinColumnName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
  41 +#end
40 #end 42 #end
41 43
42 const props = defineProps<{ 44 const props = defineProps<{
43 - ${subJoinColumn.javaField}?: number // ${subJoinColumn.columnComment}(主表的关联字段) 45 + ${subJoinColumn.javaField}?: number // ${subJoinColumn.columnComment}(主表的关联字段)
44 }>() 46 }>()
45 47
46 #if ($table.templateType == 11) ## erp 48 #if ($table.templateType == 11) ## erp
47 - const [FormModal, formModalApi] = useVbenModal({  
48 - connectedComponent: ${subSimpleClassName}Form,  
49 - destroyOnClose: true,  
50 - }); 49 +const [FormModal, formModalApi] = useVbenModal({
  50 + connectedComponent: ${subSimpleClassName}Form,
  51 + destroyOnClose: true,
  52 +});
51 53
52 /** 创建${subTable.classComment} */ 54 /** 创建${subTable.classComment} */
53 function handleCreate() { 55 function handleCreate() {
@@ -68,14 +70,10 @@ async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) { @@ -68,14 +70,10 @@ async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
68 const hideLoading = message.loading({ 70 const hideLoading = message.loading({
69 content: $t('ui.actionMessage.deleting', [row.id]), 71 content: $t('ui.actionMessage.deleting', [row.id]),
70 duration: 0, 72 duration: 0,
71 - key: 'action_process_msg',  
72 }); 73 });
73 try { 74 try {
74 - await delete${subSimpleClassName}(row.id as number);  
75 - message.success({  
76 - content: $t('ui.actionMessage.deleteSuccess', [row.id]),  
77 - key: 'action_process_msg',  
78 - }); 75 + await delete${subSimpleClassName}(row.id!);
  76 + message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
79 await getList(); 77 await getList();
80 } finally { 78 } finally {
81 hideLoading(); 79 hideLoading();
@@ -88,12 +86,11 @@ async function handleDeleteBatch() { @@ -88,12 +86,11 @@ async function handleDeleteBatch() {
88 const hideLoading = message.loading({ 86 const hideLoading = message.loading({
89 content: $t('ui.actionMessage.deleting'), 87 content: $t('ui.actionMessage.deleting'),
90 duration: 0, 88 duration: 0,
91 - key: 'action_process_msg',  
92 }); 89 });
93 try { 90 try {
94 await delete${subSimpleClassName}List(checkedIds.value); 91 await delete${subSimpleClassName}List(checkedIds.value);
95 checkedIds.value = []; 92 checkedIds.value = [];
96 - message.success( $t('ui.actionMessage.deleteSuccess') ); 93 + message.success($t('ui.actionMessage.deleteSuccess'));
97 await getList(); 94 await getList();
98 } finally { 95 } finally {
99 hideLoading(); 96 hideLoading();
@@ -106,92 +103,92 @@ function handleRowCheckboxChange({ @@ -106,92 +103,92 @@ function handleRowCheckboxChange({
106 }: { 103 }: {
107 records: ${simpleClassName}Api.${subSimpleClassName}[]; 104 records: ${simpleClassName}Api.${subSimpleClassName}[];
108 }) { 105 }) {
109 - checkedIds.value = records.map((item) => item.id); 106 + checkedIds.value = records.map((item) => item.id!);
110 } 107 }
111 #end 108 #end
112 #end 109 #end
113 110
114 - const loading = ref(true) // 列表的加载中  
115 - const list = ref<${simpleClassName}Api.${subSimpleClassName}[]>([]) // 列表的数据 111 +const loading = ref(true) // 列表的加载中
  112 +const list = ref<${simpleClassName}Api.${subSimpleClassName}[]>([]) // 列表的数据
116 #if ($table.templateType == 11) ## erp 113 #if ($table.templateType == 11) ## erp
117 - const total = ref(0) // 列表的总页数 114 +const total = ref(0) // 列表的总页数
118 #end 115 #end
119 #if ($table.templateType == 11) ## erp 116 #if ($table.templateType == 11) ## erp
120 - const queryFormRef = ref() // 搜索的表单  
121 - const queryParams = reactive({  
122 - pageNo: 1,  
123 - pageSize: 10,  
124 - #foreach ($column in $subColumns)  
125 - #if ($column.listOperation)  
126 - #if ($column.listOperationCondition != 'BETWEEN')  
127 - $column.javaField: undefined,  
128 - #end  
129 - #if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")  
130 - $column.javaField: undefined,  
131 - #end  
132 - #end 117 +const queryFormRef = ref() // 搜索的表单
  118 +const queryParams = reactive({
  119 + pageNo: 1,
  120 + pageSize: 10,
  121 + #foreach ($column in $subColumns)
  122 + #if ($column.listOperation)
  123 + #if ($column.listOperationCondition != 'BETWEEN')
  124 + $column.javaField: undefined,
133 #end 125 #end
134 - }) 126 + #if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")
  127 + $column.javaField: undefined,
  128 + #end
  129 + #end
  130 + #end
  131 +})
135 132
136 /** 搜索按钮操作 */ 133 /** 搜索按钮操作 */
137 -const handleQuery = () => { 134 +function handleQuery() {
138 queryParams.pageNo = 1 135 queryParams.pageNo = 1
139 getList() 136 getList()
140 } 137 }
141 138
142 /** 重置按钮操作 */ 139 /** 重置按钮操作 */
143 -const resetQuery = () => { 140 +function resetQuery() {
144 queryFormRef.value.resetFields() 141 queryFormRef.value.resetFields()
145 handleQuery() 142 handleQuery()
146 } 143 }
147 #end 144 #end
148 /** 查询列表 */ 145 /** 查询列表 */
149 - const getList = async () => {  
150 - loading.value = true  
151 - try {  
152 - if (!props.${subJoinColumn.javaField}){  
153 - return []  
154 - }  
155 - ## 特殊:树表专属逻辑(树不需要分页接口)  
156 - #if ($table.templateType == 11) ## erp  
157 - const params = cloneDeep(queryParams) as any;  
158 - #foreach ($column in $columns)  
159 - #if ($column.listOperation)  
160 - #if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")  
161 - if (params.${column.javaField} && Array.isArray(params.${column.javaField})) {  
162 - params.${column.javaField} = (params.${column.javaField} as string[]).join(',');  
163 - }  
164 - #end  
165 - #end  
166 - #end  
167 - params.${subJoinColumn.javaField} = props.${subJoinColumn.javaField};  
168 - const data = await get${subSimpleClassName}Page(params)  
169 - list.value = data.list  
170 - total.value = data.total  
171 - #else  
172 - #if ($subTable.subJoinMany) ## 一对多  
173 - list.value = await get${subSimpleClassName}ListBy${SubJoinColumnName}(props.${subJoinColumn.javaField}!);  
174 - #else  
175 - list.value = [await get${subSimpleClassName}By${SubJoinColumnName}(props.${subJoinColumn.javaField}!)];  
176 - #end  
177 - #end  
178 - } finally {  
179 - loading.value = false 146 +async function getList() {
  147 + loading.value = true
  148 + try {
  149 + if (!props.${subJoinColumn.javaField}){
  150 + return []
180 } 151 }
  152 + ## 特殊:树表专属逻辑(树不需要分页接口)
  153 + #if ($table.templateType == 11) ## erp
  154 + const params = cloneDeep(queryParams) as any;
  155 + #foreach ($column in $columns)
  156 + #if ($column.listOperation)
  157 + #if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")
  158 + if (params.${column.javaField} && Array.isArray(params.${column.javaField})) {
  159 + params.${column.javaField} = (params.${column.javaField} as string[]).join(',');
  160 + }
  161 + #end
  162 + #end
  163 + #end
  164 + params.${subJoinColumn.javaField} = props.${subJoinColumn.javaField};
  165 + const data = await get${subSimpleClassName}Page(params)
  166 + list.value = data.list
  167 + total.value = data.total
  168 + #else
  169 + #if ($subTable.subJoinMany) ## 一对多
  170 + list.value = await get${subSimpleClassName}ListBy${SubJoinColumnName}(props.${subJoinColumn.javaField}!);
  171 + #else
  172 + list.value = [await get${subSimpleClassName}By${SubJoinColumnName}(props.${subJoinColumn.javaField}!)];
  173 + #end
  174 + #end
  175 + } finally {
  176 + loading.value = false
181 } 177 }
  178 +}
182 179
183 - /** 监听主表的关联字段的变化,加载对应的子表数据 */  
184 - watch(  
185 - () => props.${subJoinColumn.javaField},  
186 - async (val) => {  
187 - if (!val) {  
188 - return;  
189 - }  
190 - await nextTick();  
191 - await getList()  
192 - },  
193 - { immediate: true },  
194 - ); 180 +/** 监听主表的关联字段的变化,加载对应的子表数据 */
  181 +watch(
  182 + () => props.${subJoinColumn.javaField},
  183 + async (val) => {
  184 + if (!val) {
  185 + return;
  186 + }
  187 + await nextTick();
  188 + await getList()
  189 + },
  190 + { immediate: true },
  191 +);
195 192
196 #if ($table.templateType == 11) ## erp 193 #if ($table.templateType == 11) ## erp
197 /** 初始化 */ 194 /** 初始化 */
@@ -292,7 +289,7 @@ onMounted(() =&gt; { @@ -292,7 +289,7 @@ onMounted(() =&gt; {
292 <!-- 列表 --> 289 <!-- 列表 -->
293 <ContentWrap title="${table.classComment}"> 290 <ContentWrap title="${table.classComment}">
294 <template #extra> 291 <template #extra>
295 - <TableToolbar 292 + <VbenVxeTableToolbar
296 ref="tableToolbarRef" 293 ref="tableToolbarRef"
297 v-model:hidden-search="hiddenSearchBar" 294 v-model:hidden-search="hiddenSearchBar"
298 > 295 >
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/api/api.ts.vm
1 -import type { PageParam, PageResult } from '@vben/request';  
2 -import type { Dayjs } from 'dayjs';  
3 -  
4 -import { requestClient } from '#/api/request';  
5 -#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}")  
6 -  
7 -export namespace ${simpleClassName}Api {  
8 -## 特殊:主子表专属逻辑  
9 -#foreach ($subTable in $subTables)  
10 - #set ($index = $foreach.count - 1)  
11 - #set ($subSimpleClassName = $subSimpleClassNames.get($index))  
12 - #set ($subColumns = $subColumnsList.get($index))##当前字段数组  
13 - /** ${subTable.classComment}信息 */  
14 - export interface ${subSimpleClassName} {  
15 - #foreach ($column in $subColumns)  
16 - #if ($column.createOperation || $column.updateOperation)  
17 - #if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")  
18 - ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: number; // ${column.columnComment}  
19 - #elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime")  
20 - ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: string | Dayjs; // ${column.columnComment}  
21 - #else  
22 - ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: ${column.javaType.toLowerCase()}; // ${column.columnComment}  
23 - #end  
24 - #end  
25 - #end  
26 - }  
27 -  
28 -#end  
29 - /** ${table.classComment}信息 */  
30 - export interface ${simpleClassName} {  
31 -#foreach ($column in $columns)  
32 -#if ($column.createOperation || $column.updateOperation)  
33 -#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")  
34 - ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: number; // ${column.columnComment}  
35 -#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime")  
36 - ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: string | Dayjs; // ${column.columnComment}  
37 -#else  
38 - ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: ${column.javaType.toLowerCase()}; // ${column.columnComment}  
39 -#end  
40 -#end  
41 -#end  
42 -#if ( $table.templateType == 2 )  
43 - children?: ${simpleClassName}[];  
44 -#end  
45 -## 特殊:主子表专属逻辑  
46 -#if ( $table.templateType == 10 || $table.templateType == 12 )  
47 - #foreach ($subTable in $subTables)  
48 - #set ($index = $foreach.count - 1)  
49 - #set ($subSimpleClassName = $subSimpleClassNames.get($index))  
50 - #if ( $subTable.subJoinMany )  
51 - ${subSimpleClassName.toLowerCase()}s?: ${subSimpleClassName}[]  
52 - #else  
53 - ${subSimpleClassName.toLowerCase()}?: ${subSimpleClassName}  
54 - #end  
55 - #end  
56 -#end  
57 - }  
58 -}  
59 -  
60 -#if ( $table.templateType != 2 )  
61 -/** 查询${table.classComment}分页 */  
62 -export function get${simpleClassName}Page(params: PageParam) {  
63 - return requestClient.get<PageResult<${simpleClassName}Api.${simpleClassName}>>('${baseURL}/page', { params });  
64 -}  
65 -#else  
66 -/** 查询${table.classComment}列表 */  
67 -export function get${simpleClassName}List(params: any) {  
68 - return requestClient.get<${simpleClassName}Api.${simpleClassName}[]>('${baseURL}/list', { params });  
69 -}  
70 -#end  
71 -  
72 -/** 查询${table.classComment}详情 */  
73 -export function get${simpleClassName}(id: number) {  
74 - return requestClient.get<${simpleClassName}Api.${simpleClassName}>(`${baseURL}/get?id=${id}`);  
75 -}  
76 -  
77 -/** 新增${table.classComment} */  
78 -export function create${simpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) {  
79 - return requestClient.post('${baseURL}/create', data);  
80 -}  
81 -  
82 -/** 修改${table.classComment} */  
83 -export function update${simpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) {  
84 - return requestClient.put('${baseURL}/update', data);  
85 -}  
86 -  
87 -/** 删除${table.classComment} */  
88 -export function delete${simpleClassName}(id: number) {  
89 - return requestClient.delete(`${baseURL}/delete?id=${id}`);  
90 -}  
91 -  
92 -#if ( $table.templateType != 2 && $deleteBatchEnable)  
93 -/** 批量删除${table.classComment} */  
94 -export function delete${simpleClassName}List(ids: number[]) {  
95 - return requestClient.delete(`${baseURL}/delete-list?ids=${ids.join(',')}`)  
96 -}  
97 -#end  
98 -  
99 -/** 导出${table.classComment} */  
100 -export function export${simpleClassName}(params: any) {  
101 - return requestClient.download('${baseURL}/export-excel', params);  
102 -}  
103 -  
104 -## 特殊:主子表专属逻辑  
105 -#foreach ($subTable in $subTables)  
106 -#set ($index = $foreach.count - 1)  
107 -#set ($subSimpleClassName = $subSimpleClassNames.get($index))  
108 -#set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段  
109 -#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段  
110 -#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写  
111 -#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index))  
112 -#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))  
113 -#set ($subClassNameVar = $subClassNameVars.get($index))  
114 -  
115 -// ==================== 子表($subTable.classComment) ====================  
116 -  
117 -## 情况一:MASTER_ERP 时,需要分查询页子表  
118 -#if ( $table.templateType == 11 )  
119 -/** 获得${subTable.classComment}分页 */  
120 -export function get${subSimpleClassName}Page(params: PageParam) {  
121 - return requestClient.get<PageResult<${simpleClassName}Api.${subSimpleClassName}>>(`${baseURL}/${subSimpleClassName_strikeCase}/page`, { params });  
122 -}  
123 -## 情况二:非 MASTER_ERP 时,需要列表查询子表  
124 -#else  
125 - #if ( $subTable.subJoinMany )  
126 -/** 获得${subTable.classComment}列表 */  
127 -export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}: number) {  
128 - return requestClient.get<${simpleClassName}Api.${subSimpleClassName}[]>(`${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`);  
129 -}  
130 - #else  
131 -/** 获得${subTable.classComment} */  
132 -export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}: number) {  
133 - return requestClient.get<${simpleClassName}Api.${subSimpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`);  
134 -}  
135 - #end  
136 -#end  
137 -## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作  
138 -#if ( $table.templateType == 11 )  
139 -/** 新增${subTable.classComment} */  
140 -export function create${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) {  
141 - return requestClient.post(`${baseURL}/${subSimpleClassName_strikeCase}/create`, data);  
142 -}  
143 -  
144 -/** 修改${subTable.classComment} */  
145 -export function update${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) {  
146 - return requestClient.put(`${baseURL}/${subSimpleClassName_strikeCase}/update`, data);  
147 -}  
148 -  
149 -/** 删除${subTable.classComment} */  
150 -export function delete${subSimpleClassName}(id: number) {  
151 - return requestClient.delete(`${baseURL}/${subSimpleClassName_strikeCase}/delete?id=${id}`);  
152 -}  
153 -  
154 -#if ($deleteBatchEnable)  
155 -/** 批量删除${subTable.classComment} */  
156 -export function delete${subSimpleClassName}List(ids: number[]) {  
157 - return requestClient.delete(`${baseURL}/${subSimpleClassName_strikeCase}/delete-list?ids=${ids.join(',')}`)  
158 -}  
159 -#end  
160 -  
161 -/** 获得${subTable.classComment} */  
162 -export function get${subSimpleClassName}(id: number) {  
163 - return requestClient.get<${simpleClassName}Api.${subSimpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get?id=${id}`);  
164 -}  
165 -#end  
166 -#end  
167 - 1 +#parse("codegen/vue3_vben5_ele/schema/api/api.ts.vm")
168 \ No newline at end of file 2 \ No newline at end of file
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/data.ts.vm
  1 +#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
1 import type { VbenFormSchema } from '#/adapter/form'; 2 import type { VbenFormSchema } from '#/adapter/form';
2 import type { VxeTableGridOptions } from '#/adapter/vxe-table'; 3 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
3 -import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}'; 4 +import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
4 5
5 -import { z } from '#/adapter/form';  
6 -import {  
7 - DICT_TYPE,  
8 - getDictOptions,  
9 - getRangePickerDefaultProps,  
10 -} from '#/utils'; 6 +import { DICT_TYPE } from '@vben/constants';
  7 +import { getDictOptions } from '@vben/hooks';
11 #if(${table.templateType} == 2)## 树表需要导入这些 8 #if(${table.templateType} == 2)## 树表需要导入这些
12 import { get${simpleClassName}List } from '#/api/${table.moduleName}/${table.businessName}'; 9 import { get${simpleClassName}List } from '#/api/${table.moduleName}/${table.businessName}';
13 import { handleTree } from '@vben/utils'; 10 import { handleTree } from '@vben/utils';
14 #end 11 #end
15 12
  13 +import { getRangePickerDefaultProps } from '#/utils';
  14 +
16 /** 新增/修改的表单 */ 15 /** 新增/修改的表单 */
17 export function useFormSchema(): VbenFormSchema[] { 16 export function useFormSchema(): VbenFormSchema[] {
18 return [ 17 return [
@@ -186,7 +185,7 @@ export function useGridFormSchema(): VbenFormSchema[] { @@ -186,7 +185,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
186 } 185 }
187 186
188 /** 列表的字段 */ 187 /** 列表的字段 */
189 -export function useGridColumns(): VxeTableGridOptions<${simpleClassName}Api.${simpleClassName}>['columns'] { 188 +export function useGridColumns(): VxeTableGridOptions<${apiName}.${simpleClassName}>['columns'] {
190 return [ 189 return [
191 #if ($table.templateType != 2 && $deleteBatchEnable) 190 #if ($table.templateType != 2 && $deleteBatchEnable)
192 { type: 'checkbox', width: 40 }, 191 { type: 'checkbox', width: 40 },
@@ -389,7 +388,7 @@ export function use${subSimpleClassName}GridFormSchema(): VbenFormSchema[] { @@ -389,7 +388,7 @@ export function use${subSimpleClassName}GridFormSchema(): VbenFormSchema[] {
389 } 388 }
390 389
391 /** 列表的字段 */ 390 /** 列表的字段 */
392 -export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>['columns'] { 391 +export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${apiName}.${subSimpleClassName}>['columns'] {
393 return [ 392 return [
394 #if ($table.templateType != 2 && $deleteBatchEnable) 393 #if ($table.templateType != 2 && $deleteBatchEnable)
395 { type: 'checkbox', width: 40 }, 394 { type: 'checkbox', width: 40 },
@@ -426,7 +425,7 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions&lt;${sim @@ -426,7 +425,7 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions&lt;${sim
426 #else 425 #else
427 #if ($subTable.subJoinMany) ## 一对多 426 #if ($subTable.subJoinMany) ## 一对多
428 /** 新增/修改列表的字段 */ 427 /** 新增/修改列表的字段 */
429 - export function use${subSimpleClassName}GridEditColumns(): VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>['columns'] { 428 + export function use${subSimpleClassName}GridEditColumns(): VxeTableGridOptions<${apiName}.${subSimpleClassName}>['columns'] {
430 return [ 429 return [
431 #foreach($column in $subColumns) 430 #foreach($column in $subColumns)
432 #if ($column.createOperation || $column.updateOperation) 431 #if ($column.createOperation || $column.updateOperation)
@@ -602,4 +601,4 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions&lt;${sim @@ -602,4 +601,4 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions&lt;${sim
602 } 601 }
603 #end 602 #end
604 #end 603 #end
605 -#end 604 -#end
  605 +#end
606 \ No newline at end of file 606 \ No newline at end of file
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/form.vue.vm
  1 +#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
1 <script lang="ts" setup> 2 <script lang="ts" setup>
2 -import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}'; 3 +import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
  4 +
  5 +import { computed, ref } from 'vue';
3 6
4 import { useVbenModal } from '@vben/common-ui'; 7 import { useVbenModal } from '@vben/common-ui';
5 -import { message, Tabs, Checkbox, Input, Textarea, Select,RadioGroup,CheckboxGroup, DatePicker } from 'ant-design-vue'; 8 +
  9 +import { message#if ($table.templateType == 11), Tabs#end } from 'ant-design-vue';
  10 +
  11 +import { useVbenForm } from '#/adapter/form';
  12 +import { create${simpleClassName}, get${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
  13 +import { $t } from '#/locales';
6 ## 特殊:主子表专属逻辑 14 ## 特殊:主子表专属逻辑
7 #if ( $table.templateType == 10 || $table.templateType == 12 ) 15 #if ( $table.templateType == 10 || $table.templateType == 12 )
8 #foreach ($subSimpleClassName in $subSimpleClassNames) 16 #foreach ($subSimpleClassName in $subSimpleClassNames)
@@ -12,21 +20,15 @@ import { message, Tabs, Checkbox, Input, Textarea, Select,RadioGroup,CheckboxGro @@ -12,21 +20,15 @@ import { message, Tabs, Checkbox, Input, Textarea, Select,RadioGroup,CheckboxGro
12 #end 20 #end
13 #end 21 #end
14 22
15 -import { computed, ref } from 'vue';  
16 -import { $t } from '#/locales';  
17 -import { useVbenForm } from '#/adapter/form';  
18 -import { get${simpleClassName}, create${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';  
19 -  
20 import { useFormSchema } from '../data'; 23 import { useFormSchema } from '../data';
21 24
22 const emit = defineEmits(['success']); 25 const emit = defineEmits(['success']);
23 -const formData = ref<${simpleClassName}Api.${simpleClassName}>(); 26 +const formData = ref<${apiName}.${simpleClassName}>();
24 const getTitle = computed(() => { 27 const getTitle = computed(() => {
25 return formData.value?.id 28 return formData.value?.id
26 ? $t('ui.actionTitle.edit', ['${table.classComment}']) 29 ? $t('ui.actionTitle.edit', ['${table.classComment}'])
27 : $t('ui.actionTitle.create', ['${table.classComment}']); 30 : $t('ui.actionTitle.create', ['${table.classComment}']);
28 }); 31 });
29 -  
30 ## 特殊:主子表专属逻辑 32 ## 特殊:主子表专属逻辑
31 #if ( $table.templateType == 10 || $table.templateType == 12 ) 33 #if ( $table.templateType == 10 || $table.templateType == 12 )
32 #if ( $subTables && $subTables.size() > 0 ) 34 #if ( $subTables && $subTables.size() > 0 )
@@ -51,7 +53,7 @@ const [Form, formApi] = useVbenForm({ @@ -51,7 +53,7 @@ const [Form, formApi] = useVbenForm({
51 }, 53 },
52 layout: 'horizontal', 54 layout: 'horizontal',
53 schema: useFormSchema(), 55 schema: useFormSchema(),
54 - showDefaultActions: false 56 + showDefaultActions: false,
55 }); 57 });
56 58
57 const [Modal, modalApi] = useVbenModal({ 59 const [Modal, modalApi] = useVbenModal({
@@ -60,8 +62,8 @@ const [Modal, modalApi] = useVbenModal({ @@ -60,8 +62,8 @@ const [Modal, modalApi] = useVbenModal({
60 if (!valid) { 62 if (!valid) {
61 return; 63 return;
62 } 64 }
63 - ## 特殊:主子表专属逻辑  
64 - #if ( $table.templateType == 10 || $table.templateType == 12 ) 65 +## 特殊:主子表专属逻辑
  66 +#if ( $table.templateType == 10 || $table.templateType == 12 )
65 #if ( $subTables && $subTables.size() > 0 ) 67 #if ( $subTables && $subTables.size() > 0 )
66 // 校验子表单 68 // 校验子表单
67 #foreach ($subTable in $subTables) 69 #foreach ($subTable in $subTables)
@@ -78,12 +80,12 @@ const [Modal, modalApi] = useVbenModal({ @@ -78,12 +80,12 @@ const [Modal, modalApi] = useVbenModal({
78 #end 80 #end
79 #end 81 #end
80 #end 82 #end
81 - #end 83 +#end
82 modalApi.lock(); 84 modalApi.lock();
83 // 提交表单 85 // 提交表单
84 - const data = (await formApi.getValues()) as ${simpleClassName}Api.${simpleClassName};  
85 - ## 特殊:主子表专属逻辑  
86 - #if ( $table.templateType == 10 || $table.templateType == 12 ) 86 + const data = (await formApi.getValues()) as ${apiName}.${simpleClassName};
  87 +## 特殊:主子表专属逻辑
  88 +#if ( $table.templateType == 10 || $table.templateType == 12 )
87 #if ( $subTables && $subTables.size() > 0 ) 89 #if ( $subTables && $subTables.size() > 0 )
88 // 拼接子表的数据 90 // 拼接子表的数据
89 #foreach ($subTable in $subTables) 91 #foreach ($subTable in $subTables)
@@ -96,13 +98,13 @@ const [Modal, modalApi] = useVbenModal({ @@ -96,13 +98,13 @@ const [Modal, modalApi] = useVbenModal({
96 #end 98 #end
97 #end 99 #end
98 #end 100 #end
99 - #end 101 +#end
100 try { 102 try {
101 await (formData.value?.id ? update${simpleClassName}(data) : create${simpleClassName}(data)); 103 await (formData.value?.id ? update${simpleClassName}(data) : create${simpleClassName}(data));
102 // 关闭并提示 104 // 关闭并提示
103 await modalApi.close(); 105 await modalApi.close();
104 emit('success'); 106 emit('success');
105 - message.success( $t('ui.actionMessage.operationSuccess') ); 107 + message.success($t('ui.actionMessage.operationSuccess'));
106 } finally { 108 } finally {
107 modalApi.unlock(); 109 modalApi.unlock();
108 } 110 }
@@ -113,21 +115,24 @@ const [Modal, modalApi] = useVbenModal({ @@ -113,21 +115,24 @@ const [Modal, modalApi] = useVbenModal({
113 return; 115 return;
114 } 116 }
115 // 加载数据 117 // 加载数据
116 - let data = modalApi.getData<${simpleClassName}Api.${simpleClassName}>();  
117 - if (!data) { 118 + const data = modalApi.getData<${apiName}.${simpleClassName}>();
  119 + if (!data || !data.id) {
  120 +#if (${table.templateType} == 2)## 树表特有
  121 + // 设置上级
  122 + await formApi.setValues(data);
  123 +#end
118 return; 124 return;
119 } 125 }
120 - if (data.id) {  
121 - modalApi.lock();  
122 - try {  
123 - data = await get${simpleClassName}(data.id);  
124 - } finally {  
125 - modalApi.unlock(); 126 + modalApi.lock();
  127 + try {
  128 + formData.value = await get${simpleClassName}(data.id);
  129 + // 设置到 values
  130 + if (formData.value) {
  131 + await formApi.setValues(formData.value);
126 } 132 }
  133 + } finally {
  134 + modalApi.unlock();
127 } 135 }
128 - // 设置到 values  
129 - formData.value = data;  
130 - await formApi.setValues(formData.value);  
131 }, 136 },
132 }); 137 });
133 </script> 138 </script>
@@ -135,8 +140,8 @@ const [Modal, modalApi] = useVbenModal({ @@ -135,8 +140,8 @@ const [Modal, modalApi] = useVbenModal({
135 <template> 140 <template>
136 <Modal :title="getTitle"> 141 <Modal :title="getTitle">
137 <Form class="mx-4" /> 142 <Form class="mx-4" />
138 - ## 特殊:主子表专属逻辑  
139 - #if ( $table.templateType == 10 || $table.templateType == 12 ) 143 +## 特殊:主子表专属逻辑
  144 +#if ( $table.templateType == 10 || $table.templateType == 12 )
140 <!-- 子表的表单 --> 145 <!-- 子表的表单 -->
141 <Tabs v-model:active-key="subTabsName"> 146 <Tabs v-model:active-key="subTabsName">
142 #foreach ($subTable in $subTables) 147 #foreach ($subTable in $subTables)
@@ -149,6 +154,6 @@ const [Modal, modalApi] = useVbenModal({ @@ -149,6 +154,6 @@ const [Modal, modalApi] = useVbenModal({
149 </Tabs.TabPane> 154 </Tabs.TabPane>
150 #end 155 #end
151 </Tabs> 156 </Tabs>
152 - #end 157 +#end
153 </Modal> 158 </Modal>
154 </template> 159 </template>
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/index.vue.vm
  1 +#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
1 <script lang="ts" setup> 2 <script lang="ts" setup>
2 import type { VxeTableGridOptions } from '#/adapter/vxe-table'; 3 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
3 -import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}'; 4 +import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
4 5
5 -import { Page, useVbenModal } from '@vben/common-ui';  
6 -import { message,Tabs } from 'ant-design-vue';  
7 -import Form from './modules/form.vue'; 6 +import { ref } from 'vue';
8 7
9 -## 特殊:主子表专属逻辑  
10 -#if ( $table.templateType == 11 || $table.templateType == 12 )  
11 - #foreach ($subSimpleClassName in $subSimpleClassNames)  
12 - #set ($index = $foreach.count - 1)  
13 - #set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index))  
14 -import ${subSimpleClassName}List from './modules/${subSimpleClassName_strikeCase}-list.vue'  
15 - #end  
16 -#end 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';
  10 +
  11 +import { message#if ($table.templateType == 11), Tabs#end } from 'ant-design-vue';
17 12
18 -import { ref, computed } from 'vue';  
19 -import { $t } from '#/locales';  
20 import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; 13 import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
21 #if (${table.templateType} == 2)## 树表接口 14 #if (${table.templateType} == 2)## 树表接口
22 -import { get${simpleClassName}List, delete${simpleClassName}, export${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}'; 15 +import {
  16 + delete${simpleClassName},
  17 + export${simpleClassName},
  18 + get${simpleClassName}List,
  19 +} from '#/api/${table.moduleName}/${table.businessName}';
23 #else## 标准表接口 20 #else## 标准表接口
24 -import { get${simpleClassName}Page, delete${simpleClassName},#if ($deleteBatchEnable) delete${simpleClassName}List,#end export${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}'; 21 +import {
  22 + delete${simpleClassName},#if ($deleteBatchEnable)
  23 +
  24 + delete${simpleClassName}List,#end
  25 + export${simpleClassName},
  26 + get${simpleClassName}Page,
  27 +} from '#/api/${table.moduleName}/${table.businessName}';
25 #end 28 #end
26 -import { downloadFileFromBlobPart, isEmpty } from '@vben/utils'; 29 +import { $t } from '#/locales';
27 30
28 import { useGridColumns, useGridFormSchema } from './data'; 31 import { useGridColumns, useGridFormSchema } from './data';
  32 +import Form from './modules/form.vue';
  33 +## 特殊:主子表专属逻辑
  34 +#if ( $table.templateType == 11 || $table.templateType == 12 )
  35 + #foreach ($subSimpleClassName in $subSimpleClassNames)
  36 + #set ($index = $foreach.count - 1)
  37 + #set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index))
  38 +import ${subSimpleClassName}List from './modules/${subSimpleClassName_strikeCase}-list.vue';
  39 + #end
  40 +#end
29 41
30 #if ($table.templateType == 12 || $table.templateType == 11) ## 内嵌和erp情况 42 #if ($table.templateType == 12 || $table.templateType == 11) ## 内嵌和erp情况
31 /** 子表的列表 */ 43 /** 子表的列表 */
32 const subTabsName = ref('$subClassNameVars.get(0)') 44 const subTabsName = ref('$subClassNameVars.get(0)')
33 #if ($table.templateType == 11) 45 #if ($table.templateType == 11)
34 -const select${simpleClassName} = ref<${simpleClassName}Api.${simpleClassName}>(); 46 +const select${simpleClassName} = ref<${apiName}.${simpleClassName}>();
35 #end 47 #end
36 #end 48 #end
37 -  
38 const [FormModal, formModalApi] = useVbenModal({ 49 const [FormModal, formModalApi] = useVbenModal({
39 connectedComponent: Form, 50 connectedComponent: Form,
40 destroyOnClose: true, 51 destroyOnClose: true,
41 }); 52 });
42 -  
43 #if (${table.templateType} == 2)## 树表特有:控制表格展开收缩 53 #if (${table.templateType} == 2)## 树表特有:控制表格展开收缩
  54 +
44 /** 切换树形展开/收缩状态 */ 55 /** 切换树形展开/收缩状态 */
45 const isExpanded = ref(true); 56 const isExpanded = ref(true);
46 -function toggleExpand() { 57 +function handleExpand() {
47 isExpanded.value = !isExpanded.value; 58 isExpanded.value = !isExpanded.value;
48 gridApi.grid.setAllTreeExpand(isExpanded.value); 59 gridApi.grid.setAllTreeExpand(isExpanded.value);
49 } 60 }
50 #end 61 #end
51 62
52 /** 刷新表格 */ 63 /** 刷新表格 */
53 -function onRefresh() { 64 +function handleRefresh() {
54 #if ($table.templateType == 12) ## 内嵌情况 65 #if ($table.templateType == 12) ## 内嵌情况
55 gridApi.reload(); 66 gridApi.reload();
56 #else 67 #else
@@ -60,34 +71,31 @@ function onRefresh() { @@ -60,34 +71,31 @@ function onRefresh() {
60 71
61 /** 创建${table.classComment} */ 72 /** 创建${table.classComment} */
62 function handleCreate() { 73 function handleCreate() {
63 - formModalApi.setData({}).open(); 74 + formModalApi.setData(null).open();
64 } 75 }
65 -  
66 -/** 编辑${table.classComment} */  
67 -function handleEdit(row: ${simpleClassName}Api.${simpleClassName}) {  
68 - formModalApi.setData(row).open();  
69 -}  
70 -  
71 #if (${table.templateType} == 2)## 树表特有:新增下级 76 #if (${table.templateType} == 2)## 树表特有:新增下级
72 -/** 新增下级${table.classComment} */  
73 -function handleAppend(row: ${simpleClassName}Api.${simpleClassName}) { 77 +
  78 +/** 添加下级${table.classComment} */
  79 +function handleAppend(row: ${apiName}.${simpleClassName}) {
74 formModalApi.setData({ ${treeParentColumn.javaField}: row.id }).open(); 80 formModalApi.setData({ ${treeParentColumn.javaField}: row.id }).open();
75 } 81 }
76 #end 82 #end
77 83
  84 +/** 编辑${table.classComment} */
  85 +function handleEdit(row: ${apiName}.${simpleClassName}) {
  86 + formModalApi.setData(row).open();
  87 +}
  88 +
78 /** 删除${table.classComment} */ 89 /** 删除${table.classComment} */
79 -async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) { 90 +async function handleDelete(row: ${apiName}.${simpleClassName}) {
80 const hideLoading = message.loading({ 91 const hideLoading = message.loading({
81 content: $t('ui.actionMessage.deleting', [row.id]), 92 content: $t('ui.actionMessage.deleting', [row.id]),
82 - key: 'action_key_msg', 93 + duration: 0,
83 }); 94 });
84 try { 95 try {
85 - await delete${simpleClassName}(row.id as number);  
86 - message.success({  
87 - content: $t('ui.actionMessage.deleteSuccess', [row.id]),  
88 - key: 'action_key_msg',  
89 - });  
90 - onRefresh(); 96 + await delete${simpleClassName}(row.id!);
  97 + message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
  98 + handleRefresh();
91 } finally { 99 } finally {
92 hideLoading(); 100 hideLoading();
93 } 101 }
@@ -96,30 +104,28 @@ async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) { @@ -96,30 +104,28 @@ async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) {
96 #if ($table.templateType != 2 && $deleteBatchEnable) 104 #if ($table.templateType != 2 && $deleteBatchEnable)
97 /** 批量删除${table.classComment} */ 105 /** 批量删除${table.classComment} */
98 async function handleDeleteBatch() { 106 async function handleDeleteBatch() {
  107 + await confirm($t('ui.actionMessage.deleteBatchConfirm'));
99 const hideLoading = message.loading({ 108 const hideLoading = message.loading({
100 - content: $t('ui.actionMessage.deleting'),  
101 - key: 'action_key_msg', 109 + content: $t('ui.actionMessage.deletingBatch'),
  110 + duration: 0,
102 }); 111 });
103 try { 112 try {
104 await delete${simpleClassName}List(checkedIds.value); 113 await delete${simpleClassName}List(checkedIds.value);
105 checkedIds.value = []; 114 checkedIds.value = [];
106 - message.success({  
107 - content: $t('ui.actionMessage.deleteSuccess'),  
108 - key: 'action_key_msg',  
109 - });  
110 - onRefresh(); 115 + message.success($t('ui.actionMessage.deleteSuccess'));
  116 + handleRefresh();
111 } finally { 117 } finally {
112 hideLoading(); 118 hideLoading();
113 } 119 }
114 } 120 }
115 121
116 -const checkedIds = ref<number[]>([]) 122 +const checkedIds = ref<number[]>([]);
117 function handleRowCheckboxChange({ 123 function handleRowCheckboxChange({
118 records, 124 records,
119 }: { 125 }: {
120 - records: ${simpleClassName}Api.${simpleClassName}[]; 126 + records: ${apiName}.${simpleClassName}[];
121 }) { 127 }) {
122 - checkedIds.value = records.map((item) => item.id); 128 + checkedIds.value = records.map((item) => item.id!);
123 } 129 }
124 #end 130 #end
125 131
@@ -141,20 +147,11 @@ const [Grid, gridApi] = useVbenVxeGrid({ @@ -141,20 +147,11 @@ const [Grid, gridApi] = useVbenVxeGrid({
141 height: 'auto', 147 height: 'auto',
142 #end 148 #end
143 #if (${table.templateType} == 2)## 树表设置 149 #if (${table.templateType} == 2)## 树表设置
144 - treeConfig: {  
145 - parentField: '${treeParentColumn.javaField}',  
146 - rowField: 'id',  
147 - transform: true,  
148 - expandAll: true,  
149 - reserve: true,  
150 - },  
151 - pagerConfig: {  
152 - enabled: false,  
153 - },  
154 -#else## 标准表设置  
155 pagerConfig: { 150 pagerConfig: {
156 - enabled: true, 151 + enabled: false,
157 }, 152 },
  153 +#else## 标准表设置
  154 + keepSource: true,
158 #end 155 #end
159 proxyConfig: { 156 proxyConfig: {
160 ajax: { 157 ajax: {
@@ -184,27 +181,35 @@ const [Grid, gridApi] = useVbenVxeGrid({ @@ -184,27 +181,35 @@ const [Grid, gridApi] = useVbenVxeGrid({
184 refresh: true, 181 refresh: true,
185 search: true, 182 search: true,
186 }, 183 },
187 - } as VxeTableGridOptions<${simpleClassName}Api.${simpleClassName}>,  
188 -#if (${table.templateType} == 11 || $deleteBatchEnable)  
189 - gridEvents:{  
190 - #if(${table.templateType} == 11)  
191 - cellClick: ({ row }: { row: ${simpleClassName}Api.${simpleClassName}}) => { 184 +#if (${table.templateType} == 2)## 树表设置
  185 + treeConfig: {
  186 + parentField: '${treeParentColumn.javaField}',
  187 + rowField: 'id',
  188 + transform: true,
  189 + expandAll: true,
  190 + reserve: true,
  191 + },
  192 +#end
  193 + } as VxeTableGridOptions<${apiName}.${simpleClassName}>,
  194 +#if (${table.templateType} == 11 || (${table.templateType} != 2 && $deleteBatchEnable))
  195 + gridEvents: {
  196 + #if(${table.templateType} == 11)
  197 + cellClick: ({ row }: { row: ${apiName}.${simpleClassName}}) => {
192 select${simpleClassName}.value = row; 198 select${simpleClassName}.value = row;
193 }, 199 },
194 - #end  
195 - #if(${table.templateType} != 2 && $deleteBatchEnable)  
196 - checkboxAll: handleRowCheckboxChange,  
197 - checkboxChange: handleRowCheckboxChange,  
198 - #end  
199 - } 200 + #end
  201 + #if (${table.templateType} != 2 && $deleteBatchEnable)
  202 + checkboxAll: handleRowCheckboxChange,
  203 + checkboxChange: handleRowCheckboxChange,
  204 + #end
  205 + },
200 #end 206 #end
201 }); 207 });
202 </script> 208 </script>
203 209
204 <template> 210 <template>
205 <Page auto-content-height> 211 <Page auto-content-height>
206 - <FormModal @success="onRefresh" />  
207 - 212 + <FormModal @success="handleRefresh" />
208 #if ($table.templateType == 11) ## erp情况 213 #if ($table.templateType == 11) ## erp情况
209 <div> 214 <div>
210 #end 215 #end
@@ -228,13 +233,6 @@ const [Grid, gridApi] = useVbenVxeGrid({ @@ -228,13 +233,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
228 <template #toolbar-tools> 233 <template #toolbar-tools>
229 <TableAction 234 <TableAction
230 :actions="[ 235 :actions="[
231 - #if (${table.templateType} == 2)## 树表特有:展开/收缩按钮  
232 - {  
233 - label: isExpanded ? '收缩' : '展开',  
234 - type: 'primary',  
235 - onClick: toggleExpand,  
236 - },  
237 - #end  
238 { 236 {
239 label: $t('ui.actionTitle.create', ['${table.classComment}']), 237 label: $t('ui.actionTitle.create', ['${table.classComment}']),
240 type: 'primary', 238 type: 'primary',
@@ -242,6 +240,13 @@ const [Grid, gridApi] = useVbenVxeGrid({ @@ -242,6 +240,13 @@ const [Grid, gridApi] = useVbenVxeGrid({
242 auth: ['${table.moduleName}:${simpleClassName_strikeCase}:create'], 240 auth: ['${table.moduleName}:${simpleClassName_strikeCase}:create'],
243 onClick: handleCreate, 241 onClick: handleCreate,
244 }, 242 },
  243 + #if (${table.templateType} == 2)## 树表特有:展开/收缩按钮
  244 + {
  245 + label: isExpanded ? '收缩' : '展开',
  246 + type: 'primary',
  247 + onClick: handleExpand,
  248 + },
  249 + #end
245 { 250 {
246 label: $t('ui.actionTitle.export'), 251 label: $t('ui.actionTitle.export'),
247 type: 'primary', 252 type: 'primary',
@@ -255,8 +260,8 @@ const [Grid, gridApi] = useVbenVxeGrid({ @@ -255,8 +260,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
255 type: 'primary', 260 type: 'primary',
256 danger: true, 261 danger: true,
257 icon: ACTION_ICON.DELETE, 262 icon: ACTION_ICON.DELETE,
258 - disabled: isEmpty(checkedIds),  
259 auth: ['${table.moduleName}:${simpleClassName_strikeCase}:delete'], 263 auth: ['${table.moduleName}:${simpleClassName_strikeCase}:delete'],
  264 + disabled: isEmpty(checkedIds),
260 onClick: handleDeleteBatch, 265 onClick: handleDeleteBatch,
261 }, 266 },
262 #end 267 #end
@@ -297,7 +302,6 @@ const [Grid, gridApi] = useVbenVxeGrid({ @@ -297,7 +302,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
297 /> 302 />
298 </template> 303 </template>
299 </Grid> 304 </Grid>
300 -  
301 #if ($table.templateType == 11) ## erp情况 305 #if ($table.templateType == 11) ## erp情况
302 <!-- 子表的表单 --> 306 <!-- 子表的表单 -->
303 <Tabs v-model:active-key="subTabsName" class="mt-2"> 307 <Tabs v-model:active-key="subTabsName" class="mt-2">
@@ -314,4 +318,4 @@ const [Grid, gridApi] = useVbenVxeGrid({ @@ -314,4 +318,4 @@ const [Grid, gridApi] = useVbenVxeGrid({
314 </div> 318 </div>
315 #end 319 #end
316 </Page> 320 </Page>
317 -</template> 321 -</template>
  322 +</template>
318 \ No newline at end of file 323 \ No newline at end of file
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/form_sub_erp.vue.vm
@@ -2,8 +2,9 @@ @@ -2,8 +2,9 @@
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 ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
5 <script lang="ts" setup> 6 <script lang="ts" setup>
6 - import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}'; 7 + import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
7 8
8 import { useVbenModal } from '@vben/common-ui'; 9 import { useVbenModal } from '@vben/common-ui';
9 import { message } from 'ant-design-vue'; 10 import { message } from 'ant-design-vue';
@@ -16,7 +17,7 @@ @@ -16,7 +17,7 @@
16 import { use${subSimpleClassName}FormSchema } from '../data'; 17 import { use${subSimpleClassName}FormSchema } from '../data';
17 18
18 const emit = defineEmits(['success']); 19 const emit = defineEmits(['success']);
19 - const formData = ref<${simpleClassName}Api.${subSimpleClassName}>(); 20 + const formData = ref<${apiName}.${subSimpleClassName}>();
20 const getTitle = computed(() => { 21 const getTitle = computed(() => {
21 return formData.value?.id 22 return formData.value?.id
22 ? $t('ui.actionTitle.edit', ['${subTable.classComment}']) 23 ? $t('ui.actionTitle.edit', ['${subTable.classComment}'])
@@ -42,10 +43,9 @@ @@ -42,10 +43,9 @@
42 if (!valid) { 43 if (!valid) {
43 return; 44 return;
44 } 45 }
45 -  
46 modalApi.lock(); 46 modalApi.lock();
47 // 提交表单 47 // 提交表单
48 - const data = (await formApi.getValues()) as ${simpleClassName}Api.${subSimpleClassName}; 48 + const data = (await formApi.getValues()) as ${apiName}.${subSimpleClassName};
49 data.${subJoinColumn.javaField} = formData.value?.${subJoinColumn.javaField}; 49 data.${subJoinColumn.javaField} = formData.value?.${subJoinColumn.javaField};
50 try { 50 try {
51 await (formData.value?.id ? update${subSimpleClassName}(data) : create${subSimpleClassName}(data)); 51 await (formData.value?.id ? update${subSimpleClassName}(data) : create${subSimpleClassName}(data));
@@ -62,9 +62,8 @@ @@ -62,9 +62,8 @@
62 formData.value = undefined; 62 formData.value = undefined;
63 return; 63 return;
64 } 64 }
65 -  
66 // 加载数据 65 // 加载数据
67 - let data = modalApi.getData<${simpleClassName}Api.${subSimpleClassName}>(); 66 + let data = modalApi.getData<${apiName}.${subSimpleClassName}>();
68 if (!data) { 67 if (!data) {
69 return; 68 return;
70 } 69 }
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/form_sub_normal.vue.vm
@@ -4,10 +4,12 @@ @@ -4,10 +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 ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
7 <script lang="ts" setup> 8 <script lang="ts" setup>
8 - import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}'; 9 + import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
9 10
10 import { computed, ref, h, onMounted,watch,nextTick } from 'vue'; 11 import { computed, ref, h, onMounted,watch,nextTick } from 'vue';
  12 +
11 import { $t } from '#/locales'; 13 import { $t } from '#/locales';
12 14
13 #if ($subTable.subJoinMany) ## 一对多 15 #if ($subTable.subJoinMany) ## 一对多
@@ -33,10 +35,10 @@ const props = defineProps&lt;{ @@ -33,10 +35,10 @@ const props = defineProps&lt;{
33 function onActionClick({ 35 function onActionClick({
34 code, 36 code,
35 row, 37 row,
36 -}: OnActionClickParams<${simpleClassName}Api.${subSimpleClassName}>) { 38 +}: OnActionClickParams<${apiName}.${subSimpleClassName}>) {
37 switch (code) { 39 switch (code) {
38 case 'delete': { 40 case 'delete': {
39 - onDelete(row); 41 + handleDelete(row);
40 break; 42 break;
41 } 43 }
42 } 44 }
@@ -62,21 +64,21 @@ gridOptions: { @@ -62,21 +64,21 @@ gridOptions: {
62 }); 64 });
63 65
64 /** 添加${subTable.classComment} */ 66 /** 添加${subTable.classComment} */
65 -const onAdd = async () => {  
66 - await gridApi.grid.insertAt({} as ${simpleClassName}Api.${subSimpleClassName}, -1); 67 +const handleAdd = async () => {
  68 + await gridApi.grid.insertAt({} as ${apiName}.${subSimpleClassName}, -1);
67 } 69 }
68 70
69 /** 删除${subTable.classComment} */ 71 /** 删除${subTable.classComment} */
70 -const onDelete = async (row: ${simpleClassName}Api.${subSimpleClassName}) => { 72 +const handleDelete = async (row: ${apiName}.${subSimpleClassName}) => {
71 await gridApi.grid.remove(row); 73 await gridApi.grid.remove(row);
72 } 74 }
73 75
74 /** 提供获取表格数据的方法供父组件调用 */ 76 /** 提供获取表格数据的方法供父组件调用 */
75 defineExpose({ 77 defineExpose({
76 - getData: (): ${simpleClassName}Api.${subSimpleClassName}[] => {  
77 - const data = gridApi.grid.getData() as ${simpleClassName}Api.${subSimpleClassName}[];  
78 - const removeRecords = gridApi.grid.getRemoveRecords() as ${simpleClassName}Api.${subSimpleClassName}[];  
79 - const insertRecords = gridApi.grid.getInsertRecords() as ${simpleClassName}Api.${subSimpleClassName}[]; 78 + getData: (): ${apiName}.${subSimpleClassName}[] => {
  79 + const data = gridApi.grid.getData() as ${apiName}.${subSimpleClassName}[];
  80 + const removeRecords = gridApi.grid.getRemoveRecords() as ${apiName}.${subSimpleClassName}[];
  81 + const insertRecords = gridApi.grid.getInsertRecords() as ${apiName}.${subSimpleClassName}[];
80 return data 82 return data
81 .filter((row) => !removeRecords.some((removed) => removed.id === row.id)) 83 .filter((row) => !removeRecords.some((removed) => removed.id === row.id))
82 .concat(insertRecords.map((row: any) => ({ ...row, id: undefined }))); 84 .concat(insertRecords.map((row: any) => ({ ...row, id: undefined })));
@@ -186,7 +188,7 @@ watch( @@ -186,7 +188,7 @@ watch(
186 #end 188 #end
187 </Grid> 189 </Grid>
188 <div class="flex justify-center -mt-4"> 190 <div class="flex justify-center -mt-4">
189 - <Button :icon="h(Plus)" type="primary" ghost @click="onAdd" v-access:code="['${subTable.moduleName}:${simpleClassName_strikeCase}:create']"> 191 + <Button :icon="h(Plus)" type="primary" ghost @click="handleAdd" v-access:code="['${subTable.moduleName}:${simpleClassName_strikeCase}:create']">
190 {{ $t('ui.actionTitle.create', ['${subTable.classComment}']) }} 192 {{ $t('ui.actionTitle.create', ['${subTable.classComment}']) }}
191 </Button> 193 </Button>
192 </div> 194 </div>
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/list_sub_erp.vue.vm
@@ -5,14 +5,15 @@ @@ -5,14 +5,15 @@
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 ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
8 <script lang="ts" setup> 9 <script lang="ts" setup>
9 import type { VxeTableGridOptions } from '#/adapter/vxe-table'; 10 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
10 -import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}'; 11 +import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
11 12
12 #if ($table.templateType == 11) ## erp 13 #if ($table.templateType == 11) ## erp
13 import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue' 14 import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue'
14 #end 15 #end
15 -import { useVbenModal } from '@vben/common-ui'; 16 +import { confirm, useVbenModal } from '@vben/common-ui';
16 import { message } from 'ant-design-vue'; 17 import { message } from 'ant-design-vue';
17 import { ref, computed, nextTick,watch } from 'vue'; 18 import { ref, computed, nextTick,watch } from 'vue';
18 import { $t } from '#/locales'; 19 import { $t } from '#/locales';
@@ -51,23 +52,20 @@ function handleCreate() { @@ -51,23 +52,20 @@ function handleCreate() {
51 } 52 }
52 53
53 /** 编辑${subTable.classComment} */ 54 /** 编辑${subTable.classComment} */
54 -function handleEdit(row: ${simpleClassName}Api.${subSimpleClassName}) { 55 +function handleEdit(row: ${apiName}.${subSimpleClassName}) {
55 formModalApi.setData(row).open(); 56 formModalApi.setData(row).open();
56 } 57 }
57 58
58 /** 删除${subTable.classComment} */ 59 /** 删除${subTable.classComment} */
59 -async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) { 60 +async function handleDelete(row: ${apiName}.${subSimpleClassName}) {
60 const hideLoading = message.loading({ 61 const hideLoading = message.loading({
61 content: $t('ui.actionMessage.deleting', [row.id]), 62 content: $t('ui.actionMessage.deleting', [row.id]),
62 - key: 'action_process_msg', 63 + duration: 0,
63 }); 64 });
64 try { 65 try {
65 - await delete${subSimpleClassName}(row.id as number);  
66 - message.success({  
67 - content: $t('ui.actionMessage.deleteSuccess', [row.id]),  
68 - key: 'action_key_msg',  
69 - });  
70 - onRefresh(); 66 + await delete${subSimpleClassName}(row.id!);
  67 + message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
  68 + handleRefresh();
71 } finally { 69 } finally {
72 hideLoading(); 70 hideLoading();
73 } 71 }
@@ -76,18 +74,16 @@ async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) { @@ -76,18 +74,16 @@ async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
76 #if ($deleteBatchEnable) 74 #if ($deleteBatchEnable)
77 /** 批量删除${subTable.classComment} */ 75 /** 批量删除${subTable.classComment} */
78 async function handleDeleteBatch() { 76 async function handleDeleteBatch() {
  77 + await confirm($t('ui.actionMessage.deleteBatchConfirm'));
79 const hideLoading = message.loading({ 78 const hideLoading = message.loading({
80 - content: $t('ui.actionMessage.deleting'),  
81 - key: 'action_key_msg', 79 + content: $t('ui.actionMessage.deletingBatch'),
  80 + duration: 0,
82 }); 81 });
83 try { 82 try {
84 await delete${subSimpleClassName}List(checkedIds.value); 83 await delete${subSimpleClassName}List(checkedIds.value);
85 checkedIds.value = []; 84 checkedIds.value = [];
86 - message.success({  
87 - content: $t('ui.actionMessage.deleteSuccess', [row.id]),  
88 - key: 'action_key_msg',  
89 - });  
90 - onRefresh(); 85 + message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
  86 + handleRefresh();
91 } finally { 87 } finally {
92 hideLoading(); 88 hideLoading();
93 } 89 }
@@ -97,9 +93,9 @@ const checkedIds = ref&lt;number[]&gt;([]) @@ -97,9 +93,9 @@ const checkedIds = ref&lt;number[]&gt;([])
97 function handleRowCheckboxChange({ 93 function handleRowCheckboxChange({
98 records, 94 records,
99 }: { 95 }: {
100 - records: ${simpleClassName}Api.${subSimpleClassName}[]; 96 + records: ${apiName}.${subSimpleClassName}[];
101 }) { 97 }) {
102 - checkedIds.value = records.map((item) => item.id); 98 + checkedIds.value = records.map((item) => item.id!);
103 } 99 }
104 #end 100 #end
105 101
@@ -149,7 +145,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ @@ -149,7 +145,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
149 keyField: 'id', 145 keyField: 'id',
150 isHover: true, 146 isHover: true,
151 }, 147 },
152 - } as VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>, 148 + } as VxeTableGridOptions<${apiName}.${subSimpleClassName}>,
153 #if (${table.templateType} == 11 && $deleteBatchEnable) 149 #if (${table.templateType} == 11 && $deleteBatchEnable)
154 gridEvents:{ 150 gridEvents:{
155 checkboxAll: handleRowCheckboxChange, 151 checkboxAll: handleRowCheckboxChange,
@@ -159,7 +155,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ @@ -159,7 +155,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
159 }); 155 });
160 156
161 /** 刷新表格 */ 157 /** 刷新表格 */
162 -async function onRefresh() { 158 +async function handleRefresh() {
163 #if ($table.templateType == 11) ## erp 159 #if ($table.templateType == 11) ## erp
164 await gridApi.query(); 160 await gridApi.query();
165 #else 161 #else
@@ -179,7 +175,7 @@ watch( @@ -179,7 +175,7 @@ watch(
179 return; 175 return;
180 } 176 }
181 await nextTick(); 177 await nextTick();
182 - await onRefresh() 178 + await handleRefresh()
183 }, 179 },
184 { immediate: true }, 180 { immediate: true },
185 ); 181 );
@@ -187,7 +183,7 @@ watch( @@ -187,7 +183,7 @@ watch(
187 183
188 <template> 184 <template>
189 #if ($table.templateType == 11) ## erp 185 #if ($table.templateType == 11) ## erp
190 - <FormModal @success="onRefresh" /> 186 + <FormModal @success="handleRefresh" />
191 <Grid table-title="${subTable.classComment}列表"> 187 <Grid table-title="${subTable.classComment}列表">
192 <template #toolbar-tools> 188 <template #toolbar-tools>
193 <TableAction 189 <TableAction
yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/general/api/api.ts.vm
@@ -98,7 +98,7 @@ export function delete${simpleClassName}List(ids: number[]) { @@ -98,7 +98,7 @@ export function delete${simpleClassName}List(ids: number[]) {
98 98
99 /** 导出${table.classComment} */ 99 /** 导出${table.classComment} */
100 export function export${simpleClassName}(params: any) { 100 export function export${simpleClassName}(params: any) {
101 - return requestClient.download('${baseURL}/export-excel', params); 101 + return requestClient.download('${baseURL}/export-excel', { params });
102 } 102 }
103 103
104 ## 特殊:主子表专属逻辑 104 ## 特殊:主子表专属逻辑
yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/general/views/form.vue.vm
@@ -3,10 +3,11 @@ import type { ${simpleClassName}Api } from &#39;#/api/${table.moduleName}/${simpleCl @@ -3,10 +3,11 @@ import type { ${simpleClassName}Api } from &#39;#/api/${table.moduleName}/${simpleCl
3 import type { FormRules } from 'element-plus'; 3 import type { FormRules } from 'element-plus';
4 4
5 import { useVbenModal } from '@vben/common-ui'; 5 import { useVbenModal } from '@vben/common-ui';
  6 +import { DICT_TYPE } from '@vben/constants';
  7 +import { getDictOptions } from '@vben/hooks';
6 import { Tinymce as RichTextarea } from '#/components/tinymce'; 8 import { Tinymce as RichTextarea } from '#/components/tinymce';
7 import { ImageUpload, FileUpload } from "#/components/upload"; 9 import { ImageUpload, FileUpload } from "#/components/upload";
8 import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElSelect, ElOption, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElDatePicker, ElTreeSelect } from 'element-plus'; 10 import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElSelect, ElOption, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElDatePicker, ElTreeSelect } from 'element-plus';
9 -import { DICT_TYPE, getDictOptions } from '#/utils';  
10 #if($table.templateType == 2)## 树表需要导入这些 11 #if($table.templateType == 2)## 树表需要导入这些
11 import { get${simpleClassName}List } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}'; 12 import { get${simpleClassName}List } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
12 import { handleTree } from '@vben/utils' 13 import { handleTree } from '@vben/utils'
yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/general/views/index.vue.vm
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}'; 2 import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
3 -import type { VxeTableInstance } from '#/adapter/vxe-table'; 3 +
  4 +import { ref, h, reactive, onMounted, nextTick } from 'vue';
4 5
5 import { Page, useVbenModal } from '@vben/common-ui'; 6 import { Page, useVbenModal } from '@vben/common-ui';
6 -import { cloneDeep, formatDateTime } from '@vben/utils'; 7 +import { DICT_TYPE } from '@vben/constants';
  8 +import { getDictOptions } from '@vben/hooks';
  9 +import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
  10 +import { cloneDeep, downloadFileFromBlobPart, formatDateTime } from '@vben/utils';
7 import { ElButton, ElMessage, ElLoading, ElTabs, ElTabPane, ElPagination, ElForm, ElFormItem, ElDatePicker, ElSelect, ElOption, ElInput } from 'element-plus'; 11 import { ElButton, ElMessage, ElLoading, ElTabs, ElTabPane, ElPagination, ElForm, ElFormItem, ElDatePicker, ElSelect, ElOption, ElInput } from 'element-plus';
8 -import { DictTag } from '#/components/dict-tag';  
9 -import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';  
10 import ${simpleClassName}Form from './modules/form.vue'; 12 import ${simpleClassName}Form from './modules/form.vue';
11 import { Download, Plus, RefreshCw, Search, Trash2 } from '@vben/icons'; 13 import { Download, Plus, RefreshCw, Search, Trash2 } from '@vben/icons';
12 import { ContentWrap } from '#/components/content-wrap'; 14 import { ContentWrap } from '#/components/content-wrap';
13 import { VxeColumn, VxeTable } from '#/adapter/vxe-table'; 15 import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
14 -import { TableToolbar } from '#/components/table-toolbar';  
15 -import { useTableToolbar } from '#/hooks';  
16 16
17 ## 特殊:主子表专属逻辑 17 ## 特殊:主子表专属逻辑
18 #if ( $table.templateType == 11 || $table.templateType == 12 ) 18 #if ( $table.templateType == 11 || $table.templateType == 12 )
@@ -23,7 +23,6 @@ import { useTableToolbar } from &#39;#/hooks&#39;; @@ -23,7 +23,6 @@ import { useTableToolbar } from &#39;#/hooks&#39;;
23 #end 23 #end
24 #end 24 #end
25 25
26 -import { ref, h, reactive,onMounted,nextTick } from 'vue';  
27 import { $t } from '#/locales'; 26 import { $t } from '#/locales';
28 #if (${table.templateType} == 2)## 树表接口 27 #if (${table.templateType} == 2)## 树表接口
29 import { handleTree,isEmpty } from '@vben/utils' 28 import { handleTree,isEmpty } from '@vben/utils'
@@ -104,7 +103,7 @@ const getList = async () =&gt; { @@ -104,7 +103,7 @@ const getList = async () =&gt; {
104 } 103 }
105 104
106 /** 搜索按钮操作 */ 105 /** 搜索按钮操作 */
107 -const handleQuery = () => { 106 +function handleQuery() {
108 #if ( $table.templateType != 2 ) 107 #if ( $table.templateType != 2 )
109 queryParams.pageNo = 1 108 queryParams.pageNo = 1
110 #end 109 #end
@@ -112,7 +111,7 @@ const handleQuery = () =&gt; { @@ -112,7 +111,7 @@ const handleQuery = () =&gt; {
112 } 111 }
113 112
114 /** 重置按钮操作 */ 113 /** 重置按钮操作 */
115 -const resetQuery = () => { 114 +function resetQuery() {
116 queryFormRef.value.resetFields() 115 queryFormRef.value.resetFields()
117 handleQuery() 116 handleQuery()
118 } 117 }
@@ -124,7 +123,7 @@ const [FormModal, formModalApi] = useVbenModal({ @@ -124,7 +123,7 @@ const [FormModal, formModalApi] = useVbenModal({
124 123
125 /** 创建${table.classComment} */ 124 /** 创建${table.classComment} */
126 function handleCreate() { 125 function handleCreate() {
127 - formModalApi.setData({}).open(); 126 + formModalApi.setData(null).open();
128 } 127 }
129 128
130 /** 编辑${table.classComment} */ 129 /** 编辑${table.classComment} */
@@ -143,7 +142,6 @@ function handleAppend(row: ${simpleClassName}Api.${simpleClassName}) { @@ -143,7 +142,6 @@ function handleAppend(row: ${simpleClassName}Api.${simpleClassName}) {
143 async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) { 142 async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) {
144 const loadingInstance = ElLoading.service({ 143 const loadingInstance = ElLoading.service({
145 text: $t('ui.actionMessage.deleting', [row.id]), 144 text: $t('ui.actionMessage.deleting', [row.id]),
146 - background: 'rgba(0, 0, 0, 0.7)',  
147 }); 145 });
148 try { 146 try {
149 await delete${simpleClassName}(row.id as number); 147 await delete${simpleClassName}(row.id as number);
@@ -159,7 +157,6 @@ async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) { @@ -159,7 +157,6 @@ async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) {
159 async function handleDeleteBatch() { 157 async function handleDeleteBatch() {
160 const loadingInstance = ElLoading.service({ 158 const loadingInstance = ElLoading.service({
161 text: $t('ui.actionMessage.deleting'), 159 text: $t('ui.actionMessage.deleting'),
162 - background: 'rgba(0, 0, 0, 0.7)',  
163 }); 160 });
164 try { 161 try {
165 await delete${simpleClassName}List(checkedIds.value); 162 await delete${simpleClassName}List(checkedIds.value);
@@ -177,12 +174,12 @@ function handleRowCheckboxChange({ @@ -177,12 +174,12 @@ function handleRowCheckboxChange({
177 }: { 174 }: {
178 records: ${simpleClassName}Api.${simpleClassName}[]; 175 records: ${simpleClassName}Api.${simpleClassName}[];
179 }) { 176 }) {
180 - checkedIds.value = records.map((item) => item.id); 177 + checkedIds.value = records.map((item) => item.id!);
181 } 178 }
182 #end 179 #end
183 180
184 /** 导出表格 */ 181 /** 导出表格 */
185 -async function onExport() { 182 +async function handleExport() {
186 try { 183 try {
187 exportLoading.value = true; 184 exportLoading.value = true;
188 const data = await export${simpleClassName}(queryParams); 185 const data = await export${simpleClassName}(queryParams);
@@ -195,7 +192,7 @@ try { @@ -195,7 +192,7 @@ try {
195 #if (${table.templateType} == 2) 192 #if (${table.templateType} == 2)
196 /** 切换树形展开/收缩状态 */ 193 /** 切换树形展开/收缩状态 */
197 const isExpanded = ref(true); 194 const isExpanded = ref(true);
198 -function toggleExpand() { 195 +function handleExpand() {
199 isExpanded.value = !isExpanded.value; 196 isExpanded.value = !isExpanded.value;
200 tableRef.value?.setAllTreeExpand(isExpanded.value); 197 tableRef.value?.setAllTreeExpand(isExpanded.value);
201 } 198 }
@@ -306,7 +303,7 @@ onMounted(() =&gt; { @@ -306,7 +303,7 @@ onMounted(() =&gt; {
306 v-model:hidden-search="hiddenSearchBar" 303 v-model:hidden-search="hiddenSearchBar"
307 > 304 >
308 #if (${table.templateType} == 2) 305 #if (${table.templateType} == 2)
309 - <el-button @click="toggleExpand" class="mr-2"> 306 + <el-button @click="handleExpand" class="mr-2">
310 {{ isExpanded ? '收缩' : '展开' }} 307 {{ isExpanded ? '收缩' : '展开' }}
311 </el-button> 308 </el-button>
312 #end 309 #end
@@ -324,7 +321,7 @@ onMounted(() =&gt; { @@ -324,7 +321,7 @@ onMounted(() =&gt; {
324 type="primary" 321 type="primary"
325 class="ml-2" 322 class="ml-2"
326 :loading="exportLoading" 323 :loading="exportLoading"
327 - @click="onExport" 324 + @click="handleExport"
328 v-access:code="['${permissionPrefix}:export']" 325 v-access:code="['${permissionPrefix}:export']"
329 > 326 >
330 {{ $t('ui.actionTitle.export') }} 327 {{ $t('ui.actionTitle.export') }}
yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/general/views/modules/form_sub_erp.vue.vm
@@ -6,13 +6,15 @@ @@ -6,13 +6,15 @@
6 import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}'; 6 import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
7 import type { FormRules } from 'element-plus'; 7 import type { FormRules } from 'element-plus';
8 8
  9 + import { computed, ref, reactive } from 'vue';
  10 +
9 import { useVbenModal } from '@vben/common-ui'; 11 import { useVbenModal } from '@vben/common-ui';
  12 + import { DICT_TYPE } from '@vben/constants';
  13 + import { getDictOptions } from '@vben/hooks';
10 import { Tinymce as RichTextarea } from '#/components/tinymce'; 14 import { Tinymce as RichTextarea } from '#/components/tinymce';
11 import { ImageUpload, FileUpload } from "#/components/upload"; 15 import { ImageUpload, FileUpload } from "#/components/upload";
12 import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElSelect, ElOption, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElDatePicker, ElTreeSelect } from 'element-plus'; 16 import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElSelect, ElOption, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElDatePicker, ElTreeSelect } from 'element-plus';
13 - import { DICT_TYPE, getDictOptions } from '#/utils';  
14 17
15 - import { computed, ref, reactive } from 'vue';  
16 import { $t } from '#/locales'; 18 import { $t } from '#/locales';
17 19
18 import { get${subSimpleClassName}, create${subSimpleClassName}, update${subSimpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}'; 20 import { get${subSimpleClassName}, create${subSimpleClassName}, update${subSimpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/general/views/modules/form_sub_normal.vue.vm
@@ -5,12 +5,15 @@ @@ -5,12 +5,15 @@
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 <script lang="ts" setup> 7 <script lang="ts" setup>
8 - import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}'; 8 +import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
9 9
10 - import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElButton, ElSelect, ElOption, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElDatePicker } from 'element-plus';  
11 - import { computed, ref, reactive, h, onMounted,watch,nextTick } from 'vue';  
12 - import { $t } from '#/locales';  
13 - import { DICT_TYPE, getDictOptions } from '#/utils'; 10 +import { computed, ref, reactive, h, onMounted,watch,nextTick } from 'vue';
  11 +
  12 +import { DICT_TYPE } from '@vben/constants';
  13 +import { getDictOptions } from '@vben/hooks';
  14 +
  15 +import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElButton, ElSelect, ElOption, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElDatePicker } from 'element-plus';
  16 +import { $t } from '#/locales';
14 17
15 #if ($subTable.subJoinMany) ## 一对多 18 #if ($subTable.subJoinMany) ## 一对多
16 import type { VxeTableInstance } from '#/adapter/vxe-table'; 19 import type { VxeTableInstance } from '#/adapter/vxe-table';
@@ -31,12 +34,12 @@ const props = defineProps&lt;{ @@ -31,12 +34,12 @@ const props = defineProps&lt;{
31 const list = ref<${simpleClassName}Api.${subSimpleClassName}[]>([]) // 列表的数据 34 const list = ref<${simpleClassName}Api.${subSimpleClassName}[]>([]) // 列表的数据
32 const tableRef = ref<VxeTableInstance>(); 35 const tableRef = ref<VxeTableInstance>();
33 /** 添加${subTable.classComment} */ 36 /** 添加${subTable.classComment} */
34 -const onAdd = async () => { 37 +async function handleAdd() {
35 await tableRef.value?.insertAt({} as ${simpleClassName}Api.${subSimpleClassName}, -1); 38 await tableRef.value?.insertAt({} as ${simpleClassName}Api.${subSimpleClassName}, -1);
36 } 39 }
37 40
38 /** 删除${subTable.classComment} */ 41 /** 删除${subTable.classComment} */
39 -const onDelete = async (row: ${simpleClassName}Api.${subSimpleClassName}) => { 42 +async function onDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
40 await tableRef.value?.remove(row); 43 await tableRef.value?.remove(row);
41 } 44 }
42 45
@@ -46,9 +49,11 @@ defineExpose({ @@ -46,9 +49,11 @@ defineExpose({
46 const data = list.value as ${simpleClassName}Api.${subSimpleClassName}[]; 49 const data = list.value as ${simpleClassName}Api.${subSimpleClassName}[];
47 const removeRecords = tableRef.value?.getRemoveRecords() as ${simpleClassName}Api.${subSimpleClassName}[]; 50 const removeRecords = tableRef.value?.getRemoveRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
48 const insertRecords = tableRef.value?.getInsertRecords() as ${simpleClassName}Api.${subSimpleClassName}[]; 51 const insertRecords = tableRef.value?.getInsertRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
49 - return data  
50 - .filter((row) => !removeRecords.some((removed) => removed.id === row.id))  
51 - ?.concat(insertRecords.map((row: any) => ({ ...row, id: undefined }))); 52 + return [
  53 + ...data.filter(
  54 + (row) => !removeRecords.some((removed) => removed.id === row.id),
  55 + ),
  56 + ...insertRecords.map((row: any) => ({ ...row, id: undefined })),
52 }, 57 },
53 }); 58 });
54 59
@@ -227,7 +232,7 @@ watch( @@ -227,7 +232,7 @@ watch(
227 </vxe-column> 232 </vxe-column>
228 </vxe-table> 233 </vxe-table>
229 <div class="flex justify-center mt-4"> 234 <div class="flex justify-center mt-4">
230 - <el-button :icon="h(Plus)" type="primary" plain @click="onAdd" v-access:code="['${permissionPrefix}:create']"> 235 + <el-button :icon="h(Plus)" type="primary" plain @click="handleAdd" v-access:code="['${permissionPrefix}:create']">
231 {{ $t('ui.actionTitle.create', ['${subTable.classComment}']) }} 236 {{ $t('ui.actionTitle.create', ['${subTable.classComment}']) }}
232 </el-button> 237 </el-button>
233 </div> 238 </div>
yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/general/views/modules/list_sub_erp.vue.vm
@@ -9,23 +9,25 @@ @@ -9,23 +9,25 @@
9 import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}'; 9 import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
10 import type { VxeTableInstance } from '#/adapter/vxe-table'; 10 import type { VxeTableInstance } from '#/adapter/vxe-table';
11 11
  12 + import { reactive,ref, h, nextTick,watch,onMounted } from 'vue';
  13 +
  14 + import { DICT_TYPE } from '@vben/constants';
  15 + import { getDictOptions } from '@vben/hooks';
  16 +
12 import { DictTag } from '#/components/dict-tag'; 17 import { DictTag } from '#/components/dict-tag';
13 - import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils'; 18 + import { getRangePickerDefaultProps } from '#/utils';
14 import { VxeColumn, VxeTable } from '#/adapter/vxe-table'; 19 import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
15 - import { reactive,ref, h, nextTick,watch,onMounted } from 'vue';  
16 - import { cloneDeep, formatDateTime } from '@vben/utils';  
17 import { ContentWrap } from '#/components/content-wrap'; 20 import { ContentWrap } from '#/components/content-wrap';
18 21
19 #if ($table.templateType == 11) ## erp 22 #if ($table.templateType == 11) ## erp
20 import { useVbenModal } from '@vben/common-ui'; 23 import { useVbenModal } from '@vben/common-ui';
  24 + import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
21 import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue' 25 import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue'
22 import { Tinymce as RichTextarea } from '#/components/tinymce'; 26 import { Tinymce as RichTextarea } from '#/components/tinymce';
23 import { ImageUpload, FileUpload } from "#/components/upload"; 27 import { ImageUpload, FileUpload } from "#/components/upload";
24 import { ElMessage, ElLoading, ElButton, ElTabs, ElTabPane, ElPagination, ElForm, ElFormItem, ElDatePicker, ElSelect, ElOption, ElInput } from 'element-plus'; 28 import { ElMessage, ElLoading, ElButton, ElTabs, ElTabPane, ElPagination, ElForm, ElFormItem, ElDatePicker, ElSelect, ElOption, ElInput } from 'element-plus';
25 import { Plus, Trash2 } from '@vben/icons'; 29 import { Plus, Trash2 } from '@vben/icons';
26 import { $t } from '#/locales'; 30 import { $t } from '#/locales';
27 - import { TableToolbar } from '#/components/table-toolbar';  
28 - import { useTableToolbar } from '#/hooks';  
29 #end 31 #end
30 32
31 #if ($table.templateType == 11) ## erp 33 #if ($table.templateType == 11) ## erp
@@ -67,7 +69,6 @@ function handleEdit(row: ${simpleClassName}Api.${subSimpleClassName}) { @@ -67,7 +69,6 @@ function handleEdit(row: ${simpleClassName}Api.${subSimpleClassName}) {
67 async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) { 69 async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
68 const loadingInstance = ElLoading.service({ 70 const loadingInstance = ElLoading.service({
69 text: $t('ui.actionMessage.deleting', [row.id]), 71 text: $t('ui.actionMessage.deleting', [row.id]),
70 - background: 'rgba(0, 0, 0, 0.7)',  
71 }); 72 });
72 try { 73 try {
73 await delete${subSimpleClassName}(row.id as number); 74 await delete${subSimpleClassName}(row.id as number);
@@ -83,7 +84,6 @@ async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) { @@ -83,7 +84,6 @@ async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
83 async function handleDeleteBatch() { 84 async function handleDeleteBatch() {
84 const loadingInstance = ElLoading.service({ 85 const loadingInstance = ElLoading.service({
85 text: $t('ui.actionMessage.deleting'), 86 text: $t('ui.actionMessage.deleting'),
86 - background: 'rgba(0, 0, 0, 0.7)',  
87 }); 87 });
88 try { 88 try {
89 await delete${subSimpleClassName}List(checkedIds.value); 89 await delete${subSimpleClassName}List(checkedIds.value);
@@ -101,7 +101,7 @@ function handleRowCheckboxChange({ @@ -101,7 +101,7 @@ function handleRowCheckboxChange({
101 }: { 101 }: {
102 records: ${simpleClassName}Api.${subSimpleClassName}[]; 102 records: ${simpleClassName}Api.${subSimpleClassName}[];
103 }) { 103 }) {
104 - checkedIds.value = records.map((item) => item.id); 104 + checkedIds.value = records.map((item) => item.id!);
105 } 105 }
106 #end 106 #end
107 #end 107 #end
@@ -129,64 +129,64 @@ function handleRowCheckboxChange({ @@ -129,64 +129,64 @@ function handleRowCheckboxChange({
129 }) 129 })
130 130
131 /** 搜索按钮操作 */ 131 /** 搜索按钮操作 */
132 -const handleQuery = () => { 132 +function handleQuery() {
133 queryParams.pageNo = 1 133 queryParams.pageNo = 1
134 getList() 134 getList()
135 } 135 }
136 136
137 /** 重置按钮操作 */ 137 /** 重置按钮操作 */
138 -const resetQuery = () => { 138 +function resetQuery() {
139 queryFormRef.value.resetFields() 139 queryFormRef.value.resetFields()
140 handleQuery() 140 handleQuery()
141 } 141 }
142 #end 142 #end
143 - /** 查询列表 */  
144 - const getList = async () => {  
145 - loading.value = true  
146 - try {  
147 - if (!props.${subJoinColumn.javaField}){  
148 - return []  
149 - }  
150 - ## 特殊:树表专属逻辑(树不需要分页接口)  
151 - #if ($table.templateType == 11) ## erp  
152 - const params = cloneDeep(queryParams) as any;  
153 - #foreach ($column in $columns)  
154 - #if ($column.listOperation)  
155 - #if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")  
156 - if (params.${column.javaField} && Array.isArray(params.${column.javaField})) {  
157 - params.${column.javaField} = (params.${column.javaField} as string[]).join(',');  
158 - }  
159 - #end  
160 - #end  
161 - #end  
162 - params.${subJoinColumn.javaField} = props.${subJoinColumn.javaField};  
163 - const data = await get${subSimpleClassName}Page(params)  
164 - list.value = data.list  
165 - total.value = data.total  
166 - #else  
167 - #if ($subTable.subJoinMany) ## 一对多  
168 - list.value = await get${subSimpleClassName}ListBy${SubJoinColumnName}(props.${subJoinColumn.javaField}!);  
169 - #else  
170 - list.value = [await get${subSimpleClassName}By${SubJoinColumnName}(props.${subJoinColumn.javaField}!)];  
171 - #end  
172 - #end  
173 - } finally {  
174 - loading.value = false 143 +/** 查询列表 */
  144 +async function getList() {
  145 + loading.value = true
  146 + try {
  147 + if (!props.${subJoinColumn.javaField}){
  148 + return []
175 } 149 }
  150 + ## 特殊:树表专属逻辑(树不需要分页接口)
  151 + #if ($table.templateType == 11) ## erp
  152 + const params = cloneDeep(queryParams) as any;
  153 + #foreach ($column in $columns)
  154 + #if ($column.listOperation)
  155 + #if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")
  156 + if (params.${column.javaField} && Array.isArray(params.${column.javaField})) {
  157 + params.${column.javaField} = (params.${column.javaField} as string[]).join(',');
  158 + }
  159 + #end
  160 + #end
  161 + #end
  162 + params.${subJoinColumn.javaField} = props.${subJoinColumn.javaField};
  163 + const data = await get${subSimpleClassName}Page(params)
  164 + list.value = data.list
  165 + total.value = data.total
  166 + #else
  167 + #if ($subTable.subJoinMany) ## 一对多
  168 + list.value = await get${subSimpleClassName}ListBy${SubJoinColumnName}(props.${subJoinColumn.javaField}!);
  169 + #else
  170 + list.value = [await get${subSimpleClassName}By${SubJoinColumnName}(props.${subJoinColumn.javaField}!)];
  171 + #end
  172 + #end
  173 + } finally {
  174 + loading.value = false
176 } 175 }
  176 +}
177 177
178 - /** 监听主表的关联字段的变化,加载对应的子表数据 */  
179 - watch(  
180 - () => props.${subJoinColumn.javaField},  
181 - async (val) => {  
182 - if (!val) {  
183 - return;  
184 - }  
185 - await nextTick();  
186 - await getList()  
187 - },  
188 - { immediate: true },  
189 - ); 178 +/** 监听主表的关联字段的变化,加载对应的子表数据 */
  179 +watch(
  180 + () => props.${subJoinColumn.javaField},
  181 + async (val) => {
  182 + if (!val) {
  183 + return;
  184 + }
  185 + await nextTick();
  186 + await getList()
  187 + },
  188 + { immediate: true },
  189 +);
190 190
191 #if ($table.templateType == 11) ## erp 191 #if ($table.templateType == 11) ## erp
192 /** 初始化 */ 192 /** 初始化 */
@@ -290,7 +290,7 @@ onMounted(() =&gt; { @@ -290,7 +290,7 @@ onMounted(() =&gt; {
290 <!-- 列表 --> 290 <!-- 列表 -->
291 <ContentWrap title="${table.classComment}"> 291 <ContentWrap title="${table.classComment}">
292 <template #extra> 292 <template #extra>
293 - <TableToolbar 293 + <VbenVxeTableToolbar
294 ref="tableToolbarRef" 294 ref="tableToolbarRef"
295 v-model:hidden-search="hiddenSearchBar" 295 v-model:hidden-search="hiddenSearchBar"
296 > 296 >
yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/api/api.ts.vm
  1 +#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
  2 +#if ( $table.templateType != 2 )
1 import type { PageParam, PageResult } from '@vben/request'; 3 import type { PageParam, PageResult } from '@vben/request';
  4 +#end
2 import type { Dayjs } from 'dayjs'; 5 import type { Dayjs } from 'dayjs';
3 6
4 import { requestClient } from '#/api/request'; 7 import { requestClient } from '#/api/request';
5 #set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}") 8 #set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}")
6 9
7 -export namespace ${simpleClassName}Api { 10 +export namespace ${apiName} {
8 ## 特殊:主子表专属逻辑 11 ## 特殊:主子表专属逻辑
9 #foreach ($subTable in $subTables) 12 #foreach ($subTable in $subTables)
10 #set ($index = $foreach.count - 1) 13 #set ($index = $foreach.count - 1)
@@ -60,27 +63,35 @@ export namespace ${simpleClassName}Api { @@ -60,27 +63,35 @@ export namespace ${simpleClassName}Api {
60 #if ( $table.templateType != 2 ) 63 #if ( $table.templateType != 2 )
61 /** 查询${table.classComment}分页 */ 64 /** 查询${table.classComment}分页 */
62 export function get${simpleClassName}Page(params: PageParam) { 65 export function get${simpleClassName}Page(params: PageParam) {
63 - return requestClient.get<PageResult<${simpleClassName}Api.${simpleClassName}>>('${baseURL}/page', { params }); 66 + return requestClient.get<PageResult<${apiName}.${simpleClassName}>>(
  67 + '${baseURL}/page',
  68 + { params },
  69 + );
64 } 70 }
65 #else 71 #else
66 /** 查询${table.classComment}列表 */ 72 /** 查询${table.classComment}列表 */
67 export function get${simpleClassName}List(params: any) { 73 export function get${simpleClassName}List(params: any) {
68 - return requestClient.get<${simpleClassName}Api.${simpleClassName}[]>('${baseURL}/list', { params }); 74 + return requestClient.get<${apiName}.${simpleClassName}[]>(
  75 + '${baseURL}/list',
  76 + { params },
  77 + );
69 } 78 }
70 #end 79 #end
71 80
72 /** 查询${table.classComment}详情 */ 81 /** 查询${table.classComment}详情 */
73 export function get${simpleClassName}(id: number) { 82 export function get${simpleClassName}(id: number) {
74 - return requestClient.get<${simpleClassName}Api.${simpleClassName}>(`${baseURL}/get?id=${id}`); 83 + return requestClient.get<${apiName}.${simpleClassName}>(
  84 + `${baseURL}/get?id=${id}`,
  85 + );
75 } 86 }
76 87
77 /** 新增${table.classComment} */ 88 /** 新增${table.classComment} */
78 -export function create${simpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) { 89 +export function create${simpleClassName}(data: ${apiName}.${simpleClassName}) {
79 return requestClient.post('${baseURL}/create', data); 90 return requestClient.post('${baseURL}/create', data);
80 } 91 }
81 92
82 /** 修改${table.classComment} */ 93 /** 修改${table.classComment} */
83 -export function update${simpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) { 94 +export function update${simpleClassName}(data: ${apiName}.${simpleClassName}) {
84 return requestClient.put('${baseURL}/update', data); 95 return requestClient.put('${baseURL}/update', data);
85 } 96 }
86 97
@@ -88,17 +99,19 @@ export function update${simpleClassName}(data: ${simpleClassName}Api.${simpleCla @@ -88,17 +99,19 @@ export function update${simpleClassName}(data: ${simpleClassName}Api.${simpleCla
88 export function delete${simpleClassName}(id: number) { 99 export function delete${simpleClassName}(id: number) {
89 return requestClient.delete(`${baseURL}/delete?id=${id}`); 100 return requestClient.delete(`${baseURL}/delete?id=${id}`);
90 } 101 }
91 -  
92 #if ( $table.templateType != 2 && $deleteBatchEnable) 102 #if ( $table.templateType != 2 && $deleteBatchEnable)
  103 +
93 /** 批量删除${table.classComment} */ 104 /** 批量删除${table.classComment} */
94 export function delete${simpleClassName}List(ids: number[]) { 105 export function delete${simpleClassName}List(ids: number[]) {
95 - return requestClient.delete(`${baseURL}/delete-list?ids=${ids.join(',')}`) 106 + return requestClient.delete(
  107 + `${baseURL}/delete-list?ids=${ids.join(',')}`,
  108 + );
96 } 109 }
97 #end 110 #end
98 111
99 /** 导出${table.classComment} */ 112 /** 导出${table.classComment} */
100 export function export${simpleClassName}(params: any) { 113 export function export${simpleClassName}(params: any) {
101 - return requestClient.download('${baseURL}/export-excel', params); 114 + return requestClient.download('${baseURL}/export-excel', { params });
102 } 115 }
103 116
104 ## 特殊:主子表专属逻辑 117 ## 特殊:主子表专属逻辑
@@ -118,31 +131,38 @@ export function export${simpleClassName}(params: any) { @@ -118,31 +131,38 @@ export function export${simpleClassName}(params: any) {
118 #if ( $table.templateType == 11 ) 131 #if ( $table.templateType == 11 )
119 /** 获得${subTable.classComment}分页 */ 132 /** 获得${subTable.classComment}分页 */
120 export function get${subSimpleClassName}Page(params: PageParam) { 133 export function get${subSimpleClassName}Page(params: PageParam) {
121 - return requestClient.get<PageResult<${simpleClassName}Api.${subSimpleClassName}>>(`${baseURL}/${subSimpleClassName_strikeCase}/page`, { params }); 134 + return requestClient.get<PageResult<${apiName}.${subSimpleClassName}>>(
  135 + `${baseURL}/${subSimpleClassName_strikeCase}/page`,
  136 + { params },
  137 + );
122 } 138 }
123 ## 情况二:非 MASTER_ERP 时,需要列表查询子表 139 ## 情况二:非 MASTER_ERP 时,需要列表查询子表
124 #else 140 #else
125 #if ( $subTable.subJoinMany ) 141 #if ( $subTable.subJoinMany )
126 /** 获得${subTable.classComment}列表 */ 142 /** 获得${subTable.classComment}列表 */
127 export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}: number) { 143 export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}: number) {
128 - return requestClient.get<${simpleClassName}Api.${subSimpleClassName}[]>(`${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`); 144 + return requestClient.get<${apiName}.${subSimpleClassName}[]>(
  145 + `${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`,
  146 + );
129 } 147 }
130 #else 148 #else
131 /** 获得${subTable.classComment} */ 149 /** 获得${subTable.classComment} */
132 export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}: number) { 150 export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}: number) {
133 - return requestClient.get<${simpleClassName}Api.${subSimpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`); 151 + return requestClient.get<${apiName}.${subSimpleClassName}>(
  152 + `${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`,
  153 + );
134 } 154 }
135 #end 155 #end
136 #end 156 #end
137 ## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作 157 ## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作
138 #if ( $table.templateType == 11 ) 158 #if ( $table.templateType == 11 )
139 /** 新增${subTable.classComment} */ 159 /** 新增${subTable.classComment} */
140 -export function create${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) { 160 +export function create${subSimpleClassName}(data: ${apiName}.${subSimpleClassName}) {
141 return requestClient.post(`${baseURL}/${subSimpleClassName_strikeCase}/create`, data); 161 return requestClient.post(`${baseURL}/${subSimpleClassName_strikeCase}/create`, data);
142 } 162 }
143 163
144 /** 修改${subTable.classComment} */ 164 /** 修改${subTable.classComment} */
145 -export function update${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) { 165 +export function update${subSimpleClassName}(data: ${apiName}.${subSimpleClassName}) {
146 return requestClient.put(`${baseURL}/${subSimpleClassName_strikeCase}/update`, data); 166 return requestClient.put(`${baseURL}/${subSimpleClassName_strikeCase}/update`, data);
147 } 167 }
148 168
@@ -154,13 +174,17 @@ export function delete${subSimpleClassName}(id: number) { @@ -154,13 +174,17 @@ export function delete${subSimpleClassName}(id: number) {
154 #if ($deleteBatchEnable) 174 #if ($deleteBatchEnable)
155 /** 批量删除${subTable.classComment} */ 175 /** 批量删除${subTable.classComment} */
156 export function delete${subSimpleClassName}List(ids: number[]) { 176 export function delete${subSimpleClassName}List(ids: number[]) {
157 - return requestClient.delete(`${baseURL}/${subSimpleClassName_strikeCase}/delete-list?ids=${ids.join(',')}`) 177 + return requestClient.delete(
  178 + `${baseURL}/${subSimpleClassName_strikeCase}/delete-list?ids=${ids.join(',')}`,
  179 + );
158 } 180 }
159 #end 181 #end
160 182
161 /** 获得${subTable.classComment} */ 183 /** 获得${subTable.classComment} */
162 export function get${subSimpleClassName}(id: number) { 184 export function get${subSimpleClassName}(id: number) {
163 - return requestClient.get<${simpleClassName}Api.${subSimpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get?id=${id}`); 185 + return requestClient.get<${apiName}.${subSimpleClassName}>(
  186 + `${baseURL}/${subSimpleClassName_strikeCase}/get?id=${id}`,
  187 + );
164 } 188 }
165 #end 189 #end
166 #end 190 #end
yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/data.ts.vm
  1 +#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
1 import type { VbenFormSchema } from '#/adapter/form'; 2 import type { VbenFormSchema } from '#/adapter/form';
2 import type { VxeTableGridOptions } from '#/adapter/vxe-table'; 3 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
3 -import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}'; 4 +import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
4 5
5 -import { z } from '#/adapter/form';  
6 -import {  
7 - DICT_TYPE,  
8 - getDictOptions,  
9 - getRangePickerDefaultProps,  
10 -} from '#/utils'; 6 +import { DICT_TYPE } from '@vben/constants';
  7 +import { getDictOptions } from '@vben/hooks';
11 #if(${table.templateType} == 2)## 树表需要导入这些 8 #if(${table.templateType} == 2)## 树表需要导入这些
12 import { get${simpleClassName}List } from '#/api/${table.moduleName}/${table.businessName}'; 9 import { get${simpleClassName}List } from '#/api/${table.moduleName}/${table.businessName}';
13 import { handleTree } from '@vben/utils'; 10 import { handleTree } from '@vben/utils';
14 #end 11 #end
15 12
  13 +import { getRangePickerDefaultProps } from '#/utils';
  14 +
16 /** 新增/修改的表单 */ 15 /** 新增/修改的表单 */
17 export function useFormSchema(): VbenFormSchema[] { 16 export function useFormSchema(): VbenFormSchema[] {
18 return [ 17 return [
@@ -30,7 +29,7 @@ export function useFormSchema(): VbenFormSchema[] { @@ -30,7 +29,7 @@ export function useFormSchema(): VbenFormSchema[] {
30 label: '上级${table.classComment}', 29 label: '上级${table.classComment}',
31 component: 'ApiTreeSelect', 30 component: 'ApiTreeSelect',
32 componentProps: { 31 componentProps: {
33 - allowClear: true, 32 + clearable: true,
34 api: async () => { 33 api: async () => {
35 const data = await get${simpleClassName}List({}); 34 const data = await get${simpleClassName}List({});
36 data.unshift({ 35 data.unshift({
@@ -158,13 +157,13 @@ export function useGridFormSchema(): VbenFormSchema[] { @@ -158,13 +157,13 @@ export function useGridFormSchema(): VbenFormSchema[] {
158 #if ($column.htmlType == "input" || $column.htmlType == "textarea" || $column.htmlType == "editor") 157 #if ($column.htmlType == "input" || $column.htmlType == "textarea" || $column.htmlType == "editor")
159 component: 'Input', 158 component: 'Input',
160 componentProps: { 159 componentProps: {
161 - allowClear: true, 160 + clearable: true,
162 placeholder: '请输入${comment}', 161 placeholder: '请输入${comment}',
163 }, 162 },
164 #elseif ($column.htmlType == "select" || $column.htmlType == "radio") 163 #elseif ($column.htmlType == "select" || $column.htmlType == "radio")
165 component: 'Select', 164 component: 'Select',
166 componentProps: { 165 componentProps: {
167 - allowClear: true, 166 + clearable: true,
168 #if ("" != $dictType)## 设置了 dictType 数据字典的情况 167 #if ("" != $dictType)## 设置了 dictType 数据字典的情况
169 options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), 168 options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
170 #else## 未设置 dictType 数据字典的情况 169 #else## 未设置 dictType 数据字典的情况
@@ -176,7 +175,7 @@ export function useGridFormSchema(): VbenFormSchema[] { @@ -176,7 +175,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
176 component: 'RangePicker', 175 component: 'RangePicker',
177 componentProps: { 176 componentProps: {
178 ...getRangePickerDefaultProps(), 177 ...getRangePickerDefaultProps(),
179 - allowClear: true, 178 + clearable: true,
180 }, 179 },
181 #end 180 #end
182 }, 181 },
@@ -186,7 +185,7 @@ export function useGridFormSchema(): VbenFormSchema[] { @@ -186,7 +185,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
186 } 185 }
187 186
188 /** 列表的字段 */ 187 /** 列表的字段 */
189 -export function useGridColumns(): VxeTableGridOptions<${simpleClassName}Api.${simpleClassName}>['columns'] { 188 +export function useGridColumns(): VxeTableGridOptions<${apiName}.${simpleClassName}>['columns'] {
190 return [ 189 return [
191 #if ($table.templateType != 2 && $deleteBatchEnable) 190 #if ($table.templateType != 2 && $deleteBatchEnable)
192 { type: 'checkbox', width: 40 }, 191 { type: 'checkbox', width: 40 },
@@ -361,13 +360,13 @@ export function use${subSimpleClassName}GridFormSchema(): VbenFormSchema[] { @@ -361,13 +360,13 @@ export function use${subSimpleClassName}GridFormSchema(): VbenFormSchema[] {
361 #if ($column.htmlType == "input" || $column.htmlType == "textarea" || $column.htmlType == "editor") 360 #if ($column.htmlType == "input" || $column.htmlType == "textarea" || $column.htmlType == "editor")
362 component: 'Input', 361 component: 'Input',
363 componentProps: { 362 componentProps: {
364 - allowClear: true, 363 + clearable: true,
365 placeholder: '请输入${comment}', 364 placeholder: '请输入${comment}',
366 }, 365 },
367 #elseif ($column.htmlType == "select" || $column.htmlType == "radio") 366 #elseif ($column.htmlType == "select" || $column.htmlType == "radio")
368 component: 'Select', 367 component: 'Select',
369 componentProps: { 368 componentProps: {
370 - allowClear: true, 369 + clearable: true,
371 #if ("" != $dictType)## 设置了 dictType 数据字典的情况 370 #if ("" != $dictType)## 设置了 dictType 数据字典的情况
372 options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), 371 options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
373 #else## 未设置 dictType 数据字典的情况 372 #else## 未设置 dictType 数据字典的情况
@@ -379,7 +378,7 @@ export function use${subSimpleClassName}GridFormSchema(): VbenFormSchema[] { @@ -379,7 +378,7 @@ export function use${subSimpleClassName}GridFormSchema(): VbenFormSchema[] {
379 component: 'RangePicker', 378 component: 'RangePicker',
380 componentProps: { 379 componentProps: {
381 ...getRangePickerDefaultProps(), 380 ...getRangePickerDefaultProps(),
382 - allowClear: true, 381 + clearable: true,
383 }, 382 },
384 #end 383 #end
385 }, 384 },
@@ -389,7 +388,7 @@ export function use${subSimpleClassName}GridFormSchema(): VbenFormSchema[] { @@ -389,7 +388,7 @@ export function use${subSimpleClassName}GridFormSchema(): VbenFormSchema[] {
389 } 388 }
390 389
391 /** 列表的字段 */ 390 /** 列表的字段 */
392 -export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>['columns'] { 391 +export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${apiName}.${subSimpleClassName}>['columns'] {
393 return [ 392 return [
394 #if ($table.templateType != 2 && $deleteBatchEnable) 393 #if ($table.templateType != 2 && $deleteBatchEnable)
395 { type: 'checkbox', width: 40 }, 394 { type: 'checkbox', width: 40 },
@@ -426,7 +425,7 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions&lt;${sim @@ -426,7 +425,7 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions&lt;${sim
426 #else 425 #else
427 #if ($subTable.subJoinMany) ## 一对多 426 #if ($subTable.subJoinMany) ## 一对多
428 /** 新增/修改列表的字段 */ 427 /** 新增/修改列表的字段 */
429 - export function use${subSimpleClassName}GridEditColumns(): VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>['columns'] { 428 + export function use${subSimpleClassName}GridEditColumns(): VxeTableGridOptions<${apiName}.${subSimpleClassName}>['columns'] {
430 return [ 429 return [
431 #foreach($column in $subColumns) 430 #foreach($column in $subColumns)
432 #if ($column.createOperation || $column.updateOperation) 431 #if ($column.createOperation || $column.updateOperation)
@@ -602,4 +601,4 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions&lt;${sim @@ -602,4 +601,4 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions&lt;${sim
602 } 601 }
603 #end 602 #end
604 #end 603 #end
605 -#end 604 -#end
  605 +#end
606 \ No newline at end of file 606 \ No newline at end of file
yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/form.vue.vm
  1 +#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
1 <script lang="ts" setup> 2 <script lang="ts" setup>
2 -import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}'; 3 +import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
  4 +
  5 +import { computed, ref } from 'vue';
3 6
4 import { useVbenModal } from '@vben/common-ui'; 7 import { useVbenModal } from '@vben/common-ui';
5 -import { ElMessage, ElTabs, ElTabPane, ElCheckbox, ElInput, ElSelect, ElRadioGroup, ElCheckboxGroup, ElDatePicker } from 'element-plus'; 8 +
  9 +import { ElMessage#if ($table.templateType == 11), ElTabs, ElTabPane#end } from 'element-plus';
  10 +
  11 +import { useVbenForm } from '#/adapter/form';
  12 +import { create${simpleClassName}, get${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
  13 +import { $t } from '#/locales';
6 ## 特殊:主子表专属逻辑 14 ## 特殊:主子表专属逻辑
7 #if ( $table.templateType == 10 || $table.templateType == 12 ) 15 #if ( $table.templateType == 10 || $table.templateType == 12 )
8 #foreach ($subSimpleClassName in $subSimpleClassNames) 16 #foreach ($subSimpleClassName in $subSimpleClassNames)
@@ -12,21 +20,15 @@ import { ElMessage, ElTabs, ElTabPane, ElCheckbox, ElInput, ElSelect, ElRadioGro @@ -12,21 +20,15 @@ import { ElMessage, ElTabs, ElTabPane, ElCheckbox, ElInput, ElSelect, ElRadioGro
12 #end 20 #end
13 #end 21 #end
14 22
15 -import { computed, ref } from 'vue';  
16 -import { $t } from '#/locales';  
17 -import { useVbenForm } from '#/adapter/form';  
18 -import { get${simpleClassName}, create${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';  
19 -  
20 import { useFormSchema } from '../data'; 23 import { useFormSchema } from '../data';
21 24
22 const emit = defineEmits(['success']); 25 const emit = defineEmits(['success']);
23 -const formData = ref<${simpleClassName}Api.${simpleClassName}>(); 26 +const formData = ref<${apiName}.${simpleClassName}>();
24 const getTitle = computed(() => { 27 const getTitle = computed(() => {
25 return formData.value?.id 28 return formData.value?.id
26 ? $t('ui.actionTitle.edit', ['${table.classComment}']) 29 ? $t('ui.actionTitle.edit', ['${table.classComment}'])
27 : $t('ui.actionTitle.create', ['${table.classComment}']); 30 : $t('ui.actionTitle.create', ['${table.classComment}']);
28 }); 31 });
29 -  
30 ## 特殊:主子表专属逻辑 32 ## 特殊:主子表专属逻辑
31 #if ( $table.templateType == 10 || $table.templateType == 12 ) 33 #if ( $table.templateType == 10 || $table.templateType == 12 )
32 #if ( $subTables && $subTables.size() > 0 ) 34 #if ( $subTables && $subTables.size() > 0 )
@@ -51,7 +53,7 @@ const [Form, formApi] = useVbenForm({ @@ -51,7 +53,7 @@ const [Form, formApi] = useVbenForm({
51 }, 53 },
52 layout: 'horizontal', 54 layout: 'horizontal',
53 schema: useFormSchema(), 55 schema: useFormSchema(),
54 - showDefaultActions: false 56 + showDefaultActions: false,
55 }); 57 });
56 58
57 const [Modal, modalApi] = useVbenModal({ 59 const [Modal, modalApi] = useVbenModal({
@@ -60,8 +62,8 @@ const [Modal, modalApi] = useVbenModal({ @@ -60,8 +62,8 @@ const [Modal, modalApi] = useVbenModal({
60 if (!valid) { 62 if (!valid) {
61 return; 63 return;
62 } 64 }
63 - ## 特殊:主子表专属逻辑  
64 - #if ( $table.templateType == 10 || $table.templateType == 12 ) 65 +## 特殊:主子表专属逻辑
  66 +#if ( $table.templateType == 10 || $table.templateType == 12 )
65 #if ( $subTables && $subTables.size() > 0 ) 67 #if ( $subTables && $subTables.size() > 0 )
66 // 校验子表单 68 // 校验子表单
67 #foreach ($subTable in $subTables) 69 #foreach ($subTable in $subTables)
@@ -78,12 +80,12 @@ const [Modal, modalApi] = useVbenModal({ @@ -78,12 +80,12 @@ const [Modal, modalApi] = useVbenModal({
78 #end 80 #end
79 #end 81 #end
80 #end 82 #end
81 - #end 83 +#end
82 modalApi.lock(); 84 modalApi.lock();
83 // 提交表单 85 // 提交表单
84 - const data = (await formApi.getValues()) as ${simpleClassName}Api.${simpleClassName};  
85 - ## 特殊:主子表专属逻辑  
86 - #if ( $table.templateType == 10 || $table.templateType == 12 ) 86 + const data = (await formApi.getValues()) as ${apiName}.${simpleClassName};
  87 +## 特殊:主子表专属逻辑
  88 +#if ( $table.templateType == 10 || $table.templateType == 12 )
87 #if ( $subTables && $subTables.size() > 0 ) 89 #if ( $subTables && $subTables.size() > 0 )
88 // 拼接子表的数据 90 // 拼接子表的数据
89 #foreach ($subTable in $subTables) 91 #foreach ($subTable in $subTables)
@@ -96,7 +98,7 @@ const [Modal, modalApi] = useVbenModal({ @@ -96,7 +98,7 @@ const [Modal, modalApi] = useVbenModal({
96 #end 98 #end
97 #end 99 #end
98 #end 100 #end
99 - #end 101 +#end
100 try { 102 try {
101 await (formData.value?.id ? update${simpleClassName}(data) : create${simpleClassName}(data)); 103 await (formData.value?.id ? update${simpleClassName}(data) : create${simpleClassName}(data));
102 // 关闭并提示 104 // 关闭并提示
@@ -113,21 +115,24 @@ const [Modal, modalApi] = useVbenModal({ @@ -113,21 +115,24 @@ const [Modal, modalApi] = useVbenModal({
113 return; 115 return;
114 } 116 }
115 // 加载数据 117 // 加载数据
116 - let data = modalApi.getData<${simpleClassName}Api.${simpleClassName}>();  
117 - if (!data) { 118 + const data = modalApi.getData<${apiName}.${simpleClassName}>();
  119 + if (!data || !data.id) {
  120 +#if (${table.templateType} == 2)## 树表特有
  121 + // 设置上级
  122 + await formApi.setValues(data);
  123 +#end
118 return; 124 return;
119 } 125 }
120 - if (data.id) {  
121 - modalApi.lock();  
122 - try {  
123 - data = await get${simpleClassName}(data.id);  
124 - } finally {  
125 - modalApi.unlock(); 126 + modalApi.lock();
  127 + try {
  128 + formData.value = await get${simpleClassName}(data.id);
  129 + // 设置到 values
  130 + if (formData.value) {
  131 + await formApi.setValues(formData.value);
126 } 132 }
  133 + } finally {
  134 + modalApi.unlock();
127 } 135 }
128 - // 设置到 values  
129 - formData.value = data;  
130 - await formApi.setValues(formData.value);  
131 }, 136 },
132 }); 137 });
133 </script> 138 </script>
@@ -135,20 +140,20 @@ const [Modal, modalApi] = useVbenModal({ @@ -135,20 +140,20 @@ const [Modal, modalApi] = useVbenModal({
135 <template> 140 <template>
136 <Modal :title="getTitle"> 141 <Modal :title="getTitle">
137 <Form class="mx-4" /> 142 <Form class="mx-4" />
138 - ## 特殊:主子表专属逻辑  
139 - #if ( $table.templateType == 10 || $table.templateType == 12 ) 143 +## 特殊:主子表专属逻辑
  144 +#if ( $table.templateType == 10 || $table.templateType == 12 )
140 <!-- 子表的表单 --> 145 <!-- 子表的表单 -->
141 - <el-tabs v-model="subTabsName"> 146 + <ElTabs v-model="subTabsName">
142 #foreach ($subTable in $subTables) 147 #foreach ($subTable in $subTables)
143 #set ($index = $foreach.count - 1) 148 #set ($index = $foreach.count - 1)
144 #set ($subClassNameVar = $subClassNameVars.get($index)) 149 #set ($subClassNameVar = $subClassNameVars.get($index))
145 #set ($subSimpleClassName = $subSimpleClassNames.get($index)) 150 #set ($subSimpleClassName = $subSimpleClassNames.get($index))
146 #set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index)) 151 #set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
147 - <el-tab-pane name="$subClassNameVar" label="${subTable.classComment}"> 152 + <ElTabPane name="$subClassNameVar" label="${subTable.classComment}">
148 <${subSimpleClassName}Form ref="${subClassNameVar}FormRef" :${subJoinColumn_strikeCase}="formData?.id" /> 153 <${subSimpleClassName}Form ref="${subClassNameVar}FormRef" :${subJoinColumn_strikeCase}="formData?.id" />
149 - </el-tab-pane> 154 + </ElTabPane>
150 #end 155 #end
151 - </el-tabs>  
152 - #end 156 + </ElTabs>
  157 +#end
153 </Modal> 158 </Modal>
154 </template> 159 </template>
yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/index.vue.vm
  1 +#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
1 <script lang="ts" setup> 2 <script lang="ts" setup>
2 import type { VxeTableGridOptions } from '#/adapter/vxe-table'; 3 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
3 -import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}'; 4 +import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
4 5
5 -import { Page, useVbenModal } from '@vben/common-ui';  
6 -import { ElMessage, ElTabs, ElTabPane, ElLoading } from 'element-plus';  
7 -import Form from './modules/form.vue'; 6 +import { ref } from 'vue';
8 7
9 -## 特殊:主子表专属逻辑  
10 -#if ( $table.templateType == 11 || $table.templateType == 12 )  
11 - #foreach ($subSimpleClassName in $subSimpleClassNames)  
12 - #set ($index = $foreach.count - 1)  
13 - #set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index))  
14 -import ${subSimpleClassName}List from './modules/${subSimpleClassName_strikeCase}-list.vue'  
15 - #end  
16 -#end 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';
  10 +
  11 +import { ElLoading, ElMessage#if ($table.templateType == 11), ElTabs, ElTabPane#end } from 'element-plus';
17 12
18 -import { ref, computed } from 'vue';  
19 -import { $t } from '#/locales';  
20 import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; 13 import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
21 #if (${table.templateType} == 2)## 树表接口 14 #if (${table.templateType} == 2)## 树表接口
22 -import { get${simpleClassName}List, delete${simpleClassName}, export${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}'; 15 +import {
  16 + delete${simpleClassName},
  17 + export${simpleClassName},
  18 + get${simpleClassName}List,
  19 +} from '#/api/${table.moduleName}/${table.businessName}';
23 #else## 标准表接口 20 #else## 标准表接口
24 -import { get${simpleClassName}Page, delete${simpleClassName},#if ($deleteBatchEnable) delete${simpleClassName}List,#end export${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}'; 21 +import {
  22 + delete${simpleClassName},#if ($deleteBatchEnable)
  23 +
  24 + delete${simpleClassName}List,#end
  25 + export${simpleClassName},
  26 + get${simpleClassName}Page,
  27 +} from '#/api/${table.moduleName}/${table.businessName}';
25 #end 28 #end
26 -import { downloadFileFromBlobPart, isEmpty } from '@vben/utils'; 29 +import { $t } from '#/locales';
27 30
28 import { useGridColumns, useGridFormSchema } from './data'; 31 import { useGridColumns, useGridFormSchema } from './data';
  32 +import Form from './modules/form.vue';
  33 +## 特殊:主子表专属逻辑
  34 +#if ( $table.templateType == 11 || $table.templateType == 12 )
  35 + #foreach ($subSimpleClassName in $subSimpleClassNames)
  36 + #set ($index = $foreach.count - 1)
  37 + #set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index))
  38 +import ${subSimpleClassName}List from './modules/${subSimpleClassName_strikeCase}-list.vue';
  39 + #end
  40 +#end
29 41
30 #if ($table.templateType == 12 || $table.templateType == 11) ## 内嵌和erp情况 42 #if ($table.templateType == 12 || $table.templateType == 11) ## 内嵌和erp情况
31 /** 子表的列表 */ 43 /** 子表的列表 */
32 const subTabsName = ref('$subClassNameVars.get(0)') 44 const subTabsName = ref('$subClassNameVars.get(0)')
33 #if ($table.templateType == 11) 45 #if ($table.templateType == 11)
34 -const select${simpleClassName} = ref<${simpleClassName}Api.${simpleClassName}>(); 46 +const select${simpleClassName} = ref<${apiName}.${simpleClassName}>();
35 #end 47 #end
36 #end 48 #end
37 -  
38 const [FormModal, formModalApi] = useVbenModal({ 49 const [FormModal, formModalApi] = useVbenModal({
39 connectedComponent: Form, 50 connectedComponent: Form,
40 destroyOnClose: true, 51 destroyOnClose: true,
41 }); 52 });
42 -  
43 #if (${table.templateType} == 2)## 树表特有:控制表格展开收缩 53 #if (${table.templateType} == 2)## 树表特有:控制表格展开收缩
  54 +
44 /** 切换树形展开/收缩状态 */ 55 /** 切换树形展开/收缩状态 */
45 const isExpanded = ref(true); 56 const isExpanded = ref(true);
46 -function toggleExpand() { 57 +function handleExpand() {
47 isExpanded.value = !isExpanded.value; 58 isExpanded.value = !isExpanded.value;
48 gridApi.grid.setAllTreeExpand(isExpanded.value); 59 gridApi.grid.setAllTreeExpand(isExpanded.value);
49 } 60 }
50 #end 61 #end
51 62
52 /** 刷新表格 */ 63 /** 刷新表格 */
53 -function onRefresh() { 64 +function handleRefresh() {
54 #if ($table.templateType == 12) ## 内嵌情况 65 #if ($table.templateType == 12) ## 内嵌情况
55 gridApi.reload(); 66 gridApi.reload();
56 #else 67 #else
@@ -60,31 +71,30 @@ function onRefresh() { @@ -60,31 +71,30 @@ function onRefresh() {
60 71
61 /** 创建${table.classComment} */ 72 /** 创建${table.classComment} */
62 function handleCreate() { 73 function handleCreate() {
63 - formModalApi.setData({}).open(); 74 + formModalApi.setData(null).open();
64 } 75 }
65 -  
66 -/** 编辑${table.classComment} */  
67 -function handleEdit(row: ${simpleClassName}Api.${simpleClassName}) {  
68 - formModalApi.setData(row).open();  
69 -}  
70 -  
71 #if (${table.templateType} == 2)## 树表特有:新增下级 76 #if (${table.templateType} == 2)## 树表特有:新增下级
72 -/** 新增下级${table.classComment} */  
73 -function handleAppend(row: ${simpleClassName}Api.${simpleClassName}) { 77 +
  78 +/** 添加下级${table.classComment} */
  79 +function handleAppend(row: ${apiName}.${simpleClassName}) {
74 formModalApi.setData({ ${treeParentColumn.javaField}: row.id }).open(); 80 formModalApi.setData({ ${treeParentColumn.javaField}: row.id }).open();
75 } 81 }
76 #end 82 #end
77 83
  84 +/** 编辑${table.classComment} */
  85 +function handleEdit(row: ${apiName}.${simpleClassName}) {
  86 + formModalApi.setData(row).open();
  87 +}
  88 +
78 /** 删除${table.classComment} */ 89 /** 删除${table.classComment} */
79 -async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) { 90 +async function handleDelete(row: ${apiName}.${simpleClassName}) {
80 const loadingInstance = ElLoading.service({ 91 const loadingInstance = ElLoading.service({
81 text: $t('ui.actionMessage.deleting', [row.id]), 92 text: $t('ui.actionMessage.deleting', [row.id]),
82 - background: 'rgba(0, 0, 0, 0.7)',  
83 }); 93 });
84 try { 94 try {
85 - await delete${simpleClassName}(row.id as number); 95 + await delete${simpleClassName}(row.id!);
86 ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.id])); 96 ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.id]));
87 - onRefresh(); 97 + handleRefresh();
88 } finally { 98 } finally {
89 loadingInstance.close(); 99 loadingInstance.close();
90 } 100 }
@@ -93,27 +103,27 @@ async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) { @@ -93,27 +103,27 @@ async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) {
93 #if ($table.templateType != 2 && $deleteBatchEnable) 103 #if ($table.templateType != 2 && $deleteBatchEnable)
94 /** 批量删除${table.classComment} */ 104 /** 批量删除${table.classComment} */
95 async function handleDeleteBatch() { 105 async function handleDeleteBatch() {
  106 + await confirm($t('ui.actionMessage.deleteBatchConfirm'));
96 const loadingInstance = ElLoading.service({ 107 const loadingInstance = ElLoading.service({
97 - text: $t('ui.actionMessage.deleting'),  
98 - background: 'rgba(0, 0, 0, 0.7)', 108 + text: $t('ui.actionMessage.deletingBatch'),
99 }); 109 });
100 try { 110 try {
101 await delete${simpleClassName}List(checkedIds.value); 111 await delete${simpleClassName}List(checkedIds.value);
102 checkedIds.value = []; 112 checkedIds.value = [];
103 ElMessage.success($t('ui.actionMessage.deleteSuccess')); 113 ElMessage.success($t('ui.actionMessage.deleteSuccess'));
104 - onRefresh(); 114 + handleRefresh();
105 } finally { 115 } finally {
106 loadingInstance.close(); 116 loadingInstance.close();
107 } 117 }
108 } 118 }
109 119
110 -const checkedIds = ref<number[]>([]) 120 +const checkedIds = ref<number[]>([]);
111 function handleRowCheckboxChange({ 121 function handleRowCheckboxChange({
112 records, 122 records,
113 }: { 123 }: {
114 - records: ${simpleClassName}Api.${simpleClassName}[]; 124 + records: ${apiName}.${simpleClassName}[];
115 }) { 125 }) {
116 - checkedIds.value = records.map((item) => item.id); 126 + checkedIds.value = records.map((item) => item.id!);
117 } 127 }
118 #end 128 #end
119 129
@@ -135,20 +145,11 @@ const [Grid, gridApi] = useVbenVxeGrid({ @@ -135,20 +145,11 @@ const [Grid, gridApi] = useVbenVxeGrid({
135 height: 'auto', 145 height: 'auto',
136 #end 146 #end
137 #if (${table.templateType} == 2)## 树表设置 147 #if (${table.templateType} == 2)## 树表设置
138 - treeConfig: {  
139 - parentField: '${treeParentColumn.javaField}',  
140 - rowField: 'id',  
141 - transform: true,  
142 - expandAll: true,  
143 - reserve: true,  
144 - },  
145 - pagerConfig: {  
146 - enabled: false,  
147 - },  
148 -#else## 标准表设置  
149 pagerConfig: { 148 pagerConfig: {
150 - enabled: true, 149 + enabled: false,
151 }, 150 },
  151 +#else## 标准表设置
  152 + keepSource: true,
152 #end 153 #end
153 proxyConfig: { 154 proxyConfig: {
154 ajax: { 155 ajax: {
@@ -178,27 +179,35 @@ const [Grid, gridApi] = useVbenVxeGrid({ @@ -178,27 +179,35 @@ const [Grid, gridApi] = useVbenVxeGrid({
178 refresh: true, 179 refresh: true,
179 search: true, 180 search: true,
180 }, 181 },
181 - } as VxeTableGridOptions<${simpleClassName}Api.${simpleClassName}>,  
182 -#if (${table.templateType} == 11 || $deleteBatchEnable)  
183 - gridEvents:{  
184 - #if(${table.templateType} == 11)  
185 - cellClick: ({ row }: { row: ${simpleClassName}Api.${simpleClassName}}) => { 182 +#if (${table.templateType} == 2)## 树表设置
  183 + treeConfig: {
  184 + parentField: '${treeParentColumn.javaField}',
  185 + rowField: 'id',
  186 + transform: true,
  187 + expandAll: true,
  188 + reserve: true,
  189 + },
  190 +#end
  191 + } as VxeTableGridOptions<${apiName}.${simpleClassName}>,
  192 +#if (${table.templateType} == 11 || (${table.templateType} != 2 && $deleteBatchEnable))
  193 + gridEvents: {
  194 + #if(${table.templateType} == 11)
  195 + cellClick: ({ row }: { row: ${apiName}.${simpleClassName}}) => {
186 select${simpleClassName}.value = row; 196 select${simpleClassName}.value = row;
187 }, 197 },
188 - #end  
189 - #if(${table.templateType} != 2 && $deleteBatchEnable)  
190 - checkboxAll: handleRowCheckboxChange,  
191 - checkboxChange: handleRowCheckboxChange,  
192 - #end  
193 - } 198 + #end
  199 + #if (${table.templateType} != 2 && $deleteBatchEnable)
  200 + checkboxAll: handleRowCheckboxChange,
  201 + checkboxChange: handleRowCheckboxChange,
  202 + #end
  203 + },
194 #end 204 #end
195 }); 205 });
196 </script> 206 </script>
197 207
198 <template> 208 <template>
199 <Page auto-content-height> 209 <Page auto-content-height>
200 - <FormModal @success="onRefresh" />  
201 - 210 + <FormModal @success="handleRefresh" />
202 #if ($table.templateType == 11) ## erp情况 211 #if ($table.templateType == 11) ## erp情况
203 <div> 212 <div>
204 #end 213 #end
@@ -206,29 +215,22 @@ const [Grid, gridApi] = useVbenVxeGrid({ @@ -206,29 +215,22 @@ const [Grid, gridApi] = useVbenVxeGrid({
206 #if ($table.templateType == 12) ## 内嵌情况 215 #if ($table.templateType == 12) ## 内嵌情况
207 <template #expand_content="{ row }"> 216 <template #expand_content="{ row }">
208 <!-- 子表的表单 --> 217 <!-- 子表的表单 -->
209 - <el-tabs v-model="subTabsName" class="mx-8"> 218 + <ElTabs v-model="subTabsName" class="mx-8">
210 #foreach ($subTable in $subTables) 219 #foreach ($subTable in $subTables)
211 #set ($index = $foreach.count - 1) 220 #set ($index = $foreach.count - 1)
212 #set ($subClassNameVar = $subClassNameVars.get($index)) 221 #set ($subClassNameVar = $subClassNameVars.get($index))
213 #set ($subSimpleClassName = $subSimpleClassNames.get($index)) 222 #set ($subSimpleClassName = $subSimpleClassNames.get($index))
214 #set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index)) 223 #set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
215 - <el-tab-pane name="$subClassNameVar" label="${subTable.classComment}"> 224 + <ElTabPane name="$subClassNameVar" label="${subTable.classComment}">
216 <${subSimpleClassName}List :${subJoinColumn_strikeCase}="row?.id" /> 225 <${subSimpleClassName}List :${subJoinColumn_strikeCase}="row?.id" />
217 - </el-tab-pane> 226 + </ElTabPane>
218 #end 227 #end
219 - </el-tabs> 228 + </ElTabs>
220 </template> 229 </template>
221 #end 230 #end
222 <template #toolbar-tools> 231 <template #toolbar-tools>
223 <TableAction 232 <TableAction
224 :actions="[ 233 :actions="[
225 - #if (${table.templateType} == 2)## 树表特有:展开/收缩按钮  
226 - {  
227 - label: isExpanded ? '收缩' : '展开',  
228 - type: 'primary',  
229 - onClick: toggleExpand,  
230 - },  
231 - #end  
232 { 234 {
233 label: $t('ui.actionTitle.create', ['${table.classComment}']), 235 label: $t('ui.actionTitle.create', ['${table.classComment}']),
234 type: 'primary', 236 type: 'primary',
@@ -236,6 +238,13 @@ const [Grid, gridApi] = useVbenVxeGrid({ @@ -236,6 +238,13 @@ const [Grid, gridApi] = useVbenVxeGrid({
236 auth: ['${table.moduleName}:${simpleClassName_strikeCase}:create'], 238 auth: ['${table.moduleName}:${simpleClassName_strikeCase}:create'],
237 onClick: handleCreate, 239 onClick: handleCreate,
238 }, 240 },
  241 + #if (${table.templateType} == 2)## 树表特有:展开/收缩按钮
  242 + {
  243 + label: isExpanded ? '收缩' : '展开',
  244 + type: 'primary',
  245 + onClick: handleExpand,
  246 + },
  247 + #end
239 { 248 {
240 label: $t('ui.actionTitle.export'), 249 label: $t('ui.actionTitle.export'),
241 type: 'primary', 250 type: 'primary',
@@ -248,8 +257,8 @@ const [Grid, gridApi] = useVbenVxeGrid({ @@ -248,8 +257,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
248 label: $t('ui.actionTitle.deleteBatch'), 257 label: $t('ui.actionTitle.deleteBatch'),
249 type: 'danger', 258 type: 'danger',
250 icon: ACTION_ICON.DELETE, 259 icon: ACTION_ICON.DELETE,
251 - disabled: isEmpty(checkedIds),  
252 auth: ['${table.moduleName}:${simpleClassName_strikeCase}:delete'], 260 auth: ['${table.moduleName}:${simpleClassName_strikeCase}:delete'],
  261 + disabled: isEmpty(checkedIds),
253 onClick: handleDeleteBatch, 262 onClick: handleDeleteBatch,
254 }, 263 },
255 #end 264 #end
@@ -262,7 +271,8 @@ const [Grid, gridApi] = useVbenVxeGrid({ @@ -262,7 +271,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
262 #if (${table.templateType} == 2)## 树表特有:新增下级 271 #if (${table.templateType} == 2)## 树表特有:新增下级
263 { 272 {
264 label: '新增下级', 273 label: '新增下级',
265 - type: 'text', 274 + type: 'primary',
  275 + link: true,
266 icon: ACTION_ICON.ADD, 276 icon: ACTION_ICON.ADD,
267 auth: ['${table.moduleName}:${simpleClassName_strikeCase}:create'], 277 auth: ['${table.moduleName}:${simpleClassName_strikeCase}:create'],
268 onClick: handleAppend.bind(null, row), 278 onClick: handleAppend.bind(null, row),
@@ -270,7 +280,8 @@ const [Grid, gridApi] = useVbenVxeGrid({ @@ -270,7 +280,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
270 #end 280 #end
271 { 281 {
272 label: $t('common.edit'), 282 label: $t('common.edit'),
273 - type: 'text', 283 + type: 'primary',
  284 + link: true,
274 icon: ACTION_ICON.EDIT, 285 icon: ACTION_ICON.EDIT,
275 auth: ['${table.moduleName}:${simpleClassName_strikeCase}:update'], 286 auth: ['${table.moduleName}:${simpleClassName_strikeCase}:update'],
276 onClick: handleEdit.bind(null, row), 287 onClick: handleEdit.bind(null, row),
@@ -278,7 +289,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ @@ -278,7 +289,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
278 { 289 {
279 label: $t('common.delete'), 290 label: $t('common.delete'),
280 type: 'danger', 291 type: 'danger',
281 - text: true, 292 + link: true,
282 icon: ACTION_ICON.DELETE, 293 icon: ACTION_ICON.DELETE,
283 auth: ['${table.moduleName}:${simpleClassName_strikeCase}:delete'], 294 auth: ['${table.moduleName}:${simpleClassName_strikeCase}:delete'],
284 popConfirm: { 295 popConfirm: {
@@ -290,21 +301,20 @@ const [Grid, gridApi] = useVbenVxeGrid({ @@ -290,21 +301,20 @@ const [Grid, gridApi] = useVbenVxeGrid({
290 /> 301 />
291 </template> 302 </template>
292 </Grid> 303 </Grid>
293 -  
294 #if ($table.templateType == 11) ## erp情况 304 #if ($table.templateType == 11) ## erp情况
295 <!-- 子表的表单 --> 305 <!-- 子表的表单 -->
296 - <el-tabs v-model="subTabsName" class="mt-2"> 306 + <ElTabs v-model="subTabsName" class="mt-2">
297 #foreach ($subTable in $subTables) 307 #foreach ($subTable in $subTables)
298 #set ($index = $foreach.count - 1) 308 #set ($index = $foreach.count - 1)
299 #set ($subClassNameVar = $subClassNameVars.get($index)) 309 #set ($subClassNameVar = $subClassNameVars.get($index))
300 #set ($subSimpleClassName = $subSimpleClassNames.get($index)) 310 #set ($subSimpleClassName = $subSimpleClassNames.get($index))
301 #set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index)) 311 #set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
302 - <el-tab-pane name="$subClassNameVar" label="${subTable.classComment}"> 312 + <ElTabPane name="$subClassNameVar" label="${subTable.classComment}">
303 <${subSimpleClassName}List :${subJoinColumn_strikeCase}="select${simpleClassName}?.id" /> 313 <${subSimpleClassName}List :${subJoinColumn_strikeCase}="select${simpleClassName}?.id" />
304 - </el-tab-pane> 314 + </ElTabPane>
305 #end 315 #end
306 - </el-tabs> 316 + </ElTabs>
307 </div> 317 </div>
308 #end 318 #end
309 </Page> 319 </Page>
310 -</template> 320 -</template>
  321 +</template>
311 \ No newline at end of file 322 \ No newline at end of file
yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/modules/form_sub_erp.vue.vm
@@ -2,21 +2,24 @@ @@ -2,21 +2,24 @@
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 ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
5 <script lang="ts" setup> 6 <script lang="ts" setup>
6 - import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}'; 7 + import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
  8 +
  9 + import { computed, ref } from 'vue';
7 10
8 import { useVbenModal } from '@vben/common-ui'; 11 import { useVbenModal } from '@vben/common-ui';
  12 +
9 import { ElMessage } from 'element-plus'; 13 import { ElMessage } from 'element-plus';
10 14
11 - import { computed, ref } from 'vue';  
12 - import { $t } from '#/locales';  
13 import { useVbenForm } from '#/adapter/form'; 15 import { useVbenForm } from '#/adapter/form';
14 import { get${subSimpleClassName}, create${subSimpleClassName}, update${subSimpleClassName} } from '#/api/${table.moduleName}/${table.businessName}'; 16 import { get${subSimpleClassName}, create${subSimpleClassName}, update${subSimpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
  17 + import { $t } from '#/locales';
15 18
16 import { use${subSimpleClassName}FormSchema } from '../data'; 19 import { use${subSimpleClassName}FormSchema } from '../data';
17 20
18 const emit = defineEmits(['success']); 21 const emit = defineEmits(['success']);
19 - const formData = ref<${simpleClassName}Api.${subSimpleClassName}>(); 22 + const formData = ref<${apiName}.${subSimpleClassName}>();
20 const getTitle = computed(() => { 23 const getTitle = computed(() => {
21 return formData.value?.id 24 return formData.value?.id
22 ? $t('ui.actionTitle.edit', ['${subTable.classComment}']) 25 ? $t('ui.actionTitle.edit', ['${subTable.classComment}'])
@@ -45,7 +48,7 @@ @@ -45,7 +48,7 @@
45 48
46 modalApi.lock(); 49 modalApi.lock();
47 // 提交表单 50 // 提交表单
48 - const data = (await formApi.getValues()) as ${simpleClassName}Api.${subSimpleClassName}; 51 + const data = (await formApi.getValues()) as ${apiName}.${subSimpleClassName};
49 data.${subJoinColumn.javaField} = formData.value?.${subJoinColumn.javaField}; 52 data.${subJoinColumn.javaField} = formData.value?.${subJoinColumn.javaField};
50 try { 53 try {
51 await (formData.value?.id ? update${subSimpleClassName}(data) : create${subSimpleClassName}(data)); 54 await (formData.value?.id ? update${subSimpleClassName}(data) : create${subSimpleClassName}(data));
@@ -64,7 +67,7 @@ @@ -64,7 +67,7 @@
64 } 67 }
65 68
66 // 加载数据 69 // 加载数据
67 - let data = modalApi.getData<${simpleClassName}Api.${subSimpleClassName}>(); 70 + let data = modalApi.getData<${apiName}.${subSimpleClassName}>();
68 if (!data) { 71 if (!data) {
69 return; 72 return;
70 } 73 }
yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/modules/form_sub_normal.vue.vm
@@ -4,10 +4,12 @@ @@ -4,10 +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 ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
7 <script lang="ts" setup> 8 <script lang="ts" setup>
8 - import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}'; 9 + import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
9 10
10 import { computed, ref, h, onMounted,watch,nextTick } from 'vue'; 11 import { computed, ref, h, onMounted,watch,nextTick } from 'vue';
  12 +
11 import { $t } from '#/locales'; 13 import { $t } from '#/locales';
12 14
13 #if ($subTable.subJoinMany) ## 一对多 15 #if ($subTable.subJoinMany) ## 一对多
@@ -33,7 +35,7 @@ const props = defineProps&lt;{ @@ -33,7 +35,7 @@ const props = defineProps&lt;{
33 function onActionClick({ 35 function onActionClick({
34 code, 36 code,
35 row, 37 row,
36 -}: OnActionClickParams<${simpleClassName}Api.${subSimpleClassName}>) { 38 +}: OnActionClickParams<${apiName}.${subSimpleClassName}>) {
37 switch (code) { 39 switch (code) {
38 case 'delete': { 40 case 'delete': {
39 onDelete(row); 41 onDelete(row);
@@ -67,16 +69,16 @@ const onAdd = async () =&gt; { @@ -67,16 +69,16 @@ const onAdd = async () =&gt; {
67 } 69 }
68 70
69 /** 删除${subTable.classComment} */ 71 /** 删除${subTable.classComment} */
70 -const onDelete = async (row: ${simpleClassName}Api.${subSimpleClassName}) => { 72 +const onDelete = async (row: ${apiName}.${subSimpleClassName}) => {
71 await gridApi.grid.remove(row); 73 await gridApi.grid.remove(row);
72 } 74 }
73 75
74 /** 提供获取表格数据的方法供父组件调用 */ 76 /** 提供获取表格数据的方法供父组件调用 */
75 defineExpose({ 77 defineExpose({
76 - getData: (): ${simpleClassName}Api.${subSimpleClassName}[] => {  
77 - const data = gridApi.grid.getData() as ${simpleClassName}Api.${subSimpleClassName}[];  
78 - const removeRecords = gridApi.grid.getRemoveRecords() as ${simpleClassName}Api.${subSimpleClassName}[];  
79 - const insertRecords = gridApi.grid.getInsertRecords() as ${simpleClassName}Api.${subSimpleClassName}[]; 78 + getData: (): ${apiName}.${subSimpleClassName}[] => {
  79 + const data = gridApi.grid.getData() as ${apiName}.${subSimpleClassName}[];
  80 + const removeRecords = gridApi.grid.getRemoveRecords() as ${apiName}.${subSimpleClassName}[];
  81 + const insertRecords = gridApi.grid.getInsertRecords() as ${apiName}.${subSimpleClassName}[];
80 return data 82 return data
81 .filter((row) => !removeRecords.some((removed) => removed.id === row.id)) 83 .filter((row) => !removeRecords.some((removed) => removed.id === row.id))
82 .concat(insertRecords.map((row: any) => ({ ...row, id: undefined }))); 84 .concat(insertRecords.map((row: any) => ({ ...row, id: undefined })));
yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/modules/list_sub_erp.vue.vm
@@ -5,9 +5,10 @@ @@ -5,9 +5,10 @@
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 ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
8 <script lang="ts" setup> 9 <script lang="ts" setup>
9 import type { VxeTableGridOptions } from '#/adapter/vxe-table'; 10 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
10 -import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}'; 11 +import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
11 12
12 #if ($table.templateType == 11) ## erp 13 #if ($table.templateType == 11) ## erp
13 import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue' 14 import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue'
@@ -41,6 +42,19 @@ const [FormModal, formModalApi] = useVbenModal({ @@ -41,6 +42,19 @@ const [FormModal, formModalApi] = useVbenModal({
41 destroyOnClose: true, 42 destroyOnClose: true,
42 }); 43 });
43 44
  45 +/** 刷新表格 */
  46 +async function handleRefresh() {
  47 +#if ($table.templateType == 11) ## erp
  48 + await gridApi.query();
  49 +#else
  50 + #if ($subTable.subJoinMany) ## 一对多
  51 + await gridApi.grid.loadData(await get${subSimpleClassName}ListBy${SubJoinColumnName}(props.${subJoinColumn.javaField}!));
  52 + #else
  53 + await gridApi.grid.loadData([await get${subSimpleClassName}By${SubJoinColumnName}(props.${subJoinColumn.javaField}!)]);
  54 + #end
  55 +#end
  56 +}
  57 +
44 /** 创建${subTable.classComment} */ 58 /** 创建${subTable.classComment} */
45 function handleCreate() { 59 function handleCreate() {
46 if (!props.${subJoinColumn.javaField}){ 60 if (!props.${subJoinColumn.javaField}){
@@ -51,20 +65,19 @@ function handleCreate() { @@ -51,20 +65,19 @@ function handleCreate() {
51 } 65 }
52 66
53 /** 编辑${subTable.classComment} */ 67 /** 编辑${subTable.classComment} */
54 -function handleEdit(row: ${simpleClassName}Api.${subSimpleClassName}) { 68 +function handleEdit(row: ${apiName}.${subSimpleClassName}) {
55 formModalApi.setData(row).open(); 69 formModalApi.setData(row).open();
56 } 70 }
57 71
58 /** 删除${subTable.classComment} */ 72 /** 删除${subTable.classComment} */
59 -async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) { 73 +async function handleDelete(row: ${apiName}.${subSimpleClassName}) {
60 const loadingInstance = ElLoading.service({ 74 const loadingInstance = ElLoading.service({
61 text: $t('ui.actionMessage.deleting', [row.id]), 75 text: $t('ui.actionMessage.deleting', [row.id]),
62 - background: 'rgba(0, 0, 0, 0.7)',  
63 }); 76 });
64 try { 77 try {
65 - await delete${subSimpleClassName}(row.id as number); 78 + await delete${subSimpleClassName}(row.id!);
66 ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.id])); 79 ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.id]));
67 - onRefresh(); 80 + handleRefresh();
68 } finally { 81 } finally {
69 loadingInstance.close(); 82 loadingInstance.close();
70 } 83 }
@@ -75,13 +88,12 @@ async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) { @@ -75,13 +88,12 @@ async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
75 async function handleDeleteBatch() { 88 async function handleDeleteBatch() {
76 const loadingInstance = ElLoading.service({ 89 const loadingInstance = ElLoading.service({
77 text: $t('ui.actionMessage.deleting'), 90 text: $t('ui.actionMessage.deleting'),
78 - background: 'rgba(0, 0, 0, 0.7)',  
79 }); 91 });
80 try { 92 try {
81 await delete${subSimpleClassName}List(checkedIds.value); 93 await delete${subSimpleClassName}List(checkedIds.value);
82 checkedIds.value = []; 94 checkedIds.value = [];
83 ElMessage.success($t('ui.actionMessage.deleteSuccess')); 95 ElMessage.success($t('ui.actionMessage.deleteSuccess'));
84 - onRefresh(); 96 + handleRefresh();
85 } finally { 97 } finally {
86 loadingInstance.close(); 98 loadingInstance.close();
87 } 99 }
@@ -91,9 +103,9 @@ const checkedIds = ref&lt;number[]&gt;([]) @@ -91,9 +103,9 @@ const checkedIds = ref&lt;number[]&gt;([])
91 function handleRowCheckboxChange({ 103 function handleRowCheckboxChange({
92 records, 104 records,
93 }: { 105 }: {
94 - records: ${simpleClassName}Api.${subSimpleClassName}[]; 106 + records: ${apiName}.${subSimpleClassName}[];
95 }) { 107 }) {
96 - checkedIds.value = records.map((item) => item.id); 108 + checkedIds.value = records.map((item) => item.id!);
97 } 109 }
98 #end 110 #end
99 111
@@ -143,7 +155,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ @@ -143,7 +155,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
143 keyField: 'id', 155 keyField: 'id',
144 isHover: true, 156 isHover: true,
145 }, 157 },
146 - } as VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>, 158 + } as VxeTableGridOptions<${apiName}.${subSimpleClassName}>,
147 #if (${table.templateType} == 11 && $deleteBatchEnable) 159 #if (${table.templateType} == 11 && $deleteBatchEnable)
148 gridEvents:{ 160 gridEvents:{
149 checkboxAll: handleRowCheckboxChange, 161 checkboxAll: handleRowCheckboxChange,
@@ -152,19 +164,6 @@ const [Grid, gridApi] = useVbenVxeGrid({ @@ -152,19 +164,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
152 #end 164 #end
153 }); 165 });
154 166
155 -/** 刷新表格 */  
156 -async function onRefresh() {  
157 -#if ($table.templateType == 11) ## erp  
158 - await gridApi.query();  
159 -#else  
160 - #if ($subTable.subJoinMany) ## 一对多  
161 - await gridApi.grid.loadData(await get${subSimpleClassName}ListBy${SubJoinColumnName}(props.${subJoinColumn.javaField}!));  
162 - #else  
163 - await gridApi.grid.loadData([await get${subSimpleClassName}By${SubJoinColumnName}(props.${subJoinColumn.javaField}!)]);  
164 - #end  
165 -#end  
166 -}  
167 -  
168 /** 监听主表的关联字段的变化,加载对应的子表数据 */ 167 /** 监听主表的关联字段的变化,加载对应的子表数据 */
169 watch( 168 watch(
170 () => props.${subJoinColumn.javaField}, 169 () => props.${subJoinColumn.javaField},
@@ -173,7 +172,7 @@ watch( @@ -173,7 +172,7 @@ watch(
173 return; 172 return;
174 } 173 }
175 await nextTick(); 174 await nextTick();
176 - await onRefresh() 175 + await handleRefresh()
177 }, 176 },
178 { immediate: true }, 177 { immediate: true },
179 ); 178 );
@@ -181,7 +180,7 @@ watch( @@ -181,7 +180,7 @@ watch(
181 180
182 <template> 181 <template>
183 #if ($table.templateType == 11) ## erp 182 #if ($table.templateType == 11) ## erp
184 - <FormModal @success="onRefresh" /> 183 + <FormModal @success="handleRefresh" />
185 <Grid table-title="${subTable.classComment}列表"> 184 <Grid table-title="${subTable.classComment}列表">
186 <template #toolbar-tools> 185 <template #toolbar-tools>
187 <TableAction 186 <TableAction
@@ -211,7 +210,8 @@ watch( @@ -211,7 +210,8 @@ watch(
211 :actions="[ 210 :actions="[
212 { 211 {
213 label: $t('common.edit'), 212 label: $t('common.edit'),
214 - type: 'text', 213 + type: 'primary',
  214 + link: true,
215 icon: ACTION_ICON.EDIT, 215 icon: ACTION_ICON.EDIT,
216 auth: ['${table.moduleName}:${simpleClassName_strikeCase}:update'], 216 auth: ['${table.moduleName}:${simpleClassName_strikeCase}:update'],
217 onClick: handleEdit.bind(null, row), 217 onClick: handleEdit.bind(null, row),
@@ -219,7 +219,7 @@ watch( @@ -219,7 +219,7 @@ watch(
219 { 219 {
220 label: $t('common.delete'), 220 label: $t('common.delete'),
221 type: 'danger', 221 type: 'danger',
222 - text: true, 222 + link: true,
223 icon: ACTION_ICON.DELETE, 223 icon: ACTION_ICON.DELETE,
224 auth: ['${table.moduleName}:${simpleClassName_strikeCase}:delete'], 224 auth: ['${table.moduleName}:${simpleClassName_strikeCase}:delete'],
225 popConfirm: { 225 popConfirm: {
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotAbstractDataSinkConfig.java
@@ -17,6 +17,8 @@ import lombok.Data; @@ -17,6 +17,8 @@ import lombok.Data;
17 @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true) 17 @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true)
18 @JsonSubTypes({ 18 @JsonSubTypes({
19 @JsonSubTypes.Type(value = IotDataSinkHttpConfig.class, name = "1"), 19 @JsonSubTypes.Type(value = IotDataSinkHttpConfig.class, name = "1"),
  20 + @JsonSubTypes.Type(value = IotDataSinkTcpConfig.class, name = "2"),
  21 + @JsonSubTypes.Type(value = IotDataSinkWebSocketConfig.class, name = "3"),
20 @JsonSubTypes.Type(value = IotDataSinkMqttConfig.class, name = "10"), 22 @JsonSubTypes.Type(value = IotDataSinkMqttConfig.class, name = "10"),
21 @JsonSubTypes.Type(value = IotDataSinkRedisConfig.class, name = "21"), 23 @JsonSubTypes.Type(value = IotDataSinkRedisConfig.class, name = "21"),
22 @JsonSubTypes.Type(value = IotDataSinkRocketMQConfig.class, name = "30"), 24 @JsonSubTypes.Type(value = IotDataSinkRocketMQConfig.class, name = "30"),
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkTcpConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * IoT TCP 配置 {@link IotAbstractDataSinkConfig} 实现类
  7 + *
  8 + * @author HUIHUI
  9 + */
  10 +@Data
  11 +public class IotDataSinkTcpConfig extends IotAbstractDataSinkConfig {
  12 +
  13 + /**
  14 + * TCP 服务器地址
  15 + */
  16 + private String host;
  17 +
  18 + /**
  19 + * TCP 服务器端口
  20 + */
  21 + private Integer port;
  22 +
  23 + /**
  24 + * 连接超时时间(毫秒)
  25 + */
  26 + private Integer connectTimeoutMs = 5000;
  27 +
  28 + /**
  29 + * 读取超时时间(毫秒)
  30 + */
  31 + private Integer readTimeoutMs = 10000;
  32 +
  33 + /**
  34 + * 是否启用 SSL
  35 + */
  36 + private Boolean ssl = false;
  37 +
  38 + /**
  39 + * SSL 证书路径(当 ssl=true 时需要)
  40 + */
  41 + private String sslCertPath;
  42 +
  43 + /**
  44 + * 数据格式:JSON 或 BINARY
  45 + */
  46 + private String dataFormat = "JSON";
  47 +
  48 + /**
  49 + * 心跳间隔时间(毫秒),0 表示不启用心跳
  50 + */
  51 + private Long heartbeatIntervalMs = 30000L;
  52 +
  53 + /**
  54 + * 重连间隔时间(毫秒)
  55 + */
  56 + private Long reconnectIntervalMs = 5000L;
  57 +
  58 + /**
  59 + * 最大重连次数
  60 + */
  61 + private Integer maxReconnectAttempts = 3;
  62 +
  63 +}
0 \ No newline at end of file 64 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkWebSocketConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * IoT WebSocket 配置 {@link IotAbstractDataSinkConfig} 实现类
  7 + * <p>
  8 + * 配置设备消息通过 WebSocket 协议发送到外部 WebSocket 服务器
  9 + * 支持 WebSocket (ws://) 和 WebSocket Secure (wss://) 连接
  10 + *
  11 + * @author HUIHUI
  12 + */
  13 +@Data
  14 +public class IotDataSinkWebSocketConfig extends IotAbstractDataSinkConfig {
  15 +
  16 + /**
  17 + * WebSocket 服务器地址
  18 + * 例如:ws://localhost:8080/ws 或 wss://example.com/ws
  19 + */
  20 + private String serverUrl;
  21 +
  22 + /**
  23 + * 连接超时时间(毫秒)
  24 + */
  25 + private Integer connectTimeoutMs = 5000;
  26 +
  27 + /**
  28 + * 发送超时时间(毫秒)
  29 + */
  30 + private Integer sendTimeoutMs = 10000;
  31 +
  32 + /**
  33 + * 心跳间隔时间(毫秒),0 表示不启用心跳
  34 + */
  35 + private Long heartbeatIntervalMs = 30000L;
  36 +
  37 + /**
  38 + * 心跳消息内容(JSON 格式)
  39 + */
  40 + private String heartbeatMessage = "{\"type\":\"heartbeat\"}";
  41 +
  42 + /**
  43 + * 子协议列表(逗号分隔)
  44 + */
  45 + private String subprotocols;
  46 +
  47 + /**
  48 + * 自定义请求头(JSON 格式)
  49 + */
  50 + private String customHeaders;
  51 +
  52 + /**
  53 + * 是否启用 SSL 证书验证(仅对 wss:// 生效)
  54 + */
  55 + private Boolean verifySslCert = true;
  56 +
  57 + /**
  58 + * 数据格式:JSON 或 TEXT
  59 + */
  60 + private String dataFormat = "JSON";
  61 +
  62 + /**
  63 + * 重连间隔时间(毫秒)
  64 + */
  65 + private Long reconnectIntervalMs = 5000L;
  66 +
  67 + /**
  68 + * 最大重连次数
  69 + */
  70 + private Integer maxReconnectAttempts = 3;
  71 +
  72 + /**
  73 + * 是否启用压缩
  74 + */
  75 + private Boolean enableCompression = false;
  76 +
  77 + /**
  78 + * 消息发送重试次数
  79 + */
  80 + private Integer sendRetryCount = 1;
  81 +
  82 + /**
  83 + * 消息发送重试间隔(毫秒)
  84 + */
  85 + private Long sendRetryIntervalMs = 1000L;
  86 +
  87 +}
0 \ No newline at end of file 88 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java
@@ -76,4 +76,12 @@ public interface RedisKeyConstants { @@ -76,4 +76,12 @@ public interface RedisKeyConstants {
76 */ 76 */
77 String DATA_SINK = "iot:data_sink"; 77 String DATA_SINK = "iot:data_sink";
78 78
  79 + /**
  80 + * 场景联动规则的数据缓存,使用 Spring Cache 操作
  81 + * <p>
  82 + * KEY 格式:scene_rule_list_${productId}_${deviceId}
  83 + * VALUE 数据类型:String 数组(JSON),即 {@link cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO} 列表
  84 + */
  85 + String SCENE_RULE_LIST = "iot:scene_rule_list";
  86 +
79 } 87 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleTriggerTypeEnum.java
@@ -19,7 +19,6 @@ import java.util.Arrays; @@ -19,7 +19,6 @@ import java.util.Arrays;
19 @Getter 19 @Getter
20 public enum IotSceneRuleTriggerTypeEnum implements ArrayValuable<Integer> { 20 public enum IotSceneRuleTriggerTypeEnum implements ArrayValuable<Integer> {
21 21
22 - // TODO @芋艿:后续“对应”部分,要 @下,等包结构梳理完;  
23 /** 22 /**
24 * 设备上下线变更 23 * 设备上下线变更
25 * 24 *
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleAction.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.data.action;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  4 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig;
  5 +import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum;
  6 +import cn.iocoder.yudao.module.iot.service.rule.data.action.tcp.IotTcpClient;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +import org.springframework.stereotype.Component;
  9 +
  10 +import java.time.Duration;
  11 +
  12 +/**
  13 + * TCP 的 {@link IotDataRuleAction} 实现类
  14 + * <p>
  15 + * 负责将设备消息发送到外部 TCP 服务器
  16 + * 支持普通 TCP 和 SSL TCP 连接,支持 JSON 和 BINARY 数据格式
  17 + * 使用连接池管理 TCP 连接,提高性能和资源利用率
  18 + *
  19 + * @author HUIHUI
  20 + */
  21 +@Component
  22 +@Slf4j
  23 +public class IotTcpDataRuleAction extends
  24 + IotDataRuleCacheableAction<IotDataSinkTcpConfig, IotTcpClient> {
  25 +
  26 + private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(5);
  27 + private static final Duration SEND_TIMEOUT = Duration.ofSeconds(10);
  28 +
  29 + @Override
  30 + public Integer getType() {
  31 + return IotDataSinkTypeEnum.TCP.getType();
  32 + }
  33 +
  34 + @Override
  35 + protected IotTcpClient initProducer(IotDataSinkTcpConfig config) throws Exception {
  36 + // 1.1 参数校验
  37 + if (config.getHost() == null || config.getHost().trim().isEmpty()) {
  38 + throw new IllegalArgumentException("TCP 服务器地址不能为空");
  39 + }
  40 + if (config.getPort() == null || config.getPort() <= 0 || config.getPort() > 65535) {
  41 + throw new IllegalArgumentException("TCP 服务器端口无效");
  42 + }
  43 +
  44 + // 2.1 创建 TCP 客户端
  45 + IotTcpClient tcpClient = new IotTcpClient(
  46 + config.getHost(),
  47 + config.getPort(),
  48 + config.getConnectTimeoutMs(),
  49 + config.getReadTimeoutMs(),
  50 + config.getSsl(),
  51 + config.getSslCertPath(),
  52 + config.getDataFormat()
  53 + );
  54 + // 2.2 连接服务器
  55 + tcpClient.connect();
  56 + log.info("[initProducer][TCP 客户端创建并连接成功,服务器: {}:{},SSL: {},数据格式: {}]",
  57 + config.getHost(), config.getPort(), config.getSsl(), config.getDataFormat());
  58 + return tcpClient;
  59 + }
  60 +
  61 + @Override
  62 + protected void closeProducer(IotTcpClient producer) throws Exception {
  63 + if (producer != null) {
  64 + producer.close();
  65 + }
  66 + }
  67 +
  68 + @Override
  69 + protected void execute(IotDeviceMessage message, IotDataSinkTcpConfig config) throws Exception {
  70 + try {
  71 + // 1.1 获取或创建 TCP 客户端
  72 + IotTcpClient tcpClient = getProducer(config);
  73 + // 1.2 检查连接状态,如果断开则重新连接
  74 + if (!tcpClient.isConnected()) {
  75 + log.warn("[execute][TCP 连接已断开,尝试重新连接,服务器: {}:{}]", config.getHost(), config.getPort());
  76 + tcpClient.connect();
  77 + }
  78 +
  79 + // 2.1 发送消息并等待结果
  80 + tcpClient.sendMessage(message);
  81 + // 2.2 记录发送成功日志
  82 + log.info("[execute][message({}) config({}) 发送成功,TCP 服务器: {}:{}]",
  83 + message, config, config.getHost(), config.getPort());
  84 + } catch (Exception e) {
  85 + log.error("[execute][message({}) config({}) 发送失败,TCP 服务器: {}:{}]",
  86 + message, config, config.getHost(), config.getPort(), e);
  87 + throw e;
  88 + }
  89 + }
  90 +
  91 +}
0 \ No newline at end of file 92 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/tcp/IotTcpClient.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.data.action.tcp;
  2 +
  3 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  4 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import lombok.extern.slf4j.Slf4j;
  6 +
  7 +import javax.net.ssl.SSLSocketFactory;
  8 +import java.io.BufferedReader;
  9 +import java.io.IOException;
  10 +import java.io.InputStreamReader;
  11 +import java.io.OutputStream;
  12 +import java.net.InetSocketAddress;
  13 +import java.net.Socket;
  14 +import java.nio.charset.StandardCharsets;
  15 +import java.util.concurrent.atomic.AtomicBoolean;
  16 +
  17 +/**
  18 + * IoT TCP 客户端
  19 + * <p>
  20 + * 负责与外部 TCP 服务器建立连接并发送设备消息
  21 + * 支持 JSON 和 BINARY 两种数据格式,支持 SSL 加密连接
  22 + *
  23 + * @author HUIHUI
  24 + */
  25 +@Slf4j
  26 +public class IotTcpClient {
  27 +
  28 + private final String host;
  29 + private final Integer port;
  30 + private final Integer connectTimeoutMs;
  31 + private final Integer readTimeoutMs;
  32 + private final Boolean ssl;
  33 + private final String sslCertPath;
  34 + private final String dataFormat;
  35 +
  36 + private Socket socket;
  37 + private OutputStream outputStream;
  38 + private BufferedReader reader;
  39 + private final AtomicBoolean connected = new AtomicBoolean(false);
  40 +
  41 + // TODO @puhui999:default 值,IotDataSinkTcpConfig.java 枚举起来哈;
  42 + public IotTcpClient(String host, Integer port, Integer connectTimeoutMs, Integer readTimeoutMs,
  43 + Boolean ssl, String sslCertPath, String dataFormat) {
  44 + this.host = host;
  45 + this.port = port;
  46 + this.connectTimeoutMs = connectTimeoutMs != null ? connectTimeoutMs : 5000;
  47 + this.readTimeoutMs = readTimeoutMs != null ? readTimeoutMs : 10000;
  48 + this.ssl = ssl != null ? ssl : false;
  49 + this.sslCertPath = sslCertPath;
  50 + this.dataFormat = dataFormat != null ? dataFormat : "JSON";
  51 + }
  52 +
  53 + /**
  54 + * 连接到 TCP 服务器
  55 + */
  56 + public void connect() throws Exception {
  57 + if (connected.get()) {
  58 + log.warn("[connect][TCP 客户端已经连接,无需重复连接]");
  59 + return;
  60 + }
  61 +
  62 + try {
  63 + if (ssl) {
  64 + // SSL 连接
  65 + SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
  66 + socket = sslSocketFactory.createSocket();
  67 + } else {
  68 + // 普通连接
  69 + socket = new Socket();
  70 + }
  71 +
  72 + // 连接服务器
  73 + socket.connect(new InetSocketAddress(host, port), connectTimeoutMs);
  74 + socket.setSoTimeout(readTimeoutMs);
  75 +
  76 + // 获取输入输出流
  77 + outputStream = socket.getOutputStream();
  78 + reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
  79 +
  80 + // 更新状态
  81 + connected.set(true);
  82 + log.info("[connect][TCP 客户端连接成功,服务器地址: {}:{}]", host, port);
  83 + } catch (Exception e) {
  84 + close();
  85 + log.error("[connect][TCP 客户端连接失败,服务器地址: {}:{}]", host, port, e);
  86 + throw e;
  87 + }
  88 + }
  89 +
  90 + /**
  91 + * 发送设备消息
  92 + *
  93 + * @param message 设备消息
  94 + * @throws Exception 发送异常
  95 + */
  96 + public void sendMessage(IotDeviceMessage message) throws Exception {
  97 + if (!connected.get()) {
  98 + throw new IllegalStateException("TCP 客户端未连接");
  99 + }
  100 +
  101 + try {
  102 + // TODO @puhui999:枚举值
  103 + String messageData;
  104 + if ("JSON".equalsIgnoreCase(dataFormat)) {
  105 + // JSON 格式
  106 + messageData = JsonUtils.toJsonString(message);
  107 + } else {
  108 + // BINARY 格式(这里简化为字符串,实际可能需要自定义二进制协议)
  109 + messageData = message.toString();
  110 + }
  111 +
  112 + // 发送消息
  113 + outputStream.write(messageData.getBytes(StandardCharsets.UTF_8));
  114 + outputStream.write('\n'); // 添加换行符作为消息分隔符
  115 + outputStream.flush();
  116 + log.debug("[sendMessage][发送消息成功,设备 ID: {},消息长度: {}]",
  117 + message.getDeviceId(), messageData.length());
  118 + } catch (Exception e) {
  119 + log.error("[sendMessage][发送消息失败,设备 ID: {}]", message.getDeviceId(), e);
  120 + throw e;
  121 + }
  122 + }
  123 +
  124 + /**
  125 + * 关闭连接
  126 + */
  127 + public void close() {
  128 + if (!connected.get()) {
  129 + return;
  130 + }
  131 +
  132 + try {
  133 + // 关闭资源
  134 + if (reader != null) {
  135 + try {
  136 + reader.close();
  137 + } catch (IOException e) {
  138 + log.warn("[close][关闭输入流失败]", e);
  139 + }
  140 + }
  141 + if (outputStream != null) {
  142 + try {
  143 + outputStream.close();
  144 + } catch (IOException e) {
  145 + log.warn("[close][关闭输出流失败]", e);
  146 + }
  147 + }
  148 + if (socket != null) {
  149 + try {
  150 + socket.close();
  151 + } catch (IOException e) {
  152 + log.warn("[close][关闭 Socket 失败]", e);
  153 + }
  154 + }
  155 +
  156 + // 更新状态
  157 + connected.set(false);
  158 + log.info("[close][TCP 客户端连接已关闭,服务器地址: {}:{}]", host, port);
  159 + } catch (Exception e) {
  160 + log.error("[close][关闭 TCP 客户端连接异常]", e);
  161 + }
  162 + }
  163 +
  164 + /**
  165 + * 检查连接状态
  166 + *
  167 + * @return 是否已连接
  168 + */
  169 + public boolean isConnected() {
  170 + return connected.get() && socket != null && !socket.isClosed();
  171 + }
  172 +
  173 + @Override
  174 + public String toString() {
  175 + return "IotTcpClient{" +
  176 + "host='" + host + '\'' +
  177 + ", port=" + port +
  178 + ", ssl=" + ssl +
  179 + ", dataFormat='" + dataFormat + '\'' +
  180 + ", connected=" + connected.get() +
  181 + '}';
  182 + }
  183 +
  184 +}
0 \ No newline at end of file 185 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceImpl.java
@@ -16,14 +16,17 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; @@ -16,14 +16,17 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
16 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; 16 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
17 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 17 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
18 import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotSceneRuleMapper; 18 import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotSceneRuleMapper;
  19 +import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants;
19 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; 20 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
20 -import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager;  
21 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; 21 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
22 import cn.iocoder.yudao.module.iot.service.product.IotProductService; 22 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
23 import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction; 23 import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction;
24 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherManager; 24 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherManager;
  25 +import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler;
25 import jakarta.annotation.Resource; 26 import jakarta.annotation.Resource;
26 import lombok.extern.slf4j.Slf4j; 27 import lombok.extern.slf4j.Slf4j;
  28 +import org.springframework.cache.annotation.CacheEvict;
  29 +import org.springframework.cache.annotation.Cacheable;
27 import org.springframework.stereotype.Service; 30 import org.springframework.stereotype.Service;
28 import org.springframework.validation.annotation.Validated; 31 import org.springframework.validation.annotation.Validated;
29 32
@@ -47,9 +50,6 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -47,9 +50,6 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
47 @Resource 50 @Resource
48 private IotSceneRuleMapper sceneRuleMapper; 51 private IotSceneRuleMapper sceneRuleMapper;
49 52
50 - // TODO @puhui999:定时任务,基于它调度;  
51 - @Resource(name = "iotSchedulerManager")  
52 - private IotSchedulerManager schedulerManager;  
53 @Resource 53 @Resource
54 private IotProductService productService; 54 private IotProductService productService;
55 @Resource 55 @Resource
@@ -59,38 +59,68 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -59,38 +59,68 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
59 private IotSceneRuleMatcherManager sceneRuleMatcherManager; 59 private IotSceneRuleMatcherManager sceneRuleMatcherManager;
60 @Resource 60 @Resource
61 private List<IotSceneRuleAction> sceneRuleActions; 61 private List<IotSceneRuleAction> sceneRuleActions;
  62 + @Resource
  63 + private IotSceneRuleTimerHandler timerHandler;
62 64
63 @Override 65 @Override
  66 + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
64 public Long createSceneRule(IotSceneRuleSaveReqVO createReqVO) { 67 public Long createSceneRule(IotSceneRuleSaveReqVO createReqVO) {
65 IotSceneRuleDO sceneRule = BeanUtils.toBean(createReqVO, IotSceneRuleDO.class); 68 IotSceneRuleDO sceneRule = BeanUtils.toBean(createReqVO, IotSceneRuleDO.class);
66 sceneRuleMapper.insert(sceneRule); 69 sceneRuleMapper.insert(sceneRule);
  70 +
  71 + // 注册定时触发器
  72 + timerHandler.registerTimerTriggers(sceneRule);
  73 +
67 return sceneRule.getId(); 74 return sceneRule.getId();
68 } 75 }
69 76
70 @Override 77 @Override
  78 + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
71 public void updateSceneRule(IotSceneRuleSaveReqVO updateReqVO) { 79 public void updateSceneRule(IotSceneRuleSaveReqVO updateReqVO) {
72 // 校验存在 80 // 校验存在
73 validateSceneRuleExists(updateReqVO.getId()); 81 validateSceneRuleExists(updateReqVO.getId());
74 // 更新 82 // 更新
75 IotSceneRuleDO updateObj = BeanUtils.toBean(updateReqVO, IotSceneRuleDO.class); 83 IotSceneRuleDO updateObj = BeanUtils.toBean(updateReqVO, IotSceneRuleDO.class);
76 sceneRuleMapper.updateById(updateObj); 84 sceneRuleMapper.updateById(updateObj);
  85 +
  86 + // 更新定时触发器
  87 + timerHandler.updateTimerTriggers(updateObj);
77 } 88 }
78 89
79 @Override 90 @Override
  91 + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
80 public void updateSceneRuleStatus(Long id, Integer status) { 92 public void updateSceneRuleStatus(Long id, Integer status) {
81 - // 校验存在 93 + // 1. 校验存在
82 validateSceneRuleExists(id); 94 validateSceneRuleExists(id);
83 - // 更新状态 95 +
  96 + // 2. 更新状态
84 IotSceneRuleDO updateObj = new IotSceneRuleDO().setId(id).setStatus(status); 97 IotSceneRuleDO updateObj = new IotSceneRuleDO().setId(id).setStatus(status);
85 sceneRuleMapper.updateById(updateObj); 98 sceneRuleMapper.updateById(updateObj);
  99 +
  100 + // 3. 根据状态管理定时触发器
  101 + if (CommonStatusEnum.isEnable(status)) {
  102 + // 启用时,获取完整的场景规则信息并注册定时触发器
  103 + IotSceneRuleDO sceneRule = sceneRuleMapper.selectById(id);
  104 + if (sceneRule != null) {
  105 + timerHandler.registerTimerTriggers(sceneRule);
  106 + }
  107 + } else {
  108 + // 禁用时,暂停定时触发器
  109 + timerHandler.pauseTimerTriggers(id);
  110 + }
86 } 111 }
87 112
88 @Override 113 @Override
  114 + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
89 public void deleteSceneRule(Long id) { 115 public void deleteSceneRule(Long id) {
90 - // 校验存在 116 + // 1. 校验存在
91 validateSceneRuleExists(id); 117 validateSceneRuleExists(id);
92 - // 删除 118 +
  119 + // 2. 删除
93 sceneRuleMapper.deleteById(id); 120 sceneRuleMapper.deleteById(id);
  121 +
  122 + // 3. 删除定时触发器
  123 + timerHandler.unregisterTimerTriggers(id);
94 } 124 }
95 125
96 private void validateSceneRuleExists(Long id) { 126 private void validateSceneRuleExists(Long id) {
@@ -126,16 +156,14 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -126,16 +156,14 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
126 return sceneRuleMapper.selectListByStatus(status); 156 return sceneRuleMapper.selectListByStatus(status);
127 } 157 }
128 158
129 - // TODO 芋艿,缓存待实现 @puhui999  
130 @Override 159 @Override
  160 + @Cacheable(value = RedisKeyConstants.SCENE_RULE_LIST, key = "#productId + '_' + #deviceId ")
131 @TenantIgnore // 忽略租户隔离:因为 IotSceneRuleMessageHandler 调用时,一般未传递租户,所以需要忽略 161 @TenantIgnore // 忽略租户隔离:因为 IotSceneRuleMessageHandler 调用时,一般未传递租户,所以需要忽略
132 public List<IotSceneRuleDO> getSceneRuleListByProductIdAndDeviceIdFromCache(Long productId, Long deviceId) { 162 public List<IotSceneRuleDO> getSceneRuleListByProductIdAndDeviceIdFromCache(Long productId, Long deviceId) {
133 - List<IotSceneRuleDO> list = sceneRuleMapper.selectList();  
134 - // 只返回启用状态的规则场景  
135 - List<IotSceneRuleDO> enabledList = filterList(list,  
136 - sceneRule -> CommonStatusEnum.isEnable(sceneRule.getStatus())); 163 + // 1. 查询启用状态的规则场景
  164 + List<IotSceneRuleDO> enabledList = sceneRuleMapper.selectList(IotSceneRuleDO::getStatus, CommonStatusEnum.ENABLE.getStatus());
137 165
138 - // 根据 productKey 和 deviceName 进行匹配 166 + // 2. 根据 productKey 和 deviceName 进行匹配
139 return filterList(enabledList, sceneRule -> { 167 return filterList(enabledList, sceneRule -> {
140 if (CollUtil.isEmpty(sceneRule.getTriggers())) { 168 if (CollUtil.isEmpty(sceneRule.getTriggers())) {
141 return false; 169 return false;
@@ -144,21 +172,19 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -144,21 +172,19 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
144 for (IotSceneRuleDO.Trigger trigger : sceneRule.getTriggers()) { 172 for (IotSceneRuleDO.Trigger trigger : sceneRule.getTriggers()) {
145 // 检查触发器是否匹配指定的产品和设备 173 // 检查触发器是否匹配指定的产品和设备
146 try { 174 try {
147 - // 1. 检查产品是否匹配  
148 - if (trigger.getProductId() == null) {  
149 - return false;  
150 - }  
151 - if (trigger.getDeviceId() == null) { 175 + // 检查产品是否匹配
  176 + if (trigger.getProductId() == null || trigger.getDeviceId() == null) {
152 return false; 177 return false;
153 } 178 }
154 // 检查是否是全部设备的特殊标识 179 // 检查是否是全部设备的特殊标识
155 if (IotDeviceDO.DEVICE_ID_ALL.equals(trigger.getDeviceId())) { 180 if (IotDeviceDO.DEVICE_ID_ALL.equals(trigger.getDeviceId())) {
156 - return true; // 匹配所有设备 181 + return true;
157 } 182 }
158 // 检查具体设备 ID 是否匹配 183 // 检查具体设备 ID 是否匹配
159 return ObjUtil.equal(productId, trigger.getProductId()) && ObjUtil.equal(deviceId, trigger.getDeviceId()); 184 return ObjUtil.equal(productId, trigger.getProductId()) && ObjUtil.equal(deviceId, trigger.getDeviceId());
160 } catch (Exception e) { 185 } catch (Exception e) {
161 - log.warn("[isMatchProductAndDevice][产品({}) 设备({}) 匹配触发器异常]", productId, deviceId, e); 186 + log.warn("[getSceneRuleListByProductIdAndDeviceIdFromCache][产品({}) 设备({}) 匹配触发器异常]",
  187 + productId, deviceId, e);
162 return false; 188 return false;
163 } 189 }
164 } 190 }
@@ -168,9 +194,10 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -168,9 +194,10 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
168 194
169 @Override 195 @Override
170 public void executeSceneRuleByDevice(IotDeviceMessage message) { 196 public void executeSceneRuleByDevice(IotDeviceMessage message) {
171 - // TODO @芋艿:这里的 tenantId,通过设备获取;@puhui999:  
172 - TenantUtils.execute(message.getTenantId(), () -> {  
173 - // 1. 获得设备匹配的规则场景 197 + // 1.1 这里的 tenantId,通过设备获取;
  198 + IotDeviceDO device = deviceService.getDeviceFromCache(message.getDeviceId());
  199 + TenantUtils.execute(device.getTenantId(), () -> {
  200 + // 1.2 获得设备匹配的规则场景
174 List<IotSceneRuleDO> sceneRules = getMatchedSceneRuleListByMessage(message); 201 List<IotSceneRuleDO> sceneRules = getMatchedSceneRuleListByMessage(message);
175 if (CollUtil.isEmpty(sceneRules)) { 202 if (CollUtil.isEmpty(sceneRules)) {
176 return; 203 return;
@@ -214,16 +241,16 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -214,16 +241,16 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
214 */ 241 */
215 private List<IotSceneRuleDO> getMatchedSceneRuleListByMessage(IotDeviceMessage message) { 242 private List<IotSceneRuleDO> getMatchedSceneRuleListByMessage(IotDeviceMessage message) {
216 // 1. 匹配设备 243 // 1. 匹配设备
217 - // TODO @芋艿:可能需要 getSelf(); 缓存 @puhui999; 244 + // TODO 缓存 @puhui999:可能需要 getSelf()
218 // 1.1 通过 deviceId 获取设备信息 245 // 1.1 通过 deviceId 获取设备信息
219 - IotDeviceDO device = deviceService.getDeviceFromCache(message.getDeviceId()); 246 + IotDeviceDO device = getSelf().deviceService.getDeviceFromCache(message.getDeviceId());
220 if (device == null) { 247 if (device == null) {
221 log.warn("[getMatchedSceneRuleListByMessage][设备({}) 不存在]", message.getDeviceId()); 248 log.warn("[getMatchedSceneRuleListByMessage][设备({}) 不存在]", message.getDeviceId());
222 return List.of(); 249 return List.of();
223 } 250 }
224 251
225 // 1.2 通过 productId 获取产品信息 252 // 1.2 通过 productId 获取产品信息
226 - IotProductDO product = productService.getProductFromCache(device.getProductId()); 253 + IotProductDO product = getSelf().productService.getProductFromCache(device.getProductId());
227 if (product == null) { 254 if (product == null) {
228 log.warn("[getMatchedSceneRuleListByMessage][产品({}) 不存在]", device.getProductId()); 255 log.warn("[getMatchedSceneRuleListByMessage][产品({}) 不存在]", device.getProductId());
229 return List.of(); 256 return List.of();
@@ -273,7 +300,6 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -273,7 +300,6 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
273 */ 300 */
274 private boolean matchSingleTrigger(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, IotSceneRuleDO sceneRule) { 301 private boolean matchSingleTrigger(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, IotSceneRuleDO sceneRule) {
275 try { 302 try {
276 - // 2. 检查触发器的条件分组  
277 return sceneRuleMatcherManager.isMatched(message, trigger) && isTriggerConditionGroupsMatched(message, trigger, sceneRule); 303 return sceneRuleMatcherManager.isMatched(message, trigger) && isTriggerConditionGroupsMatched(message, trigger, sceneRule);
278 } catch (Exception e) { 304 } catch (Exception e) {
279 log.error("[matchSingleTrigger][触发器匹配异常] sceneRuleId: {}, triggerType: {}, message: {}", 305 log.error("[matchSingleTrigger][触发器匹配异常] sceneRuleId: {}, triggerType: {}, message: {}",
@@ -290,18 +316,19 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -290,18 +316,19 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
290 * @param sceneRule 场景规则(用于日志) 316 * @param sceneRule 场景规则(用于日志)
291 * @return 是否匹配 317 * @return 是否匹配
292 */ 318 */
293 - private boolean isTriggerConditionGroupsMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, IotSceneRuleDO sceneRule) {  
294 - // 如果没有条件分组,则认为匹配成功(只依赖基础触发器匹配) 319 + private boolean isTriggerConditionGroupsMatched(IotDeviceMessage message,
  320 + IotSceneRuleDO.Trigger trigger,
  321 + IotSceneRuleDO sceneRule) {
  322 + // 1. 如果没有条件分组,则认为匹配成功(只依赖基础触发器匹配)
295 if (CollUtil.isEmpty(trigger.getConditionGroups())) { 323 if (CollUtil.isEmpty(trigger.getConditionGroups())) {
296 return true; 324 return true;
297 } 325 }
298 326
299 - // 检查条件分组:分组与分组之间是"或"的关系,条件与条件之间是"且"的关系 327 + // 2. 检查条件分组:分组与分组之间是"或"的关系,条件与条件之间是"且"的关系
300 for (List<IotSceneRuleDO.TriggerCondition> conditionGroup : trigger.getConditionGroups()) { 328 for (List<IotSceneRuleDO.TriggerCondition> conditionGroup : trigger.getConditionGroups()) {
301 if (CollUtil.isEmpty(conditionGroup)) { 329 if (CollUtil.isEmpty(conditionGroup)) {
302 continue; 330 continue;
303 } 331 }
304 -  
305 // 检查当前分组中的所有条件是否都匹配(且关系) 332 // 检查当前分组中的所有条件是否都匹配(且关系)
306 boolean allConditionsMatched = true; 333 boolean allConditionsMatched = true;
307 for (IotSceneRuleDO.TriggerCondition condition : conditionGroup) { 334 for (IotSceneRuleDO.TriggerCondition condition : conditionGroup) {
@@ -310,14 +337,13 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -310,14 +337,13 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
310 break; 337 break;
311 } 338 }
312 } 339 }
313 -  
314 // 如果当前分组的所有条件都匹配,则整个触发器匹配成功 340 // 如果当前分组的所有条件都匹配,则整个触发器匹配成功
315 if (allConditionsMatched) { 341 if (allConditionsMatched) {
316 return true; 342 return true;
317 } 343 }
318 } 344 }
319 345
320 - // 所有分组都不匹配 346 + // 3. 所有分组都不匹配
321 return false; 347 return false;
322 } 348 }
323 349
@@ -352,13 +378,13 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -352,13 +378,13 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
352 sceneRules.forEach(sceneRule -> { 378 sceneRules.forEach(sceneRule -> {
353 // 2. 遍历规则场景的动作 379 // 2. 遍历规则场景的动作
354 sceneRule.getActions().forEach(actionConfig -> { 380 sceneRule.getActions().forEach(actionConfig -> {
355 - // 3.1 获取对应的动作 Action 数组 381 + // 2.1 获取对应的动作 Action 数组
356 List<IotSceneRuleAction> actions = filterList(sceneRuleActions, 382 List<IotSceneRuleAction> actions = filterList(sceneRuleActions,
357 action -> action.getType().getType().equals(actionConfig.getType())); 383 action -> action.getType().getType().equals(actionConfig.getType()));
358 if (CollUtil.isEmpty(actions)) { 384 if (CollUtil.isEmpty(actions)) {
359 return; 385 return;
360 } 386 }
361 - // 3.2 执行动作 387 + // 2.2 执行动作
362 actions.forEach(action -> { 388 actions.forEach(action -> {
363 try { 389 try {
364 action.execute(message, sceneRule, actionConfig); 390 action.execute(message, sceneRule, actionConfig);
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertRecoverSceneRuleAction.java
@@ -14,7 +14,6 @@ import java.util.List; @@ -14,7 +14,6 @@ import java.util.List;
14 14
15 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; 15 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
16 16
17 -// TODO @puhui999、@芋艿:未测试;需要场景联动开发完  
18 /** 17 /**
19 * IoT 告警恢复的 {@link IotSceneRuleAction} 实现类 18 * IoT 告警恢复的 {@link IotSceneRuleAction} 实现类
20 * 19 *
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleAction.java
@@ -17,7 +17,6 @@ import org.springframework.stereotype.Component; @@ -17,7 +17,6 @@ import org.springframework.stereotype.Component;
17 import javax.annotation.Nullable; 17 import javax.annotation.Nullable;
18 import java.util.List; 18 import java.util.List;
19 19
20 -// TODO @puhui999、@芋艿:未测试;需要场景联动开发完  
21 /** 20 /**
22 * IoT 告警触发的 {@link IotSceneRuleAction} 实现类 21 * IoT 告警触发的 {@link IotSceneRuleAction} 实现类
23 * 22 *
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceControlSceneRuleAction.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.action; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.action;
2 2
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  7 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 8 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
5 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum; 9 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum;
6 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; 10 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
@@ -9,8 +13,11 @@ import jakarta.annotation.Resource; @@ -9,8 +13,11 @@ import jakarta.annotation.Resource;
9 import lombok.extern.slf4j.Slf4j; 13 import lombok.extern.slf4j.Slf4j;
10 import org.springframework.stereotype.Component; 14 import org.springframework.stereotype.Component;
11 15
  16 +import java.util.List;
  17 +import java.util.Map;
  18 +
12 /** 19 /**
13 - * IoT 设备控制的 {@link IotSceneRuleAction} 实现类 20 + * IoT 设备属性设置的 {@link IotSceneRuleAction} 实现类
14 * 21 *
15 * @author 芋道源码 22 * @author 芋道源码
16 */ 23 */
@@ -23,28 +30,108 @@ public class IotDeviceControlSceneRuleAction implements IotSceneRuleAction { @@ -23,28 +30,108 @@ public class IotDeviceControlSceneRuleAction implements IotSceneRuleAction {
23 @Resource 30 @Resource
24 private IotDeviceMessageService deviceMessageService; 31 private IotDeviceMessageService deviceMessageService;
25 32
26 - // TODO @puhui999:这里  
27 @Override 33 @Override
28 public void execute(IotDeviceMessage message, 34 public void execute(IotDeviceMessage message,
29 IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) { 35 IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
30 - //IotSceneRuleDO.ActionDeviceControl control = actionConfig.getDeviceControl();  
31 - //Assert.notNull(control, "设备控制配置不能为空");  
32 - //// 遍历每个设备,下发消息  
33 - //control.getDeviceNames().forEach(deviceName -> {  
34 - // IotDeviceDO device = deviceService.getDeviceFromCache(control.getProductKey(), deviceName);  
35 - // if (device == null) {  
36 - // log.error("[execute][message({}) actionConfig({}) 对应的设备不存在]", message, actionConfig);  
37 - // return;  
38 - // }  
39 - // try {  
40 - // // TODO @芋艿:@puhui999:这块可能要改,从 type => method  
41 - // IotDeviceMessage downstreamMessage = deviceMessageService.sendDeviceMessage(IotDeviceMessage.requestOf(  
42 - // control.getType() + control.getIdentifier(), control.getData()).setDeviceId(device.getId()));  
43 - // log.info("[execute][message({}) actionConfig({}) 下发消息({})成功]", message, actionConfig, downstreamMessage);  
44 - // } catch (Exception e) {  
45 - // log.error("[execute][message({}) actionConfig({}) 下发消息失败]", message, actionConfig, e);  
46 - // }  
47 - //}); 36 + // 1. 参数校验
  37 + if (actionConfig.getDeviceId() == null) {
  38 + log.error("[execute][规则场景({}) 动作配置({}) 设备编号不能为空]", rule.getId(), actionConfig);
  39 + return;
  40 + }
  41 + if (StrUtil.isEmpty(actionConfig.getIdentifier())) {
  42 + log.error("[execute][规则场景({}) 动作配置({}) 属性标识符不能为空]", rule.getId(), actionConfig);
  43 + return;
  44 + }
  45 +
  46 + // 2. 判断是否为全部设备
  47 + if (IotDeviceDO.DEVICE_ID_ALL.equals(actionConfig.getDeviceId())) {
  48 + executeForAllDevices(message, rule, actionConfig);
  49 + } else {
  50 + executeForSingleDevice(message, rule, actionConfig);
  51 + }
  52 + }
  53 +
  54 + /**
  55 + * 为单个设备执行属性设置
  56 + */
  57 + private void executeForSingleDevice(IotDeviceMessage message,
  58 + IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
  59 + // 1. 获取设备信息
  60 + IotDeviceDO device = deviceService.getDeviceFromCache(actionConfig.getDeviceId());
  61 + if (device == null) {
  62 + log.error("[executeForSingleDevice][规则场景({}) 动作配置({}) 对应的设备({}) 不存在]",
  63 + rule.getId(), actionConfig, actionConfig.getDeviceId());
  64 + return;
  65 + }
  66 +
  67 + // 2. 执行属性设置
  68 + executePropertySetForDevice(rule, actionConfig, device);
  69 + }
  70 +
  71 + /**
  72 + * 为产品下的所有设备执行属性设置
  73 + */
  74 + private void executeForAllDevices(IotDeviceMessage message,
  75 + IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
  76 + // 1. 参数校验
  77 + if (actionConfig.getProductId() == null) {
  78 + log.error("[executeForAllDevices][规则场景({}) 动作配置({}) 产品编号不能为空]", rule.getId(), actionConfig);
  79 + return;
  80 + }
  81 +
  82 + // 2. 获取产品下的所有设备
  83 + List<IotDeviceDO> devices = deviceService.getDeviceListByProductId(actionConfig.getProductId());
  84 + if (CollUtil.isEmpty(devices)) {
  85 + log.warn("[executeForAllDevices][规则场景({}) 动作配置({}) 产品({}) 下没有设备]",
  86 + rule.getId(), actionConfig, actionConfig.getProductId());
  87 + return;
  88 + }
  89 +
  90 + // 3. 遍历所有设备执行属性设置
  91 + for (IotDeviceDO device : devices) {
  92 + executePropertySetForDevice(rule, actionConfig, device);
  93 + }
  94 + }
  95 +
  96 + /**
  97 + * 为指定设备执行属性设置
  98 + */
  99 + private void executePropertySetForDevice(IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
  100 + // 1. 构建属性设置消息
  101 + IotDeviceMessage downstreamMessage = buildPropertySetMessage(actionConfig, device);
  102 + if (downstreamMessage == null) {
  103 + log.error("[executePropertySetForDevice][规则场景({}) 动作配置({}) 设备({}) 构建属性设置消息失败]",
  104 + rule.getId(), actionConfig, device.getId());
  105 + return;
  106 + }
  107 +
  108 + // 2. 发送设备消息
  109 + try {
  110 + IotDeviceMessage result = deviceMessageService.sendDeviceMessage(downstreamMessage, device);
  111 + log.info("[executePropertySetForDevice][规则场景({}) 动作配置({}) 设备({}) 属性设置消息({}) 发送成功]",
  112 + rule.getId(), actionConfig, device.getId(), result.getId());
  113 + } catch (Exception e) {
  114 + log.error("[executePropertySetForDevice][规则场景({}) 动作配置({}) 设备({}) 属性设置消息发送失败]",
  115 + rule.getId(), actionConfig, device.getId(), e);
  116 + }
  117 + }
  118 +
  119 + /**
  120 + * 构建属性设置消息
  121 + *
  122 + * @param actionConfig 动作配置
  123 + * @param device 设备信息
  124 + * @return 设备消息
  125 + */
  126 + private IotDeviceMessage buildPropertySetMessage(IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
  127 + try {
  128 + // 属性设置参数格式: {"properties": {"identifier": value}}
  129 + Object params = Map.of("properties", Map.of(actionConfig.getIdentifier(), actionConfig.getParams()));
  130 + return IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), params);
  131 + } catch (Exception e) {
  132 + log.error("[buildPropertySetMessage][构建属性设置消息异常]", e);
  133 + return null;
  134 + }
48 } 135 }
49 136
50 @Override 137 @Override
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceServiceInvokeSceneRuleAction.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene.action;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  6 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  7 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
  8 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  9 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum;
  10 +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
  11 +import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService;
  12 +import jakarta.annotation.Resource;
  13 +import lombok.extern.slf4j.Slf4j;
  14 +import org.springframework.stereotype.Component;
  15 +
  16 +import java.util.List;
  17 +import java.util.Map;
  18 +
  19 +/**
  20 + * IoT 设备服务调用的 {@link IotSceneRuleAction} 实现类
  21 + *
  22 + * @author HUIHUI
  23 + */
  24 +@Component
  25 +@Slf4j
  26 +public class IotDeviceServiceInvokeSceneRuleAction implements IotSceneRuleAction {
  27 +
  28 + @Resource
  29 + private IotDeviceService deviceService;
  30 + @Resource
  31 + private IotDeviceMessageService deviceMessageService;
  32 +
  33 + @Override
  34 + public void execute(IotDeviceMessage message,
  35 + IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
  36 + // 1. 参数校验
  37 + if (actionConfig.getDeviceId() == null) {
  38 + log.error("[execute][规则场景({}) 动作配置({}) 设备编号不能为空]", rule.getId(), actionConfig);
  39 + return;
  40 + }
  41 + if (StrUtil.isEmpty(actionConfig.getIdentifier())) {
  42 + log.error("[execute][规则场景({}) 动作配置({}) 服务标识符不能为空]", rule.getId(), actionConfig);
  43 + return;
  44 + }
  45 +
  46 + // 2. 判断是否为全部设备
  47 + if (IotDeviceDO.DEVICE_ID_ALL.equals(actionConfig.getDeviceId())) {
  48 + executeForAllDevices(message, rule, actionConfig);
  49 + } else {
  50 + executeForSingleDevice(message, rule, actionConfig);
  51 + }
  52 + }
  53 +
  54 + /**
  55 + * 为单个设备执行服务调用
  56 + */
  57 + private void executeForSingleDevice(IotDeviceMessage message,
  58 + IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
  59 + // 1. 获取设备信息
  60 + IotDeviceDO device = deviceService.getDeviceFromCache(actionConfig.getDeviceId());
  61 + if (device == null) {
  62 + log.error("[executeForSingleDevice][规则场景({}) 动作配置({}) 对应的设备({}) 不存在]",
  63 + rule.getId(), actionConfig, actionConfig.getDeviceId());
  64 + return;
  65 + }
  66 +
  67 + // 2. 执行服务调用
  68 + executeServiceInvokeForDevice(rule, actionConfig, device);
  69 + }
  70 +
  71 + /**
  72 + * 为产品下的所有设备执行服务调用
  73 + */
  74 + private void executeForAllDevices(IotDeviceMessage message,
  75 + IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
  76 + // 1. 参数校验
  77 + if (actionConfig.getProductId() == null) {
  78 + log.error("[executeForAllDevices][规则场景({}) 动作配置({}) 产品编号不能为空]", rule.getId(), actionConfig);
  79 + return;
  80 + }
  81 +
  82 + // 2. 获取产品下的所有设备
  83 + List<IotDeviceDO> devices = deviceService.getDeviceListByProductId(actionConfig.getProductId());
  84 + if (CollUtil.isEmpty(devices)) {
  85 + log.warn("[executeForAllDevices][规则场景({}) 动作配置({}) 产品({}) 下没有设备]",
  86 + rule.getId(), actionConfig, actionConfig.getProductId());
  87 + return;
  88 + }
  89 +
  90 + // 3. 遍历所有设备执行服务调用
  91 + for (IotDeviceDO device : devices) {
  92 + executeServiceInvokeForDevice(rule, actionConfig, device);
  93 + }
  94 + }
  95 +
  96 + /**
  97 + * 为指定设备执行服务调用
  98 + */
  99 + private void executeServiceInvokeForDevice(IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
  100 + // 1. 构建服务调用消息
  101 + IotDeviceMessage downstreamMessage = buildServiceInvokeMessage(actionConfig, device);
  102 + if (downstreamMessage == null) {
  103 + log.error("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 构建服务调用消息失败]",
  104 + rule.getId(), actionConfig, device.getId());
  105 + return;
  106 + }
  107 +
  108 + // 2. 发送设备消息
  109 + try {
  110 + IotDeviceMessage result = deviceMessageService.sendDeviceMessage(downstreamMessage, device);
  111 + log.info("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 服务调用消息({}) 发送成功]",
  112 + rule.getId(), actionConfig, device.getId(), result.getId());
  113 + } catch (Exception e) {
  114 + log.error("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 服务调用消息发送失败]",
  115 + rule.getId(), actionConfig, device.getId(), e);
  116 + }
  117 + }
  118 +
  119 + /**
  120 + * 构建服务调用消息
  121 + *
  122 + * @param actionConfig 动作配置
  123 + * @param device 设备信息
  124 + * @return 设备消息
  125 + */
  126 + private IotDeviceMessage buildServiceInvokeMessage(IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
  127 + try {
  128 + // 服务调用参数格式: {"identifier": "serviceId", "params": {...}}
  129 + Object params = Map.of(
  130 + "identifier", actionConfig.getIdentifier(),
  131 + "params", actionConfig.getParams() != null ? actionConfig.getParams() : Map.of()
  132 + );
  133 + return IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod(), params);
  134 + } catch (Exception e) {
  135 + log.error("[buildServiceInvokeMessage][构建服务调用消息异常]", e);
  136 + return null;
  137 + }
  138 + }
  139 +
  140 + @Override
  141 + public IotSceneRuleActionTypeEnum getType() {
  142 + return IotSceneRuleActionTypeEnum.DEVICE_SERVICE_INVOKE;
  143 + }
  144 +
  145 +}
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcher.java
@@ -4,10 +4,8 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition.IotScene @@ -4,10 +4,8 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition.IotScene
4 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger.IotSceneRuleTriggerMatcher; 4 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger.IotSceneRuleTriggerMatcher;
5 5
6 /** 6 /**
7 - * IoT 场景规则匹配器基础接口  
8 - * <p>  
9 - * 定义所有匹配器的通用行为,包括优先级、名称和启用状态  
10 - * <p> 7 + * IoT 场景规则匹配器基础接口:定义所有匹配器的通用行为,包括优先级、名称和启用状态
  8 + *
11 * - {@link IotSceneRuleTriggerMatcher} 触发器匹配器 9 * - {@link IotSceneRuleTriggerMatcher} 触发器匹配器
12 * - {@link IotSceneRuleConditionMatcher} 条件匹配器 10 * - {@link IotSceneRuleConditionMatcher} 条件匹配器
13 * 11 *
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherHelper.java
@@ -18,10 +18,8 @@ import java.util.Map; @@ -18,10 +18,8 @@ import java.util.Map;
18 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; 18 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
19 19
20 /** 20 /**
21 - * IoT 场景规则匹配器工具类  
22 - * <p>  
23 - * 提供通用的条件评估逻辑和工具方法,供触发器和条件匹配器使用  
24 - * <p> 21 + * IoT 场景规则匹配器工具类:提供通用的条件评估逻辑和工具方法,供触发器和条件匹配器使用
  22 + *
25 * 该类包含了匹配器实现中常用的工具方法,如条件评估、参数校验、日志记录等 23 * 该类包含了匹配器实现中常用的工具方法,如条件评估、参数校验、日志记录等
26 * 24 *
27 * @author HUIHUI 25 * @author HUIHUI
@@ -91,7 +89,7 @@ public final class IotSceneRuleMatcherHelper { @@ -91,7 +89,7 @@ public final class IotSceneRuleMatcherHelper {
91 Map<String, Object> springExpressionVariables = new HashMap<>(); 89 Map<String, Object> springExpressionVariables = new HashMap<>();
92 90
93 // 设置源值 91 // 设置源值
94 - springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_SOURCE, sourceValue); 92 + springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_SOURCE, StrUtil.toString(sourceValue));
95 93
96 // 处理参数值 94 // 处理参数值
97 if (StrUtil.isNotBlank(paramValue)) { 95 if (StrUtil.isNotBlank(paramValue)) {
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherManager.java
@@ -16,9 +16,7 @@ import java.util.function.Function; @@ -16,9 +16,7 @@ import java.util.function.Function;
16 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; 16 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
17 17
18 /** 18 /**
19 - * IoT 场景规则匹配器统一管理器  
20 - * <p>  
21 - * 负责管理所有匹配器(触发器匹配器和条件匹配器),并提供统一的匹配入口 19 + * IoT 场景规则匹配器统一管理器:负责管理所有匹配器(触发器匹配器和条件匹配器),并提供统一的匹配入口
22 * 20 *
23 * @author HUIHUI 21 * @author HUIHUI
24 */ 22 */
@@ -44,13 +42,12 @@ public class IotSceneRuleMatcherManager { @@ -44,13 +42,12 @@ public class IotSceneRuleMatcherManager {
44 return; 42 return;
45 } 43 }
46 44
47 - // 按优先级排序并过滤启用的匹配器 45 + // 1.1 按优先级排序并过滤启用的匹配器
48 List<IotSceneRuleMatcher> allMatchers = matchers.stream() 46 List<IotSceneRuleMatcher> allMatchers = matchers.stream()
49 .filter(IotSceneRuleMatcher::isEnabled) 47 .filter(IotSceneRuleMatcher::isEnabled)
50 .sorted(Comparator.comparing(IotSceneRuleMatcher::getPriority)) 48 .sorted(Comparator.comparing(IotSceneRuleMatcher::getPriority))
51 .toList(); 49 .toList();
52 -  
53 - // 分离触发器匹配器和条件匹配器 50 + // 1.2 分离触发器匹配器和条件匹配器
54 List<IotSceneRuleTriggerMatcher> triggerMatchers = allMatchers.stream() 51 List<IotSceneRuleTriggerMatcher> triggerMatchers = allMatchers.stream()
55 .filter(matcher -> matcher instanceof IotSceneRuleTriggerMatcher) 52 .filter(matcher -> matcher instanceof IotSceneRuleTriggerMatcher)
56 .map(matcher -> (IotSceneRuleTriggerMatcher) matcher) 53 .map(matcher -> (IotSceneRuleTriggerMatcher) matcher)
@@ -60,7 +57,7 @@ public class IotSceneRuleMatcherManager { @@ -60,7 +57,7 @@ public class IotSceneRuleMatcherManager {
60 .map(matcher -> (IotSceneRuleConditionMatcher) matcher) 57 .map(matcher -> (IotSceneRuleConditionMatcher) matcher)
61 .toList(); 58 .toList();
62 59
63 - // 构建触发器匹配器映射表 60 + // 2.1 构建触发器匹配器映射表
64 this.triggerMatchers = convertMap(triggerMatchers, IotSceneRuleTriggerMatcher::getSupportedTriggerType, 61 this.triggerMatchers = convertMap(triggerMatchers, IotSceneRuleTriggerMatcher::getSupportedTriggerType,
65 Function.identity(), 62 Function.identity(),
66 (existing, replacement) -> { 63 (existing, replacement) -> {
@@ -70,7 +67,7 @@ public class IotSceneRuleMatcherManager { @@ -70,7 +67,7 @@ public class IotSceneRuleMatcherManager {
70 existing.getSupportedTriggerType() : replacement.getSupportedTriggerType()); 67 existing.getSupportedTriggerType() : replacement.getSupportedTriggerType());
71 return existing.getPriority() <= replacement.getPriority() ? existing : replacement; 68 return existing.getPriority() <= replacement.getPriority() ? existing : replacement;
72 }, LinkedHashMap::new); 69 }, LinkedHashMap::new);
73 - // 构建条件匹配器映射表 70 + // 2.2 构建条件匹配器映射表
74 this.conditionMatchers = convertMap(conditionMatchers, IotSceneRuleConditionMatcher::getSupportedConditionType, 71 this.conditionMatchers = convertMap(conditionMatchers, IotSceneRuleConditionMatcher::getSupportedConditionType,
75 Function.identity(), 72 Function.identity(),
76 (existing, replacement) -> { 73 (existing, replacement) -> {
@@ -82,7 +79,7 @@ public class IotSceneRuleMatcherManager { @@ -82,7 +79,7 @@ public class IotSceneRuleMatcherManager {
82 }, 79 },
83 LinkedHashMap::new); 80 LinkedHashMap::new);
84 81
85 - // 日志输出初始化信息 82 + // 3. 日志输出初始化信息
86 log.info("[IotSceneRuleMatcherManager][初始化完成,共加载({})个匹配器,其中触发器匹配器({})个,条件匹配器({})个]", 83 log.info("[IotSceneRuleMatcherManager][初始化完成,共加载({})个匹配器,其中触发器匹配器({})个,条件匹配器({})个]",
87 allMatchers.size(), this.triggerMatchers.size(), this.conditionMatchers.size()); 84 allMatchers.size(), this.triggerMatchers.size(), this.conditionMatchers.size());
88 this.triggerMatchers.forEach((type, matcher) -> 85 this.triggerMatchers.forEach((type, matcher) ->
@@ -135,7 +132,7 @@ public class IotSceneRuleMatcherManager { @@ -135,7 +132,7 @@ public class IotSceneRuleMatcherManager {
135 return false; 132 return false;
136 } 133 }
137 134
138 - // 根据条件类型查找对应的匹配器 135 + // 1. 根据条件类型查找对应的匹配器
139 IotSceneRuleConditionTypeEnum conditionType = IotSceneRuleConditionTypeEnum.typeOf(condition.getType()); 136 IotSceneRuleConditionTypeEnum conditionType = IotSceneRuleConditionTypeEnum.typeOf(condition.getType());
140 if (conditionType == null) { 137 if (conditionType == null) {
141 log.warn("[isConditionMatched][conditionType({}) 未知的条件类型]", condition.getType()); 138 log.warn("[isConditionMatched][conditionType({}) 未知的条件类型]", condition.getType());
@@ -147,7 +144,7 @@ public class IotSceneRuleMatcherManager { @@ -147,7 +144,7 @@ public class IotSceneRuleMatcherManager {
147 return false; 144 return false;
148 } 145 }
149 146
150 - // 执行匹配逻辑 147 + // 2. 执行匹配逻辑
151 try { 148 try {
152 return matcher.matches(message, condition); 149 return matcher.matches(message, condition);
153 } catch (Exception e) { 150 } catch (Exception e) {
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcher.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcher.java
@@ -17,15 +17,13 @@ import java.time.format.DateTimeFormatter; @@ -17,15 +17,13 @@ import java.time.format.DateTimeFormatter;
17 import java.util.List; 17 import java.util.List;
18 18
19 /** 19 /**
20 - * 当前时间条件匹配器  
21 - * <p>  
22 - * 处理时间相关的子条件匹配逻辑 20 + * 当前时间条件匹配器:处理时间相关的子条件匹配逻辑
23 * 21 *
24 * @author HUIHUI 22 * @author HUIHUI
25 */ 23 */
26 @Component 24 @Component
27 @Slf4j 25 @Slf4j
28 -public class CurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher { 26 +public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher {
29 27
30 /** 28 /**
31 * 时间格式化器 - HH:mm:ss 29 * 时间格式化器 - HH:mm:ss
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcher.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcher.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
2 2
  3 +
3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
4 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 5 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
@@ -7,15 +8,14 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; @@ -7,15 +8,14 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
7 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; 8 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
8 import org.springframework.stereotype.Component; 9 import org.springframework.stereotype.Component;
9 10
  11 +
10 /** 12 /**
11 - * 设备属性条件匹配器  
12 - * <p>  
13 - * 处理设备属性相关的子条件匹配逻辑 13 + * 设备属性条件匹配器:处理设备属性相关的子条件匹配逻辑
14 * 14 *
15 * @author HUIHUI 15 * @author HUIHUI
16 */ 16 */
17 @Component 17 @Component
18 -public class DevicePropertyConditionMatcher implements IotSceneRuleConditionMatcher { 18 +public class IotDevicePropertyConditionMatcher implements IotSceneRuleConditionMatcher {
19 19
20 @Override 20 @Override
21 public IotSceneRuleConditionTypeEnum getSupportedConditionType() { 21 public IotSceneRuleConditionTypeEnum getSupportedConditionType() {
@@ -43,10 +43,10 @@ public class DevicePropertyConditionMatcher implements IotSceneRuleConditionMatc @@ -43,10 +43,10 @@ public class DevicePropertyConditionMatcher implements IotSceneRuleConditionMatc
43 return false; 43 return false;
44 } 44 }
45 45
46 - // 2.1. 获取属性值  
47 - Object propertyValue = message.getParams(); 46 + // 2.1. 获取属性值 - 使用工具类方法正确提取属性值
  47 + Object propertyValue = IotDeviceMessageUtils.extractPropertyValue(message, condition.getIdentifier());
48 if (propertyValue == null) { 48 if (propertyValue == null) {
49 - IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中属性值为空"); 49 + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中属性值为空或未找到指定属性");
50 return false; 50 return false;
51 } 51 }
52 52
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcher.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcher.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
2 2
3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  4 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
5 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; 6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
6 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; 7 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
7 import org.springframework.stereotype.Component; 8 import org.springframework.stereotype.Component;
8 9
9 /** 10 /**
10 - * 设备状态条件匹配器  
11 - * <p>  
12 - * 处理设备状态相关的子条件匹配逻辑 11 + * 设备状态条件匹配器:处理设备状态相关的子条件匹配逻辑
13 * 12 *
14 * @author HUIHUI 13 * @author HUIHUI
15 */ 14 */
16 @Component 15 @Component
17 -public class DeviceStateConditionMatcher implements IotSceneRuleConditionMatcher { 16 +public class IotDeviceStateConditionMatcher implements IotSceneRuleConditionMatcher {
18 17
19 @Override 18 @Override
20 public IotSceneRuleConditionTypeEnum getSupportedConditionType() { 19 public IotSceneRuleConditionTypeEnum getSupportedConditionType() {
@@ -35,8 +34,9 @@ public class DeviceStateConditionMatcher implements IotSceneRuleConditionMatcher @@ -35,8 +34,9 @@ public class DeviceStateConditionMatcher implements IotSceneRuleConditionMatcher
35 return false; 34 return false;
36 } 35 }
37 36
38 - // 2.1 获取设备状态值  
39 - Object stateValue = message.getParams(); 37 + // 2.1 获取设备状态值 - 使用工具类方法正确提取状态值
  38 + // 对于设备状态条件,状态值通过 getIdentifier 获取(实际是从 params.state 字段)
  39 + String stateValue = IotDeviceMessageUtils.getIdentifier(message);
40 if (stateValue == null) { 40 if (stateValue == null) {
41 IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中设备状态值为空"); 41 IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中设备状态值为空");
42 return false; 42 return false;
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotSceneRuleConditionMatcher.java
@@ -6,12 +6,9 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; @@ -6,12 +6,9 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
6 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcher; 6 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcher;
7 7
8 /** 8 /**
9 - * IoT 场景规则条件匹配器接口  
10 - * <p>  
11 - * 专门处理子条件的匹配逻辑,如设备状态、属性值、时间条件等  
12 - * <p>  
13 - * 条件匹配器负责判断设备消息是否满足场景规则的附加条件,  
14 - * 在触发器匹配成功后进行进一步的条件筛选 9 + * IoT 场景规则条件匹配器接口:专门处理子条件的匹配逻辑,如设备状态、属性值、时间条件等
  10 + *
  11 + * 条件匹配器负责判断设备消息是否满足场景规则的附加条件,在触发器匹配成功后进行进一步的条件筛选
15 * 12 *
16 * @author HUIHUI 13 * @author HUIHUI
17 */ 14 */
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcher.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcher.java
@@ -10,14 +10,12 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatche @@ -10,14 +10,12 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatche
10 import org.springframework.stereotype.Component; 10 import org.springframework.stereotype.Component;
11 11
12 /** 12 /**
13 - * 设备事件上报触发器匹配器  
14 - * <p>  
15 - * 处理设备事件上报的触发器匹配逻辑 13 + * 设备事件上报触发器匹配器:处理设备事件上报的触发器匹配逻辑
16 * 14 *
17 * @author HUIHUI 15 * @author HUIHUI
18 */ 16 */
19 @Component 17 @Component
20 -public class DeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatcher { 18 +public class IotDeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
21 19
22 @Override 20 @Override
23 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { 21 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcher.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcher.java
@@ -9,14 +9,12 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatche @@ -9,14 +9,12 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatche
9 import org.springframework.stereotype.Component; 9 import org.springframework.stereotype.Component;
10 10
11 /** 11 /**
12 - * 设备属性上报触发器匹配器  
13 - * <p>  
14 - * 处理设备属性数据上报的触发器匹配逻辑 12 + * 设备属性上报触发器匹配器:处理设备属性数据上报的触发器匹配逻辑
15 * 13 *
16 * @author HUIHUI 14 * @author HUIHUI
17 */ 15 */
18 @Component 16 @Component
19 -public class DevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatcher { 17 +public class IotDevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
20 18
21 @Override 19 @Override
22 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { 20 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
@@ -52,10 +50,10 @@ public class DevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatc @@ -52,10 +50,10 @@ public class DevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatc
52 return false; 50 return false;
53 } 51 }
54 52
55 - // 2.1 获取属性值  
56 - Object propertyValue = message.getParams(); 53 + // 2.1 获取属性值 - 使用工具类方法正确提取属性值
  54 + Object propertyValue = IotDeviceMessageUtils.extractPropertyValue(message, trigger.getIdentifier());
57 if (propertyValue == null) { 55 if (propertyValue == null) {
58 - IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中属性值为空"); 56 + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中属性值为空或未找到指定属性");
59 return false; 57 return false;
60 } 58 }
61 59
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcher.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcher.java
@@ -9,14 +9,12 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatche @@ -9,14 +9,12 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatche
9 import org.springframework.stereotype.Component; 9 import org.springframework.stereotype.Component;
10 10
11 /** 11 /**
12 - * 设备服务调用触发器匹配器  
13 - * <p>  
14 - * 处理设备服务调用的触发器匹配逻辑 12 + * 设备服务调用触发器匹配器:处理设备服务调用的触发器匹配逻辑
15 * 13 *
16 * @author HUIHUI 14 * @author HUIHUI
17 */ 15 */
18 @Component 16 @Component
19 -public class DeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMatcher { 17 +public class IotDeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMatcher {
20 18
21 @Override 19 @Override
22 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { 20 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
@@ -46,7 +44,7 @@ public class DeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMat @@ -46,7 +44,7 @@ public class DeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMat
46 44
47 // 2. 对于服务调用触发器,通常只需要匹配服务标识符即可 45 // 2. 对于服务调用触发器,通常只需要匹配服务标识符即可
48 // 不需要检查操作符和值,因为服务调用本身就是触发条件 46 // 不需要检查操作符和值,因为服务调用本身就是触发条件
49 - // TODO @puhui999: 服务调用时校验输入参数是否匹配条件 47 + // TODO @puhui999: 服务调用时校验输入参数是否匹配条件?
50 IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger); 48 IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger);
51 return true; 49 return true;
52 } 50 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcher.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcher.java
@@ -2,20 +2,19 @@ package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; @@ -2,20 +2,19 @@ package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2
3 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 3 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
7 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; 8 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
8 import org.springframework.stereotype.Component; 9 import org.springframework.stereotype.Component;
9 10
10 /** 11 /**
11 - * 设备状态更新触发器匹配器  
12 - * <p>  
13 - * 处理设备上下线状态变更的触发器匹配逻辑 12 + * 设备状态更新触发器匹配器:处理设备上下线状态变更的触发器匹配逻辑
14 * 13 *
15 * @author HUIHUI 14 * @author HUIHUI
16 */ 15 */
17 @Component 16 @Component
18 -public class DeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatcher { 17 +public class IotDeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatcher {
19 18
20 @Override 19 @Override
21 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { 20 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
@@ -43,16 +42,17 @@ public class DeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatch @@ -43,16 +42,17 @@ public class DeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatch
43 return false; 42 return false;
44 } 43 }
45 44
46 - // 2.1 获取设备状态值  
47 - Object stateValue = message.getParams();  
48 - if (stateValue == null) { 45 + // 2.1 获取设备状态值 - 使用工具类方法正确提取状态值
  46 + // 对于状态更新消息,状态值通过 getIdentifier 获取(实际是从 params.state 字段)
  47 + String stateIdentifier = IotDeviceMessageUtils.getIdentifier(message);
  48 + if (stateIdentifier == null) {
49 IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中设备状态值为空"); 49 IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中设备状态值为空");
50 return false; 50 return false;
51 } 51 }
52 52
53 // 2.2 使用条件评估器进行匹配 53 // 2.2 使用条件评估器进行匹配
54 - // TODO @puhui999: 状态匹配重新实现  
55 - boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(stateValue, trigger.getOperator(), trigger.getValue()); 54 + // 状态值通常是字符串或数字,直接使用标识符作为状态值
  55 + boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(stateIdentifier, trigger.getOperator(), trigger.getValue());
56 if (matched) { 56 if (matched) {
57 IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger); 57 IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger);
58 } else { 58 } else {
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotSceneRuleTriggerMatcher.java
@@ -6,12 +6,9 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; @@ -6,12 +6,9 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
6 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcher; 6 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcher;
7 7
8 /** 8 /**
9 - * IoT 场景规则触发器匹配器接口  
10 - * <p>  
11 - * 专门处理主触发条件的匹配逻辑,如设备消息类型、定时器等  
12 - * <p>  
13 - * 触发器匹配器负责判断设备消息是否满足场景规则的主触发条件,  
14 - * 是场景规则执行的第一道门槛 9 + * IoT 场景规则触发器匹配器接口:专门处理主触发条件的匹配逻辑,如设备消息类型、定时器等
  10 + *
  11 + * 触发器匹配器负责判断设备消息是否满足场景规则的主触发条件,是场景规则执行的第一道门槛
15 * 12 *
16 * @author HUIHUI 13 * @author HUIHUI
17 */ 14 */
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcher.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotTimerTriggerMatcher.java
@@ -9,15 +9,14 @@ import org.quartz.CronExpression; @@ -9,15 +9,14 @@ import org.quartz.CronExpression;
9 import org.springframework.stereotype.Component; 9 import org.springframework.stereotype.Component;
10 10
11 /** 11 /**
12 - * 定时触发器匹配器  
13 - * <p>  
14 - * 处理定时触发的触发器匹配逻辑 12 + * 定时触发器匹配器:处理定时触发的触发器匹配逻辑
  13 + *
15 * 注意:定时触发器不依赖设备消息,主要用于定时任务场景 14 * 注意:定时触发器不依赖设备消息,主要用于定时任务场景
16 * 15 *
17 * @author HUIHUI 16 * @author HUIHUI
18 */ 17 */
19 @Component 18 @Component
20 -public class TimerTriggerMatcher implements IotSceneRuleTriggerMatcher { 19 +public class IotTimerTriggerMatcher implements IotSceneRuleTriggerMatcher {
21 20
22 @Override 21 @Override
23 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { 22 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotSceneRuleTimerHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene.timer;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.ObjUtil;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  7 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  8 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  9 +import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager;
  10 +import cn.iocoder.yudao.module.iot.job.rule.IotSceneRuleJob;
  11 +import jakarta.annotation.Resource;
  12 +import lombok.extern.slf4j.Slf4j;
  13 +import org.quartz.SchedulerException;
  14 +import org.springframework.stereotype.Component;
  15 +
  16 +import java.util.List;
  17 +
  18 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
  19 +
  20 +/**
  21 + * IoT 场景规则定时触发器处理器:负责管理定时触发器的注册、更新、删除等操作
  22 + *
  23 + * @author HUIHUI
  24 + */
  25 +@Component
  26 +@Slf4j
  27 +public class IotSceneRuleTimerHandler {
  28 +
  29 + @Resource(name = "iotSchedulerManager")
  30 + private IotSchedulerManager schedulerManager;
  31 +
  32 + /**
  33 + * 注册场景规则的定时触发器
  34 + *
  35 + * @param sceneRule 场景规则
  36 + */
  37 + public void registerTimerTriggers(IotSceneRuleDO sceneRule) {
  38 + // 1. 过滤出定时触发器
  39 + if (sceneRule == null || CollUtil.isEmpty(sceneRule.getTriggers())) {
  40 + return;
  41 + }
  42 + List<IotSceneRuleDO.Trigger> timerTriggers = filterList(sceneRule.getTriggers(),
  43 + trigger -> ObjUtil.equals(trigger.getType(), IotSceneRuleTriggerTypeEnum.TIMER.getType()));
  44 + if (CollUtil.isEmpty(timerTriggers)) {
  45 + return;
  46 + }
  47 +
  48 + // 2. 注册每个定时触发器
  49 + timerTriggers.forEach(trigger -> registerSingleTimerTrigger(sceneRule, trigger));
  50 + }
  51 +
  52 + /**
  53 + * 更新场景规则的定时触发器
  54 + *
  55 + * @param sceneRule 场景规则
  56 + */
  57 + public void updateTimerTriggers(IotSceneRuleDO sceneRule) {
  58 + if (sceneRule == null) {
  59 + return;
  60 + }
  61 +
  62 + // 1. 先删除旧的定时任务
  63 + unregisterTimerTriggers(sceneRule.getId());
  64 +
  65 + // 2.1 如果场景规则已禁用,则不重新注册
  66 + if (CommonStatusEnum.isDisable(sceneRule.getStatus())) {
  67 + log.info("[updateTimerTriggers][场景规则({}) 已禁用,不注册定时触发器]", sceneRule.getId());
  68 + return;
  69 + }
  70 +
  71 + // 2.2 重新注册定时触发器
  72 + registerTimerTriggers(sceneRule);
  73 + }
  74 +
  75 + /**
  76 + * 注销场景规则的定时触发器
  77 + *
  78 + * @param sceneRuleId 场景规则 ID
  79 + */
  80 + public void unregisterTimerTriggers(Long sceneRuleId) {
  81 + if (sceneRuleId == null) {
  82 + return;
  83 + }
  84 +
  85 + String jobName = buildJobName(sceneRuleId);
  86 + try {
  87 + schedulerManager.deleteJob(jobName);
  88 + log.info("[unregisterTimerTriggers][场景规则({}) 定时触发器注销成功]", sceneRuleId);
  89 + } catch (SchedulerException e) {
  90 + log.error("[unregisterTimerTriggers][场景规则({}) 定时触发器注销失败]", sceneRuleId, e);
  91 + }
  92 + }
  93 +
  94 + /**
  95 + * 暂停场景规则的定时触发器
  96 + *
  97 + * @param sceneRuleId 场景规则 ID
  98 + */
  99 + public void pauseTimerTriggers(Long sceneRuleId) {
  100 + if (sceneRuleId == null) {
  101 + return;
  102 + }
  103 +
  104 + String jobName = buildJobName(sceneRuleId);
  105 + try {
  106 + schedulerManager.pauseJob(jobName);
  107 + log.info("[pauseTimerTriggers][场景规则({}) 定时触发器暂停成功]", sceneRuleId);
  108 + } catch (SchedulerException e) {
  109 + log.error("[pauseTimerTriggers][场景规则({}) 定时触发器暂停失败]", sceneRuleId, e);
  110 + }
  111 + }
  112 +
  113 + /**
  114 + * 注册单个定时触发器
  115 + *
  116 + * @param sceneRule 场景规则
  117 + * @param trigger 定时触发器配置
  118 + */
  119 + private void registerSingleTimerTrigger(IotSceneRuleDO sceneRule, IotSceneRuleDO.Trigger trigger) {
  120 + // 1. 参数校验
  121 + if (StrUtil.isBlank(trigger.getCronExpression())) {
  122 + log.error("[registerSingleTimerTrigger][场景规则({}) 定时触发器缺少 CRON 表达式]", sceneRule.getId());
  123 + return;
  124 + }
  125 +
  126 + try {
  127 + // 2.1 构建任务名称和数据
  128 + String jobName = buildJobName(sceneRule.getId());
  129 + // 2.2 注册定时任务
  130 + schedulerManager.addOrUpdateJob(
  131 + IotSceneRuleJob.class,
  132 + jobName,
  133 + trigger.getCronExpression(),
  134 + IotSceneRuleJob.buildJobDataMap(sceneRule.getId())
  135 + );
  136 + log.info("[registerSingleTimerTrigger][场景规则({}) 定时触发器注册成功,CRON: {}]",
  137 + sceneRule.getId(), trigger.getCronExpression());
  138 + } catch (SchedulerException e) {
  139 + log.error("[registerSingleTimerTrigger][场景规则({}) 定时触发器注册失败,CRON: {}]",
  140 + sceneRule.getId(), trigger.getCronExpression(), e);
  141 + }
  142 + }
  143 +
  144 + /**
  145 + * 构建任务名称
  146 + *
  147 + * @param sceneRuleId 场景规则 ID
  148 + * @return 任务名称
  149 + */
  150 + private String buildJobName(Long sceneRuleId) {
  151 + return "iot_scene_rule_timer_" + sceneRuleId;
  152 + }
  153 +
  154 +}
yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDevicePropertyMapper.xml
@@ -72,7 +72,7 @@ @@ -72,7 +72,7 @@
72 FROM device_property_${reqVO.deviceId} 72 FROM device_property_${reqVO.deviceId}
73 WHERE ${@cn.hutool.core.util.StrUtil@toUnderlineCase(reqVO.identifier)} IS NOT NULL 73 WHERE ${@cn.hutool.core.util.StrUtil@toUnderlineCase(reqVO.identifier)} IS NOT NULL
74 AND ts BETWEEN ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[0])} 74 AND ts BETWEEN ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[0])}
75 - AND ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[1])} 75 + AND ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[1])}
76 ORDER BY ts DESC 76 ORDER BY ts DESC
77 </select> 77 </select>
78 78
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleActionTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.data.action;
  2 +
  3 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  4 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig;
  6 +import cn.iocoder.yudao.module.iot.service.rule.data.action.tcp.IotTcpClient;
  7 +import org.junit.jupiter.api.BeforeEach;
  8 +import org.junit.jupiter.api.Test;
  9 +import org.mockito.Mock;
  10 +import org.mockito.MockitoAnnotations;
  11 +
  12 +import static org.junit.jupiter.api.Assertions.*;
  13 +import static org.mockito.Mockito.*;
  14 +
  15 +/**
  16 + * {@link IotTcpDataRuleAction} 的单元测试
  17 + *
  18 + * @author HUIHUI
  19 + */
  20 +class IotTcpDataRuleActionTest {
  21 +
  22 + private IotTcpDataRuleAction tcpDataRuleAction;
  23 +
  24 + @Mock
  25 + private IotTcpClient mockTcpClient;
  26 +
  27 + @BeforeEach
  28 + public void setUp() {
  29 + MockitoAnnotations.openMocks(this);
  30 + tcpDataRuleAction = new IotTcpDataRuleAction();
  31 + }
  32 +
  33 + @Test
  34 + public void testGetType() {
  35 + // 准备参数
  36 + Integer expectedType = 2; // 数据接收类型枚举中 TCP 类型的值
  37 +
  38 + // 调用方法
  39 + Integer actualType = tcpDataRuleAction.getType();
  40 +
  41 + // 断言结果
  42 + assertEquals(expectedType, actualType);
  43 + }
  44 +
  45 + // TODO @puhui999:_ 后面是小写哈,单测的命名规则。
  46 + @Test
  47 + public void testInitProducer_Success() throws Exception {
  48 + // 准备参数
  49 + IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
  50 + config.setHost("localhost");
  51 + config.setPort(8080);
  52 + config.setDataFormat("JSON");
  53 + config.setSsl(false);
  54 +
  55 + // 调用方法 & 断言结果
  56 + // 此测试需要实际的 TCP 连接,在单元测试中可能不可用
  57 + // 目前我们只验证配置是否有效
  58 + assertNotNull(config.getHost());
  59 + assertTrue(config.getPort() > 0 && config.getPort() <= 65535);
  60 + }
  61 +
  62 + @Test
  63 + public void testInitProducer_InvalidHost() {
  64 + // 准备参数
  65 + IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
  66 + config.setHost("");
  67 + config.setPort(8080);
  68 +
  69 + // 调用方法 & 断言结果
  70 + IotTcpDataRuleAction action = new IotTcpDataRuleAction();
  71 +
  72 + // 测试验证逻辑(通常在 initProducer 方法中)
  73 + assertThrows(IllegalArgumentException.class, () -> {
  74 + if (config.getHost() == null || config.getHost().trim().isEmpty()) {
  75 + throw new IllegalArgumentException("TCP 服务器地址不能为空");
  76 + }
  77 + });
  78 + }
  79 +
  80 + @Test
  81 + public void testInitProducer_InvalidPort() {
  82 + // 准备参数
  83 + IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
  84 + config.setHost("localhost");
  85 + config.setPort(-1);
  86 +
  87 + // 调用方法 & 断言结果
  88 + assertThrows(IllegalArgumentException.class, () -> {
  89 + if (config.getPort() == null || config.getPort() <= 0 || config.getPort() > 65535) {
  90 + throw new IllegalArgumentException("TCP 服务器端口无效");
  91 + }
  92 + });
  93 + }
  94 +
  95 + @Test
  96 + public void testCloseProducer() throws Exception {
  97 + // 准备参数
  98 + IotTcpClient client = mock(IotTcpClient.class);
  99 +
  100 + // 调用方法
  101 + tcpDataRuleAction.closeProducer(client);
  102 +
  103 + // 断言结果
  104 + verify(client, times(1)).close();
  105 + }
  106 +
  107 + @Test
  108 + public void testExecute_WithValidConfig() {
  109 + // 准备参数
  110 + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.report",
  111 + "{\"temperature\": 25.5, \"humidity\": 60}");
  112 +
  113 + IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
  114 + config.setHost("localhost");
  115 + config.setPort(8080);
  116 + config.setDataFormat("JSON");
  117 +
  118 + // 调用方法 & 断言结果
  119 + // 通常这需要实际的 TCP 连接
  120 + // 在单元测试中,我们只验证输入参数
  121 + assertNotNull(message);
  122 + assertNotNull(config);
  123 + assertNotNull(config.getHost());
  124 + assertTrue(config.getPort() > 0);
  125 + }
  126 +
  127 + @Test
  128 + public void testConfig_DefaultValues() {
  129 + // 准备参数
  130 + IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
  131 +
  132 + // 调用方法 & 断言结果
  133 + // 验证默认值
  134 + assertEquals("JSON", config.getDataFormat());
  135 + assertEquals(5000, config.getConnectTimeoutMs());
  136 + assertEquals(10000, config.getReadTimeoutMs());
  137 + assertEquals(false, config.getSsl());
  138 + assertEquals(30000L, config.getHeartbeatIntervalMs());
  139 + assertEquals(5000L, config.getReconnectIntervalMs());
  140 + assertEquals(3, config.getMaxReconnectAttempts());
  141 + }
  142 +
  143 + @Test
  144 + public void testMessageSerialization() {
  145 + // 准备参数
  146 + IotDeviceMessage message = IotDeviceMessage.builder()
  147 + .deviceId(123L)
  148 + .method("thing.property.report")
  149 + .params("{\"temperature\": 25.5}")
  150 + .build();
  151 +
  152 + // 调用方法
  153 + String json = JsonUtils.toJsonString(message);
  154 +
  155 + // 断言结果
  156 + assertNotNull(json);
  157 + assertTrue(json.contains("\"deviceId\":123"));
  158 + assertTrue(json.contains("\"method\":\"thing.property.report\""));
  159 + assertTrue(json.contains("\"temperature\":25.5"));
  160 + }
  161 +
  162 +}
0 \ No newline at end of file 163 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotBaseConditionMatcherTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
  2 +
  3 +import cn.hutool.extra.spring.SpringUtil;
  4 +import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils;
  5 +import org.springframework.context.annotation.Bean;
  6 +import org.springframework.context.annotation.Configuration;
  7 +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
  8 +
  9 +/**
  10 + * Matcher 测试基类
  11 + * 提供通用的 Spring 测试配置
  12 + *
  13 + * @author HUIHUI
  14 + */
  15 +@SpringJUnitConfig
  16 +public abstract class IotBaseConditionMatcherTest {
  17 +
  18 + /**
  19 + * 注入一下 SpringUtil,解析 EL 表达式时需要
  20 + * {@link SpringExpressionUtils#parseExpression}
  21 + */
  22 + @Configuration
  23 + static class TestConfig {
  24 +
  25 + @Bean
  26 + public SpringUtil springUtil() {
  27 + return new SpringUtil();
  28 + }
  29 +
  30 + }
  31 +
  32 +}
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcherTest.java renamed to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcherTest.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
2 2
3 -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;  
4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; 5 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; 6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
  7 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
  8 +import org.junit.jupiter.api.BeforeEach;
8 import org.junit.jupiter.api.Test; 9 import org.junit.jupiter.api.Test;
9 -import org.mockito.InjectMocks;  
10 10
11 import java.time.LocalDateTime; 11 import java.time.LocalDateTime;
12 import java.time.ZoneOffset; 12 import java.time.ZoneOffset;
@@ -16,14 +16,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString @@ -16,14 +16,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
16 import static org.junit.jupiter.api.Assertions.*; 16 import static org.junit.jupiter.api.Assertions.*;
17 17
18 /** 18 /**
19 - * {@link CurrentTimeConditionMatcher} 的单元测试 19 + * {@link IotCurrentTimeConditionMatcher} 的单元测试
20 * 20 *
21 * @author HUIHUI 21 * @author HUIHUI
22 */ 22 */
23 -public class CurrentTimeConditionMatcherTest extends BaseMockitoUnitTest { 23 +public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherTest {
24 24
25 - @InjectMocks  
26 - private CurrentTimeConditionMatcher matcher; 25 + private IotCurrentTimeConditionMatcher matcher;
  26 +
  27 + @BeforeEach
  28 + public void setUp() {
  29 + matcher = new IotCurrentTimeConditionMatcher();
  30 + }
27 31
28 @Test 32 @Test
29 public void testGetSupportedConditionType() { 33 public void testGetSupportedConditionType() {
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcherTest.java renamed to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcherTest.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
2 2
3 -import cn.hutool.core.map.MapUtil;  
4 -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;  
5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; 5 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; 6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
  7 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
  8 +import org.junit.jupiter.api.BeforeEach;
9 import org.junit.jupiter.api.Test; 9 import org.junit.jupiter.api.Test;
10 -import org.mockito.InjectMocks;  
11 10
12 import java.util.HashMap; 11 import java.util.HashMap;
13 import java.util.Map; 12 import java.util.Map;
14 13
15 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; 14 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
16 -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;  
17 import static org.junit.jupiter.api.Assertions.*; 15 import static org.junit.jupiter.api.Assertions.*;
18 16
19 /** 17 /**
20 - * {@link DevicePropertyConditionMatcher} 的单元测试 18 + * {@link IotDevicePropertyConditionMatcher} 的单元测试
21 * 19 *
22 * @author HUIHUI 20 * @author HUIHUI
23 */ 21 */
24 -public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { 22 +public class IotDevicePropertyConditionMatcherTest extends IotBaseConditionMatcherTest {
25 23
26 - @InjectMocks  
27 - private DevicePropertyConditionMatcher matcher; 24 + private IotDevicePropertyConditionMatcher matcher;
  25 +
  26 + @BeforeEach
  27 + public void setUp() {
  28 + matcher = new IotDevicePropertyConditionMatcher();
  29 + }
28 30
29 @Test 31 @Test
30 public void testGetSupportedConditionType() { 32 public void testGetSupportedConditionType() {
@@ -41,27 +43,17 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -41,27 +43,17 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
41 int result = matcher.getPriority(); 43 int result = matcher.getPriority();
42 44
43 // 断言 45 // 断言
44 - assertEquals(20, result);  
45 - }  
46 -  
47 - @Test  
48 - public void testIsEnabled() {  
49 - // 调用  
50 - boolean result = matcher.isEnabled();  
51 -  
52 - // 断言  
53 - assertTrue(result); 46 + assertEquals(25, result); // 修正:实际返回值是 25
54 } 47 }
55 48
56 @Test 49 @Test
57 public void testMatches_temperatureEquals_success() { 50 public void testMatches_temperatureEquals_success() {
58 - // 准备参数  
59 - String propertyName = "temperature"; 51 + // 准备参数:创建属性上报消息
  52 + String propertyIdentifier = "temperature";
60 Double propertyValue = 25.5; 53 Double propertyValue = 25.5;
61 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
62 - IotDeviceMessage message = createDeviceMessage(properties); 54 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
63 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 55 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
64 - propertyName, 56 + propertyIdentifier,
65 IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), 57 IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
66 String.valueOf(propertyValue) 58 String.valueOf(propertyValue)
67 ); 59 );
@@ -75,14 +67,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -75,14 +67,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
75 67
76 @Test 68 @Test
77 public void testMatches_humidityGreaterThan_success() { 69 public void testMatches_humidityGreaterThan_success() {
78 - // 准备参数  
79 - String propertyName = "humidity"; 70 + // 准备参数:创建属性上报消息
  71 + String propertyIdentifier = "humidity";
80 Integer propertyValue = 75; 72 Integer propertyValue = 75;
81 Integer compareValue = 70; 73 Integer compareValue = 70;
82 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
83 - IotDeviceMessage message = createDeviceMessage(properties); 74 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
84 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 75 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
85 - propertyName, 76 + propertyIdentifier,
86 IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), 77 IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
87 String.valueOf(compareValue) 78 String.valueOf(compareValue)
88 ); 79 );
@@ -96,14 +87,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -96,14 +87,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
96 87
97 @Test 88 @Test
98 public void testMatches_pressureLessThan_success() { 89 public void testMatches_pressureLessThan_success() {
99 - // 准备参数  
100 - String propertyName = "pressure"; 90 + // 准备参数:创建属性上报消息
  91 + String propertyIdentifier = "pressure";
101 Double propertyValue = 1010.5; 92 Double propertyValue = 1010.5;
102 Integer compareValue = 1020; 93 Integer compareValue = 1020;
103 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
104 - IotDeviceMessage message = createDeviceMessage(properties); 94 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
105 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 95 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
106 - propertyName, 96 + propertyIdentifier,
107 IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(), 97 IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(),
108 String.valueOf(compareValue) 98 String.valueOf(compareValue)
109 ); 99 );
@@ -117,14 +107,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -117,14 +107,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
117 107
118 @Test 108 @Test
119 public void testMatches_statusNotEquals_success() { 109 public void testMatches_statusNotEquals_success() {
120 - // 准备参数  
121 - String propertyName = "status"; 110 + // 准备参数:创建属性上报消息
  111 + String propertyIdentifier = "status";
122 String propertyValue = "active"; 112 String propertyValue = "active";
123 String compareValue = "inactive"; 113 String compareValue = "inactive";
124 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
125 - IotDeviceMessage message = createDeviceMessage(properties); 114 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
126 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 115 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
127 - propertyName, 116 + propertyIdentifier,
128 IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator(), 117 IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator(),
129 compareValue 118 compareValue
130 ); 119 );
@@ -138,14 +127,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -138,14 +127,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
138 127
139 @Test 128 @Test
140 public void testMatches_propertyMismatch_fail() { 129 public void testMatches_propertyMismatch_fail() {
141 - // 准备参数  
142 - String propertyName = "temperature"; 130 + // 准备参数:创建属性上报消息,值不满足条件
  131 + String propertyIdentifier = "temperature";
143 Double propertyValue = 15.0; 132 Double propertyValue = 15.0;
144 Integer compareValue = 20; 133 Integer compareValue = 20;
145 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
146 - IotDeviceMessage message = createDeviceMessage(properties); 134 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
147 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 135 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
148 - propertyName, 136 + propertyIdentifier,
149 IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), 137 IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
150 String.valueOf(compareValue) 138 String.valueOf(compareValue)
151 ); 139 );
@@ -158,14 +146,16 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -158,14 +146,16 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
158 } 146 }
159 147
160 @Test 148 @Test
161 - public void testMatches_propertyNotFound_fail() {  
162 - // 准备参数  
163 - Map<String, Object> properties = MapUtil.of("temperature", 25.5);  
164 - IotDeviceMessage message = createDeviceMessage(properties); 149 + public void testMatches_identifierMismatch_fail() {
  150 + // 准备参数:标识符不匹配
  151 + String messageIdentifier = "temperature";
  152 + String conditionIdentifier = "humidity";
  153 + Double propertyValue = 25.5;
  154 + IotDeviceMessage message = createPropertyPostMessage(messageIdentifier, propertyValue);
165 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 155 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
166 - randomString(), // 随机不存在的属性名  
167 - IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),  
168 - "50" 156 + conditionIdentifier,
  157 + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
  158 + String.valueOf(propertyValue)
169 ); 159 );
170 160
171 // 调用 161 // 调用
@@ -178,8 +168,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -178,8 +168,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
178 @Test 168 @Test
179 public void testMatches_nullCondition_fail() { 169 public void testMatches_nullCondition_fail() {
180 // 准备参数 170 // 准备参数
181 - Map<String, Object> properties = MapUtil.of("temperature", 25.5);  
182 - IotDeviceMessage message = createDeviceMessage(properties); 171 + IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
183 172
184 // 调用 173 // 调用
185 boolean result = matcher.matches(message, null); 174 boolean result = matcher.matches(message, null);
@@ -191,8 +180,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -191,8 +180,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
191 @Test 180 @Test
192 public void testMatches_nullConditionType_fail() { 181 public void testMatches_nullConditionType_fail() {
193 // 准备参数 182 // 准备参数
194 - Map<String, Object> properties = MapUtil.of("temperature", 25.5);  
195 - IotDeviceMessage message = createDeviceMessage(properties); 183 + IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
196 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); 184 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
197 condition.setType(null); 185 condition.setType(null);
198 186
@@ -206,8 +194,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -206,8 +194,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
206 @Test 194 @Test
207 public void testMatches_missingIdentifier_fail() { 195 public void testMatches_missingIdentifier_fail() {
208 // 准备参数 196 // 准备参数
209 - Map<String, Object> properties = MapUtil.of("temperature", 25.5);  
210 - IotDeviceMessage message = createDeviceMessage(properties); 197 + IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
211 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); 198 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
212 condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType()); 199 condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
213 condition.setIdentifier(null); // 缺少标识符 200 condition.setIdentifier(null); // 缺少标识符
@@ -224,8 +211,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -224,8 +211,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
224 @Test 211 @Test
225 public void testMatches_missingOperator_fail() { 212 public void testMatches_missingOperator_fail() {
226 // 准备参数 213 // 准备参数
227 - Map<String, Object> properties = MapUtil.of("temperature", 25.5);  
228 - IotDeviceMessage message = createDeviceMessage(properties); 214 + IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
229 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); 215 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
230 condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType()); 216 condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
231 condition.setIdentifier("temperature"); 217 condition.setIdentifier("temperature");
@@ -242,8 +228,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -242,8 +228,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
242 @Test 228 @Test
243 public void testMatches_missingParam_fail() { 229 public void testMatches_missingParam_fail() {
244 // 准备参数 230 // 准备参数
245 - Map<String, Object> properties = MapUtil.of("temperature", 25.5);  
246 - IotDeviceMessage message = createDeviceMessage(properties); 231 + IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
247 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); 232 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
248 condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType()); 233 condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
249 condition.setIdentifier("temperature"); 234 condition.setIdentifier("temperature");
@@ -275,7 +260,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -275,7 +260,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
275 260
276 @Test 261 @Test
277 public void testMatches_nullDeviceProperties_fail() { 262 public void testMatches_nullDeviceProperties_fail() {
278 - // 准备参数 263 + // 准备参数:消息的 params 为 null
279 IotDeviceMessage message = new IotDeviceMessage(); 264 IotDeviceMessage message = new IotDeviceMessage();
280 message.setParams(null); 265 message.setParams(null);
281 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 266 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
@@ -292,14 +277,79 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -292,14 +277,79 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
292 } 277 }
293 278
294 @Test 279 @Test
  280 + public void testMatches_propertiesStructure_success() {
  281 + // 测试使用 properties 结构的消息(真实的属性上报场景)
  282 + String identifier = "temperature";
  283 + Double propertyValue = 25.5;
  284 + IotDeviceMessage message = createPropertyPostMessageWithProperties(identifier, propertyValue);
  285 + IotSceneRuleDO.TriggerCondition condition = createValidCondition(
  286 + identifier,
  287 + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
  288 + "20"
  289 + );
  290 +
  291 + // 调用
  292 + boolean result = matcher.matches(message, condition);
  293 +
  294 + // 断言:修复后的实现应该能正确从 properties 中提取属性值
  295 + assertTrue(result);
  296 + }
  297 +
  298 + @Test
  299 + public void testMatches_simpleValueMessage_success() {
  300 + // 测试简单值消息(params 直接是属性值)
  301 + Double propertyValue = 25.5;
  302 + IotDeviceMessage message = createSimpleValueMessage(propertyValue);
  303 + IotSceneRuleDO.TriggerCondition condition = createValidCondition(
  304 + "any", // 对于简单值消息,标识符匹配会被跳过
  305 + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
  306 + "20"
  307 + );
  308 +
  309 + // 调用
  310 + boolean result = matcher.matches(message, condition);
  311 +
  312 + // 断言:修复后的实现应该能处理简单值消息
  313 + // 但由于标识符匹配失败,结果为 false
  314 + assertFalse(result);
  315 + }
  316 +
  317 + @Test
  318 + public void testMatches_valueFieldStructure_success() {
  319 + // 测试使用 value 字段的消息结构
  320 + String identifier = "temperature";
  321 + Double propertyValue = 25.5;
  322 +
  323 + IotDeviceMessage message = new IotDeviceMessage();
  324 + message.setDeviceId(randomLongId());
  325 + message.setMethod("thing.event.post");
  326 +
  327 + Map<String, Object> params = new HashMap<>();
  328 + params.put("identifier", identifier);
  329 + params.put("value", propertyValue);
  330 + message.setParams(params);
  331 +
  332 + IotSceneRuleDO.TriggerCondition condition = createValidCondition(
  333 + identifier,
  334 + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
  335 + "20"
  336 + );
  337 +
  338 + // 调用
  339 + boolean result = matcher.matches(message, condition);
  340 +
  341 + // 断言:修复后的实现应该能从 value 字段提取属性值
  342 + assertTrue(result);
  343 + }
  344 +
  345 + @Test
295 public void testMatches_voltageGreaterThanOrEquals_success() { 346 public void testMatches_voltageGreaterThanOrEquals_success() {
296 - // 准备参数  
297 - String propertyName = "voltage"; 347 + // 准备参数:创建属性上报消息
  348 + String propertyIdentifier = "voltage";
298 Double propertyValue = 12.0; 349 Double propertyValue = 12.0;
299 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
300 - IotDeviceMessage message = createDeviceMessage(properties); 350 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
301 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 351 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
302 - propertyName, 352 + propertyIdentifier,
303 IotSceneRuleConditionOperatorEnum.GREATER_THAN_OR_EQUALS.getOperator(), 353 IotSceneRuleConditionOperatorEnum.GREATER_THAN_OR_EQUALS.getOperator(),
304 String.valueOf(propertyValue) 354 String.valueOf(propertyValue)
305 ); 355 );
@@ -313,14 +363,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -313,14 +363,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
313 363
314 @Test 364 @Test
315 public void testMatches_currentLessThanOrEquals_success() { 365 public void testMatches_currentLessThanOrEquals_success() {
316 - // 准备参数  
317 - String propertyName = "current"; 366 + // 准备参数:创建属性上报消息
  367 + String propertyIdentifier = "current";
318 Double propertyValue = 2.5; 368 Double propertyValue = 2.5;
319 Double compareValue = 3.0; 369 Double compareValue = 3.0;
320 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
321 - IotDeviceMessage message = createDeviceMessage(properties); 370 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
322 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 371 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
323 - propertyName, 372 + propertyIdentifier,
324 IotSceneRuleConditionOperatorEnum.LESS_THAN_OR_EQUALS.getOperator(), 373 IotSceneRuleConditionOperatorEnum.LESS_THAN_OR_EQUALS.getOperator(),
325 String.valueOf(compareValue) 374 String.valueOf(compareValue)
326 ); 375 );
@@ -334,13 +383,12 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -334,13 +383,12 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
334 383
335 @Test 384 @Test
336 public void testMatches_stringProperty_success() { 385 public void testMatches_stringProperty_success() {
337 - // 准备参数  
338 - String propertyName = "mode"; 386 + // 准备参数:创建属性上报消息
  387 + String propertyIdentifier = "mode";
339 String propertyValue = "auto"; 388 String propertyValue = "auto";
340 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
341 - IotDeviceMessage message = createDeviceMessage(properties); 389 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
342 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 390 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
343 - propertyName, 391 + propertyIdentifier,
344 IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), 392 IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
345 propertyValue 393 propertyValue
346 ); 394 );
@@ -354,13 +402,12 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -354,13 +402,12 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
354 402
355 @Test 403 @Test
356 public void testMatches_booleanProperty_success() { 404 public void testMatches_booleanProperty_success() {
357 - // 准备参数  
358 - String propertyName = "enabled"; 405 + // 准备参数:创建属性上报消息
  406 + String propertyIdentifier = "enabled";
359 Boolean propertyValue = true; 407 Boolean propertyValue = true;
360 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
361 - IotDeviceMessage message = createDeviceMessage(properties); 408 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
362 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 409 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
363 - propertyName, 410 + propertyIdentifier,
364 IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), 411 IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
365 String.valueOf(propertyValue) 412 String.valueOf(propertyValue)
366 ); 413 );
@@ -372,40 +419,61 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -372,40 +419,61 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
372 assertTrue(result); 419 assertTrue(result);
373 } 420 }
374 421
375 - @Test  
376 - public void testMatches_multipleProperties_success() {  
377 - // 准备参数  
378 - Map<String, Object> properties = MapUtil.builder(new HashMap<String, Object>())  
379 - .put("temperature", 25.5)  
380 - .put("humidity", 60)  
381 - .put("status", "active")  
382 - .put("enabled", true)  
383 - .build();  
384 - IotDeviceMessage message = createDeviceMessage(properties);  
385 - String targetProperty = "humidity";  
386 - Integer targetValue = 60;  
387 - IotSceneRuleDO.TriggerCondition condition = createValidCondition(  
388 - targetProperty,  
389 - IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),  
390 - String.valueOf(targetValue)  
391 - ); 422 + // ========== 辅助方法 ==========
392 423
393 - // 调用  
394 - boolean result = matcher.matches(message, condition); 424 + /**
  425 + * 创建设备消息用于测试
  426 + *
  427 + * 支持的消息格式:
  428 + * 1. 直接属性值:params 直接是属性值(适用于简单消息)
  429 + * 2. 标识符+值:params 包含 identifier 和对应的属性值
  430 + * 3. properties 结构:params.properties[identifier] = value
  431 + * 4. data 结构:params.data[identifier] = value
  432 + * 5. value 字段:params.value = value
  433 + */
  434 + private IotDeviceMessage createPropertyPostMessage(String identifier, Object value) {
  435 + IotDeviceMessage message = new IotDeviceMessage();
  436 + message.setDeviceId(randomLongId());
  437 + message.setMethod("thing.event.post"); // 使用事件上报方法
395 438
396 - // 断言  
397 - assertTrue(result); 439 + // 创建符合修复后逻辑的 params 结构
  440 + Map<String, Object> params = new HashMap<>();
  441 + params.put("identifier", identifier);
  442 + // 直接将属性值放在标识符对应的字段中
  443 + params.put(identifier, value);
  444 + message.setParams(params);
  445 +
  446 + return message;
398 } 447 }
399 448
400 - // ========== 辅助方法 ========== 449 + /**
  450 + * 创建使用 properties 结构的消息(模拟真实的属性上报消息)
  451 + */
  452 + private IotDeviceMessage createPropertyPostMessageWithProperties(String identifier, Object value) {
  453 + IotDeviceMessage message = new IotDeviceMessage();
  454 + message.setDeviceId(randomLongId());
  455 + message.setMethod("thing.property.post"); // 属性上报方法
  456 +
  457 + Map<String, Object> properties = new HashMap<>();
  458 + properties.put(identifier, value);
  459 +
  460 + Map<String, Object> params = new HashMap<>();
  461 + params.put("properties", properties);
  462 + message.setParams(params);
  463 +
  464 + return message;
  465 + }
401 466
402 /** 467 /**
403 - * 创建设备消息 468 + * 创建简单值消息(params 直接是属性值)
404 */ 469 */
405 - private IotDeviceMessage createDeviceMessage(Map<String, Object> properties) { 470 + private IotDeviceMessage createSimpleValueMessage(Object value) {
406 IotDeviceMessage message = new IotDeviceMessage(); 471 IotDeviceMessage message = new IotDeviceMessage();
407 message.setDeviceId(randomLongId()); 472 message.setDeviceId(randomLongId());
408 - message.setParams(properties); 473 + message.setMethod("thing.property.post");
  474 + // 直接将属性值作为 params
  475 + message.setParams(value);
  476 +
409 return message; 477 return message;
410 } 478 }
411 479
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcherTest.java renamed to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcherTest.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
2 2
3 -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;  
4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; 3 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; 6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
  8 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
  9 +import org.junit.jupiter.api.BeforeEach;
9 import org.junit.jupiter.api.Test; 10 import org.junit.jupiter.api.Test;
10 -import org.mockito.InjectMocks;  
11 11
12 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; 12 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
13 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; 13 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
14 import static org.junit.jupiter.api.Assertions.*; 14 import static org.junit.jupiter.api.Assertions.*;
15 15
16 /** 16 /**
17 - * {@link DeviceStateConditionMatcher} 的单元测试 17 + * {@link IotDeviceStateConditionMatcher} 的单元测试
18 * 18 *
19 * @author HUIHUI 19 * @author HUIHUI
20 */ 20 */
21 -public class DeviceStateConditionMatcherTest extends BaseMockitoUnitTest { 21 +public class IotDeviceStateConditionMatcherTest extends IotBaseConditionMatcherTest {
22 22
23 - @InjectMocks  
24 - private DeviceStateConditionMatcher matcher; 23 + private IotDeviceStateConditionMatcher matcher;
  24 +
  25 + @BeforeEach
  26 + public void setUp() {
  27 + matcher = new IotDeviceStateConditionMatcher();
  28 + }
25 29
26 @Test 30 @Test
27 public void testGetSupportedConditionType() { 31 public void testGetSupportedConditionType() {
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcherTest.java renamed to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcherTest.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2
3 import cn.hutool.core.map.MapUtil; 3 import cn.hutool.core.map.MapUtil;
4 -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;  
5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  8 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
  9 +import org.junit.jupiter.api.BeforeEach;
9 import org.junit.jupiter.api.Test; 10 import org.junit.jupiter.api.Test;
10 -import org.mockito.InjectMocks;  
11 11
12 import java.util.HashMap; 12 import java.util.HashMap;
13 import java.util.Map; 13 import java.util.Map;
@@ -18,14 +18,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString @@ -18,14 +18,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
18 import static org.junit.jupiter.api.Assertions.*; 18 import static org.junit.jupiter.api.Assertions.*;
19 19
20 /** 20 /**
21 - * {@link DeviceEventPostTriggerMatcher} 的单元测试 21 + * {@link IotDeviceEventPostTriggerMatcher} 的单元测试
22 * 22 *
23 * @author HUIHUI 23 * @author HUIHUI
24 */ 24 */
25 -public class DeviceEventPostTriggerMatcherTest extends BaseMockitoUnitTest { 25 +public class IotDeviceEventPostTriggerMatcherTest extends IotBaseConditionMatcherTest {
26 26
27 - @InjectMocks  
28 - private DeviceEventPostTriggerMatcher matcher; 27 + private IotDeviceEventPostTriggerMatcher matcher;
  28 +
  29 + @BeforeEach
  30 + public void setUp() {
  31 + matcher = new IotDeviceEventPostTriggerMatcher();
  32 + }
29 33
30 @Test 34 @Test
31 public void testGetSupportedTriggerType_success() { 35 public void testGetSupportedTriggerType_success() {
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcherTest.java renamed to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcherTest.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2
3 import cn.hutool.core.map.MapUtil; 3 import cn.hutool.core.map.MapUtil;
4 -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;  
5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
9 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; 8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  9 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
  10 +import org.junit.jupiter.api.BeforeEach;
10 import org.junit.jupiter.api.Test; 11 import org.junit.jupiter.api.Test;
11 -import org.mockito.InjectMocks;  
12 12
13 import java.util.HashMap; 13 import java.util.HashMap;
14 import java.util.Map; 14 import java.util.Map;
@@ -20,14 +20,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString @@ -20,14 +20,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
20 import static org.junit.jupiter.api.Assertions.*; 20 import static org.junit.jupiter.api.Assertions.*;
21 21
22 /** 22 /**
23 - * {@link DevicePropertyPostTriggerMatcher} 的单元测试 23 + * {@link IotDevicePropertyPostTriggerMatcher} 的单元测试
24 * 24 *
25 * @author HUIHUI 25 * @author HUIHUI
26 */ 26 */
27 -public class DevicePropertyPostTriggerMatcherTest extends BaseMockitoUnitTest { 27 +public class IotDevicePropertyPostTriggerMatcherTest extends IotBaseConditionMatcherTest {
28 28
29 - @InjectMocks  
30 - private DevicePropertyPostTriggerMatcher matcher; 29 + private IotDevicePropertyPostTriggerMatcher matcher;
  30 +
  31 + @BeforeEach
  32 + public void setUp() {
  33 + matcher = new IotDevicePropertyPostTriggerMatcher();
  34 + }
31 35
32 @Test 36 @Test
33 public void testGetSupportedTriggerType_success() { 37 public void testGetSupportedTriggerType_success() {
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcherTest.java renamed to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcherTest.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2
3 import cn.hutool.core.map.MapUtil; 3 import cn.hutool.core.map.MapUtil;
4 -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;  
5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  8 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
  9 +import org.junit.jupiter.api.BeforeEach;
9 import org.junit.jupiter.api.Test; 10 import org.junit.jupiter.api.Test;
10 -import org.mockito.InjectMocks;  
11 11
12 import java.util.HashMap; 12 import java.util.HashMap;
13 import java.util.Map; 13 import java.util.Map;
@@ -18,14 +18,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString @@ -18,14 +18,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
18 import static org.junit.jupiter.api.Assertions.*; 18 import static org.junit.jupiter.api.Assertions.*;
19 19
20 /** 20 /**
21 - * {@link DeviceServiceInvokeTriggerMatcher} 的单元测试 21 + * {@link IotDeviceServiceInvokeTriggerMatcher} 的单元测试
22 * 22 *
23 * @author HUIHUI 23 * @author HUIHUI
24 */ 24 */
25 -public class DeviceServiceInvokeTriggerMatcherTest extends BaseMockitoUnitTest { 25 +public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMatcherTest {
26 26
27 - @InjectMocks  
28 - private DeviceServiceInvokeTriggerMatcher matcher; 27 + private IotDeviceServiceInvokeTriggerMatcher matcher;
  28 +
  29 + @BeforeEach
  30 + public void setUp() {
  31 + matcher = new IotDeviceServiceInvokeTriggerMatcher();
  32 + }
29 33
30 @Test 34 @Test
31 public void testGetSupportedTriggerType_success() { 35 public void testGetSupportedTriggerType_success() {
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcherTest.java renamed to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcherTest.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2
3 -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;  
4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 3 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
9 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; 8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  9 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
  10 +import org.junit.jupiter.api.BeforeEach;
10 import org.junit.jupiter.api.Test; 11 import org.junit.jupiter.api.Test;
11 -import org.mockito.InjectMocks;  
12 12
13 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; 13 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
14 import static org.junit.jupiter.api.Assertions.*; 14 import static org.junit.jupiter.api.Assertions.*;
15 15
16 /** 16 /**
17 - * {@link DeviceStateUpdateTriggerMatcher} 的单元测试 17 + * {@link IotDeviceStateUpdateTriggerMatcher} 的单元测试
18 * 18 *
19 * @author HUIHUI 19 * @author HUIHUI
20 */ 20 */
21 -public class DeviceStateUpdateTriggerMatcherTest extends BaseMockitoUnitTest { 21 +public class IotDeviceStateUpdateTriggerMatcherTest extends IotBaseConditionMatcherTest {
22 22
23 - @InjectMocks  
24 - private DeviceStateUpdateTriggerMatcher matcher; 23 + private IotDeviceStateUpdateTriggerMatcher matcher;
  24 +
  25 + @BeforeEach
  26 + public void setUp() {
  27 + matcher = new IotDeviceStateUpdateTriggerMatcher();
  28 + }
25 29
26 @Test 30 @Test
27 public void testGetSupportedTriggerType_success() { 31 public void testGetSupportedTriggerType_success() {
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcherTest.java renamed to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotTimerTriggerMatcherTest.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2
3 -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;  
4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; 5 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  6 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
  7 +import org.junit.jupiter.api.BeforeEach;
7 import org.junit.jupiter.api.Test; 8 import org.junit.jupiter.api.Test;
8 -import org.mockito.InjectMocks;  
9 9
10 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; 10 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
11 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; 11 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
12 import static org.junit.jupiter.api.Assertions.*; 12 import static org.junit.jupiter.api.Assertions.*;
13 13
14 /** 14 /**
15 - * {@link TimerTriggerMatcher} 的单元测试 15 + * {@link IotTimerTriggerMatcher} 的单元测试
16 * 16 *
17 * @author HUIHUI 17 * @author HUIHUI
18 */ 18 */
19 -public class TimerTriggerMatcherTest extends BaseMockitoUnitTest { 19 +public class IotTimerTriggerMatcherTest extends IotBaseConditionMatcherTest {
20 20
21 - @InjectMocks  
22 - private TimerTriggerMatcher matcher; 21 + private IotTimerTriggerMatcher matcher;
  22 +
  23 + @BeforeEach
  24 + public void setUp() {
  25 + matcher = new IotTimerTriggerMatcher();
  26 + }
23 27
24 @Test 28 @Test
25 public void testGetSupportedTriggerType_success() { 29 public void testGetSupportedTriggerType_success() {
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotSceneRuleTimerHandlerTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene.timer;
  2 +
  3 +import cn.hutool.core.collection.ListUtil;
  4 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  5 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  6 +import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager;
  7 +import cn.iocoder.yudao.module.iot.job.rule.IotSceneRuleJob;
  8 +import org.junit.jupiter.api.BeforeEach;
  9 +import org.junit.jupiter.api.Test;
  10 +import org.junit.jupiter.api.extension.ExtendWith;
  11 +import org.mockito.InjectMocks;
  12 +import org.mockito.Mock;
  13 +import org.mockito.junit.jupiter.MockitoExtension;
  14 +import org.quartz.SchedulerException;
  15 +
  16 +import static org.mockito.ArgumentMatchers.any;
  17 +import static org.mockito.ArgumentMatchers.eq;
  18 +import static org.mockito.Mockito.*;
  19 +
  20 +/**
  21 + * {@link IotSceneRuleTimerHandler} 的单元测试类
  22 + *
  23 + * @author HUIHUI
  24 + */
  25 +@ExtendWith(MockitoExtension.class)
  26 +public class IotSceneRuleTimerHandlerTest {
  27 +
  28 + @Mock
  29 + private IotSchedulerManager schedulerManager;
  30 +
  31 + @InjectMocks
  32 + private IotSceneRuleTimerHandler timerHandler;
  33 +
  34 + @BeforeEach
  35 + void setUp() {
  36 + // 重置 Mock 对象
  37 + reset(schedulerManager);
  38 + }
  39 +
  40 + @Test
  41 + public void testRegisterTimerTriggers_success() throws SchedulerException {
  42 + // 准备参数
  43 + Long sceneRuleId = 1L;
  44 + IotSceneRuleDO sceneRule = new IotSceneRuleDO();
  45 + sceneRule.setId(sceneRuleId);
  46 + sceneRule.setStatus(0); // 0 表示启用
  47 + // 创建定时触发器
  48 + IotSceneRuleDO.Trigger timerTrigger = new IotSceneRuleDO.Trigger();
  49 + timerTrigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType());
  50 + timerTrigger.setCronExpression("0 0 12 * * ?"); // 每天中午12点
  51 + sceneRule.setTriggers(ListUtil.toList(timerTrigger));
  52 +
  53 + // 调用
  54 + timerHandler.registerTimerTriggers(sceneRule);
  55 +
  56 + // 验证
  57 + verify(schedulerManager, times(1)).addOrUpdateJob(
  58 + eq(IotSceneRuleJob.class),
  59 + eq("iot_scene_rule_timer_" + sceneRuleId),
  60 + eq("0 0 12 * * ?"),
  61 + eq(IotSceneRuleJob.buildJobDataMap(sceneRuleId))
  62 + );
  63 + }
  64 +
  65 + @Test
  66 + public void testRegisterTimerTriggers_noTimerTrigger() throws SchedulerException {
  67 + // 准备参数 - 没有定时触发器
  68 + IotSceneRuleDO sceneRule = new IotSceneRuleDO();
  69 + sceneRule.setStatus(0); // 0 表示启用
  70 + // 创建非定时触发器
  71 + IotSceneRuleDO.Trigger deviceTrigger = new IotSceneRuleDO.Trigger();
  72 + deviceTrigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST.getType());
  73 + sceneRule.setTriggers(ListUtil.toList(deviceTrigger));
  74 +
  75 + // 调用
  76 + timerHandler.registerTimerTriggers(sceneRule);
  77 +
  78 + // 验证 - 不应该调用调度器
  79 + verify(schedulerManager, never()).addOrUpdateJob(any(), any(), any(), any());
  80 + }
  81 +
  82 + @Test
  83 + public void testRegisterTimerTriggers_emptyCronExpression() throws SchedulerException {
  84 + // 准备参数 - CRON 表达式为空
  85 + Long sceneRuleId = 2L;
  86 + IotSceneRuleDO sceneRule = new IotSceneRuleDO();
  87 + sceneRule.setId(sceneRuleId);
  88 + sceneRule.setStatus(0); // 0 表示启用
  89 + // 创建定时触发器但没有 CRON 表达式
  90 + IotSceneRuleDO.Trigger timerTrigger = new IotSceneRuleDO.Trigger();
  91 + timerTrigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType());
  92 + timerTrigger.setCronExpression(""); // 空的 CRON 表达式
  93 + sceneRule.setTriggers(ListUtil.toList(timerTrigger));
  94 +
  95 + // 调用
  96 + timerHandler.registerTimerTriggers(sceneRule);
  97 +
  98 + // 验证 - 不应该调用调度器
  99 + verify(schedulerManager, never()).addOrUpdateJob(any(), any(), any(), any());
  100 + }
  101 +
  102 + @Test
  103 + public void testUnregisterTimerTriggers_success() throws SchedulerException {
  104 + // 准备参数
  105 + Long sceneRuleId = 3L;
  106 +
  107 + // 调用
  108 + timerHandler.unregisterTimerTriggers(sceneRuleId);
  109 +
  110 + // 验证
  111 + verify(schedulerManager, times(1)).deleteJob("iot_scene_rule_timer_" + sceneRuleId);
  112 + }
  113 +
  114 + @Test
  115 + public void testPauseTimerTriggers_success() throws SchedulerException {
  116 + // 准备参数
  117 + Long sceneRuleId = 4L;
  118 +
  119 + // 调用
  120 + timerHandler.pauseTimerTriggers(sceneRuleId);
  121 +
  122 + // 验证
  123 + verify(schedulerManager, times(1)).pauseJob("iot_scene_rule_timer_" + sceneRuleId);
  124 + }
  125 +
  126 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java
@@ -43,7 +43,7 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable&lt;String&gt; { @@ -43,7 +43,7 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable&lt;String&gt; {
43 // ========== 设备配置 ========== 43 // ========== 设备配置 ==========
44 // 可参考:https://help.aliyun.com/zh/iot/user-guide/remote-configuration-1 44 // 可参考:https://help.aliyun.com/zh/iot/user-guide/remote-configuration-1
45 45
46 - CONFIG_PUSH("thing.config.push", "配置推送", true), 46 + CONFIG_PUSH("thing.config.push", "配置推送", false),
47 47
48 // ========== OTA 固件 ========== 48 // ========== OTA 固件 ==========
49 // 可参考:https://help.aliyun.com/zh/iot/user-guide/perform-ota-updates 49 // 可参考:https://help.aliyun.com/zh/iot/user-guide/perform-ota-updates
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java
@@ -69,6 +69,83 @@ public class IotDeviceMessageUtils { @@ -69,6 +69,83 @@ public class IotDeviceMessageUtils {
69 return null; 69 return null;
70 } 70 }
71 71
  72 + /**
  73 + * 从设备消息中提取指定标识符的属性值
  74 + * - 支持多种消息格式和属性值提取策略
  75 + * - 兼容现有的消息结构
  76 + * - 提供统一的属性值提取接口
  77 + * <p>
  78 + * 支持的提取策略(按优先级顺序):
  79 + * 1. 直接值:如果 params 不是 Map,直接返回该值(适用于简单消息)
  80 + * 2. 标识符字段:从 params[identifier] 获取
  81 + * 3. properties 结构:从 params.properties[identifier] 获取(标准属性上报)
  82 + * 4. data 结构:从 params.data[identifier] 获取
  83 + * 5. value 字段:从 params.value 获取(单值消息)
  84 + * 6. 单值 Map:如果 Map 只包含 identifier 和一个值,返回该值
  85 + *
  86 + * @param message 设备消息
  87 + * @param identifier 属性标识符
  88 + * @return 属性值,如果未找到则返回 null
  89 + */
  90 + @SuppressWarnings("unchecked")
  91 + public static Object extractPropertyValue(IotDeviceMessage message, String identifier) {
  92 + Object params = message.getParams();
  93 + if (params == null) {
  94 + return null;
  95 + }
  96 +
  97 + // 策略1:如果 params 不是 Map,直接返回该值(适用于简单的单属性消息)
  98 + if (!(params instanceof Map)) {
  99 + return params;
  100 + }
  101 +
  102 + Map<String, Object> paramsMap = (Map<String, Object>) params;
  103 +
  104 + // 策略2:直接通过标识符获取属性值
  105 + Object directValue = paramsMap.get(identifier);
  106 + if (directValue != null) {
  107 + return directValue;
  108 + }
  109 +
  110 + // 策略3:从 properties 字段中获取(适用于标准属性上报消息)
  111 + Object properties = paramsMap.get("properties");
  112 + if (properties instanceof Map) {
  113 + Map<String, Object> propertiesMap = (Map<String, Object>) properties;
  114 + Object propertyValue = propertiesMap.get(identifier);
  115 + if (propertyValue != null) {
  116 + return propertyValue;
  117 + }
  118 + }
  119 +
  120 + // 策略4:从 data 字段中获取(适用于某些消息格式)
  121 + Object data = paramsMap.get("data");
  122 + if (data instanceof Map) {
  123 + Map<String, Object> dataMap = (Map<String, Object>) data;
  124 + Object dataValue = dataMap.get(identifier);
  125 + if (dataValue != null) {
  126 + return dataValue;
  127 + }
  128 + }
  129 +
  130 + // 策略5:从 value 字段中获取(适用于单值消息)
  131 + Object value = paramsMap.get("value");
  132 + if (value != null) {
  133 + return value;
  134 + }
  135 +
  136 + // 策略6:如果 Map 只有两个字段且包含 identifier,返回另一个字段的值
  137 + if (paramsMap.size() == 2 && paramsMap.containsKey("identifier")) {
  138 + for (Map.Entry<String, Object> entry : paramsMap.entrySet()) {
  139 + if (!"identifier".equals(entry.getKey())) {
  140 + return entry.getValue();
  141 + }
  142 + }
  143 + }
  144 +
  145 + // 未找到对应的属性值
  146 + return null;
  147 + }
  148 +
72 // ========== Topic 相关 ========== 149 // ========== Topic 相关 ==========
73 150
74 public static String buildMessageBusGatewayDeviceMessageTopic(String serverId) { 151 public static String buildMessageBusGatewayDeviceMessageTopic(String serverId) {
yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtilsTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.util;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  4 +import org.junit.jupiter.api.Test;
  5 +
  6 +import java.util.HashMap;
  7 +import java.util.Map;
  8 +
  9 +import static org.junit.jupiter.api.Assertions.assertEquals;
  10 +import static org.junit.jupiter.api.Assertions.assertNull;
  11 +
  12 +/**
  13 + * {@link IotDeviceMessageUtils} 的单元测试
  14 + *
  15 + * @author HUIHUI
  16 + */
  17 +public class IotDeviceMessageUtilsTest {
  18 +
  19 + @Test
  20 + public void testExtractPropertyValue_directValue() {
  21 + // 测试直接值(非 Map)
  22 + IotDeviceMessage message = new IotDeviceMessage();
  23 + message.setParams(25.5);
  24 +
  25 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  26 + assertEquals(25.5, result);
  27 + }
  28 +
  29 + @Test
  30 + public void testExtractPropertyValue_directIdentifier() {
  31 + // 测试直接通过标识符获取
  32 + IotDeviceMessage message = new IotDeviceMessage();
  33 + Map<String, Object> params = new HashMap<>();
  34 + params.put("temperature", 25.5);
  35 + message.setParams(params);
  36 +
  37 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  38 + assertEquals(25.5, result);
  39 + }
  40 +
  41 + @Test
  42 + public void testExtractPropertyValue_propertiesStructure() {
  43 + // 测试 properties 结构
  44 + IotDeviceMessage message = new IotDeviceMessage();
  45 + Map<String, Object> properties = new HashMap<>();
  46 + properties.put("temperature", 25.5);
  47 + properties.put("humidity", 60);
  48 +
  49 + Map<String, Object> params = new HashMap<>();
  50 + params.put("properties", properties);
  51 + message.setParams(params);
  52 +
  53 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  54 + assertEquals(25.5, result);
  55 + }
  56 +
  57 + @Test
  58 + public void testExtractPropertyValue_dataStructure() {
  59 + // 测试 data 结构
  60 + IotDeviceMessage message = new IotDeviceMessage();
  61 + Map<String, Object> data = new HashMap<>();
  62 + data.put("temperature", 25.5);
  63 +
  64 + Map<String, Object> params = new HashMap<>();
  65 + params.put("data", data);
  66 + message.setParams(params);
  67 +
  68 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  69 + assertEquals(25.5, result);
  70 + }
  71 +
  72 + @Test
  73 + public void testExtractPropertyValue_valueField() {
  74 + // 测试 value 字段
  75 + IotDeviceMessage message = new IotDeviceMessage();
  76 + Map<String, Object> params = new HashMap<>();
  77 + params.put("identifier", "temperature");
  78 + params.put("value", 25.5);
  79 + message.setParams(params);
  80 +
  81 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  82 + assertEquals(25.5, result);
  83 + }
  84 +
  85 + @Test
  86 + public void testExtractPropertyValue_singleValueMap() {
  87 + // 测试单值 Map(包含 identifier 和一个值)
  88 + IotDeviceMessage message = new IotDeviceMessage();
  89 + Map<String, Object> params = new HashMap<>();
  90 + params.put("identifier", "temperature");
  91 + params.put("actualValue", 25.5);
  92 + message.setParams(params);
  93 +
  94 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  95 + assertEquals(25.5, result);
  96 + }
  97 +
  98 + @Test
  99 + public void testExtractPropertyValue_notFound() {
  100 + // 测试未找到属性值
  101 + IotDeviceMessage message = new IotDeviceMessage();
  102 + Map<String, Object> params = new HashMap<>();
  103 + params.put("humidity", 60);
  104 + message.setParams(params);
  105 +
  106 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  107 + assertNull(result);
  108 + }
  109 +
  110 + @Test
  111 + public void testExtractPropertyValue_nullParams() {
  112 + // 测试 params 为 null
  113 + IotDeviceMessage message = new IotDeviceMessage();
  114 + message.setParams(null);
  115 +
  116 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  117 + assertNull(result);
  118 + }
  119 +
  120 + @Test
  121 + public void testExtractPropertyValue_priorityOrder() {
  122 + // 测试优先级顺序:直接标识符 > properties > data > value
  123 + IotDeviceMessage message = new IotDeviceMessage();
  124 +
  125 + Map<String, Object> properties = new HashMap<>();
  126 + properties.put("temperature", 20.0);
  127 +
  128 + Map<String, Object> data = new HashMap<>();
  129 + data.put("temperature", 30.0);
  130 +
  131 + Map<String, Object> params = new HashMap<>();
  132 + params.put("temperature", 25.5); // 最高优先级
  133 + params.put("properties", properties);
  134 + params.put("data", data);
  135 + params.put("value", 40.0);
  136 + message.setParams(params);
  137 +
  138 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  139 + assertEquals(25.5, result); // 应该返回直接标识符的值
  140 + }
  141 +}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java
@@ -125,7 +125,7 @@ public class IotMqttConnectionManager { @@ -125,7 +125,7 @@ public class IotMqttConnectionManager {
125 */ 125 */
126 public IotMqttConnectionManager.ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) { 126 public IotMqttConnectionManager.ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) {
127 // 通过设备 ID 获取连接端点 127 // 通过设备 ID 获取连接端点
128 - var endpoint = getDeviceEndpoint(deviceId); 128 + MqttEndpoint endpoint = getDeviceEndpoint(deviceId);
129 if (endpoint == null) { 129 if (endpoint == null) {
130 return null; 130 return null;
131 } 131 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java
@@ -68,7 +68,13 @@ public class IotMqttUpstreamHandler { @@ -68,7 +68,13 @@ public class IotMqttUpstreamHandler {
68 68
69 log.info("[handle][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username); 69 log.info("[handle][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username);
70 70
71 - // 2. 设置异常和关闭处理器 71 + // 2. 设置心跳处理器(监听客户端的 PINGREQ 消息)
  72 + endpoint.pingHandler(v -> {
  73 + log.debug("[handle][收到客户端心跳,客户端 ID: {}]", clientId);
  74 + // Vert.x 会自动发送 PINGRESP 响应,无需手动处理
  75 + });
  76 +
  77 + // 3. 设置异常和关闭处理器
72 endpoint.exceptionHandler(ex -> { 78 endpoint.exceptionHandler(ex -> {
73 log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, connectionManager.getEndpointAddress(endpoint)); 79 log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, connectionManager.getEndpointAddress(endpoint));
74 cleanupConnection(endpoint); 80 cleanupConnection(endpoint);
@@ -77,7 +83,7 @@ public class IotMqttUpstreamHandler { @@ -77,7 +83,7 @@ public class IotMqttUpstreamHandler {
77 cleanupConnection(endpoint); 83 cleanupConnection(endpoint);
78 }); 84 });
79 85
80 - // 3. 设置消息处理器 86 + // 4. 设置消息处理器
81 endpoint.publishHandler(message -> { 87 endpoint.publishHandler(message -> {
82 try { 88 try {
83 processMessage(clientId, message.topicName(), message.payload().getBytes()); 89 processMessage(clientId, message.topicName(), message.payload().getBytes());
@@ -100,7 +106,7 @@ public class IotMqttUpstreamHandler { @@ -100,7 +106,7 @@ public class IotMqttUpstreamHandler {
100 } 106 }
101 }); 107 });
102 108
103 - // 4. 设置订阅处理器 109 + // 5. 设置订阅处理器
104 endpoint.subscribeHandler(subscribe -> { 110 endpoint.subscribeHandler(subscribe -> {
105 // 提取主题名称列表用于日志显示 111 // 提取主题名称列表用于日志显示
106 List<String> topicNames = subscribe.topicSubscriptions().stream() 112 List<String> topicNames = subscribe.topicSubscriptions().stream()
@@ -115,22 +121,22 @@ public class IotMqttUpstreamHandler { @@ -115,22 +121,22 @@ public class IotMqttUpstreamHandler {
115 endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels); 121 endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels);
116 }); 122 });
117 123
118 - // 5. 设置取消订阅处理器 124 + // 6. 设置取消订阅处理器
119 endpoint.unsubscribeHandler(unsubscribe -> { 125 endpoint.unsubscribeHandler(unsubscribe -> {
120 log.debug("[handle][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics()); 126 log.debug("[handle][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics());
121 endpoint.unsubscribeAcknowledge(unsubscribe.messageId()); 127 endpoint.unsubscribeAcknowledge(unsubscribe.messageId());
122 }); 128 });
123 129
124 - // 6. 设置 QoS 2消息的 PUBREL 处理器 130 + // 7. 设置 QoS 2消息的 PUBREL 处理器
125 endpoint.publishReleaseHandler(endpoint::publishComplete); 131 endpoint.publishReleaseHandler(endpoint::publishComplete);
126 132
127 - // 7. 设置断开连接处理器 133 + // 8. 设置断开连接处理器
128 endpoint.disconnectHandler(v -> { 134 endpoint.disconnectHandler(v -> {
129 log.debug("[handle][设备断开连接,客户端 ID: {}]", clientId); 135 log.debug("[handle][设备断开连接,客户端 ID: {}]", clientId);
130 cleanupConnection(endpoint); 136 cleanupConnection(endpoint);
131 }); 137 });
132 138
133 - // 8. 接受连接 139 + // 9. 接受连接
134 endpoint.accept(false); 140 endpoint.accept(false);
135 } 141 }
136 142
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
@@ -103,7 +103,6 @@ yudao: @@ -103,7 +103,6 @@ yudao:
103 port: 1883 103 port: 1883
104 max-message-size: 8192 104 max-message-size: 8192
105 connect-timeout-seconds: 60 105 connect-timeout-seconds: 60
106 - keep-alive-timeout-seconds: 300  
107 ssl-enabled: false 106 ssl-enabled: false
108 107
109 --- #################### 日志相关配置 #################### 108 --- #################### 日志相关配置 ####################
yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityRespVO.java
@@ -14,7 +14,7 @@ import java.time.LocalDateTime; @@ -14,7 +14,7 @@ import java.time.LocalDateTime;
14 public class RewardActivityRespVO extends RewardActivityBaseVO { 14 public class RewardActivityRespVO extends RewardActivityBaseVO {
15 15
16 @Schema(description = "活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") 16 @Schema(description = "活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
17 - private Integer id; 17 + private Long id;
18 18
19 @Schema(description = "活动状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") 19 @Schema(description = "活动状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
20 private Integer status; 20 private Integer status;
yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java
@@ -284,8 +284,8 @@ public class CouponServiceImpl implements CouponService { @@ -284,8 +284,8 @@ public class CouponServiceImpl implements CouponService {
284 } 284 }
285 // 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时) 285 // 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时)
286 if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType()) 286 if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType())
287 - && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TAKE_LIMIT_COUNT_MAX) // 非不限制  
288 - && couponTemplate.getTakeCount() + userIds.size() > couponTemplate.getTotalCount()) { 287 + && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TAKE_LIMIT_COUNT_MAX) // 校验不限制领取数
  288 + && ObjUtil.notEqual(couponTemplate.getTotalCount(), CouponTemplateDO.TOTAL_COUNT_MAX)) { // 校验不限制发放数量
289 throw exception(COUPON_TEMPLATE_NOT_ENOUGH); 289 throw exception(COUPON_TEMPLATE_NOT_ENOUGH);
290 } 290 }
291 // 校验"固定日期"的有效期类型是否过期 291 // 校验"固定日期"的有效期类型是否过期
yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
@@ -68,8 +68,11 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC @@ -68,8 +68,11 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
68 payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath()); 68 payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath());
69 } else if (Objects.equals(config.getApiVersion(), API_VERSION_V3)) { 69 } else if (Objects.equals(config.getApiVersion(), API_VERSION_V3)) {
70 payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath()); 70 payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
71 - payConfig.setPublicKeyPath(FileUtils.createTempFile(config.getPublicKeyContent()).getPath());  
72 - // 特殊:强制使用微信公用模式,避免灰度期间的问题!!! 71 + // 参考 https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/ICUE53 和 https://t.zsxq.com/ODR5V
  72 + if (StrUtil.isNotBlank(config.getPublicKeyContent())) {
  73 + payConfig.setPrivateCertPath(FileUtils.createTempFile(Base64.decode(config.getPublicKeyContent())).getPath());
  74 + }
  75 + // 特殊:强制使用微信公钥模式,避免灰度期间的问题!!!
73 payConfig.setStrictlyNeedWechatPaySerial(true); 76 payConfig.setStrictlyNeedWechatPaySerial(true);
74 } 77 }
75 78
yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/weixin/WxPayClientConfig.java
@@ -81,7 +81,6 @@ public class WxPayClientConfig implements PayClientConfig { @@ -81,7 +81,6 @@ public class WxPayClientConfig implements PayClientConfig {
81 /** 81 /**
82 * pub_key.pem 证书文件的对应字符串 82 * pub_key.pem 证书文件的对应字符串
83 */ 83 */
84 - @NotBlank(message = "pub_key.pem 不能为空", groups = V3.class)  
85 private String publicKeyContent; 84 private String publicKeyContent;
86 @NotBlank(message = "publicKeyId 不能为空", groups = V3.class) 85 @NotBlank(message = "publicKeyId 不能为空", groups = V3.class)
87 private String publicKeyId; 86 private String publicKeyId;
yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java
@@ -610,7 +610,7 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest { @@ -610,7 +610,7 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
610 orderExtensionMapper.insert(orderExtension); 610 orderExtensionMapper.insert(orderExtension);
611 // 重要:需要将 order 的 extensionId 更新下 611 // 重要:需要将 order 的 extensionId 更新下
612 order.setExtensionId(orderExtension.getId()); 612 order.setExtensionId(orderExtension.getId());
613 - orderMapper.updateById(new PayOrderDO().setId(order.getId()).setExtensionId(orderExtension.getId())); 613 + orderMapper.updateById(order);
614 // 准备参数 614 // 准备参数
615 PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); 615 PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
616 PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, 616 PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java
@@ -54,6 +54,7 @@ import me.zhyd.oauth.model.AuthResponse; @@ -54,6 +54,7 @@ import me.zhyd.oauth.model.AuthResponse;
54 import me.zhyd.oauth.model.AuthUser; 54 import me.zhyd.oauth.model.AuthUser;
55 import me.zhyd.oauth.request.AuthRequest; 55 import me.zhyd.oauth.request.AuthRequest;
56 import me.zhyd.oauth.utils.AuthStateUtils; 56 import me.zhyd.oauth.utils.AuthStateUtils;
  57 +import org.springframework.beans.factory.annotation.Autowired;
57 import org.springframework.beans.factory.annotation.Value; 58 import org.springframework.beans.factory.annotation.Value;
58 import org.springframework.cache.annotation.Cacheable; 59 import org.springframework.cache.annotation.Cacheable;
59 import org.springframework.data.redis.core.StringRedisTemplate; 60 import org.springframework.data.redis.core.StringRedisTemplate;
@@ -101,7 +102,8 @@ public class SocialClientServiceImpl implements SocialClientService { @@ -101,7 +102,8 @@ public class SocialClientServiceImpl implements SocialClientService {
101 @Value("${yudao.wxa-subscribe-message.miniprogram-state:formal}") 102 @Value("${yudao.wxa-subscribe-message.miniprogram-state:formal}")
102 public String miniprogramState; 103 public String miniprogramState;
103 104
104 - @Resource 105 + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
  106 + @Autowired(required = false) // 由于 justauth.enable 配置项,可以关闭 AuthRequestFactory 的功能,所以这里只能不强制注入
105 private AuthRequestFactory authRequestFactory; 107 private AuthRequestFactory authRequestFactory;
106 108
107 @Resource 109 @Resource