Commit 521a24b382d95dc9f80b9be1ffe6abd96d8017d8
Committed by
GitHub
Merge branch 'YunaiV:master-jdk17' into master-jdk17
Showing
111 changed files
with
3021 additions
and
1278 deletions
Too many changes.
To preserve performance only 100 of 111 files are displayed.
sql/tools/convertor.py
| ... | ... | @@ -6,12 +6,12 @@ Author: dhb52 (https://gitee.com/dhb52) |
| 6 | 6 | pip install simple-ddl-parser |
| 7 | 7 | |
| 8 | 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 | 17 | import argparse |
| ... | ... | @@ -24,6 +24,9 @@ from typing import Dict, Generator, Optional, Tuple, Union |
| 24 | 24 | |
| 25 | 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 | 30 | PREAMBLE = """/* |
| 28 | 31 | Yudao Database Transfer Tool |
| 29 | 32 | |
| ... | ... | @@ -919,9 +922,15 @@ def main(): |
| 919 | 922 | help="目标数据库类型", |
| 920 | 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 | 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 | 934 | convertor = None |
| 926 | 935 | if args.type == "postgres": |
| 927 | 936 | convertor = PostgreSQLConvertor(sql_file) | ... | ... |
yudao-dependencies/pom.xml
| ... | ... | @@ -24,7 +24,7 @@ |
| 24 | 24 | <!-- DB 相关 --> |
| 25 | 25 | <druid.version>1.2.27</druid.version> |
| 26 | 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 | 28 | <mybatis-plus-join.version>1.5.4</mybatis-plus-join.version> |
| 29 | 29 | <dynamic-datasource.version>4.3.1</dynamic-datasource.version> |
| 30 | 30 | <easy-trans.version>3.0.6</easy-trans.version> |
| ... | ... | @@ -61,7 +61,7 @@ |
| 61 | 61 | <transmittable-thread-local.version>2.14.5</transmittable-thread-local.version> |
| 62 | 62 | <commons-net.version>3.11.1</commons-net.version> |
| 63 | 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 | 65 | <tika-core.version>3.2.2</tika-core.version> |
| 66 | 66 | <ip2region.version>2.7.0</ip2region.version> |
| 67 | 67 | <bizlog-sdk.version>3.0.6</bizlog-sdk.version> |
| ... | ... | @@ -526,7 +526,7 @@ |
| 526 | 526 | <version>${commons-net.version}</version> |
| 527 | 527 | </dependency> |
| 528 | 528 | <dependency> |
| 529 | - <groupId>com.jcraft</groupId> | |
| 529 | + <groupId>com.github.mwiede</groupId> | |
| 530 | 530 | <artifactId>jsch</artifactId> <!-- 解决 sftp 连接 --> |
| 531 | 531 | <version>${jsch.version}</version> |
| 532 | 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 | import cn.iocoder.yudao.framework.mybatis.core.handler.DefaultDBFieldHandler; |
| 7 | 7 | import com.baomidou.mybatisplus.annotation.DbType; |
| 8 | 8 | import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; |
| 9 | +import com.baomidou.mybatisplus.core.handlers.IJsonTypeHandler; | |
| 9 | 10 | import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; |
| 10 | 11 | import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator; |
| 11 | 12 | import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; |
| ... | ... | @@ -80,8 +81,8 @@ public class YudaoMybatisAutoConfiguration { |
| 80 | 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 | 86 | // 特殊:设置 JacksonTypeHandler 的 ObjectMapper! |
| 86 | 87 | ObjectMapper objectMapper = CollUtil.getFirst(objectMappers); |
| 87 | 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 | 60 | * 人大金仓 |
| 61 | 61 | */ |
| 62 | 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 | 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 | 1 | package cn.iocoder.yudao.framework.swagger.config; |
| 2 | 2 | |
| 3 | +import com.github.xiaoymin.knife4j.spring.configuration.Knife4jAutoConfiguration; | |
| 3 | 4 | import io.swagger.v3.oas.models.Components; |
| 4 | 5 | import io.swagger.v3.oas.models.OpenAPI; |
| 5 | 6 | import io.swagger.v3.oas.models.info.Contact; |
| ... | ... | @@ -11,6 +12,7 @@ import io.swagger.v3.oas.models.parameters.Parameter; |
| 11 | 12 | import io.swagger.v3.oas.models.security.SecurityRequirement; |
| 12 | 13 | import io.swagger.v3.oas.models.security.SecurityScheme; |
| 13 | 14 | import org.springdoc.core.customizers.OpenApiBuilderCustomizer; |
| 15 | +import org.springdoc.core.customizers.OperationCustomizer; | |
| 14 | 16 | import org.springdoc.core.customizers.ServerBaseUrlCustomizer; |
| 15 | 17 | import org.springdoc.core.models.GroupedOpenApi; |
| 16 | 18 | import org.springdoc.core.properties.SpringDocConfigProperties; |
| ... | ... | @@ -43,7 +45,7 @@ import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_ |
| 43 | 45 | * |
| 44 | 46 | * @author 芋道源码 |
| 45 | 47 | */ |
| 46 | -@AutoConfiguration | |
| 48 | +@AutoConfiguration(before = Knife4jAutoConfiguration.class) // before 原因,保证覆写的 Knife4jOpenApiCustomizer 先生效!相关 https://github.com/YunaiV/ruoyi-vue-pro/issues/954 讨论 | |
| 47 | 49 | @ConditionalOnClass({OpenAPI.class}) |
| 48 | 50 | @EnableConfigurationProperties(SwaggerProperties.class) |
| 49 | 51 | @ConditionalOnProperty(prefix = "springdoc.api-docs", name = "enabled", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用 |
| ... | ... | @@ -127,6 +129,7 @@ public class YudaoSwaggerAutoConfiguration { |
| 127 | 129 | .addOperationCustomizer((operation, handlerMethod) -> operation |
| 128 | 130 | .addParametersItem(buildTenantHeaderParameter()) |
| 129 | 131 | .addParametersItem(buildSecurityHeaderParameter())) |
| 132 | + .addOperationCustomizer(buildOperationIdCustomizer()) | |
| 130 | 133 | .build(); |
| 131 | 134 | } |
| 132 | 135 | |
| ... | ... | @@ -158,5 +161,26 @@ public class YudaoSwaggerAutoConfiguration { |
| 158 | 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 | 1 | package cn.iocoder.yudao.framework.web.config; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.util.StrUtil; | |
| 3 | 4 | import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiErrorLogCommonApi; |
| 4 | 5 | import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; |
| 5 | 6 | import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyFilter; |
| ... | ... | @@ -7,6 +8,7 @@ import cn.iocoder.yudao.framework.web.core.filter.DemoFilter; |
| 7 | 8 | import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; |
| 8 | 9 | import cn.iocoder.yudao.framework.web.core.handler.GlobalResponseBodyHandler; |
| 9 | 10 | import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; |
| 11 | +import com.google.common.collect.Maps; | |
| 10 | 12 | import jakarta.annotation.Resource; |
| 11 | 13 | import jakarta.servlet.Filter; |
| 12 | 14 | import org.springframework.beans.factory.annotation.Value; |
| ... | ... | @@ -14,6 +16,7 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; |
| 14 | 16 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; |
| 15 | 17 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| 16 | 18 | import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; |
| 19 | +import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations; | |
| 17 | 20 | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| 18 | 21 | import org.springframework.boot.web.client.RestTemplateBuilder; |
| 19 | 22 | import org.springframework.boot.web.servlet.FilterRegistrationBean; |
| ... | ... | @@ -27,35 +30,57 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource; |
| 27 | 30 | import org.springframework.web.filter.CorsFilter; |
| 28 | 31 | import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; |
| 29 | 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 | 38 | @AutoConfiguration |
| 32 | 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 | 45 | @Value("${spring.application.name}") |
| 41 | 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 | 86 | @Bean | ... | ... |
yudao-module-ai/pom.xml
| ... | ... | @@ -78,6 +78,12 @@ |
| 78 | 78 | <groupId>org.springframework.ai</groupId> |
| 79 | 79 | <artifactId>spring-ai-starter-model-openai</artifactId> |
| 80 | 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 | 87 | </dependency> |
| 82 | 88 | <dependency> |
| 83 | 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 | 100 | @Schema(description = "任务后置通知设置", example = "{}") |
| 101 | 101 | private HttpRequestSetting taskAfterTriggerSetting; |
| 102 | 102 | |
| 103 | + @Schema(description = "自定义打印模板设置", example = "{}") | |
| 104 | + @Valid | |
| 105 | + private PrintTemplateSetting printTemplateSetting; | |
| 106 | + | |
| 103 | 107 | @Schema(description = "流程 ID 规则") |
| 104 | 108 | @Data |
| 105 | 109 | @Valid |
| ... | ... | @@ -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 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 7 | 7 | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| 8 | 8 | import cn.iocoder.yudao.framework.common.util.number.NumberUtils; |
| 9 | +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; | |
| 9 | 10 | import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*; |
| 10 | 11 | import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert; |
| 11 | 12 | import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; |
| ... | ... | @@ -26,6 +27,7 @@ import jakarta.validation.Valid; |
| 26 | 27 | import org.flowable.engine.history.HistoricProcessInstance; |
| 27 | 28 | import org.flowable.engine.repository.ProcessDefinition; |
| 28 | 29 | import org.flowable.task.api.Task; |
| 30 | +import org.flowable.task.api.history.HistoricTaskInstance; | |
| 29 | 31 | import org.springframework.security.access.prepost.PreAuthorize; |
| 30 | 32 | import org.springframework.validation.annotation.Validated; |
| 31 | 33 | import org.springframework.web.bind.annotation.*; |
| ... | ... | @@ -34,9 +36,11 @@ import java.util.List; |
| 34 | 36 | import java.util.Map; |
| 35 | 37 | import java.util.Set; |
| 36 | 38 | |
| 39 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | |
| 37 | 40 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| 38 | 41 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; |
| 39 | 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 | 45 | @Tag(name = "管理后台 - 流程实例") // 流程实例,通过流程定义创建的一次“申请” |
| 42 | 46 | @RestController |
| ... | ... | @@ -192,8 +196,30 @@ public class BpmProcessInstanceController { |
| 192 | 196 | @GetMapping("/get-bpmn-model-view") |
| 193 | 197 | @Operation(summary = "获取流程实例的 BPMN 模型视图", description = "在【流程详细】界面中,进行调用") |
| 194 | 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 | 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 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.PageParam; |
| 4 | 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 | 7 | import io.swagger.v3.oas.annotations.media.Schema; |
| 6 | 8 | import lombok.Data; |
| 7 | 9 | import org.springframework.format.annotation.DateTimeFormat; |
| ... | ... | @@ -21,6 +23,10 @@ public class BpmTaskPageReqVO extends PageParam { |
| 21 | 23 | @Schema(description = "流程定义的标识", example = "2048") |
| 22 | 24 | private String processDefinitionKey; // 精准匹配 |
| 23 | 25 | |
| 26 | + @Schema(description = "审批状态", example = "1") | |
| 27 | + @InEnum(BpmTaskStatusEnum.class) | |
| 28 | + private Integer status; // 仅【已办】使用 | |
| 29 | + | |
| 24 | 30 | @Schema(description = "创建时间") |
| 25 | 31 | @DateTimeFormat(pattern = DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) |
| 26 | 32 | private LocalDateTime[] createTime; | ... | ... |
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java
| 1 | 1 | package cn.iocoder.yudao.module.bpm.convert.task; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | +import cn.hutool.core.date.DateUtil; | |
| 4 | 5 | import cn.hutool.core.util.StrUtil; |
| 5 | 6 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 6 | 7 | import cn.iocoder.yudao.framework.common.util.collection.MapUtils; |
| 7 | 8 | import cn.iocoder.yudao.framework.common.util.collection.SetUtils; |
| 9 | +import cn.iocoder.yudao.framework.common.util.date.DateUtils; | |
| 8 | 10 | import cn.iocoder.yudao.framework.common.util.number.NumberUtils; |
| 9 | 11 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| 10 | 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 | 14 | import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; |
| 12 | 15 | import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO; |
| 13 | 16 | import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO; |
| 14 | 17 | import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceBpmnModelViewRespVO; |
| 15 | 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 | 20 | import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO; |
| 17 | 21 | import cn.iocoder.yudao.module.bpm.convert.definition.BpmProcessDefinitionConvert; |
| 18 | 22 | import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; |
| 19 | 23 | import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; |
| 20 | 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 | 27 | import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; |
| 22 | 28 | import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; |
| 23 | 29 | import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO; |
| ... | ... | @@ -35,10 +41,7 @@ import org.mapstruct.Mapping; |
| 35 | 41 | import org.mapstruct.MappingTarget; |
| 36 | 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 | 46 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; |
| 44 | 47 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; |
| ... | ... | @@ -293,4 +296,47 @@ public interface BpmProcessInstanceConvert { |
| 293 | 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 | 224 | @TableField(typeHandler = JacksonTypeHandler.class) |
| 225 | 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 | 51 | String noPrefix = processIdRule.getPrefix() + infix + processIdRule.getPostfix(); |
| 52 | 52 | String key = RedisKeyConstants.BPM_PROCESS_ID + noPrefix; |
| 53 | 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 | 56 | stringRedisTemplate.expire(key, Duration.ofDays(1L)); |
| 57 | 57 | } |
| 58 | 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 | 63 | ErrorCode TASK_WITHDRAW_FAIL_PROCESS_NOT_RUNNING = new ErrorCode(1_009_005_017, "撤回失败,流程实例未运行!"); |
| 64 | 64 | ErrorCode TASK_WITHDRAW_FAIL_TASK_NOT_EXISTS = new ErrorCode(1_009_005_018, "撤回失败,未查询到用户已办任务!"); |
| 65 | 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 | 68 | // ========== 动态表单模块 1-009-010-000 ========== |
| 69 | 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 | 1 | package cn.iocoder.yudao.module.bpm.enums.task; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.util.ArrayUtil; | |
| 3 | 4 | import cn.iocoder.yudao.framework.common.core.ArrayValuable; |
| 4 | 5 | import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; |
| 5 | 6 | import lombok.AllArgsConstructor; |
| ... | ... | @@ -47,4 +48,8 @@ public enum BpmProcessInstanceStatusEnum implements ArrayValuable<Integer> { |
| 47 | 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 | 1 | package cn.iocoder.yudao.module.bpm.enums.task; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.util.ArrayUtil; | |
| 3 | 4 | import cn.hutool.core.util.ObjUtil; |
| 5 | +import cn.iocoder.yudao.framework.common.core.ArrayValuable; | |
| 4 | 6 | import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; |
| 5 | 7 | import lombok.AllArgsConstructor; |
| 6 | 8 | import lombok.Getter; |
| 7 | 9 | |
| 10 | +import java.util.Arrays; | |
| 11 | + | |
| 8 | 12 | /** |
| 9 | 13 | * 流程任务 Task 的状态枚举 |
| 10 | 14 | * |
| ... | ... | @@ -12,7 +16,7 @@ import lombok.Getter; |
| 12 | 16 | */ |
| 13 | 17 | @Getter |
| 14 | 18 | @AllArgsConstructor |
| 15 | -public enum BpmTaskStatusEnum { | |
| 19 | +public enum BpmTaskStatusEnum implements ArrayValuable<Integer> { | |
| 16 | 20 | |
| 17 | 21 | SKIP(-2, "跳过"), |
| 18 | 22 | NOT_START(-1, "未开始"), |
| ... | ... | @@ -35,6 +39,8 @@ public enum BpmTaskStatusEnum { |
| 35 | 39 | */ |
| 36 | 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 | 46 | * <p> |
| ... | ... | @@ -46,6 +52,11 @@ public enum BpmTaskStatusEnum { |
| 46 | 52 | */ |
| 47 | 53 | private final String name; |
| 48 | 54 | |
| 55 | + @Override | |
| 56 | + public Integer[] array() { | |
| 57 | + return ARRAYS; | |
| 58 | + } | |
| 59 | + | |
| 49 | 60 | public static boolean isRejectStatus(Integer status) { |
| 50 | 61 | return REJECT.getStatus().equals(status); |
| 51 | 62 | } |
| ... | ... | @@ -68,4 +79,8 @@ public enum BpmTaskStatusEnum { |
| 68 | 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 | 43 | * @see ProcessInstance#getProcessVariables() |
| 44 | 44 | */ |
| 45 | 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 | 52 | * @see ProcessInstance#getProcessVariables() |
| 52 | 53 | */ |
| 53 | 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 | 658 | |
| 659 | 659 | // 根据类型,获取入口连线 |
| 660 | 660 | List<SequenceFlow> sequenceFlows = getElementIncomingFlows(source); |
| 661 | + // 1. 没有入口连线,则返回 false | |
| 661 | 662 | if (CollUtil.isEmpty(sequenceFlows)) { |
| 662 | - return true; | |
| 663 | + return false; | |
| 663 | 664 | } |
| 664 | - // 循环找到目标元素 | |
| 665 | + // 2. 循环找目标元素, 找到目标节点 | |
| 665 | 666 | for (SequenceFlow sequenceFlow : sequenceFlows) { |
| 666 | 667 | // 如果发现连线重复,说明循环了,跳过这个循环 |
| 667 | 668 | if (visitedElements.contains(sequenceFlow.getId())) { |
| ... | ... | @@ -669,21 +670,22 @@ public class BpmnModelUtils { |
| 669 | 670 | } |
| 670 | 671 | // 添加已经走过的连线 |
| 671 | 672 | visitedElements.add(sequenceFlow.getId()); |
| 672 | - // 这条线路存在目标节点,这条线路完成,进入下个线路 | |
| 673 | + // 这条线路存在目标节点,直接返回 true | |
| 673 | 674 | FlowElement sourceFlowElement = sequenceFlow.getSourceFlowElement(); |
| 674 | 675 | if (target.getId().equals(sourceFlowElement.getId())) { |
| 675 | - continue; | |
| 676 | + return true; | |
| 676 | 677 | } |
| 677 | - // 如果目标节点为并行网关,则不继续 | |
| 678 | + // 如果目标节点为并行网关,跳过这个循环 (TODO 疑问:这个判断作用是防止回退到并行网关分支上的节点吗?) | |
| 678 | 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 | 785 | return resultElements; |
| 784 | 786 | } |
| 785 | 787 | |
| 786 | - @SuppressWarnings("PatternVariableCanBeUsed") | |
| 787 | 788 | private static void simulateNextFlowElements(FlowElement currentElement, Map<String, Object> variables, |
| 788 | 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 | 737 | BoundaryEvent boundaryEvent = null; |
| 738 | 738 | if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())) { |
| 739 | 739 | boundaryEvent = buildTimeoutBoundaryEvent(receiveTask, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(), |
| 740 | - node.getDelaySetting().getDelayTime(), null, null); | |
| 740 | + null, null, node.getDelaySetting().getDelayTime()); | |
| 741 | 741 | } else if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())) { |
| 742 | 742 | boundaryEvent = buildTimeoutBoundaryEvent(receiveTask, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(), |
| 743 | - null, null, node.getDelaySetting().getDelayTime()); | |
| 743 | + node.getDelaySetting().getDelayTime(), null, null); | |
| 744 | 744 | } else { |
| 745 | 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 | 2 | |
| 3 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | 4 | import cn.hutool.core.collection.ListUtil; |
| 5 | +import cn.hutool.core.date.DatePattern; | |
| 5 | 6 | import cn.hutool.core.date.DateUtil; |
| 6 | 7 | import cn.hutool.core.lang.Assert; |
| 8 | +import cn.hutool.core.map.MapUtil; | |
| 7 | 9 | import cn.hutool.core.util.*; |
| 8 | 10 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 9 | 11 | import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; |
| ... | ... | @@ -12,6 +14,7 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| 12 | 14 | import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; |
| 13 | 15 | import cn.iocoder.yudao.framework.common.util.object.PageUtils; |
| 14 | 16 | import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; |
| 17 | +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; | |
| 15 | 18 | import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; |
| 16 | 19 | import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; |
| 17 | 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 | 75 | import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; |
| 73 | 76 | import static cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum.REJECT_CHILD_PROCESS; |
| 74 | 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 | 79 | import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseNodeType; |
| 76 | 80 | import static java.util.Arrays.asList; |
| 77 | 81 | import static java.util.Collections.singletonList; |
| ... | ... | @@ -187,6 +191,10 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService |
| 187 | 191 | if (CollUtil.isNotEmpty(reqVO.getProcessVariables())) { |
| 188 | 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 | 198 | // 1.3 读取其它相关数据 |
| 191 | 199 | ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition( |
| 192 | 200 | historicProcessInstance != null ? historicProcessInstance.getProcessDefinitionId() |
| ... | ... | @@ -218,10 +226,24 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService |
| 218 | 226 | |
| 219 | 227 | // 3.1 计算当前登录用户的待办任务 |
| 220 | 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 | 244 | List<ActivityNode> simulateActivityNodes = getSimulateApproveNodeList(startUserId, bpmnModel, |
| 223 | 245 | processDefinitionInfo, |
| 224 | - processVariables, activities); | |
| 246 | + processVariables, activities, needSimulateTaskDefKeysByReturn); | |
| 225 | 247 | |
| 226 | 248 | // 4. 拼接最终数据 |
| 227 | 249 | return buildApprovalDetail(reqVO, bpmnModel, processDefinition, processDefinitionInfo, historicProcessInstance, |
| ... | ... | @@ -461,7 +483,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService |
| 461 | 483 | } |
| 462 | 484 | |
| 463 | 485 | /** |
| 464 | - * 获取结束节点的状态 | |
| 486 | + * 获取结束节点的状态 | |
| 465 | 487 | */ |
| 466 | 488 | private Integer getEndActivityNodeStatus(HistoricTaskInstance task) { |
| 467 | 489 | Integer status = FlowableUtils.getTaskStatus(task); |
| ... | ... | @@ -546,7 +568,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService |
| 546 | 568 | private List<ActivityNode> getSimulateApproveNodeList(Long startUserId, BpmnModel bpmnModel, |
| 547 | 569 | BpmProcessDefinitionInfoDO processDefinitionInfo, |
| 548 | 570 | Map<String, Object> processVariables, |
| 549 | - List<HistoricActivityInstance> activities) { | |
| 571 | + List<HistoricActivityInstance> activities, | |
| 572 | + Set<String> needSimulateTaskDefKeysByReturn) { | |
| 550 | 573 | // TODO @芋艿:【可优化】在驳回场景下,未来的预测准确性不高。原因是,驳回后,HistoricActivityInstance |
| 551 | 574 | // 包括了历史的操作,不是只有 startEvent 到当前节点的记录 |
| 552 | 575 | Set<String> runActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId); |
| ... | ... | @@ -555,7 +578,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService |
| 555 | 578 | List<FlowElement> flowElements = BpmnModelUtils.simulateProcess(bpmnModel, processVariables); |
| 556 | 579 | return convertList(flowElements, flowElement -> buildNotRunApproveNodeForBpmn( |
| 557 | 580 | startUserId, bpmnModel, flowElements, |
| 558 | - processDefinitionInfo, processVariables, flowElement, runActivityIds)); | |
| 581 | + processDefinitionInfo, processVariables, flowElement, runActivityIds, needSimulateTaskDefKeysByReturn)); | |
| 559 | 582 | } |
| 560 | 583 | // 情况二:SIMPLE 设计器 |
| 561 | 584 | if (Objects.equals(BpmModelTypeEnum.SIMPLE.getType(), processDefinitionInfo.getModelType())) { |
| ... | ... | @@ -564,17 +587,19 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService |
| 564 | 587 | List<BpmSimpleModelNodeVO> simpleNodes = SimpleModelUtils.simulateProcess(simpleModel, processVariables); |
| 565 | 588 | return convertList(simpleNodes, simpleNode -> buildNotRunApproveNodeForSimple( |
| 566 | 589 | startUserId, bpmnModel, |
| 567 | - processDefinitionInfo, processVariables, simpleNode, runActivityIds)); | |
| 590 | + processDefinitionInfo, processVariables, simpleNode, runActivityIds, needSimulateTaskDefKeysByReturn)); | |
| 568 | 591 | } |
| 569 | 592 | throw new IllegalArgumentException("未知设计器类型:" + processDefinitionInfo.getModelType()); |
| 570 | 593 | } |
| 571 | 594 | |
| 572 | 595 | private ActivityNode buildNotRunApproveNodeForSimple(Long startUserId, BpmnModel bpmnModel, |
| 573 | 596 | BpmProcessDefinitionInfoDO processDefinitionInfo, Map<String, Object> processVariables, |
| 574 | - BpmSimpleModelNodeVO node, Set<String> runActivityIds) { | |
| 597 | + BpmSimpleModelNodeVO node, Set<String> runActivityIds, | |
| 598 | + Set<String> needSimulateTaskDefKeysByReturn) { | |
| 575 | 599 | // TODO @芋艿:【可优化】在驳回场景下,未来的预测准确性不高。原因是,驳回后,HistoricActivityInstance |
| 576 | 600 | // 包括了历史的操作,不是只有 startEvent 到当前节点的记录 |
| 577 | - if (runActivityIds.contains(node.getId())) { | |
| 601 | + if (runActivityIds.contains(node.getId()) | |
| 602 | + && !needSimulateTaskDefKeysByReturn.contains(node.getId())) { // 特殊:回退操作时候,会记录需要预测的节点到流程变量中。即使在历史操作中,也需要预测 | |
| 578 | 603 | return null; |
| 579 | 604 | } |
| 580 | 605 | Integer status = BpmTaskStatusEnum.NOT_START.getStatus(); |
| ... | ... | @@ -621,13 +646,16 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService |
| 621 | 646 | private ActivityNode buildNotRunApproveNodeForBpmn(Long startUserId, BpmnModel bpmnModel, List<FlowElement> flowElements, |
| 622 | 647 | BpmProcessDefinitionInfoDO processDefinitionInfo, |
| 623 | 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 | 653 | return null; |
| 627 | 654 | } |
| 655 | + | |
| 628 | 656 | Integer status = BpmTaskStatusEnum.NOT_START.getStatus(); |
| 629 | 657 | // 如果节点被跳过,状态设置为跳过 |
| 630 | - if(BpmnModelUtils.isSkipNode(node, processVariables)){ | |
| 658 | + if (BpmnModelUtils.isSkipNode(node, processVariables)) { | |
| 631 | 659 | status = BpmTaskStatusEnum.SKIP.getStatus(); |
| 632 | 660 | } |
| 633 | 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 | 176 | */ |
| 177 | 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 | 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 | 2 | |
| 3 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | 4 | import cn.hutool.core.convert.Convert; |
| 5 | +import cn.hutool.core.date.DateUtil; | |
| 5 | 6 | import cn.hutool.core.lang.Assert; |
| 6 | 7 | import cn.hutool.core.util.*; |
| 7 | 8 | import cn.hutool.extra.spring.SpringUtil; |
| 8 | 9 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 10 | +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; | |
| 9 | 11 | import cn.iocoder.yudao.framework.common.util.date.DateUtils; |
| 10 | 12 | import cn.iocoder.yudao.framework.common.util.number.NumberUtils; |
| 11 | 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 | 70 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; |
| 69 | 71 | import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; |
| 70 | 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 | 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 | 231 | if (StrUtil.isNotBlank(pageVO.getName())) { |
| 231 | 232 | taskQuery.taskNameLike("%" + pageVO.getName() + "%"); |
| 232 | 233 | } |
| 234 | + if (pageVO.getStatus() != null) { | |
| 235 | + taskQuery.taskVariableValueEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS, pageVO.getStatus()); | |
| 236 | + } | |
| 233 | 237 | // if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { |
| 234 | 238 | // taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); |
| 235 | 239 | // taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); |
| ... | ... | @@ -491,6 +495,17 @@ public class BpmTaskServiceImpl implements BpmTaskService { |
| 491 | 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 | 605 | bpmnModel, reqVO.getNextAssignees(), instance); |
| 591 | 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 | 615 | taskService.complete(task.getId(), variables, true); |
| 595 | 616 | |
| 596 | 617 | // 【加签专属】处理加签任务 |
| ... | ... | @@ -840,34 +861,34 @@ public class BpmTaskServiceImpl implements BpmTaskService { |
| 840 | 861 | if (task.isSuspended()) { |
| 841 | 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 | 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 | 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 | 884 | FlowElement source = BpmnModelUtils.getFlowElementById(bpmnModel, sourceKey); |
| 864 | - // 1.3 获取跳转的节点元素 | |
| 885 | + // 1.2 获取跳转的节点元素 | |
| 865 | 886 | FlowElement target = BpmnModelUtils.getFlowElementById(bpmnModel, targetKey); |
| 866 | 887 | if (target == null) { |
| 867 | 888 | throw exception(TASK_TARGET_NODE_NOT_EXISTS); |
| 868 | 889 | } |
| 869 | 890 | |
| 870 | - // 2.2 只有串行可到达的节点,才可以退回。类似非串行、子流程无法退回 | |
| 891 | + // 2. 只有串行可到达的节点,才可以退回。类似非串行、子流程无法退回 | |
| 871 | 892 | if (!BpmnModelUtils.isSequentialReachable(source, target, null)) { |
| 872 | 893 | throw exception(TASK_RETURN_FAIL_SOURCE_TARGET_ERROR); |
| 873 | 894 | } |
| ... | ... | @@ -878,11 +899,12 @@ public class BpmTaskServiceImpl implements BpmTaskService { |
| 878 | 899 | * 执行退回逻辑 |
| 879 | 900 | * |
| 880 | 901 | * @param userId 用户编号 |
| 902 | + * @param bpmnModel 流程模型 | |
| 881 | 903 | * @param currentTask 当前退回的任务 |
| 882 | 904 | * @param targetElement 需要退回到的目标任务 |
| 883 | 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 | 908 | // 1. 获得所有需要回撤的任务 taskDefinitionKey,用于稍后的 moveActivityIdsToSingleActivityId 回撤 |
| 887 | 909 | // 1.1 获取所有正常进行的任务节点 Key |
| 888 | 910 | List<Task> taskList = taskService.createTaskQuery().processInstanceId(currentTask.getProcessInstanceId()).list(); |
| ... | ... | @@ -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 | 946 | // 4. 执行驳回 |
| 922 | 947 | // 使用 moveExecutionsToSingleActivityId 替换 moveActivityIdsToSingleActivityId 原因: |
| 923 | 948 | // 当多实例任务回退的时候有问题。相关 issue: https://github.com/flowable/flowable-engine/issues/3944 |
| 924 | 949 | runtimeService.createChangeActivityStateBuilder() |
| 925 | 950 | .processInstanceId(currentTask.getProcessInstanceId()) |
| 926 | 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 | 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 | 988 | @Override |
| 931 | 989 | @Transactional(rollbackFor = Exception.class) |
| 932 | 990 | public void delegateTask(Long userId, BpmTaskDelegateReqVO reqVO) { |
| ... | ... | @@ -1438,12 +1496,11 @@ public class BpmTaskServiceImpl implements BpmTaskService { |
| 1438 | 1496 | return; |
| 1439 | 1497 | } |
| 1440 | 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 | 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 | 1504 | if (userTaskElement.getId().equals(START_USER_NODE_ID) |
| 1448 | 1505 | && (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核 |
| 1449 | 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 | 7 | import lombok.Data; |
| 8 | 8 | import lombok.EqualsAndHashCode; |
| 9 | 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 | 16 | @Schema(description = "管理后台 - 线索分页 Request VO") |
| 12 | 17 | @Data |
| ... | ... | @@ -42,4 +47,8 @@ public class CrmCluePageReqVO extends PageParam { |
| 42 | 47 | @Schema(description = "跟进状态", example = "true") |
| 43 | 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<CrmClueDO> { |
| 33 | 33 | .eqIfPresent(CrmClueDO::getLevel, pageReqVO.getLevel()) |
| 34 | 34 | .eqIfPresent(CrmClueDO::getSource, pageReqVO.getSource()) |
| 35 | 35 | .eqIfPresent(CrmClueDO::getFollowUpStatus, pageReqVO.getFollowUpStatus()) |
| 36 | + .betweenIfPresent(CrmClueDO::getCreateTime, pageReqVO.getCreateTime()) | |
| 36 | 37 | .orderByDesc(CrmClueDO::getId); |
| 37 | 38 | return selectJoinPage(pageReqVO, CrmClueDO.class, query); |
| 38 | 39 | } | ... | ... |
yudao-module-infra/pom.xml
| ... | ... | @@ -101,7 +101,7 @@ |
| 101 | 101 | <artifactId>commons-net</artifactId> <!-- 文件客户端:解决 ftp 连接 --> |
| 102 | 102 | </dependency> |
| 103 | 103 | <dependency> |
| 104 | - <groupId>com.jcraft</groupId> | |
| 104 | + <groupId>com.github.mwiede</groupId> | |
| 105 | 105 | <artifactId>jsch</artifactId> <!-- 文件客户端:解决 sftp 连接 --> |
| 106 | 106 | </dependency> |
| 107 | 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 | 26 | import org.springframework.web.bind.annotation.*; |
| 27 | 27 | import org.springframework.web.multipart.MultipartFile; |
| 28 | 28 | |
| 29 | +import java.nio.charset.StandardCharsets; | |
| 29 | 30 | import java.util.List; |
| 30 | 31 | |
| 31 | 32 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| ... | ... | @@ -99,8 +100,10 @@ public class FileController { |
| 99 | 100 | if (StrUtil.isEmpty(path)) { |
| 100 | 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 | 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 | 21 | VUE3_VBEN5_ANTD_SCHEMA(40), // Vue3 VBEN5 + ANTD + schema 模版 |
| 22 | 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 | 4 | import cn.hutool.core.util.CharsetUtil; |
| 5 | 5 | import cn.hutool.core.util.StrUtil; |
| 6 | 6 | import cn.hutool.extra.ftp.Ftp; |
| 7 | +import cn.hutool.extra.ftp.FtpConfig; | |
| 7 | 8 | import cn.hutool.extra.ftp.FtpException; |
| 8 | 9 | import cn.hutool.extra.ftp.FtpMode; |
| 9 | 10 | import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient; |
| ... | ... | @@ -18,6 +19,15 @@ import java.io.ByteArrayOutputStream; |
| 18 | 19 | */ |
| 19 | 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 | 31 | private Ftp ftp; |
| 22 | 32 | |
| 23 | 33 | public FtpFileClient(Long id, FtpFileClientConfig config) { |
| ... | ... | @@ -26,9 +36,12 @@ public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> { |
| 26 | 36 | |
| 27 | 37 | @Override |
| 28 | 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 | 47 | @Override |
| ... | ... | @@ -72,4 +85,4 @@ public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> { |
| 72 | 85 | ftp.reconnectIfTimeout(); |
| 73 | 86 | } |
| 74 | 87 | |
| 75 | -} | |
| 76 | 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 | 1 | package cn.iocoder.yudao.module.infra.framework.file.core.client.ftp; |
| 2 | 2 | |
| 3 | 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 | 4 | import jakarta.validation.constraints.NotEmpty; |
| 8 | 5 | import jakarta.validation.constraints.NotNull; |
| 6 | +import lombok.Data; | |
| 7 | +import org.hibernate.validator.constraints.URL; | |
| 9 | 8 | |
| 10 | 9 | /** |
| 11 | 10 | * Ftp 文件客户端的配置类 | ... | ... |
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/sftp/SftpFileClient.java
| 1 | 1 | package cn.iocoder.yudao.module.infra.framework.file.core.client.sftp; |
| 2 | 2 | |
| 3 | 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 | 8 | import cn.hutool.extra.ssh.Sftp; |
| 5 | 9 | import cn.iocoder.yudao.framework.common.util.io.FileUtils; |
| 6 | 10 | import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient; |
| 11 | +import com.jcraft.jsch.JSch; | |
| 7 | 12 | |
| 8 | 13 | import java.io.File; |
| 9 | 14 | |
| ... | ... | @@ -14,6 +19,20 @@ import java.io.File; |
| 14 | 19 | */ |
| 15 | 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 | 36 | private Sftp sftp; |
| 18 | 37 | |
| 19 | 38 | public SftpFileClient(Long id, SftpFileClientConfig config) { |
| ... | ... | @@ -22,18 +41,27 @@ public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> { |
| 22 | 41 | |
| 23 | 42 | @Override |
| 24 | 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 | 52 | @Override |
| 30 | 53 | public String upload(byte[] content, String path, String type) { |
| 31 | 54 | // 执行写入 |
| 32 | 55 | String filePath = getFilePath(path); |
| 56 | + String fileName = FileUtil.getName(filePath); | |
| 57 | + String dir = StrUtil.removeSuffix(filePath, fileName); | |
| 33 | 58 | File file = FileUtils.createTempFile(content); |
| 34 | 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 | 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 | 1 | package cn.iocoder.yudao.module.infra.framework.file.core.client.sftp; |
| 2 | 2 | |
| 3 | 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 | 4 | import jakarta.validation.constraints.NotEmpty; |
| 8 | 5 | import jakarta.validation.constraints.NotNull; |
| 6 | +import lombok.Data; | |
| 7 | +import org.hibernate.validator.constraints.URL; | |
| 9 | 8 | |
| 10 | 9 | /** |
| 11 | 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 | 343 | filePath = formatFilePath(filePath, bindingMap); |
| 344 | 344 | String content = templateEngine.getTemplate(vmPath).render(bindingMap); |
| 345 | 345 | // 格式化代码 |
| 346 | - content = prettyCode(content); | |
| 346 | + content = prettyCode(content, vmPath); | |
| 347 | 347 | result.put(filePath, content); |
| 348 | 348 | } |
| 349 | 349 | |
| ... | ... | @@ -383,11 +383,14 @@ public class CodegenEngine { |
| 383 | 383 | * 如果不处理,Vue 的 Pretty 格式校验可能会报错 |
| 384 | 384 | * |
| 385 | 385 | * @param content 格式化前的代码 |
| 386 | + * @param vmPath 模板路径 | |
| 386 | 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 | 394 | // Vue 界面:去除多的 dateFormatter,只有一个的情况下,说明没使用到 |
| 392 | 395 | if (StrUtil.count(content, "dateFormatter") == 1) { |
| 393 | 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 () => { |
| 217 | 217 | |
| 218 | 218 | const checkedIds = ref<number[]>([]) |
| 219 | 219 | const handleRowCheckboxChange = (records: ${subSimpleClassName}[]) => { |
| 220 | - checkedIds.value = records.map((item) => item.id); | |
| 220 | + checkedIds.value = records.map((item) => item.id!); | |
| 221 | 221 | } |
| 222 | 222 | #end |
| 223 | 223 | #end | ... | ... |
yudao-module-infra/src/main/resources/codegen/vue3/views/index.vue.vm
| ... | ... | @@ -374,7 +374,7 @@ const handleDeleteBatch = async () => { |
| 374 | 374 | |
| 375 | 375 | const checkedIds = ref<number[]>([]) |
| 376 | 376 | const handleRowCheckboxChange = (records: ${simpleClassName}[]) => { |
| 377 | - checkedIds.value = records.map((item) => item.id); | |
| 377 | + checkedIds.value = records.map((item) => item.id!); | |
| 378 | 378 | } |
| 379 | 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 | 98 | |
| 99 | 99 | /** 导出${table.classComment} */ |
| 100 | 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 | 1 | <script lang="ts" setup> |
| 2 | -import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}'; | |
| 3 | 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 | 7 | import { useVbenModal } from '@vben/common-ui'; |
| 8 | +import { DICT_TYPE } from '@vben/constants'; | |
| 9 | +import { getDictOptions } from '@vben/hooks'; | |
| 6 | 10 | import { Tinymce as RichTextarea } from '#/components/tinymce'; |
| 7 | 11 | import { ImageUpload, FileUpload } from "#/components/upload"; |
| 8 | 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 | 13 | #if($table.templateType == 2)## 树表需要导入这些 |
| 11 | 14 | import { get${simpleClassName}List } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}'; |
| 12 | 15 | import { handleTree } from '@vben/utils' |
| ... | ... | @@ -20,7 +23,6 @@ import { handleTree } from '@vben/utils' |
| 20 | 23 | #end |
| 21 | 24 | #end |
| 22 | 25 | |
| 23 | -import { computed, ref } from 'vue'; | |
| 24 | 26 | import { $t } from '#/locales'; |
| 25 | 27 | import { get${simpleClassName}, create${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}'; |
| 26 | 28 | |
| ... | ... | @@ -70,7 +72,7 @@ const getTitle = computed(() => { |
| 70 | 72 | #end |
| 71 | 73 | |
| 72 | 74 | /** 重置表单 */ |
| 73 | -const resetForm = () => { | |
| 75 | +function resetForm() { | |
| 74 | 76 | formData.value = { |
| 75 | 77 | #foreach ($column in $columns) |
| 76 | 78 | #if ($column.createOperation || $column.updateOperation) |
| ... | ... | @@ -88,7 +90,7 @@ const resetForm = () => { |
| 88 | 90 | ## 特殊:树表专属逻辑 |
| 89 | 91 | #if ( $table.templateType == 2 ) |
| 90 | 92 | /** 获得${table.classComment}树 */ |
| 91 | -const get${simpleClassName}Tree = async () => { | |
| 93 | +async function get${simpleClassName}Tree() { | |
| 92 | 94 | ${classNameVar}Tree.value = [] |
| 93 | 95 | const data = await get${simpleClassName}List({}); |
| 94 | 96 | data.unshift({ |
| ... | ... | @@ -147,7 +149,6 @@ const [Modal, modalApi] = useVbenModal({ |
| 147 | 149 | emit('success'); |
| 148 | 150 | message.success({ |
| 149 | 151 | content: $t('ui.actionMessage.operationSuccess'), |
| 150 | - key: 'action_process_msg', | |
| 151 | 152 | }); |
| 152 | 153 | } finally { |
| 153 | 154 | modalApi.unlock(); | ... | ... |
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/general/views/index.vue.vm
| 1 | 1 | <script lang="ts" setup> |
| 2 | 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 | 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 | 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 | 12 | import ${simpleClassName}Form from './modules/form.vue'; |
| 11 | 13 | import { Download, Plus, RefreshCw, Search, Trash2 } from '@vben/icons'; |
| 12 | 14 | import { ContentWrap } from '#/components/content-wrap'; |
| 13 | 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 | 18 | #if ( $table.templateType == 11 || $table.templateType == 12 ) |
| ... | ... | @@ -23,7 +23,6 @@ import { useTableToolbar } from '#/hooks'; |
| 23 | 23 | #end |
| 24 | 24 | #end |
| 25 | 25 | |
| 26 | -import { ref, h, reactive,onMounted,nextTick } from 'vue'; | |
| 27 | 26 | import { $t } from '#/locales'; |
| 28 | 27 | #if (${table.templateType} == 2)## 树表接口 |
| 29 | 28 | import { handleTree,isEmpty } from '@vben/utils' |
| ... | ... | @@ -77,7 +76,7 @@ const queryFormRef = ref() // 搜索的表单 |
| 77 | 76 | const exportLoading = ref(false) // 导出的加载中 |
| 78 | 77 | |
| 79 | 78 | /** 查询列表 */ |
| 80 | -const getList = async () => { | |
| 79 | +async function getList() { | |
| 81 | 80 | loading.value = true |
| 82 | 81 | try { |
| 83 | 82 | const params = cloneDeep(queryParams) as any; |
| ... | ... | @@ -104,7 +103,7 @@ const getList = async () => { |
| 104 | 103 | } |
| 105 | 104 | |
| 106 | 105 | /** 搜索按钮操作 */ |
| 107 | -const handleQuery = () => { | |
| 106 | +function handleQuery() { | |
| 108 | 107 | #if ( $table.templateType != 2 ) |
| 109 | 108 | queryParams.pageNo = 1 |
| 110 | 109 | #end |
| ... | ... | @@ -112,7 +111,7 @@ const handleQuery = () => { |
| 112 | 111 | } |
| 113 | 112 | |
| 114 | 113 | /** 重置按钮操作 */ |
| 115 | -const resetQuery = () => { | |
| 114 | +function resetQuery() { | |
| 116 | 115 | queryFormRef.value.resetFields() |
| 117 | 116 | handleQuery() |
| 118 | 117 | } |
| ... | ... | @@ -124,7 +123,7 @@ const [FormModal, formModalApi] = useVbenModal({ |
| 124 | 123 | |
| 125 | 124 | /** 创建${table.classComment} */ |
| 126 | 125 | function handleCreate() { |
| 127 | - formModalApi.setData({}).open(); | |
| 126 | + formModalApi.setData(null).open(); | |
| 128 | 127 | } |
| 129 | 128 | |
| 130 | 129 | /** 编辑${table.classComment} */ |
| ... | ... | @@ -144,14 +143,10 @@ async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) { |
| 144 | 143 | const hideLoading = message.loading({ |
| 145 | 144 | content: $t('ui.actionMessage.deleting', [row.id]), |
| 146 | 145 | duration: 0, |
| 147 | - key: 'action_process_msg', | |
| 148 | 146 | }); |
| 149 | 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 | 150 | await getList(); |
| 156 | 151 | } finally { |
| 157 | 152 | hideLoading(); |
| ... | ... | @@ -164,12 +159,11 @@ async function handleDeleteBatch() { |
| 164 | 159 | const hideLoading = message.loading({ |
| 165 | 160 | content: $t('ui.actionMessage.deleting'), |
| 166 | 161 | duration: 0, |
| 167 | - key: 'action_process_msg', | |
| 168 | 162 | }); |
| 169 | 163 | try { |
| 170 | 164 | await delete${simpleClassName}List(checkedIds.value); |
| 171 | 165 | checkedIds.value = []; |
| 172 | - message.success( $t('ui.actionMessage.deleteSuccess') ); | |
| 166 | + message.success($t('ui.actionMessage.deleteSuccess')); | |
| 173 | 167 | await getList(); |
| 174 | 168 | } finally { |
| 175 | 169 | hideLoading(); |
| ... | ... | @@ -182,12 +176,12 @@ function handleRowCheckboxChange({ |
| 182 | 176 | }: { |
| 183 | 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 | 181 | #end |
| 188 | 182 | |
| 189 | 183 | /** 导出表格 */ |
| 190 | -async function onExport() { | |
| 184 | +async function handleExport() { | |
| 191 | 185 | try { |
| 192 | 186 | exportLoading.value = true; |
| 193 | 187 | const data = await export${simpleClassName}(queryParams); |
| ... | ... | @@ -200,7 +194,7 @@ try { |
| 200 | 194 | #if (${table.templateType} == 2) |
| 201 | 195 | /** 切换树形展开/收缩状态 */ |
| 202 | 196 | const isExpanded = ref(true); |
| 203 | -function toggleExpand() { | |
| 197 | +function handleExpand() { | |
| 204 | 198 | isExpanded.value = !isExpanded.value; |
| 205 | 199 | tableRef.value?.setAllTreeExpand(isExpanded.value); |
| 206 | 200 | } |
| ... | ... | @@ -303,12 +297,12 @@ onMounted(() => { |
| 303 | 297 | <!-- 列表 --> |
| 304 | 298 | <ContentWrap title="${table.classComment}"> |
| 305 | 299 | <template #extra> |
| 306 | - <TableToolbar | |
| 300 | + <VbenVxeTableToolbar | |
| 307 | 301 | ref="tableToolbarRef" |
| 308 | 302 | v-model:hidden-search="hiddenSearchBar" |
| 309 | 303 | > |
| 310 | 304 | #if (${table.templateType} == 2) |
| 311 | - <Button @click="toggleExpand" class="mr-2"> | |
| 305 | + <Button @click="handleExpand" class="mr-2"> | |
| 312 | 306 | {{ isExpanded ? '收缩' : '展开' }} |
| 313 | 307 | </Button> |
| 314 | 308 | #end |
| ... | ... | @@ -326,7 +320,7 @@ onMounted(() => { |
| 326 | 320 | type="primary" |
| 327 | 321 | class="ml-2" |
| 328 | 322 | :loading="exportLoading" |
| 329 | - @click="onExport" | |
| 323 | + @click="handleExport" | |
| 330 | 324 | v-access:code="['${permissionPrefix}:export']" |
| 331 | 325 | > |
| 332 | 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 | 3 | #set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 |
| 4 | 4 | #set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) |
| 5 | 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 | 38 | #end |
| 37 | 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 | 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 | 82 | modalApi.lock(); |
| 53 | - // 提交表单 | |
| 54 | - const data = formData.value as ${simpleClassName}Api.${subSimpleClassName}; | |
| 55 | 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 | 85 | } finally { |
| 65 | 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 | 103 | #end |
| 103 | 104 | #end |
| 104 | - }; | |
| 105 | - formRef.value?.resetFields(); | |
| 106 | - } | |
| 105 | + #end | |
| 106 | + }; | |
| 107 | + formRef.value?.resetFields(); | |
| 108 | +} | |
| 107 | 109 | </script> |
| 108 | 110 | |
| 109 | 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 | 5 | #set ($subClassNameVar = $subClassNameVars.get($subIndex)) |
| 6 | 6 | #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 |
| 7 | 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 | 18 | #if ($subTable.subJoinMany) ## 一对多 |
| 16 | 19 | import type { VxeTableInstance } from '#/adapter/vxe-table'; |
| ... | ... | @@ -30,13 +33,14 @@ const props = defineProps<{ |
| 30 | 33 | #if ($subTable.subJoinMany) ## 一对多 |
| 31 | 34 | const list = ref<${simpleClassName}Api.${subSimpleClassName}[]>([]) // 列表的数据 |
| 32 | 35 | const tableRef = ref<VxeTableInstance>(); |
| 36 | + | |
| 33 | 37 | /** 添加${subTable.classComment} */ |
| 34 | -const onAdd = async () => { | |
| 38 | +async function handleAdd() { | |
| 35 | 39 | await tableRef.value?.insertAt({} as ${simpleClassName}Api.${subSimpleClassName}, -1); |
| 36 | 40 | } |
| 37 | 41 | |
| 38 | 42 | /** 删除${subTable.classComment} */ |
| 39 | -const onDelete = async (row: ${simpleClassName}Api.${subSimpleClassName}) => { | |
| 43 | +async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) { | |
| 40 | 44 | await tableRef.value?.remove(row); |
| 41 | 45 | } |
| 42 | 46 | |
| ... | ... | @@ -46,9 +50,11 @@ defineExpose({ |
| 46 | 50 | const data = list.value as ${simpleClassName}Api.${subSimpleClassName}[]; |
| 47 | 51 | const removeRecords = tableRef.value?.getRemoveRecords() as ${simpleClassName}Api.${subSimpleClassName}[]; |
| 48 | 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 | 129 | #if ( $column.id == $subJoinColumn.id) ## 特殊:忽略主子表的 join 字段,不用填写 |
| 124 | 130 | #elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里 |
| 125 | 131 | <vxe-column field="${javaField}" title="${comment}" align="center"> |
| 126 | - <template #default="{row}"> | |
| 132 | + <template #default="{ row }"> | |
| 127 | 133 | <Input v-model:value="row.${javaField}" /> |
| 128 | 134 | </template> |
| 129 | 135 | </vxe-column> |
| 130 | 136 | #elseif($column.htmlType == "imageUpload")## 图片上传 |
| 131 | 137 | <vxe-column field="${javaField}" title="${comment}" align="center"> |
| 132 | - <template #default="{row}"> | |
| 138 | + <template #default="{ row }"> | |
| 133 | 139 | <ImageUpload v-model:value="row.${javaField}" /> |
| 134 | 140 | </template> |
| 135 | 141 | </vxe-column> |
| 136 | 142 | #elseif($column.htmlType == "fileUpload")## 文件上传 |
| 137 | 143 | <vxe-column field="${javaField}" title="${comment}" align="center"> |
| 138 | - <template #default="{row}"> | |
| 144 | + <template #default="{ row }"> | |
| 139 | 145 | <FileUpload v-model:value="row.${javaField}" /> |
| 140 | 146 | </template> |
| 141 | 147 | </vxe-column> |
| 142 | 148 | #elseif($column.htmlType == "select")## 下拉框 |
| 143 | 149 | <vxe-column field="${javaField}" title="${comment}" align="center"> |
| 144 | - <template #default="{row}"> | |
| 150 | + <template #default="{ row }"> | |
| 145 | 151 | <Select v-model:value="row.${javaField}" placeholder="请选择${comment}"> |
| 146 | 152 | #if ("" != $dictType)## 有数据字典 |
| 147 | 153 | <Select.Option |
| ... | ... | @@ -159,7 +165,7 @@ watch( |
| 159 | 165 | </vxe-column> |
| 160 | 166 | #elseif($column.htmlType == "checkbox")## 多选框 |
| 161 | 167 | <vxe-column field="${javaField}" title="${comment}" align="center"> |
| 162 | - <template #default="{row}"> | |
| 168 | + <template #default="{ row }"> | |
| 163 | 169 | <CheckboxGroup v-model:value="row.${javaField}"> |
| 164 | 170 | #if ("" != $dictType)## 有数据字典 |
| 165 | 171 | <Checkbox |
| ... | ... | @@ -177,7 +183,7 @@ watch( |
| 177 | 183 | </vxe-column> |
| 178 | 184 | #elseif($column.htmlType == "radio")## 单选框 |
| 179 | 185 | <vxe-column field="${javaField}" title="${comment}" align="center"> |
| 180 | - <template #default="{row}"> | |
| 186 | + <template #default="{ row }"> | |
| 181 | 187 | <RadioGroup v-model:value="row.${javaField}"> |
| 182 | 188 | #if ("" != $dictType)## 有数据字典 |
| 183 | 189 | <Radio |
| ... | ... | @@ -195,7 +201,7 @@ watch( |
| 195 | 201 | </vxe-column> |
| 196 | 202 | #elseif($column.htmlType == "datetime")## 时间框 |
| 197 | 203 | <vxe-column field="${javaField}" title="${comment}" align="center"> |
| 198 | - <template #default="{row}"> | |
| 204 | + <template #default="{ row }"> | |
| 199 | 205 | <DatePicker |
| 200 | 206 | v-model:value="row.${javaField}" |
| 201 | 207 | :showTime="true" |
| ... | ... | @@ -206,7 +212,7 @@ watch( |
| 206 | 212 | </vxe-column> |
| 207 | 213 | #elseif($column.htmlType == "textarea" || $column.htmlType == "editor")## 文本框 |
| 208 | 214 | <vxe-column field="${javaField}" title="${comment}" align="center"> |
| 209 | - <template #default="{row}"> | |
| 215 | + <template #default="{ row }"> | |
| 210 | 216 | <Textarea v-model:value="row.${javaField}" /> |
| 211 | 217 | </template> |
| 212 | 218 | </vxe-column> |
| ... | ... | @@ -214,12 +220,12 @@ watch( |
| 214 | 220 | #end |
| 215 | 221 | #end |
| 216 | 222 | <vxe-column field="operation" title="操作" align="center"> |
| 217 | - <template #default="{row}"> | |
| 223 | + <template #default="{ row }"> | |
| 218 | 224 | <Button |
| 219 | 225 | size="small" |
| 220 | 226 | type="link" |
| 221 | 227 | danger |
| 222 | - @click="onDelete(row as any)" | |
| 228 | + @click="handleDelect(row)" | |
| 223 | 229 | v-access:code="['${permissionPrefix}:delete']" |
| 224 | 230 | > |
| 225 | 231 | {{ $t('ui.actionTitle.delete') }} |
| ... | ... | @@ -228,7 +234,7 @@ watch( |
| 228 | 234 | </vxe-column> |
| 229 | 235 | </vxe-table> |
| 230 | 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 | 238 | {{ $t('ui.actionTitle.create', ['${subTable.classComment}']) }} |
| 233 | 239 | </Button> |
| 234 | 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 | 6 | #set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($subIndex)) |
| 7 | 7 | #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 |
| 8 | 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 | 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 | 31 | #end |
| 30 | 32 | |
| 31 | 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 | 42 | #end |
| 41 | 43 | |
| 42 | 44 | const props = defineProps<{ |
| 43 | - ${subJoinColumn.javaField}?: number // ${subJoinColumn.columnComment}(主表的关联字段) | |
| 45 | + ${subJoinColumn.javaField}?: number // ${subJoinColumn.columnComment}(主表的关联字段) | |
| 44 | 46 | }>() |
| 45 | 47 | |
| 46 | 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 | 54 | /** 创建${subTable.classComment} */ |
| 53 | 55 | function handleCreate() { |
| ... | ... | @@ -68,14 +70,10 @@ async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) { |
| 68 | 70 | const hideLoading = message.loading({ |
| 69 | 71 | content: $t('ui.actionMessage.deleting', [row.id]), |
| 70 | 72 | duration: 0, |
| 71 | - key: 'action_process_msg', | |
| 72 | 73 | }); |
| 73 | 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 | 77 | await getList(); |
| 80 | 78 | } finally { |
| 81 | 79 | hideLoading(); |
| ... | ... | @@ -88,12 +86,11 @@ async function handleDeleteBatch() { |
| 88 | 86 | const hideLoading = message.loading({ |
| 89 | 87 | content: $t('ui.actionMessage.deleting'), |
| 90 | 88 | duration: 0, |
| 91 | - key: 'action_process_msg', | |
| 92 | 89 | }); |
| 93 | 90 | try { |
| 94 | 91 | await delete${subSimpleClassName}List(checkedIds.value); |
| 95 | 92 | checkedIds.value = []; |
| 96 | - message.success( $t('ui.actionMessage.deleteSuccess') ); | |
| 93 | + message.success($t('ui.actionMessage.deleteSuccess')); | |
| 97 | 94 | await getList(); |
| 98 | 95 | } finally { |
| 99 | 96 | hideLoading(); |
| ... | ... | @@ -106,92 +103,92 @@ function handleRowCheckboxChange({ |
| 106 | 103 | }: { |
| 107 | 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 | 108 | #end |
| 112 | 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 | 113 | #if ($table.templateType == 11) ## erp |
| 117 | - const total = ref(0) // 列表的总页数 | |
| 114 | +const total = ref(0) // 列表的总页数 | |
| 118 | 115 | #end |
| 119 | 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 | 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 | 135 | queryParams.pageNo = 1 |
| 139 | 136 | getList() |
| 140 | 137 | } |
| 141 | 138 | |
| 142 | 139 | /** 重置按钮操作 */ |
| 143 | -const resetQuery = () => { | |
| 140 | +function resetQuery() { | |
| 144 | 141 | queryFormRef.value.resetFields() |
| 145 | 142 | handleQuery() |
| 146 | 143 | } |
| 147 | 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 | 193 | #if ($table.templateType == 11) ## erp |
| 197 | 194 | /** 初始化 */ |
| ... | ... | @@ -292,7 +289,7 @@ onMounted(() => { |
| 292 | 289 | <!-- 列表 --> |
| 293 | 290 | <ContentWrap title="${table.classComment}"> |
| 294 | 291 | <template #extra> |
| 295 | - <TableToolbar | |
| 292 | + <VbenVxeTableToolbar | |
| 296 | 293 | ref="tableToolbarRef" |
| 297 | 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 | 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 | 2 | import type { VbenFormSchema } from '#/adapter/form'; |
| 2 | 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 | 8 | #if(${table.templateType} == 2)## 树表需要导入这些 |
| 12 | 9 | import { get${simpleClassName}List } from '#/api/${table.moduleName}/${table.businessName}'; |
| 13 | 10 | import { handleTree } from '@vben/utils'; |
| 14 | 11 | #end |
| 15 | 12 | |
| 13 | +import { getRangePickerDefaultProps } from '#/utils'; | |
| 14 | + | |
| 16 | 15 | /** 新增/修改的表单 */ |
| 17 | 16 | export function useFormSchema(): VbenFormSchema[] { |
| 18 | 17 | return [ |
| ... | ... | @@ -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 | 189 | return [ |
| 191 | 190 | #if ($table.templateType != 2 && $deleteBatchEnable) |
| 192 | 191 | { type: 'checkbox', width: 40 }, |
| ... | ... | @@ -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 | 392 | return [ |
| 394 | 393 | #if ($table.templateType != 2 && $deleteBatchEnable) |
| 395 | 394 | { type: 'checkbox', width: 40 }, |
| ... | ... | @@ -426,7 +425,7 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${sim |
| 426 | 425 | #else |
| 427 | 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 | 429 | return [ |
| 431 | 430 | #foreach($column in $subColumns) |
| 432 | 431 | #if ($column.createOperation || $column.updateOperation) |
| ... | ... | @@ -602,4 +601,4 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${sim |
| 602 | 601 | } |
| 603 | 602 | #end |
| 604 | 603 | #end |
| 605 | 604 | -#end |
| 605 | +#end | |
| 606 | 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 | 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 | 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 | 15 | #if ( $table.templateType == 10 || $table.templateType == 12 ) |
| 8 | 16 | #foreach ($subSimpleClassName in $subSimpleClassNames) |
| ... | ... | @@ -12,21 +20,15 @@ import { message, Tabs, Checkbox, Input, Textarea, Select,RadioGroup,CheckboxGro |
| 12 | 20 | #end |
| 13 | 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 | 23 | import { useFormSchema } from '../data'; |
| 21 | 24 | |
| 22 | 25 | const emit = defineEmits(['success']); |
| 23 | -const formData = ref<${simpleClassName}Api.${simpleClassName}>(); | |
| 26 | +const formData = ref<${apiName}.${simpleClassName}>(); | |
| 24 | 27 | const getTitle = computed(() => { |
| 25 | 28 | return formData.value?.id |
| 26 | 29 | ? $t('ui.actionTitle.edit', ['${table.classComment}']) |
| 27 | 30 | : $t('ui.actionTitle.create', ['${table.classComment}']); |
| 28 | 31 | }); |
| 29 | - | |
| 30 | 32 | ## 特殊:主子表专属逻辑 |
| 31 | 33 | #if ( $table.templateType == 10 || $table.templateType == 12 ) |
| 32 | 34 | #if ( $subTables && $subTables.size() > 0 ) |
| ... | ... | @@ -51,7 +53,7 @@ const [Form, formApi] = useVbenForm({ |
| 51 | 53 | }, |
| 52 | 54 | layout: 'horizontal', |
| 53 | 55 | schema: useFormSchema(), |
| 54 | - showDefaultActions: false | |
| 56 | + showDefaultActions: false, | |
| 55 | 57 | }); |
| 56 | 58 | |
| 57 | 59 | const [Modal, modalApi] = useVbenModal({ |
| ... | ... | @@ -60,8 +62,8 @@ const [Modal, modalApi] = useVbenModal({ |
| 60 | 62 | if (!valid) { |
| 61 | 63 | return; |
| 62 | 64 | } |
| 63 | - ## 特殊:主子表专属逻辑 | |
| 64 | - #if ( $table.templateType == 10 || $table.templateType == 12 ) | |
| 65 | +## 特殊:主子表专属逻辑 | |
| 66 | +#if ( $table.templateType == 10 || $table.templateType == 12 ) | |
| 65 | 67 | #if ( $subTables && $subTables.size() > 0 ) |
| 66 | 68 | // 校验子表单 |
| 67 | 69 | #foreach ($subTable in $subTables) |
| ... | ... | @@ -78,12 +80,12 @@ const [Modal, modalApi] = useVbenModal({ |
| 78 | 80 | #end |
| 79 | 81 | #end |
| 80 | 82 | #end |
| 81 | - #end | |
| 83 | +#end | |
| 82 | 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 | 89 | #if ( $subTables && $subTables.size() > 0 ) |
| 88 | 90 | // 拼接子表的数据 |
| 89 | 91 | #foreach ($subTable in $subTables) |
| ... | ... | @@ -96,13 +98,13 @@ const [Modal, modalApi] = useVbenModal({ |
| 96 | 98 | #end |
| 97 | 99 | #end |
| 98 | 100 | #end |
| 99 | - #end | |
| 101 | +#end | |
| 100 | 102 | try { |
| 101 | 103 | await (formData.value?.id ? update${simpleClassName}(data) : create${simpleClassName}(data)); |
| 102 | 104 | // 关闭并提示 |
| 103 | 105 | await modalApi.close(); |
| 104 | 106 | emit('success'); |
| 105 | - message.success( $t('ui.actionMessage.operationSuccess') ); | |
| 107 | + message.success($t('ui.actionMessage.operationSuccess')); | |
| 106 | 108 | } finally { |
| 107 | 109 | modalApi.unlock(); |
| 108 | 110 | } |
| ... | ... | @@ -113,21 +115,24 @@ const [Modal, modalApi] = useVbenModal({ |
| 113 | 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 | 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 | 138 | </script> |
| ... | ... | @@ -135,8 +140,8 @@ const [Modal, modalApi] = useVbenModal({ |
| 135 | 140 | <template> |
| 136 | 141 | <Modal :title="getTitle"> |
| 137 | 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 | 146 | <Tabs v-model:active-key="subTabsName"> |
| 142 | 147 | #foreach ($subTable in $subTables) |
| ... | ... | @@ -149,6 +154,6 @@ const [Modal, modalApi] = useVbenModal({ |
| 149 | 154 | </Tabs.TabPane> |
| 150 | 155 | #end |
| 151 | 156 | </Tabs> |
| 152 | - #end | |
| 157 | +#end | |
| 153 | 158 | </Modal> |
| 154 | 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 | 2 | <script lang="ts" setup> |
| 2 | 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 | 13 | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| 21 | 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 | 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 | 28 | #end |
| 26 | -import { downloadFileFromBlobPart, isEmpty } from '@vben/utils'; | |
| 29 | +import { $t } from '#/locales'; | |
| 27 | 30 | |
| 28 | 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 | 42 | #if ($table.templateType == 12 || $table.templateType == 11) ## 内嵌和erp情况 |
| 31 | 43 | /** 子表的列表 */ |
| 32 | 44 | const subTabsName = ref('$subClassNameVars.get(0)') |
| 33 | 45 | #if ($table.templateType == 11) |
| 34 | -const select${simpleClassName} = ref<${simpleClassName}Api.${simpleClassName}>(); | |
| 46 | +const select${simpleClassName} = ref<${apiName}.${simpleClassName}>(); | |
| 35 | 47 | #end |
| 36 | 48 | #end |
| 37 | - | |
| 38 | 49 | const [FormModal, formModalApi] = useVbenModal({ |
| 39 | 50 | connectedComponent: Form, |
| 40 | 51 | destroyOnClose: true, |
| 41 | 52 | }); |
| 42 | - | |
| 43 | 53 | #if (${table.templateType} == 2)## 树表特有:控制表格展开收缩 |
| 54 | + | |
| 44 | 55 | /** 切换树形展开/收缩状态 */ |
| 45 | 56 | const isExpanded = ref(true); |
| 46 | -function toggleExpand() { | |
| 57 | +function handleExpand() { | |
| 47 | 58 | isExpanded.value = !isExpanded.value; |
| 48 | 59 | gridApi.grid.setAllTreeExpand(isExpanded.value); |
| 49 | 60 | } |
| 50 | 61 | #end |
| 51 | 62 | |
| 52 | 63 | /** 刷新表格 */ |
| 53 | -function onRefresh() { | |
| 64 | +function handleRefresh() { | |
| 54 | 65 | #if ($table.templateType == 12) ## 内嵌情况 |
| 55 | 66 | gridApi.reload(); |
| 56 | 67 | #else |
| ... | ... | @@ -60,34 +71,31 @@ function onRefresh() { |
| 60 | 71 | |
| 61 | 72 | /** 创建${table.classComment} */ |
| 62 | 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 | 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 | 80 | formModalApi.setData({ ${treeParentColumn.javaField}: row.id }).open(); |
| 75 | 81 | } |
| 76 | 82 | #end |
| 77 | 83 | |
| 84 | +/** 编辑${table.classComment} */ | |
| 85 | +function handleEdit(row: ${apiName}.${simpleClassName}) { | |
| 86 | + formModalApi.setData(row).open(); | |
| 87 | +} | |
| 88 | + | |
| 78 | 89 | /** 删除${table.classComment} */ |
| 79 | -async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) { | |
| 90 | +async function handleDelete(row: ${apiName}.${simpleClassName}) { | |
| 80 | 91 | const hideLoading = message.loading({ |
| 81 | 92 | content: $t('ui.actionMessage.deleting', [row.id]), |
| 82 | - key: 'action_key_msg', | |
| 93 | + duration: 0, | |
| 83 | 94 | }); |
| 84 | 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 | 99 | } finally { |
| 92 | 100 | hideLoading(); |
| 93 | 101 | } |
| ... | ... | @@ -96,30 +104,28 @@ async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) { |
| 96 | 104 | #if ($table.templateType != 2 && $deleteBatchEnable) |
| 97 | 105 | /** 批量删除${table.classComment} */ |
| 98 | 106 | async function handleDeleteBatch() { |
| 107 | + await confirm($t('ui.actionMessage.deleteBatchConfirm')); | |
| 99 | 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 | 112 | try { |
| 104 | 113 | await delete${simpleClassName}List(checkedIds.value); |
| 105 | 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 | 117 | } finally { |
| 112 | 118 | hideLoading(); |
| 113 | 119 | } |
| 114 | 120 | } |
| 115 | 121 | |
| 116 | -const checkedIds = ref<number[]>([]) | |
| 122 | +const checkedIds = ref<number[]>([]); | |
| 117 | 123 | function handleRowCheckboxChange({ |
| 118 | 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 | 130 | #end |
| 125 | 131 | |
| ... | ... | @@ -141,20 +147,11 @@ const [Grid, gridApi] = useVbenVxeGrid({ |
| 141 | 147 | height: 'auto', |
| 142 | 148 | #end |
| 143 | 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 | 150 | pagerConfig: { |
| 156 | - enabled: true, | |
| 151 | + enabled: false, | |
| 157 | 152 | }, |
| 153 | +#else## 标准表设置 | |
| 154 | + keepSource: true, | |
| 158 | 155 | #end |
| 159 | 156 | proxyConfig: { |
| 160 | 157 | ajax: { |
| ... | ... | @@ -184,27 +181,35 @@ const [Grid, gridApi] = useVbenVxeGrid({ |
| 184 | 181 | refresh: true, |
| 185 | 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 | 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 | 206 | #end |
| 201 | 207 | }); |
| 202 | 208 | </script> |
| 203 | 209 | |
| 204 | 210 | <template> |
| 205 | 211 | <Page auto-content-height> |
| 206 | - <FormModal @success="onRefresh" /> | |
| 207 | - | |
| 212 | + <FormModal @success="handleRefresh" /> | |
| 208 | 213 | #if ($table.templateType == 11) ## erp情况 |
| 209 | 214 | <div> |
| 210 | 215 | #end |
| ... | ... | @@ -228,13 +233,6 @@ const [Grid, gridApi] = useVbenVxeGrid({ |
| 228 | 233 | <template #toolbar-tools> |
| 229 | 234 | <TableAction |
| 230 | 235 | :actions="[ |
| 231 | - #if (${table.templateType} == 2)## 树表特有:展开/收缩按钮 | |
| 232 | - { | |
| 233 | - label: isExpanded ? '收缩' : '展开', | |
| 234 | - type: 'primary', | |
| 235 | - onClick: toggleExpand, | |
| 236 | - }, | |
| 237 | - #end | |
| 238 | 236 | { |
| 239 | 237 | label: $t('ui.actionTitle.create', ['${table.classComment}']), |
| 240 | 238 | type: 'primary', |
| ... | ... | @@ -242,6 +240,13 @@ const [Grid, gridApi] = useVbenVxeGrid({ |
| 242 | 240 | auth: ['${table.moduleName}:${simpleClassName_strikeCase}:create'], |
| 243 | 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 | 251 | label: $t('ui.actionTitle.export'), |
| 247 | 252 | type: 'primary', |
| ... | ... | @@ -255,8 +260,8 @@ const [Grid, gridApi] = useVbenVxeGrid({ |
| 255 | 260 | type: 'primary', |
| 256 | 261 | danger: true, |
| 257 | 262 | icon: ACTION_ICON.DELETE, |
| 258 | - disabled: isEmpty(checkedIds), | |
| 259 | 263 | auth: ['${table.moduleName}:${simpleClassName_strikeCase}:delete'], |
| 264 | + disabled: isEmpty(checkedIds), | |
| 260 | 265 | onClick: handleDeleteBatch, |
| 261 | 266 | }, |
| 262 | 267 | #end |
| ... | ... | @@ -297,7 +302,6 @@ const [Grid, gridApi] = useVbenVxeGrid({ |
| 297 | 302 | /> |
| 298 | 303 | </template> |
| 299 | 304 | </Grid> |
| 300 | - | |
| 301 | 305 | #if ($table.templateType == 11) ## erp情况 |
| 302 | 306 | <!-- 子表的表单 --> |
| 303 | 307 | <Tabs v-model:active-key="subTabsName" class="mt-2"> |
| ... | ... | @@ -314,4 +318,4 @@ const [Grid, gridApi] = useVbenVxeGrid({ |
| 314 | 318 | </div> |
| 315 | 319 | #end |
| 316 | 320 | </Page> |
| 317 | 321 | -</template> |
| 322 | +</template> | |
| 318 | 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 | 2 | #set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 |
| 3 | 3 | #set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 |
| 4 | 4 | #set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) |
| 5 | +#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api") | |
| 5 | 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 | 9 | import { useVbenModal } from '@vben/common-ui'; |
| 9 | 10 | import { message } from 'ant-design-vue'; |
| ... | ... | @@ -16,7 +17,7 @@ |
| 16 | 17 | import { use${subSimpleClassName}FormSchema } from '../data'; |
| 17 | 18 | |
| 18 | 19 | const emit = defineEmits(['success']); |
| 19 | - const formData = ref<${simpleClassName}Api.${subSimpleClassName}>(); | |
| 20 | + const formData = ref<${apiName}.${subSimpleClassName}>(); | |
| 20 | 21 | const getTitle = computed(() => { |
| 21 | 22 | return formData.value?.id |
| 22 | 23 | ? $t('ui.actionTitle.edit', ['${subTable.classComment}']) |
| ... | ... | @@ -42,10 +43,9 @@ |
| 42 | 43 | if (!valid) { |
| 43 | 44 | return; |
| 44 | 45 | } |
| 45 | - | |
| 46 | 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 | 49 | data.${subJoinColumn.javaField} = formData.value?.${subJoinColumn.javaField}; |
| 50 | 50 | try { |
| 51 | 51 | await (formData.value?.id ? update${subSimpleClassName}(data) : create${subSimpleClassName}(data)); |
| ... | ... | @@ -62,9 +62,8 @@ |
| 62 | 62 | formData.value = undefined; |
| 63 | 63 | return; |
| 64 | 64 | } |
| 65 | - | |
| 66 | 65 | // 加载数据 |
| 67 | - let data = modalApi.getData<${simpleClassName}Api.${subSimpleClassName}>(); | |
| 66 | + let data = modalApi.getData<${apiName}.${subSimpleClassName}>(); | |
| 68 | 67 | if (!data) { |
| 69 | 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 | 4 | #set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) |
| 5 | 5 | #set ($subClassNameVar = $subClassNameVars.get($subIndex)) |
| 6 | 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 | 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 | 11 | import { computed, ref, h, onMounted,watch,nextTick } from 'vue'; |
| 12 | + | |
| 11 | 13 | import { $t } from '#/locales'; |
| 12 | 14 | |
| 13 | 15 | #if ($subTable.subJoinMany) ## 一对多 |
| ... | ... | @@ -33,10 +35,10 @@ const props = defineProps<{ |
| 33 | 35 | function onActionClick({ |
| 34 | 36 | code, |
| 35 | 37 | row, |
| 36 | -}: OnActionClickParams<${simpleClassName}Api.${subSimpleClassName}>) { | |
| 38 | +}: OnActionClickParams<${apiName}.${subSimpleClassName}>) { | |
| 37 | 39 | switch (code) { |
| 38 | 40 | case 'delete': { |
| 39 | - onDelete(row); | |
| 41 | + handleDelete(row); | |
| 40 | 42 | break; |
| 41 | 43 | } |
| 42 | 44 | } |
| ... | ... | @@ -62,21 +64,21 @@ gridOptions: { |
| 62 | 64 | }); |
| 63 | 65 | |
| 64 | 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 | 71 | /** 删除${subTable.classComment} */ |
| 70 | -const onDelete = async (row: ${simpleClassName}Api.${subSimpleClassName}) => { | |
| 72 | +const handleDelete = async (row: ${apiName}.${subSimpleClassName}) => { | |
| 71 | 73 | await gridApi.grid.remove(row); |
| 72 | 74 | } |
| 73 | 75 | |
| 74 | 76 | /** 提供获取表格数据的方法供父组件调用 */ |
| 75 | 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 | 82 | return data |
| 81 | 83 | .filter((row) => !removeRecords.some((removed) => removed.id === row.id)) |
| 82 | 84 | .concat(insertRecords.map((row: any) => ({ ...row, id: undefined }))); |
| ... | ... | @@ -186,7 +188,7 @@ watch( |
| 186 | 188 | #end |
| 187 | 189 | </Grid> |
| 188 | 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 | 192 | {{ $t('ui.actionTitle.create', ['${subTable.classComment}']) }} |
| 191 | 193 | </Button> |
| 192 | 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 | 5 | #set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 |
| 6 | 6 | #set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($subIndex)) |
| 7 | 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 | 9 | <script lang="ts" setup> |
| 9 | 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 | 13 | #if ($table.templateType == 11) ## erp |
| 13 | 14 | import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue' |
| 14 | 15 | #end |
| 15 | -import { useVbenModal } from '@vben/common-ui'; | |
| 16 | +import { confirm, useVbenModal } from '@vben/common-ui'; | |
| 16 | 17 | import { message } from 'ant-design-vue'; |
| 17 | 18 | import { ref, computed, nextTick,watch } from 'vue'; |
| 18 | 19 | import { $t } from '#/locales'; |
| ... | ... | @@ -51,23 +52,20 @@ function handleCreate() { |
| 51 | 52 | } |
| 52 | 53 | |
| 53 | 54 | /** 编辑${subTable.classComment} */ |
| 54 | -function handleEdit(row: ${simpleClassName}Api.${subSimpleClassName}) { | |
| 55 | +function handleEdit(row: ${apiName}.${subSimpleClassName}) { | |
| 55 | 56 | formModalApi.setData(row).open(); |
| 56 | 57 | } |
| 57 | 58 | |
| 58 | 59 | /** 删除${subTable.classComment} */ |
| 59 | -async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) { | |
| 60 | +async function handleDelete(row: ${apiName}.${subSimpleClassName}) { | |
| 60 | 61 | const hideLoading = message.loading({ |
| 61 | 62 | content: $t('ui.actionMessage.deleting', [row.id]), |
| 62 | - key: 'action_process_msg', | |
| 63 | + duration: 0, | |
| 63 | 64 | }); |
| 64 | 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 | 69 | } finally { |
| 72 | 70 | hideLoading(); |
| 73 | 71 | } |
| ... | ... | @@ -76,18 +74,16 @@ async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) { |
| 76 | 74 | #if ($deleteBatchEnable) |
| 77 | 75 | /** 批量删除${subTable.classComment} */ |
| 78 | 76 | async function handleDeleteBatch() { |
| 77 | + await confirm($t('ui.actionMessage.deleteBatchConfirm')); | |
| 79 | 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 | 82 | try { |
| 84 | 83 | await delete${subSimpleClassName}List(checkedIds.value); |
| 85 | 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 | 87 | } finally { |
| 92 | 88 | hideLoading(); |
| 93 | 89 | } |
| ... | ... | @@ -97,9 +93,9 @@ const checkedIds = ref<number[]>([]) |
| 97 | 93 | function handleRowCheckboxChange({ |
| 98 | 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 | 100 | #end |
| 105 | 101 | |
| ... | ... | @@ -149,7 +145,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ |
| 149 | 145 | keyField: 'id', |
| 150 | 146 | isHover: true, |
| 151 | 147 | }, |
| 152 | - } as VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>, | |
| 148 | + } as VxeTableGridOptions<${apiName}.${subSimpleClassName}>, | |
| 153 | 149 | #if (${table.templateType} == 11 && $deleteBatchEnable) |
| 154 | 150 | gridEvents:{ |
| 155 | 151 | checkboxAll: handleRowCheckboxChange, |
| ... | ... | @@ -159,7 +155,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ |
| 159 | 155 | }); |
| 160 | 156 | |
| 161 | 157 | /** 刷新表格 */ |
| 162 | -async function onRefresh() { | |
| 158 | +async function handleRefresh() { | |
| 163 | 159 | #if ($table.templateType == 11) ## erp |
| 164 | 160 | await gridApi.query(); |
| 165 | 161 | #else |
| ... | ... | @@ -179,7 +175,7 @@ watch( |
| 179 | 175 | return; |
| 180 | 176 | } |
| 181 | 177 | await nextTick(); |
| 182 | - await onRefresh() | |
| 178 | + await handleRefresh() | |
| 183 | 179 | }, |
| 184 | 180 | { immediate: true }, |
| 185 | 181 | ); |
| ... | ... | @@ -187,7 +183,7 @@ watch( |
| 187 | 183 | |
| 188 | 184 | <template> |
| 189 | 185 | #if ($table.templateType == 11) ## erp |
| 190 | - <FormModal @success="onRefresh" /> | |
| 186 | + <FormModal @success="handleRefresh" /> | |
| 191 | 187 | <Grid table-title="${subTable.classComment}列表"> |
| 192 | 188 | <template #toolbar-tools> |
| 193 | 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 | 98 | |
| 99 | 99 | /** 导出${table.classComment} */ |
| 100 | 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 '#/api/${table.moduleName}/${simpleCl |
| 3 | 3 | import type { FormRules } from 'element-plus'; |
| 4 | 4 | |
| 5 | 5 | import { useVbenModal } from '@vben/common-ui'; |
| 6 | +import { DICT_TYPE } from '@vben/constants'; | |
| 7 | +import { getDictOptions } from '@vben/hooks'; | |
| 6 | 8 | import { Tinymce as RichTextarea } from '#/components/tinymce'; |
| 7 | 9 | import { ImageUpload, FileUpload } from "#/components/upload"; |
| 8 | 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 | 11 | #if($table.templateType == 2)## 树表需要导入这些 |
| 11 | 12 | import { get${simpleClassName}List } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}'; |
| 12 | 13 | import { handleTree } from '@vben/utils' | ... | ... |
yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/general/views/index.vue.vm
| 1 | 1 | <script lang="ts" setup> |
| 2 | 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 | 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 | 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 | 12 | import ${simpleClassName}Form from './modules/form.vue'; |
| 11 | 13 | import { Download, Plus, RefreshCw, Search, Trash2 } from '@vben/icons'; |
| 12 | 14 | import { ContentWrap } from '#/components/content-wrap'; |
| 13 | 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 | 18 | #if ( $table.templateType == 11 || $table.templateType == 12 ) |
| ... | ... | @@ -23,7 +23,6 @@ import { useTableToolbar } from '#/hooks'; |
| 23 | 23 | #end |
| 24 | 24 | #end |
| 25 | 25 | |
| 26 | -import { ref, h, reactive,onMounted,nextTick } from 'vue'; | |
| 27 | 26 | import { $t } from '#/locales'; |
| 28 | 27 | #if (${table.templateType} == 2)## 树表接口 |
| 29 | 28 | import { handleTree,isEmpty } from '@vben/utils' |
| ... | ... | @@ -104,7 +103,7 @@ const getList = async () => { |
| 104 | 103 | } |
| 105 | 104 | |
| 106 | 105 | /** 搜索按钮操作 */ |
| 107 | -const handleQuery = () => { | |
| 106 | +function handleQuery() { | |
| 108 | 107 | #if ( $table.templateType != 2 ) |
| 109 | 108 | queryParams.pageNo = 1 |
| 110 | 109 | #end |
| ... | ... | @@ -112,7 +111,7 @@ const handleQuery = () => { |
| 112 | 111 | } |
| 113 | 112 | |
| 114 | 113 | /** 重置按钮操作 */ |
| 115 | -const resetQuery = () => { | |
| 114 | +function resetQuery() { | |
| 116 | 115 | queryFormRef.value.resetFields() |
| 117 | 116 | handleQuery() |
| 118 | 117 | } |
| ... | ... | @@ -124,7 +123,7 @@ const [FormModal, formModalApi] = useVbenModal({ |
| 124 | 123 | |
| 125 | 124 | /** 创建${table.classComment} */ |
| 126 | 125 | function handleCreate() { |
| 127 | - formModalApi.setData({}).open(); | |
| 126 | + formModalApi.setData(null).open(); | |
| 128 | 127 | } |
| 129 | 128 | |
| 130 | 129 | /** 编辑${table.classComment} */ |
| ... | ... | @@ -143,7 +142,6 @@ function handleAppend(row: ${simpleClassName}Api.${simpleClassName}) { |
| 143 | 142 | async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) { |
| 144 | 143 | const loadingInstance = ElLoading.service({ |
| 145 | 144 | text: $t('ui.actionMessage.deleting', [row.id]), |
| 146 | - background: 'rgba(0, 0, 0, 0.7)', | |
| 147 | 145 | }); |
| 148 | 146 | try { |
| 149 | 147 | await delete${simpleClassName}(row.id as number); |
| ... | ... | @@ -159,7 +157,6 @@ async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) { |
| 159 | 157 | async function handleDeleteBatch() { |
| 160 | 158 | const loadingInstance = ElLoading.service({ |
| 161 | 159 | text: $t('ui.actionMessage.deleting'), |
| 162 | - background: 'rgba(0, 0, 0, 0.7)', | |
| 163 | 160 | }); |
| 164 | 161 | try { |
| 165 | 162 | await delete${simpleClassName}List(checkedIds.value); |
| ... | ... | @@ -177,12 +174,12 @@ function handleRowCheckboxChange({ |
| 177 | 174 | }: { |
| 178 | 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 | 179 | #end |
| 183 | 180 | |
| 184 | 181 | /** 导出表格 */ |
| 185 | -async function onExport() { | |
| 182 | +async function handleExport() { | |
| 186 | 183 | try { |
| 187 | 184 | exportLoading.value = true; |
| 188 | 185 | const data = await export${simpleClassName}(queryParams); |
| ... | ... | @@ -195,7 +192,7 @@ try { |
| 195 | 192 | #if (${table.templateType} == 2) |
| 196 | 193 | /** 切换树形展开/收缩状态 */ |
| 197 | 194 | const isExpanded = ref(true); |
| 198 | -function toggleExpand() { | |
| 195 | +function handleExpand() { | |
| 199 | 196 | isExpanded.value = !isExpanded.value; |
| 200 | 197 | tableRef.value?.setAllTreeExpand(isExpanded.value); |
| 201 | 198 | } |
| ... | ... | @@ -306,7 +303,7 @@ onMounted(() => { |
| 306 | 303 | v-model:hidden-search="hiddenSearchBar" |
| 307 | 304 | > |
| 308 | 305 | #if (${table.templateType} == 2) |
| 309 | - <el-button @click="toggleExpand" class="mr-2"> | |
| 306 | + <el-button @click="handleExpand" class="mr-2"> | |
| 310 | 307 | {{ isExpanded ? '收缩' : '展开' }} |
| 311 | 308 | </el-button> |
| 312 | 309 | #end |
| ... | ... | @@ -324,7 +321,7 @@ onMounted(() => { |
| 324 | 321 | type="primary" |
| 325 | 322 | class="ml-2" |
| 326 | 323 | :loading="exportLoading" |
| 327 | - @click="onExport" | |
| 324 | + @click="handleExport" | |
| 328 | 325 | v-access:code="['${permissionPrefix}:export']" |
| 329 | 326 | > |
| 330 | 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 | 6 | import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}'; |
| 7 | 7 | import type { FormRules } from 'element-plus'; |
| 8 | 8 | |
| 9 | + import { computed, ref, reactive } from 'vue'; | |
| 10 | + | |
| 9 | 11 | import { useVbenModal } from '@vben/common-ui'; |
| 12 | + import { DICT_TYPE } from '@vben/constants'; | |
| 13 | + import { getDictOptions } from '@vben/hooks'; | |
| 10 | 14 | import { Tinymce as RichTextarea } from '#/components/tinymce'; |
| 11 | 15 | import { ImageUpload, FileUpload } from "#/components/upload"; |
| 12 | 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 | 18 | import { $t } from '#/locales'; |
| 17 | 19 | |
| 18 | 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 | 5 | #set ($subClassNameVar = $subClassNameVars.get($subIndex)) |
| 6 | 6 | #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 |
| 7 | 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 | 18 | #if ($subTable.subJoinMany) ## 一对多 |
| 16 | 19 | import type { VxeTableInstance } from '#/adapter/vxe-table'; |
| ... | ... | @@ -31,12 +34,12 @@ const props = defineProps<{ |
| 31 | 34 | const list = ref<${simpleClassName}Api.${subSimpleClassName}[]>([]) // 列表的数据 |
| 32 | 35 | const tableRef = ref<VxeTableInstance>(); |
| 33 | 36 | /** 添加${subTable.classComment} */ |
| 34 | -const onAdd = async () => { | |
| 37 | +async function handleAdd() { | |
| 35 | 38 | await tableRef.value?.insertAt({} as ${simpleClassName}Api.${subSimpleClassName}, -1); |
| 36 | 39 | } |
| 37 | 40 | |
| 38 | 41 | /** 删除${subTable.classComment} */ |
| 39 | -const onDelete = async (row: ${simpleClassName}Api.${subSimpleClassName}) => { | |
| 42 | +async function onDelete(row: ${simpleClassName}Api.${subSimpleClassName}) { | |
| 40 | 43 | await tableRef.value?.remove(row); |
| 41 | 44 | } |
| 42 | 45 | |
| ... | ... | @@ -46,9 +49,11 @@ defineExpose({ |
| 46 | 49 | const data = list.value as ${simpleClassName}Api.${subSimpleClassName}[]; |
| 47 | 50 | const removeRecords = tableRef.value?.getRemoveRecords() as ${simpleClassName}Api.${subSimpleClassName}[]; |
| 48 | 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 | 232 | </vxe-column> |
| 228 | 233 | </vxe-table> |
| 229 | 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 | 236 | {{ $t('ui.actionTitle.create', ['${subTable.classComment}']) }} |
| 232 | 237 | </el-button> |
| 233 | 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 | 9 | import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}'; |
| 10 | 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 | 17 | import { DictTag } from '#/components/dict-tag'; |
| 13 | - import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils'; | |
| 18 | + import { getRangePickerDefaultProps } from '#/utils'; | |
| 14 | 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 | 20 | import { ContentWrap } from '#/components/content-wrap'; |
| 18 | 21 | |
| 19 | 22 | #if ($table.templateType == 11) ## erp |
| 20 | 23 | import { useVbenModal } from '@vben/common-ui'; |
| 24 | + import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table'; | |
| 21 | 25 | import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue' |
| 22 | 26 | import { Tinymce as RichTextarea } from '#/components/tinymce'; |
| 23 | 27 | import { ImageUpload, FileUpload } from "#/components/upload"; |
| 24 | 28 | import { ElMessage, ElLoading, ElButton, ElTabs, ElTabPane, ElPagination, ElForm, ElFormItem, ElDatePicker, ElSelect, ElOption, ElInput } from 'element-plus'; |
| 25 | 29 | import { Plus, Trash2 } from '@vben/icons'; |
| 26 | 30 | import { $t } from '#/locales'; |
| 27 | - import { TableToolbar } from '#/components/table-toolbar'; | |
| 28 | - import { useTableToolbar } from '#/hooks'; | |
| 29 | 31 | #end |
| 30 | 32 | |
| 31 | 33 | #if ($table.templateType == 11) ## erp |
| ... | ... | @@ -67,7 +69,6 @@ function handleEdit(row: ${simpleClassName}Api.${subSimpleClassName}) { |
| 67 | 69 | async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) { |
| 68 | 70 | const loadingInstance = ElLoading.service({ |
| 69 | 71 | text: $t('ui.actionMessage.deleting', [row.id]), |
| 70 | - background: 'rgba(0, 0, 0, 0.7)', | |
| 71 | 72 | }); |
| 72 | 73 | try { |
| 73 | 74 | await delete${subSimpleClassName}(row.id as number); |
| ... | ... | @@ -83,7 +84,6 @@ async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) { |
| 83 | 84 | async function handleDeleteBatch() { |
| 84 | 85 | const loadingInstance = ElLoading.service({ |
| 85 | 86 | text: $t('ui.actionMessage.deleting'), |
| 86 | - background: 'rgba(0, 0, 0, 0.7)', | |
| 87 | 87 | }); |
| 88 | 88 | try { |
| 89 | 89 | await delete${subSimpleClassName}List(checkedIds.value); |
| ... | ... | @@ -101,7 +101,7 @@ function handleRowCheckboxChange({ |
| 101 | 101 | }: { |
| 102 | 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 | 106 | #end |
| 107 | 107 | #end |
| ... | ... | @@ -129,64 +129,64 @@ function handleRowCheckboxChange({ |
| 129 | 129 | }) |
| 130 | 130 | |
| 131 | 131 | /** 搜索按钮操作 */ |
| 132 | -const handleQuery = () => { | |
| 132 | +function handleQuery() { | |
| 133 | 133 | queryParams.pageNo = 1 |
| 134 | 134 | getList() |
| 135 | 135 | } |
| 136 | 136 | |
| 137 | 137 | /** 重置按钮操作 */ |
| 138 | -const resetQuery = () => { | |
| 138 | +function resetQuery() { | |
| 139 | 139 | queryFormRef.value.resetFields() |
| 140 | 140 | handleQuery() |
| 141 | 141 | } |
| 142 | 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 | 191 | #if ($table.templateType == 11) ## erp |
| 192 | 192 | /** 初始化 */ |
| ... | ... | @@ -290,7 +290,7 @@ onMounted(() => { |
| 290 | 290 | <!-- 列表 --> |
| 291 | 291 | <ContentWrap title="${table.classComment}"> |
| 292 | 292 | <template #extra> |
| 293 | - <TableToolbar | |
| 293 | + <VbenVxeTableToolbar | |
| 294 | 294 | ref="tableToolbarRef" |
| 295 | 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 | 3 | import type { PageParam, PageResult } from '@vben/request'; |
| 4 | +#end | |
| 2 | 5 | import type { Dayjs } from 'dayjs'; |
| 3 | 6 | |
| 4 | 7 | import { requestClient } from '#/api/request'; |
| 5 | 8 | #set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}") |
| 6 | 9 | |
| 7 | -export namespace ${simpleClassName}Api { | |
| 10 | +export namespace ${apiName} { | |
| 8 | 11 | ## 特殊:主子表专属逻辑 |
| 9 | 12 | #foreach ($subTable in $subTables) |
| 10 | 13 | #set ($index = $foreach.count - 1) |
| ... | ... | @@ -60,27 +63,35 @@ export namespace ${simpleClassName}Api { |
| 60 | 63 | #if ( $table.templateType != 2 ) |
| 61 | 64 | /** 查询${table.classComment}分页 */ |
| 62 | 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 | 71 | #else |
| 66 | 72 | /** 查询${table.classComment}列表 */ |
| 67 | 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 | 79 | #end |
| 71 | 80 | |
| 72 | 81 | /** 查询${table.classComment}详情 */ |
| 73 | 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 | 88 | /** 新增${table.classComment} */ |
| 78 | -export function create${simpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) { | |
| 89 | +export function create${simpleClassName}(data: ${apiName}.${simpleClassName}) { | |
| 79 | 90 | return requestClient.post('${baseURL}/create', data); |
| 80 | 91 | } |
| 81 | 92 | |
| 82 | 93 | /** 修改${table.classComment} */ |
| 83 | -export function update${simpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) { | |
| 94 | +export function update${simpleClassName}(data: ${apiName}.${simpleClassName}) { | |
| 84 | 95 | return requestClient.put('${baseURL}/update', data); |
| 85 | 96 | } |
| 86 | 97 | |
| ... | ... | @@ -88,17 +99,19 @@ export function update${simpleClassName}(data: ${simpleClassName}Api.${simpleCla |
| 88 | 99 | export function delete${simpleClassName}(id: number) { |
| 89 | 100 | return requestClient.delete(`${baseURL}/delete?id=${id}`); |
| 90 | 101 | } |
| 91 | - | |
| 92 | 102 | #if ( $table.templateType != 2 && $deleteBatchEnable) |
| 103 | + | |
| 93 | 104 | /** 批量删除${table.classComment} */ |
| 94 | 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 | 110 | #end |
| 98 | 111 | |
| 99 | 112 | /** 导出${table.classComment} */ |
| 100 | 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 | 131 | #if ( $table.templateType == 11 ) |
| 119 | 132 | /** 获得${subTable.classComment}分页 */ |
| 120 | 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 | 139 | ## 情况二:非 MASTER_ERP 时,需要列表查询子表 |
| 124 | 140 | #else |
| 125 | 141 | #if ( $subTable.subJoinMany ) |
| 126 | 142 | /** 获得${subTable.classComment}列表 */ |
| 127 | 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 | 148 | #else |
| 131 | 149 | /** 获得${subTable.classComment} */ |
| 132 | 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 | 155 | #end |
| 136 | 156 | #end |
| 137 | 157 | ## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作 |
| 138 | 158 | #if ( $table.templateType == 11 ) |
| 139 | 159 | /** 新增${subTable.classComment} */ |
| 140 | -export function create${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) { | |
| 160 | +export function create${subSimpleClassName}(data: ${apiName}.${subSimpleClassName}) { | |
| 141 | 161 | return requestClient.post(`${baseURL}/${subSimpleClassName_strikeCase}/create`, data); |
| 142 | 162 | } |
| 143 | 163 | |
| 144 | 164 | /** 修改${subTable.classComment} */ |
| 145 | -export function update${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) { | |
| 165 | +export function update${subSimpleClassName}(data: ${apiName}.${subSimpleClassName}) { | |
| 146 | 166 | return requestClient.put(`${baseURL}/${subSimpleClassName_strikeCase}/update`, data); |
| 147 | 167 | } |
| 148 | 168 | |
| ... | ... | @@ -154,13 +174,17 @@ export function delete${subSimpleClassName}(id: number) { |
| 154 | 174 | #if ($deleteBatchEnable) |
| 155 | 175 | /** 批量删除${subTable.classComment} */ |
| 156 | 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 | 181 | #end |
| 160 | 182 | |
| 161 | 183 | /** 获得${subTable.classComment} */ |
| 162 | 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 | 189 | #end |
| 166 | 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 | 2 | import type { VbenFormSchema } from '#/adapter/form'; |
| 2 | 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 | 8 | #if(${table.templateType} == 2)## 树表需要导入这些 |
| 12 | 9 | import { get${simpleClassName}List } from '#/api/${table.moduleName}/${table.businessName}'; |
| 13 | 10 | import { handleTree } from '@vben/utils'; |
| 14 | 11 | #end |
| 15 | 12 | |
| 13 | +import { getRangePickerDefaultProps } from '#/utils'; | |
| 14 | + | |
| 16 | 15 | /** 新增/修改的表单 */ |
| 17 | 16 | export function useFormSchema(): VbenFormSchema[] { |
| 18 | 17 | return [ |
| ... | ... | @@ -30,7 +29,7 @@ export function useFormSchema(): VbenFormSchema[] { |
| 30 | 29 | label: '上级${table.classComment}', |
| 31 | 30 | component: 'ApiTreeSelect', |
| 32 | 31 | componentProps: { |
| 33 | - allowClear: true, | |
| 32 | + clearable: true, | |
| 34 | 33 | api: async () => { |
| 35 | 34 | const data = await get${simpleClassName}List({}); |
| 36 | 35 | data.unshift({ |
| ... | ... | @@ -158,13 +157,13 @@ export function useGridFormSchema(): VbenFormSchema[] { |
| 158 | 157 | #if ($column.htmlType == "input" || $column.htmlType == "textarea" || $column.htmlType == "editor") |
| 159 | 158 | component: 'Input', |
| 160 | 159 | componentProps: { |
| 161 | - allowClear: true, | |
| 160 | + clearable: true, | |
| 162 | 161 | placeholder: '请输入${comment}', |
| 163 | 162 | }, |
| 164 | 163 | #elseif ($column.htmlType == "select" || $column.htmlType == "radio") |
| 165 | 164 | component: 'Select', |
| 166 | 165 | componentProps: { |
| 167 | - allowClear: true, | |
| 166 | + clearable: true, | |
| 168 | 167 | #if ("" != $dictType)## 设置了 dictType 数据字典的情况 |
| 169 | 168 | options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), |
| 170 | 169 | #else## 未设置 dictType 数据字典的情况 |
| ... | ... | @@ -176,7 +175,7 @@ export function useGridFormSchema(): VbenFormSchema[] { |
| 176 | 175 | component: 'RangePicker', |
| 177 | 176 | componentProps: { |
| 178 | 177 | ...getRangePickerDefaultProps(), |
| 179 | - allowClear: true, | |
| 178 | + clearable: true, | |
| 180 | 179 | }, |
| 181 | 180 | #end |
| 182 | 181 | }, |
| ... | ... | @@ -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 | 189 | return [ |
| 191 | 190 | #if ($table.templateType != 2 && $deleteBatchEnable) |
| 192 | 191 | { type: 'checkbox', width: 40 }, |
| ... | ... | @@ -361,13 +360,13 @@ export function use${subSimpleClassName}GridFormSchema(): VbenFormSchema[] { |
| 361 | 360 | #if ($column.htmlType == "input" || $column.htmlType == "textarea" || $column.htmlType == "editor") |
| 362 | 361 | component: 'Input', |
| 363 | 362 | componentProps: { |
| 364 | - allowClear: true, | |
| 363 | + clearable: true, | |
| 365 | 364 | placeholder: '请输入${comment}', |
| 366 | 365 | }, |
| 367 | 366 | #elseif ($column.htmlType == "select" || $column.htmlType == "radio") |
| 368 | 367 | component: 'Select', |
| 369 | 368 | componentProps: { |
| 370 | - allowClear: true, | |
| 369 | + clearable: true, | |
| 371 | 370 | #if ("" != $dictType)## 设置了 dictType 数据字典的情况 |
| 372 | 371 | options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), |
| 373 | 372 | #else## 未设置 dictType 数据字典的情况 |
| ... | ... | @@ -379,7 +378,7 @@ export function use${subSimpleClassName}GridFormSchema(): VbenFormSchema[] { |
| 379 | 378 | component: 'RangePicker', |
| 380 | 379 | componentProps: { |
| 381 | 380 | ...getRangePickerDefaultProps(), |
| 382 | - allowClear: true, | |
| 381 | + clearable: true, | |
| 383 | 382 | }, |
| 384 | 383 | #end |
| 385 | 384 | }, |
| ... | ... | @@ -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 | 392 | return [ |
| 394 | 393 | #if ($table.templateType != 2 && $deleteBatchEnable) |
| 395 | 394 | { type: 'checkbox', width: 40 }, |
| ... | ... | @@ -426,7 +425,7 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${sim |
| 426 | 425 | #else |
| 427 | 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 | 429 | return [ |
| 431 | 430 | #foreach($column in $subColumns) |
| 432 | 431 | #if ($column.createOperation || $column.updateOperation) |
| ... | ... | @@ -602,4 +601,4 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${sim |
| 602 | 601 | } |
| 603 | 602 | #end |
| 604 | 603 | #end |
| 605 | 604 | -#end |
| 605 | +#end | |
| 606 | 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 | 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 | 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 | 15 | #if ( $table.templateType == 10 || $table.templateType == 12 ) |
| 8 | 16 | #foreach ($subSimpleClassName in $subSimpleClassNames) |
| ... | ... | @@ -12,21 +20,15 @@ import { ElMessage, ElTabs, ElTabPane, ElCheckbox, ElInput, ElSelect, ElRadioGro |
| 12 | 20 | #end |
| 13 | 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 | 23 | import { useFormSchema } from '../data'; |
| 21 | 24 | |
| 22 | 25 | const emit = defineEmits(['success']); |
| 23 | -const formData = ref<${simpleClassName}Api.${simpleClassName}>(); | |
| 26 | +const formData = ref<${apiName}.${simpleClassName}>(); | |
| 24 | 27 | const getTitle = computed(() => { |
| 25 | 28 | return formData.value?.id |
| 26 | 29 | ? $t('ui.actionTitle.edit', ['${table.classComment}']) |
| 27 | 30 | : $t('ui.actionTitle.create', ['${table.classComment}']); |
| 28 | 31 | }); |
| 29 | - | |
| 30 | 32 | ## 特殊:主子表专属逻辑 |
| 31 | 33 | #if ( $table.templateType == 10 || $table.templateType == 12 ) |
| 32 | 34 | #if ( $subTables && $subTables.size() > 0 ) |
| ... | ... | @@ -51,7 +53,7 @@ const [Form, formApi] = useVbenForm({ |
| 51 | 53 | }, |
| 52 | 54 | layout: 'horizontal', |
| 53 | 55 | schema: useFormSchema(), |
| 54 | - showDefaultActions: false | |
| 56 | + showDefaultActions: false, | |
| 55 | 57 | }); |
| 56 | 58 | |
| 57 | 59 | const [Modal, modalApi] = useVbenModal({ |
| ... | ... | @@ -60,8 +62,8 @@ const [Modal, modalApi] = useVbenModal({ |
| 60 | 62 | if (!valid) { |
| 61 | 63 | return; |
| 62 | 64 | } |
| 63 | - ## 特殊:主子表专属逻辑 | |
| 64 | - #if ( $table.templateType == 10 || $table.templateType == 12 ) | |
| 65 | +## 特殊:主子表专属逻辑 | |
| 66 | +#if ( $table.templateType == 10 || $table.templateType == 12 ) | |
| 65 | 67 | #if ( $subTables && $subTables.size() > 0 ) |
| 66 | 68 | // 校验子表单 |
| 67 | 69 | #foreach ($subTable in $subTables) |
| ... | ... | @@ -78,12 +80,12 @@ const [Modal, modalApi] = useVbenModal({ |
| 78 | 80 | #end |
| 79 | 81 | #end |
| 80 | 82 | #end |
| 81 | - #end | |
| 83 | +#end | |
| 82 | 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 | 89 | #if ( $subTables && $subTables.size() > 0 ) |
| 88 | 90 | // 拼接子表的数据 |
| 89 | 91 | #foreach ($subTable in $subTables) |
| ... | ... | @@ -96,7 +98,7 @@ const [Modal, modalApi] = useVbenModal({ |
| 96 | 98 | #end |
| 97 | 99 | #end |
| 98 | 100 | #end |
| 99 | - #end | |
| 101 | +#end | |
| 100 | 102 | try { |
| 101 | 103 | await (formData.value?.id ? update${simpleClassName}(data) : create${simpleClassName}(data)); |
| 102 | 104 | // 关闭并提示 |
| ... | ... | @@ -113,21 +115,24 @@ const [Modal, modalApi] = useVbenModal({ |
| 113 | 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 | 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 | 138 | </script> |
| ... | ... | @@ -135,20 +140,20 @@ const [Modal, modalApi] = useVbenModal({ |
| 135 | 140 | <template> |
| 136 | 141 | <Modal :title="getTitle"> |
| 137 | 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 | 147 | #foreach ($subTable in $subTables) |
| 143 | 148 | #set ($index = $foreach.count - 1) |
| 144 | 149 | #set ($subClassNameVar = $subClassNameVars.get($index)) |
| 145 | 150 | #set ($subSimpleClassName = $subSimpleClassNames.get($index)) |
| 146 | 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 | 153 | <${subSimpleClassName}Form ref="${subClassNameVar}FormRef" :${subJoinColumn_strikeCase}="formData?.id" /> |
| 149 | - </el-tab-pane> | |
| 154 | + </ElTabPane> | |
| 150 | 155 | #end |
| 151 | - </el-tabs> | |
| 152 | - #end | |
| 156 | + </ElTabs> | |
| 157 | +#end | |
| 153 | 158 | </Modal> |
| 154 | 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 | 2 | <script lang="ts" setup> |
| 2 | 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 | 13 | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| 21 | 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 | 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 | 28 | #end |
| 26 | -import { downloadFileFromBlobPart, isEmpty } from '@vben/utils'; | |
| 29 | +import { $t } from '#/locales'; | |
| 27 | 30 | |
| 28 | 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 | 42 | #if ($table.templateType == 12 || $table.templateType == 11) ## 内嵌和erp情况 |
| 31 | 43 | /** 子表的列表 */ |
| 32 | 44 | const subTabsName = ref('$subClassNameVars.get(0)') |
| 33 | 45 | #if ($table.templateType == 11) |
| 34 | -const select${simpleClassName} = ref<${simpleClassName}Api.${simpleClassName}>(); | |
| 46 | +const select${simpleClassName} = ref<${apiName}.${simpleClassName}>(); | |
| 35 | 47 | #end |
| 36 | 48 | #end |
| 37 | - | |
| 38 | 49 | const [FormModal, formModalApi] = useVbenModal({ |
| 39 | 50 | connectedComponent: Form, |
| 40 | 51 | destroyOnClose: true, |
| 41 | 52 | }); |
| 42 | - | |
| 43 | 53 | #if (${table.templateType} == 2)## 树表特有:控制表格展开收缩 |
| 54 | + | |
| 44 | 55 | /** 切换树形展开/收缩状态 */ |
| 45 | 56 | const isExpanded = ref(true); |
| 46 | -function toggleExpand() { | |
| 57 | +function handleExpand() { | |
| 47 | 58 | isExpanded.value = !isExpanded.value; |
| 48 | 59 | gridApi.grid.setAllTreeExpand(isExpanded.value); |
| 49 | 60 | } |
| 50 | 61 | #end |
| 51 | 62 | |
| 52 | 63 | /** 刷新表格 */ |
| 53 | -function onRefresh() { | |
| 64 | +function handleRefresh() { | |
| 54 | 65 | #if ($table.templateType == 12) ## 内嵌情况 |
| 55 | 66 | gridApi.reload(); |
| 56 | 67 | #else |
| ... | ... | @@ -60,31 +71,30 @@ function onRefresh() { |
| 60 | 71 | |
| 61 | 72 | /** 创建${table.classComment} */ |
| 62 | 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 | 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 | 80 | formModalApi.setData({ ${treeParentColumn.javaField}: row.id }).open(); |
| 75 | 81 | } |
| 76 | 82 | #end |
| 77 | 83 | |
| 84 | +/** 编辑${table.classComment} */ | |
| 85 | +function handleEdit(row: ${apiName}.${simpleClassName}) { | |
| 86 | + formModalApi.setData(row).open(); | |
| 87 | +} | |
| 88 | + | |
| 78 | 89 | /** 删除${table.classComment} */ |
| 79 | -async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) { | |
| 90 | +async function handleDelete(row: ${apiName}.${simpleClassName}) { | |
| 80 | 91 | const loadingInstance = ElLoading.service({ |
| 81 | 92 | text: $t('ui.actionMessage.deleting', [row.id]), |
| 82 | - background: 'rgba(0, 0, 0, 0.7)', | |
| 83 | 93 | }); |
| 84 | 94 | try { |
| 85 | - await delete${simpleClassName}(row.id as number); | |
| 95 | + await delete${simpleClassName}(row.id!); | |
| 86 | 96 | ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.id])); |
| 87 | - onRefresh(); | |
| 97 | + handleRefresh(); | |
| 88 | 98 | } finally { |
| 89 | 99 | loadingInstance.close(); |
| 90 | 100 | } |
| ... | ... | @@ -93,27 +103,27 @@ async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) { |
| 93 | 103 | #if ($table.templateType != 2 && $deleteBatchEnable) |
| 94 | 104 | /** 批量删除${table.classComment} */ |
| 95 | 105 | async function handleDeleteBatch() { |
| 106 | + await confirm($t('ui.actionMessage.deleteBatchConfirm')); | |
| 96 | 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 | 110 | try { |
| 101 | 111 | await delete${simpleClassName}List(checkedIds.value); |
| 102 | 112 | checkedIds.value = []; |
| 103 | 113 | ElMessage.success($t('ui.actionMessage.deleteSuccess')); |
| 104 | - onRefresh(); | |
| 114 | + handleRefresh(); | |
| 105 | 115 | } finally { |
| 106 | 116 | loadingInstance.close(); |
| 107 | 117 | } |
| 108 | 118 | } |
| 109 | 119 | |
| 110 | -const checkedIds = ref<number[]>([]) | |
| 120 | +const checkedIds = ref<number[]>([]); | |
| 111 | 121 | function handleRowCheckboxChange({ |
| 112 | 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 | 128 | #end |
| 119 | 129 | |
| ... | ... | @@ -135,20 +145,11 @@ const [Grid, gridApi] = useVbenVxeGrid({ |
| 135 | 145 | height: 'auto', |
| 136 | 146 | #end |
| 137 | 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 | 148 | pagerConfig: { |
| 150 | - enabled: true, | |
| 149 | + enabled: false, | |
| 151 | 150 | }, |
| 151 | +#else## 标准表设置 | |
| 152 | + keepSource: true, | |
| 152 | 153 | #end |
| 153 | 154 | proxyConfig: { |
| 154 | 155 | ajax: { |
| ... | ... | @@ -178,27 +179,35 @@ const [Grid, gridApi] = useVbenVxeGrid({ |
| 178 | 179 | refresh: true, |
| 179 | 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 | 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 | 204 | #end |
| 195 | 205 | }); |
| 196 | 206 | </script> |
| 197 | 207 | |
| 198 | 208 | <template> |
| 199 | 209 | <Page auto-content-height> |
| 200 | - <FormModal @success="onRefresh" /> | |
| 201 | - | |
| 210 | + <FormModal @success="handleRefresh" /> | |
| 202 | 211 | #if ($table.templateType == 11) ## erp情况 |
| 203 | 212 | <div> |
| 204 | 213 | #end |
| ... | ... | @@ -206,29 +215,22 @@ const [Grid, gridApi] = useVbenVxeGrid({ |
| 206 | 215 | #if ($table.templateType == 12) ## 内嵌情况 |
| 207 | 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 | 219 | #foreach ($subTable in $subTables) |
| 211 | 220 | #set ($index = $foreach.count - 1) |
| 212 | 221 | #set ($subClassNameVar = $subClassNameVars.get($index)) |
| 213 | 222 | #set ($subSimpleClassName = $subSimpleClassNames.get($index)) |
| 214 | 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 | 225 | <${subSimpleClassName}List :${subJoinColumn_strikeCase}="row?.id" /> |
| 217 | - </el-tab-pane> | |
| 226 | + </ElTabPane> | |
| 218 | 227 | #end |
| 219 | - </el-tabs> | |
| 228 | + </ElTabs> | |
| 220 | 229 | </template> |
| 221 | 230 | #end |
| 222 | 231 | <template #toolbar-tools> |
| 223 | 232 | <TableAction |
| 224 | 233 | :actions="[ |
| 225 | - #if (${table.templateType} == 2)## 树表特有:展开/收缩按钮 | |
| 226 | - { | |
| 227 | - label: isExpanded ? '收缩' : '展开', | |
| 228 | - type: 'primary', | |
| 229 | - onClick: toggleExpand, | |
| 230 | - }, | |
| 231 | - #end | |
| 232 | 234 | { |
| 233 | 235 | label: $t('ui.actionTitle.create', ['${table.classComment}']), |
| 234 | 236 | type: 'primary', |
| ... | ... | @@ -236,6 +238,13 @@ const [Grid, gridApi] = useVbenVxeGrid({ |
| 236 | 238 | auth: ['${table.moduleName}:${simpleClassName_strikeCase}:create'], |
| 237 | 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 | 249 | label: $t('ui.actionTitle.export'), |
| 241 | 250 | type: 'primary', |
| ... | ... | @@ -248,8 +257,8 @@ const [Grid, gridApi] = useVbenVxeGrid({ |
| 248 | 257 | label: $t('ui.actionTitle.deleteBatch'), |
| 249 | 258 | type: 'danger', |
| 250 | 259 | icon: ACTION_ICON.DELETE, |
| 251 | - disabled: isEmpty(checkedIds), | |
| 252 | 260 | auth: ['${table.moduleName}:${simpleClassName_strikeCase}:delete'], |
| 261 | + disabled: isEmpty(checkedIds), | |
| 253 | 262 | onClick: handleDeleteBatch, |
| 254 | 263 | }, |
| 255 | 264 | #end |
| ... | ... | @@ -262,7 +271,8 @@ const [Grid, gridApi] = useVbenVxeGrid({ |
| 262 | 271 | #if (${table.templateType} == 2)## 树表特有:新增下级 |
| 263 | 272 | { |
| 264 | 273 | label: '新增下级', |
| 265 | - type: 'text', | |
| 274 | + type: 'primary', | |
| 275 | + link: true, | |
| 266 | 276 | icon: ACTION_ICON.ADD, |
| 267 | 277 | auth: ['${table.moduleName}:${simpleClassName_strikeCase}:create'], |
| 268 | 278 | onClick: handleAppend.bind(null, row), |
| ... | ... | @@ -270,7 +280,8 @@ const [Grid, gridApi] = useVbenVxeGrid({ |
| 270 | 280 | #end |
| 271 | 281 | { |
| 272 | 282 | label: $t('common.edit'), |
| 273 | - type: 'text', | |
| 283 | + type: 'primary', | |
| 284 | + link: true, | |
| 274 | 285 | icon: ACTION_ICON.EDIT, |
| 275 | 286 | auth: ['${table.moduleName}:${simpleClassName_strikeCase}:update'], |
| 276 | 287 | onClick: handleEdit.bind(null, row), |
| ... | ... | @@ -278,7 +289,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ |
| 278 | 289 | { |
| 279 | 290 | label: $t('common.delete'), |
| 280 | 291 | type: 'danger', |
| 281 | - text: true, | |
| 292 | + link: true, | |
| 282 | 293 | icon: ACTION_ICON.DELETE, |
| 283 | 294 | auth: ['${table.moduleName}:${simpleClassName_strikeCase}:delete'], |
| 284 | 295 | popConfirm: { |
| ... | ... | @@ -290,21 +301,20 @@ const [Grid, gridApi] = useVbenVxeGrid({ |
| 290 | 301 | /> |
| 291 | 302 | </template> |
| 292 | 303 | </Grid> |
| 293 | - | |
| 294 | 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 | 307 | #foreach ($subTable in $subTables) |
| 298 | 308 | #set ($index = $foreach.count - 1) |
| 299 | 309 | #set ($subClassNameVar = $subClassNameVars.get($index)) |
| 300 | 310 | #set ($subSimpleClassName = $subSimpleClassNames.get($index)) |
| 301 | 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 | 313 | <${subSimpleClassName}List :${subJoinColumn_strikeCase}="select${simpleClassName}?.id" /> |
| 304 | - </el-tab-pane> | |
| 314 | + </ElTabPane> | |
| 305 | 315 | #end |
| 306 | - </el-tabs> | |
| 316 | + </ElTabs> | |
| 307 | 317 | </div> |
| 308 | 318 | #end |
| 309 | 319 | </Page> |
| 310 | 320 | -</template> |
| 321 | +</template> | |
| 311 | 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 | 2 | #set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 |
| 3 | 3 | #set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 |
| 4 | 4 | #set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) |
| 5 | +#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api") | |
| 5 | 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 | 11 | import { useVbenModal } from '@vben/common-ui'; |
| 12 | + | |
| 9 | 13 | import { ElMessage } from 'element-plus'; |
| 10 | 14 | |
| 11 | - import { computed, ref } from 'vue'; | |
| 12 | - import { $t } from '#/locales'; | |
| 13 | 15 | import { useVbenForm } from '#/adapter/form'; |
| 14 | 16 | import { get${subSimpleClassName}, create${subSimpleClassName}, update${subSimpleClassName} } from '#/api/${table.moduleName}/${table.businessName}'; |
| 17 | + import { $t } from '#/locales'; | |
| 15 | 18 | |
| 16 | 19 | import { use${subSimpleClassName}FormSchema } from '../data'; |
| 17 | 20 | |
| 18 | 21 | const emit = defineEmits(['success']); |
| 19 | - const formData = ref<${simpleClassName}Api.${subSimpleClassName}>(); | |
| 22 | + const formData = ref<${apiName}.${subSimpleClassName}>(); | |
| 20 | 23 | const getTitle = computed(() => { |
| 21 | 24 | return formData.value?.id |
| 22 | 25 | ? $t('ui.actionTitle.edit', ['${subTable.classComment}']) |
| ... | ... | @@ -45,7 +48,7 @@ |
| 45 | 48 | |
| 46 | 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 | 52 | data.${subJoinColumn.javaField} = formData.value?.${subJoinColumn.javaField}; |
| 50 | 53 | try { |
| 51 | 54 | await (formData.value?.id ? update${subSimpleClassName}(data) : create${subSimpleClassName}(data)); |
| ... | ... | @@ -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 | 71 | if (!data) { |
| 69 | 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 | 4 | #set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) |
| 5 | 5 | #set ($subClassNameVar = $subClassNameVars.get($subIndex)) |
| 6 | 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 | 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 | 11 | import { computed, ref, h, onMounted,watch,nextTick } from 'vue'; |
| 12 | + | |
| 11 | 13 | import { $t } from '#/locales'; |
| 12 | 14 | |
| 13 | 15 | #if ($subTable.subJoinMany) ## 一对多 |
| ... | ... | @@ -33,7 +35,7 @@ const props = defineProps<{ |
| 33 | 35 | function onActionClick({ |
| 34 | 36 | code, |
| 35 | 37 | row, |
| 36 | -}: OnActionClickParams<${simpleClassName}Api.${subSimpleClassName}>) { | |
| 38 | +}: OnActionClickParams<${apiName}.${subSimpleClassName}>) { | |
| 37 | 39 | switch (code) { |
| 38 | 40 | case 'delete': { |
| 39 | 41 | onDelete(row); |
| ... | ... | @@ -67,16 +69,16 @@ const onAdd = async () => { |
| 67 | 69 | } |
| 68 | 70 | |
| 69 | 71 | /** 删除${subTable.classComment} */ |
| 70 | -const onDelete = async (row: ${simpleClassName}Api.${subSimpleClassName}) => { | |
| 72 | +const onDelete = async (row: ${apiName}.${subSimpleClassName}) => { | |
| 71 | 73 | await gridApi.grid.remove(row); |
| 72 | 74 | } |
| 73 | 75 | |
| 74 | 76 | /** 提供获取表格数据的方法供父组件调用 */ |
| 75 | 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 | 82 | return data |
| 81 | 83 | .filter((row) => !removeRecords.some((removed) => removed.id === row.id)) |
| 82 | 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 | 5 | #set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 |
| 6 | 6 | #set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($subIndex)) |
| 7 | 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 | 9 | <script lang="ts" setup> |
| 9 | 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 | 13 | #if ($table.templateType == 11) ## erp |
| 13 | 14 | import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue' |
| ... | ... | @@ -41,6 +42,19 @@ const [FormModal, formModalApi] = useVbenModal({ |
| 41 | 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 | 58 | /** 创建${subTable.classComment} */ |
| 45 | 59 | function handleCreate() { |
| 46 | 60 | if (!props.${subJoinColumn.javaField}){ |
| ... | ... | @@ -51,20 +65,19 @@ function handleCreate() { |
| 51 | 65 | } |
| 52 | 66 | |
| 53 | 67 | /** 编辑${subTable.classComment} */ |
| 54 | -function handleEdit(row: ${simpleClassName}Api.${subSimpleClassName}) { | |
| 68 | +function handleEdit(row: ${apiName}.${subSimpleClassName}) { | |
| 55 | 69 | formModalApi.setData(row).open(); |
| 56 | 70 | } |
| 57 | 71 | |
| 58 | 72 | /** 删除${subTable.classComment} */ |
| 59 | -async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) { | |
| 73 | +async function handleDelete(row: ${apiName}.${subSimpleClassName}) { | |
| 60 | 74 | const loadingInstance = ElLoading.service({ |
| 61 | 75 | text: $t('ui.actionMessage.deleting', [row.id]), |
| 62 | - background: 'rgba(0, 0, 0, 0.7)', | |
| 63 | 76 | }); |
| 64 | 77 | try { |
| 65 | - await delete${subSimpleClassName}(row.id as number); | |
| 78 | + await delete${subSimpleClassName}(row.id!); | |
| 66 | 79 | ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.id])); |
| 67 | - onRefresh(); | |
| 80 | + handleRefresh(); | |
| 68 | 81 | } finally { |
| 69 | 82 | loadingInstance.close(); |
| 70 | 83 | } |
| ... | ... | @@ -75,13 +88,12 @@ async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) { |
| 75 | 88 | async function handleDeleteBatch() { |
| 76 | 89 | const loadingInstance = ElLoading.service({ |
| 77 | 90 | text: $t('ui.actionMessage.deleting'), |
| 78 | - background: 'rgba(0, 0, 0, 0.7)', | |
| 79 | 91 | }); |
| 80 | 92 | try { |
| 81 | 93 | await delete${subSimpleClassName}List(checkedIds.value); |
| 82 | 94 | checkedIds.value = []; |
| 83 | 95 | ElMessage.success($t('ui.actionMessage.deleteSuccess')); |
| 84 | - onRefresh(); | |
| 96 | + handleRefresh(); | |
| 85 | 97 | } finally { |
| 86 | 98 | loadingInstance.close(); |
| 87 | 99 | } |
| ... | ... | @@ -91,9 +103,9 @@ const checkedIds = ref<number[]>([]) |
| 91 | 103 | function handleRowCheckboxChange({ |
| 92 | 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 | 110 | #end |
| 99 | 111 | |
| ... | ... | @@ -143,7 +155,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ |
| 143 | 155 | keyField: 'id', |
| 144 | 156 | isHover: true, |
| 145 | 157 | }, |
| 146 | - } as VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>, | |
| 158 | + } as VxeTableGridOptions<${apiName}.${subSimpleClassName}>, | |
| 147 | 159 | #if (${table.templateType} == 11 && $deleteBatchEnable) |
| 148 | 160 | gridEvents:{ |
| 149 | 161 | checkboxAll: handleRowCheckboxChange, |
| ... | ... | @@ -152,19 +164,6 @@ const [Grid, gridApi] = useVbenVxeGrid({ |
| 152 | 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 | 168 | watch( |
| 170 | 169 | () => props.${subJoinColumn.javaField}, |
| ... | ... | @@ -173,7 +172,7 @@ watch( |
| 173 | 172 | return; |
| 174 | 173 | } |
| 175 | 174 | await nextTick(); |
| 176 | - await onRefresh() | |
| 175 | + await handleRefresh() | |
| 177 | 176 | }, |
| 178 | 177 | { immediate: true }, |
| 179 | 178 | ); |
| ... | ... | @@ -181,7 +180,7 @@ watch( |
| 181 | 180 | |
| 182 | 181 | <template> |
| 183 | 182 | #if ($table.templateType == 11) ## erp |
| 184 | - <FormModal @success="onRefresh" /> | |
| 183 | + <FormModal @success="handleRefresh" /> | |
| 185 | 184 | <Grid table-title="${subTable.classComment}列表"> |
| 186 | 185 | <template #toolbar-tools> |
| 187 | 186 | <TableAction |
| ... | ... | @@ -211,7 +210,8 @@ watch( |
| 211 | 210 | :actions="[ |
| 212 | 211 | { |
| 213 | 212 | label: $t('common.edit'), |
| 214 | - type: 'text', | |
| 213 | + type: 'primary', | |
| 214 | + link: true, | |
| 215 | 215 | icon: ACTION_ICON.EDIT, |
| 216 | 216 | auth: ['${table.moduleName}:${simpleClassName_strikeCase}:update'], |
| 217 | 217 | onClick: handleEdit.bind(null, row), |
| ... | ... | @@ -219,7 +219,7 @@ watch( |
| 219 | 219 | { |
| 220 | 220 | label: $t('common.delete'), |
| 221 | 221 | type: 'danger', |
| 222 | - text: true, | |
| 222 | + link: true, | |
| 223 | 223 | icon: ACTION_ICON.DELETE, |
| 224 | 224 | auth: ['${table.moduleName}:${simpleClassName_strikeCase}:delete'], |
| 225 | 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 | 17 | @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true) |
| 18 | 18 | @JsonSubTypes({ |
| 19 | 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 | 22 | @JsonSubTypes.Type(value = IotDataSinkMqttConfig.class, name = "10"), |
| 21 | 23 | @JsonSubTypes.Type(value = IotDataSinkRedisConfig.class, name = "21"), |
| 22 | 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 | 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 | 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 | 76 | */ |
| 77 | 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
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 | 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 | 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 | 16 | import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; |
| 17 | 17 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; |
| 18 | 18 | import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotSceneRuleMapper; |
| 19 | +import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; | |
| 19 | 20 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; |
| 20 | -import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager; | |
| 21 | 21 | import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; |
| 22 | 22 | import cn.iocoder.yudao.module.iot.service.product.IotProductService; |
| 23 | 23 | import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction; |
| 24 | 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 | 26 | import jakarta.annotation.Resource; |
| 26 | 27 | import lombok.extern.slf4j.Slf4j; |
| 28 | +import org.springframework.cache.annotation.CacheEvict; | |
| 29 | +import org.springframework.cache.annotation.Cacheable; | |
| 27 | 30 | import org.springframework.stereotype.Service; |
| 28 | 31 | import org.springframework.validation.annotation.Validated; |
| 29 | 32 | |
| ... | ... | @@ -47,9 +50,6 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { |
| 47 | 50 | @Resource |
| 48 | 51 | private IotSceneRuleMapper sceneRuleMapper; |
| 49 | 52 | |
| 50 | - // TODO @puhui999:定时任务,基于它调度; | |
| 51 | - @Resource(name = "iotSchedulerManager") | |
| 52 | - private IotSchedulerManager schedulerManager; | |
| 53 | 53 | @Resource |
| 54 | 54 | private IotProductService productService; |
| 55 | 55 | @Resource |
| ... | ... | @@ -59,38 +59,68 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { |
| 59 | 59 | private IotSceneRuleMatcherManager sceneRuleMatcherManager; |
| 60 | 60 | @Resource |
| 61 | 61 | private List<IotSceneRuleAction> sceneRuleActions; |
| 62 | + @Resource | |
| 63 | + private IotSceneRuleTimerHandler timerHandler; | |
| 62 | 64 | |
| 63 | 65 | @Override |
| 66 | + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true) | |
| 64 | 67 | public Long createSceneRule(IotSceneRuleSaveReqVO createReqVO) { |
| 65 | 68 | IotSceneRuleDO sceneRule = BeanUtils.toBean(createReqVO, IotSceneRuleDO.class); |
| 66 | 69 | sceneRuleMapper.insert(sceneRule); |
| 70 | + | |
| 71 | + // 注册定时触发器 | |
| 72 | + timerHandler.registerTimerTriggers(sceneRule); | |
| 73 | + | |
| 67 | 74 | return sceneRule.getId(); |
| 68 | 75 | } |
| 69 | 76 | |
| 70 | 77 | @Override |
| 78 | + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true) | |
| 71 | 79 | public void updateSceneRule(IotSceneRuleSaveReqVO updateReqVO) { |
| 72 | 80 | // 校验存在 |
| 73 | 81 | validateSceneRuleExists(updateReqVO.getId()); |
| 74 | 82 | // 更新 |
| 75 | 83 | IotSceneRuleDO updateObj = BeanUtils.toBean(updateReqVO, IotSceneRuleDO.class); |
| 76 | 84 | sceneRuleMapper.updateById(updateObj); |
| 85 | + | |
| 86 | + // 更新定时触发器 | |
| 87 | + timerHandler.updateTimerTriggers(updateObj); | |
| 77 | 88 | } |
| 78 | 89 | |
| 79 | 90 | @Override |
| 91 | + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true) | |
| 80 | 92 | public void updateSceneRuleStatus(Long id, Integer status) { |
| 81 | - // 校验存在 | |
| 93 | + // 1. 校验存在 | |
| 82 | 94 | validateSceneRuleExists(id); |
| 83 | - // 更新状态 | |
| 95 | + | |
| 96 | + // 2. 更新状态 | |
| 84 | 97 | IotSceneRuleDO updateObj = new IotSceneRuleDO().setId(id).setStatus(status); |
| 85 | 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 | 113 | @Override |
| 114 | + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true) | |
| 89 | 115 | public void deleteSceneRule(Long id) { |
| 90 | - // 校验存在 | |
| 116 | + // 1. 校验存在 | |
| 91 | 117 | validateSceneRuleExists(id); |
| 92 | - // 删除 | |
| 118 | + | |
| 119 | + // 2. 删除 | |
| 93 | 120 | sceneRuleMapper.deleteById(id); |
| 121 | + | |
| 122 | + // 3. 删除定时触发器 | |
| 123 | + timerHandler.unregisterTimerTriggers(id); | |
| 94 | 124 | } |
| 95 | 125 | |
| 96 | 126 | private void validateSceneRuleExists(Long id) { |
| ... | ... | @@ -126,16 +156,14 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { |
| 126 | 156 | return sceneRuleMapper.selectListByStatus(status); |
| 127 | 157 | } |
| 128 | 158 | |
| 129 | - // TODO 芋艿,缓存待实现 @puhui999 | |
| 130 | 159 | @Override |
| 160 | + @Cacheable(value = RedisKeyConstants.SCENE_RULE_LIST, key = "#productId + '_' + #deviceId ") | |
| 131 | 161 | @TenantIgnore // 忽略租户隔离:因为 IotSceneRuleMessageHandler 调用时,一般未传递租户,所以需要忽略 |
| 132 | 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 | 167 | return filterList(enabledList, sceneRule -> { |
| 140 | 168 | if (CollUtil.isEmpty(sceneRule.getTriggers())) { |
| 141 | 169 | return false; |
| ... | ... | @@ -144,21 +172,19 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { |
| 144 | 172 | for (IotSceneRuleDO.Trigger trigger : sceneRule.getTriggers()) { |
| 145 | 173 | // 检查触发器是否匹配指定的产品和设备 |
| 146 | 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 | 177 | return false; |
| 153 | 178 | } |
| 154 | 179 | // 检查是否是全部设备的特殊标识 |
| 155 | 180 | if (IotDeviceDO.DEVICE_ID_ALL.equals(trigger.getDeviceId())) { |
| 156 | - return true; // 匹配所有设备 | |
| 181 | + return true; | |
| 157 | 182 | } |
| 158 | 183 | // 检查具体设备 ID 是否匹配 |
| 159 | 184 | return ObjUtil.equal(productId, trigger.getProductId()) && ObjUtil.equal(deviceId, trigger.getDeviceId()); |
| 160 | 185 | } catch (Exception e) { |
| 161 | - log.warn("[isMatchProductAndDevice][产品({}) 设备({}) 匹配触发器异常]", productId, deviceId, e); | |
| 186 | + log.warn("[getSceneRuleListByProductIdAndDeviceIdFromCache][产品({}) 设备({}) 匹配触发器异常]", | |
| 187 | + productId, deviceId, e); | |
| 162 | 188 | return false; |
| 163 | 189 | } |
| 164 | 190 | } |
| ... | ... | @@ -168,9 +194,10 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { |
| 168 | 194 | |
| 169 | 195 | @Override |
| 170 | 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 | 201 | List<IotSceneRuleDO> sceneRules = getMatchedSceneRuleListByMessage(message); |
| 175 | 202 | if (CollUtil.isEmpty(sceneRules)) { |
| 176 | 203 | return; |
| ... | ... | @@ -214,16 +241,16 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { |
| 214 | 241 | */ |
| 215 | 242 | private List<IotSceneRuleDO> getMatchedSceneRuleListByMessage(IotDeviceMessage message) { |
| 216 | 243 | // 1. 匹配设备 |
| 217 | - // TODO @芋艿:可能需要 getSelf(); 缓存 @puhui999; | |
| 244 | + // TODO 缓存 @puhui999:可能需要 getSelf() | |
| 218 | 245 | // 1.1 通过 deviceId 获取设备信息 |
| 219 | - IotDeviceDO device = deviceService.getDeviceFromCache(message.getDeviceId()); | |
| 246 | + IotDeviceDO device = getSelf().deviceService.getDeviceFromCache(message.getDeviceId()); | |
| 220 | 247 | if (device == null) { |
| 221 | 248 | log.warn("[getMatchedSceneRuleListByMessage][设备({}) 不存在]", message.getDeviceId()); |
| 222 | 249 | return List.of(); |
| 223 | 250 | } |
| 224 | 251 | |
| 225 | 252 | // 1.2 通过 productId 获取产品信息 |
| 226 | - IotProductDO product = productService.getProductFromCache(device.getProductId()); | |
| 253 | + IotProductDO product = getSelf().productService.getProductFromCache(device.getProductId()); | |
| 227 | 254 | if (product == null) { |
| 228 | 255 | log.warn("[getMatchedSceneRuleListByMessage][产品({}) 不存在]", device.getProductId()); |
| 229 | 256 | return List.of(); |
| ... | ... | @@ -273,7 +300,6 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { |
| 273 | 300 | */ |
| 274 | 301 | private boolean matchSingleTrigger(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, IotSceneRuleDO sceneRule) { |
| 275 | 302 | try { |
| 276 | - // 2. 检查触发器的条件分组 | |
| 277 | 303 | return sceneRuleMatcherManager.isMatched(message, trigger) && isTriggerConditionGroupsMatched(message, trigger, sceneRule); |
| 278 | 304 | } catch (Exception e) { |
| 279 | 305 | log.error("[matchSingleTrigger][触发器匹配异常] sceneRuleId: {}, triggerType: {}, message: {}", |
| ... | ... | @@ -290,18 +316,19 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { |
| 290 | 316 | * @param sceneRule 场景规则(用于日志) |
| 291 | 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 | 323 | if (CollUtil.isEmpty(trigger.getConditionGroups())) { |
| 296 | 324 | return true; |
| 297 | 325 | } |
| 298 | 326 | |
| 299 | - // 检查条件分组:分组与分组之间是"或"的关系,条件与条件之间是"且"的关系 | |
| 327 | + // 2. 检查条件分组:分组与分组之间是"或"的关系,条件与条件之间是"且"的关系 | |
| 300 | 328 | for (List<IotSceneRuleDO.TriggerCondition> conditionGroup : trigger.getConditionGroups()) { |
| 301 | 329 | if (CollUtil.isEmpty(conditionGroup)) { |
| 302 | 330 | continue; |
| 303 | 331 | } |
| 304 | - | |
| 305 | 332 | // 检查当前分组中的所有条件是否都匹配(且关系) |
| 306 | 333 | boolean allConditionsMatched = true; |
| 307 | 334 | for (IotSceneRuleDO.TriggerCondition condition : conditionGroup) { |
| ... | ... | @@ -310,14 +337,13 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { |
| 310 | 337 | break; |
| 311 | 338 | } |
| 312 | 339 | } |
| 313 | - | |
| 314 | 340 | // 如果当前分组的所有条件都匹配,则整个触发器匹配成功 |
| 315 | 341 | if (allConditionsMatched) { |
| 316 | 342 | return true; |
| 317 | 343 | } |
| 318 | 344 | } |
| 319 | 345 | |
| 320 | - // 所有分组都不匹配 | |
| 346 | + // 3. 所有分组都不匹配 | |
| 321 | 347 | return false; |
| 322 | 348 | } |
| 323 | 349 | |
| ... | ... | @@ -352,13 +378,13 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { |
| 352 | 378 | sceneRules.forEach(sceneRule -> { |
| 353 | 379 | // 2. 遍历规则场景的动作 |
| 354 | 380 | sceneRule.getActions().forEach(actionConfig -> { |
| 355 | - // 3.1 获取对应的动作 Action 数组 | |
| 381 | + // 2.1 获取对应的动作 Action 数组 | |
| 356 | 382 | List<IotSceneRuleAction> actions = filterList(sceneRuleActions, |
| 357 | 383 | action -> action.getType().getType().equals(actionConfig.getType())); |
| 358 | 384 | if (CollUtil.isEmpty(actions)) { |
| 359 | 385 | return; |
| 360 | 386 | } |
| 361 | - // 3.2 执行动作 | |
| 387 | + // 2.2 执行动作 | |
| 362 | 388 | actions.forEach(action -> { |
| 363 | 389 | try { |
| 364 | 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
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleAction.java
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceControlSceneRuleAction.java
| 1 | 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 | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; | |
| 4 | 8 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; |
| 5 | 9 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum; |
| 6 | 10 | import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; |
| ... | ... | @@ -9,8 +13,11 @@ import jakarta.annotation.Resource; |
| 9 | 13 | import lombok.extern.slf4j.Slf4j; |
| 10 | 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 | 22 | * @author 芋道源码 |
| 16 | 23 | */ |
| ... | ... | @@ -23,28 +30,108 @@ public class IotDeviceControlSceneRuleAction implements IotSceneRuleAction { |
| 23 | 30 | @Resource |
| 24 | 31 | private IotDeviceMessageService deviceMessageService; |
| 25 | 32 | |
| 26 | - // TODO @puhui999:这里 | |
| 27 | 33 | @Override |
| 28 | 34 | public void execute(IotDeviceMessage message, |
| 29 | 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 | 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 | 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 | 9 | * - {@link IotSceneRuleTriggerMatcher} 触发器匹配器 |
| 12 | 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 | 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 | 25 | * @author HUIHUI |
| ... | ... | @@ -91,7 +89,7 @@ public final class IotSceneRuleMatcherHelper { |
| 91 | 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 | 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 | 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 | 21 | * @author HUIHUI |
| 24 | 22 | */ |
| ... | ... | @@ -44,13 +42,12 @@ public class IotSceneRuleMatcherManager { |
| 44 | 42 | return; |
| 45 | 43 | } |
| 46 | 44 | |
| 47 | - // 按优先级排序并过滤启用的匹配器 | |
| 45 | + // 1.1 按优先级排序并过滤启用的匹配器 | |
| 48 | 46 | List<IotSceneRuleMatcher> allMatchers = matchers.stream() |
| 49 | 47 | .filter(IotSceneRuleMatcher::isEnabled) |
| 50 | 48 | .sorted(Comparator.comparing(IotSceneRuleMatcher::getPriority)) |
| 51 | 49 | .toList(); |
| 52 | - | |
| 53 | - // 分离触发器匹配器和条件匹配器 | |
| 50 | + // 1.2 分离触发器匹配器和条件匹配器 | |
| 54 | 51 | List<IotSceneRuleTriggerMatcher> triggerMatchers = allMatchers.stream() |
| 55 | 52 | .filter(matcher -> matcher instanceof IotSceneRuleTriggerMatcher) |
| 56 | 53 | .map(matcher -> (IotSceneRuleTriggerMatcher) matcher) |
| ... | ... | @@ -60,7 +57,7 @@ public class IotSceneRuleMatcherManager { |
| 60 | 57 | .map(matcher -> (IotSceneRuleConditionMatcher) matcher) |
| 61 | 58 | .toList(); |
| 62 | 59 | |
| 63 | - // 构建触发器匹配器映射表 | |
| 60 | + // 2.1 构建触发器匹配器映射表 | |
| 64 | 61 | this.triggerMatchers = convertMap(triggerMatchers, IotSceneRuleTriggerMatcher::getSupportedTriggerType, |
| 65 | 62 | Function.identity(), |
| 66 | 63 | (existing, replacement) -> { |
| ... | ... | @@ -70,7 +67,7 @@ public class IotSceneRuleMatcherManager { |
| 70 | 67 | existing.getSupportedTriggerType() : replacement.getSupportedTriggerType()); |
| 71 | 68 | return existing.getPriority() <= replacement.getPriority() ? existing : replacement; |
| 72 | 69 | }, LinkedHashMap::new); |
| 73 | - // 构建条件匹配器映射表 | |
| 70 | + // 2.2 构建条件匹配器映射表 | |
| 74 | 71 | this.conditionMatchers = convertMap(conditionMatchers, IotSceneRuleConditionMatcher::getSupportedConditionType, |
| 75 | 72 | Function.identity(), |
| 76 | 73 | (existing, replacement) -> { |
| ... | ... | @@ -82,7 +79,7 @@ public class IotSceneRuleMatcherManager { |
| 82 | 79 | }, |
| 83 | 80 | LinkedHashMap::new); |
| 84 | 81 | |
| 85 | - // 日志输出初始化信息 | |
| 82 | + // 3. 日志输出初始化信息 | |
| 86 | 83 | log.info("[IotSceneRuleMatcherManager][初始化完成,共加载({})个匹配器,其中触发器匹配器({})个,条件匹配器({})个]", |
| 87 | 84 | allMatchers.size(), this.triggerMatchers.size(), this.conditionMatchers.size()); |
| 88 | 85 | this.triggerMatchers.forEach((type, matcher) -> |
| ... | ... | @@ -135,7 +132,7 @@ public class IotSceneRuleMatcherManager { |
| 135 | 132 | return false; |
| 136 | 133 | } |
| 137 | 134 | |
| 138 | - // 根据条件类型查找对应的匹配器 | |
| 135 | + // 1. 根据条件类型查找对应的匹配器 | |
| 139 | 136 | IotSceneRuleConditionTypeEnum conditionType = IotSceneRuleConditionTypeEnum.typeOf(condition.getType()); |
| 140 | 137 | if (conditionType == null) { |
| 141 | 138 | log.warn("[isConditionMatched][conditionType({}) 未知的条件类型]", condition.getType()); |
| ... | ... | @@ -147,7 +144,7 @@ public class IotSceneRuleMatcherManager { |
| 147 | 144 | return false; |
| 148 | 145 | } |
| 149 | 146 | |
| 150 | - // 执行匹配逻辑 | |
| 147 | + // 2. 执行匹配逻辑 | |
| 151 | 148 | try { |
| 152 | 149 | return matcher.matches(message, condition); |
| 153 | 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 | 17 | import java.util.List; |
| 18 | 18 | |
| 19 | 19 | /** |
| 20 | - * 当前时间条件匹配器 | |
| 21 | - * <p> | |
| 22 | - * 处理时间相关的子条件匹配逻辑 | |
| 20 | + * 当前时间条件匹配器:处理时间相关的子条件匹配逻辑 | |
| 23 | 21 | * |
| 24 | 22 | * @author HUIHUI |
| 25 | 23 | */ |
| 26 | 24 | @Component |
| 27 | 25 | @Slf4j |
| 28 | -public class CurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher { | |
| 26 | +public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher { | |
| 29 | 27 | |
| 30 | 28 | /** |
| 31 | 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 | 1 | package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; |
| 2 | 2 | |
| 3 | + | |
| 3 | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 4 | 5 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 5 | 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 | 8 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; |
| 8 | 9 | import org.springframework.stereotype.Component; |
| 9 | 10 | |
| 11 | + | |
| 10 | 12 | /** |
| 11 | - * 设备属性条件匹配器 | |
| 12 | - * <p> | |
| 13 | - * 处理设备属性相关的子条件匹配逻辑 | |
| 13 | + * 设备属性条件匹配器:处理设备属性相关的子条件匹配逻辑 | |
| 14 | 14 | * |
| 15 | 15 | * @author HUIHUI |
| 16 | 16 | */ |
| 17 | 17 | @Component |
| 18 | -public class DevicePropertyConditionMatcher implements IotSceneRuleConditionMatcher { | |
| 18 | +public class IotDevicePropertyConditionMatcher implements IotSceneRuleConditionMatcher { | |
| 19 | 19 | |
| 20 | 20 | @Override |
| 21 | 21 | public IotSceneRuleConditionTypeEnum getSupportedConditionType() { |
| ... | ... | @@ -43,10 +43,10 @@ public class DevicePropertyConditionMatcher implements IotSceneRuleConditionMatc |
| 43 | 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 | 48 | if (propertyValue == null) { |
| 49 | - IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中属性值为空"); | |
| 49 | + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中属性值为空或未找到指定属性"); | |
| 50 | 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 | 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.util.IotDeviceMessageUtils; | |
| 4 | 5 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; |
| 5 | 6 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; |
| 6 | 7 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; |
| 7 | 8 | import org.springframework.stereotype.Component; |
| 8 | 9 | |
| 9 | 10 | /** |
| 10 | - * 设备状态条件匹配器 | |
| 11 | - * <p> | |
| 12 | - * 处理设备状态相关的子条件匹配逻辑 | |
| 11 | + * 设备状态条件匹配器:处理设备状态相关的子条件匹配逻辑 | |
| 13 | 12 | * |
| 14 | 13 | * @author HUIHUI |
| 15 | 14 | */ |
| 16 | 15 | @Component |
| 17 | -public class DeviceStateConditionMatcher implements IotSceneRuleConditionMatcher { | |
| 16 | +public class IotDeviceStateConditionMatcher implements IotSceneRuleConditionMatcher { | |
| 18 | 17 | |
| 19 | 18 | @Override |
| 20 | 19 | public IotSceneRuleConditionTypeEnum getSupportedConditionType() { |
| ... | ... | @@ -35,8 +34,9 @@ public class DeviceStateConditionMatcher implements IotSceneRuleConditionMatcher |
| 35 | 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 | 40 | if (stateValue == null) { |
| 41 | 41 | IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中设备状态值为空"); |
| 42 | 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 | 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 | 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 | 10 | import org.springframework.stereotype.Component; |
| 11 | 11 | |
| 12 | 12 | /** |
| 13 | - * 设备事件上报触发器匹配器 | |
| 14 | - * <p> | |
| 15 | - * 处理设备事件上报的触发器匹配逻辑 | |
| 13 | + * 设备事件上报触发器匹配器:处理设备事件上报的触发器匹配逻辑 | |
| 16 | 14 | * |
| 17 | 15 | * @author HUIHUI |
| 18 | 16 | */ |
| 19 | 17 | @Component |
| 20 | -public class DeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatcher { | |
| 18 | +public class IotDeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatcher { | |
| 21 | 19 | |
| 22 | 20 | @Override |
| 23 | 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 | 9 | import org.springframework.stereotype.Component; |
| 10 | 10 | |
| 11 | 11 | /** |
| 12 | - * 设备属性上报触发器匹配器 | |
| 13 | - * <p> | |
| 14 | - * 处理设备属性数据上报的触发器匹配逻辑 | |
| 12 | + * 设备属性上报触发器匹配器:处理设备属性数据上报的触发器匹配逻辑 | |
| 15 | 13 | * |
| 16 | 14 | * @author HUIHUI |
| 17 | 15 | */ |
| 18 | 16 | @Component |
| 19 | -public class DevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatcher { | |
| 17 | +public class IotDevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatcher { | |
| 20 | 18 | |
| 21 | 19 | @Override |
| 22 | 20 | public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { |
| ... | ... | @@ -52,10 +50,10 @@ public class DevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatc |
| 52 | 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 | 55 | if (propertyValue == null) { |
| 58 | - IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中属性值为空"); | |
| 56 | + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中属性值为空或未找到指定属性"); | |
| 59 | 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 | 9 | import org.springframework.stereotype.Component; |
| 10 | 10 | |
| 11 | 11 | /** |
| 12 | - * 设备服务调用触发器匹配器 | |
| 13 | - * <p> | |
| 14 | - * 处理设备服务调用的触发器匹配逻辑 | |
| 12 | + * 设备服务调用触发器匹配器:处理设备服务调用的触发器匹配逻辑 | |
| 15 | 13 | * |
| 16 | 14 | * @author HUIHUI |
| 17 | 15 | */ |
| 18 | 16 | @Component |
| 19 | -public class DeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMatcher { | |
| 17 | +public class IotDeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMatcher { | |
| 20 | 18 | |
| 21 | 19 | @Override |
| 22 | 20 | public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { |
| ... | ... | @@ -46,7 +44,7 @@ public class DeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMat |
| 46 | 44 | |
| 47 | 45 | // 2. 对于服务调用触发器,通常只需要匹配服务标识符即可 |
| 48 | 46 | // 不需要检查操作符和值,因为服务调用本身就是触发条件 |
| 49 | - // TODO @puhui999: 服务调用时校验输入参数是否匹配条件 | |
| 47 | + // TODO @puhui999: 服务调用时校验输入参数是否匹配条件? | |
| 50 | 48 | IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger); |
| 51 | 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 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | |
| 5 | 6 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; |
| 6 | 7 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; |
| 7 | 8 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; |
| 8 | 9 | import org.springframework.stereotype.Component; |
| 9 | 10 | |
| 10 | 11 | /** |
| 11 | - * 设备状态更新触发器匹配器 | |
| 12 | - * <p> | |
| 13 | - * 处理设备上下线状态变更的触发器匹配逻辑 | |
| 12 | + * 设备状态更新触发器匹配器:处理设备上下线状态变更的触发器匹配逻辑 | |
| 14 | 13 | * |
| 15 | 14 | * @author HUIHUI |
| 16 | 15 | */ |
| 17 | 16 | @Component |
| 18 | -public class DeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatcher { | |
| 17 | +public class IotDeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatcher { | |
| 19 | 18 | |
| 20 | 19 | @Override |
| 21 | 20 | public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { |
| ... | ... | @@ -43,16 +42,17 @@ public class DeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatch |
| 43 | 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 | 49 | IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中设备状态值为空"); |
| 50 | 50 | return false; |
| 51 | 51 | } |
| 52 | 52 | |
| 53 | 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 | 56 | if (matched) { |
| 57 | 57 | IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger); |
| 58 | 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 | 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 | 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 | 9 | import org.springframework.stereotype.Component; |
| 10 | 10 | |
| 11 | 11 | /** |
| 12 | - * 定时触发器匹配器 | |
| 13 | - * <p> | |
| 14 | - * 处理定时触发的触发器匹配逻辑 | |
| 12 | + * 定时触发器匹配器:处理定时触发的触发器匹配逻辑 | |
| 13 | + * | |
| 15 | 14 | * 注意:定时触发器不依赖设备消息,主要用于定时任务场景 |
| 16 | 15 | * |
| 17 | 16 | * @author HUIHUI |
| 18 | 17 | */ |
| 19 | 18 | @Component |
| 20 | -public class TimerTriggerMatcher implements IotSceneRuleTriggerMatcher { | |
| 19 | +public class IotTimerTriggerMatcher implements IotSceneRuleTriggerMatcher { | |
| 21 | 20 | |
| 22 | 21 | @Override |
| 23 | 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 | 72 | FROM device_property_${reqVO.deviceId} |
| 73 | 73 | WHERE ${@cn.hutool.core.util.StrUtil@toUnderlineCase(reqVO.identifier)} IS NOT NULL |
| 74 | 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 | 76 | ORDER BY ts DESC |
| 77 | 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 | 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 | 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 | 3 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | 4 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; |
| 6 | 5 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; |
| 7 | 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 | 9 | import org.junit.jupiter.api.Test; |
| 9 | -import org.mockito.InjectMocks; | |
| 10 | 10 | |
| 11 | 11 | import java.time.LocalDateTime; |
| 12 | 12 | import java.time.ZoneOffset; |
| ... | ... | @@ -16,14 +16,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString |
| 16 | 16 | import static org.junit.jupiter.api.Assertions.*; |
| 17 | 17 | |
| 18 | 18 | /** |
| 19 | - * {@link CurrentTimeConditionMatcher} 的单元测试 | |
| 19 | + * {@link IotCurrentTimeConditionMatcher} 的单元测试 | |
| 20 | 20 | * |
| 21 | 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 | 32 | @Test |
| 29 | 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 | 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 | 3 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 6 | 4 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; |
| 7 | 5 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; |
| 8 | 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 | 9 | import org.junit.jupiter.api.Test; |
| 10 | -import org.mockito.InjectMocks; | |
| 11 | 10 | |
| 12 | 11 | import java.util.HashMap; |
| 13 | 12 | import java.util.Map; |
| 14 | 13 | |
| 15 | 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 | 15 | import static org.junit.jupiter.api.Assertions.*; |
| 18 | 16 | |
| 19 | 17 | /** |
| 20 | - * {@link DevicePropertyConditionMatcher} 的单元测试 | |
| 18 | + * {@link IotDevicePropertyConditionMatcher} 的单元测试 | |
| 21 | 19 | * |
| 22 | 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 | 31 | @Test |
| 30 | 32 | public void testGetSupportedConditionType() { |
| ... | ... | @@ -41,27 +43,17 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { |
| 41 | 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 | 49 | @Test |
| 57 | 50 | public void testMatches_temperatureEquals_success() { |
| 58 | - // 准备参数 | |
| 59 | - String propertyName = "temperature"; | |
| 51 | + // 准备参数:创建属性上报消息 | |
| 52 | + String propertyIdentifier = "temperature"; | |
| 60 | 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 | 55 | IotSceneRuleDO.TriggerCondition condition = createValidCondition( |
| 64 | - propertyName, | |
| 56 | + propertyIdentifier, | |
| 65 | 57 | IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), |
| 66 | 58 | String.valueOf(propertyValue) |
| 67 | 59 | ); |
| ... | ... | @@ -75,14 +67,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { |
| 75 | 67 | |
| 76 | 68 | @Test |
| 77 | 69 | public void testMatches_humidityGreaterThan_success() { |
| 78 | - // 准备参数 | |
| 79 | - String propertyName = "humidity"; | |
| 70 | + // 准备参数:创建属性上报消息 | |
| 71 | + String propertyIdentifier = "humidity"; | |
| 80 | 72 | Integer propertyValue = 75; |
| 81 | 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 | 75 | IotSceneRuleDO.TriggerCondition condition = createValidCondition( |
| 85 | - propertyName, | |
| 76 | + propertyIdentifier, | |
| 86 | 77 | IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), |
| 87 | 78 | String.valueOf(compareValue) |
| 88 | 79 | ); |
| ... | ... | @@ -96,14 +87,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { |
| 96 | 87 | |
| 97 | 88 | @Test |
| 98 | 89 | public void testMatches_pressureLessThan_success() { |
| 99 | - // 准备参数 | |
| 100 | - String propertyName = "pressure"; | |
| 90 | + // 准备参数:创建属性上报消息 | |
| 91 | + String propertyIdentifier = "pressure"; | |
| 101 | 92 | Double propertyValue = 1010.5; |
| 102 | 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 | 95 | IotSceneRuleDO.TriggerCondition condition = createValidCondition( |
| 106 | - propertyName, | |
| 96 | + propertyIdentifier, | |
| 107 | 97 | IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(), |
| 108 | 98 | String.valueOf(compareValue) |
| 109 | 99 | ); |
| ... | ... | @@ -117,14 +107,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { |
| 117 | 107 | |
| 118 | 108 | @Test |
| 119 | 109 | public void testMatches_statusNotEquals_success() { |
| 120 | - // 准备参数 | |
| 121 | - String propertyName = "status"; | |
| 110 | + // 准备参数:创建属性上报消息 | |
| 111 | + String propertyIdentifier = "status"; | |
| 122 | 112 | String propertyValue = "active"; |
| 123 | 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 | 115 | IotSceneRuleDO.TriggerCondition condition = createValidCondition( |
| 127 | - propertyName, | |
| 116 | + propertyIdentifier, | |
| 128 | 117 | IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator(), |
| 129 | 118 | compareValue |
| 130 | 119 | ); |
| ... | ... | @@ -138,14 +127,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { |
| 138 | 127 | |
| 139 | 128 | @Test |
| 140 | 129 | public void testMatches_propertyMismatch_fail() { |
| 141 | - // 准备参数 | |
| 142 | - String propertyName = "temperature"; | |
| 130 | + // 准备参数:创建属性上报消息,值不满足条件 | |
| 131 | + String propertyIdentifier = "temperature"; | |
| 143 | 132 | Double propertyValue = 15.0; |
| 144 | 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 | 135 | IotSceneRuleDO.TriggerCondition condition = createValidCondition( |
| 148 | - propertyName, | |
| 136 | + propertyIdentifier, | |
| 149 | 137 | IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), |
| 150 | 138 | String.valueOf(compareValue) |
| 151 | 139 | ); |
| ... | ... | @@ -158,14 +146,16 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { |
| 158 | 146 | } |
| 159 | 147 | |
| 160 | 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 | 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 | 168 | @Test |
| 179 | 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 | 174 | boolean result = matcher.matches(message, null); |
| ... | ... | @@ -191,8 +180,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { |
| 191 | 180 | @Test |
| 192 | 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 | 184 | IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); |
| 197 | 185 | condition.setType(null); |
| 198 | 186 | |
| ... | ... | @@ -206,8 +194,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { |
| 206 | 194 | @Test |
| 207 | 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 | 198 | IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); |
| 212 | 199 | condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType()); |
| 213 | 200 | condition.setIdentifier(null); // 缺少标识符 |
| ... | ... | @@ -224,8 +211,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { |
| 224 | 211 | @Test |
| 225 | 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 | 215 | IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); |
| 230 | 216 | condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType()); |
| 231 | 217 | condition.setIdentifier("temperature"); |
| ... | ... | @@ -242,8 +228,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { |
| 242 | 228 | @Test |
| 243 | 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 | 232 | IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); |
| 248 | 233 | condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType()); |
| 249 | 234 | condition.setIdentifier("temperature"); |
| ... | ... | @@ -275,7 +260,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { |
| 275 | 260 | |
| 276 | 261 | @Test |
| 277 | 262 | public void testMatches_nullDeviceProperties_fail() { |
| 278 | - // 准备参数 | |
| 263 | + // 准备参数:消息的 params 为 null | |
| 279 | 264 | IotDeviceMessage message = new IotDeviceMessage(); |
| 280 | 265 | message.setParams(null); |
| 281 | 266 | IotSceneRuleDO.TriggerCondition condition = createValidCondition( |
| ... | ... | @@ -292,14 +277,79 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { |
| 292 | 277 | } |
| 293 | 278 | |
| 294 | 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 | 346 | public void testMatches_voltageGreaterThanOrEquals_success() { |
| 296 | - // 准备参数 | |
| 297 | - String propertyName = "voltage"; | |
| 347 | + // 准备参数:创建属性上报消息 | |
| 348 | + String propertyIdentifier = "voltage"; | |
| 298 | 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 | 351 | IotSceneRuleDO.TriggerCondition condition = createValidCondition( |
| 302 | - propertyName, | |
| 352 | + propertyIdentifier, | |
| 303 | 353 | IotSceneRuleConditionOperatorEnum.GREATER_THAN_OR_EQUALS.getOperator(), |
| 304 | 354 | String.valueOf(propertyValue) |
| 305 | 355 | ); |
| ... | ... | @@ -313,14 +363,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { |
| 313 | 363 | |
| 314 | 364 | @Test |
| 315 | 365 | public void testMatches_currentLessThanOrEquals_success() { |
| 316 | - // 准备参数 | |
| 317 | - String propertyName = "current"; | |
| 366 | + // 准备参数:创建属性上报消息 | |
| 367 | + String propertyIdentifier = "current"; | |
| 318 | 368 | Double propertyValue = 2.5; |
| 319 | 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 | 371 | IotSceneRuleDO.TriggerCondition condition = createValidCondition( |
| 323 | - propertyName, | |
| 372 | + propertyIdentifier, | |
| 324 | 373 | IotSceneRuleConditionOperatorEnum.LESS_THAN_OR_EQUALS.getOperator(), |
| 325 | 374 | String.valueOf(compareValue) |
| 326 | 375 | ); |
| ... | ... | @@ -334,13 +383,12 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { |
| 334 | 383 | |
| 335 | 384 | @Test |
| 336 | 385 | public void testMatches_stringProperty_success() { |
| 337 | - // 准备参数 | |
| 338 | - String propertyName = "mode"; | |
| 386 | + // 准备参数:创建属性上报消息 | |
| 387 | + String propertyIdentifier = "mode"; | |
| 339 | 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 | 390 | IotSceneRuleDO.TriggerCondition condition = createValidCondition( |
| 343 | - propertyName, | |
| 391 | + propertyIdentifier, | |
| 344 | 392 | IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), |
| 345 | 393 | propertyValue |
| 346 | 394 | ); |
| ... | ... | @@ -354,13 +402,12 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { |
| 354 | 402 | |
| 355 | 403 | @Test |
| 356 | 404 | public void testMatches_booleanProperty_success() { |
| 357 | - // 准备参数 | |
| 358 | - String propertyName = "enabled"; | |
| 405 | + // 准备参数:创建属性上报消息 | |
| 406 | + String propertyIdentifier = "enabled"; | |
| 359 | 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 | 409 | IotSceneRuleDO.TriggerCondition condition = createValidCondition( |
| 363 | - propertyName, | |
| 410 | + propertyIdentifier, | |
| 364 | 411 | IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), |
| 365 | 412 | String.valueOf(propertyValue) |
| 366 | 413 | ); |
| ... | ... | @@ -372,40 +419,61 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { |
| 372 | 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 | 471 | IotDeviceMessage message = new IotDeviceMessage(); |
| 407 | 472 | message.setDeviceId(randomLongId()); |
| 408 | - message.setParams(properties); | |
| 473 | + message.setMethod("thing.property.post"); | |
| 474 | + // 直接将属性值作为 params | |
| 475 | + message.setParams(value); | |
| 476 | + | |
| 409 | 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 | 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 | 3 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; |
| 5 | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 6 | 5 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; |
| 7 | 6 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; |
| 8 | 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 | 10 | import org.junit.jupiter.api.Test; |
| 10 | -import org.mockito.InjectMocks; | |
| 11 | 11 | |
| 12 | 12 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; |
| 13 | 13 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; |
| 14 | 14 | import static org.junit.jupiter.api.Assertions.*; |
| 15 | 15 | |
| 16 | 16 | /** |
| 17 | - * {@link DeviceStateConditionMatcher} 的单元测试 | |
| 17 | + * {@link IotDeviceStateConditionMatcher} 的单元测试 | |
| 18 | 18 | * |
| 19 | 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 | 30 | @Test |
| 27 | 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 | 1 | package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.map.MapUtil; |
| 4 | -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; | |
| 5 | 4 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 6 | 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | 6 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; |
| 8 | 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 | 10 | import org.junit.jupiter.api.Test; |
| 10 | -import org.mockito.InjectMocks; | |
| 11 | 11 | |
| 12 | 12 | import java.util.HashMap; |
| 13 | 13 | import java.util.Map; |
| ... | ... | @@ -18,14 +18,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString |
| 18 | 18 | import static org.junit.jupiter.api.Assertions.*; |
| 19 | 19 | |
| 20 | 20 | /** |
| 21 | - * {@link DeviceEventPostTriggerMatcher} 的单元测试 | |
| 21 | + * {@link IotDeviceEventPostTriggerMatcher} 的单元测试 | |
| 22 | 22 | * |
| 23 | 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 | 34 | @Test |
| 31 | 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 | 1 | package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.map.MapUtil; |
| 4 | -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; | |
| 5 | 4 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 6 | 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | 6 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; |
| 8 | 7 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; |
| 9 | 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 | 11 | import org.junit.jupiter.api.Test; |
| 11 | -import org.mockito.InjectMocks; | |
| 12 | 12 | |
| 13 | 13 | import java.util.HashMap; |
| 14 | 14 | import java.util.Map; |
| ... | ... | @@ -20,14 +20,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString |
| 20 | 20 | import static org.junit.jupiter.api.Assertions.*; |
| 21 | 21 | |
| 22 | 22 | /** |
| 23 | - * {@link DevicePropertyPostTriggerMatcher} 的单元测试 | |
| 23 | + * {@link IotDevicePropertyPostTriggerMatcher} 的单元测试 | |
| 24 | 24 | * |
| 25 | 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 | 36 | @Test |
| 33 | 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 | 1 | package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.map.MapUtil; |
| 4 | -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; | |
| 5 | 4 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 6 | 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | 6 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; |
| 8 | 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 | 10 | import org.junit.jupiter.api.Test; |
| 10 | -import org.mockito.InjectMocks; | |
| 11 | 11 | |
| 12 | 12 | import java.util.HashMap; |
| 13 | 13 | import java.util.Map; |
| ... | ... | @@ -18,14 +18,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString |
| 18 | 18 | import static org.junit.jupiter.api.Assertions.*; |
| 19 | 19 | |
| 20 | 20 | /** |
| 21 | - * {@link DeviceServiceInvokeTriggerMatcher} 的单元测试 | |
| 21 | + * {@link IotDeviceServiceInvokeTriggerMatcher} 的单元测试 | |
| 22 | 22 | * |
| 23 | 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 | 34 | @Test |
| 31 | 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 | 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 | 3 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 5 | 4 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; |
| 6 | 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | 6 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; |
| 8 | 7 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; |
| 9 | 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 | 11 | import org.junit.jupiter.api.Test; |
| 11 | -import org.mockito.InjectMocks; | |
| 12 | 12 | |
| 13 | 13 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; |
| 14 | 14 | import static org.junit.jupiter.api.Assertions.*; |
| 15 | 15 | |
| 16 | 16 | /** |
| 17 | - * {@link DeviceStateUpdateTriggerMatcher} 的单元测试 | |
| 17 | + * {@link IotDeviceStateUpdateTriggerMatcher} 的单元测试 | |
| 18 | 18 | * |
| 19 | 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 | 30 | @Test |
| 27 | 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 | 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 | 3 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 5 | 4 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; |
| 6 | 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 | 8 | import org.junit.jupiter.api.Test; |
| 8 | -import org.mockito.InjectMocks; | |
| 9 | 9 | |
| 10 | 10 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; |
| 11 | 11 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; |
| 12 | 12 | import static org.junit.jupiter.api.Assertions.*; |
| 13 | 13 | |
| 14 | 14 | /** |
| 15 | - * {@link TimerTriggerMatcher} 的单元测试 | |
| 15 | + * {@link IotTimerTriggerMatcher} 的单元测试 | |
| 16 | 16 | * |
| 17 | 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 | 28 | @Test |
| 25 | 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<String> { |
| 43 | 43 | // ========== 设备配置 ========== |
| 44 | 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 | 48 | // ========== OTA 固件 ========== |
| 49 | 49 | // 可参考:https://help.aliyun.com/zh/iot/user-guide/perform-ota-updates | ... | ... |