# Conflicts:
#	yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java
#	yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCancelReqVO.java
#	yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCreateReqVO.java
#	yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceImpl.java
#	yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceControlSceneRuleAction.java
#	yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherManager.java
#	yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java
This commit is contained in:
YunaiV
2025-10-02 17:56:24 +08:00
112 changed files with 3287 additions and 1433 deletions

View File

@@ -165,14 +165,6 @@ public class YudaoTenantAutoConfiguration {
return ignoreUrls;
}
// ========== Job ==========
@Bean
@ConditionalOnClass(name = "com.xxl.job.core.handler.annotation.XxlJob")
public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) {
return new TenantJobAspect(tenantFrameworkService);
}
// ========== MQ ==========
/**
@@ -203,6 +195,14 @@ public class YudaoTenantAutoConfiguration {
return new TenantRocketMQInitializer();
}
// ========== Job ==========
@Bean
@ConditionalOnClass(name = "com.xxl.job.core.handler.annotation.XxlJob")
public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) {
return new TenantJobAspect(tenantFrameworkService);
}
// ========== Redis ==========
@Bean

View File

@@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.mybatis.core.handler.DefaultDBFieldHandler;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import com.baomidou.mybatisplus.core.handlers.IJsonTypeHandler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
@@ -80,8 +81,8 @@ public class YudaoMybatisAutoConfiguration {
throw new IllegalArgumentException(StrUtil.format("DbType{} 找不到合适的 IKeyGenerator 实现类", dbType));
}
@Bean
public JacksonTypeHandler jacksonTypeHandler(List<ObjectMapper> objectMappers) {
@Bean // 特殊:返回结果使用 Object 而不用 JacksonTypeHandler 的原因,避免因为 JacksonTypeHandler 被 mybatis 全局使用!
public Object jacksonTypeHandler(List<ObjectMapper> objectMappers) {
// 特殊:设置 JacksonTypeHandler 的 ObjectMapper
ObjectMapper objectMapper = CollUtil.getFirst(objectMappers);
if (objectMapper == null) {

View File

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.annotation.TableLogic;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fhs.core.trans.vo.TransPojo;
import lombok.Data;
import org.apache.ibatis.type.JdbcType;
import java.io.Serializable;
import java.time.LocalDateTime;
@@ -37,14 +38,14 @@ public abstract class BaseDO implements Serializable, TransPojo {
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
@TableField(fill = FieldFill.INSERT)
@TableField(fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR)
private String creator;
/**
* 更新者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.VARCHAR)
private String updater;
/**
* 是否删除

View File

@@ -60,6 +60,12 @@ public enum DbTypeEnum {
* 人大金仓
*/
KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION('#{value}' IN #{column}) <> 0"),
/**
* OceanBase
*/
OCEAN_BASE(DbType.OCEAN_BASE, "OceanBase", "FIND_IN_SET('#{value}', #{column}) <> 0")
;
public static final Map<String, DbTypeEnum> MAP_BY_NAME = Arrays.stream(values())

View File

@@ -1,5 +1,6 @@
package cn.iocoder.yudao.framework.swagger.config;
import com.github.xiaoymin.knife4j.spring.configuration.Knife4jAutoConfiguration;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
@@ -12,6 +13,7 @@ import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springdoc.core.*;
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
import org.springdoc.core.providers.JavadocProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
@@ -39,7 +41,7 @@ import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_
*
* @author 芋道源码
*/
@AutoConfiguration
@AutoConfiguration(before = Knife4jAutoConfiguration.class) // before 原因,保证覆写的 Knife4jOpenApiCustomizer 先生效!相关 https://github.com/YunaiV/ruoyi-vue-pro/issues/954 讨论
@ConditionalOnClass({OpenAPI.class})
@EnableConfigurationProperties(SwaggerProperties.class)
@ConditionalOnProperty(prefix = "springdoc.api-docs", name = "enabled", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用
@@ -98,7 +100,6 @@ public class YudaoSwaggerAutoConfiguration {
Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomizers,
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers,
Optional<JavadocProvider> javadocProvider) {
return new OpenAPIService(openAPI, securityParser, springDocConfigProperties,
propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider);
}
@@ -124,6 +125,7 @@ public class YudaoSwaggerAutoConfiguration {
.addOperationCustomizer((operation, handlerMethod) -> operation
.addParametersItem(buildTenantHeaderParameter())
.addParametersItem(buildSecurityHeaderParameter()))
.addOperationCustomizer(buildOperationIdCustomizer())
.build();
}
@@ -155,5 +157,26 @@ public class YudaoSwaggerAutoConfiguration {
.schema(new StringSchema()._default("Bearer test1").name(HEADER_TENANT_ID).description("认证 Token")); // 默认:使用用户编号为 1
}
/**
* 核心自定义OperationId生成规则组合「类名前缀 + 方法名」
*
* @see <a href="https://github.com/YunaiV/ruoyi-vue-pro/issues/957">app-api 前缀不生效,都是使用 admin-api</a>
*/
private static OperationCustomizer buildOperationIdCustomizer() {
return (operation, handlerMethod) -> {
// 1. 获取控制器类名(如 UserController
String className = handlerMethod.getBeanType().getSimpleName();
// 2. 提取类名前缀(去除 Controller 后缀,如 UserController -> User
String classPrefix = className.replaceAll("Controller$", "");
// 3. 获取方法名(如 list
String methodName = handlerMethod.getMethod().getName();
// 4. 组合生成 operationId如 User_list
String operationId = classPrefix + "_" + methodName;
// 5. 设置自定义 operationId
operation.setOperationId(operationId);
return operation;
};
}
}

View File

@@ -1,5 +1,6 @@
package cn.iocoder.yudao.framework.web.config;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiErrorLogCommonApi;
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyFilter;
@@ -7,11 +8,14 @@ import cn.iocoder.yudao.framework.web.core.filter.DemoFilter;
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
import cn.iocoder.yudao.framework.web.core.handler.GlobalResponseBodyHandler;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import com.google.common.collect.Maps;
import jakarta.servlet.Filter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
@@ -23,40 +27,57 @@ import org.springframework.web.client.RestTemplate;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.annotation.Resource;
import javax.servlet.Filter;
import java.util.Map;
import java.util.function.Predicate;
@AutoConfiguration
@EnableConfigurationProperties(WebProperties.class)
public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
public class YudaoWebAutoConfiguration {
@Resource
private WebProperties webProperties;
/**
* 应用名
*/
@Value("${spring.application.name}")
private String applicationName;
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurePathMatch(configurer, webProperties.getAdminApi());
configurePathMatch(configurer, webProperties.getAppApi());
}
@Bean
public WebMvcRegistrations webMvcRegistrations(WebProperties webProperties) {
return new WebMvcRegistrations() {
/**
* 设置 API 前缀,仅仅匹配 controller 包下的
*
* @param configurer 配置
* @param api API 配置
*/
private void configurePathMatch(PathMatchConfigurer configurer, WebProperties.Api api) {
AntPathMatcher antPathMatcher = new AntPathMatcher(".");
configurer.addPathPrefix(api.getPrefix(), clazz -> clazz.isAnnotationPresent(RestController.class)
&& antPathMatcher.match(api.getController(), clazz.getPackage().getName())); // 仅仅匹配 controller 包
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
var mapping = new RequestMappingHandlerMapping();
// 实例化时就带上前缀
mapping.setPathPrefixes(buildPathPrefixes(webProperties));
return mapping;
}
/**
* 构建 prefix → 匹配条件的映射
*/
private Map<String, Predicate<Class<?>>> buildPathPrefixes(WebProperties webProperties) {
AntPathMatcher antPathMatcher = new AntPathMatcher(".");
Map<String, Predicate<Class<?>>> pathPrefixes = Maps.newLinkedHashMapWithExpectedSize(2);
putPathPrefix(pathPrefixes, webProperties.getAdminApi(), antPathMatcher);
putPathPrefix(pathPrefixes, webProperties.getAppApi(), antPathMatcher);
return pathPrefixes;
}
/**
* 设置 API 前缀,仅仅匹配 controller 包下的
*/
private void putPathPrefix(Map<String, Predicate<Class<?>>> pathPrefixes, WebProperties.Api api, AntPathMatcher matcher) {
if (api == null || StrUtil.isEmpty(api.getPrefix())) {
return;
}
pathPrefixes.put(api.getPrefix(), // api 前缀
clazz -> clazz.isAnnotationPresent(RestController.class)
&& matcher.match(api.getController(), clazz.getPackage().getName()));
}
};
}
@Bean

View File

@@ -63,7 +63,7 @@ public interface ErrorCodeConstants {
ErrorCode TASK_WITHDRAW_FAIL_PROCESS_NOT_RUNNING = new ErrorCode(1_009_005_017, "撤回失败,流程实例未运行!");
ErrorCode TASK_WITHDRAW_FAIL_TASK_NOT_EXISTS = new ErrorCode(1_009_005_018, "撤回失败,未查询到用户已办任务!");
ErrorCode TASK_WITHDRAW_FAIL_NOT_ALLOW = new ErrorCode(1_009_005_019, "撤回失败,此流程不允许撤回操作!");
ErrorCode TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW = new ErrorCode(1_009_005_019, "撤回失败,下一节点不满足撤回条件!");
ErrorCode TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW = new ErrorCode(1_009_005_020, "撤回失败,下一节点不满足撤回条件!");
// ========== 动态表单模块 1-009-010-000 ==========
ErrorCode FORM_NOT_EXISTS = new ErrorCode(1_009_010_000, "动态表单不存在");

View File

@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.bpm.enums.task;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import lombok.AllArgsConstructor;
@@ -47,4 +48,8 @@ public enum BpmProcessInstanceStatusEnum implements ArrayValuable<Integer> {
APPROVE.getStatus(), REJECT.getStatus(), CANCEL.getStatus());
}
public static BpmProcessInstanceStatusEnum valueOf(Integer status) {
return ArrayUtil.firstMatch(item -> item.getStatus().equals(status), values());
}
}

View File

@@ -1,10 +1,14 @@
package cn.iocoder.yudao.module.bpm.enums.task;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 流程任务 Task 的状态枚举
*
@@ -12,7 +16,7 @@ import lombok.Getter;
*/
@Getter
@AllArgsConstructor
public enum BpmTaskStatusEnum {
public enum BpmTaskStatusEnum implements ArrayValuable<Integer> {
SKIP(-2, "跳过"),
NOT_START(-1, "未开始"),
@@ -35,6 +39,8 @@ public enum BpmTaskStatusEnum {
*/
WAIT(0, "待审批");
public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmTaskStatusEnum::getStatus).toArray(Integer[]::new);
/**
* 状态
* <p>
@@ -46,6 +52,11 @@ public enum BpmTaskStatusEnum {
*/
private final String name;
@Override
public Integer[] array() {
return ARRAYS;
}
public static boolean isRejectStatus(Integer status) {
return REJECT.getStatus().equals(status);
}
@@ -68,4 +79,8 @@ public enum BpmTaskStatusEnum {
return ObjUtil.equal(status, CANCEL.getStatus());
}
public static BpmTaskStatusEnum valueOf(Integer status) {
return ArrayUtil.firstMatch(item -> item.getStatus().equals(status), values());
}
}

View File

@@ -100,6 +100,10 @@ public class BpmModelMetaInfoVO {
@Schema(description = "任务后置通知设置", example = "{}")
private HttpRequestSetting taskAfterTriggerSetting;
@Schema(description = "自定义打印模板设置", example = "{}")
@Valid
private PrintTemplateSetting printTemplateSetting;
@Schema(description = "流程 ID 规则")
@Data
@Valid
@@ -180,4 +184,17 @@ public class BpmModelMetaInfoVO {
}
@Schema(description = "自定义打印模板设置")
@Data
public static class PrintTemplateSetting {
@Schema(description = "是否自定义打印模板", example = "false")
@NotNull(message = "是否自定义打印模板不能为空")
private Boolean enable;
@Schema(description = "打印模板", example = "<p></p>")
private String template;
}
}

View File

@@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
@@ -24,6 +25,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.task.api.Task;
import org.flowable.task.api.history.HistoricTaskInstance;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -34,9 +36,11 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS;
@Tag(name = "管理后台 - 流程实例") // 流程实例,通过流程定义创建的一次“申请”
@RestController
@@ -192,8 +196,30 @@ public class BpmProcessInstanceController {
@GetMapping("/get-bpmn-model-view")
@Operation(summary = "获取流程实例的 BPMN 模型视图", description = "在【流程详细】界面中,进行调用")
@Parameter(name = "id", description = "流程实例的编号", required = true)
public CommonResult<BpmProcessInstanceBpmnModelViewRespVO> getProcessInstanceBpmnModelView(@RequestParam(value = "id") String id) {
public CommonResult<BpmProcessInstanceBpmnModelViewRespVO> getProcessInstanceBpmnModelView(
@RequestParam(value = "id") String id) {
return success(processInstanceService.getProcessInstanceBpmnModelView(id));
}
@GetMapping("/get-print-data")
@Operation(summary = "获得流程实例的打印数据")
@Parameter(name = "id", description = "流程实例的编号", required = true)
@PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
public CommonResult<BpmProcessPrintDataRespVO> getProcessInstancePrintData(
@RequestParam("processInstanceId") String processInstanceId) {
HistoricProcessInstance historicProcessInstance = processInstanceService.getHistoricProcessInstance(processInstanceId);
if (historicProcessInstance == null) {
throw exception(PROCESS_INSTANCE_NOT_EXISTS);
}
AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(historicProcessInstance.getStartUserId())).getCheckedData();
DeptRespDTO dept = deptApi.getDept(startUser.getDeptId()).getCheckedData();
List<HistoricTaskInstance> tasks = taskService.getFinishedTaskListByProcessInstanceIdWithoutCancel(processInstanceId);
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
convertSet(tasks, item -> Long.valueOf(item.getAssignee())));
return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstancePrintData(historicProcessInstance,
processDefinitionService.getProcessDefinitionInfo(historicProcessInstance.getProcessDefinitionId()),
tasks, userMap,
new UserSimpleBaseVO().setNickname(startUser.getNickname()).setDeptName(dept.getName())));
}
}

View File

@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotEmpty;
@Schema(description = "管理后台 - 流程实例的取消 Request VO")
@Data

View File

@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotEmpty;
import java.util.List;
import java.util.Map;

View File

@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 流程实例的打印数据 Response VO")
@Data
public class BpmProcessPrintDataRespVO {
@Schema(description = "流程实例数据")
private BpmProcessInstanceRespVO processInstance;
@Schema(description = "是否开启自定义打印模板", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean printTemplateEnable;
@Schema(description = "自定义打印模板 HTML")
private String printTemplateHtml;
@Schema(description = "审批任务列表")
private List<Task> tasks;
@Schema(description = "流程任务")
@Data
public static class Task {
@Schema(description = "流程任务的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private String id;
@Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
private String name;
@Schema(description = "签名 URL", example = "https://www.iocoder.cn/sign.png")
private String signPicUrl;
@Schema(description = "任务描述", requiredMode = Schema.RequiredMode.REQUIRED)
private String description; // 该字段由后端拼接
}
}

View File

@@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
@@ -21,6 +23,10 @@ public class BpmTaskPageReqVO extends PageParam {
@Schema(description = "流程定义的标识", example = "2048")
private String processDefinitionKey; // 精准匹配
@Schema(description = "审批状态", example = "1")
@InEnum(BpmTaskStatusEnum.class)
private Integer status; // 仅【已办】使用
@Schema(description = "创建时间")
@DateTimeFormat(pattern = DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;

View File

@@ -1,23 +1,29 @@
package cn.iocoder.yudao.module.bpm.convert.task;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceBpmnModelViewRespVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceRespVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessPrintDataRespVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO;
import cn.iocoder.yudao.module.bpm.convert.definition.BpmProcessDefinitionConvert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO;
@@ -35,10 +41,7 @@ import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@@ -293,4 +296,47 @@ public interface BpmProcessInstanceConvert {
.setActivityNodes(activityNodes);
}
default BpmProcessPrintDataRespVO buildProcessInstancePrintData(HistoricProcessInstance historicProcessInstance,
BpmProcessDefinitionInfoDO processDefinitionInfo,
List<HistoricTaskInstance> tasks,
Map<Long, AdminUserRespDTO> userMap,
UserSimpleBaseVO startUser) {
BpmModelMetaInfoVO.PrintTemplateSetting printTemplateSetting = processDefinitionInfo.getPrintTemplateSetting();
BpmProcessPrintDataRespVO printData = new BpmProcessPrintDataRespVO();
// 打印模板是否开启
printData.setPrintTemplateEnable(printTemplateSetting != null && Boolean.TRUE.equals(printTemplateSetting.getEnable()));
// 流程相关数据
BpmProcessInstanceRespVO processInstance = new BpmProcessInstanceRespVO()
.setId(historicProcessInstance.getId()).setName(historicProcessInstance.getName())
.setBusinessKey(historicProcessInstance.getBusinessKey())
.setStartTime(DateUtils.of(historicProcessInstance.getStartTime()))
.setEndTime(DateUtils.of(historicProcessInstance.getEndTime()))
.setStartUser(startUser).setStatus(FlowableUtils.getProcessInstanceStatus(historicProcessInstance))
.setFormVariables(historicProcessInstance.getProcessVariables())
.setProcessDefinition(BeanUtils.toBean(processDefinitionInfo, BpmProcessDefinitionRespVO.class));
printData.setProcessInstance(processInstance);
// 审批历史
List<BpmProcessPrintDataRespVO.Task> approveTasks = new ArrayList<>(tasks.size());
tasks.forEach(item -> {
Map<String, Object> taskLocalVariables = item.getTaskLocalVariables();
BpmProcessPrintDataRespVO.Task approveTask = new BpmProcessPrintDataRespVO.Task();
approveTask.setName(item.getName());
approveTask.setId(item.getId());
approveTask.setSignPicUrl((String) taskLocalVariables.get(BpmnVariableConstants.TASK_SIGN_PIC_URL));
approveTask.setDescription(StrUtil.format("{} / {} / {} / {} / {}",
userMap.get(Long.valueOf(item.getAssignee())).getNickname(),
item.getName(),
DateUtil.formatDateTime(item.getEndTime()),
BpmTaskStatusEnum.valueOf((Integer) taskLocalVariables.get(BpmnVariableConstants.TASK_VARIABLE_STATUS)).getName(),
taskLocalVariables.get(BpmnVariableConstants.TASK_VARIABLE_REASON)));
approveTasks.add(approveTask);
});
printData.setTasks(approveTasks);
// 自定义模板
if (printData.getPrintTemplateEnable() && printTemplateSetting != null) {
printData.setPrintTemplateHtml(printTemplateSetting.getTemplate());
}
return printData;
}
}

View File

@@ -224,4 +224,10 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
@TableField(typeHandler = JacksonTypeHandler.class)
private BpmModelMetaInfoVO.HttpRequestSetting taskAfterTriggerSetting;
/**
* 自定义打印模板设置
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private BpmModelMetaInfoVO.PrintTemplateSetting printTemplateSetting;
}

View File

@@ -51,8 +51,8 @@ public class BpmProcessIdRedisDAO {
String noPrefix = processIdRule.getPrefix() + infix + processIdRule.getPostfix();
String key = RedisKeyConstants.BPM_PROCESS_ID + noPrefix;
Long no = stringRedisTemplate.opsForValue().increment(key);
if (StrUtil.isNotEmpty(infix)) {
// 特殊:没有前缀,则不能过期,不能每次都是从 0 开始
if (StrUtil.isEmpty(infix)) {
// 特殊:没有前缀,则不能过期,不能每次都是从 0 开始。可见 https://t.zsxq.com/MU1E2 讨论
stringRedisTemplate.expire(key, Duration.ofDays(1L));
}
return noPrefix + String.format("%0" + processIdRule.getLength() + "d", no);

View File

@@ -43,14 +43,23 @@ public class BpmnVariableConstants {
* @see ProcessInstance#getProcessVariables()
*/
public static final String PROCESS_INSTANCE_VARIABLE_START_USER_ID = "PROCESS_START_USER_ID";
/**
* 流程实例的变量 - 用于判断流程实例变量节点是否驳回. 格式 RETURN_FLAG_{节点 id}
* 流程实例的变量 - 用于判断流程实例变量节点是否驳回格式 RETURN_FLAG_{节点 id}
*
* 目的是:回到发起节点时,因为审批人与发起人相同,所以被自动通过。但是,此时还是希望不要自动通过
* 目的是:退回到发起节点时,因为审批人与发起人相同,所以被自动通过。但是,此时还是希望不要自动通过
*
* @see ProcessInstance#getProcessVariables()
*/
public static final String PROCESS_INSTANCE_VARIABLE_RETURN_FLAG = "RETURN_FLAG_%s";
/**
* 流程实例的变量前缀 - 用于退回操作,记录需要预测的节点:格式 NEED_SIMULATE_TASK_{节点定义 id}
*
* 目的是:退回操作,预测节点会不准,在流程变量中记录需要预测的节点,来辅助预测
*/
public static final String PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX = "NEED_SIMULATE_TASK_";
/**
* 流程实例的变量 - 是否跳过表达式
*

View File

@@ -658,10 +658,11 @@ public class BpmnModelUtils {
// 根据类型,获取入口连线
List<SequenceFlow> sequenceFlows = getElementIncomingFlows(source);
// 1. 没有入口连线,则返回 false
if (CollUtil.isEmpty(sequenceFlows)) {
return true;
return false;
}
// 循环找目标元素
// 2. 循环找目标元素, 找到目标节点
for (SequenceFlow sequenceFlow : sequenceFlows) {
// 如果发现连线重复,说明循环了,跳过这个循环
if (visitedElements.contains(sequenceFlow.getId())) {
@@ -669,21 +670,22 @@ public class BpmnModelUtils {
}
// 添加已经走过的连线
visitedElements.add(sequenceFlow.getId());
// 这条线路存在目标节点,这条线路完成,进入下个线路
// 这条线路存在目标节点,直接返回 true
FlowElement sourceFlowElement = sequenceFlow.getSourceFlowElement();
if (target.getId().equals(sourceFlowElement.getId())) {
return true;
}
// 如果目标节点为并行网关,跳过这个循环 (TODO 疑问:这个判断作用是防止回退到并行网关分支上的节点吗?)
if (sourceFlowElement instanceof ParallelGateway) {
continue;
}
// 如果目标节点为并行网关,则不继续
if (sourceFlowElement instanceof ParallelGateway) {
return false;
}
// 否则就继续迭代
if (!isSequentialReachable(sourceFlowElement, target, visitedElements)) {
return false;
// 继续迭代,如果找到目标节点直接返回 true
if (isSequentialReachable(sourceFlowElement, target, visitedElements)) {
return true;
}
}
return true;
// 未找到返回 false
return false;
}
/**
@@ -783,7 +785,6 @@ public class BpmnModelUtils {
return resultElements;
}
@SuppressWarnings("PatternVariableCanBeUsed")
private static void simulateNextFlowElements(FlowElement currentElement, Map<String, Object> variables,
List<FlowElement> resultElements, Set<FlowElement> visitElements) {
// 如果为空,或者已经遍历过,则直接结束

View File

@@ -737,10 +737,10 @@ public class SimpleModelUtils {
BoundaryEvent boundaryEvent = null;
if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())) {
boundaryEvent = buildTimeoutBoundaryEvent(receiveTask, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(),
node.getDelaySetting().getDelayTime(), null, null);
null, null, node.getDelaySetting().getDelayTime());
} else if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())) {
boundaryEvent = buildTimeoutBoundaryEvent(receiveTask, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(),
null, null, node.getDelaySetting().getDelayTime());
node.getDelaySetting().getDelayTime(), null, null);
} else {
throw new UnsupportedOperationException("不支持的延迟类型:" + node.getDelaySetting());
}

View File

@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
@@ -71,6 +72,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
import static cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ActivityNode;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseNodeType;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
@@ -186,6 +188,10 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
if (CollUtil.isNotEmpty(reqVO.getProcessVariables())) {
processVariables.putAll(reqVO.getProcessVariables());
}
// 特殊如果是未发起的场景则设置发起用户解决“发起流程”时需要使用到该变量的问题。例如说https://t.zsxq.com/fMw5g
if (historicProcessInstance == null) {
processVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, loginUserId);
}
// 1.3 读取其它相关数据
ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition(
historicProcessInstance != null ? historicProcessInstance.getProcessDefinitionId()
@@ -217,10 +223,24 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
// 3.1 计算当前登录用户的待办任务
BpmTaskRespVO todoTask = taskService.getTodoTask(loginUserId, reqVO.getTaskId(), reqVO.getProcessInstanceId());
// 3.2 预测未运行节点的审批信息
// 3.2 获取由于退回操作,需要预测的节点。从流程变量中获取,回退操作会设置这些变量
Set<String> needSimulateTaskDefKeysByReturn = new HashSet<>();
if (StrUtil.isNotEmpty(reqVO.getProcessInstanceId())) {
Map<String, Object> variables = runtimeService.getVariables(reqVO.getProcessInstanceId());
Map<String, Object> simulateTaskVariables = MapUtil.filter(variables,
item -> item.getKey().startsWith(PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX));
simulateTaskVariables.forEach((key, value) ->
needSimulateTaskDefKeysByReturn.add(StrUtil.removePrefix(key, PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX)));
}
// 移除运行中的节点,运行中的节点无需预测
// TODO @jason是不是 foreach runActivityNodes然后移除 needSimulateTaskDefKeysByReturn 更好?(理解成本低一点)
CollectionUtils.convertList(runActivityNodes, ActivityNode::getId).forEach(needSimulateTaskDefKeysByReturn::remove);
// 3.3 预测未运行节点的审批信息
List<ActivityNode> simulateActivityNodes = getSimulateApproveNodeList(startUserId, bpmnModel,
processDefinitionInfo,
processVariables, activities);
processVariables, activities, needSimulateTaskDefKeysByReturn);
// 4. 拼接最终数据
return buildApprovalDetail(reqVO, bpmnModel, processDefinition, processDefinitionInfo, historicProcessInstance,
@@ -460,7 +480,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
}
/**
* 获取结束节点的状态
* 获取结束节点的状态
*/
private Integer getEndActivityNodeStatus(HistoricTaskInstance task) {
Integer status = FlowableUtils.getTaskStatus(task);
@@ -545,7 +565,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
private List<ActivityNode> getSimulateApproveNodeList(Long startUserId, BpmnModel bpmnModel,
BpmProcessDefinitionInfoDO processDefinitionInfo,
Map<String, Object> processVariables,
List<HistoricActivityInstance> activities) {
List<HistoricActivityInstance> activities,
Set<String> needSimulateTaskDefKeysByReturn) {
// TODO @芋艿【可优化】在驳回场景下未来的预测准确性不高。原因是驳回后HistoricActivityInstance
// 包括了历史的操作,不是只有 startEvent 到当前节点的记录
Set<String> runActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId);
@@ -554,7 +575,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
List<FlowElement> flowElements = BpmnModelUtils.simulateProcess(bpmnModel, processVariables);
return convertList(flowElements, flowElement -> buildNotRunApproveNodeForBpmn(
startUserId, bpmnModel, flowElements,
processDefinitionInfo, processVariables, flowElement, runActivityIds));
processDefinitionInfo, processVariables, flowElement, runActivityIds, needSimulateTaskDefKeysByReturn));
}
// 情况二SIMPLE 设计器
if (Objects.equals(BpmModelTypeEnum.SIMPLE.getType(), processDefinitionInfo.getModelType())) {
@@ -563,17 +584,19 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
List<BpmSimpleModelNodeVO> simpleNodes = SimpleModelUtils.simulateProcess(simpleModel, processVariables);
return convertList(simpleNodes, simpleNode -> buildNotRunApproveNodeForSimple(
startUserId, bpmnModel,
processDefinitionInfo, processVariables, simpleNode, runActivityIds));
processDefinitionInfo, processVariables, simpleNode, runActivityIds, needSimulateTaskDefKeysByReturn));
}
throw new IllegalArgumentException("未知设计器类型:" + processDefinitionInfo.getModelType());
}
private ActivityNode buildNotRunApproveNodeForSimple(Long startUserId, BpmnModel bpmnModel,
BpmProcessDefinitionInfoDO processDefinitionInfo, Map<String, Object> processVariables,
BpmSimpleModelNodeVO node, Set<String> runActivityIds) {
BpmSimpleModelNodeVO node, Set<String> runActivityIds,
Set<String> needSimulateTaskDefKeysByReturn) {
// TODO @芋艿【可优化】在驳回场景下未来的预测准确性不高。原因是驳回后HistoricActivityInstance
// 包括了历史的操作,不是只有 startEvent 到当前节点的记录
if (runActivityIds.contains(node.getId())) {
if (runActivityIds.contains(node.getId())
&& !needSimulateTaskDefKeysByReturn.contains(node.getId())) { // 特殊:回退操作时候,会记录需要预测的节点到流程变量中。即使在历史操作中,也需要预测
return null;
}
Integer status = BpmTaskStatusEnum.NOT_START.getStatus();
@@ -620,13 +643,16 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
private ActivityNode buildNotRunApproveNodeForBpmn(Long startUserId, BpmnModel bpmnModel, List<FlowElement> flowElements,
BpmProcessDefinitionInfoDO processDefinitionInfo,
Map<String, Object> processVariables,
FlowElement node, Set<String> runActivityIds) {
if (runActivityIds.contains(node.getId())) {
FlowElement node, Set<String> runActivityIds,
Set<String> needSimulateTaskDefKeysByReturn) {
// 回退操作时候,会记录需要预测的节点到流程变量中。即使节点在历史操作中,也需要预测。
if (!needSimulateTaskDefKeysByReturn.contains(node.getId()) && runActivityIds.contains(node.getId())) {
return null;
}
Integer status = BpmTaskStatusEnum.NOT_START.getStatus();
// 如果节点被跳过,状态设置为跳过
if(BpmnModelUtils.isSkipNode(node, processVariables)){
if (BpmnModelUtils.isSkipNode(node, processVariables)) {
status = BpmTaskStatusEnum.SKIP.getStatus();
}
ActivityNode activityNode = new ActivityNode().setId(node.getId())

View File

@@ -176,6 +176,14 @@ public interface BpmTaskService {
*/
List<HistoricActivityInstance> getHistoricActivityListByExecutionId(String executionId);
/**
* 获得指定流程实例的已完成的流程任务列表,不包含取消状态
*
* @param processInstanceId 流程实例的编号
* @return 流程任务列表
*/
List<HistoricTaskInstance> getFinishedTaskListByProcessInstanceIdWithoutCancel(String processInstanceId);
// ========== Update 写入相关方法 ==========
/**

View File

@@ -2,10 +2,12 @@ package cn.iocoder.yudao.module.bpm.service.task;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.*;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
@@ -68,8 +70,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE;
//import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.*;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*;
/**
@@ -230,6 +231,9 @@ public class BpmTaskServiceImpl implements BpmTaskService {
if (StrUtil.isNotBlank(pageVO.getName())) {
taskQuery.taskNameLike("%" + pageVO.getName() + "%");
}
if (pageVO.getStatus() != null) {
taskQuery.taskVariableValueEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS, pageVO.getStatus());
}
// if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) {
// taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0]));
// taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1]));
@@ -491,6 +495,17 @@ public class BpmTaskServiceImpl implements BpmTaskService {
return historyService.createHistoricActivityInstanceQuery().executionId(executionId).list();
}
@Override
public List<HistoricTaskInstance> getFinishedTaskListByProcessInstanceIdWithoutCancel(String processInstanceId) {
return historyService.createHistoricTaskInstanceQuery()
.finished()
.includeTaskLocalVariables()
.processInstanceId(processInstanceId)
.taskVariableValueNotEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS,
BpmTaskStatusEnum.CANCEL.getStatus())
.orderByHistoricTaskInstanceStartTime().asc().list();
}
/**
* 判断指定用户,是否是当前任务的审批人
*
@@ -590,7 +605,13 @@ public class BpmTaskServiceImpl implements BpmTaskService {
bpmnModel, reqVO.getNextAssignees(), instance);
runtimeService.setVariables(task.getProcessInstanceId(), variables);
// 5. 调用 BPM complete 去完成任务
// 5. 移除辅助预测的流程变量,这些变量在回退操作中设置
// todo @jason可以直接 + 拼接哈
String simulateVariableName = StrUtil.concat(false,
BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX, task.getTaskDefinitionKey());
runtimeService.removeVariable(task.getProcessInstanceId(), simulateVariableName);
// 6. 调用 BPM complete 去完成任务
taskService.complete(task.getId(), variables, true);
// 【加签专属】处理加签任务
@@ -840,34 +861,34 @@ public class BpmTaskServiceImpl implements BpmTaskService {
if (task.isSuspended()) {
throw exception(TASK_IS_PENDING);
}
// 1.2 校验源头和目标节点的关系,并返回目标元素
FlowElement targetElement = validateTargetTaskCanReturn(task.getTaskDefinitionKey(),
reqVO.getTargetTaskDefinitionKey(), task.getProcessDefinitionId());
// 1.2 获取流程模型信息
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId());
// 1.3 校验源头和目标节点的关系,并返回目标元素
FlowElement targetElement = validateTargetTaskCanReturn(bpmnModel, task.getTaskDefinitionKey(),
reqVO.getTargetTaskDefinitionKey());
// 2. 调用 Flowable 框架的退回逻辑
returnTask(userId, task, targetElement, reqVO);
returnTask(userId, bpmnModel, task, targetElement, reqVO);
}
/**
* 退回流程节点时,校验目标任务节点是否可退回
*
* @param sourceKey 当前任务节点 Key
* @param targetKey 目标任务节点 key
* @param processDefinitionId 当前流程定义 ID
* @param bpmnModel 流程模型
* @param sourceKey 当前任务节点 Key
* @param targetKey 目标任务节点 key
* @return 目标任务节点元素
*/
private FlowElement validateTargetTaskCanReturn(String sourceKey, String targetKey, String processDefinitionId) {
// 1.1 获取流程模型信息
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processDefinitionId);
// 1.3 获取当前任务节点元素
private FlowElement validateTargetTaskCanReturn(BpmnModel bpmnModel, String sourceKey, String targetKey) {
// 1.1 获取当前任务节点元素
FlowElement source = BpmnModelUtils.getFlowElementById(bpmnModel, sourceKey);
// 1.3 获取跳转的节点元素
// 1.2 获取跳转的节点元素
FlowElement target = BpmnModelUtils.getFlowElementById(bpmnModel, targetKey);
if (target == null) {
throw exception(TASK_TARGET_NODE_NOT_EXISTS);
}
// 2.2 只有串行可到达的节点,才可以退回。类似非串行、子流程无法退回
// 2. 只有串行可到达的节点,才可以退回。类似非串行、子流程无法退回
if (!BpmnModelUtils.isSequentialReachable(source, target, null)) {
throw exception(TASK_RETURN_FAIL_SOURCE_TARGET_ERROR);
}
@@ -878,11 +899,12 @@ public class BpmTaskServiceImpl implements BpmTaskService {
* 执行退回逻辑
*
* @param userId 用户编号
* @param bpmnModel 流程模型
* @param currentTask 当前退回的任务
* @param targetElement 需要退回到的目标任务
* @param reqVO 前端参数封装
*/
public void returnTask(Long userId, Task currentTask, FlowElement targetElement, BpmTaskReturnReqVO reqVO) {
public void returnTask(Long userId, BpmnModel bpmnModel, Task currentTask, FlowElement targetElement, BpmTaskReturnReqVO reqVO) {
// 1. 获得所有需要回撤的任务 taskDefinitionKey用于稍后的 moveActivityIdsToSingleActivityId 回撤
// 1.1 获取所有正常进行的任务节点 Key
List<Task> taskList = taskService.createTaskQuery().processInstanceId(currentTask.getProcessInstanceId()).list();
@@ -915,18 +937,54 @@ public class BpmTaskServiceImpl implements BpmTaskService {
}
});
// 3. 设置流程变量节点驳回标记:用于驳回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略。导致自动通过
runtimeService.setVariable(currentTask.getProcessInstanceId(),
String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, reqVO.getTargetTaskDefinitionKey()), Boolean.TRUE);
// 3. 构建需要预测的任务流程变量
// TODO @jason【驳回预测相关】是不是搞成一个变量里面是 set 更简洁一点呀?
Set<String> taskDefinitionKeyList = getNeedSimulateTaskDefinitionKeys(bpmnModel, currentTask, targetElement);
Map<String, Object> needSimulateVariables = convertMap(taskDefinitionKeyList,
taskId -> StrUtil.concat(false, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX, taskId), item -> Boolean.TRUE);
// 4. 执行驳回
// 使用 moveExecutionsToSingleActivityId 替换 moveActivityIdsToSingleActivityId 原因:
// 当多实例任务回退的时候有问题。相关 issue: https://github.com/flowable/flowable-engine/issues/3944
runtimeService.createChangeActivityStateBuilder()
.processInstanceId(currentTask.getProcessInstanceId())
.moveExecutionsToSingleActivityId(runExecutionIds, reqVO.getTargetTaskDefinitionKey())
// 设置需要预测的任务流程变量,用于辅助预测
.processVariables(needSimulateVariables)
// 设置流程变量local节点退回标记, 用于退回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略,导致自动通过
.localVariable(reqVO.getTargetTaskDefinitionKey(),
String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, reqVO.getTargetTaskDefinitionKey()),
Boolean.TRUE)
.changeState();
}
private Set<String> getNeedSimulateTaskDefinitionKeys(BpmnModel bpmnModel, Task currentTask, FlowElement targetElement) {
// 1. 获取需要预测的任务的 definition key。因为当前任务还没完成也需要预测
Set<String> taskDefinitionKeys = CollUtil.newHashSet(currentTask.getTaskDefinitionKey());
// 2.1 从已结束任务中找到要回退的目标任务,按时间倒序最近的一个目标任务
List<HistoricTaskInstance> endTaskList = CollectionUtils.filterList(
getTaskListByProcessInstanceId(currentTask.getProcessInstanceId(), Boolean.FALSE),
item -> item.getEndTime() != null);
// 2.2 遍历已结束的任务,找到在 targetTask 之后生成的任务,且串行可达的任务
HistoricTaskInstance targetTask = findFirst(endTaskList,
item -> item.getTaskDefinitionKey().equals(targetElement.getId()));
// TODO @jason【驳回预测相关】是不是 if targetTask 先判空?
endTaskList.forEach(item -> {
FlowElement element = getFlowElementById(bpmnModel, item.getTaskDefinitionKey());
// 如果已结束的任务在回退目标节点之后生成,且串行可达,则标记为需要预算节点
// TODO 串行可达的方法需要和判断可回退节点 validateTargetTaskCanReturn 分开吗? 并行网关可能会有问题。
// TODO @jason【驳回预测相关】这里是不是判断 element 哈?
if (targetTask != null
// TODO @jason【驳回预测相关】这里直接 createTime 的 compare 更简单?因为不太会出现空哈。
&& DateUtil.compare(item.getCreateTime(), targetTask.getCreateTime()) > 0
&& BpmnModelUtils.isSequentialReachable(element, targetElement, null)) {
taskDefinitionKeys.add(item.getTaskDefinitionKey());
}
});
return taskDefinitionKeys;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delegateTask(Long userId, BpmTaskDelegateReqVO reqVO) {
@@ -1438,12 +1496,11 @@ public class BpmTaskServiceImpl implements BpmTaskService {
return;
}
FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
// 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略
// TODO 芋艿:【优化】未来有没更好的判断方式?!另外,还要考虑清理机制。就是说,下次处理了之后,就移除这个标识
Boolean returnTaskFlag = runtimeService.getVariable(processInstance.getProcessInstanceId(),
String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class);
// 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略(使用 local variable
Boolean returnTaskFlag = runtimeService.getVariableLocal(task.getExecutionId(),
String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class);
Boolean skipStartUserNodeFlag = Convert.toBool(runtimeService.getVariable(processInstance.getProcessInstanceId(),
PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class));
BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class));
if (userTaskElement.getId().equals(START_USER_NODE_ID)
&& (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核
|| BooleanUtil.isTrue(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核

View File

@@ -7,6 +7,11 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 线索分页 Request VO")
@Data
@@ -42,4 +47,8 @@ public class CrmCluePageReqVO extends PageParam {
@Schema(description = "跟进状态", example = "true")
private Boolean followUpStatus;
@Schema(description = "创建时间", example = "[2023-01-01 00:00:00, 2023-01-31 23:59:59]")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

View File

@@ -33,6 +33,7 @@ public interface CrmClueMapper extends BaseMapperX<CrmClueDO> {
.eqIfPresent(CrmClueDO::getLevel, pageReqVO.getLevel())
.eqIfPresent(CrmClueDO::getSource, pageReqVO.getSource())
.eqIfPresent(CrmClueDO::getFollowUpStatus, pageReqVO.getFollowUpStatus())
.betweenIfPresent(CrmClueDO::getCreateTime, pageReqVO.getCreateTime())
.orderByDesc(CrmClueDO::getId);
return selectJoinPage(pageReqVO, CrmClueDO.class, query);
}

View File

@@ -7,7 +7,6 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmCluePageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueSaveReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueTransferReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer.CrmCustomerSaveReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.followup.CrmFollowUpRecordDO;
import cn.iocoder.yudao.module.crm.dal.mysql.clue.CrmClueMapper;
@@ -113,7 +112,7 @@ public class CrmClueServiceImpl implements CrmClueService {
private void validateRelationDataExists(CrmClueSaveReqVO reqVO) {
// 校验负责人
if (Objects.nonNull(reqVO.getOwnerUserId()) &&
Objects.isNull(adminUserApi.getUser(reqVO.getOwnerUserId()))) {
Objects.isNull(adminUserApi.getUser(reqVO.getOwnerUserId()).getCheckedData())) {
throw exception(USER_NOT_EXISTS);
}
}

View File

@@ -21,9 +21,8 @@ public enum CodegenFrontTypeEnum {
VUE3_VBEN5_ANTD_SCHEMA(40), // Vue3 VBEN5 + ANTD + schema 模版
VUE3_VBEN5_ANTD_GENERAL(41), // Vue3 VBEN5 + ANTD 标准模版
// TODO @puhui999:50、51 会好点;
VUE3_VBEN5_EP_SCHEMA(42), // Vue3 VBEN5 + EP + schema 模版
VUE3_VBEN5_EP_GENERAL(43), // Vue3 VBEN5 + EP 标准模版
VUE3_VBEN5_EP_SCHEMA(50), // Vue3 VBEN5 + EP + schema 模版
VUE3_VBEN5_EP_GENERAL(51), // Vue3 VBEN5 + EP 标准模版
;
/**

View File

@@ -26,6 +26,7 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -99,8 +100,10 @@ public class FileController {
if (StrUtil.isEmpty(path)) {
throw new IllegalArgumentException("结尾的 path 路径必须传递");
}
// 解码,解决中文路径的问题 https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/807/
path = URLUtil.decode(path);
// 解码,解决中文路径的问题
// https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/807/
// https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1432/
path = URLUtil.decode(path, StandardCharsets.UTF_8, false);
// 读取内容
byte[] content = fileService.getFileContent(configId, path);

View File

@@ -343,7 +343,7 @@ public class CodegenEngine {
filePath = formatFilePath(filePath, bindingMap);
String content = templateEngine.getTemplate(vmPath).render(bindingMap);
// 格式化代码
content = prettyCode(content);
content = prettyCode(content, vmPath);
result.put(filePath, content);
}
@@ -383,11 +383,14 @@ public class CodegenEngine {
* 如果不处理Vue 的 Pretty 格式校验可能会报错
*
* @param content 格式化前的代码
* @param vmPath 模板路径
* @return 格式化后的代码
*/
private String prettyCode(String content) {
// Vue 界面:去除字段后面多余的 , 逗号,解决前端的 Pretty 代码格式检查的报错
content = content.replaceAll(",\n}", "\n}").replaceAll(",\n }", "\n }");
private String prettyCode(String content, String vmPath) {
// Vue 界面:去除字段后面多余的 , 逗号,解决前端的 Pretty 代码格式检查的报错(需要排除 vben5
if (!StrUtil.contains(vmPath, "vben5")) {
content = content.replaceAll(",\n}", "\n}").replaceAll(",\n }", "\n }");
}
// Vue 界面:去除多的 dateFormatter只有一个的情况下说明没使用到
if (StrUtil.count(content, "dateFormatter") == 1) {
content = StrUtils.removeLineContains(content, "dateFormatter");

View File

@@ -27,6 +27,16 @@ export function delete${simpleClassName}(id) {
})
}
#if ( $table.templateType != 2 && $deleteBatchEnable)
/** 批量删除${table.classComment} */
export function delete${simpleClassName}List(ids) {
return request({
url: `${baseURL}/delete-list?ids=${ids.join(',')}`,
method: 'delete'
})
}
#end
// 获得${table.classComment}
export function get${simpleClassName}(id) {
return request({
@@ -130,6 +140,15 @@ export function export${simpleClassName}Excel(params) {
method: 'delete'
})
}
#if ($deleteBatchEnable)
/** 批量删除${subTable.classComment} */
export function delete${subSimpleClassName}List(ids) {
return request({
url: `${baseURL}/${subSimpleClassName_strikeCase}/delete-list?ids=${ids.join(',')}`,
method: 'delete'
})
}
#end
// 获得${subTable.classComment}
export function get${subSimpleClassName}(id) {
return request({

View File

@@ -1,19 +1,56 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}")
// ${table.classComment} VO
export interface ${simpleClassName}VO {
#foreach ($column in $columns)
#if ($column.createOperation || $column.updateOperation)
#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")
${column.javaField}: number // ${column.columnComment}
#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime")
${column.javaField}: Date // ${column.columnComment}
#else
${column.javaField}: ${column.javaType.toLowerCase()} // ${column.columnComment}
#end
#end
## 特殊:主子表专属逻辑
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subColumns = $subColumnsList.get($index))##当前字段数组
/** ${subTable.classComment}信息 */
export interface ${subSimpleClassName} {
#foreach ($column in $subColumns)
#if ($column.createOperation || $column.updateOperation)
#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: number; // ${column.columnComment}
#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: string | Dayjs; // ${column.columnComment}
#else
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: ${column.javaType.toLowerCase()}; // ${column.columnComment}
#end
#end
#end
}
#end
/** ${table.classComment}信息 */
export interface ${simpleClassName} {
#foreach ($column in $columns)
#if ($column.createOperation || $column.updateOperation)
#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: number; // ${column.columnComment}
#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: string | Dayjs; // ${column.columnComment}
#else
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: ${column.javaType.toLowerCase()}; // ${column.columnComment}
#end
#end
#end
#if ( $table.templateType == 2 )
children?: ${simpleClassName}[];
#end
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#if ( $subTable.subJoinMany )
${subSimpleClassName.toLowerCase()}s?: ${subSimpleClassName}[]
#else
${subSimpleClassName.toLowerCase()}?: ${subSimpleClassName}
#end
#end
#end
}
// ${table.classComment} API
@@ -36,12 +73,12 @@ export const ${simpleClassName}Api = {
},
// 新增${table.classComment}
create${simpleClassName}: async (data: ${simpleClassName}VO) => {
create${simpleClassName}: async (data: ${simpleClassName}) => {
return await request.post({ url: `${baseURL}/create`, data })
},
// 修改${table.classComment}
update${simpleClassName}: async (data: ${simpleClassName}VO) => {
update${simpleClassName}: async (data: ${simpleClassName}) => {
return await request.put({ url: `${baseURL}/update`, data })
},
@@ -50,6 +87,13 @@ export const ${simpleClassName}Api = {
return await request.delete({ url: `${baseURL}/delete?id=` + id })
},
#if ( $table.templateType != 2 && $deleteBatchEnable)
/** 批量删除${table.classComment} */
delete${simpleClassName}List: async (ids: number[]) => {
return await request.delete({ url: `${baseURL}/delete-list?ids=${ids.join(',')}` })
},
#end
// 导出${table.classComment} Excel
export${simpleClassName}: async (params) => {
return await request.download({ url: `${baseURL}/export-excel`, params })
@@ -92,12 +136,12 @@ export const ${simpleClassName}Api = {
## 特殊MASTER_ERP 时,支持单个的新增、修改、删除操作
#if ( $table.templateType == 11 )
// 新增${subTable.classComment}
create${subSimpleClassName}: async (data) => {
create${subSimpleClassName}: async (data: ${subSimpleClassName}) => {
return await request.post({ url: `${baseURL}/${subSimpleClassName_strikeCase}/create`, data })
},
// 修改${subTable.classComment}
update${subSimpleClassName}: async (data) => {
update${subSimpleClassName}: async (data: ${subSimpleClassName}) => {
return await request.put({ url: `${baseURL}/${subSimpleClassName_strikeCase}/update`, data })
},
@@ -106,6 +150,13 @@ export const ${simpleClassName}Api = {
return await request.delete({ url: `${baseURL}/${subSimpleClassName_strikeCase}/delete?id=` + id })
},
#if ($deleteBatchEnable)
/** 批量删除${subTable.classComment} */
delete${subSimpleClassName}List: async (ids: number[]) => {
return await request.delete({ url: `${baseURL}/${subSimpleClassName_strikeCase}/delete-list?ids=${ids.join(',')}` })
},
#end
// 获得${subTable.classComment}
get${subSimpleClassName}: async (id: number) => {
return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/get?id=` + id })

View File

@@ -217,7 +217,7 @@ const handleDeleteBatch = async () => {
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: ${subSimpleClassName}[]) => {
checkedIds.value = records.map((item) => item.id);
checkedIds.value = records.map((item) => item.id!);
}
#end
#end

View File

@@ -374,7 +374,7 @@ const handleDeleteBatch = async () => {
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: ${simpleClassName}[]) => {
checkedIds.value = records.map((item) => item.id);
checkedIds.value = records.map((item) => item.id!);
}
#end

View File

@@ -91,18 +91,17 @@ export function delete${simpleClassName}(id: number) {
#if ( $table.templateType != 2 && $deleteBatchEnable)
/** 批量删除${table.classComment} */
export function delete${simpleClassName}ListByIds(ids: number[]) {
export function delete${simpleClassName}List(ids: number[]) {
return requestClient.delete(`${baseURL}/delete-list?ids=${ids.join(',')}`)
}
#end
/** 导出${table.classComment} */
export function export${simpleClassName}(params: any) {
return requestClient.download('${baseURL}/export-excel', params);
return requestClient.download('${baseURL}/export-excel', { params });
}
## 特殊:主子表专属逻辑
## TODO @puhui999下面这块缩进调整了会乱掉么
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
@@ -115,54 +114,54 @@ export function export${simpleClassName}(params: any) {
// ==================== 子表($subTable.classComment ====================
## 情况一MASTER_ERP 时,需要分查询页子表
#if ( $table.templateType == 11 )
/** 获得${subTable.classComment}分页 */
export function get${subSimpleClassName}Page(params: PageParam) {
return requestClient.get<PageResult<${simpleClassName}Api.${subSimpleClassName}>>(`${baseURL}/${subSimpleClassName_strikeCase}/page`, { params });
}
## 情况二:非 MASTER_ERP 时,需要列表查询子表
#else
#if ( $subTable.subJoinMany )
/** 获得${subTable.classComment}列表 */
export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}: number) {
return requestClient.get<${simpleClassName}Api.${subSimpleClassName}[]>(`${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`);
}
#else
/** 获得${subTable.classComment} */
export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}: number) {
return requestClient.get<${simpleClassName}Api.${subSimpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`);
}
#end
#end
## 特殊MASTER_ERP 时,支持单个的新增、修改、删除操作
#if ( $table.templateType == 11 )
/** 新增${subTable.classComment} */
export function create${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) {
return requestClient.post(`${baseURL}/${subSimpleClassName_strikeCase}/create`, data);
}
## 情况一MASTER_ERP 时,需要分查询页子表
#if ( $table.templateType == 11 )
/** 获得${subTable.classComment}分页 */
export function get${subSimpleClassName}Page(params: PageParam) {
return requestClient.get<PageResult<${simpleClassName}Api.${subSimpleClassName}>>(`${baseURL}/${subSimpleClassName_strikeCase}/page`, { params });
}
## 情况二:非 MASTER_ERP 时,需要列表查询子表
#else
#if ( $subTable.subJoinMany )
/** 获得${subTable.classComment}列表 */
export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}: number) {
return requestClient.get<${simpleClassName}Api.${subSimpleClassName}[]>(`${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`);
}
#else
/** 获得${subTable.classComment} */
export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}: number) {
return requestClient.get<${simpleClassName}Api.${subSimpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`);
}
#end
#end
## 特殊MASTER_ERP 时,支持单个的新增、修改、删除操作
#if ( $table.templateType == 11 )
/** 新增${subTable.classComment} */
export function create${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) {
return requestClient.post(`${baseURL}/${subSimpleClassName_strikeCase}/create`, data);
}
/** 修改${subTable.classComment} */
export function update${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) {
return requestClient.put(`${baseURL}/${subSimpleClassName_strikeCase}/update`, data);
}
/** 修改${subTable.classComment} */
export function update${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) {
return requestClient.put(`${baseURL}/${subSimpleClassName_strikeCase}/update`, data);
}
/** 删除${subTable.classComment} */
export function delete${subSimpleClassName}(id: number) {
return requestClient.delete(`${baseURL}/${subSimpleClassName_strikeCase}/delete?id=${id}`);
}
/** 删除${subTable.classComment} */
export function delete${subSimpleClassName}(id: number) {
return requestClient.delete(`${baseURL}/${subSimpleClassName_strikeCase}/delete?id=${id}`);
}
#if ($deleteBatchEnable)
/** 批量删除${subTable.classComment} */
export function delete${subSimpleClassName}ListByIds(ids: number[]) {
return requestClient.delete(`${baseURL}/${subSimpleClassName_strikeCase}/delete-list?ids=${ids.join(',')}`)
}
#end
/** 获得${subTable.classComment} */
export function get${subSimpleClassName}(id: number) {
return requestClient.get<${simpleClassName}Api.${subSimpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get?id=${id}`);
}
#end
#if ($deleteBatchEnable)
/** 批量删除${subTable.classComment} */
export function delete${subSimpleClassName}List(ids: number[]) {
return requestClient.delete(`${baseURL}/${subSimpleClassName_strikeCase}/delete-list?ids=${ids.join(',')}`)
}
#end
/** 获得${subTable.classComment} */
export function get${subSimpleClassName}(id: number) {
return requestClient.get<${simpleClassName}Api.${subSimpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get?id=${id}`);
}
#end
#end

View File

@@ -1,12 +1,15 @@
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { Rule } from 'ant-design-vue/es/form';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload, FileUpload } from "#/components/upload";
import { message, Tabs, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'ant-design-vue';
import { DICT_TYPE, getDictOptions } from '#/utils';
#if($table.templateType == 2)## 树表需要导入这些
import { get${simpleClassName}List } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { handleTree } from '@vben/utils'
@@ -20,7 +23,6 @@ import { handleTree } from '@vben/utils'
#end
#end
import { computed, ref } from 'vue';
import { $t } from '#/locales';
import { get${simpleClassName}, create${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
@@ -70,7 +72,7 @@ const getTitle = computed(() => {
#end
/** 重置表单 */
const resetForm = () => {
function resetForm() {
formData.value = {
#foreach ($column in $columns)
#if ($column.createOperation || $column.updateOperation)
@@ -88,7 +90,7 @@ const resetForm = () => {
## 特殊:树表专属逻辑
#if ( $table.templateType == 2 )
/** 获得${table.classComment}树 */
const get${simpleClassName}Tree = async () => {
async function get${simpleClassName}Tree() {
${classNameVar}Tree.value = []
const data = await get${simpleClassName}List({});
data.unshift({
@@ -147,7 +149,6 @@ const [Modal, modalApi] = useVbenModal({
emit('success');
message.success({
content: $t('ui.actionMessage.operationSuccess'),
key: 'action_process_msg',
});
} finally {
modalApi.unlock();

View File

@@ -1,18 +1,18 @@
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { VxeTableInstance } from '#/adapter/vxe-table';
import { ref, h, reactive, onMounted, nextTick } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { cloneDeep, formatDateTime } from '@vben/utils';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
import { cloneDeep, downloadFileFromBlobPart, formatDateTime } from '@vben/utils';
import { Button, message,Tabs,Pagination,Form,RangePicker,DatePicker,Select,Input } from 'ant-design-vue';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
import ${simpleClassName}Form from './modules/form.vue';
import { Download, Plus, RefreshCw, Search, Trash2 } from '@vben/icons';
import { ContentWrap } from '#/components/content-wrap';
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { TableToolbar } from '#/components/table-toolbar';
import { useTableToolbar } from '#/hooks';
## 特殊:主子表专属逻辑
#if ( $table.templateType == 11 || $table.templateType == 12 )
@@ -23,7 +23,6 @@ import { useTableToolbar } from '#/hooks';
#end
#end
import { ref, h, reactive,onMounted,nextTick } from 'vue';
import { $t } from '#/locales';
#if (${table.templateType} == 2)## 树表接口
import { handleTree,isEmpty } from '@vben/utils'
@@ -77,7 +76,7 @@ const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
/** 查询列表 */
const getList = async () => {
async function getList() {
loading.value = true
try {
const params = cloneDeep(queryParams) as any;
@@ -104,7 +103,7 @@ const getList = async () => {
}
/** 搜索按钮操作 */
const handleQuery = () => {
function handleQuery() {
#if ( $table.templateType != 2 )
queryParams.pageNo = 1
#end
@@ -112,7 +111,7 @@ const handleQuery = () => {
}
/** 重置按钮操作 */
const resetQuery = () => {
function resetQuery() {
queryFormRef.value.resetFields()
handleQuery()
}
@@ -124,7 +123,7 @@ const [FormModal, formModalApi] = useVbenModal({
/** 创建${table.classComment} */
function handleCreate() {
formModalApi.setData({}).open();
formModalApi.setData(null).open();
}
/** 编辑${table.classComment} */
@@ -144,14 +143,10 @@ async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]),
duration: 0,
key: 'action_process_msg',
});
try {
await delete${simpleClassName}(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_process_msg',
});
await delete${simpleClassName}(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
await getList();
} finally {
hideLoading();
@@ -164,12 +159,11 @@ async function handleDeleteBatch() {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting'),
duration: 0,
key: 'action_process_msg',
});
try {
await delete${simpleClassName}List(checkedIds.value);
checkedIds.value = [];
message.success( $t('ui.actionMessage.deleteSuccess') );
message.success($t('ui.actionMessage.deleteSuccess'));
await getList();
} finally {
hideLoading();
@@ -182,12 +176,12 @@ function handleRowCheckboxChange({
}: {
records: ${simpleClassName}Api.${simpleClassName}[];
}) {
checkedIds.value = records.map((item) => item.id);
checkedIds.value = records.map((item) => item.id!);
}
#end
/** 导出表格 */
async function onExport() {
async function handleExport() {
try {
exportLoading.value = true;
const data = await export${simpleClassName}(queryParams);
@@ -200,7 +194,7 @@ try {
#if (${table.templateType} == 2)
/** 切换树形展开/收缩状态 */
const isExpanded = ref(true);
function toggleExpand() {
function handleExpand() {
isExpanded.value = !isExpanded.value;
tableRef.value?.setAllTreeExpand(isExpanded.value);
}
@@ -303,12 +297,12 @@ onMounted(() => {
<!-- 列表 -->
<ContentWrap title="${table.classComment}">
<template #extra>
<TableToolbar
<VbenVxeTableToolbar
ref="tableToolbarRef"
v-model:hidden-search="hiddenSearchBar"
>
#if (${table.templateType} == 2)
<Button @click="toggleExpand" class="mr-2">
<Button @click="handleExpand" class="mr-2">
{{ isExpanded ? '收缩' : '展开' }}
</Button>
#end
@@ -326,7 +320,7 @@ onMounted(() => {
type="primary"
class="ml-2"
:loading="exportLoading"
@click="onExport"
@click="handleExport"
v-access:code="['${permissionPrefix}:export']"
>
{{ $t('ui.actionTitle.export') }}

View File

@@ -3,29 +3,97 @@
#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { Rule } from 'ant-design-vue/es/form';
import type { Rule } from 'ant-design-vue/es/form';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { useVbenModal } from '@vben/common-ui';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload, FileUpload } from "#/components/upload";
import { message, Tabs, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'ant-design-vue';
import { DICT_TYPE, getDictOptions } from '#/utils';
import { computed, ref } from 'vue';
import { computed, ref } from 'vue';
import { $t } from '#/locales';
import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { cloneDeep, formatDateTime } from '@vben/utils';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload, FileUpload } from "#/components/upload";
import { message, Tabs, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'ant-design-vue';
import { get${subSimpleClassName}, create${subSimpleClassName}, update${subSimpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { $t } from '#/locales';
const emit = defineEmits(['success']);
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['${subTable.classComment}'])
: $t('ui.actionTitle.create', ['${subTable.classComment}']);
});
import { get${subSimpleClassName}, create${subSimpleClassName}, update${subSimpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
const formRef = ref();
const formData = ref<Partial<${simpleClassName}Api.${subSimpleClassName}>>({
const emit = defineEmits(['success']);
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['${subTable.classComment}'])
: $t('ui.actionTitle.create', ['${subTable.classComment}']);
});
const formRef = ref();
const formData = ref<Partial<${simpleClassName}Api.${subSimpleClassName}>>({
#foreach ($column in $subColumns)
#if ($column.createOperation || $column.updateOperation)
#if ($column.htmlType == "checkbox")
$column.javaField: [],
#else
$column.javaField: undefined,
#end
#end
#end
});
const rules: Record<string, Rule[]> = {
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键
#set($comment=$column.columnComment)
$column.javaField: [{ required: true, message: '${comment}不能为空', trigger: #if($column.htmlType == 'select')'change'#else'blur'#end }],
#end
#end
};
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
await formRef.value?.validate();
modalApi.lock();
// 提交表单
const data = formData.value as ${simpleClassName}Api.${subSimpleClassName};
try {
await (formData.value?.id ? update${subSimpleClassName}(data) : create${subSimpleClassName}(data));
// 关闭并提示
await modalApi.close();
emit('success');
message.success({
content: $t('ui.actionMessage.operationSuccess'),
});
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
resetForm()
return;
}
// 加载数据
let data = modalApi.getData<${simpleClassName}Api.${subSimpleClassName}>();
if (!data) {
return;
}
if (data.id) {
modalApi.lock();
try {
data = await get${subSimpleClassName}(data.id);
} finally {
modalApi.unlock();
}
}
// 设置到 values
formData.value = data;
},
});
/** 重置表单 */
function resetForm(){
formData.value = {
#foreach ($column in $subColumns)
#if ($column.createOperation || $column.updateOperation)
#if ($column.htmlType == "checkbox")
@@ -35,75 +103,9 @@
#end
#end
#end
});
const rules: Record<string, Rule[]> = {
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键
#set($comment=$column.columnComment)
$column.javaField: [{ required: true, message: '${comment}不能为空', trigger: #if($column.htmlType == 'select')'change'#else'blur'#end }],
#end
#end
};
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
await formRef.value?.validate();
modalApi.lock();
// 提交表单
const data = formData.value as ${simpleClassName}Api.${subSimpleClassName};
try {
await (formData.value?.id ? update${subSimpleClassName}(data) : create${subSimpleClassName}(data));
// 关闭并提示
await modalApi.close();
emit('success');
message.success({
content: $t('ui.actionMessage.operationSuccess'),
key: 'action_process_msg',
});
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
resetForm()
return;
}
// 加载数据
let data = modalApi.getData<${simpleClassName}Api.${subSimpleClassName}>();
if (!data) {
return;
}
if (data.id) {
modalApi.lock();
try {
data = await get${subSimpleClassName}(data.id);
} finally {
modalApi.unlock();
}
}
// 设置到 values
formData.value = data;
},
});
/** 重置表单 */
const resetForm = () => {
formData.value = {
#foreach ($column in $subColumns)
#if ($column.createOperation || $column.updateOperation)
#if ($column.htmlType == "checkbox")
$column.javaField: [],
#else
$column.javaField: undefined,
#end
#end
#end
};
formRef.value?.resetFields();
}
formRef.value?.resetFields();
}
</script>
<template>

View File

@@ -5,12 +5,15 @@
#set ($subClassNameVar = $subClassNameVars.get($subIndex))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { message, Tabs, Form, Input, Textarea,Button, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker } from 'ant-design-vue';
import { computed, ref, h, onMounted,watch,nextTick } from 'vue';
import { $t } from '#/locales';
import { DICT_TYPE, getDictOptions } from '#/utils';
import { computed, ref, h, onMounted,watch,nextTick } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { message, Tabs, Form, Input, Textarea,Button, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker } from 'ant-design-vue';
import { $t } from '#/locales';
#if ($subTable.subJoinMany) ## 一对多
import type { VxeTableInstance } from '#/adapter/vxe-table';
@@ -30,13 +33,14 @@ const props = defineProps<{
#if ($subTable.subJoinMany) ## 一对多
const list = ref<${simpleClassName}Api.${subSimpleClassName}[]>([]) // 列表的数据
const tableRef = ref<VxeTableInstance>();
/** 添加${subTable.classComment} */
const onAdd = async () => {
async function handleAdd() {
await tableRef.value?.insertAt({} as ${simpleClassName}Api.${subSimpleClassName}, -1);
}
/** 删除${subTable.classComment} */
const onDelete = async (row: ${simpleClassName}Api.${subSimpleClassName}) => {
async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
await tableRef.value?.remove(row);
}
@@ -46,9 +50,11 @@ defineExpose({
const data = list.value as ${simpleClassName}Api.${subSimpleClassName}[];
const removeRecords = tableRef.value?.getRemoveRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
const insertRecords = tableRef.value?.getInsertRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
return data
.filter((row) => !removeRecords.some((removed) => removed.id === row.id))
?.concat(insertRecords.map((row: any) => ({ ...row, id: undefined })));
return [
...data.filter(
(row) => !removeRecords.some((removed) => removed.id === row.id),
),
...insertRecords.map((row: any) => ({ ...row, id: undefined })),
},
});
@@ -123,25 +129,25 @@ watch(
#if ( $column.id == $subJoinColumn.id) ## 特殊:忽略主子表的 join 字段,不用填写
#elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
<vxe-column field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<template #default="{ row }">
<Input v-model:value="row.${javaField}" />
</template>
</vxe-column>
#elseif($column.htmlType == "imageUpload")## 图片上传
<vxe-column field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<template #default="{ row }">
<ImageUpload v-model:value="row.${javaField}" />
</template>
</vxe-column>
#elseif($column.htmlType == "fileUpload")## 文件上传
<vxe-column field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<template #default="{ row }">
<FileUpload v-model:value="row.${javaField}" />
</template>
</vxe-column>
#elseif($column.htmlType == "select")## 下拉框
<vxe-column field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<template #default="{ row }">
<Select v-model:value="row.${javaField}" placeholder="请选择${comment}">
#if ("" != $dictType)## 有数据字典
<Select.Option
@@ -159,7 +165,7 @@ watch(
</vxe-column>
#elseif($column.htmlType == "checkbox")## 多选框
<vxe-column field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<template #default="{ row }">
<CheckboxGroup v-model:value="row.${javaField}">
#if ("" != $dictType)## 有数据字典
<Checkbox
@@ -177,7 +183,7 @@ watch(
</vxe-column>
#elseif($column.htmlType == "radio")## 单选框
<vxe-column field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<template #default="{ row }">
<RadioGroup v-model:value="row.${javaField}">
#if ("" != $dictType)## 有数据字典
<Radio
@@ -195,7 +201,7 @@ watch(
</vxe-column>
#elseif($column.htmlType == "datetime")## 时间框
<vxe-column field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<template #default="{ row }">
<DatePicker
v-model:value="row.${javaField}"
:showTime="true"
@@ -206,7 +212,7 @@ watch(
</vxe-column>
#elseif($column.htmlType == "textarea" || $column.htmlType == "editor")## 文本框
<vxe-column field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<template #default="{ row }">
<Textarea v-model:value="row.${javaField}" />
</template>
</vxe-column>
@@ -214,12 +220,12 @@ watch(
#end
#end
<vxe-column field="operation" title="操作" align="center">
<template #default="{row}">
<template #default="{ row }">
<Button
size="small"
type="link"
danger
@click="onDelete(row as any)"
@click="handleDelect(row)"
v-access:code="['${permissionPrefix}:delete']"
>
{{ $t('ui.actionTitle.delete') }}
@@ -228,7 +234,7 @@ watch(
</vxe-column>
</vxe-table>
<div class="flex justify-center mt-4">
<Button :icon="h(Plus)" type="primary" ghost @click="onAdd" v-access:code="['${permissionPrefix}:create']">
<Button :icon="h(Plus)" type="primary" ghost @click="handleAdd" v-access:code="['${permissionPrefix}:create']">
{{ $t('ui.actionTitle.create', ['${subTable.classComment}']) }}
</Button>
</div>

View File

@@ -6,48 +6,50 @@
#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($subIndex))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { VxeTableInstance } from '#/adapter/vxe-table';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { VxeTableInstance } from '#/adapter/vxe-table';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { reactive,ref, h, nextTick,watch,onMounted } from 'vue';
import { cloneDeep, formatDateTime } from '@vben/utils';
import { ContentWrap } from '#/components/content-wrap';
import { reactive,ref, h, nextTick,watch,onMounted } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { DictTag } from '#/components/dict-tag';
import { getRangePickerDefaultProps } from '#/utils';
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { ContentWrap } from '#/components/content-wrap';
#if ($table.templateType == 11) ## erp
import { useVbenModal } from '@vben/common-ui';
import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue'
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload, FileUpload } from "#/components/upload";
import { message,Button, Tabs,Pagination, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox,RangePicker, DatePicker, TreeSelect } from 'ant-design-vue';
import { Plus, Trash2 } from '@vben/icons';
import { $t } from '#/locales';
import { TableToolbar } from '#/components/table-toolbar';
import { useTableToolbar } from '#/hooks';
import { useVbenModal } from '@vben/common-ui';
import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue'
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload, FileUpload } from "#/components/upload";
import { message,Button, Tabs,Pagination, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox,RangePicker, DatePicker, TreeSelect } from 'ant-design-vue';
import { Plus, Trash2 } from '@vben/icons';
import { $t } from '#/locales';
#end
#if ($table.templateType == 11) ## erp
import { delete${subSimpleClassName},#if ($deleteBatchEnable) delete${subSimpleClassName}List,#end get${subSimpleClassName}Page } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { isEmpty } from '@vben/utils';
#else
#if ($subTable.subJoinMany) ## 一对多
import { get${subSimpleClassName}ListBy${SubJoinColumnName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
#else
import { get${subSimpleClassName}By${SubJoinColumnName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
#end
import { delete${subSimpleClassName},#if ($deleteBatchEnable) delete${subSimpleClassName}List,#end get${subSimpleClassName}Page } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { isEmpty } from '@vben/utils';
#else
#if ($subTable.subJoinMany) ## 一对多
import { get${subSimpleClassName}ListBy${SubJoinColumnName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
#else
import { get${subSimpleClassName}By${SubJoinColumnName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
#end
#end
const props = defineProps<{
${subJoinColumn.javaField}?: number // ${subJoinColumn.columnComment}(主表的关联字段)
${subJoinColumn.javaField}?: number // ${subJoinColumn.columnComment}(主表的关联字段)
}>()
#if ($table.templateType == 11) ## erp
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: ${subSimpleClassName}Form,
destroyOnClose: true,
});
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: ${subSimpleClassName}Form,
destroyOnClose: true,
});
/** 创建${subTable.classComment} */
function handleCreate() {
@@ -68,14 +70,10 @@ async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]),
duration: 0,
key: 'action_process_msg',
});
try {
await delete${subSimpleClassName}(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_process_msg',
});
await delete${subSimpleClassName}(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
await getList();
} finally {
hideLoading();
@@ -88,12 +86,11 @@ async function handleDeleteBatch() {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting'),
duration: 0,
key: 'action_process_msg',
});
try {
await delete${subSimpleClassName}List(checkedIds.value);
checkedIds.value = [];
message.success( $t('ui.actionMessage.deleteSuccess') );
message.success($t('ui.actionMessage.deleteSuccess'));
await getList();
} finally {
hideLoading();
@@ -106,92 +103,92 @@ function handleRowCheckboxChange({
}: {
records: ${simpleClassName}Api.${subSimpleClassName}[];
}) {
checkedIds.value = records.map((item) => item.id);
checkedIds.value = records.map((item) => item.id!);
}
#end
#end
const loading = ref(true) // 列表的加载中
const list = ref<${simpleClassName}Api.${subSimpleClassName}[]>([]) // 列表的数据
const loading = ref(true) // 列表的加载中
const list = ref<${simpleClassName}Api.${subSimpleClassName}[]>([]) // 列表的数据
#if ($table.templateType == 11) ## erp
const total = ref(0) // 列表的总页数
const total = ref(0) // 列表的总页数
#end
#if ($table.templateType == 11) ## erp
const queryFormRef = ref() // 搜索的表单
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
#foreach ($column in $subColumns)
#if ($column.listOperation)
#if ($column.listOperationCondition != 'BETWEEN')
$column.javaField: undefined,
#end
#if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")
$column.javaField: undefined,
#end
#end
const queryFormRef = ref() // 搜索的表单
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
#foreach ($column in $subColumns)
#if ($column.listOperation)
#if ($column.listOperationCondition != 'BETWEEN')
$column.javaField: undefined,
#end
})
#if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")
$column.javaField: undefined,
#end
#end
#end
})
/** 搜索按钮操作 */
const handleQuery = () => {
function handleQuery() {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
function resetQuery() {
queryFormRef.value.resetFields()
handleQuery()
}
#end
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
if (!props.${subJoinColumn.javaField}){
return []
}
## 特殊:树表专属逻辑(树不需要分页接口)
#if ($table.templateType == 11) ## erp
const params = cloneDeep(queryParams) as any;
#foreach ($column in $columns)
#if ($column.listOperation)
#if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")
if (params.${column.javaField} && Array.isArray(params.${column.javaField})) {
params.${column.javaField} = (params.${column.javaField} as string[]).join(',');
}
#end
#end
#end
params.${subJoinColumn.javaField} = props.${subJoinColumn.javaField};
const data = await get${subSimpleClassName}Page(params)
list.value = data.list
total.value = data.total
#else
#if ($subTable.subJoinMany) ## 一对多
list.value = await get${subSimpleClassName}ListBy${SubJoinColumnName}(props.${subJoinColumn.javaField}!);
#else
list.value = [await get${subSimpleClassName}By${SubJoinColumnName}(props.${subJoinColumn.javaField}!)];
#end
#end
} finally {
loading.value = false
async function getList() {
loading.value = true
try {
if (!props.${subJoinColumn.javaField}){
return []
}
## 特殊:树表专属逻辑(树不需要分页接口)
#if ($table.templateType == 11) ## erp
const params = cloneDeep(queryParams) as any;
#foreach ($column in $columns)
#if ($column.listOperation)
#if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")
if (params.${column.javaField} && Array.isArray(params.${column.javaField})) {
params.${column.javaField} = (params.${column.javaField} as string[]).join(',');
}
#end
#end
#end
params.${subJoinColumn.javaField} = props.${subJoinColumn.javaField};
const data = await get${subSimpleClassName}Page(params)
list.value = data.list
total.value = data.total
#else
#if ($subTable.subJoinMany) ## 一对多
list.value = await get${subSimpleClassName}ListBy${SubJoinColumnName}(props.${subJoinColumn.javaField}!);
#else
list.value = [await get${subSimpleClassName}By${SubJoinColumnName}(props.${subJoinColumn.javaField}!)];
#end
#end
} finally {
loading.value = false
}
}
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.${subJoinColumn.javaField},
async (val) => {
if (!val) {
return;
}
await nextTick();
await getList()
},
{ immediate: true },
);
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.${subJoinColumn.javaField},
async (val) => {
if (!val) {
return;
}
await nextTick();
await getList()
},
{ immediate: true },
);
#if ($table.templateType == 11) ## erp
/** 初始化 */
@@ -292,7 +289,7 @@ onMounted(() => {
<!-- 列表 -->
<ContentWrap title="${table.classComment}">
<template #extra>
<TableToolbar
<VbenVxeTableToolbar
ref="tableToolbarRef"
v-model:hidden-search="hiddenSearchBar"
>

View File

@@ -1,167 +1 @@
import type { PageParam, PageResult } from '@vben/request';
import type { Dayjs } from 'dayjs';
import { requestClient } from '#/api/request';
#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}")
export namespace ${simpleClassName}Api {
## 特殊:主子表专属逻辑
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subColumns = $subColumnsList.get($index))##当前字段数组
/** ${subTable.classComment}信息 */
export interface ${subSimpleClassName} {
#foreach ($column in $subColumns)
#if ($column.createOperation || $column.updateOperation)
#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: number; // ${column.columnComment}
#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: string | Dayjs; // ${column.columnComment}
#else
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: ${column.javaType.toLowerCase()}; // ${column.columnComment}
#end
#end
#end
}
#end
/** ${table.classComment}信息 */
export interface ${simpleClassName} {
#foreach ($column in $columns)
#if ($column.createOperation || $column.updateOperation)
#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: number; // ${column.columnComment}
#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: string | Dayjs; // ${column.columnComment}
#else
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: ${column.javaType.toLowerCase()}; // ${column.columnComment}
#end
#end
#end
#if ( $table.templateType == 2 )
children?: ${simpleClassName}[];
#end
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#if ( $subTable.subJoinMany )
${subSimpleClassName.toLowerCase()}s?: ${subSimpleClassName}[]
#else
${subSimpleClassName.toLowerCase()}?: ${subSimpleClassName}
#end
#end
#end
}
}
#if ( $table.templateType != 2 )
/** 查询${table.classComment}分页 */
export function get${simpleClassName}Page(params: PageParam) {
return requestClient.get<PageResult<${simpleClassName}Api.${simpleClassName}>>('${baseURL}/page', { params });
}
#else
/** 查询${table.classComment}列表 */
export function get${simpleClassName}List(params: any) {
return requestClient.get<${simpleClassName}Api.${simpleClassName}[]>('${baseURL}/list', { params });
}
#end
/** 查询${table.classComment}详情 */
export function get${simpleClassName}(id: number) {
return requestClient.get<${simpleClassName}Api.${simpleClassName}>(`${baseURL}/get?id=${id}`);
}
/** 新增${table.classComment} */
export function create${simpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) {
return requestClient.post('${baseURL}/create', data);
}
/** 修改${table.classComment} */
export function update${simpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) {
return requestClient.put('${baseURL}/update', data);
}
/** 删除${table.classComment} */
export function delete${simpleClassName}(id: number) {
return requestClient.delete(`${baseURL}/delete?id=${id}`);
}
#if ( $table.templateType != 2 && $deleteBatchEnable)
/** 批量删除${table.classComment} */
export function delete${simpleClassName}ListByIds(ids: number[]) {
return requestClient.delete(`${baseURL}/delete-list?ids=${ids.join(',')}`)
}
#end
/** 导出${table.classComment} */
export function export${simpleClassName}(params: any) {
return requestClient.download('${baseURL}/export-excel', params);
}
## 特殊:主子表专属逻辑
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段
#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index))
#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
// ==================== 子表($subTable.classComment ====================
## 情况一MASTER_ERP 时,需要分查询页子表
#if ( $table.templateType == 11 )
/** 获得${subTable.classComment}分页 */
export function get${subSimpleClassName}Page(params: PageParam) {
return requestClient.get<PageResult<${simpleClassName}Api.${subSimpleClassName}>>(`${baseURL}/${subSimpleClassName_strikeCase}/page`, { params });
}
## 情况二:非 MASTER_ERP 时,需要列表查询子表
#else
#if ( $subTable.subJoinMany )
/** 获得${subTable.classComment}列表 */
export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}: number) {
return requestClient.get<${simpleClassName}Api.${subSimpleClassName}[]>(`${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`);
}
#else
/** 获得${subTable.classComment} */
export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}: number) {
return requestClient.get<${simpleClassName}Api.${subSimpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`);
}
#end
#end
## 特殊MASTER_ERP 时,支持单个的新增、修改、删除操作
#if ( $table.templateType == 11 )
/** 新增${subTable.classComment} */
export function create${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) {
return requestClient.post(`${baseURL}/${subSimpleClassName_strikeCase}/create`, data);
}
/** 修改${subTable.classComment} */
export function update${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) {
return requestClient.put(`${baseURL}/${subSimpleClassName_strikeCase}/update`, data);
}
/** 删除${subTable.classComment} */
export function delete${subSimpleClassName}(id: number) {
return requestClient.delete(`${baseURL}/${subSimpleClassName_strikeCase}/delete?id=${id}`);
}
#if ($deleteBatchEnable)
/** 批量删除${subTable.classComment} */
export function delete${subSimpleClassName}ListByIds(ids: number[]) {
return requestClient.delete(`${baseURL}/${subSimpleClassName_strikeCase}/delete-list?ids=${ids.join(',')}`)
}
#end
/** 获得${subTable.classComment} */
export function get${subSimpleClassName}(id: number) {
return requestClient.get<${simpleClassName}Api.${subSimpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get?id=${id}`);
}
#end
#end
#parse("codegen/vue3_vben5_ele/schema/api/api.ts.vm")

View File

@@ -1,18 +1,17 @@
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
import { z } from '#/adapter/form';
import {
DICT_TYPE,
getDictOptions,
getRangePickerDefaultProps,
} from '#/utils';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
#if(${table.templateType} == 2)## 树表需要导入这些
import { get${simpleClassName}List } from '#/api/${table.moduleName}/${table.businessName}';
import { handleTree } from '@vben/utils';
#end
import { getRangePickerDefaultProps } from '#/utils';
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
@@ -186,7 +185,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<${simpleClassName}Api.${simpleClassName}>['columns'] {
export function useGridColumns(): VxeTableGridOptions<${apiName}.${simpleClassName}>['columns'] {
return [
#if ($table.templateType != 2 && $deleteBatchEnable)
{ type: 'checkbox', width: 40 },
@@ -389,7 +388,7 @@ export function use${subSimpleClassName}GridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>['columns'] {
export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${apiName}.${subSimpleClassName}>['columns'] {
return [
#if ($table.templateType != 2 && $deleteBatchEnable)
{ type: 'checkbox', width: 40 },
@@ -426,7 +425,7 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${sim
#else
#if ($subTable.subJoinMany) ## 一对多
/** 新增/修改列表的字段 */
export function use${subSimpleClassName}GridEditColumns(): VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>['columns'] {
export function use${subSimpleClassName}GridEditColumns(): VxeTableGridOptions<${apiName}.${subSimpleClassName}>['columns'] {
return [
#foreach($column in $subColumns)
#if ($column.createOperation || $column.updateOperation)
@@ -602,4 +601,4 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${sim
}
#end
#end
#end
#end

View File

@@ -1,8 +1,16 @@
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message, Tabs, Checkbox, Input, Textarea, Select,RadioGroup,CheckboxGroup, DatePicker } from 'ant-design-vue';
import { message#if ($table.templateType == 11), Tabs#end } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { create${simpleClassName}, get${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
import { $t } from '#/locales';
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
#foreach ($subSimpleClassName in $subSimpleClassNames)
@@ -12,21 +20,15 @@ import { message, Tabs, Checkbox, Input, Textarea, Select,RadioGroup,CheckboxGro
#end
#end
import { computed, ref } from 'vue';
import { $t } from '#/locales';
import { useVbenForm } from '#/adapter/form';
import { get${simpleClassName}, create${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<${simpleClassName}Api.${simpleClassName}>();
const formData = ref<${apiName}.${simpleClassName}>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['${table.classComment}'])
: $t('ui.actionTitle.create', ['${table.classComment}']);
});
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
#if ( $subTables && $subTables.size() > 0 )
@@ -51,7 +53,7 @@ const [Form, formApi] = useVbenForm({
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
@@ -60,8 +62,8 @@ const [Modal, modalApi] = useVbenModal({
if (!valid) {
return;
}
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
#if ( $subTables && $subTables.size() > 0 )
// 校验子表单
#foreach ($subTable in $subTables)
@@ -78,12 +80,12 @@ const [Modal, modalApi] = useVbenModal({
#end
#end
#end
#end
#end
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as ${simpleClassName}Api.${simpleClassName};
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
const data = (await formApi.getValues()) as ${apiName}.${simpleClassName};
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
#if ( $subTables && $subTables.size() > 0 )
// 拼接子表的数据
#foreach ($subTable in $subTables)
@@ -96,13 +98,13 @@ const [Modal, modalApi] = useVbenModal({
#end
#end
#end
#end
#end
try {
await (formData.value?.id ? update${simpleClassName}(data) : create${simpleClassName}(data));
// 关闭并提示
await modalApi.close();
emit('success');
message.success( $t('ui.actionMessage.operationSuccess') );
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
@@ -113,21 +115,24 @@ const [Modal, modalApi] = useVbenModal({
return;
}
// 加载数据
let data = modalApi.getData<${simpleClassName}Api.${simpleClassName}>();
if (!data) {
const data = modalApi.getData<${apiName}.${simpleClassName}>();
if (!data || !data.id) {
#if (${table.templateType} == 2)## 树表特有
// 设置上级
await formApi.setValues(data);
#end
return;
}
if (data.id) {
modalApi.lock();
try {
data = await get${simpleClassName}(data.id);
} finally {
modalApi.unlock();
modalApi.lock();
try {
formData.value = await get${simpleClassName}(data.id);
// 设置到 values
if (formData.value) {
await formApi.setValues(formData.value);
}
} finally {
modalApi.unlock();
}
// 设置到 values
formData.value = data;
await formApi.setValues(formData.value);
},
});
</script>
@@ -135,8 +140,8 @@ const [Modal, modalApi] = useVbenModal({
<template>
<Modal :title="getTitle">
<Form class="mx-4" />
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
<!-- 子表的表单 -->
<Tabs v-model:active-key="subTabsName">
#foreach ($subTable in $subTables)
@@ -149,6 +154,6 @@ const [Modal, modalApi] = useVbenModal({
</Tabs.TabPane>
#end
</Tabs>
#end
#end
</Modal>
</template>

View File

@@ -1,56 +1,67 @@
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
import { Page, useVbenModal } from '@vben/common-ui';
import { message,Tabs } from 'ant-design-vue';
import { ref } from 'vue';
import {#if ($table.templateType != 2 && $deleteBatchEnable) confirm,#end Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart#if ($table.templateType != 2 && $deleteBatchEnable), isEmpty#end } from '@vben/utils';
import { message#if ($table.templateType == 11), Tabs#end } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
#if (${table.templateType} == 2)## 树表接口
import {
delete${simpleClassName},
export${simpleClassName},
get${simpleClassName}List,
} from '#/api/${table.moduleName}/${table.businessName}';
#else## 标准表接口
import {
delete${simpleClassName},#if ($deleteBatchEnable)
delete${simpleClassName}List,#end
export${simpleClassName},
get${simpleClassName}Page,
} from '#/api/${table.moduleName}/${table.businessName}';
#end
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
## 特殊:主子表专属逻辑
#if ( $table.templateType == 11 || $table.templateType == 12 )
#foreach ($subSimpleClassName in $subSimpleClassNames)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index))
import ${subSimpleClassName}List from './modules/${subSimpleClassName_strikeCase}-list.vue'
import ${subSimpleClassName}List from './modules/${subSimpleClassName_strikeCase}-list.vue';
#end
#end
import { ref, computed } from 'vue';
import { $t } from '#/locales';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
#if (${table.templateType} == 2)## 树表接口
import { get${simpleClassName}List, delete${simpleClassName}, export${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
#else## 标准表接口
import { get${simpleClassName}Page, delete${simpleClassName},#if ($deleteBatchEnable) delete${simpleClassName}List,#end export${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
#end
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
import { useGridColumns, useGridFormSchema } from './data';
#if ($table.templateType == 12 || $table.templateType == 11) ## 内嵌和erp情况
/** 子表的列表 */
const subTabsName = ref('$subClassNameVars.get(0)')
#if ($table.templateType == 11)
const select${simpleClassName} = ref<${simpleClassName}Api.${simpleClassName}>();
const select${simpleClassName} = ref<${apiName}.${simpleClassName}>();
#end
#end
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
#if (${table.templateType} == 2)## 树表特有:控制表格展开收缩
/** 切换树形展开/收缩状态 */
const isExpanded = ref(true);
function toggleExpand() {
function handleExpand() {
isExpanded.value = !isExpanded.value;
gridApi.grid.setAllTreeExpand(isExpanded.value);
}
#end
/** 刷新表格 */
function onRefresh() {
function handleRefresh() {
#if ($table.templateType == 12) ## 内嵌情况
gridApi.reload();
#else
@@ -60,34 +71,31 @@ function onRefresh() {
/** 创建${table.classComment} */
function handleCreate() {
formModalApi.setData({}).open();
formModalApi.setData(null).open();
}
/** 编辑${table.classComment} */
function handleEdit(row: ${simpleClassName}Api.${simpleClassName}) {
formModalApi.setData(row).open();
}
#if (${table.templateType} == 2)## 树表特有:新增下级
/** 新增下级${table.classComment} */
function handleAppend(row: ${simpleClassName}Api.${simpleClassName}) {
/** 添加下级${table.classComment} */
function handleAppend(row: ${apiName}.${simpleClassName}) {
formModalApi.setData({ ${treeParentColumn.javaField}: row.id }).open();
}
#end
/** 编辑${table.classComment} */
function handleEdit(row: ${apiName}.${simpleClassName}) {
formModalApi.setData(row).open();
}
/** 删除${table.classComment} */
async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) {
async function handleDelete(row: ${apiName}.${simpleClassName}) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]),
key: 'action_key_msg',
duration: 0,
});
try {
await delete${simpleClassName}(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_key_msg',
});
onRefresh();
await delete${simpleClassName}(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
handleRefresh();
} finally {
hideLoading();
}
@@ -96,30 +104,28 @@ async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) {
#if ($table.templateType != 2 && $deleteBatchEnable)
/** 批量删除${table.classComment} */
async function handleDeleteBatch() {
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting'),
key: 'action_key_msg',
content: $t('ui.actionMessage.deletingBatch'),
duration: 0,
});
try {
await delete${simpleClassName}List(checkedIds.value);
checkedIds.value = [];
message.success({
content: $t('ui.actionMessage.deleteSuccess'),
key: 'action_key_msg',
});
onRefresh();
message.success($t('ui.actionMessage.deleteSuccess'));
handleRefresh();
} finally {
hideLoading();
}
}
const checkedIds = ref<number[]>([])
const checkedIds = ref<number[]>([]);
function handleRowCheckboxChange({
records,
}: {
records: ${simpleClassName}Api.${simpleClassName}[];
records: ${apiName}.${simpleClassName}[];
}) {
checkedIds.value = records.map((item) => item.id);
checkedIds.value = records.map((item) => item.id!);
}
#end
@@ -141,20 +147,11 @@ const [Grid, gridApi] = useVbenVxeGrid({
height: 'auto',
#end
#if (${table.templateType} == 2)## 树表设置
treeConfig: {
parentField: '${treeParentColumn.javaField}',
rowField: 'id',
transform: true,
expandAll: true,
reserve: true,
},
pagerConfig: {
enabled: false,
},
#else## 标准表设置
pagerConfig: {
enabled: true,
enabled: false,
},
#else## 标准表设置
keepSource: true,
#end
proxyConfig: {
ajax: {
@@ -184,27 +181,35 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true,
search: true,
},
} as VxeTableGridOptions<${simpleClassName}Api.${simpleClassName}>,
#if (${table.templateType} == 11 || $deleteBatchEnable)
gridEvents:{
#if(${table.templateType} == 11)
cellClick: ({ row }: { row: ${simpleClassName}Api.${simpleClassName}}) => {
#if (${table.templateType} == 2)## 树表设置
treeConfig: {
parentField: '${treeParentColumn.javaField}',
rowField: 'id',
transform: true,
expandAll: true,
reserve: true,
},
#end
} as VxeTableGridOptions<${apiName}.${simpleClassName}>,
#if (${table.templateType} == 11 || (${table.templateType} != 2 && $deleteBatchEnable))
gridEvents: {
#if(${table.templateType} == 11)
cellClick: ({ row }: { row: ${apiName}.${simpleClassName}}) => {
select${simpleClassName}.value = row;
},
#end
#if(${table.templateType} != 2 && $deleteBatchEnable)
checkboxAll: handleRowCheckboxChange,
checkboxChange: handleRowCheckboxChange,
#end
}
#end
#if (${table.templateType} != 2 && $deleteBatchEnable)
checkboxAll: handleRowCheckboxChange,
checkboxChange: handleRowCheckboxChange,
#end
},
#end
});
</script>
<template>
<Page auto-content-height>
<FormModal @success="onRefresh" />
<FormModal @success="handleRefresh" />
#if ($table.templateType == 11) ## erp情况
<div>
#end
@@ -228,13 +233,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
<template #toolbar-tools>
<TableAction
:actions="[
#if (${table.templateType} == 2)## 树表特有:展开/收缩按钮
{
label: isExpanded ? '收缩' : '展开',
type: 'primary',
onClick: toggleExpand,
},
#end
{
label: $t('ui.actionTitle.create', ['${table.classComment}']),
type: 'primary',
@@ -242,6 +240,13 @@ const [Grid, gridApi] = useVbenVxeGrid({
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:create'],
onClick: handleCreate,
},
#if (${table.templateType} == 2)## 树表特有:展开/收缩按钮
{
label: isExpanded ? '收缩' : '展开',
type: 'primary',
onClick: handleExpand,
},
#end
{
label: $t('ui.actionTitle.export'),
type: 'primary',
@@ -255,8 +260,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
type: 'primary',
danger: true,
icon: ACTION_ICON.DELETE,
disabled: isEmpty(checkedIds),
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:delete'],
disabled: isEmpty(checkedIds),
onClick: handleDeleteBatch,
},
#end
@@ -297,7 +302,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
/>
</template>
</Grid>
#if ($table.templateType == 11) ## erp情况
<!-- 子表的表单 -->
<Tabs v-model:active-key="subTabsName" class="mt-2">
@@ -314,4 +318,4 @@ const [Grid, gridApi] = useVbenVxeGrid({
</div>
#end
</Page>
</template>
</template>

View File

@@ -2,8 +2,9 @@
#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组
#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
@@ -16,7 +17,7 @@
import { use${subSimpleClassName}FormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<${simpleClassName}Api.${subSimpleClassName}>();
const formData = ref<${apiName}.${subSimpleClassName}>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['${subTable.classComment}'])
@@ -42,10 +43,9 @@
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as ${simpleClassName}Api.${subSimpleClassName};
const data = (await formApi.getValues()) as ${apiName}.${subSimpleClassName};
data.${subJoinColumn.javaField} = formData.value?.${subJoinColumn.javaField};
try {
await (formData.value?.id ? update${subSimpleClassName}(data) : create${subSimpleClassName}(data));
@@ -62,9 +62,8 @@
formData.value = undefined;
return;
}
// 加载数据
let data = modalApi.getData<${simpleClassName}Api.${subSimpleClassName}>();
let data = modalApi.getData<${apiName}.${subSimpleClassName}>();
if (!data) {
return;
}

View File

@@ -4,10 +4,12 @@
#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
#set ($subClassNameVar = $subClassNameVars.get($subIndex))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
import { computed, ref, h, onMounted,watch,nextTick } from 'vue';
import { $t } from '#/locales';
#if ($subTable.subJoinMany) ## 一对多
@@ -33,10 +35,10 @@ const props = defineProps<{
function onActionClick({
code,
row,
}: OnActionClickParams<${simpleClassName}Api.${subSimpleClassName}>) {
}: OnActionClickParams<${apiName}.${subSimpleClassName}>) {
switch (code) {
case 'delete': {
onDelete(row);
handleDelete(row);
break;
}
}
@@ -62,21 +64,21 @@ gridOptions: {
});
/** 添加${subTable.classComment} */
const onAdd = async () => {
await gridApi.grid.insertAt({} as ${simpleClassName}Api.${subSimpleClassName}, -1);
const handleAdd = async () => {
await gridApi.grid.insertAt({} as ${apiName}.${subSimpleClassName}, -1);
}
/** 删除${subTable.classComment} */
const onDelete = async (row: ${simpleClassName}Api.${subSimpleClassName}) => {
const handleDelete = async (row: ${apiName}.${subSimpleClassName}) => {
await gridApi.grid.remove(row);
}
/** 提供获取表格数据的方法供父组件调用 */
defineExpose({
getData: (): ${simpleClassName}Api.${subSimpleClassName}[] => {
const data = gridApi.grid.getData() as ${simpleClassName}Api.${subSimpleClassName}[];
const removeRecords = gridApi.grid.getRemoveRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
const insertRecords = gridApi.grid.getInsertRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
getData: (): ${apiName}.${subSimpleClassName}[] => {
const data = gridApi.grid.getData() as ${apiName}.${subSimpleClassName}[];
const removeRecords = gridApi.grid.getRemoveRecords() as ${apiName}.${subSimpleClassName}[];
const insertRecords = gridApi.grid.getInsertRecords() as ${apiName}.${subSimpleClassName}[];
return data
.filter((row) => !removeRecords.some((removed) => removed.id === row.id))
.concat(insertRecords.map((row: any) => ({ ...row, id: undefined })));
@@ -186,7 +188,7 @@ watch(
#end
</Grid>
<div class="flex justify-center -mt-4">
<Button :icon="h(Plus)" type="primary" ghost @click="onAdd" v-access:code="['${subTable.moduleName}:${simpleClassName_strikeCase}:create']">
<Button :icon="h(Plus)" type="primary" ghost @click="handleAdd" v-access:code="['${subTable.moduleName}:${simpleClassName_strikeCase}:create']">
{{ $t('ui.actionTitle.create', ['${subTable.classComment}']) }}
</Button>
</div>

View File

@@ -5,14 +5,15 @@
#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($subIndex))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
#if ($table.templateType == 11) ## erp
import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue'
#end
import { useVbenModal } from '@vben/common-ui';
import { confirm, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ref, computed, nextTick,watch } from 'vue';
import { $t } from '#/locales';
@@ -51,23 +52,20 @@ function handleCreate() {
}
/** 编辑${subTable.classComment} */
function handleEdit(row: ${simpleClassName}Api.${subSimpleClassName}) {
function handleEdit(row: ${apiName}.${subSimpleClassName}) {
formModalApi.setData(row).open();
}
/** 删除${subTable.classComment} */
async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
async function handleDelete(row: ${apiName}.${subSimpleClassName}) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]),
key: 'action_process_msg',
duration: 0,
});
try {
await delete${subSimpleClassName}(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_key_msg',
});
onRefresh();
await delete${subSimpleClassName}(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
handleRefresh();
} finally {
hideLoading();
}
@@ -76,18 +74,16 @@ async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
#if ($deleteBatchEnable)
/** 批量删除${subTable.classComment} */
async function handleDeleteBatch() {
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting'),
key: 'action_key_msg',
content: $t('ui.actionMessage.deletingBatch'),
duration: 0,
});
try {
await delete${subSimpleClassName}List(checkedIds.value);
checkedIds.value = [];
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_key_msg',
});
onRefresh();
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
handleRefresh();
} finally {
hideLoading();
}
@@ -97,9 +93,9 @@ const checkedIds = ref<number[]>([])
function handleRowCheckboxChange({
records,
}: {
records: ${simpleClassName}Api.${subSimpleClassName}[];
records: ${apiName}.${subSimpleClassName}[];
}) {
checkedIds.value = records.map((item) => item.id);
checkedIds.value = records.map((item) => item.id!);
}
#end
@@ -149,7 +145,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
keyField: 'id',
isHover: true,
},
} as VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>,
} as VxeTableGridOptions<${apiName}.${subSimpleClassName}>,
#if (${table.templateType} == 11 && $deleteBatchEnable)
gridEvents:{
checkboxAll: handleRowCheckboxChange,
@@ -159,7 +155,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
});
/** 刷新表格 */
async function onRefresh() {
async function handleRefresh() {
#if ($table.templateType == 11) ## erp
await gridApi.query();
#else
@@ -179,7 +175,7 @@ watch(
return;
}
await nextTick();
await onRefresh()
await handleRefresh()
},
{ immediate: true },
);
@@ -187,7 +183,7 @@ watch(
<template>
#if ($table.templateType == 11) ## erp
<FormModal @success="onRefresh" />
<FormModal @success="handleRefresh" />
<Grid table-title="${subTable.classComment}列表">
<template #toolbar-tools>
<TableAction

View File

@@ -0,0 +1,167 @@
import type { PageParam, PageResult } from '@vben/request';
import type { Dayjs } from 'dayjs';
import { requestClient } from '#/api/request';
#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}")
export namespace ${simpleClassName}Api {
## 特殊:主子表专属逻辑
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subColumns = $subColumnsList.get($index))##当前字段数组
/** ${subTable.classComment}信息 */
export interface ${subSimpleClassName} {
#foreach ($column in $subColumns)
#if ($column.createOperation || $column.updateOperation)
#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: number; // ${column.columnComment}
#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: string | Dayjs; // ${column.columnComment}
#else
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: ${column.javaType.toLowerCase()}; // ${column.columnComment}
#end
#end
#end
}
#end
/** ${table.classComment}信息 */
export interface ${simpleClassName} {
#foreach ($column in $columns)
#if ($column.createOperation || $column.updateOperation)
#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: number; // ${column.columnComment}
#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: string | Dayjs; // ${column.columnComment}
#else
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: ${column.javaType.toLowerCase()}; // ${column.columnComment}
#end
#end
#end
#if ( $table.templateType == 2 )
children?: ${simpleClassName}[];
#end
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#if ( $subTable.subJoinMany )
${subSimpleClassName.toLowerCase()}s?: ${subSimpleClassName}[]
#else
${subSimpleClassName.toLowerCase()}?: ${subSimpleClassName}
#end
#end
#end
}
}
#if ( $table.templateType != 2 )
/** 查询${table.classComment}分页 */
export function get${simpleClassName}Page(params: PageParam) {
return requestClient.get<PageResult<${simpleClassName}Api.${simpleClassName}>>('${baseURL}/page', { params });
}
#else
/** 查询${table.classComment}列表 */
export function get${simpleClassName}List(params: any) {
return requestClient.get<${simpleClassName}Api.${simpleClassName}[]>('${baseURL}/list', { params });
}
#end
/** 查询${table.classComment}详情 */
export function get${simpleClassName}(id: number) {
return requestClient.get<${simpleClassName}Api.${simpleClassName}>(`${baseURL}/get?id=${id}`);
}
/** 新增${table.classComment} */
export function create${simpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) {
return requestClient.post('${baseURL}/create', data);
}
/** 修改${table.classComment} */
export function update${simpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) {
return requestClient.put('${baseURL}/update', data);
}
/** 删除${table.classComment} */
export function delete${simpleClassName}(id: number) {
return requestClient.delete(`${baseURL}/delete?id=${id}`);
}
#if ( $table.templateType != 2 && $deleteBatchEnable)
/** 批量删除${table.classComment} */
export function delete${simpleClassName}List(ids: number[]) {
return requestClient.delete(`${baseURL}/delete-list?ids=${ids.join(',')}`)
}
#end
/** 导出${table.classComment} */
export function export${simpleClassName}(params: any) {
return requestClient.download('${baseURL}/export-excel', { params });
}
## 特殊:主子表专属逻辑
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段
#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index))
#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
// ==================== 子表($subTable.classComment ====================
## 情况一MASTER_ERP 时,需要分查询页子表
#if ( $table.templateType == 11 )
/** 获得${subTable.classComment}分页 */
export function get${subSimpleClassName}Page(params: PageParam) {
return requestClient.get<PageResult<${simpleClassName}Api.${subSimpleClassName}>>(`${baseURL}/${subSimpleClassName_strikeCase}/page`, { params });
}
## 情况二:非 MASTER_ERP 时,需要列表查询子表
#else
#if ( $subTable.subJoinMany )
/** 获得${subTable.classComment}列表 */
export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}: number) {
return requestClient.get<${simpleClassName}Api.${subSimpleClassName}[]>(`${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`);
}
#else
/** 获得${subTable.classComment} */
export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}: number) {
return requestClient.get<${simpleClassName}Api.${subSimpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`);
}
#end
#end
## 特殊MASTER_ERP 时,支持单个的新增、修改、删除操作
#if ( $table.templateType == 11 )
/** 新增${subTable.classComment} */
export function create${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) {
return requestClient.post(`${baseURL}/${subSimpleClassName_strikeCase}/create`, data);
}
/** 修改${subTable.classComment} */
export function update${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) {
return requestClient.put(`${baseURL}/${subSimpleClassName_strikeCase}/update`, data);
}
/** 删除${subTable.classComment} */
export function delete${subSimpleClassName}(id: number) {
return requestClient.delete(`${baseURL}/${subSimpleClassName_strikeCase}/delete?id=${id}`);
}
#if ($deleteBatchEnable)
/** 批量删除${subTable.classComment} */
export function delete${subSimpleClassName}List(ids: number[]) {
return requestClient.delete(`${baseURL}/${subSimpleClassName_strikeCase}/delete-list?ids=${ids.join(',')}`)
}
#end
/** 获得${subTable.classComment} */
export function get${subSimpleClassName}(id: number) {
return requestClient.get<${simpleClassName}Api.${subSimpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get?id=${id}`);
}
#end
#end

View File

@@ -3,10 +3,11 @@ import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleCl
import type { FormRules } from 'element-plus';
import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload, FileUpload } from "#/components/upload";
import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElSelect, ElOption, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElDatePicker, ElTreeSelect } from 'element-plus';
import { DICT_TYPE, getDictOptions } from '#/utils';
#if($table.templateType == 2)## 树表需要导入这些
import { get${simpleClassName}List } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { handleTree } from '@vben/utils'

View File

@@ -1,18 +1,18 @@
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { VxeTableInstance } from '#/adapter/vxe-table';
import { ref, h, reactive, onMounted, nextTick } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { cloneDeep, formatDateTime } from '@vben/utils';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
import { cloneDeep, downloadFileFromBlobPart, formatDateTime } from '@vben/utils';
import { ElButton, ElMessage, ElLoading, ElTabs, ElTabPane, ElPagination, ElForm, ElFormItem, ElDatePicker, ElSelect, ElOption, ElInput } from 'element-plus';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
import ${simpleClassName}Form from './modules/form.vue';
import { Download, Plus, RefreshCw, Search, Trash2 } from '@vben/icons';
import { ContentWrap } from '#/components/content-wrap';
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { TableToolbar } from '#/components/table-toolbar';
import { useTableToolbar } from '#/hooks';
## 特殊:主子表专属逻辑
#if ( $table.templateType == 11 || $table.templateType == 12 )
@@ -23,7 +23,6 @@ import { useTableToolbar } from '#/hooks';
#end
#end
import { ref, h, reactive,onMounted,nextTick } from 'vue';
import { $t } from '#/locales';
#if (${table.templateType} == 2)## 树表接口
import { handleTree,isEmpty } from '@vben/utils'
@@ -104,7 +103,7 @@ const getList = async () => {
}
/** 搜索按钮操作 */
const handleQuery = () => {
function handleQuery() {
#if ( $table.templateType != 2 )
queryParams.pageNo = 1
#end
@@ -112,7 +111,7 @@ const handleQuery = () => {
}
/** 重置按钮操作 */
const resetQuery = () => {
function resetQuery() {
queryFormRef.value.resetFields()
handleQuery()
}
@@ -124,7 +123,7 @@ const [FormModal, formModalApi] = useVbenModal({
/** 创建${table.classComment} */
function handleCreate() {
formModalApi.setData({}).open();
formModalApi.setData(null).open();
}
/** 编辑${table.classComment} */
@@ -143,7 +142,6 @@ function handleAppend(row: ${simpleClassName}Api.${simpleClassName}) {
async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.id]),
background: 'rgba(0, 0, 0, 0.7)',
});
try {
await delete${simpleClassName}(row.id as number);
@@ -159,7 +157,6 @@ async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) {
async function handleDeleteBatch() {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting'),
background: 'rgba(0, 0, 0, 0.7)',
});
try {
await delete${simpleClassName}List(checkedIds.value);
@@ -177,12 +174,12 @@ function handleRowCheckboxChange({
}: {
records: ${simpleClassName}Api.${simpleClassName}[];
}) {
checkedIds.value = records.map((item) => item.id);
checkedIds.value = records.map((item) => item.id!);
}
#end
/** 导出表格 */
async function onExport() {
async function handleExport() {
try {
exportLoading.value = true;
const data = await export${simpleClassName}(queryParams);
@@ -195,7 +192,7 @@ try {
#if (${table.templateType} == 2)
/** 切换树形展开/收缩状态 */
const isExpanded = ref(true);
function toggleExpand() {
function handleExpand() {
isExpanded.value = !isExpanded.value;
tableRef.value?.setAllTreeExpand(isExpanded.value);
}
@@ -306,7 +303,7 @@ onMounted(() => {
v-model:hidden-search="hiddenSearchBar"
>
#if (${table.templateType} == 2)
<el-button @click="toggleExpand" class="mr-2">
<el-button @click="handleExpand" class="mr-2">
{{ isExpanded ? '收缩' : '展开' }}
</el-button>
#end
@@ -324,7 +321,7 @@ onMounted(() => {
type="primary"
class="ml-2"
:loading="exportLoading"
@click="onExport"
@click="handleExport"
v-access:code="['${permissionPrefix}:export']"
>
{{ $t('ui.actionTitle.export') }}

View File

@@ -6,13 +6,15 @@
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { FormRules } from 'element-plus';
import { computed, ref, reactive } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload, FileUpload } from "#/components/upload";
import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElSelect, ElOption, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElDatePicker, ElTreeSelect } from 'element-plus';
import { DICT_TYPE, getDictOptions } from '#/utils';
import { computed, ref, reactive } from 'vue';
import { $t } from '#/locales';
import { get${subSimpleClassName}, create${subSimpleClassName}, update${subSimpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';

View File

@@ -5,12 +5,15 @@
#set ($subClassNameVar = $subClassNameVars.get($subIndex))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElButton, ElSelect, ElOption, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElDatePicker } from 'element-plus';
import { computed, ref, reactive, h, onMounted,watch,nextTick } from 'vue';
import { $t } from '#/locales';
import { DICT_TYPE, getDictOptions } from '#/utils';
import { computed, ref, reactive, h, onMounted,watch,nextTick } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElButton, ElSelect, ElOption, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElDatePicker } from 'element-plus';
import { $t } from '#/locales';
#if ($subTable.subJoinMany) ## 一对多
import type { VxeTableInstance } from '#/adapter/vxe-table';
@@ -31,12 +34,12 @@ const props = defineProps<{
const list = ref<${simpleClassName}Api.${subSimpleClassName}[]>([]) // 列表的数据
const tableRef = ref<VxeTableInstance>();
/** 添加${subTable.classComment} */
const onAdd = async () => {
async function handleAdd() {
await tableRef.value?.insertAt({} as ${simpleClassName}Api.${subSimpleClassName}, -1);
}
/** 删除${subTable.classComment} */
const onDelete = async (row: ${simpleClassName}Api.${subSimpleClassName}) => {
async function onDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
await tableRef.value?.remove(row);
}
@@ -46,9 +49,11 @@ defineExpose({
const data = list.value as ${simpleClassName}Api.${subSimpleClassName}[];
const removeRecords = tableRef.value?.getRemoveRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
const insertRecords = tableRef.value?.getInsertRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
return data
.filter((row) => !removeRecords.some((removed) => removed.id === row.id))
?.concat(insertRecords.map((row: any) => ({ ...row, id: undefined })));
return [
...data.filter(
(row) => !removeRecords.some((removed) => removed.id === row.id),
),
...insertRecords.map((row: any) => ({ ...row, id: undefined })),
},
});
@@ -227,7 +232,7 @@ watch(
</vxe-column>
</vxe-table>
<div class="flex justify-center mt-4">
<el-button :icon="h(Plus)" type="primary" plain @click="onAdd" v-access:code="['${permissionPrefix}:create']">
<el-button :icon="h(Plus)" type="primary" plain @click="handleAdd" v-access:code="['${permissionPrefix}:create']">
{{ $t('ui.actionTitle.create', ['${subTable.classComment}']) }}
</el-button>
</div>

View File

@@ -9,23 +9,25 @@
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { VxeTableInstance } from '#/adapter/vxe-table';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { reactive,ref, h, nextTick,watch,onMounted } from 'vue';
import { cloneDeep, formatDateTime } from '@vben/utils';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { DictTag } from '#/components/dict-tag';
import { getRangePickerDefaultProps } from '#/utils';
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { ContentWrap } from '#/components/content-wrap';
#if ($table.templateType == 11) ## erp
import { useVbenModal } from '@vben/common-ui';
import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue'
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload, FileUpload } from "#/components/upload";
import { ElMessage, ElLoading, ElButton, ElTabs, ElTabPane, ElPagination, ElForm, ElFormItem, ElDatePicker, ElSelect, ElOption, ElInput } from 'element-plus';
import { Plus, Trash2 } from '@vben/icons';
import { $t } from '#/locales';
import { TableToolbar } from '#/components/table-toolbar';
import { useTableToolbar } from '#/hooks';
#end
#if ($table.templateType == 11) ## erp
@@ -67,7 +69,6 @@ function handleEdit(row: ${simpleClassName}Api.${subSimpleClassName}) {
async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.id]),
background: 'rgba(0, 0, 0, 0.7)',
});
try {
await delete${subSimpleClassName}(row.id as number);
@@ -83,7 +84,6 @@ async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
async function handleDeleteBatch() {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting'),
background: 'rgba(0, 0, 0, 0.7)',
});
try {
await delete${subSimpleClassName}List(checkedIds.value);
@@ -101,7 +101,7 @@ function handleRowCheckboxChange({
}: {
records: ${simpleClassName}Api.${subSimpleClassName}[];
}) {
checkedIds.value = records.map((item) => item.id);
checkedIds.value = records.map((item) => item.id!);
}
#end
#end
@@ -129,64 +129,64 @@ function handleRowCheckboxChange({
})
/** 搜索按钮操作 */
const handleQuery = () => {
function handleQuery() {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
function resetQuery() {
queryFormRef.value.resetFields()
handleQuery()
}
#end
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
if (!props.${subJoinColumn.javaField}){
return []
}
## 特殊:树表专属逻辑(树不需要分页接口)
#if ($table.templateType == 11) ## erp
const params = cloneDeep(queryParams) as any;
#foreach ($column in $columns)
#if ($column.listOperation)
#if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")
if (params.${column.javaField} && Array.isArray(params.${column.javaField})) {
params.${column.javaField} = (params.${column.javaField} as string[]).join(',');
}
#end
#end
#end
params.${subJoinColumn.javaField} = props.${subJoinColumn.javaField};
const data = await get${subSimpleClassName}Page(params)
list.value = data.list
total.value = data.total
#else
#if ($subTable.subJoinMany) ## 一对多
list.value = await get${subSimpleClassName}ListBy${SubJoinColumnName}(props.${subJoinColumn.javaField}!);
#else
list.value = [await get${subSimpleClassName}By${SubJoinColumnName}(props.${subJoinColumn.javaField}!)];
#end
#end
} finally {
loading.value = false
/** 查询列表 */
async function getList() {
loading.value = true
try {
if (!props.${subJoinColumn.javaField}){
return []
}
## 特殊:树表专属逻辑(树不需要分页接口)
#if ($table.templateType == 11) ## erp
const params = cloneDeep(queryParams) as any;
#foreach ($column in $columns)
#if ($column.listOperation)
#if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")
if (params.${column.javaField} && Array.isArray(params.${column.javaField})) {
params.${column.javaField} = (params.${column.javaField} as string[]).join(',');
}
#end
#end
#end
params.${subJoinColumn.javaField} = props.${subJoinColumn.javaField};
const data = await get${subSimpleClassName}Page(params)
list.value = data.list
total.value = data.total
#else
#if ($subTable.subJoinMany) ## 一对多
list.value = await get${subSimpleClassName}ListBy${SubJoinColumnName}(props.${subJoinColumn.javaField}!);
#else
list.value = [await get${subSimpleClassName}By${SubJoinColumnName}(props.${subJoinColumn.javaField}!)];
#end
#end
} finally {
loading.value = false
}
}
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.${subJoinColumn.javaField},
async (val) => {
if (!val) {
return;
}
await nextTick();
await getList()
},
{ immediate: true },
);
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.${subJoinColumn.javaField},
async (val) => {
if (!val) {
return;
}
await nextTick();
await getList()
},
{ immediate: true },
);
#if ($table.templateType == 11) ## erp
/** 初始化 */
@@ -290,7 +290,7 @@ onMounted(() => {
<!-- 列表 -->
<ContentWrap title="${table.classComment}">
<template #extra>
<TableToolbar
<VbenVxeTableToolbar
ref="tableToolbarRef"
v-model:hidden-search="hiddenSearchBar"
>

View File

@@ -0,0 +1,191 @@
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
#if ( $table.templateType != 2 )
import type { PageParam, PageResult } from '@vben/request';
#end
import type { Dayjs } from 'dayjs';
import { requestClient } from '#/api/request';
#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}")
export namespace ${apiName} {
## 特殊:主子表专属逻辑
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subColumns = $subColumnsList.get($index))##当前字段数组
/** ${subTable.classComment}信息 */
export interface ${subSimpleClassName} {
#foreach ($column in $subColumns)
#if ($column.createOperation || $column.updateOperation)
#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: number; // ${column.columnComment}
#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: string | Dayjs; // ${column.columnComment}
#else
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: ${column.javaType.toLowerCase()}; // ${column.columnComment}
#end
#end
#end
}
#end
/** ${table.classComment}信息 */
export interface ${simpleClassName} {
#foreach ($column in $columns)
#if ($column.createOperation || $column.updateOperation)
#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: number; // ${column.columnComment}
#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: string | Dayjs; // ${column.columnComment}
#else
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: ${column.javaType.toLowerCase()}; // ${column.columnComment}
#end
#end
#end
#if ( $table.templateType == 2 )
children?: ${simpleClassName}[];
#end
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#if ( $subTable.subJoinMany )
${subSimpleClassName.toLowerCase()}s?: ${subSimpleClassName}[]
#else
${subSimpleClassName.toLowerCase()}?: ${subSimpleClassName}
#end
#end
#end
}
}
#if ( $table.templateType != 2 )
/** 查询${table.classComment}分页 */
export function get${simpleClassName}Page(params: PageParam) {
return requestClient.get<PageResult<${apiName}.${simpleClassName}>>(
'${baseURL}/page',
{ params },
);
}
#else
/** 查询${table.classComment}列表 */
export function get${simpleClassName}List(params: any) {
return requestClient.get<${apiName}.${simpleClassName}[]>(
'${baseURL}/list',
{ params },
);
}
#end
/** 查询${table.classComment}详情 */
export function get${simpleClassName}(id: number) {
return requestClient.get<${apiName}.${simpleClassName}>(
`${baseURL}/get?id=${id}`,
);
}
/** 新增${table.classComment} */
export function create${simpleClassName}(data: ${apiName}.${simpleClassName}) {
return requestClient.post('${baseURL}/create', data);
}
/** 修改${table.classComment} */
export function update${simpleClassName}(data: ${apiName}.${simpleClassName}) {
return requestClient.put('${baseURL}/update', data);
}
/** 删除${table.classComment} */
export function delete${simpleClassName}(id: number) {
return requestClient.delete(`${baseURL}/delete?id=${id}`);
}
#if ( $table.templateType != 2 && $deleteBatchEnable)
/** 批量删除${table.classComment} */
export function delete${simpleClassName}List(ids: number[]) {
return requestClient.delete(
`${baseURL}/delete-list?ids=${ids.join(',')}`,
);
}
#end
/** 导出${table.classComment} */
export function export${simpleClassName}(params: any) {
return requestClient.download('${baseURL}/export-excel', { params });
}
## 特殊:主子表专属逻辑
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段
#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index))
#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
// ==================== 子表($subTable.classComment ====================
## 情况一MASTER_ERP 时,需要分查询页子表
#if ( $table.templateType == 11 )
/** 获得${subTable.classComment}分页 */
export function get${subSimpleClassName}Page(params: PageParam) {
return requestClient.get<PageResult<${apiName}.${subSimpleClassName}>>(
`${baseURL}/${subSimpleClassName_strikeCase}/page`,
{ params },
);
}
## 情况二:非 MASTER_ERP 时,需要列表查询子表
#else
#if ( $subTable.subJoinMany )
/** 获得${subTable.classComment}列表 */
export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}: number) {
return requestClient.get<${apiName}.${subSimpleClassName}[]>(
`${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`,
);
}
#else
/** 获得${subTable.classComment} */
export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}: number) {
return requestClient.get<${apiName}.${subSimpleClassName}>(
`${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`,
);
}
#end
#end
## 特殊MASTER_ERP 时,支持单个的新增、修改、删除操作
#if ( $table.templateType == 11 )
/** 新增${subTable.classComment} */
export function create${subSimpleClassName}(data: ${apiName}.${subSimpleClassName}) {
return requestClient.post(`${baseURL}/${subSimpleClassName_strikeCase}/create`, data);
}
/** 修改${subTable.classComment} */
export function update${subSimpleClassName}(data: ${apiName}.${subSimpleClassName}) {
return requestClient.put(`${baseURL}/${subSimpleClassName_strikeCase}/update`, data);
}
/** 删除${subTable.classComment} */
export function delete${subSimpleClassName}(id: number) {
return requestClient.delete(`${baseURL}/${subSimpleClassName_strikeCase}/delete?id=${id}`);
}
#if ($deleteBatchEnable)
/** 批量删除${subTable.classComment} */
export function delete${subSimpleClassName}List(ids: number[]) {
return requestClient.delete(
`${baseURL}/${subSimpleClassName_strikeCase}/delete-list?ids=${ids.join(',')}`,
);
}
#end
/** 获得${subTable.classComment} */
export function get${subSimpleClassName}(id: number) {
return requestClient.get<${apiName}.${subSimpleClassName}>(
`${baseURL}/${subSimpleClassName_strikeCase}/get?id=${id}`,
);
}
#end
#end

View File

@@ -1,18 +1,17 @@
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
import { z } from '#/adapter/form';
import {
DICT_TYPE,
getDictOptions,
getRangePickerDefaultProps,
} from '#/utils';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
#if(${table.templateType} == 2)## 树表需要导入这些
import { get${simpleClassName}List } from '#/api/${table.moduleName}/${table.businessName}';
import { handleTree } from '@vben/utils';
#end
import { getRangePickerDefaultProps } from '#/utils';
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
@@ -30,7 +29,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '上级${table.classComment}',
component: 'ApiTreeSelect',
componentProps: {
allowClear: true,
clearable: true,
api: async () => {
const data = await get${simpleClassName}List({});
data.unshift({
@@ -158,13 +157,13 @@ export function useGridFormSchema(): VbenFormSchema[] {
#if ($column.htmlType == "input" || $column.htmlType == "textarea" || $column.htmlType == "editor")
component: 'Input',
componentProps: {
allowClear: true,
clearable: true,
placeholder: '请输入${comment}',
},
#elseif ($column.htmlType == "select" || $column.htmlType == "radio")
component: 'Select',
componentProps: {
allowClear: true,
clearable: true,
#if ("" != $dictType)## 设置了 dictType 数据字典的情况
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
#else## 未设置 dictType 数据字典的情况
@@ -176,7 +175,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
clearable: true,
},
#end
},
@@ -186,7 +185,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<${simpleClassName}Api.${simpleClassName}>['columns'] {
export function useGridColumns(): VxeTableGridOptions<${apiName}.${simpleClassName}>['columns'] {
return [
#if ($table.templateType != 2 && $deleteBatchEnable)
{ type: 'checkbox', width: 40 },
@@ -361,13 +360,13 @@ export function use${subSimpleClassName}GridFormSchema(): VbenFormSchema[] {
#if ($column.htmlType == "input" || $column.htmlType == "textarea" || $column.htmlType == "editor")
component: 'Input',
componentProps: {
allowClear: true,
clearable: true,
placeholder: '请输入${comment}',
},
#elseif ($column.htmlType == "select" || $column.htmlType == "radio")
component: 'Select',
componentProps: {
allowClear: true,
clearable: true,
#if ("" != $dictType)## 设置了 dictType 数据字典的情况
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
#else## 未设置 dictType 数据字典的情况
@@ -379,7 +378,7 @@ export function use${subSimpleClassName}GridFormSchema(): VbenFormSchema[] {
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
clearable: true,
},
#end
},
@@ -389,7 +388,7 @@ export function use${subSimpleClassName}GridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>['columns'] {
export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${apiName}.${subSimpleClassName}>['columns'] {
return [
#if ($table.templateType != 2 && $deleteBatchEnable)
{ type: 'checkbox', width: 40 },
@@ -426,7 +425,7 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${sim
#else
#if ($subTable.subJoinMany) ## 一对多
/** 新增/修改列表的字段 */
export function use${subSimpleClassName}GridEditColumns(): VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>['columns'] {
export function use${subSimpleClassName}GridEditColumns(): VxeTableGridOptions<${apiName}.${subSimpleClassName}>['columns'] {
return [
#foreach($column in $subColumns)
#if ($column.createOperation || $column.updateOperation)
@@ -602,4 +601,4 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${sim
}
#end
#end
#end
#end

View File

@@ -1,8 +1,16 @@
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage, ElTabs, ElTabPane, ElCheckbox, ElInput, ElSelect, ElRadioGroup, ElCheckboxGroup, ElDatePicker } from 'element-plus';
import { ElMessage#if ($table.templateType == 11), ElTabs, ElTabPane#end } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import { create${simpleClassName}, get${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
import { $t } from '#/locales';
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
#foreach ($subSimpleClassName in $subSimpleClassNames)
@@ -12,21 +20,15 @@ import { ElMessage, ElTabs, ElTabPane, ElCheckbox, ElInput, ElSelect, ElRadioGro
#end
#end
import { computed, ref } from 'vue';
import { $t } from '#/locales';
import { useVbenForm } from '#/adapter/form';
import { get${simpleClassName}, create${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<${simpleClassName}Api.${simpleClassName}>();
const formData = ref<${apiName}.${simpleClassName}>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['${table.classComment}'])
: $t('ui.actionTitle.create', ['${table.classComment}']);
});
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
#if ( $subTables && $subTables.size() > 0 )
@@ -51,7 +53,7 @@ const [Form, formApi] = useVbenForm({
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
@@ -60,8 +62,8 @@ const [Modal, modalApi] = useVbenModal({
if (!valid) {
return;
}
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
#if ( $subTables && $subTables.size() > 0 )
// 校验子表单
#foreach ($subTable in $subTables)
@@ -78,12 +80,12 @@ const [Modal, modalApi] = useVbenModal({
#end
#end
#end
#end
#end
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as ${simpleClassName}Api.${simpleClassName};
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
const data = (await formApi.getValues()) as ${apiName}.${simpleClassName};
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
#if ( $subTables && $subTables.size() > 0 )
// 拼接子表的数据
#foreach ($subTable in $subTables)
@@ -96,7 +98,7 @@ const [Modal, modalApi] = useVbenModal({
#end
#end
#end
#end
#end
try {
await (formData.value?.id ? update${simpleClassName}(data) : create${simpleClassName}(data));
// 关闭并提示
@@ -113,21 +115,24 @@ const [Modal, modalApi] = useVbenModal({
return;
}
// 加载数据
let data = modalApi.getData<${simpleClassName}Api.${simpleClassName}>();
if (!data) {
const data = modalApi.getData<${apiName}.${simpleClassName}>();
if (!data || !data.id) {
#if (${table.templateType} == 2)## 树表特有
// 设置上级
await formApi.setValues(data);
#end
return;
}
if (data.id) {
modalApi.lock();
try {
data = await get${simpleClassName}(data.id);
} finally {
modalApi.unlock();
modalApi.lock();
try {
formData.value = await get${simpleClassName}(data.id);
// 设置到 values
if (formData.value) {
await formApi.setValues(formData.value);
}
} finally {
modalApi.unlock();
}
// 设置到 values
formData.value = data;
await formApi.setValues(formData.value);
},
});
</script>
@@ -135,20 +140,20 @@ const [Modal, modalApi] = useVbenModal({
<template>
<Modal :title="getTitle">
<Form class="mx-4" />
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
<!-- 子表的表单 -->
<el-tabs v-model="subTabsName">
<ElTabs v-model="subTabsName">
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
<el-tab-pane name="$subClassNameVar" label="${subTable.classComment}">
<ElTabPane name="$subClassNameVar" label="${subTable.classComment}">
<${subSimpleClassName}Form ref="${subClassNameVar}FormRef" :${subJoinColumn_strikeCase}="formData?.id" />
</el-tab-pane>
</ElTabPane>
#end
</el-tabs>
#end
</ElTabs>
#end
</Modal>
</template>

View File

@@ -1,56 +1,67 @@
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
import { Page, useVbenModal } from '@vben/common-ui';
import { ElMessage, ElTabs, ElTabPane, ElLoading } from 'element-plus';
import { ref } from 'vue';
import {#if ($table.templateType != 2 && $deleteBatchEnable) confirm,#end Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart#if ($table.templateType != 2 && $deleteBatchEnable), isEmpty#end } from '@vben/utils';
import { ElLoading, ElMessage#if ($table.templateType == 11), ElTabs, ElTabPane#end } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
#if (${table.templateType} == 2)## 树表接口
import {
delete${simpleClassName},
export${simpleClassName},
get${simpleClassName}List,
} from '#/api/${table.moduleName}/${table.businessName}';
#else## 标准表接口
import {
delete${simpleClassName},#if ($deleteBatchEnable)
delete${simpleClassName}List,#end
export${simpleClassName},
get${simpleClassName}Page,
} from '#/api/${table.moduleName}/${table.businessName}';
#end
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
## 特殊:主子表专属逻辑
#if ( $table.templateType == 11 || $table.templateType == 12 )
#foreach ($subSimpleClassName in $subSimpleClassNames)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index))
import ${subSimpleClassName}List from './modules/${subSimpleClassName_strikeCase}-list.vue'
import ${subSimpleClassName}List from './modules/${subSimpleClassName_strikeCase}-list.vue';
#end
#end
import { ref, computed } from 'vue';
import { $t } from '#/locales';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
#if (${table.templateType} == 2)## 树表接口
import { get${simpleClassName}List, delete${simpleClassName}, export${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
#else## 标准表接口
import { get${simpleClassName}Page, delete${simpleClassName},#if ($deleteBatchEnable) delete${simpleClassName}List,#end export${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
#end
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
import { useGridColumns, useGridFormSchema } from './data';
#if ($table.templateType == 12 || $table.templateType == 11) ## 内嵌和erp情况
/** 子表的列表 */
const subTabsName = ref('$subClassNameVars.get(0)')
#if ($table.templateType == 11)
const select${simpleClassName} = ref<${simpleClassName}Api.${simpleClassName}>();
const select${simpleClassName} = ref<${apiName}.${simpleClassName}>();
#end
#end
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
#if (${table.templateType} == 2)## 树表特有:控制表格展开收缩
/** 切换树形展开/收缩状态 */
const isExpanded = ref(true);
function toggleExpand() {
function handleExpand() {
isExpanded.value = !isExpanded.value;
gridApi.grid.setAllTreeExpand(isExpanded.value);
}
#end
/** 刷新表格 */
function onRefresh() {
function handleRefresh() {
#if ($table.templateType == 12) ## 内嵌情况
gridApi.reload();
#else
@@ -60,31 +71,30 @@ function onRefresh() {
/** 创建${table.classComment} */
function handleCreate() {
formModalApi.setData({}).open();
formModalApi.setData(null).open();
}
/** 编辑${table.classComment} */
function handleEdit(row: ${simpleClassName}Api.${simpleClassName}) {
formModalApi.setData(row).open();
}
#if (${table.templateType} == 2)## 树表特有:新增下级
/** 新增下级${table.classComment} */
function handleAppend(row: ${simpleClassName}Api.${simpleClassName}) {
/** 添加下级${table.classComment} */
function handleAppend(row: ${apiName}.${simpleClassName}) {
formModalApi.setData({ ${treeParentColumn.javaField}: row.id }).open();
}
#end
/** 编辑${table.classComment} */
function handleEdit(row: ${apiName}.${simpleClassName}) {
formModalApi.setData(row).open();
}
/** 删除${table.classComment} */
async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) {
async function handleDelete(row: ${apiName}.${simpleClassName}) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.id]),
background: 'rgba(0, 0, 0, 0.7)',
});
try {
await delete${simpleClassName}(row.id as number);
await delete${simpleClassName}(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.id]));
onRefresh();
handleRefresh();
} finally {
loadingInstance.close();
}
@@ -93,27 +103,27 @@ async function handleDelete(row: ${simpleClassName}Api.${simpleClassName}) {
#if ($table.templateType != 2 && $deleteBatchEnable)
/** 批量删除${table.classComment} */
async function handleDeleteBatch() {
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting'),
background: 'rgba(0, 0, 0, 0.7)',
text: $t('ui.actionMessage.deletingBatch'),
});
try {
await delete${simpleClassName}List(checkedIds.value);
checkedIds.value = [];
ElMessage.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
handleRefresh();
} finally {
loadingInstance.close();
}
}
const checkedIds = ref<number[]>([])
const checkedIds = ref<number[]>([]);
function handleRowCheckboxChange({
records,
}: {
records: ${simpleClassName}Api.${simpleClassName}[];
records: ${apiName}.${simpleClassName}[];
}) {
checkedIds.value = records.map((item) => item.id);
checkedIds.value = records.map((item) => item.id!);
}
#end
@@ -135,20 +145,11 @@ const [Grid, gridApi] = useVbenVxeGrid({
height: 'auto',
#end
#if (${table.templateType} == 2)## 树表设置
treeConfig: {
parentField: '${treeParentColumn.javaField}',
rowField: 'id',
transform: true,
expandAll: true,
reserve: true,
},
pagerConfig: {
enabled: false,
},
#else## 标准表设置
pagerConfig: {
enabled: true,
enabled: false,
},
#else## 标准表设置
keepSource: true,
#end
proxyConfig: {
ajax: {
@@ -178,27 +179,35 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true,
search: true,
},
} as VxeTableGridOptions<${simpleClassName}Api.${simpleClassName}>,
#if (${table.templateType} == 11 || $deleteBatchEnable)
gridEvents:{
#if(${table.templateType} == 11)
cellClick: ({ row }: { row: ${simpleClassName}Api.${simpleClassName}}) => {
#if (${table.templateType} == 2)## 树表设置
treeConfig: {
parentField: '${treeParentColumn.javaField}',
rowField: 'id',
transform: true,
expandAll: true,
reserve: true,
},
#end
} as VxeTableGridOptions<${apiName}.${simpleClassName}>,
#if (${table.templateType} == 11 || (${table.templateType} != 2 && $deleteBatchEnable))
gridEvents: {
#if(${table.templateType} == 11)
cellClick: ({ row }: { row: ${apiName}.${simpleClassName}}) => {
select${simpleClassName}.value = row;
},
#end
#if(${table.templateType} != 2 && $deleteBatchEnable)
checkboxAll: handleRowCheckboxChange,
checkboxChange: handleRowCheckboxChange,
#end
}
#end
#if (${table.templateType} != 2 && $deleteBatchEnable)
checkboxAll: handleRowCheckboxChange,
checkboxChange: handleRowCheckboxChange,
#end
},
#end
});
</script>
<template>
<Page auto-content-height>
<FormModal @success="onRefresh" />
<FormModal @success="handleRefresh" />
#if ($table.templateType == 11) ## erp情况
<div>
#end
@@ -206,29 +215,22 @@ const [Grid, gridApi] = useVbenVxeGrid({
#if ($table.templateType == 12) ## 内嵌情况
<template #expand_content="{ row }">
<!-- 子表的表单 -->
<el-tabs v-model="subTabsName" class="mx-8">
<ElTabs v-model="subTabsName" class="mx-8">
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
<el-tab-pane name="$subClassNameVar" label="${subTable.classComment}">
<ElTabPane name="$subClassNameVar" label="${subTable.classComment}">
<${subSimpleClassName}List :${subJoinColumn_strikeCase}="row?.id" />
</el-tab-pane>
</ElTabPane>
#end
</el-tabs>
</ElTabs>
</template>
#end
<template #toolbar-tools>
<TableAction
:actions="[
#if (${table.templateType} == 2)## 树表特有:展开/收缩按钮
{
label: isExpanded ? '收缩' : '展开',
type: 'primary',
onClick: toggleExpand,
},
#end
{
label: $t('ui.actionTitle.create', ['${table.classComment}']),
type: 'primary',
@@ -236,6 +238,13 @@ const [Grid, gridApi] = useVbenVxeGrid({
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:create'],
onClick: handleCreate,
},
#if (${table.templateType} == 2)## 树表特有:展开/收缩按钮
{
label: isExpanded ? '收缩' : '展开',
type: 'primary',
onClick: handleExpand,
},
#end
{
label: $t('ui.actionTitle.export'),
type: 'primary',
@@ -248,8 +257,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
label: $t('ui.actionTitle.deleteBatch'),
type: 'danger',
icon: ACTION_ICON.DELETE,
disabled: isEmpty(checkedIds),
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:delete'],
disabled: isEmpty(checkedIds),
onClick: handleDeleteBatch,
},
#end
@@ -262,7 +271,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
#if (${table.templateType} == 2)## 树表特有:新增下级
{
label: '新增下级',
type: 'text',
type: 'primary',
link: true,
icon: ACTION_ICON.ADD,
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:create'],
onClick: handleAppend.bind(null, row),
@@ -270,7 +280,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
#end
{
label: $t('common.edit'),
type: 'text',
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:update'],
onClick: handleEdit.bind(null, row),
@@ -278,7 +289,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
{
label: $t('common.delete'),
type: 'danger',
text: true,
link: true,
icon: ACTION_ICON.DELETE,
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:delete'],
popConfirm: {
@@ -290,21 +301,20 @@ const [Grid, gridApi] = useVbenVxeGrid({
/>
</template>
</Grid>
#if ($table.templateType == 11) ## erp情况
<!-- 子表的表单 -->
<el-tabs v-model="subTabsName" class="mt-2">
<ElTabs v-model="subTabsName" class="mt-2">
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
<el-tab-pane name="$subClassNameVar" label="${subTable.classComment}">
<ElTabPane name="$subClassNameVar" label="${subTable.classComment}">
<${subSimpleClassName}List :${subJoinColumn_strikeCase}="select${simpleClassName}?.id" />
</el-tab-pane>
</ElTabPane>
#end
</el-tabs>
</ElTabs>
</div>
#end
</Page>
</template>
</template>

View File

@@ -2,21 +2,24 @@
#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组
#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
import { computed, ref } from 'vue';
import { $t } from '#/locales';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import { get${subSimpleClassName}, create${subSimpleClassName}, update${subSimpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
import { $t } from '#/locales';
import { use${subSimpleClassName}FormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<${simpleClassName}Api.${subSimpleClassName}>();
const formData = ref<${apiName}.${subSimpleClassName}>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['${subTable.classComment}'])
@@ -45,7 +48,7 @@
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as ${simpleClassName}Api.${subSimpleClassName};
const data = (await formApi.getValues()) as ${apiName}.${subSimpleClassName};
data.${subJoinColumn.javaField} = formData.value?.${subJoinColumn.javaField};
try {
await (formData.value?.id ? update${subSimpleClassName}(data) : create${subSimpleClassName}(data));
@@ -64,7 +67,7 @@
}
// 加载数据
let data = modalApi.getData<${simpleClassName}Api.${subSimpleClassName}>();
let data = modalApi.getData<${apiName}.${subSimpleClassName}>();
if (!data) {
return;
}

View File

@@ -4,10 +4,12 @@
#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
#set ($subClassNameVar = $subClassNameVars.get($subIndex))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
import { computed, ref, h, onMounted,watch,nextTick } from 'vue';
import { $t } from '#/locales';
#if ($subTable.subJoinMany) ## 一对多
@@ -33,7 +35,7 @@ const props = defineProps<{
function onActionClick({
code,
row,
}: OnActionClickParams<${simpleClassName}Api.${subSimpleClassName}>) {
}: OnActionClickParams<${apiName}.${subSimpleClassName}>) {
switch (code) {
case 'delete': {
onDelete(row);
@@ -67,16 +69,16 @@ const onAdd = async () => {
}
/** 删除${subTable.classComment} */
const onDelete = async (row: ${simpleClassName}Api.${subSimpleClassName}) => {
const onDelete = async (row: ${apiName}.${subSimpleClassName}) => {
await gridApi.grid.remove(row);
}
/** 提供获取表格数据的方法供父组件调用 */
defineExpose({
getData: (): ${simpleClassName}Api.${subSimpleClassName}[] => {
const data = gridApi.grid.getData() as ${simpleClassName}Api.${subSimpleClassName}[];
const removeRecords = gridApi.grid.getRemoveRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
const insertRecords = gridApi.grid.getInsertRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
getData: (): ${apiName}.${subSimpleClassName}[] => {
const data = gridApi.grid.getData() as ${apiName}.${subSimpleClassName}[];
const removeRecords = gridApi.grid.getRemoveRecords() as ${apiName}.${subSimpleClassName}[];
const insertRecords = gridApi.grid.getInsertRecords() as ${apiName}.${subSimpleClassName}[];
return data
.filter((row) => !removeRecords.some((removed) => removed.id === row.id))
.concat(insertRecords.map((row: any) => ({ ...row, id: undefined })));

View File

@@ -5,9 +5,10 @@
#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($subIndex))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
#if ($table.templateType == 11) ## erp
import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue'
@@ -41,6 +42,19 @@ const [FormModal, formModalApi] = useVbenModal({
destroyOnClose: true,
});
/** 刷新表格 */
async function handleRefresh() {
#if ($table.templateType == 11) ## erp
await gridApi.query();
#else
#if ($subTable.subJoinMany) ## 一对多
await gridApi.grid.loadData(await get${subSimpleClassName}ListBy${SubJoinColumnName}(props.${subJoinColumn.javaField}!));
#else
await gridApi.grid.loadData([await get${subSimpleClassName}By${SubJoinColumnName}(props.${subJoinColumn.javaField}!)]);
#end
#end
}
/** 创建${subTable.classComment} */
function handleCreate() {
if (!props.${subJoinColumn.javaField}){
@@ -51,20 +65,19 @@ function handleCreate() {
}
/** 编辑${subTable.classComment} */
function handleEdit(row: ${simpleClassName}Api.${subSimpleClassName}) {
function handleEdit(row: ${apiName}.${subSimpleClassName}) {
formModalApi.setData(row).open();
}
/** 删除${subTable.classComment} */
async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
async function handleDelete(row: ${apiName}.${subSimpleClassName}) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.id]),
background: 'rgba(0, 0, 0, 0.7)',
});
try {
await delete${subSimpleClassName}(row.id as number);
await delete${subSimpleClassName}(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.id]));
onRefresh();
handleRefresh();
} finally {
loadingInstance.close();
}
@@ -75,13 +88,12 @@ async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
async function handleDeleteBatch() {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting'),
background: 'rgba(0, 0, 0, 0.7)',
});
try {
await delete${subSimpleClassName}List(checkedIds.value);
checkedIds.value = [];
ElMessage.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
handleRefresh();
} finally {
loadingInstance.close();
}
@@ -91,9 +103,9 @@ const checkedIds = ref<number[]>([])
function handleRowCheckboxChange({
records,
}: {
records: ${simpleClassName}Api.${subSimpleClassName}[];
records: ${apiName}.${subSimpleClassName}[];
}) {
checkedIds.value = records.map((item) => item.id);
checkedIds.value = records.map((item) => item.id!);
}
#end
@@ -143,7 +155,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
keyField: 'id',
isHover: true,
},
} as VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>,
} as VxeTableGridOptions<${apiName}.${subSimpleClassName}>,
#if (${table.templateType} == 11 && $deleteBatchEnable)
gridEvents:{
checkboxAll: handleRowCheckboxChange,
@@ -152,19 +164,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
#end
});
/** 刷新表格 */
async function onRefresh() {
#if ($table.templateType == 11) ## erp
await gridApi.query();
#else
#if ($subTable.subJoinMany) ## 一对多
await gridApi.grid.loadData(await get${subSimpleClassName}ListBy${SubJoinColumnName}(props.${subJoinColumn.javaField}!));
#else
await gridApi.grid.loadData([await get${subSimpleClassName}By${SubJoinColumnName}(props.${subJoinColumn.javaField}!)]);
#end
#end
}
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.${subJoinColumn.javaField},
@@ -173,7 +172,7 @@ watch(
return;
}
await nextTick();
await onRefresh()
await handleRefresh()
},
{ immediate: true },
);
@@ -181,7 +180,7 @@ watch(
<template>
#if ($table.templateType == 11) ## erp
<FormModal @success="onRefresh" />
<FormModal @success="handleRefresh" />
<Grid table-title="${subTable.classComment}列表">
<template #toolbar-tools>
<TableAction
@@ -211,7 +210,8 @@ watch(
:actions="[
{
label: $t('common.edit'),
type: 'text',
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:update'],
onClick: handleEdit.bind(null, row),
@@ -219,7 +219,7 @@ watch(
{
label: $t('common.delete'),
type: 'danger',
text: true,
link: true,
icon: ACTION_ICON.DELETE,
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:delete'],
popConfirm: {

View File

@@ -19,7 +19,6 @@ import java.util.Arrays;
@Getter
public enum IotSceneRuleTriggerTypeEnum implements ArrayValuable<Integer> {
// TODO @芋艿:后续“对应”部分,要 @下,等包结构梳理完;
/**
* 设备上下线变更
*

View File

@@ -1,86 +0,0 @@
package cn.iocoder.yudao.module.iot.core.enums;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Set;
/**
* IoT 设备消息的方法枚举
*
* @author haohao
*/
@Getter
@AllArgsConstructor
public enum IotDeviceMessageMethodEnum implements ArrayValuable<String> {
// ========== 设备状态 ==========
STATE_UPDATE("thing.state.update", "设备状态更新", true),
// TODO 芋艿:要不要加个 ping 消息;
// ========== 设备属性 ==========
// 可参考https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services
PROPERTY_POST("thing.property.post", "属性上报", true),
PROPERTY_SET("thing.property.set", "属性设置", false),
// ========== 设备事件 ==========
// 可参考https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services
EVENT_POST("thing.event.post", "事件上报", true),
// ========== 设备服务调用 ==========
// 可参考https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services
SERVICE_INVOKE("thing.service.invoke", "服务调用", false),
// ========== 设备配置 ==========
// 可参考https://help.aliyun.com/zh/iot/user-guide/remote-configuration-1
CONFIG_PUSH("thing.config.push", "配置推送", true),
// ========== OTA 固件 ==========
// 可参考https://help.aliyun.com/zh/iot/user-guide/perform-ota-updates
OTA_UPGRADE("thing.ota.upgrade", "OTA 固定信息推送", false),
OTA_PROGRESS("thing.ota.progress", "OTA 升级进度上报", true),
;
public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageMethodEnum::getMethod)
.toArray(String[]::new);
/**
* 不进行 reply 回复的方法集合
*/
public static final Set<String> REPLY_DISABLED = SetUtils.asSet(
STATE_UPDATE.getMethod(),
OTA_PROGRESS.getMethod() // 参考阿里云OTA 升级进度上报,不进行回复
);
private final String method;
private final String name;
private final Boolean upstream;
@Override
public String[] array() {
return ARRAYS;
}
public static IotDeviceMessageMethodEnum of(String method) {
return ArrayUtil.firstMatch(item -> item.getMethod().equals(method),
IotDeviceMessageMethodEnum.values());
}
public static boolean isReplyDisabled(String method) {
return REPLY_DISABLED.contains(method);
}
}

View File

@@ -69,6 +69,83 @@ public class IotDeviceMessageUtils {
return null;
}
/**
* 从设备消息中提取指定标识符的属性值
* - 支持多种消息格式和属性值提取策略
* - 兼容现有的消息结构
* - 提供统一的属性值提取接口
* <p>
* 支持的提取策略(按优先级顺序):
* 1. 直接值:如果 params 不是 Map直接返回该值适用于简单消息
* 2. 标识符字段:从 params[identifier] 获取
* 3. properties 结构:从 params.properties[identifier] 获取(标准属性上报)
* 4. data 结构:从 params.data[identifier] 获取
* 5. value 字段:从 params.value 获取(单值消息)
* 6. 单值 Map如果 Map 只包含 identifier 和一个值,返回该值
*
* @param message 设备消息
* @param identifier 属性标识符
* @return 属性值,如果未找到则返回 null
*/
@SuppressWarnings("unchecked")
public static Object extractPropertyValue(IotDeviceMessage message, String identifier) {
Object params = message.getParams();
if (params == null) {
return null;
}
// 策略1如果 params 不是 Map直接返回该值适用于简单的单属性消息
if (!(params instanceof Map)) {
return params;
}
Map<String, Object> paramsMap = (Map<String, Object>) params;
// 策略2直接通过标识符获取属性值
Object directValue = paramsMap.get(identifier);
if (directValue != null) {
return directValue;
}
// 策略3从 properties 字段中获取(适用于标准属性上报消息)
Object properties = paramsMap.get("properties");
if (properties instanceof Map) {
Map<String, Object> propertiesMap = (Map<String, Object>) properties;
Object propertyValue = propertiesMap.get(identifier);
if (propertyValue != null) {
return propertyValue;
}
}
// 策略4从 data 字段中获取(适用于某些消息格式)
Object data = paramsMap.get("data");
if (data instanceof Map) {
Map<String, Object> dataMap = (Map<String, Object>) data;
Object dataValue = dataMap.get(identifier);
if (dataValue != null) {
return dataValue;
}
}
// 策略5从 value 字段中获取(适用于单值消息)
Object value = paramsMap.get("value");
if (value != null) {
return value;
}
// 策略6如果 Map 只有两个字段且包含 identifier返回另一个字段的值
if (paramsMap.size() == 2 && paramsMap.containsKey("identifier")) {
for (Map.Entry<String, Object> entry : paramsMap.entrySet()) {
if (!"identifier".equals(entry.getKey())) {
return entry.getValue();
}
}
}
// 未找到对应的属性值
return null;
}
// ========== Topic 相关 ==========
public static String buildMessageBusGatewayDeviceMessageTopic(String serverId) {

View File

@@ -68,7 +68,13 @@ public class IotMqttUpstreamHandler {
log.info("[handle][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username);
// 2. 设置异常和关闭处理器
// 2. 设置心跳处理器(监听客户端的 PINGREQ 消息)
endpoint.pingHandler(v -> {
log.debug("[handle][收到客户端心跳,客户端 ID: {}]", clientId);
// Vert.x 会自动发送 PINGRESP 响应,无需手动处理
});
// 3. 设置异常和关闭处理器
endpoint.exceptionHandler(ex -> {
log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, connectionManager.getEndpointAddress(endpoint));
cleanupConnection(endpoint);
@@ -77,7 +83,7 @@ public class IotMqttUpstreamHandler {
cleanupConnection(endpoint);
});
// 3. 设置消息处理器
// 4. 设置消息处理器
endpoint.publishHandler(message -> {
try {
processMessage(clientId, message.topicName(), message.payload().getBytes());
@@ -100,7 +106,7 @@ public class IotMqttUpstreamHandler {
}
});
// 4. 设置订阅处理器
// 5. 设置订阅处理器
endpoint.subscribeHandler(subscribe -> {
// 提取主题名称列表用于日志显示
List<String> topicNames = subscribe.topicSubscriptions().stream()
@@ -115,22 +121,22 @@ public class IotMqttUpstreamHandler {
endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels);
});
// 5. 设置取消订阅处理器
// 6. 设置取消订阅处理器
endpoint.unsubscribeHandler(unsubscribe -> {
log.debug("[handle][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics());
endpoint.unsubscribeAcknowledge(unsubscribe.messageId());
});
// 6. 设置 QoS 2消息的 PUBREL 处理器
// 7. 设置 QoS 2消息的 PUBREL 处理器
endpoint.publishReleaseHandler(endpoint::publishComplete);
// 7. 设置断开连接处理器
// 8. 设置断开连接处理器
endpoint.disconnectHandler(v -> {
log.debug("[handle][设备断开连接,客户端 ID: {}]", clientId);
cleanupConnection(endpoint);
});
// 8. 接受连接
// 9. 接受连接
endpoint.accept(false);
}

View File

@@ -103,7 +103,6 @@ yudao:
port: 1883
max-message-size: 8192
connect-timeout-seconds: 60
keep-alive-timeout-seconds: 300
ssl-enabled: false
--- #################### 日志相关配置 ####################

View File

@@ -17,6 +17,8 @@ import lombok.Data;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true)
@JsonSubTypes({
@JsonSubTypes.Type(value = IotDataSinkHttpConfig.class, name = "1"),
@JsonSubTypes.Type(value = IotDataSinkTcpConfig.class, name = "2"),
@JsonSubTypes.Type(value = IotDataSinkWebSocketConfig.class, name = "3"),
@JsonSubTypes.Type(value = IotDataSinkMqttConfig.class, name = "10"),
@JsonSubTypes.Type(value = IotDataSinkRedisConfig.class, name = "21"),
@JsonSubTypes.Type(value = IotDataSinkRocketMQConfig.class, name = "30"),

View File

@@ -0,0 +1,63 @@
package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config;
import lombok.Data;
/**
* IoT TCP 配置 {@link IotAbstractDataSinkConfig} 实现类
*
* @author HUIHUI
*/
@Data
public class IotDataSinkTcpConfig extends IotAbstractDataSinkConfig {
/**
* TCP 服务器地址
*/
private String host;
/**
* TCP 服务器端口
*/
private Integer port;
/**
* 连接超时时间(毫秒)
*/
private Integer connectTimeoutMs = 5000;
/**
* 读取超时时间(毫秒)
*/
private Integer readTimeoutMs = 10000;
/**
* 是否启用 SSL
*/
private Boolean ssl = false;
/**
* SSL 证书路径(当 ssl=true 时需要)
*/
private String sslCertPath;
/**
* 数据格式JSON 或 BINARY
*/
private String dataFormat = "JSON";
/**
* 心跳间隔时间毫秒0 表示不启用心跳
*/
private Long heartbeatIntervalMs = 30000L;
/**
* 重连间隔时间(毫秒)
*/
private Long reconnectIntervalMs = 5000L;
/**
* 最大重连次数
*/
private Integer maxReconnectAttempts = 3;
}

View File

@@ -0,0 +1,87 @@
package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config;
import lombok.Data;
/**
* IoT WebSocket 配置 {@link IotAbstractDataSinkConfig} 实现类
* <p>
* 配置设备消息通过 WebSocket 协议发送到外部 WebSocket 服务器
* 支持 WebSocket (ws://) 和 WebSocket Secure (wss://) 连接
*
* @author HUIHUI
*/
@Data
public class IotDataSinkWebSocketConfig extends IotAbstractDataSinkConfig {
/**
* WebSocket 服务器地址
* 例如ws://localhost:8080/ws 或 wss://example.com/ws
*/
private String serverUrl;
/**
* 连接超时时间(毫秒)
*/
private Integer connectTimeoutMs = 5000;
/**
* 发送超时时间(毫秒)
*/
private Integer sendTimeoutMs = 10000;
/**
* 心跳间隔时间毫秒0 表示不启用心跳
*/
private Long heartbeatIntervalMs = 30000L;
/**
* 心跳消息内容JSON 格式)
*/
private String heartbeatMessage = "{\"type\":\"heartbeat\"}";
/**
* 子协议列表(逗号分隔)
*/
private String subprotocols;
/**
* 自定义请求头JSON 格式)
*/
private String customHeaders;
/**
* 是否启用 SSL 证书验证(仅对 wss:// 生效)
*/
private Boolean verifySslCert = true;
/**
* 数据格式JSON 或 TEXT
*/
private String dataFormat = "JSON";
/**
* 重连间隔时间(毫秒)
*/
private Long reconnectIntervalMs = 5000L;
/**
* 最大重连次数
*/
private Integer maxReconnectAttempts = 3;
/**
* 是否启用压缩
*/
private Boolean enableCompression = false;
/**
* 消息发送重试次数
*/
private Integer sendRetryCount = 1;
/**
* 消息发送重试间隔(毫秒)
*/
private Long sendRetryIntervalMs = 1000L;
}

View File

@@ -76,4 +76,12 @@ public interface RedisKeyConstants {
*/
String DATA_SINK = "iot:data_sink";
/**
* 场景联动规则的数据缓存,使用 Spring Cache 操作
* <p>
* KEY 格式scene_rule_list_${productId}_${deviceId}
* VALUE 数据类型String 数组(JSON),即 {@link cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO} 列表
*/
String SCENE_RULE_LIST = "iot:scene_rule_list";
}

View File

@@ -41,7 +41,7 @@ public class IotAlertConfigServiceImpl implements IotAlertConfigService {
public Long createAlertConfig(IotAlertConfigSaveReqVO createReqVO) {
// 校验关联数据是否存在
sceneRuleService.validateSceneRuleList(createReqVO.getSceneRuleIds());
adminUserApi.validateUserList(createReqVO.getReceiveUserIds());
adminUserApi.validateUserList(createReqVO.getReceiveUserIds()).checkError();
// 插入
IotAlertConfigDO alertConfig = BeanUtils.toBean(createReqVO, IotAlertConfigDO.class);
@@ -55,7 +55,7 @@ public class IotAlertConfigServiceImpl implements IotAlertConfigService {
validateAlertConfigExists(updateReqVO.getId());
// 校验关联数据是否存在
sceneRuleService.validateSceneRuleList(updateReqVO.getSceneRuleIds());
adminUserApi.validateUserList(updateReqVO.getReceiveUserIds());
adminUserApi.validateUserList(updateReqVO.getReceiveUserIds()).checkError();
// 更新
IotAlertConfigDO updateObj = BeanUtils.toBean(updateReqVO, IotAlertConfigDO.class);

View File

@@ -1,8 +1,8 @@
package cn.iocoder.yudao.module.iot.service.rule.data.action;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkKafkaConfig;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.producer.ProducerConfig;

View File

@@ -0,0 +1,91 @@
package cn.iocoder.yudao.module.iot.service.rule.data.action;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig;
import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum;
import cn.iocoder.yudao.module.iot.service.rule.data.action.tcp.IotTcpClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.Duration;
/**
* TCP 的 {@link IotDataRuleAction} 实现类
* <p>
* 负责将设备消息发送到外部 TCP 服务器
* 支持普通 TCP 和 SSL TCP 连接,支持 JSON 和 BINARY 数据格式
* 使用连接池管理 TCP 连接,提高性能和资源利用率
*
* @author HUIHUI
*/
@Component
@Slf4j
public class IotTcpDataRuleAction extends
IotDataRuleCacheableAction<IotDataSinkTcpConfig, IotTcpClient> {
private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(5);
private static final Duration SEND_TIMEOUT = Duration.ofSeconds(10);
@Override
public Integer getType() {
return IotDataSinkTypeEnum.TCP.getType();
}
@Override
protected IotTcpClient initProducer(IotDataSinkTcpConfig config) throws Exception {
// 1.1 参数校验
if (config.getHost() == null || config.getHost().trim().isEmpty()) {
throw new IllegalArgumentException("TCP 服务器地址不能为空");
}
if (config.getPort() == null || config.getPort() <= 0 || config.getPort() > 65535) {
throw new IllegalArgumentException("TCP 服务器端口无效");
}
// 2.1 创建 TCP 客户端
IotTcpClient tcpClient = new IotTcpClient(
config.getHost(),
config.getPort(),
config.getConnectTimeoutMs(),
config.getReadTimeoutMs(),
config.getSsl(),
config.getSslCertPath(),
config.getDataFormat()
);
// 2.2 连接服务器
tcpClient.connect();
log.info("[initProducer][TCP 客户端创建并连接成功,服务器: {}:{}SSL: {},数据格式: {}]",
config.getHost(), config.getPort(), config.getSsl(), config.getDataFormat());
return tcpClient;
}
@Override
protected void closeProducer(IotTcpClient producer) throws Exception {
if (producer != null) {
producer.close();
}
}
@Override
protected void execute(IotDeviceMessage message, IotDataSinkTcpConfig config) throws Exception {
try {
// 1.1 获取或创建 TCP 客户端
IotTcpClient tcpClient = getProducer(config);
// 1.2 检查连接状态,如果断开则重新连接
if (!tcpClient.isConnected()) {
log.warn("[execute][TCP 连接已断开,尝试重新连接,服务器: {}:{}]", config.getHost(), config.getPort());
tcpClient.connect();
}
// 2.1 发送消息并等待结果
tcpClient.sendMessage(message);
// 2.2 记录发送成功日志
log.info("[execute][message({}) config({}) 发送成功TCP 服务器: {}:{}]",
message, config, config.getHost(), config.getPort());
} catch (Exception e) {
log.error("[execute][message({}) config({}) 发送失败TCP 服务器: {}:{}]",
message, config, config.getHost(), config.getPort(), e);
throw e;
}
}
}

View File

@@ -0,0 +1,184 @@
package cn.iocoder.yudao.module.iot.service.rule.data.action.tcp;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import lombok.extern.slf4j.Slf4j;
import javax.net.ssl.SSLSocketFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* IoT TCP 客户端
* <p>
* 负责与外部 TCP 服务器建立连接并发送设备消息
* 支持 JSON 和 BINARY 两种数据格式,支持 SSL 加密连接
*
* @author HUIHUI
*/
@Slf4j
public class IotTcpClient {
private final String host;
private final Integer port;
private final Integer connectTimeoutMs;
private final Integer readTimeoutMs;
private final Boolean ssl;
private final String sslCertPath;
private final String dataFormat;
private Socket socket;
private OutputStream outputStream;
private BufferedReader reader;
private final AtomicBoolean connected = new AtomicBoolean(false);
// TODO @puhui999default 值IotDataSinkTcpConfig.java 枚举起来哈;
public IotTcpClient(String host, Integer port, Integer connectTimeoutMs, Integer readTimeoutMs,
Boolean ssl, String sslCertPath, String dataFormat) {
this.host = host;
this.port = port;
this.connectTimeoutMs = connectTimeoutMs != null ? connectTimeoutMs : 5000;
this.readTimeoutMs = readTimeoutMs != null ? readTimeoutMs : 10000;
this.ssl = ssl != null ? ssl : false;
this.sslCertPath = sslCertPath;
this.dataFormat = dataFormat != null ? dataFormat : "JSON";
}
/**
* 连接到 TCP 服务器
*/
public void connect() throws Exception {
if (connected.get()) {
log.warn("[connect][TCP 客户端已经连接,无需重复连接]");
return;
}
try {
if (ssl) {
// SSL 连接
SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
socket = sslSocketFactory.createSocket();
} else {
// 普通连接
socket = new Socket();
}
// 连接服务器
socket.connect(new InetSocketAddress(host, port), connectTimeoutMs);
socket.setSoTimeout(readTimeoutMs);
// 获取输入输出流
outputStream = socket.getOutputStream();
reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
// 更新状态
connected.set(true);
log.info("[connect][TCP 客户端连接成功,服务器地址: {}:{}]", host, port);
} catch (Exception e) {
close();
log.error("[connect][TCP 客户端连接失败,服务器地址: {}:{}]", host, port, e);
throw e;
}
}
/**
* 发送设备消息
*
* @param message 设备消息
* @throws Exception 发送异常
*/
public void sendMessage(IotDeviceMessage message) throws Exception {
if (!connected.get()) {
throw new IllegalStateException("TCP 客户端未连接");
}
try {
// TODO @puhui999枚举值
String messageData;
if ("JSON".equalsIgnoreCase(dataFormat)) {
// JSON 格式
messageData = JsonUtils.toJsonString(message);
} else {
// BINARY 格式(这里简化为字符串,实际可能需要自定义二进制协议)
messageData = message.toString();
}
// 发送消息
outputStream.write(messageData.getBytes(StandardCharsets.UTF_8));
outputStream.write('\n'); // 添加换行符作为消息分隔符
outputStream.flush();
log.debug("[sendMessage][发送消息成功,设备 ID: {},消息长度: {}]",
message.getDeviceId(), messageData.length());
} catch (Exception e) {
log.error("[sendMessage][发送消息失败,设备 ID: {}]", message.getDeviceId(), e);
throw e;
}
}
/**
* 关闭连接
*/
public void close() {
if (!connected.get()) {
return;
}
try {
// 关闭资源
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
log.warn("[close][关闭输入流失败]", e);
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
log.warn("[close][关闭输出流失败]", e);
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
log.warn("[close][关闭 Socket 失败]", e);
}
}
// 更新状态
connected.set(false);
log.info("[close][TCP 客户端连接已关闭,服务器地址: {}:{}]", host, port);
} catch (Exception e) {
log.error("[close][关闭 TCP 客户端连接异常]", e);
}
}
/**
* 检查连接状态
*
* @return 是否已连接
*/
public boolean isConnected() {
return connected.get() && socket != null && !socket.isClosed();
}
@Override
public String toString() {
return "IotTcpClient{" +
"host='" + host + '\'' +
", port=" + port +
", ssl=" + ssl +
", dataFormat='" + dataFormat + '\'' +
", connected=" + connected.get() +
'}';
}
}

View File

@@ -16,17 +16,20 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotSceneRuleMapper;
import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.service.product.IotProductService;
import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction;
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherManager;
import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
@@ -47,9 +50,6 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
@Resource
private IotSceneRuleMapper sceneRuleMapper;
// TODO @puhui999定时任务基于它调度
@Resource(name = "iotSchedulerManager")
private IotSchedulerManager schedulerManager;
@Resource
private IotProductService productService;
@Resource
@@ -59,38 +59,68 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
private IotSceneRuleMatcherManager sceneRuleMatcherManager;
@Resource
private List<IotSceneRuleAction> sceneRuleActions;
@Resource
private IotSceneRuleTimerHandler timerHandler;
@Override
@CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
public Long createSceneRule(IotSceneRuleSaveReqVO createReqVO) {
IotSceneRuleDO sceneRule = BeanUtils.toBean(createReqVO, IotSceneRuleDO.class);
sceneRuleMapper.insert(sceneRule);
// 注册定时触发器
timerHandler.registerTimerTriggers(sceneRule);
return sceneRule.getId();
}
@Override
@CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
public void updateSceneRule(IotSceneRuleSaveReqVO updateReqVO) {
// 校验存在
validateSceneRuleExists(updateReqVO.getId());
// 更新
IotSceneRuleDO updateObj = BeanUtils.toBean(updateReqVO, IotSceneRuleDO.class);
sceneRuleMapper.updateById(updateObj);
// 更新定时触发器
timerHandler.updateTimerTriggers(updateObj);
}
@Override
@CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
public void updateSceneRuleStatus(Long id, Integer status) {
// 校验存在
// 1. 校验存在
validateSceneRuleExists(id);
// 更新状态
// 2. 更新状态
IotSceneRuleDO updateObj = new IotSceneRuleDO().setId(id).setStatus(status);
sceneRuleMapper.updateById(updateObj);
// 3. 根据状态管理定时触发器
if (CommonStatusEnum.isEnable(status)) {
// 启用时,获取完整的场景规则信息并注册定时触发器
IotSceneRuleDO sceneRule = sceneRuleMapper.selectById(id);
if (sceneRule != null) {
timerHandler.registerTimerTriggers(sceneRule);
}
} else {
// 禁用时,暂停定时触发器
timerHandler.pauseTimerTriggers(id);
}
}
@Override
@CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
public void deleteSceneRule(Long id) {
// 校验存在
// 1. 校验存在
validateSceneRuleExists(id);
// 删除
// 2. 删除
sceneRuleMapper.deleteById(id);
// 3. 删除定时触发器
timerHandler.unregisterTimerTriggers(id);
}
private void validateSceneRuleExists(Long id) {
@@ -126,16 +156,14 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
return sceneRuleMapper.selectListByStatus(status);
}
// TODO 芋艿,缓存待实现 @puhui999
@Override
@Cacheable(value = RedisKeyConstants.SCENE_RULE_LIST, key = "#productId + '_' + #deviceId ")
@TenantIgnore // 忽略租户隔离:因为 IotSceneRuleMessageHandler 调用时,一般未传递租户,所以需要忽略
public List<IotSceneRuleDO> getSceneRuleListByProductIdAndDeviceIdFromCache(Long productId, Long deviceId) {
List<IotSceneRuleDO> list = sceneRuleMapper.selectList();
// 只返回启用状态的规则场景
List<IotSceneRuleDO> enabledList = filterList(list,
sceneRule -> CommonStatusEnum.isEnable(sceneRule.getStatus()));
// 1. 查询启用状态的规则场景
List<IotSceneRuleDO> enabledList = sceneRuleMapper.selectList(IotSceneRuleDO::getStatus, CommonStatusEnum.ENABLE.getStatus());
// 根据 productKey 和 deviceName 进行匹配
// 2. 根据 productKey 和 deviceName 进行匹配
return filterList(enabledList, sceneRule -> {
if (CollUtil.isEmpty(sceneRule.getTriggers())) {
return false;
@@ -144,21 +172,19 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
for (IotSceneRuleDO.Trigger trigger : sceneRule.getTriggers()) {
// 检查触发器是否匹配指定的产品和设备
try {
// 1. 检查产品是否匹配
if (trigger.getProductId() == null) {
return false;
}
if (trigger.getDeviceId() == null) {
// 检查产品是否匹配
if (trigger.getProductId() == null || trigger.getDeviceId() == null) {
return false;
}
// 检查是否是全部设备的特殊标识
if (IotDeviceDO.DEVICE_ID_ALL.equals(trigger.getDeviceId())) {
return true; // 匹配所有设备
return true;
}
// 检查具体设备 ID 是否匹配
return ObjUtil.equal(productId, trigger.getProductId()) && ObjUtil.equal(deviceId, trigger.getDeviceId());
} catch (Exception e) {
log.warn("[isMatchProductAndDevice][产品({}) 设备({}) 匹配触发器异常]", productId, deviceId, e);
log.warn("[getSceneRuleListByProductIdAndDeviceIdFromCache][产品({}) 设备({}) 匹配触发器异常]",
productId, deviceId, e);
return false;
}
}
@@ -168,9 +194,10 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
@Override
public void executeSceneRuleByDevice(IotDeviceMessage message) {
// TODO @芋艿:这里的 tenantId通过设备获取@puhui999
TenantUtils.execute(message.getTenantId(), () -> {
// 1. 获得设备匹配的规则场景
// 1.1 这里的 tenantId通过设备获取
IotDeviceDO device = deviceService.getDeviceFromCache(message.getDeviceId());
TenantUtils.execute(device.getTenantId(), () -> {
// 1.2 获得设备匹配的规则场景
List<IotSceneRuleDO> sceneRules = getMatchedSceneRuleListByMessage(message);
if (CollUtil.isEmpty(sceneRules)) {
return;
@@ -214,19 +241,19 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
*/
private List<IotSceneRuleDO> getMatchedSceneRuleListByMessage(IotDeviceMessage message) {
// 1. 匹配设备
// TODO @芋艿:可能需要 getSelf(); 缓存 @puhui999
// TODO 缓存 @puhui999:可能需要 getSelf()
// 1.1 通过 deviceId 获取设备信息
IotDeviceDO device = deviceService.getDeviceFromCache(message.getDeviceId());
IotDeviceDO device = getSelf().deviceService.getDeviceFromCache(message.getDeviceId());
if (device == null) {
log.warn("[getMatchedSceneRuleListByMessage][设备({}) 不存在]", message.getDeviceId());
return ListUtil.of();
return List.of();
}
// 1.2 通过 productId 获取产品信息
IotProductDO product = productService.getProductFromCache(device.getProductId());
IotProductDO product = getSelf().productService.getProductFromCache(device.getProductId());
if (product == null) {
log.warn("[getMatchedSceneRuleListByMessage][产品({}) 不存在]", device.getProductId());
return ListUtil.of();
return List.of();
}
// 1.3 获取匹配的规则场景
@@ -273,7 +300,6 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
*/
private boolean matchSingleTrigger(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, IotSceneRuleDO sceneRule) {
try {
// 2. 检查触发器的条件分组
return sceneRuleMatcherManager.isMatched(message, trigger) && isTriggerConditionGroupsMatched(message, trigger, sceneRule);
} catch (Exception e) {
log.error("[matchSingleTrigger][触发器匹配异常] sceneRuleId: {}, triggerType: {}, message: {}",
@@ -290,18 +316,19 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
* @param sceneRule 场景规则(用于日志)
* @return 是否匹配
*/
private boolean isTriggerConditionGroupsMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, IotSceneRuleDO sceneRule) {
// 如果没有条件分组,则认为匹配成功(只依赖基础触发器匹配)
private boolean isTriggerConditionGroupsMatched(IotDeviceMessage message,
IotSceneRuleDO.Trigger trigger,
IotSceneRuleDO sceneRule) {
// 1. 如果没有条件分组,则认为匹配成功(只依赖基础触发器匹配)
if (CollUtil.isEmpty(trigger.getConditionGroups())) {
return true;
}
// 检查条件分组:分组与分组之间是"或"的关系,条件与条件之间是"且"的关系
// 2. 检查条件分组:分组与分组之间是"或"的关系,条件与条件之间是"且"的关系
for (List<IotSceneRuleDO.TriggerCondition> conditionGroup : trigger.getConditionGroups()) {
if (CollUtil.isEmpty(conditionGroup)) {
continue;
}
// 检查当前分组中的所有条件是否都匹配(且关系)
boolean allConditionsMatched = true;
for (IotSceneRuleDO.TriggerCondition condition : conditionGroup) {
@@ -310,14 +337,13 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
break;
}
}
// 如果当前分组的所有条件都匹配,则整个触发器匹配成功
if (allConditionsMatched) {
return true;
}
}
// 所有分组都不匹配
// 3. 所有分组都不匹配
return false;
}
@@ -352,13 +378,13 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
sceneRules.forEach(sceneRule -> {
// 2. 遍历规则场景的动作
sceneRule.getActions().forEach(actionConfig -> {
// 3.1 获取对应的动作 Action 数组
// 2.1 获取对应的动作 Action 数组
List<IotSceneRuleAction> actions = filterList(sceneRuleActions,
action -> action.getType().getType().equals(actionConfig.getType()));
if (CollUtil.isEmpty(actions)) {
return;
}
// 3.2 执行动作
// 2.2 执行动作
actions.forEach(action -> {
try {
action.execute(message, sceneRule, actionConfig);

View File

@@ -14,7 +14,6 @@ import java.util.List;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
// TODO @puhui999、@芋艿:未测试;需要场景联动开发完
/**
* IoT 告警恢复的 {@link IotSceneRuleAction} 实现类
*

View File

@@ -17,7 +17,6 @@ import javax.annotation.Nullable;
import javax.annotation.Resource;
import java.util.List;
// TODO @puhui999、@芋艿:未测试;需要场景联动开发完
/**
* IoT 告警触发的 {@link IotSceneRuleAction} 实现类
*

View File

@@ -1,17 +1,23 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.action;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
/**
* IoT 设备控制的 {@link IotSceneRuleAction} 实现类
* IoT 设备属性设置的 {@link IotSceneRuleAction} 实现类
*
* @author 芋道源码
*/
@@ -24,28 +30,108 @@ public class IotDeviceControlSceneRuleAction implements IotSceneRuleAction {
@Resource
private IotDeviceMessageService deviceMessageService;
// TODO @puhui999这里
@Override
public void execute(IotDeviceMessage message,
IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
//IotSceneRuleDO.ActionDeviceControl control = actionConfig.getDeviceControl();
//Assert.notNull(control, "设备控制配置不能为空");
//// 遍历每个设备,下发消息
//control.getDeviceNames().forEach(deviceName -> {
// IotDeviceDO device = deviceService.getDeviceFromCache(control.getProductKey(), deviceName);
// if (device == null) {
// log.error("[execute][message({}) actionConfig({}) 对应的设备不存在]", message, actionConfig);
// return;
// }
// try {
// // TODO @芋艿:@puhui999这块可能要改从 type => method
// IotDeviceMessage downstreamMessage = deviceMessageService.sendDeviceMessage(IotDeviceMessage.requestOf(
// control.getType() + control.getIdentifier(), control.getData()).setDeviceId(device.getId()));
// log.info("[execute][message({}) actionConfig({}) 下发消息({})成功]", message, actionConfig, downstreamMessage);
// } catch (Exception e) {
// log.error("[execute][message({}) actionConfig({}) 下发消息失败]", message, actionConfig, e);
// }
//});
// 1. 参数校验
if (actionConfig.getDeviceId() == null) {
log.error("[execute][规则场景({}) 动作配置({}) 设备编号不能为空]", rule.getId(), actionConfig);
return;
}
if (StrUtil.isEmpty(actionConfig.getIdentifier())) {
log.error("[execute][规则场景({}) 动作配置({}) 属性标识符不能为空]", rule.getId(), actionConfig);
return;
}
// 2. 判断是否为全部设备
if (IotDeviceDO.DEVICE_ID_ALL.equals(actionConfig.getDeviceId())) {
executeForAllDevices(message, rule, actionConfig);
} else {
executeForSingleDevice(message, rule, actionConfig);
}
}
/**
* 为单个设备执行属性设置
*/
private void executeForSingleDevice(IotDeviceMessage message,
IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
// 1. 获取设备信息
IotDeviceDO device = deviceService.getDeviceFromCache(actionConfig.getDeviceId());
if (device == null) {
log.error("[executeForSingleDevice][规则场景({}) 动作配置({}) 对应的设备({}) 不存在]",
rule.getId(), actionConfig, actionConfig.getDeviceId());
return;
}
// 2. 执行属性设置
executePropertySetForDevice(rule, actionConfig, device);
}
/**
* 为产品下的所有设备执行属性设置
*/
private void executeForAllDevices(IotDeviceMessage message,
IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
// 1. 参数校验
if (actionConfig.getProductId() == null) {
log.error("[executeForAllDevices][规则场景({}) 动作配置({}) 产品编号不能为空]", rule.getId(), actionConfig);
return;
}
// 2. 获取产品下的所有设备
List<IotDeviceDO> devices = deviceService.getDeviceListByProductId(actionConfig.getProductId());
if (CollUtil.isEmpty(devices)) {
log.warn("[executeForAllDevices][规则场景({}) 动作配置({}) 产品({}) 下没有设备]",
rule.getId(), actionConfig, actionConfig.getProductId());
return;
}
// 3. 遍历所有设备执行属性设置
for (IotDeviceDO device : devices) {
executePropertySetForDevice(rule, actionConfig, device);
}
}
/**
* 为指定设备执行属性设置
*/
private void executePropertySetForDevice(IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
// 1. 构建属性设置消息
IotDeviceMessage downstreamMessage = buildPropertySetMessage(actionConfig, device);
if (downstreamMessage == null) {
log.error("[executePropertySetForDevice][规则场景({}) 动作配置({}) 设备({}) 构建属性设置消息失败]",
rule.getId(), actionConfig, device.getId());
return;
}
// 2. 发送设备消息
try {
IotDeviceMessage result = deviceMessageService.sendDeviceMessage(downstreamMessage, device);
log.info("[executePropertySetForDevice][规则场景({}) 动作配置({}) 设备({}) 属性设置消息({}) 发送成功]",
rule.getId(), actionConfig, device.getId(), result.getId());
} catch (Exception e) {
log.error("[executePropertySetForDevice][规则场景({}) 动作配置({}) 设备({}) 属性设置消息发送失败]",
rule.getId(), actionConfig, device.getId(), e);
}
}
/**
* 构建属性设置消息
*
* @param actionConfig 动作配置
* @param device 设备信息
* @return 设备消息
*/
private IotDeviceMessage buildPropertySetMessage(IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
try {
// 属性设置参数格式: {"properties": {"identifier": value}}
Object params = Map.of("properties", Map.of(actionConfig.getIdentifier(), actionConfig.getParams()));
return IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), params);
} catch (Exception e) {
log.error("[buildPropertySetMessage][构建属性设置消息异常]", e);
return null;
}
}
@Override

View File

@@ -0,0 +1,145 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.action;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
/**
* IoT 设备服务调用的 {@link IotSceneRuleAction} 实现类
*
* @author HUIHUI
*/
@Component
@Slf4j
public class IotDeviceServiceInvokeSceneRuleAction implements IotSceneRuleAction {
@Resource
private IotDeviceService deviceService;
@Resource
private IotDeviceMessageService deviceMessageService;
@Override
public void execute(IotDeviceMessage message,
IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
// 1. 参数校验
if (actionConfig.getDeviceId() == null) {
log.error("[execute][规则场景({}) 动作配置({}) 设备编号不能为空]", rule.getId(), actionConfig);
return;
}
if (StrUtil.isEmpty(actionConfig.getIdentifier())) {
log.error("[execute][规则场景({}) 动作配置({}) 服务标识符不能为空]", rule.getId(), actionConfig);
return;
}
// 2. 判断是否为全部设备
if (IotDeviceDO.DEVICE_ID_ALL.equals(actionConfig.getDeviceId())) {
executeForAllDevices(message, rule, actionConfig);
} else {
executeForSingleDevice(message, rule, actionConfig);
}
}
/**
* 为单个设备执行服务调用
*/
private void executeForSingleDevice(IotDeviceMessage message,
IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
// 1. 获取设备信息
IotDeviceDO device = deviceService.getDeviceFromCache(actionConfig.getDeviceId());
if (device == null) {
log.error("[executeForSingleDevice][规则场景({}) 动作配置({}) 对应的设备({}) 不存在]",
rule.getId(), actionConfig, actionConfig.getDeviceId());
return;
}
// 2. 执行服务调用
executeServiceInvokeForDevice(rule, actionConfig, device);
}
/**
* 为产品下的所有设备执行服务调用
*/
private void executeForAllDevices(IotDeviceMessage message,
IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
// 1. 参数校验
if (actionConfig.getProductId() == null) {
log.error("[executeForAllDevices][规则场景({}) 动作配置({}) 产品编号不能为空]", rule.getId(), actionConfig);
return;
}
// 2. 获取产品下的所有设备
List<IotDeviceDO> devices = deviceService.getDeviceListByProductId(actionConfig.getProductId());
if (CollUtil.isEmpty(devices)) {
log.warn("[executeForAllDevices][规则场景({}) 动作配置({}) 产品({}) 下没有设备]",
rule.getId(), actionConfig, actionConfig.getProductId());
return;
}
// 3. 遍历所有设备执行服务调用
for (IotDeviceDO device : devices) {
executeServiceInvokeForDevice(rule, actionConfig, device);
}
}
/**
* 为指定设备执行服务调用
*/
private void executeServiceInvokeForDevice(IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
// 1. 构建服务调用消息
IotDeviceMessage downstreamMessage = buildServiceInvokeMessage(actionConfig, device);
if (downstreamMessage == null) {
log.error("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 构建服务调用消息失败]",
rule.getId(), actionConfig, device.getId());
return;
}
// 2. 发送设备消息
try {
IotDeviceMessage result = deviceMessageService.sendDeviceMessage(downstreamMessage, device);
log.info("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 服务调用消息({}) 发送成功]",
rule.getId(), actionConfig, device.getId(), result.getId());
} catch (Exception e) {
log.error("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 服务调用消息发送失败]",
rule.getId(), actionConfig, device.getId(), e);
}
}
/**
* 构建服务调用消息
*
* @param actionConfig 动作配置
* @param device 设备信息
* @return 设备消息
*/
private IotDeviceMessage buildServiceInvokeMessage(IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
try {
// 服务调用参数格式: {"identifier": "serviceId", "params": {...}}
Object params = Map.of(
"identifier", actionConfig.getIdentifier(),
"params", actionConfig.getParams() != null ? actionConfig.getParams() : Map.of()
);
return IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod(), params);
} catch (Exception e) {
log.error("[buildServiceInvokeMessage][构建服务调用消息异常]", e);
return null;
}
}
@Override
public IotSceneRuleActionTypeEnum getType() {
return IotSceneRuleActionTypeEnum.DEVICE_SERVICE_INVOKE;
}
}

View File

@@ -4,10 +4,8 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition.IotScene
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger.IotSceneRuleTriggerMatcher;
/**
* IoT 场景规则匹配器基础接口
* <p>
* 定义所有匹配器的通用行为,包括优先级、名称和启用状态
* <p>
* IoT 场景规则匹配器基础接口:定义所有匹配器的通用行为,包括优先级、名称和启用状态
*
* - {@link IotSceneRuleTriggerMatcher} 触发器匹配器
* - {@link IotSceneRuleConditionMatcher} 条件匹配器
*

View File

@@ -18,10 +18,8 @@ import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
/**
* IoT 场景规则匹配器工具类
* <p>
* 提供通用的条件评估逻辑和工具方法,供触发器和条件匹配器使用
* <p>
* IoT 场景规则匹配器工具类:提供通用的条件评估逻辑和工具方法,供触发器和条件匹配器使用
*
* 该类包含了匹配器实现中常用的工具方法,如条件评估、参数校验、日志记录等
*
* @author HUIHUI
@@ -91,7 +89,7 @@ public final class IotSceneRuleMatcherHelper {
Map<String, Object> springExpressionVariables = new HashMap<>();
// 设置源值
springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_SOURCE, sourceValue);
springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_SOURCE, StrUtil.toString(sourceValue));
// 处理参数值
if (StrUtil.isNotBlank(paramValue)) {

View File

@@ -12,14 +12,11 @@ import org.springframework.stereotype.Component;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
* IoT 场景规则匹配器统一管理器
* <p>
* 负责管理所有匹配器(触发器匹配器和条件匹配器),并提供统一的匹配入口
* IoT 场景规则匹配器统一管理器:负责管理所有匹配器(触发器匹配器和条件匹配器),并提供统一的匹配入口
*
* @author HUIHUI
*/
@@ -45,23 +42,22 @@ public class IotSceneRuleMatcherManager {
return;
}
// 按优先级排序并过滤启用的匹配器
// 1.1 按优先级排序并过滤启用的匹配器
List<IotSceneRuleMatcher> allMatchers = matchers.stream()
.filter(IotSceneRuleMatcher::isEnabled)
.sorted(Comparator.comparing(IotSceneRuleMatcher::getPriority))
.collect(Collectors.toList());
// 分离触发器匹配器和条件匹配器
.toList();
// 1.2 分离触发器匹配器和条件匹配器
List<IotSceneRuleTriggerMatcher> triggerMatchers = allMatchers.stream()
.filter(matcher -> matcher instanceof IotSceneRuleTriggerMatcher)
.map(matcher -> (IotSceneRuleTriggerMatcher) matcher)
.collect(Collectors.toList());
.toList();
List<IotSceneRuleConditionMatcher> conditionMatchers = allMatchers.stream()
.filter(matcher -> matcher instanceof IotSceneRuleConditionMatcher)
.map(matcher -> (IotSceneRuleConditionMatcher) matcher)
.collect(Collectors.toList());
.toList();
// 构建触发器匹配器映射表
// 2.1 构建触发器匹配器映射表
this.triggerMatchers = convertMap(triggerMatchers, IotSceneRuleTriggerMatcher::getSupportedTriggerType,
Function.identity(),
(existing, replacement) -> {
@@ -71,7 +67,7 @@ public class IotSceneRuleMatcherManager {
existing.getSupportedTriggerType() : replacement.getSupportedTriggerType());
return existing.getPriority() <= replacement.getPriority() ? existing : replacement;
}, LinkedHashMap::new);
// 构建条件匹配器映射表
// 2.2 构建条件匹配器映射表
this.conditionMatchers = convertMap(conditionMatchers, IotSceneRuleConditionMatcher::getSupportedConditionType,
Function.identity(),
(existing, replacement) -> {
@@ -83,7 +79,7 @@ public class IotSceneRuleMatcherManager {
},
LinkedHashMap::new);
// 日志输出初始化信息
// 3. 日志输出初始化信息
log.info("[IotSceneRuleMatcherManager][初始化完成,共加载({})个匹配器,其中触发器匹配器({})个,条件匹配器({})个]",
allMatchers.size(), this.triggerMatchers.size(), this.conditionMatchers.size());
this.triggerMatchers.forEach((type, matcher) ->
@@ -136,7 +132,7 @@ public class IotSceneRuleMatcherManager {
return false;
}
// 根据条件类型查找对应的匹配器
// 1. 根据条件类型查找对应的匹配器
IotSceneRuleConditionTypeEnum conditionType = IotSceneRuleConditionTypeEnum.typeOf(condition.getType());
if (conditionType == null) {
log.warn("[isConditionMatched][conditionType({}) 未知的条件类型]", condition.getType());
@@ -148,7 +144,7 @@ public class IotSceneRuleMatcherManager {
return false;
}
// 执行匹配逻辑
// 2. 执行匹配逻辑
try {
return matcher.matches(message, condition);
} catch (Exception e) {

View File

@@ -17,15 +17,13 @@ import java.time.format.DateTimeFormatter;
import java.util.List;
/**
* 当前时间条件匹配器
* <p>
* 处理时间相关的子条件匹配逻辑
* 当前时间条件匹配器处理时间相关的子条件匹配逻辑
*
* @author HUIHUI
*/
@Component
@Slf4j
public class CurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher {
public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher {
/**
* 时间格式化器 - HH:mm:ss

View File

@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
@@ -7,15 +8,14 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
import org.springframework.stereotype.Component;
/**
* 设备属性条件匹配器
* <p>
* 处理设备属性相关的子条件匹配逻辑
* 设备属性条件匹配器处理设备属性相关的子条件匹配逻辑
*
* @author HUIHUI
*/
@Component
public class DevicePropertyConditionMatcher implements IotSceneRuleConditionMatcher {
public class IotDevicePropertyConditionMatcher implements IotSceneRuleConditionMatcher {
@Override
public IotSceneRuleConditionTypeEnum getSupportedConditionType() {
@@ -43,10 +43,10 @@ public class DevicePropertyConditionMatcher implements IotSceneRuleConditionMatc
return false;
}
// 2.1. 获取属性值
Object propertyValue = message.getParams();
// 2.1. 获取属性值 - 使用工具类方法正确提取属性值
Object propertyValue = IotDeviceMessageUtils.extractPropertyValue(message, condition.getIdentifier());
if (propertyValue == null) {
IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中属性值为空");
IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中属性值为空或未找到指定属性");
return false;
}

View File

@@ -1,20 +1,19 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
import org.springframework.stereotype.Component;
/**
* 设备状态条件匹配器
* <p>
* 处理设备状态相关的子条件匹配逻辑
* 设备状态条件匹配器处理设备状态相关的子条件匹配逻辑
*
* @author HUIHUI
*/
@Component
public class DeviceStateConditionMatcher implements IotSceneRuleConditionMatcher {
public class IotDeviceStateConditionMatcher implements IotSceneRuleConditionMatcher {
@Override
public IotSceneRuleConditionTypeEnum getSupportedConditionType() {
@@ -35,8 +34,9 @@ public class DeviceStateConditionMatcher implements IotSceneRuleConditionMatcher
return false;
}
// 2.1 获取设备状态值
Object stateValue = message.getParams();
// 2.1 获取设备状态值 - 使用工具类方法正确提取状态值
// 对于设备状态条件状态值通过 getIdentifier 获取实际是从 params.state 字段
String stateValue = IotDeviceMessageUtils.getIdentifier(message);
if (stateValue == null) {
IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中设备状态值为空");
return false;

View File

@@ -6,12 +6,9 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcher;
/**
* IoT 场景规则条件匹配器接口
* <p>
* 专门处理子条件匹配逻辑,如设备状态、属性值、时间条件等
* <p>
* 条件匹配器负责判断设备消息是否满足场景规则的附加条件,
* 在触发器匹配成功后进行进一步的条件筛选
* IoT 场景规则条件匹配器接口:专门处理子条件的匹配逻辑,如设备状态、属性值、时间条件等
*
* 条件匹配器负责判断设备消息是否满足场景规则的附加条件,在触发器匹配成功后进行进一步的条件筛选
*
* @author HUIHUI
*/

View File

@@ -10,14 +10,12 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatche
import org.springframework.stereotype.Component;
/**
* 设备事件上报触发器匹配器
* <p>
* 处理设备事件上报的触发器匹配逻辑
* 设备事件上报触发器匹配器处理设备事件上报的触发器匹配逻辑
*
* @author HUIHUI
*/
@Component
public class DeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
public class IotDeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
@Override
public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {

View File

@@ -9,14 +9,12 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatche
import org.springframework.stereotype.Component;
/**
* 设备属性上报触发器匹配器
* <p>
* 处理设备属性数据上报的触发器匹配逻辑
* 设备属性上报触发器匹配器处理设备属性数据上报的触发器匹配逻辑
*
* @author HUIHUI
*/
@Component
public class DevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
public class IotDevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
@Override
public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
@@ -52,10 +50,10 @@ public class DevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatc
return false;
}
// 2.1 获取属性值
Object propertyValue = message.getParams();
// 2.1 获取属性值 - 使用工具类方法正确提取属性值
Object propertyValue = IotDeviceMessageUtils.extractPropertyValue(message, trigger.getIdentifier());
if (propertyValue == null) {
IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中属性值为空");
IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中属性值为空或未找到指定属性");
return false;
}

View File

@@ -9,14 +9,12 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatche
import org.springframework.stereotype.Component;
/**
* 设备服务调用触发器匹配器
* <p>
* 处理设备服务调用的触发器匹配逻辑
* 设备服务调用触发器匹配器处理设备服务调用的触发器匹配逻辑
*
* @author HUIHUI
*/
@Component
public class DeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMatcher {
public class IotDeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMatcher {
@Override
public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
@@ -46,7 +44,7 @@ public class DeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMat
// 2. 对于服务调用触发器通常只需要匹配服务标识符即可
// 不需要检查操作符和值因为服务调用本身就是触发条件
// TODO @puhui999: 服务调用时校验输入参数是否匹配条件
// TODO @puhui999: 服务调用时校验输入参数是否匹配条件
IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger);
return true;
}

View File

@@ -2,20 +2,19 @@ package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
import org.springframework.stereotype.Component;
/**
* 设备状态更新触发器匹配器
* <p>
* 处理设备上下线状态变更的触发器匹配逻辑
* 设备状态更新触发器匹配器处理设备上下线状态变更的触发器匹配逻辑
*
* @author HUIHUI
*/
@Component
public class DeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatcher {
public class IotDeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatcher {
@Override
public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
@@ -43,16 +42,17 @@ public class DeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatch
return false;
}
// 2.1 获取设备状态值
Object stateValue = message.getParams();
if (stateValue == null) {
// 2.1 获取设备状态值 - 使用工具类方法正确提取状态值
// 对于状态更新消息状态值通过 getIdentifier 获取实际是从 params.state 字段
String stateIdentifier = IotDeviceMessageUtils.getIdentifier(message);
if (stateIdentifier == null) {
IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中设备状态值为空");
return false;
}
// 2.2 使用条件评估器进行匹配
// TODO @puhui999: 状态匹配重新实现
boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(stateValue, trigger.getOperator(), trigger.getValue());
// 状态值通常是字符串或数字直接使用标识符作为状态值
boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(stateIdentifier, trigger.getOperator(), trigger.getValue());
if (matched) {
IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger);
} else {

View File

@@ -6,12 +6,9 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcher;
/**
* IoT 场景规则触发器匹配器接口
* <p>
* 专门处理主触发条件的匹配逻辑,如设备消息类型、定时器等
* <p>
* 触发器匹配器负责判断设备消息是否满足场景规则的主触发条件,
* 是场景规则执行的第一道门槛
* IoT 场景规则触发器匹配器接口:专门处理主触发条件的匹配逻辑,如设备消息类型、定时器等
*
* 触发器匹配器负责判断设备消息是否满足场景规则的主触发条件,是场景规则执行的第一道门槛
*
* @author HUIHUI
*/

View File

@@ -9,15 +9,14 @@ import org.quartz.CronExpression;
import org.springframework.stereotype.Component;
/**
* 定时触发器匹配器
* <p>
* 处理定时触发的触发器匹配逻辑
* 定时触发器匹配器处理定时触发的触发器匹配逻辑
*
* 注意定时触发器不依赖设备消息主要用于定时任务场景
*
* @author HUIHUI
*/
@Component
public class TimerTriggerMatcher implements IotSceneRuleTriggerMatcher {
public class IotTimerTriggerMatcher implements IotSceneRuleTriggerMatcher {
@Override
public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {

View File

@@ -0,0 +1,154 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.timer;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager;
import cn.iocoder.yudao.module.iot.job.rule.IotSceneRuleJob;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.quartz.SchedulerException;
import org.springframework.stereotype.Component;
import java.util.List;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
/**
* IoT 场景规则定时触发器处理器:负责管理定时触发器的注册、更新、删除等操作
*
* @author HUIHUI
*/
@Component
@Slf4j
public class IotSceneRuleTimerHandler {
@Resource(name = "iotSchedulerManager")
private IotSchedulerManager schedulerManager;
/**
* 注册场景规则的定时触发器
*
* @param sceneRule 场景规则
*/
public void registerTimerTriggers(IotSceneRuleDO sceneRule) {
// 1. 过滤出定时触发器
if (sceneRule == null || CollUtil.isEmpty(sceneRule.getTriggers())) {
return;
}
List<IotSceneRuleDO.Trigger> timerTriggers = filterList(sceneRule.getTriggers(),
trigger -> ObjUtil.equals(trigger.getType(), IotSceneRuleTriggerTypeEnum.TIMER.getType()));
if (CollUtil.isEmpty(timerTriggers)) {
return;
}
// 2. 注册每个定时触发器
timerTriggers.forEach(trigger -> registerSingleTimerTrigger(sceneRule, trigger));
}
/**
* 更新场景规则的定时触发器
*
* @param sceneRule 场景规则
*/
public void updateTimerTriggers(IotSceneRuleDO sceneRule) {
if (sceneRule == null) {
return;
}
// 1. 先删除旧的定时任务
unregisterTimerTriggers(sceneRule.getId());
// 2.1 如果场景规则已禁用,则不重新注册
if (CommonStatusEnum.isDisable(sceneRule.getStatus())) {
log.info("[updateTimerTriggers][场景规则({}) 已禁用,不注册定时触发器]", sceneRule.getId());
return;
}
// 2.2 重新注册定时触发器
registerTimerTriggers(sceneRule);
}
/**
* 注销场景规则的定时触发器
*
* @param sceneRuleId 场景规则 ID
*/
public void unregisterTimerTriggers(Long sceneRuleId) {
if (sceneRuleId == null) {
return;
}
String jobName = buildJobName(sceneRuleId);
try {
schedulerManager.deleteJob(jobName);
log.info("[unregisterTimerTriggers][场景规则({}) 定时触发器注销成功]", sceneRuleId);
} catch (SchedulerException e) {
log.error("[unregisterTimerTriggers][场景规则({}) 定时触发器注销失败]", sceneRuleId, e);
}
}
/**
* 暂停场景规则的定时触发器
*
* @param sceneRuleId 场景规则 ID
*/
public void pauseTimerTriggers(Long sceneRuleId) {
if (sceneRuleId == null) {
return;
}
String jobName = buildJobName(sceneRuleId);
try {
schedulerManager.pauseJob(jobName);
log.info("[pauseTimerTriggers][场景规则({}) 定时触发器暂停成功]", sceneRuleId);
} catch (SchedulerException e) {
log.error("[pauseTimerTriggers][场景规则({}) 定时触发器暂停失败]", sceneRuleId, e);
}
}
/**
* 注册单个定时触发器
*
* @param sceneRule 场景规则
* @param trigger 定时触发器配置
*/
private void registerSingleTimerTrigger(IotSceneRuleDO sceneRule, IotSceneRuleDO.Trigger trigger) {
// 1. 参数校验
if (StrUtil.isBlank(trigger.getCronExpression())) {
log.error("[registerSingleTimerTrigger][场景规则({}) 定时触发器缺少 CRON 表达式]", sceneRule.getId());
return;
}
try {
// 2.1 构建任务名称和数据
String jobName = buildJobName(sceneRule.getId());
// 2.2 注册定时任务
schedulerManager.addOrUpdateJob(
IotSceneRuleJob.class,
jobName,
trigger.getCronExpression(),
IotSceneRuleJob.buildJobDataMap(sceneRule.getId())
);
log.info("[registerSingleTimerTrigger][场景规则({}) 定时触发器注册成功CRON: {}]",
sceneRule.getId(), trigger.getCronExpression());
} catch (SchedulerException e) {
log.error("[registerSingleTimerTrigger][场景规则({}) 定时触发器注册失败CRON: {}]",
sceneRule.getId(), trigger.getCronExpression(), e);
}
}
/**
* 构建任务名称
*
* @param sceneRuleId 场景规则 ID
* @return 任务名称
*/
private String buildJobName(Long sceneRuleId) {
return "iot_scene_rule_timer_" + sceneRuleId;
}
}

View File

@@ -72,7 +72,7 @@
FROM device_property_${reqVO.deviceId}
WHERE ${@cn.hutool.core.util.StrUtil@toUnderlineCase(reqVO.identifier)} IS NOT NULL
AND ts BETWEEN ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[0])}
AND ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[1])}
AND ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[1])}
ORDER BY ts DESC
</select>

View File

@@ -0,0 +1,162 @@
package cn.iocoder.yudao.module.iot.service.rule.data.action;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig;
import cn.iocoder.yudao.module.iot.service.rule.data.action.tcp.IotTcpClient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* {@link IotTcpDataRuleAction} 的单元测试
*
* @author HUIHUI
*/
class IotTcpDataRuleActionTest {
private IotTcpDataRuleAction tcpDataRuleAction;
@Mock
private IotTcpClient mockTcpClient;
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);
tcpDataRuleAction = new IotTcpDataRuleAction();
}
@Test
public void testGetType() {
// 准备参数
Integer expectedType = 2; // 数据接收类型枚举中 TCP 类型的值
// 调用方法
Integer actualType = tcpDataRuleAction.getType();
// 断言结果
assertEquals(expectedType, actualType);
}
// TODO @puhui999_ 后面是小写哈,单测的命名规则。
@Test
public void testInitProducer_Success() throws Exception {
// 准备参数
IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
config.setHost("localhost");
config.setPort(8080);
config.setDataFormat("JSON");
config.setSsl(false);
// 调用方法 & 断言结果
// 此测试需要实际的 TCP 连接,在单元测试中可能不可用
// 目前我们只验证配置是否有效
assertNotNull(config.getHost());
assertTrue(config.getPort() > 0 && config.getPort() <= 65535);
}
@Test
public void testInitProducer_InvalidHost() {
// 准备参数
IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
config.setHost("");
config.setPort(8080);
// 调用方法 & 断言结果
IotTcpDataRuleAction action = new IotTcpDataRuleAction();
// 测试验证逻辑(通常在 initProducer 方法中)
assertThrows(IllegalArgumentException.class, () -> {
if (config.getHost() == null || config.getHost().trim().isEmpty()) {
throw new IllegalArgumentException("TCP 服务器地址不能为空");
}
});
}
@Test
public void testInitProducer_InvalidPort() {
// 准备参数
IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
config.setHost("localhost");
config.setPort(-1);
// 调用方法 & 断言结果
assertThrows(IllegalArgumentException.class, () -> {
if (config.getPort() == null || config.getPort() <= 0 || config.getPort() > 65535) {
throw new IllegalArgumentException("TCP 服务器端口无效");
}
});
}
@Test
public void testCloseProducer() throws Exception {
// 准备参数
IotTcpClient client = mock(IotTcpClient.class);
// 调用方法
tcpDataRuleAction.closeProducer(client);
// 断言结果
verify(client, times(1)).close();
}
@Test
public void testExecute_WithValidConfig() {
// 准备参数
IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.report",
"{\"temperature\": 25.5, \"humidity\": 60}");
IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
config.setHost("localhost");
config.setPort(8080);
config.setDataFormat("JSON");
// 调用方法 & 断言结果
// 通常这需要实际的 TCP 连接
// 在单元测试中,我们只验证输入参数
assertNotNull(message);
assertNotNull(config);
assertNotNull(config.getHost());
assertTrue(config.getPort() > 0);
}
@Test
public void testConfig_DefaultValues() {
// 准备参数
IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
// 调用方法 & 断言结果
// 验证默认值
assertEquals("JSON", config.getDataFormat());
assertEquals(5000, config.getConnectTimeoutMs());
assertEquals(10000, config.getReadTimeoutMs());
assertEquals(false, config.getSsl());
assertEquals(30000L, config.getHeartbeatIntervalMs());
assertEquals(5000L, config.getReconnectIntervalMs());
assertEquals(3, config.getMaxReconnectAttempts());
}
@Test
public void testMessageSerialization() {
// 准备参数
IotDeviceMessage message = IotDeviceMessage.builder()
.deviceId(123L)
.method("thing.property.report")
.params("{\"temperature\": 25.5}")
.build();
// 调用方法
String json = JsonUtils.toJsonString(message);
// 断言结果
assertNotNull(json);
assertTrue(json.contains("\"deviceId\":123"));
assertTrue(json.contains("\"method\":\"thing.property.report\""));
assertTrue(json.contains("\"temperature\":25.5"));
}
}

View File

@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/**
* Matcher 测试基类
* 提供通用的 Spring 测试配置
*
* @author HUIHUI
*/
@SpringJUnitConfig
public abstract class IotBaseConditionMatcherTest {
/**
* 注入一下 SpringUtil解析 EL 表达式时需要
* {@link SpringExpressionUtils#parseExpression}
*/
@Configuration
static class TestConfig {
@Bean
public SpringUtil springUtil() {
return new SpringUtil();
}
}
}

View File

@@ -1,12 +1,12 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
@@ -16,14 +16,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
import static org.junit.jupiter.api.Assertions.*;
/**
* {@link CurrentTimeConditionMatcher} 的单元测试
* {@link IotCurrentTimeConditionMatcher} 的单元测试
*
* @author HUIHUI
*/
public class CurrentTimeConditionMatcherTest extends BaseMockitoUnitTest {
public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherTest {
@InjectMocks
private CurrentTimeConditionMatcher matcher;
private IotCurrentTimeConditionMatcher matcher;
@BeforeEach
public void setUp() {
matcher = new IotCurrentTimeConditionMatcher();
}
@Test
public void testGetSupportedConditionType() {

View File

@@ -1,30 +1,32 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import java.util.HashMap;
import java.util.Map;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@link DevicePropertyConditionMatcher} 的单元测试
* {@link IotDevicePropertyConditionMatcher} 的单元测试
*
* @author HUIHUI
*/
public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
public class IotDevicePropertyConditionMatcherTest extends IotBaseConditionMatcherTest {
@InjectMocks
private DevicePropertyConditionMatcher matcher;
private IotDevicePropertyConditionMatcher matcher;
@BeforeEach
public void setUp() {
matcher = new IotDevicePropertyConditionMatcher();
}
@Test
public void testGetSupportedConditionType() {
@@ -41,27 +43,17 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
int result = matcher.getPriority();
// 断言
assertEquals(20, result);
}
@Test
public void testIsEnabled() {
// 调用
boolean result = matcher.isEnabled();
// 断言
assertTrue(result);
assertEquals(25, result); // 修正实际返回值是 25
}
@Test
public void testMatches_temperatureEquals_success() {
// 准备参数
String propertyName = "temperature";
// 准备参数创建属性上报消息
String propertyIdentifier = "temperature";
Double propertyValue = 25.5;
Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
propertyName,
propertyIdentifier,
IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
String.valueOf(propertyValue)
);
@@ -75,14 +67,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_humidityGreaterThan_success() {
// 准备参数
String propertyName = "humidity";
// 准备参数创建属性上报消息
String propertyIdentifier = "humidity";
Integer propertyValue = 75;
Integer compareValue = 70;
Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
propertyName,
propertyIdentifier,
IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
String.valueOf(compareValue)
);
@@ -96,14 +87,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_pressureLessThan_success() {
// 准备参数
String propertyName = "pressure";
// 准备参数创建属性上报消息
String propertyIdentifier = "pressure";
Double propertyValue = 1010.5;
Integer compareValue = 1020;
Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
propertyName,
propertyIdentifier,
IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(),
String.valueOf(compareValue)
);
@@ -117,14 +107,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_statusNotEquals_success() {
// 准备参数
String propertyName = "status";
// 准备参数创建属性上报消息
String propertyIdentifier = "status";
String propertyValue = "active";
String compareValue = "inactive";
Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
propertyName,
propertyIdentifier,
IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator(),
compareValue
);
@@ -138,14 +127,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_propertyMismatch_fail() {
// 准备参数
String propertyName = "temperature";
// 准备参数创建属性上报消息值不满足条件
String propertyIdentifier = "temperature";
Double propertyValue = 15.0;
Integer compareValue = 20;
Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
propertyName,
propertyIdentifier,
IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
String.valueOf(compareValue)
);
@@ -158,14 +146,16 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
}
@Test
public void testMatches_propertyNotFound_fail() {
// 准备参数
Map<String, Object> properties = MapUtil.of("temperature", 25.5);
IotDeviceMessage message = createDeviceMessage(properties);
public void testMatches_identifierMismatch_fail() {
// 准备参数标识符不匹配
String messageIdentifier = "temperature";
String conditionIdentifier = "humidity";
Double propertyValue = 25.5;
IotDeviceMessage message = createPropertyPostMessage(messageIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
randomString(), // 随机不存在的属性名
IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
"50"
conditionIdentifier,
IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
String.valueOf(propertyValue)
);
// 调用
@@ -178,8 +168,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_nullCondition_fail() {
// 准备参数
Map<String, Object> properties = MapUtil.of("temperature", 25.5);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
// 调用
boolean result = matcher.matches(message, null);
@@ -191,8 +180,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_nullConditionType_fail() {
// 准备参数
Map<String, Object> properties = MapUtil.of("temperature", 25.5);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
condition.setType(null);
@@ -206,8 +194,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_missingIdentifier_fail() {
// 准备参数
Map<String, Object> properties = MapUtil.of("temperature", 25.5);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
condition.setIdentifier(null); // 缺少标识符
@@ -224,8 +211,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_missingOperator_fail() {
// 准备参数
Map<String, Object> properties = MapUtil.of("temperature", 25.5);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
condition.setIdentifier("temperature");
@@ -242,8 +228,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_missingParam_fail() {
// 准备参数
Map<String, Object> properties = MapUtil.of("temperature", 25.5);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
condition.setIdentifier("temperature");
@@ -275,7 +260,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_nullDeviceProperties_fail() {
// 准备参数
// 准备参数消息的 params null
IotDeviceMessage message = new IotDeviceMessage();
message.setParams(null);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
@@ -292,14 +277,79 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
}
@Test
public void testMatches_voltageGreaterThanOrEquals_success() {
// 准备参数
String propertyName = "voltage";
Double propertyValue = 12.0;
Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);
IotDeviceMessage message = createDeviceMessage(properties);
public void testMatches_propertiesStructure_success() {
// 测试使用 properties 结构的消息真实的属性上报场景
String identifier = "temperature";
Double propertyValue = 25.5;
IotDeviceMessage message = createPropertyPostMessageWithProperties(identifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
propertyName,
identifier,
IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
"20"
);
// 调用
boolean result = matcher.matches(message, condition);
// 断言修复后的实现应该能正确从 properties 中提取属性值
assertTrue(result);
}
@Test
public void testMatches_simpleValueMessage_success() {
// 测试简单值消息params 直接是属性值
Double propertyValue = 25.5;
IotDeviceMessage message = createSimpleValueMessage(propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
"any", // 对于简单值消息标识符匹配会被跳过
IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
"20"
);
// 调用
boolean result = matcher.matches(message, condition);
// 断言修复后的实现应该能处理简单值消息
// 但由于标识符匹配失败结果为 false
assertFalse(result);
}
@Test
public void testMatches_valueFieldStructure_success() {
// 测试使用 value 字段的消息结构
String identifier = "temperature";
Double propertyValue = 25.5;
IotDeviceMessage message = new IotDeviceMessage();
message.setDeviceId(randomLongId());
message.setMethod("thing.event.post");
Map<String, Object> params = new HashMap<>();
params.put("identifier", identifier);
params.put("value", propertyValue);
message.setParams(params);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
identifier,
IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
"20"
);
// 调用
boolean result = matcher.matches(message, condition);
// 断言修复后的实现应该能从 value 字段提取属性值
assertTrue(result);
}
@Test
public void testMatches_voltageGreaterThanOrEquals_success() {
// 准备参数创建属性上报消息
String propertyIdentifier = "voltage";
Double propertyValue = 12.0;
IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
propertyIdentifier,
IotSceneRuleConditionOperatorEnum.GREATER_THAN_OR_EQUALS.getOperator(),
String.valueOf(propertyValue)
);
@@ -313,14 +363,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_currentLessThanOrEquals_success() {
// 准备参数
String propertyName = "current";
// 准备参数创建属性上报消息
String propertyIdentifier = "current";
Double propertyValue = 2.5;
Double compareValue = 3.0;
Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
propertyName,
propertyIdentifier,
IotSceneRuleConditionOperatorEnum.LESS_THAN_OR_EQUALS.getOperator(),
String.valueOf(compareValue)
);
@@ -334,13 +383,12 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_stringProperty_success() {
// 准备参数
String propertyName = "mode";
// 准备参数创建属性上报消息
String propertyIdentifier = "mode";
String propertyValue = "auto";
Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
propertyName,
propertyIdentifier,
IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
propertyValue
);
@@ -354,13 +402,12 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_booleanProperty_success() {
// 准备参数
String propertyName = "enabled";
// 准备参数创建属性上报消息
String propertyIdentifier = "enabled";
Boolean propertyValue = true;
Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);
IotDeviceMessage message = createDeviceMessage(properties);
IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
propertyName,
propertyIdentifier,
IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
String.valueOf(propertyValue)
);
@@ -372,40 +419,61 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
assertTrue(result);
}
@Test
public void testMatches_multipleProperties_success() {
// 准备参数
Map<String, Object> properties = MapUtil.builder(new HashMap<String, Object>())
.put("temperature", 25.5)
.put("humidity", 60)
.put("status", "active")
.put("enabled", true)
.build();
IotDeviceMessage message = createDeviceMessage(properties);
String targetProperty = "humidity";
Integer targetValue = 60;
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
targetProperty,
IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
String.valueOf(targetValue)
);
// 调用
boolean result = matcher.matches(message, condition);
// 断言
assertTrue(result);
}
// ========== 辅助方法 ==========
/**
* 创建设备消息
* 创建设备消息用于测试
*
* 支持的消息格式
* 1. 直接属性值params 直接是属性值适用于简单消息
* 2. 标识符+params 包含 identifier 和对应的属性值
* 3. properties 结构params.properties[identifier] = value
* 4. data 结构params.data[identifier] = value
* 5. value 字段params.value = value
*/
private IotDeviceMessage createDeviceMessage(Map<String, Object> properties) {
private IotDeviceMessage createPropertyPostMessage(String identifier, Object value) {
IotDeviceMessage message = new IotDeviceMessage();
message.setDeviceId(randomLongId());
message.setParams(properties);
message.setMethod("thing.event.post"); // 使用事件上报方法
// 创建符合修复后逻辑的 params 结构
Map<String, Object> params = new HashMap<>();
params.put("identifier", identifier);
// 直接将属性值放在标识符对应的字段中
params.put(identifier, value);
message.setParams(params);
return message;
}
/**
* 创建使用 properties 结构的消息模拟真实的属性上报消息
*/
private IotDeviceMessage createPropertyPostMessageWithProperties(String identifier, Object value) {
IotDeviceMessage message = new IotDeviceMessage();
message.setDeviceId(randomLongId());
message.setMethod("thing.property.post"); // 属性上报方法
Map<String, Object> properties = new HashMap<>();
properties.put(identifier, value);
Map<String, Object> params = new HashMap<>();
params.put("properties", properties);
message.setParams(params);
return message;
}
/**
* 创建简单值消息params 直接是属性值
*/
private IotDeviceMessage createSimpleValueMessage(Object value) {
IotDeviceMessage message = new IotDeviceMessage();
message.setDeviceId(randomLongId());
message.setMethod("thing.property.post");
// 直接将属性值作为 params
message.setParams(value);
return message;
}

View File

@@ -1,27 +1,31 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@link DeviceStateConditionMatcher} 的单元测试
* {@link IotDeviceStateConditionMatcher} 的单元测试
*
* @author HUIHUI
*/
public class DeviceStateConditionMatcherTest extends BaseMockitoUnitTest {
public class IotDeviceStateConditionMatcherTest extends IotBaseConditionMatcherTest {
@InjectMocks
private DeviceStateConditionMatcher matcher;
private IotDeviceStateConditionMatcher matcher;
@BeforeEach
public void setUp() {
matcher = new IotDeviceStateConditionMatcher();
}
@Test
public void testGetSupportedConditionType() {

View File

@@ -1,13 +1,13 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import java.util.HashMap;
import java.util.Map;
@@ -18,14 +18,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
import static org.junit.jupiter.api.Assertions.*;
/**
* {@link DeviceEventPostTriggerMatcher} 的单元测试
* {@link IotDeviceEventPostTriggerMatcher} 的单元测试
*
* @author HUIHUI
*/
public class DeviceEventPostTriggerMatcherTest extends BaseMockitoUnitTest {
public class IotDeviceEventPostTriggerMatcherTest extends IotBaseConditionMatcherTest {
@InjectMocks
private DeviceEventPostTriggerMatcher matcher;
private IotDeviceEventPostTriggerMatcher matcher;
@BeforeEach
public void setUp() {
matcher = new IotDeviceEventPostTriggerMatcher();
}
@Test
public void testGetSupportedTriggerType_success() {

View File

@@ -1,14 +1,14 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import java.util.HashMap;
import java.util.Map;
@@ -20,14 +20,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
import static org.junit.jupiter.api.Assertions.*;
/**
* {@link DevicePropertyPostTriggerMatcher} 的单元测试
* {@link IotDevicePropertyPostTriggerMatcher} 的单元测试
*
* @author HUIHUI
*/
public class DevicePropertyPostTriggerMatcherTest extends BaseMockitoUnitTest {
public class IotDevicePropertyPostTriggerMatcherTest extends IotBaseConditionMatcherTest {
@InjectMocks
private DevicePropertyPostTriggerMatcher matcher;
private IotDevicePropertyPostTriggerMatcher matcher;
@BeforeEach
public void setUp() {
matcher = new IotDevicePropertyPostTriggerMatcher();
}
@Test
public void testGetSupportedTriggerType_success() {

Some files were not shown because too many files have changed in this diff Show More