From 6e492e1e6b3f30fac6d72f5fc7cb1e9f092fb43f Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 31 May 2026 20:47:58 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E5=90=8C=E6=AD=A5=E3=80=91BOOT=20?= =?UTF-8?q?=E5=92=8C=20CLOUD=20=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/tools/convertor.py | 107 ++++++-- .../framework/common/util/json/JsonUtils.java | 8 + .../mybatis/core/query/QueryWrapperX.java | 6 +- .../flowable/core/util/FlowableUtils.java | 34 ++- .../BpmTaskCandidateInvokerTest.java | 5 +- ...kCandidateStartUserSelectStrategyTest.java | 6 +- .../flowable/core/util/FlowableUtilsTest.java | 167 ++++++++++++ .../infra/enums/ErrorCodeConstants.java | 1 + .../admin/file/vo/file/FileUploadReqVO.java | 7 +- .../app/file/AppFileController.java | 2 +- .../core/client/local/LocalFileClient.java | 15 +- .../file/core/utils/FilePathUtils.java | 106 ++++++++ .../infra/service/file/FileServiceImpl.java | 34 ++- .../file/core/local/LocalFileClientTest.java | 50 ++++ .../service/file/FileServiceImplTest.java | 87 ++++++ .../module/iot/enums/ErrorCodeConstants.java | 3 + .../IotMessageBusAutoConfiguration.java | 27 +- .../tcpserver/IotModbusTcpServerProtocol.java | 18 ++ .../IotModbusTcpServerConfigCacheService.java | 26 +- .../IotModbusTcpServerConnectionManager.java | 21 +- .../alert/vo/config/IotAlertConfigRespVO.java | 9 + .../vo/config/IotAlertConfigSaveReqVO.java | 8 + .../dataobject/alert/IotAlertConfigDO.java | 18 ++ .../alert/IotAlertConfigServiceImpl.java | 30 ++- .../IotDevicePropertyServiceImpl.java | 48 ++-- .../IotAlertTriggerSceneRuleAction.java | 37 ++- .../matcher/IotSceneRuleMatcherHelper.java | 26 ++ .../IotCurrentTimeConditionMatcher.java | 11 +- .../IotDevicePropertyConditionMatcher.java | 10 +- .../IotDeviceStateConditionMatcher.java | 7 +- .../IotDeviceEventPostTriggerMatcher.java | 8 +- .../IotDevicePropertyPostTriggerMatcher.java | 8 +- .../IotDeviceServiceInvokeTriggerMatcher.java | 12 +- .../IotDeviceStateUpdateTriggerMatcher.java | 13 +- .../thingmodel/IotThingModelService.java | 11 +- .../thingmodel/IotThingModelServiceImpl.java | 252 ++++++++++++++++++ .../IotDevicePropertyServiceImplTest.java | 97 ++++--- ...ceneRuleTimerConditionIntegrationTest.java | 20 ++ .../IotAlertTriggerSceneRuleActionTest.java | 9 +- .../IotThingModelServiceImplTest.java | 201 ++++++++++++++ .../CombinationRecordServiceImpl.java | 29 +- .../src/test/resources/sql/clean.sql | 3 +- .../src/test/resources/sql/create_tables.sql | 30 +++ .../convert/order/TradeOrderConvert.java | 5 +- .../brokerage/BrokerageRecordServiceImpl.java | 35 +-- .../src/test/resources/sql/create_tables.sql | 6 +- .../member/enums/ErrorCodeConstants.java | 1 + .../admin/user/vo/MemberUserBaseVO.java | 7 + .../admin/user/vo/MemberUserPageReqVO.java | 3 + .../app/user/vo/AppMemberUserInfoRespVO.java | 3 + .../app/user/vo/AppMemberUserUpdateReqVO.java | 9 +- .../dal/dataobject/user/MemberUserDO.java | 4 + .../dal/mysql/user/MemberUserMapper.java | 5 + .../service/user/MemberUserServiceImpl.java | 36 ++- .../src/test/resources/sql/create_tables.sql | 1 + .../andon/MesProAndonConfigController.java | 15 +- .../vo/config/MesProAndonConfigRespVO.java | 3 + .../MesQcDefectRecordController.java | 9 + .../MesQcDefectRecordService.java | 8 + .../MesQcDefectRecordServiceImpl.java | 8 +- .../impl/weixin/AbstractWxPayClient.java | 3 + .../admin/mail/MailTemplateController.java | 10 +- .../vo/template/MailTemplateSimpleRespVO.java | 3 + .../notify/NotifyTemplateController.java | 10 + .../template/NotifyTemplateSimpleRespVO.java | 19 ++ .../admin/sms/SmsTemplateController.java | 10 + .../vo/template/SmsTemplateSimpleRespVO.java | 19 ++ .../dal/mysql/mail/MailTemplateMapper.java | 6 + .../mysql/notify/NotifyTemplateMapper.java | 6 + .../dal/mysql/sms/SmsTemplateMapper.java | 6 + .../service/mail/MailTemplateService.java | 8 + .../service/mail/MailTemplateServiceImpl.java | 15 +- .../service/notify/NotifyTemplateService.java | 8 + .../notify/NotifyTemplateServiceImpl.java | 5 + .../service/sms/SmsTemplateService.java | 8 + .../service/sms/SmsTemplateServiceImpl.java | 5 + .../mail/MailTemplateServiceImplTest.java | 17 ++ .../notify/NotifyTemplateServiceImplTest.java | 18 ++ .../sms/SmsTemplateServiceImplTest.java | 16 ++ 79 files changed, 1787 insertions(+), 190 deletions(-) create mode 100644 yudao-module-bpm/yudao-module-bpm-server/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtilsTest.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FilePathUtils.java create mode 100644 yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImplTest.java create mode 100644 yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notify/vo/template/NotifyTemplateSimpleRespVO.java create mode 100644 yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateSimpleRespVO.java diff --git a/sql/tools/convertor.py b/sql/tools/convertor.py index 3a8b9f37f..3d61e0ef6 100644 --- a/sql/tools/convertor.py +++ b/sql/tools/convertor.py @@ -77,6 +77,9 @@ def load_and_clean(sql_file: str) -> str: class Convertor(ABC): + # 不同数据库的关键字不完全一致;子类按需声明需要转义的列名。 + reserved_column_names = set() + def __init__(self, src: str, db_type) -> None: self.src = src self.db_type = db_type @@ -179,6 +182,31 @@ class Convertor(ABC): """ return "" + def escape_column_name(self, name: str) -> str: + """转义目标库保留字列名,例如 Oracle / Kingbase 的 level。""" + + column_name = name.lower() + if column_name in self.reserved_column_names: + return f'"{column_name}"' + return column_name + + def escape_insert_columns(self, insert_script: str) -> str: + """INSERT 显式列清单需要和 CREATE / COMMENT 使用同一套列名转义。""" + + match = re.match( + r"(INSERT INTO\s+\S+\s*\()([^)]+)(\)\s+VALUES\s+[\s\S]*)", + insert_script, + flags=re.IGNORECASE, + ) + if not match: + return insert_script + + columns = [ + self.escape_column_name(column.strip()) + for column in match.group(2).split(",") + ] + return f"{match.group(1)}{', '.join(columns)}{match.group(3)}" + @staticmethod def inserts(table_name: str, script_content: str) -> Generator: PREFIX = f"INSERT INTO `{table_name}`" @@ -204,18 +232,55 @@ class Convertor(ABC): Generator[str]: create index 语句 """ - def generate_columns(columns): - keys = [ - f"{col['name'].lower()}{' ' + col['order'].lower() if col['order'] != 'ASC' else ''}" - for col in columns[0] - ] - return ", ".join(keys) - - for no, index in enumerate(ddl["index"], 1): - columns = generate_columns(index["columns"]) + for no, index in enumerate(ddl.get("index", []), 1): + columns = ", ".join(Convertor.index_columns(index.get("columns", []))) + if not columns: + continue table_name = ddl["table_name"].lower() yield f"CREATE INDEX idx_{table_name}_{no:02d} ON {table_name} ({columns})" + @staticmethod + def index_columns(columns) -> list: + """兼容 simple-ddl-parser 不同版本的索引列结构。""" + + keys = [] + + def append(name, order="ASC"): + if not name: + return + column_name = str(name).strip("`").lower() + column_order = str(order or "ASC").upper() + if column_order == "DESC": + keys.append(f"{column_name} desc") + else: + keys.append(column_name) + + def visit(value): + # 普通索引常见结构:[[{'name': 'user_id', 'order': 'ASC'}]] + if isinstance(value, (list, tuple)): + for item in value: + visit(item) + return + if isinstance(value, dict): + name = value.get("name") + if isinstance(name, (dict, list, tuple)): + visit(name) + return + append(name, value.get("order", "ASC")) + return + # 唯一索引在部分版本中会被解析成 ['mobile', 'ASC', 'tenant_id', 'ASC']。 + if isinstance(value, str): + token = value.strip("`") + order = token.upper() + if order in ("ASC", "DESC"): + if order == "DESC" and keys and not keys[-1].endswith(" desc"): + keys[-1] = f"{keys[-1]} desc" + return + append(token) + + visit(columns) + return keys + @staticmethod def unique_index(ddl: Dict) -> Generator: if "constraints" in ddl and "uniques" in ddl["constraints"]: @@ -223,7 +288,9 @@ class Convertor(ABC): for uk in uk_list: table_name = ddl["table_name"] uk_name = uk["constraint_name"] - uk_columns = uk["columns"] + uk_columns = Convertor.index_columns(uk["columns"]) + if not uk_columns: + continue yield table_name, uk_name, uk_columns @staticmethod @@ -381,7 +448,7 @@ class PostgreSQLConvertor(Convertor): ) nullable = "NULL" if col["nullable"] else "NOT NULL" default = f"DEFAULT {col['default']}" if col["default"] is not None else "" - return f"{name} {full_type} {nullable} {default}" + return f"{self.escape_column_name(name)} {full_type} {nullable} {default}" table_name = ddl["table_name"].lower() columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]] @@ -406,7 +473,7 @@ CREATE TABLE {table_name} ( for column in table_ddl["columns"]: table_comment = column["comment"] script += ( - f"COMMENT ON COLUMN {table_ddl['table_name']}.{column['name']} IS '{table_comment}';" + f"COMMENT ON COLUMN {table_ddl['table_name']}.{self.escape_column_name(column['name'])} IS '{table_comment}';" + "\n" ) @@ -435,6 +502,7 @@ CREATE TABLE {table_name} ( """生成 insert 语句,以及根据最后的 insert id+1 生成 Sequence""" inserts = list(Convertor.inserts(table_name, self.content)) + inserts = [self.escape_insert_columns(s) for s in inserts] # 转换 MySQL 字符串转义为 PostgreSQL 格式:\\ -> \,\' -> '' inserts = [re.sub(r"\\\\|\\'", lambda m: "\\" if m.group() == "\\\\" else "''", s) for s in inserts] ## 生成 insert 脚本 @@ -482,6 +550,8 @@ INSERT INTO dual VALUES (1); class OracleConvertor(Convertor): + reserved_column_names = {"level", "size"} + def __init__(self, src): super().__init__(src, "Oracle") @@ -526,10 +596,8 @@ class OracleConvertor(Convertor): # Oracle的 INSERT '' 不能通过NOT NULL校验,因此对文字类型字段覆写为 NULL nullable = "NULL" if type in ("varchar", "text", "longtext") else nullable default = f"DEFAULT {col['default']}" if col["default"] is not None else "" - # Oracle 中 size 不能作为字段名 - field_name = '"size"' if name == "size" else name # Oracle DEFAULT 定义在 NULLABLE 之前 - return f"{field_name} {full_type} {default} {nullable}" + return f"{self.escape_column_name(name)} {full_type} {default} {nullable}" table_name = ddl["table_name"].lower() columns = [f"{generate_column(col).strip()}" for col in ddl["columns"]] @@ -554,7 +622,7 @@ CREATE TABLE {table_name} ( for column in table_ddl["columns"]: table_comment = column["comment"] script += ( - f"COMMENT ON COLUMN {table_ddl['table_name']}.{column['name']} IS '{table_comment}';" + f"COMMENT ON COLUMN {table_ddl['table_name']}.{self.escape_column_name(column['name'])} IS '{table_comment}';" + "\n" ) @@ -586,6 +654,7 @@ CREATE TABLE {table_name} ( """拷贝 INSERT 语句""" inserts = [] for insert_script in Convertor.inserts(table_name, self.content): + insert_script = self.escape_insert_columns(insert_script) # 对日期数据添加 TO_DATE 转换 insert_script = re.sub( r"('\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}')", @@ -907,6 +976,8 @@ SET IDENTITY_INSERT {table_name.lower()} OFF; class KingbaseConvertor(PostgreSQLConvertor): + reserved_column_names = {"level"} + def __init__(self, src): super().__init__(src) self.db_type = "Kingbase" @@ -925,7 +996,7 @@ class KingbaseConvertor(PostgreSQLConvertor): if full_type == "text": nullable = "NULL" default = f"DEFAULT {col['default']}" if col["default"] is not None else "" - return f"{name} {full_type} {nullable} {default}" + return f"{self.escape_column_name(name)} {full_type} {nullable} {default}" table_name = ddl["table_name"].lower() columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]] @@ -945,6 +1016,8 @@ CREATE TABLE {table_name} ( class OpengaussConvertor(KingbaseConvertor): + reserved_column_names = set() + def __init__(self, src): super().__init__(src) self.db_type = "OpenGauss" diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java index ec136800f..ac42e3ec8 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java @@ -235,6 +235,14 @@ public class JsonUtils { } } + public static String getText(JsonNode node, String fieldName) { + if (node == null) { + return null; + } + JsonNode value = node.get(fieldName); + return value != null && !value.isNull() ? value.asText() : null; + } + public static boolean isJson(String text) { return JSONUtil.isTypeJSON(text); } diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/QueryWrapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/QueryWrapperX.java index 419454777..bff012249 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/QueryWrapperX.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/QueryWrapperX.java @@ -102,13 +102,13 @@ public class QueryWrapperX extends QueryWrapper { } public QueryWrapperX betweenIfPresent(String column, Object[] values) { - if (values!= null && values.length != 0 && values[0] != null && values[1] != null) { + if (values != null && values.length != 0 && values[0] != null && values[1] != null) { return (QueryWrapperX) super.between(column, values[0], values[1]); } - if (values!= null && values.length != 0 && values[0] != null) { + if (values != null && values.length != 0 && values[0] != null) { return (QueryWrapperX) ge(column, values[0]); } - if (values!= null && values.length != 0 && values[1] != null) { + if (values != null && values.length != 0 && values[1] != null) { return (QueryWrapperX) le(column, values[1]); } return this; diff --git a/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java b/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java index 4c91611c3..fc21f7054 100644 --- a/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java +++ b/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java @@ -12,6 +12,7 @@ import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormFi import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; +import com.fasterxml.jackson.databind.JsonNode; import lombok.SneakyThrows; import org.flowable.common.engine.api.delegate.Expression; import org.flowable.common.engine.api.variable.VariableContainer; @@ -27,6 +28,7 @@ import org.flowable.engine.runtime.ProcessInstance; import org.flowable.task.api.TaskInfo; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -245,10 +247,10 @@ public class FlowableUtils { } // 解析表单配置 - Map formFieldsMap = new HashMap<>(); + Map formFieldsMap = new LinkedHashMap<>(); processDefinitionInfo.getFormFields().forEach(formFieldStr -> { - BpmFormFieldVO formField = JsonUtils.parseObject(formFieldStr, BpmFormFieldVO.class); - parseFormField(formField, formFieldsMap); + JsonNode formFieldNode = JsonUtils.parseObject(formFieldStr, JsonNode.class); + parseFormField(formFieldNode, formFieldsMap); }); // 情况一:当自定义了摘要 @@ -275,18 +277,32 @@ public class FlowableUtils { /** * 递归解析表单字段 */ - private static void parseFormField(BpmFormFieldVO formField, Map formFieldsMap) { - if (formField == null) { + private static void parseFormField(JsonNode formFieldNode, Map formFieldsMap) { + if (formFieldNode == null || !formFieldNode.isObject()) { return; } - // 如果存在 children -> 说明是布局组件 - if (formField.getChildren() != null && !formField.getChildren().isEmpty()) { - for (BpmFormFieldVO child : formField.getChildren()) { + + // 如果 children 里存在对象节点,说明是布局组件;字符串节点是分割线、标签、文字等展示组件内容,直接跳过。 + JsonNode children = formFieldNode.get("children"); + if (children != null && children.isArray() && children.size() > 0) { + boolean hasObjectChild = false; + for (JsonNode child : children) { + if (!child.isObject()) { + continue; + } + hasObjectChild = true; parseFormField(child, formFieldsMap); } - return; + if (hasObjectChild) { + return; + } } + // 真实字段才加入 map + BpmFormFieldVO formField = new BpmFormFieldVO() + .setType(JsonUtils.getText(formFieldNode, "type")) + .setField(JsonUtils.getText(formFieldNode, "field")) + .setTitle(JsonUtils.getText(formFieldNode, "title")); if (StrUtil.isNotBlank(formField.getField())) { formFieldsMap.put(formField.getField(), formField); } diff --git a/yudao-module-bpm/yudao-module-bpm-server/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvokerTest.java b/yudao-module-bpm/yudao-module-bpm-server/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvokerTest.java index d1db10d6f..a726377dd 100644 --- a/yudao-module-bpm/yudao-module-bpm-server/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvokerTest.java +++ b/yudao-module-bpm/yudao-module-bpm-server/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvokerTest.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate; +import cn.hutool.core.collection.ListUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; @@ -64,7 +65,7 @@ public class BpmTaskCandidateInvokerTest extends BaseMockitoUnitTest { public void setUp() { userStrategy = new BpmTaskCandidateUserStrategy(); // 创建 strategy 实例 when(emptyStrategy.getStrategy()).thenReturn(BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY); - strategyList = List.of(userStrategy, emptyStrategy); // 创建 strategyList + strategyList = ListUtil.of(userStrategy, emptyStrategy); // 创建 strategyList taskCandidateInvoker = new BpmTaskCandidateInvoker(strategyList, adminUserApi); } @@ -223,7 +224,7 @@ public class BpmTaskCandidateInvokerTest extends BaseMockitoUnitTest { when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap); // mock 方法(empty) when(emptyStrategy.calculateUsersByActivity(same(bpmnModel), eq(activityId), - eq(param), same(startUserId), same(processDefinitionId), same(processVariables))) + eq(param), same(startUserId), same(processDefinitionId), same(processVariables))) .thenReturn(Sets.newSet(2L)); // 调用 diff --git a/yudao-module-bpm/yudao-module-bpm-server/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategyTest.java b/yudao-module-bpm/yudao-module-bpm-server/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategyTest.java index f63ccc332..07ae14159 100644 --- a/yudao-module-bpm/yudao-module-bpm-server/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategyTest.java +++ b/yudao-module-bpm/yudao-module-bpm-server/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategyTest.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; +import cn.hutool.core.collection.ListUtil; import cn.hutool.core.map.MapUtil; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; @@ -12,7 +13,6 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Set; @@ -41,7 +41,7 @@ public class BpmTaskCandidateStartUserSelectStrategyTest extends BaseMockitoUnit // mock 方法(FlowableUtils) Map processVariables = new HashMap<>(); processVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, - MapUtil.of("activity_001", List.of(1L, 2L))); + MapUtil.of("activity_001", ListUtil.of(1L, 2L))); when(processInstance.getProcessVariables()).thenReturn(processVariables); // 调用 @@ -56,7 +56,7 @@ public class BpmTaskCandidateStartUserSelectStrategyTest extends BaseMockitoUnit String activityId = "activity_001"; Map processVariables = new HashMap<>(); processVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, - MapUtil.of("activity_001", List.of(1L, 2L))); + MapUtil.of("activity_001", ListUtil.of(1L, 2L))); // 调用 Set userIds = strategy.calculateUsersByActivity(null, activityId, null, diff --git a/yudao-module-bpm/yudao-module-bpm-server/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtilsTest.java b/yudao-module-bpm/yudao-module-bpm-server/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtilsTest.java new file mode 100644 index 000000000..e1bebe1b4 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-server/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtilsTest.java @@ -0,0 +1,167 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.util; + +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * {@link FlowableUtils} 的单元测试。 + * + * @author 芋道源码 + */ +class FlowableUtilsTest { + + @Test + public void testGetSummary_customSummary_parseDbFormFields() { + // 准备参数:模拟 DB 中 form_fields 字段,列表里每个元素都是一个 form-create 字段 JSON。 + BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionInfo(dbFormFields(), + summarySetting(true, "reason", "days", "notExists", "startTime")); + Map processVariables = processVariables(); + + // 调用 + List> summary = FlowableUtils.getSummary(processDefinitionInfo, processVariables); + + // 断言 + assertEquals(Arrays.asList( + new KeyValue<>("请假原因", "事假"), + new KeyValue<>("请假天数", "3"), + new KeyValue<>("开始时间", "2026-05-31 09:00:00")), + summary); + } + + @Test + public void testGetSummary_defaultSummary_parseFirstThreeFieldsByFormOrder() { + // 准备参数:未开启自定义摘要时,默认取表单配置顺序里的前三个真实字段。 + BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionInfo(dbFormFields(), null); + Map processVariables = processVariables(); + + // 调用 + List> summary = FlowableUtils.getSummary(processDefinitionInfo, processVariables); + + // 断言 + assertEquals(Arrays.asList( + new KeyValue<>("请假原因", "事假"), + new KeyValue<>("开始时间", "2026-05-31 09:00:00"), + new KeyValue<>("请假天数", "3")), + summary); + } + + @Test + public void testGetSummary_summaryDisabled_useDefaultSummary() { + // 准备参数:摘要设置存在但未启用时,仍走默认摘要逻辑。 + BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionInfo(dbFormFields(), + summarySetting(false, "remark")); + Map processVariables = processVariables(); + + // 调用 + List> summary = FlowableUtils.getSummary(processDefinitionInfo, processVariables); + + // 断言 + assertEquals(Arrays.asList( + new KeyValue<>("请假原因", "事假"), + new KeyValue<>("开始时间", "2026-05-31 09:00:00"), + new KeyValue<>("请假天数", "3")), + summary); + } + + @Test + public void testGetSummary_displayComponentsOnly_returnEmpty() { + // 准备参数:分割线、标签、文字等展示组件的 children 是字符串数组,不是表单字段对象。 + BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionInfo(Arrays.asList( + DIVIDER_FIELD, + TEXT_FIELD, + TAG_FIELD), null); + + // 调用 + List> summary = FlowableUtils.getSummary(processDefinitionInfo, + Collections.emptyMap()); + + // 断言 + assertEquals(Collections.emptyList(), summary); + } + + @Test + public void testGetSummary_notNormalForm_returnNull() { + // 准备参数 + BpmProcessDefinitionInfoDO processDefinitionInfo = BpmProcessDefinitionInfoDO.builder() + .formType(BpmModelFormTypeEnum.CUSTOM.getType()) + .build(); + + // 调用 & 断言 + assertNull(FlowableUtils.getSummary(null, Collections.emptyMap())); + assertNull(FlowableUtils.getSummary(processDefinitionInfo, Collections.emptyMap())); + } + + private static BpmProcessDefinitionInfoDO processDefinitionInfo(List formFields, + BpmModelMetaInfoVO.SummarySetting summarySetting) { + return BpmProcessDefinitionInfoDO.builder() + .formType(BpmModelFormTypeEnum.NORMAL.getType()) + .formFields(formFields) + .summarySetting(summarySetting) + .build(); + } + + private static BpmModelMetaInfoVO.SummarySetting summarySetting(Boolean enable, String... fields) { + BpmModelMetaInfoVO.SummarySetting summarySetting = new BpmModelMetaInfoVO.SummarySetting(); + summarySetting.setEnable(enable); + summarySetting.setSummary(Arrays.asList(fields)); + return summarySetting; + } + + private static List dbFormFields() { + return Arrays.asList( + DIVIDER_FIELD, + "{\"type\":\"input\",\"field\":\"reason\",\"title\":\"请假原因\",\"value\":\"\"," + + "\"props\":{\"type\":\"textarea\",\"placeholder\":\"请输入请假原因\"}," + + "\"$required\":\"请输入请假原因\",\"_fc_id\":\"id_F1\",\"_fc_drag_tag\":\"input\"," + + "\"hidden\":false,\"display\":true}", + TEXT_FIELD, + "{\"type\":\"elRow\",\"title\":\"栅格布局\",\"children\":[" + + "{\"type\":\"elCol\",\"props\":{\"span\":12},\"children\":[" + + "{\"type\":\"DatePicker\",\"field\":\"startTime\",\"title\":\"开始时间\"," + + "\"props\":{\"type\":\"datetime\",\"placeholder\":\"请选择开始时间\"}," + + "\"_fc_id\":\"id_F2\",\"_fc_drag_tag\":\"datePicker\"}]}," + + "\"字段说明\"," + + "{\"type\":\"elCol\",\"props\":{\"span\":12},\"children\":[" + + "{\"type\":\"inputNumber\",\"field\":\"days\",\"title\":\"请假天数\"," + + "\"props\":{\"min\":0,\"precision\":1},\"_fc_id\":\"id_F3\"," + + "\"_fc_drag_tag\":\"inputNumber\"}]}],\"_fc_id\":\"id_LAYOUT\"," + + "\"_fc_drag_tag\":\"row\"}", + TAG_FIELD, + "{\"type\":\"input\",\"field\":\"remark\",\"title\":\"备注\",\"value\":\"\"," + + "\"props\":{\"placeholder\":\"请输入备注\"},\"_fc_id\":\"id_F4\"," + + "\"_fc_drag_tag\":\"input\",\"hidden\":false,\"display\":true}"); + } + + private static Map processVariables() { + Map processVariables = new HashMap<>(); + processVariables.put("reason", "事假"); + processVariables.put("startTime", "2026-05-31 09:00:00"); + processVariables.put("days", 3); + processVariables.put("remark", "下午到家"); + return processVariables; + } + + private static final String DIVIDER_FIELD = "{\"type\":\"elDivider\",\"children\":[\"基础信息\"]," + + "\"props\":{\"contentPosition\":\"left\"},\"_fc_id\":\"id_DIVIDER\"," + + "\"_fc_drag_tag\":\"elDivider\"}"; + + private static final String TEXT_FIELD = "{\"type\":\"div\",\"children\":[\"请按实际情况填写\"]," + + "\"props\":{\"style\":{\"color\":\"#909399\"}},\"_fc_id\":\"id_TEXT\"," + + "\"_fc_drag_tag\":\"text\"}"; + + private static final String TAG_FIELD = "{\"type\":\"elTag\",\"children\":[\"重要\"]," + + "\"props\":{\"type\":\"warning\"},\"_fc_id\":\"id_TAG\",\"_fc_drag_tag\":\"elTag\"}"; + +} diff --git a/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java index 2233f353e..7cf23c827 100644 --- a/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java +++ b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java @@ -33,6 +33,7 @@ public interface ErrorCodeConstants { ErrorCode FILE_PATH_EXISTS = new ErrorCode(1_001_003_000, "文件路径已存在"); ErrorCode FILE_NOT_EXISTS = new ErrorCode(1_001_003_001, "文件不存在"); ErrorCode FILE_IS_EMPTY = new ErrorCode(1_001_003_002, "文件为空"); + ErrorCode FILE_PATH_INVALID = new ErrorCode(1_001_003_003, "文件路径不正确"); // ========== 代码生成器 1-001-004-000 ========== ErrorCode CODEGEN_TABLE_EXISTS = new ErrorCode(1_001_004_002, "表定义已经存在"); diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java index 06dff7c08..dbdbd9cd6 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file; -import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.infra.framework.file.core.utils.FilePathUtils; import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.AssertTrue; @@ -26,10 +26,7 @@ public class FileUploadReqVO { } public static boolean isDirectoryValid(String directory) { - // 1. 不能包含 .. 防止目录穿越 - // 2. 不能以 / 或 \ 开头,防止上传到根目录 - return !StrUtil.contains(directory, "..") - && !StrUtil.startWithAny(directory, "/", "\\"); + return FilePathUtils.isDirectoryValid(directory); } } diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java index c3e14a803..d3d60b382 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java @@ -36,7 +36,7 @@ public class AppFileController { @Parameter(name = "file", description = "文件附件", required = true, schema = @Schema(type = "string", format = "binary")) @PermitAll - public CommonResult uploadFile(AppFileUploadReqVO uploadReqVO) throws Exception { + public CommonResult uploadFile(@Valid AppFileUploadReqVO uploadReqVO) throws Exception { MultipartFile file = uploadReqVO.getFile(); byte[] content = IoUtil.readBytes(file.getInputStream()); return success(fileService.createFile(content, file.getOriginalFilename(), diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClient.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClient.java index 6e5c0229b..9c3af16cc 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClient.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClient.java @@ -3,8 +3,13 @@ package cn.iocoder.yudao.module.infra.framework.file.core.client.local; import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.IORuntimeException; import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient; +import cn.iocoder.yudao.module.infra.framework.file.core.utils.FilePathUtils; -import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_PATH_INVALID; /** * 本地文件客户端 @@ -50,7 +55,13 @@ public class LocalFileClient extends AbstractFileClient { } private String getFilePath(String path) { - return config.getBasePath() + File.separator + path; + FilePathUtils.validatePath(path); + Path basePath = Paths.get(config.getBasePath()).toAbsolutePath().normalize(); + Path filePath = basePath.resolve(path).normalize(); + if (!filePath.startsWith(basePath)) { + throw exception(FILE_PATH_INVALID); + } + return filePath.toString(); } } diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FilePathUtils.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FilePathUtils.java new file mode 100644 index 000000000..e61bfe78f --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FilePathUtils.java @@ -0,0 +1,106 @@ +package cn.iocoder.yudao.module.infra.framework.file.core.utils; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; + +import java.nio.file.InvalidPathException; +import java.nio.file.Paths; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_PATH_INVALID; + +/** + * 文件路径工具类 + * + * @author 芋道源码 + */ +public class FilePathUtils { + + private FilePathUtils() { + } + + /** + * 校验文件名是否合法,禁止携带目录路径。 + * + * @param name 文件名 + * @return 文件名 + */ + public static String validateFileName(String name) { + if (StrUtil.isEmpty(name)) { + return name; + } + if (!isPathValid(name) || StrUtil.contains(name, StrUtil.SLASH) || !StrUtil.equals(name, FileUtil.getName(name))) { + throw exception(FILE_PATH_INVALID); + } + return name; + } + + /** + * 校验文件目录是否合法。 + * + * @param directory 文件目录,允许为空 + * @return 是否合法 + */ + public static boolean isDirectoryValid(String directory) { + return StrUtil.isEmpty(directory) || isPathValid(directory); + } + + /** + * 校验文件目录是否合法,不合法时抛出业务异常。 + * + * @param directory 文件目录,允许为空 + */ + public static void validateDirectory(String directory) { + if (!isDirectoryValid(directory)) { + throw exception(FILE_PATH_INVALID); + } + } + + /** + * 校验文件相对路径是否合法,不合法时抛出业务异常。 + * + * @param path 文件相对路径 + */ + public static void validatePath(String path) { + if (StrUtil.isEmpty(path) || !isPathValid(path)) { + throw exception(FILE_PATH_INVALID); + } + } + + /** + * 校验路径是否为安全的相对路径,禁止绝对路径、Windows 盘符、反斜杠、空路径段和目录穿越。 + * + * @param path 路径 + * @return 是否合法 + */ + private static boolean isPathValid(String path) { + // 不能以 / 或 \ 开头,避免传入绝对路径 + if (StrUtil.startWithAny(path, StrUtil.SLASH, "\\")) { + return false; + } + // 不能包含反斜杠或空字符,避免绕过不同系统的路径解析 + if (StrUtil.contains(path, "\\") || path.indexOf('\0') >= 0) { + return false; + } + // 不能是 Windows 盘符路径,例如 C:/test.jpg + if (path.length() >= 2 && Character.isLetter(path.charAt(0)) && path.charAt(1) == ':') { + return false; + } + try { + // 使用 JDK Path 再兜底判断一次绝对路径 + if (Paths.get(path).isAbsolute()) { + return false; + } + } catch (InvalidPathException ex) { + return false; + } + // 不能包含空路径段、当前目录或上级目录,避免目录穿越 + for (String segment : path.split(StrUtil.SLASH, -1)) { + if (StrUtil.isEmpty(segment) || ".".equals(segment) || "..".equals(segment)) { + return false; + } + } + return true; + } + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java index 639c68910..a93c07d7e 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java @@ -15,6 +15,7 @@ import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresigned import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper; import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient; +import cn.iocoder.yudao.module.infra.framework.file.core.utils.FilePathUtils; import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils; import com.google.common.annotations.VisibleForTesting; import jakarta.annotation.Resource; @@ -70,11 +71,14 @@ public class FileServiceImpl implements FileService { @Override @SneakyThrows public String createFile(byte[] content, String name, String directory, String type) { - // 1.1 处理 type 为空的情况 + // 1.1 处理 name 的合法性,禁止携带目录路径 + name = FilePathUtils.validateFileName(name); + + // 1.2.1 处理 type 为空的情况 if (StrUtil.isEmpty(type)) { type = FileTypeUtils.getMineType(content, name); } - // 1.2 处理 name 为空的情况 + // 1.2.2 处理 name 为空的情况 if (StrUtil.isEmpty(name)) { name = DigestUtil.sha256Hex(content); } @@ -102,7 +106,11 @@ public class FileServiceImpl implements FileService { @VisibleForTesting String generateUploadPath(String name, String directory) { - // 1. 生成前缀、后缀 + // 1.1 处理 name 和 directory 的合法性 + name = FilePathUtils.validateFileName(name); + FilePathUtils.validatePath(name); + FilePathUtils.validateDirectory(directory); + // 1.2 生成前缀、后缀 String prefix = null; if (PATH_PREFIX_DATE_ENABLE) { prefix = LocalDateTimeUtil.format(LocalDateTimeUtil.now(), PURE_DATE_PATTERN); @@ -159,7 +167,13 @@ public class FileServiceImpl implements FileService { @Override public Long createFile(FileCreateReqVO createReqVO) { + // 1.1 校验参数的合法性 + FilePathUtils.validatePath(createReqVO.getPath()); + createReqVO.setName(FilePathUtils.validateFileName(createReqVO.getName())); + // 1.2 处理 URL 的合法性,移除 URL 中的查询参数(例如签名参数),保证 URL 的唯一性 createReqVO.setUrl(HttpUtils.removeUrlQuery(createReqVO.getUrl())); // 目的:移除私有桶情况下,URL 的签名参数 + + // 2. 保存到数据库 FileDO file = BeanUtils.toBean(createReqVO, FileDO.class); fileMapper.insert(file); return file.getId(); @@ -172,15 +186,17 @@ public class FileServiceImpl implements FileService { @Override public void deleteFile(Long id) throws Exception { - // 校验存在 + // 1.1 校验存在 FileDO file = validateFileExists(id); + // 1.2 校验路径合法性,避免误删文件存储器中的其他文件 + FilePathUtils.validatePath(file.getPath()); - // 从文件存储器中删除 + // 2.1 从文件存储器中删除 FileClient client = fileConfigService.getFileClient(file.getConfigId()); Assert.notNull(client, "客户端({}) 不能为空", file.getConfigId()); client.delete(file.getPath()); - // 删除记录 + // 2.2 删除记录 fileMapper.deleteById(id); } @@ -190,6 +206,7 @@ public class FileServiceImpl implements FileService { // 删除文件 List files = fileMapper.selectByIds(ids); for (FileDO file : files) { + FilePathUtils.validatePath(file.getPath()); // 获取客户端 FileClient client = fileConfigService.getFileClient(file.getConfigId()); Assert.notNull(client, "客户端({}) 不能为空", file.getPath()); @@ -211,8 +228,13 @@ public class FileServiceImpl implements FileService { @Override public byte[] getFileContent(Long configId, String path) throws Exception { + // 1. 校验路径合法性 + FilePathUtils.validatePath(path); + + // 2.1 获取客户端 FileClient client = fileConfigService.getFileClient(configId); Assert.notNull(client, "客户端({}) 不能为空", configId); + // 2.2 获取文件内容 return client.getContent(path); } diff --git a/yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/local/LocalFileClientTest.java b/yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/local/LocalFileClientTest.java index 6bc8c7bfe..1164afdf4 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/local/LocalFileClientTest.java +++ b/yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/local/LocalFileClientTest.java @@ -1,16 +1,57 @@ package cn.iocoder.yudao.module.infra.framework.file.core.local; +import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.resource.ResourceUtil; import cn.hutool.core.util.IdUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.module.infra.framework.file.core.client.local.LocalFileClient; import cn.iocoder.yudao.module.infra.framework.file.core.client.local.LocalFileClientConfig; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.nio.charset.StandardCharsets; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.*; public class LocalFileClientTest { + @TempDir + public File tempDir; + + @Test + public void testUpload_success() { + // 准备参数 + LocalFileClient client = createClient(); + byte[] content = "test".getBytes(StandardCharsets.UTF_8); + String path = "avatar/test.txt"; + + // 调用 + String url = client.upload(content, path, "text/plain"); + + // 断言 + assertEquals("http://127.0.0.1:48080/admin-api/infra/file/0/get/avatar/test.txt", url); + assertArrayEquals(content, FileUtil.readBytes(new File(tempDir, path))); + assertArrayEquals(content, client.getContent(path)); + + // 删除 + client.delete(path); + assertFalse(FileUtil.exist(new File(tempDir, path))); + } + + @Test + public void testUpload_pathInvalid() { + // 准备参数 + LocalFileClient client = createClient(); + byte[] content = "test".getBytes(StandardCharsets.UTF_8); + + // 调用,并断言异常 + assertThrows(ServiceException.class, () -> client.upload(content, "../test.txt", "text/plain")); + assertFalse(FileUtil.exist(new File(tempDir.getParentFile(), "test.txt"))); + } + @Test @Disabled public void test() { @@ -42,4 +83,13 @@ public class LocalFileClientTest { System.out.println(); } + private LocalFileClient createClient() { + LocalFileClientConfig config = new LocalFileClientConfig(); + config.setDomain("http://127.0.0.1:48080"); + config.setBasePath(tempDir.getAbsolutePath()); + LocalFileClient client = new LocalFileClient(0L, config); + client.init(); + return client; + } + } diff --git a/yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImplTest.java b/yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImplTest.java index 8cd8fe789..60c0b0c85 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImplTest.java +++ b/yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImplTest.java @@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; import cn.iocoder.yudao.framework.test.core.util.AssertUtils; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper; @@ -22,6 +23,7 @@ import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.bui import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_PATH_INVALID; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.*; @@ -172,6 +174,16 @@ public class FileServiceImplTest extends BaseDbUnitTest { assertServiceException(() -> fileService.deleteFile(id), FILE_NOT_EXISTS); } + @Test + public void testDeleteFile_pathInvalid() { + // mock 数据 + FileDO dbFile = randomPojo(FileDO.class, o -> o.setConfigId(10L).setPath("../tudou.jpg")); + fileMapper.insert(dbFile); + + // 调用,并断言异常 + assertServiceException(() -> fileService.deleteFile(dbFile.getId()), FILE_PATH_INVALID); + } + @Test public void testGetFileContent() throws Exception { // 准备参数 @@ -189,6 +201,59 @@ public class FileServiceImplTest extends BaseDbUnitTest { assertSame(result, content); } + @Test + public void testGetFileContent_pathInvalid() { + // 准备参数 + Long configId = 10L; + String path = "../tudou.jpg"; + + // 调用,并断言异常 + assertServiceException(() -> fileService.getFileContent(configId, path), FILE_PATH_INVALID); + } + + @Test + public void testCreateFileByPresignedPath_success() { + // 准备参数 + FileCreateReqVO reqVO = randomPojo(FileCreateReqVO.class, o -> { + o.setPath("avatar/test.jpg"); + o.setName("test.jpg"); + o.setUrl("https://www.iocoder.cn/test.jpg?token=123"); + }); + + // 调用 + Long fileId = fileService.createFile(reqVO); + + // 断言 + FileDO file = fileMapper.selectById(fileId); + assertEquals("avatar/test.jpg", file.getPath()); + assertEquals("test.jpg", file.getName()); + assertEquals("https://www.iocoder.cn/test.jpg", file.getUrl()); + } + + @Test + public void testCreateFileByPresignedPath_nameInvalid() { + // 准备参数 + FileCreateReqVO reqVO = randomPojo(FileCreateReqVO.class, o -> { + o.setPath("avatar/test.jpg"); + o.setName("../test.jpg"); + }); + + // 调用,并断言异常 + assertServiceException(() -> fileService.createFile(reqVO), FILE_PATH_INVALID); + } + + @Test + public void testCreateFileByPresignedPath_pathInvalid() { + // 准备参数 + FileCreateReqVO reqVO = randomPojo(FileCreateReqVO.class, o -> { + o.setPath("../test.jpg"); + o.setName("test.jpg"); + }); + + // 调用,并断言异常 + assertServiceException(() -> fileService.createFile(reqVO), FILE_PATH_INVALID); + } + @Test public void testGenerateUploadPath_AllEnabled() { // 准备参数 @@ -342,6 +407,28 @@ public class FileServiceImplTest extends BaseDbUnitTest { assertTrue(path.matches(directory + "/\\d{8}/test_\\d+")); } + @Test + public void testGenerateUploadPath_FileNameInvalid() { + // 准备参数 + String name = "../test.jpg"; + String directory = "avatar"; + FileServiceImpl.PATH_PREFIX_DATE_ENABLE = false; + FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = false; + + // 调用,并断言异常 + assertServiceException(() -> fileService.generateUploadPath(name, directory), FILE_PATH_INVALID); + } + + @Test + public void testGenerateUploadPath_DirectoryInvalid() { + // 准备参数 + String name = "test.jpg"; + String directory = "../avatar"; + + // 调用,并断言异常 + assertServiceException(() -> fileService.generateUploadPath(name, directory), FILE_PATH_INVALID); + } + @Test public void testGenerateUploadPath_DirectoryEmpty() { // 准备参数 diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java index 065eb2d22..2829bdfd7 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java @@ -98,6 +98,9 @@ public interface ErrorCodeConstants { // ========== IoT 告警配置 1-050-013-000 ========== ErrorCode ALERT_CONFIG_NOT_EXISTS = new ErrorCode(1_050_013_000, "IoT 告警配置不存在"); + ErrorCode ALERT_CONFIG_SMS_TEMPLATE_REQUIRED = new ErrorCode(1_050_013_001, "已选择短信接收方式,短信模板不能为空"); + ErrorCode ALERT_CONFIG_MAIL_TEMPLATE_REQUIRED = new ErrorCode(1_050_013_002, "已选择邮件接收方式,邮件模板不能为空"); + ErrorCode ALERT_CONFIG_NOTIFY_TEMPLATE_REQUIRED = new ErrorCode(1_050_013_003, "已选择站内信接收方式,站内信模板不能为空"); // ========== IoT 告警记录 1-050-014-000 ========== ErrorCode ALERT_RECORD_NOT_EXISTS = new ErrorCode(1_050_014_000, "IoT 告警记录不存在"); diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/config/IotMessageBusAutoConfiguration.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/config/IotMessageBusAutoConfiguration.java index 67ae67399..a13b662f6 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/config/IotMessageBusAutoConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/config/IotMessageBusAutoConfiguration.java @@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessag import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.messagebus.core.local.IotLocalMessageBus; +import cn.iocoder.yudao.module.iot.core.messagebus.core.rabbitmq.IotRabbitMQMessageBus; import cn.iocoder.yudao.module.iot.core.messagebus.core.redis.IotRedisMessageBus; import cn.iocoder.yudao.module.iot.core.messagebus.core.rocketmq.IotRocketMQMessageBus; import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; @@ -14,8 +15,11 @@ import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.spring.autoconfigure.RocketMQProperties; import org.apache.rocketmq.spring.core.RocketMQTemplate; import org.redisson.api.RedissonClient; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; @@ -126,4 +130,25 @@ public class IotMessageBusAutoConfiguration { } -} \ No newline at end of file + // ==================== RabbitMQ 实现 ==================== + + @Configuration + @ConditionalOnProperty(prefix = "yudao.iot.message-bus", name = "type", havingValue = "rabbitmq") + @ConditionalOnClass(RabbitTemplate.class) + public static class IotRabbitMQMessageBusConfiguration { + + @Bean + @ConditionalOnMissingBean + public RabbitAdmin rabbitAdmin(RabbitTemplate rabbitTemplate) { + return new RabbitAdmin(rabbitTemplate); + } + + @Bean + public IotRabbitMQMessageBus iotRabbitMQMessageBus(RabbitTemplate rabbitTemplate, RabbitAdmin rabbitAdmin) { + log.info("[iotRabbitMQMessageBus][创建 IoT RabbitMQ 消息总线]"); + return new IotRabbitMQMessageBus(rabbitTemplate, rabbitAdmin); + } + + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerProtocol.java index 80ce9eec0..ea6e90845 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerProtocol.java @@ -326,9 +326,27 @@ public class IotModbusTcpServerProtocol implements IotProtocol { log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e); } } + + // 3. 清理本轮不再返回配置的已连接设备,避免继续轮询已删除设备的旧点位 + Set missingDeviceIds = configCacheService.cleanupMissingConfigs(connectedDeviceIds, configs); + for (Long deviceId : missingDeviceIds) { + cleanupMissingDevice(deviceId); + } } catch (Exception e) { log.error("[refreshConfig][刷新配置失败]", e); } } + private void cleanupMissingDevice(Long deviceId) { + try { + pollScheduler.stopPolling(deviceId); + pendingRequestManager.removeDevice(deviceId); + configCacheService.removeConfig(deviceId); + connectionManager.closeConnection(deviceId); + log.info("[cleanupMissingDevice][设备 {} 配置已失效,已停止轮询并清理连接]", deviceId); + } catch (Exception e) { + log.error("[cleanupMissingDevice][清理设备失败, deviceId={}]", deviceId, e); + } + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConfigCacheService.java index 791a7c7a9..f7df6b52b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConfigCacheService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConfigCacheService.java @@ -6,17 +6,20 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + /** * IoT Modbus TCP Server 配置缓存:认证时按需加载,断连时清理,定时刷新已连接设备 * @@ -96,6 +99,27 @@ public class IotModbusTcpServerConfigCacheService { } } + /** + * 清理本轮刷新后不再有效的设备配置 + * + * @param refreshedDeviceIds 本轮参与刷新的设备编号 + * @param currentConfigs 本轮远端返回的有效配置 + * @return 本轮已不再有效的设备编号 + */ + public Set cleanupMissingConfigs(Set refreshedDeviceIds, + List currentConfigs) { + if (CollUtil.isEmpty(refreshedDeviceIds)) { + return Collections.emptySet(); + } + Set currentDeviceIds = convertSet(currentConfigs, IotModbusDeviceConfigRespDTO::getDeviceId); + Set missingDeviceIds = new HashSet<>(refreshedDeviceIds); + missingDeviceIds.removeAll(currentDeviceIds); + for (Long deviceId : missingDeviceIds) { + configCache.remove(deviceId); + } + return missingDeviceIds; + } + /** * 获取设备配置 */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConnectionManager.java index 781d8ac54..4c01f6a7f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConnectionManager.java @@ -9,6 +9,7 @@ import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -109,7 +110,7 @@ public class IotModbusTcpServerConnectionManager { * 获取所有已连接设备的 ID 集合 */ public Set getConnectedDeviceIds() { - return deviceSocketMap.keySet(); + return new HashSet<>(deviceSocketMap.keySet()); } /** @@ -130,6 +131,24 @@ public class IotModbusTcpServerConnectionManager { return info; } + /** + * 关闭指定设备连接,并先移除映射,避免 closeHandler 再按正常断连发送下线消息 + */ + public void closeConnection(Long deviceId) { + NetSocket socket = deviceSocketMap.remove(deviceId); + if (socket == null) { + return; + } + connectionMap.remove(socket); + try { + socket.close(); + log.info("[closeConnection][设备 {} 连接已关闭]", deviceId); + } catch (Exception e) { + log.warn("[closeConnection][关闭设备连接失败, deviceId={}, remoteAddress={}]", + deviceId, socket.remoteAddress(), e); + } + } + /** * 发送数据到设备 * diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/config/IotAlertConfigRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/config/IotAlertConfigRespVO.java index e68a7b785..6b11c617d 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/config/IotAlertConfigRespVO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/config/IotAlertConfigRespVO.java @@ -37,6 +37,15 @@ public class IotAlertConfigRespVO { @Schema(description = "接收的类型数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") private List receiveTypes; + @Schema(description = "短信模板编号", example = "iot_alert_sms") + private String smsTemplateCode; + + @Schema(description = "邮件模板编号", example = "iot_alert_mail") + private String mailTemplateCode; + + @Schema(description = "站内信模板编号", example = "iot_alert_notify") + private String notifyTemplateCode; + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/config/IotAlertConfigSaveReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/config/IotAlertConfigSaveReqVO.java index 694e8bfdf..e83e25504 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/config/IotAlertConfigSaveReqVO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/config/IotAlertConfigSaveReqVO.java @@ -44,4 +44,12 @@ public class IotAlertConfigSaveReqVO { @NotEmpty(message = "接收的类型数组不能为空") private List receiveTypes; + @Schema(description = "短信模板编号", example = "iot_alert_sms") + private String smsTemplateCode; + + @Schema(description = "邮件模板编号", example = "iot_alert_mail") + private String mailTemplateCode; + + @Schema(description = "站内信模板编号", example = "iot_alert_notify") + private String notifyTemplateCode; } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/alert/IotAlertConfigDO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/alert/IotAlertConfigDO.java index 69f466bf4..792bb817c 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/alert/IotAlertConfigDO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/alert/IotAlertConfigDO.java @@ -81,4 +81,22 @@ public class IotAlertConfigDO extends BaseDO { @TableField(typeHandler = IntegerListTypeHandler.class) private List receiveTypes; + /** + * 短信模板编号 + * + * 关联 SmsTemplateDO 的 code 属性 + */ + private String smsTemplateCode; + /** + * 邮件模板编号 + * + * 关联 MailTemplateDO 的 code 属性 + */ + private String mailTemplateCode; + /** + * 站内信模板编号 + * + * 关联 NotifyTemplateDO 的 code 属性 + */ + private String notifyTemplateCode; } diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertConfigServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertConfigServiceImpl.java index 77be87fb8..bd422b71a 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertConfigServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertConfigServiceImpl.java @@ -1,11 +1,14 @@ package cn.iocoder.yudao.module.iot.service.alert; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.iot.controller.admin.alert.vo.config.IotAlertConfigPageReqVO; import cn.iocoder.yudao.module.iot.controller.admin.alert.vo.config.IotAlertConfigSaveReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.alert.IotAlertConfigDO; import cn.iocoder.yudao.module.iot.dal.mysql.alert.IotAlertConfigMapper; +import cn.iocoder.yudao.module.iot.enums.alert.IotAlertReceiveTypeEnum; import cn.iocoder.yudao.module.iot.service.rule.scene.IotSceneRuleService; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import jakarta.annotation.Resource; @@ -16,7 +19,10 @@ import org.springframework.validation.annotation.Validated; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.ALERT_CONFIG_MAIL_TEMPLATE_REQUIRED; import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.ALERT_CONFIG_NOT_EXISTS; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.ALERT_CONFIG_NOTIFY_TEMPLATE_REQUIRED; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.ALERT_CONFIG_SMS_TEMPLATE_REQUIRED; /** * IoT 告警配置 Service 实现类 @@ -41,7 +47,8 @@ public class IotAlertConfigServiceImpl implements IotAlertConfigService { public Long createAlertConfig(IotAlertConfigSaveReqVO createReqVO) { // 校验关联数据是否存在 sceneRuleService.validateSceneRuleList(createReqVO.getSceneRuleIds()); - adminUserApi.validateUserList(createReqVO.getReceiveUserIds()).checkError(); + adminUserApi.validateUserList(createReqVO.getReceiveUserIds()); + validateReceiveTemplates(createReqVO); // 插入 IotAlertConfigDO alertConfig = BeanUtils.toBean(createReqVO, IotAlertConfigDO.class); @@ -55,7 +62,8 @@ public class IotAlertConfigServiceImpl implements IotAlertConfigService { validateAlertConfigExists(updateReqVO.getId()); // 校验关联数据是否存在 sceneRuleService.validateSceneRuleList(updateReqVO.getSceneRuleIds()); - adminUserApi.validateUserList(updateReqVO.getReceiveUserIds()).checkError(); + adminUserApi.validateUserList(updateReqVO.getReceiveUserIds()); + validateReceiveTemplates(updateReqVO); // 更新 IotAlertConfigDO updateObj = BeanUtils.toBean(updateReqVO, IotAlertConfigDO.class); @@ -76,6 +84,24 @@ public class IotAlertConfigServiceImpl implements IotAlertConfigService { } } + private void validateReceiveTemplates(IotAlertConfigSaveReqVO reqVO) { + if (CollUtil.isEmpty(reqVO.getReceiveTypes())) { + return; + } + if (reqVO.getReceiveTypes().contains(IotAlertReceiveTypeEnum.SMS.getType()) + && StrUtil.isBlank(reqVO.getSmsTemplateCode())) { + throw exception(ALERT_CONFIG_SMS_TEMPLATE_REQUIRED); + } + if (reqVO.getReceiveTypes().contains(IotAlertReceiveTypeEnum.MAIL.getType()) + && StrUtil.isBlank(reqVO.getMailTemplateCode())) { + throw exception(ALERT_CONFIG_MAIL_TEMPLATE_REQUIRED); + } + if (reqVO.getReceiveTypes().contains(IotAlertReceiveTypeEnum.NOTIFY.getType()) + && StrUtil.isBlank(reqVO.getNotifyTemplateCode())) { + throw exception(ALERT_CONFIG_NOTIFY_TEMPLATE_REQUIRED); + } + } + @Override public IotAlertConfigDO getAlertConfig(Long id) { return alertConfigMapper.selectById(id); diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java index 93c73f576..6bb5f5f4f 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.iot.service.device.property; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.convert.Convert; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjUtil; @@ -148,8 +147,16 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { // 1. 根据物模型,拼接合法的属性 // TODO @芋艿:【待定 004】赋能后,属性到底以 thingModel 为准(ik),还是 db 的表结构为准(tl)? List thingModels = thingModelService.getThingModelListByProductIdFromCache(device.getProductId()); - Map properties = new HashMap<>(); + Map properties = new LinkedHashMap<>(); params.forEach((key, value) -> { + if (!(key instanceof CharSequence)) { + log.error("[saveDeviceProperty][消息({}) 的属性 key({}) 类型不正确]", message, key); + return; + } + if (value == null) { + log.warn("[saveDeviceProperty][消息({}) 的属性({}) 值为空,跳过]", message, key); + return; + } // 忽略大小写匹配物模型,避免设备上报的 key 与 identifier 大小写不一致导致丢失 IotThingModelDO thingModel = CollUtil.findOne(thingModels, o -> StrUtil.equalsIgnoreCase(o.getIdentifier(), (CharSequence) key)); @@ -158,21 +165,9 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { return; } String identifier = thingModel.getIdentifier(); // 统一以物模型 identifier 作为 key,避免大小写问题 - String dataType = thingModel.getProperty().getDataType(); - if (ObjectUtils.equalsAny(dataType, - IotDataSpecsDataTypeEnum.STRUCT.getDataType(), IotDataSpecsDataTypeEnum.ARRAY.getDataType())) { - // 特殊:STRUCT 和 ARRAY 类型,在 TDengine 里,有没对应数据类型,只能通过 JSON 来存储 - properties.put(identifier, JsonUtils.toJsonString(value)); - } else if (IotDataSpecsDataTypeEnum.INT.getDataType().equals(dataType)) { - properties.put(identifier, Convert.toInt(value)); - } else if (IotDataSpecsDataTypeEnum.FLOAT.getDataType().equals(dataType)) { - properties.put(identifier, Convert.toFloat(value)); - } else if (IotDataSpecsDataTypeEnum.DOUBLE.getDataType().equals(dataType)) { - properties.put(identifier, Convert.toDouble(value)); - } else if (IotDataSpecsDataTypeEnum.BOOL.getDataType().equals(dataType)) { - properties.put(identifier, Convert.toBool(value, false) ? (byte) 1 : (byte) 0); - } else { - properties.put(identifier, value); + Object convertedValue = convertPropertyValue(message, thingModel, value); + if (convertedValue != null) { + properties.put(identifier, convertedValue); } }); if (CollUtil.isEmpty(properties)) { @@ -194,6 +189,23 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { extractAndUpdateDeviceLocation(device, (Map) message.getParams()); } + private Object convertPropertyValue(IotDeviceMessage message, IotThingModelDO thingModel, Object value) { + String identifier = thingModel.getIdentifier(); + String dataType = thingModel.getProperty().getDataType(); + try { + Object convertedValue = thingModelService.convertThingModelPropertyValue(thingModel, value); + if (convertedValue == null) { + log.warn("[saveDeviceProperty][消息({}) 的属性({}) 值({}) 无法转换为类型({}),跳过]", + message, identifier, value, dataType); + } + return convertedValue; + } catch (Exception e) { + log.error("[saveDeviceProperty][消息({}) 的属性({}) 值({}) 转换为类型({}) 异常,跳过]", + message, identifier, value, dataType, e); + return null; + } + } + @Override public Map getLatestDeviceProperties(Long deviceId) { return deviceDataRedisDAO.get(deviceId); @@ -310,4 +322,4 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { return new BigDecimal[]{longitude, latitude}; } -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleAction.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleAction.java index d5656b970..937b8f603 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleAction.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleAction.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.iot.service.rule.scene.action; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.LocalDateTimeUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; @@ -79,30 +80,38 @@ public class IotAlertTriggerSceneRuleAction implements IotSceneRuleAction { } Map templateParams = buildTemplateParams(config, deviceMessage, device); config.getReceiveUserIds().forEach(userId -> - config.getReceiveTypes().forEach(receiveType -> sendAlertMessageToUser(userId, receiveType, templateParams))); + config.getReceiveTypes().forEach(receiveType -> + sendAlertMessageToUser(userId, receiveType, config, templateParams))); } /** * 按指定接收方式,给单个用户发送告警消息 */ - private void sendAlertMessageToUser(Long userId, Integer receiveType, Map templateParams) { + private void sendAlertMessageToUser(Long userId, Integer receiveType, IotAlertConfigDO config, + Map templateParams) { IotAlertReceiveTypeEnum typeEnum = IotAlertReceiveTypeEnum.of(receiveType); if (typeEnum == null) { return; } + String templateCode = resolveTemplateCode(config, typeEnum); + if (StrUtil.isBlank(templateCode)) {//为了兼容老的结构 + templateCode=typeEnum.getTemplateCode(); + log.warn("[sendAlertMessageToUser][配置({}) 用户({}) 接收方式({}) 未配置模板,使用默认模板({})]", + config.getId(), userId, typeEnum,templateCode); + } try { switch (typeEnum) { case SMS: smsSendApi.sendSingleSmsToAdmin(new SmsSendSingleToUserReqDTO().setUserId(userId) - .setTemplateCode(typeEnum.getTemplateCode()).setTemplateParams(templateParams)); + .setTemplateCode(templateCode).setTemplateParams(templateParams)); break; case MAIL: mailSendApi.sendSingleMailToAdmin(new MailSendSingleToUserReqDTO().setUserId(userId) - .setTemplateCode(typeEnum.getTemplateCode()).setTemplateParams(templateParams)); + .setTemplateCode(templateCode).setTemplateParams(templateParams)); break; case NOTIFY: notifyMessageSendApi.sendSingleMessageToAdmin(new NotifySendSingleToUserReqDTO().setUserId(userId) - .setTemplateCode(typeEnum.getTemplateCode()).setTemplateParams(templateParams)).checkError(); + .setTemplateCode(templateCode).setTemplateParams(templateParams)); break; } } catch (Exception ex) { @@ -111,6 +120,24 @@ public class IotAlertTriggerSceneRuleAction implements IotSceneRuleAction { } } + private String resolveTemplateCode(IotAlertConfigDO config, IotAlertReceiveTypeEnum typeEnum) { + String templateCode = null; + switch (typeEnum) { + case SMS: + templateCode = config.getSmsTemplateCode(); + break; + case MAIL: + templateCode = config.getMailTemplateCode(); + break; + case NOTIFY: + templateCode = config.getNotifyTemplateCode(); + break; + default: + break; + } + return StrUtil.blankToDefault(templateCode, typeEnum.getTemplateCode()); + } + private Map buildTemplateParams(IotAlertConfigDO config, @Nullable IotDeviceMessage deviceMessage, @Nullable IotDeviceDO device) { diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherHelper.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherHelper.java index 5d62bab91..84cbbf91e 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherHelper.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherHelper.java @@ -2,13 +2,17 @@ package cn.iocoder.yudao.module.iot.service.rule.scene.matcher; import cn.hutool.core.text.CharPool; import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils; +import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; 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.IotSceneRuleConditionOperatorEnum; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; import lombok.extern.slf4j.Slf4j; import java.util.HashMap; @@ -248,4 +252,26 @@ public final class IotSceneRuleMatcherHelper { return StrUtil.isNotBlank(expectedIdentifier) && expectedIdentifier.equals(actualIdentifier); } + /** + * 校验匹配器中的产品和设备是否一致 + * + * @param message 消息 + * @param productId 产品编号 + * @param deviceId 设备编号 + * @return 校验结果 + */ + public static boolean productAndDeviceNotMatched(IotDeviceMessage message, Long productId, Long deviceId) { + if (message == null || message.getDeviceId() == null) { + return false; + } + if (deviceId != null && !IotDeviceDO.DEVICE_ID_ALL.equals(deviceId)) { + return ObjectUtil.notEqual(message.getDeviceId(), deviceId); + } + if (productId == null) { + return false; + } + IotDeviceDO device = SpringUtils.getBean(IotDeviceService.class).getDeviceFromCache(message.getDeviceId()); + return device == null || ObjectUtil.notEqual(device.getProductId(), productId); + } + } diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcher.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcher.java index a54785ad6..3b41f57b0 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcher.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcher.java @@ -31,13 +31,19 @@ public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatc return false; } - // 1.2 检查操作符和参数是否有效 + // 1.2 修复条件匹配中忽略了产品和设备的一致性验证,2025.05.25 by panda + if (IotSceneRuleMatcherHelper.productAndDeviceNotMatched(message, condition.getProductId(),condition.getDeviceId())){ + IotSceneRuleMatcherHelper.logConditionMatchFailure(message,condition,"条件匹配器中产品或设备不匹配"); + return false; + } + + // 1.3 检查操作符和参数是否有效 if (!IotSceneRuleMatcherHelper.isConditionOperatorAndParamValid(condition)) { IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "操作符或参数无效"); return false; } - // 1.3 验证操作符是否为支持的时间操作符 + // 1.4 验证操作符是否为支持的时间操作符 String operator = condition.getOperator(); IotSceneRuleConditionOperatorEnum operatorEnum = IotSceneRuleConditionOperatorEnum.operatorOf(operator); if (operatorEnum == null) { @@ -45,6 +51,7 @@ public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatc return false; } + // 1.5 验证操作符是否为时间相关的操作符 if (IotSceneRuleTimeHelper.isTimeOperator(operatorEnum)) { IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "不支持的时间操作符: " + operator); return false; diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcher.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcher.java index 5741b95a6..f2c92ae6a 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcher.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcher.java @@ -30,14 +30,20 @@ public class IotDevicePropertyConditionMatcher implements IotSceneRuleConditionM return false; } - // 1.2 检查消息中是否包含条件指定的属性标识符 + // 1.2 修复条件匹配中忽略了产品和设备的一致性验证,2025.05.25 by panda + if (IotSceneRuleMatcherHelper.productAndDeviceNotMatched(message, condition.getProductId(),condition.getDeviceId())){ + IotSceneRuleMatcherHelper.logConditionMatchFailure(message,condition,"条件匹配器中产品或设备不匹配"); + return false; + } + + // 1.3 检查消息中是否包含条件指定的属性标识符 // 注意:属性上报可能同时上报多个属性,所以需要判断 condition.getIdentifier() 是否在 message 的 params 中 if (IotDeviceMessageUtils.notContainsIdentifier(message, condition.getIdentifier())) { IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中不包含属性: " + condition.getIdentifier()); return false; } - // 1.3 检查操作符和参数是否有效 + // 1.4 检查操作符和参数是否有效 if (!IotSceneRuleMatcherHelper.isConditionOperatorAndParamValid(condition)) { IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "操作符或参数无效"); return false; diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcher.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcher.java index 232812270..15dcbbdbe 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcher.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcher.java @@ -27,8 +27,13 @@ public class IotDeviceStateConditionMatcher implements IotSceneRuleConditionMatc IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "条件基础参数无效"); return false; } + // 1.2 修复条件匹配中忽略了产品和设备的一致性验证,2025.05.25 by panda + if (IotSceneRuleMatcherHelper.productAndDeviceNotMatched(message, condition.getProductId(),condition.getDeviceId())){ + IotSceneRuleMatcherHelper.logConditionMatchFailure(message,condition,"条件匹配器中产品或设备不匹配"); + return false; + } - // 1.2 检查操作符和参数是否有效 + // 1.3 检查操作符和参数是否有效 if (!IotSceneRuleMatcherHelper.isConditionOperatorAndParamValid(condition)) { IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "操作符或参数无效"); return false; diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcher.java index d997e46e1..2d54ff0a2 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcher.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcher.java @@ -43,7 +43,13 @@ public class IotDeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatc return false; } - // 1.3 检查标识符是否匹配 + // 1.3 修复触发器中忽略了产品和设备的一致性验证,2025.05.25 by panda + if (IotSceneRuleMatcherHelper.productAndDeviceNotMatched(message, trigger.getProductId(),trigger.getDeviceId())){ + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message,trigger,"触发器中产品或设备不匹配"); + return false; + } + + // 1.4 检查标识符是否匹配 String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message); if (!IotSceneRuleMatcherHelper.isIdentifierMatched(trigger.getIdentifier(), messageIdentifier)) { IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "标识符不匹配,期望: " + diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcher.java index 1f019b576..ea89ce370 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcher.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcher.java @@ -36,7 +36,13 @@ public class IotDevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerM return false; } - // 1.3 检查消息中是否包含触发器指定的属性标识符 + // 1.3 修复触发器中忽略了产品和设备的一致性验证,2025.05.25 by panda + if (IotSceneRuleMatcherHelper.productAndDeviceNotMatched(message, trigger.getProductId(),trigger.getDeviceId())){ + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message,trigger,"触发器中产品或设备不匹配"); + return false; + } + + // 1.4 检查消息中是否包含触发器指定的属性标识符 // 注意:属性上报可能同时上报多个属性,所以需要判断 trigger.getIdentifier() 是否在 message 的 params 中 if (IotDeviceMessageUtils.notContainsIdentifier(message, trigger.getIdentifier())) { IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中不包含属性: " + diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcher.java index 842d08125..5dcde6664 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcher.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcher.java @@ -7,7 +7,9 @@ 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.device.IotDeviceService; import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; +import jakarta.annotation.Resource; import org.springframework.stereotype.Component; import java.util.Map; @@ -20,6 +22,9 @@ import java.util.Map; @Component public class IotDeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMatcher { + @Resource + private IotDeviceService iotDeviceService; + @Override public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { return IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE; @@ -37,7 +42,12 @@ public class IotDeviceServiceInvokeTriggerMatcher implements IotSceneRuleTrigger IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息方法不匹配,期望: " + IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod() + ", 实际: " + message.getMethod()); return false; } - // 1.3 检查标识符是否匹配 + // 1.3 修复触发器中忽略了产品和设备的一致性验证,2025.05.25 by panda + if (IotSceneRuleMatcherHelper.productAndDeviceNotMatched(message, trigger.getProductId(),trigger.getDeviceId())){ + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message,trigger,"触发器中产品或设备不匹配"); + return false; + } + // 1.4 检查标识符是否匹配 String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message); if (!IotSceneRuleMatcherHelper.isIdentifierMatched(trigger.getIdentifier(), messageIdentifier)) { IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "标识符不匹配,期望: " + trigger.getIdentifier() + ", 实际: " + messageIdentifier); diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcher.java index 6b8c73a50..183442eea 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcher.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcher.java @@ -5,7 +5,9 @@ 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.device.IotDeviceService; import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; +import jakarta.annotation.Resource; import org.springframework.stereotype.Component; /** @@ -16,6 +18,9 @@ import org.springframework.stereotype.Component; @Component public class IotDeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatcher { + @Resource + private IotDeviceService iotDeviceService; + @Override public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { return IotSceneRuleTriggerTypeEnum.DEVICE_STATE_UPDATE; @@ -36,7 +41,13 @@ public class IotDeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMa return false; } - // 1.3 检查操作符和值是否有效 + // 1.3 修复触发器中忽略了产品和设备的一致性验证,2025.05.25 by panda + if (IotSceneRuleMatcherHelper.productAndDeviceNotMatched(message, trigger.getProductId(),trigger.getDeviceId())){ + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message,trigger,"触发器中产品或设备不匹配"); + return false; + } + + // 1.4 检查操作符和值是否有效 if (!IotSceneRuleMatcherHelper.isTriggerOperatorAndValueValid(trigger)) { IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "操作符或值无效"); return false; diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelService.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelService.java index b8c951b94..5bdfd7c6f 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelService.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelService.java @@ -108,4 +108,13 @@ public interface IotThingModelService { */ void validateThingModelListExists(Long productId, Set identifiers); -} \ No newline at end of file + /** + * 按物模型属性的数据类型转换设备上报值 + * + * @param thingModel 物模型 + * @param value 设备上报值 + * @return 转换后的值;无法转换时返回 null + */ + Object convertThingModelPropertyValue(IotThingModelDO thingModel, Object value); + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java index 4a8b97475..902996012 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java @@ -1,10 +1,16 @@ package cn.iocoder.yudao.module.iot.service.thingmodel; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelListReqVO; import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelPageReqVO; @@ -12,9 +18,14 @@ import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelS import cn.iocoder.yudao.module.iot.convert.thingmodel.IotThingModelConvert; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelBoolOrEnumDataSpecs; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelDataSpecs; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelDateOrTextDataSpecs; import cn.iocoder.yudao.module.iot.dal.mysql.thingmodel.IotThingModelMapper; import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum; +import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField; import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService; import cn.iocoder.yudao.module.iot.service.product.IotProductService; import jakarta.annotation.Resource; @@ -26,7 +37,15 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; import java.util.Collection; +import java.util.Date; import java.util.List; import java.util.Objects; import java.util.Set; @@ -166,6 +185,239 @@ public class IotThingModelServiceImpl implements IotThingModelService { } } + @Override + public Object convertThingModelPropertyValue(IotThingModelDO thingModel, Object value) { + if (thingModel == null || thingModel.getProperty() == null || value == null) { + return null; + } + String dataType = thingModel.getProperty().getDataType(); + if (ObjectUtils.equalsAny(dataType, + IotDataSpecsDataTypeEnum.STRUCT.getDataType(), IotDataSpecsDataTypeEnum.ARRAY.getDataType())) { + // 特殊:STRUCT 和 ARRAY 类型,在 TDengine 里,没有对应数据类型,只能通过 JSON 来存储 + return convertToVarchar(JsonUtils.toJsonString(value), TDengineTableField.LENGTH_VARCHAR); + } + if (IotDataSpecsDataTypeEnum.INT.getDataType().equals(dataType)) { + return convertToInt(value); + } + if (IotDataSpecsDataTypeEnum.FLOAT.getDataType().equals(dataType)) { + return convertToFloat(value); + } + if (IotDataSpecsDataTypeEnum.DOUBLE.getDataType().equals(dataType)) { + return convertToDouble(value); + } + if (IotDataSpecsDataTypeEnum.ENUM.getDataType().equals(dataType)) { + return convertEnumToTinyInt(thingModel, value); + } + if (IotDataSpecsDataTypeEnum.BOOL.getDataType().equals(dataType)) { + return convertBoolToTinyInt(value); + } + if (IotDataSpecsDataTypeEnum.TEXT.getDataType().equals(dataType)) { + return convertToVarchar(Convert.toStr(value), getTextLength(thingModel)); + } + if (IotDataSpecsDataTypeEnum.DATE.getDataType().equals(dataType)) { + return convertToTimestamp(value); + } + return null; + } + + private Integer getTextLength(IotThingModelDO thingModel) { + ThingModelDataSpecs dataSpecs = thingModel.getProperty().getDataSpecs(); + if (!(dataSpecs instanceof ThingModelDateOrTextDataSpecs)) { + return null; + } + return ((ThingModelDateOrTextDataSpecs) dataSpecs).getLength(); + } + + private String convertToVarchar(String value, Integer length) { + if (value == null) { + return null; + } + if (length != null && value.getBytes(StandardCharsets.UTF_8).length > length) { + return null; + } + return value; + } + + private Integer convertToInt(Object value) { + BigDecimal decimal = convertToBigDecimal(value); + if (decimal == null) { + return null; + } + try { + return decimal.intValueExact(); + } catch (ArithmeticException e) { + return null; + } + } + + private Float convertToFloat(Object value) { + BigDecimal decimal = convertToBigDecimal(value); + if (decimal == null) { + return null; + } + float result = decimal.floatValue(); + return Float.isFinite(result) ? result : null; + } + + private Double convertToDouble(Object value) { + BigDecimal decimal = convertToBigDecimal(value); + if (decimal == null) { + return null; + } + double result = decimal.doubleValue(); + return Double.isFinite(result) ? result : null; + } + + private BigDecimal convertToBigDecimal(Object value) { + if (value instanceof Boolean) { + return null; + } + String text = Convert.toStr(value); + if (StrUtil.isBlank(text)) { + return null; + } + try { + return new BigDecimal(text); + } catch (NumberFormatException e) { + return null; + } + } + + private Byte convertEnumToTinyInt(IotThingModelDO thingModel, Object value) { + Integer intValue = convertToInt(value); + if (intValue == null && value instanceof CharSequence) { + intValue = getEnumValueByName(thingModel, value.toString()); + } + if (intValue == null || !isTinyInt(intValue)) { + return null; + } + if (CollUtil.isNotEmpty(thingModel.getProperty().getDataSpecsList()) + && getEnumDataSpecsByValue(thingModel, intValue) == null) { + return null; + } + return intValue.byteValue(); + } + + private Integer getEnumValueByName(IotThingModelDO thingModel, String name) { + ThingModelBoolOrEnumDataSpecs dataSpecs = getEnumDataSpecsByName(thingModel, name); + return dataSpecs != null ? dataSpecs.getValue() : null; + } + + private ThingModelBoolOrEnumDataSpecs getEnumDataSpecsByName(IotThingModelDO thingModel, String name) { + if (CollUtil.isEmpty(thingModel.getProperty().getDataSpecsList())) { + return null; + } + for (ThingModelDataSpecs dataSpecs : thingModel.getProperty().getDataSpecsList()) { + if (!(dataSpecs instanceof ThingModelBoolOrEnumDataSpecs)) { + continue; + } + ThingModelBoolOrEnumDataSpecs enumDataSpecs = (ThingModelBoolOrEnumDataSpecs) dataSpecs; + if (StrUtil.equals(enumDataSpecs.getName(), name)) { + return enumDataSpecs; + } + } + return null; + } + + private ThingModelBoolOrEnumDataSpecs getEnumDataSpecsByValue(IotThingModelDO thingModel, Integer value) { + if (CollUtil.isEmpty(thingModel.getProperty().getDataSpecsList())) { + return null; + } + for (ThingModelDataSpecs dataSpecs : thingModel.getProperty().getDataSpecsList()) { + if (!(dataSpecs instanceof ThingModelBoolOrEnumDataSpecs)) { + continue; + } + ThingModelBoolOrEnumDataSpecs enumDataSpecs = (ThingModelBoolOrEnumDataSpecs) dataSpecs; + if (Objects.equals(enumDataSpecs.getValue(), value)) { + return enumDataSpecs; + } + } + return null; + } + + private Byte convertBoolToTinyInt(Object value) { + if (value instanceof Boolean) { + return (Boolean) value ? (byte) 1 : (byte) 0; + } + if (value instanceof CharSequence) { + String text = StrUtil.trim(value.toString()); + if (StrUtil.equalsIgnoreCase(text, "true")) { + return (byte) 1; + } + if (StrUtil.equalsIgnoreCase(text, "false")) { + return (byte) 0; + } + } + Integer intValue = convertToInt(value); + if (intValue == null || (intValue != 0 && intValue != 1)) { + return null; + } + return intValue.byteValue(); + } + + private boolean isTinyInt(Integer value) { + return value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE; + } + + private Long convertToTimestamp(Object value) { + if (value instanceof LocalDateTime) { + return LocalDateTimeUtil.toEpochMilli((LocalDateTime) value); + } + if (value instanceof LocalDate) { + return LocalDateTimeUtil.toEpochMilli(((LocalDate) value).atStartOfDay()); + } + if (value instanceof Instant) { + return ((Instant) value).toEpochMilli(); + } + if (value instanceof OffsetDateTime) { + return ((OffsetDateTime) value).toInstant().toEpochMilli(); + } + if (value instanceof ZonedDateTime) { + return ((ZonedDateTime) value).toInstant().toEpochMilli(); + } + if (value instanceof Date) { + return ((Date) value).getTime(); + } + Long timestamp = convertToLong(value); + if (timestamp != null) { + return timestamp; + } + if (!(value instanceof CharSequence)) { + return null; + } + String text = StrUtil.trim(value.toString()); + if (StrUtil.isBlank(text)) { + return null; + } + try { + return OffsetDateTime.parse(text).toInstant().toEpochMilli(); + } catch (Exception ignored) { + // 尝试本地时间格式,例如 yyyy-MM-dd HH:mm:ss + } + try { + return LocalDateTimeUtil.toEpochMilli(LocalDateTimeUtil.parse(text, DatePattern.NORM_DATETIME_PATTERN)); + } catch (Exception ignored) { + // 尝试其它 Hutool 支持的本地时间格式 + } + try { + return LocalDateTimeUtil.toEpochMilli(LocalDateTimeUtils.parse(text)); + } catch (Exception ignored) { + return null; + } + } + + private Long convertToLong(Object value) { + BigDecimal decimal = convertToBigDecimal(value); + if (decimal == null) { + return null; + } + try { + return decimal.longValueExact(); + } catch (ArithmeticException e) { + return null; + } + } + private void validateProductThingModelMapperExists(Long id) { if (thingModelMapper.selectById(id) == null) { throw exception(THING_MODEL_NOT_EXISTS); diff --git a/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImplTest.java b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImplTest.java index 8f87cb89f..d2f26aaf5 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImplTest.java +++ b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImplTest.java @@ -54,6 +54,7 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest { // mock 行为 when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId())) .thenReturn(singletonList(thingModel)); + when(thingModelService.convertThingModelPropertyValue(thingModel, 100)).thenReturn(100); // 调用 service.saveDeviceProperty(device, message); @@ -91,64 +92,62 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest { } @Test - public void testSaveDeviceProperty_boolFromBooleanTrue() { - // 准备参数:物模型为 BOOL,设备上报原生 boolean true - assertBoolValueConvertedToByte(true, (byte) 1); - } - - @Test - public void testSaveDeviceProperty_boolFromBooleanFalse() { - // 准备参数:物模型为 BOOL,设备上报原生 boolean false - assertBoolValueConvertedToByte(false, (byte) 0); - } - - @Test - public void testSaveDeviceProperty_boolFromStringTrue() { - // 准备参数:物模型为 BOOL,设备上报字符串 "true" - assertBoolValueConvertedToByte("true", (byte) 1); - } - - @Test - public void testSaveDeviceProperty_boolFromStringFalse() { - // 准备参数:物模型为 BOOL,设备上报字符串 "false" - assertBoolValueConvertedToByte("false", (byte) 0); - } - - @Test - public void testSaveDeviceProperty_boolFromNumberOne() { - // 准备参数:物模型为 BOOL,设备上报数字 1 - assertBoolValueConvertedToByte(1, (byte) 1); - } - - @Test - public void testSaveDeviceProperty_boolFromNumberZero() { - // 准备参数:物模型为 BOOL,设备上报数字 0 - assertBoolValueConvertedToByte(0, (byte) 0); - } - - /** - * 校验 BOOL 类型属性上报后,最终落到 properties Map 的值类型与数值 - */ - private void assertBoolValueConvertedToByte(Object reportedValue, byte expected) { - // 准备参数 + public void testSaveDeviceProperty_convertValueFailed() { + // 准备参数:物模型存在,但是属性值无法按物模型转换 IotDeviceDO device = buildDevice(); - IotThingModelDO thingModel = buildThingModel("PowerSwitch", IotDataSpecsDataTypeEnum.BOOL.getDataType()); + IotThingModelDO temperature = buildThingModel("Temperature", IotDataSpecsDataTypeEnum.INT.getDataType()); Map params = new HashMap<>(); - params.put("PowerSwitch", reportedValue); + params.put("Temperature", "abc"); + IotDeviceMessage message = buildMessage(params); + + when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId())) + .thenReturn(singletonList(temperature)); + when(thingModelService.convertThingModelPropertyValue(temperature, "abc")).thenReturn(null); + + assertDoesNotThrow(() -> service.saveDeviceProperty(device, message)); + + verify(devicePropertyMapper, never()).insert(any(), any(), anyLong(), anyLong()); + verify(deviceDataRedisDAO, never()).putAll(anyLong(), any()); + } + + @Test + public void testSaveDeviceProperty_skipNullValue() { + // 准备参数:属性值为空,不能写入 TDengine 与 Redis + IotDeviceDO device = buildDevice(); + IotThingModelDO thingModel = buildThingModel("Temperature", IotDataSpecsDataTypeEnum.INT.getDataType()); + Map params = new HashMap<>(); + params.put("Temperature", null); IotDeviceMessage message = buildMessage(params); - // mock 行为 when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId())) .thenReturn(singletonList(thingModel)); - // 调用:不能抛异常 assertDoesNotThrow(() -> service.saveDeviceProperty(device, message)); - // 断言:写入的 value 是 byte 类型,且值匹配 + verify(thingModelService, never()).convertThingModelPropertyValue(any(), any()); + verify(devicePropertyMapper, never()).insert(any(), any(), anyLong(), anyLong()); + verify(deviceDataRedisDAO, never()).putAll(anyLong(), any()); + } + + @Test + public void testSaveDeviceProperty_skipInvalidKeyType() { + // 准备参数:Map 中包含非字符串 key,不能因为强转失败影响其它合法属性 + IotDeviceDO device = buildDevice(); + IotThingModelDO thingModel = buildThingModel("PowerSwitch", IotDataSpecsDataTypeEnum.BOOL.getDataType()); + Map params = new HashMap<>(); + params.put(123, 1); + params.put("PowerSwitch", true); + IotDeviceMessage message = buildMessage(params); + + when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId())) + .thenReturn(singletonList(thingModel)); + when(thingModelService.convertThingModelPropertyValue(thingModel, true)).thenReturn((byte) 1); + + assertDoesNotThrow(() -> service.saveDeviceProperty(device, message)); + Map dbProperties = captureMapperInsertProperties(); - Object actual = dbProperties.get("PowerSwitch"); - assertTrue(actual instanceof Byte, "BOOL 属性应被转为 Byte 类型,实际为 " + (actual == null ? "null" : actual.getClass())); - assertEquals(expected, actual); + assertEquals(1, dbProperties.size()); + assertEquals((byte) 1, dbProperties.get("PowerSwitch")); } // ========== 辅助方法 ========== @@ -173,7 +172,7 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest { /** * 构造一条属性上报消息 */ - private IotDeviceMessage buildMessage(Map params) { + private IotDeviceMessage buildMessage(Map params) { IotDeviceMessage message = new IotDeviceMessage(); message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); message.setParams(params); diff --git a/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java index 479ba40be..a760bf74d 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java @@ -1,7 +1,9 @@ package cn.iocoder.yudao.module.iot.service.rule.scene; import cn.hutool.core.collection.ListUtil; +import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; @@ -21,6 +23,9 @@ import cn.iocoder.yudao.module.iot.service.product.IotProductService; import org.junit.jupiter.api.*; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import java.lang.reflect.Field; import java.util.*; @@ -43,8 +48,23 @@ import static org.mockito.Mockito.*; * * @author HUIHUI */ +@SpringJUnitConfig(classes = IotSceneRuleTimerConditionIntegrationTest.TestConfig.class) public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTest { + /** + * 注入一下 SpringUtil,解析 EL 表达式时需要 + * {@link SpringExpressionUtils#parseExpression} + */ + @Configuration + static class TestConfig { + + @Bean + public SpringUtil springUtil() { + return new SpringUtil(); + } + + } + @InjectMocks private IotSceneRuleServiceImpl sceneRuleService; diff --git a/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleActionTest.java b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleActionTest.java index 6bce06974..5b2a4cb5c 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleActionTest.java +++ b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleActionTest.java @@ -106,6 +106,9 @@ public class IotAlertTriggerSceneRuleActionTest extends BaseMockitoUnitTest { IotAlertReceiveTypeEnum.SMS.getType(), IotAlertReceiveTypeEnum.MAIL.getType(), IotAlertReceiveTypeEnum.NOTIFY.getType())); + c.setSmsTemplateCode("custom_sms"); + c.setMailTemplateCode("custom_mail"); + c.setNotifyTemplateCode("custom_notify"); }); IotDeviceDO device = randomPojo(IotDeviceDO.class); @@ -130,13 +133,13 @@ public class IotAlertTriggerSceneRuleActionTest extends BaseMockitoUnitTest { ArgumentCaptor smsCaptor = ArgumentCaptor.forClass(SmsSendSingleToUserReqDTO.class); verify(smsSendApi, times(1)).sendSingleSmsToAdmin(smsCaptor.capture()); assertEquals(userId, smsCaptor.getValue().getUserId()); - assertEquals(IotAlertReceiveTypeEnum.SMS.getTemplateCode(), smsCaptor.getValue().getTemplateCode()); + assertEquals("custom_sms", smsCaptor.getValue().getTemplateCode()); ArgumentCaptor mailCaptor = ArgumentCaptor.forClass(MailSendSingleToUserReqDTO.class); verify(mailSendApi, times(1)).sendSingleMailToAdmin(mailCaptor.capture()); - assertEquals(IotAlertReceiveTypeEnum.MAIL.getTemplateCode(), mailCaptor.getValue().getTemplateCode()); + assertEquals("custom_mail", mailCaptor.getValue().getTemplateCode()); ArgumentCaptor notifyCaptor = ArgumentCaptor.forClass(NotifySendSingleToUserReqDTO.class); verify(notifyMessageSendApi, times(1)).sendSingleMessageToAdmin(notifyCaptor.capture()); - assertEquals(IotAlertReceiveTypeEnum.NOTIFY.getTemplateCode(), notifyCaptor.getValue().getTemplateCode()); + assertEquals("custom_notify", notifyCaptor.getValue().getTemplateCode()); } @Test diff --git a/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImplTest.java b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImplTest.java new file mode 100644 index 000000000..2668c43ab --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImplTest.java @@ -0,0 +1,201 @@ +package cn.iocoder.yudao.module.iot.service.thingmodel; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelProperty; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelBoolOrEnumDataSpecs; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelDateOrTextDataSpecs; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService; +import cn.iocoder.yudao.module.iot.service.product.IotProductService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link IotThingModelServiceImpl} 的单元测试 + * + * @author 芋道源码 + */ +@Import(IotThingModelServiceImpl.class) +public class IotThingModelServiceImplTest extends BaseDbUnitTest { + + @Resource + private IotThingModelServiceImpl thingModelService; + + @MockitoBean + private IotProductService productService; + @MockitoBean + private IotDeviceModbusPointService deviceModbusPointService; + + @Test + public void testConvertThingModelPropertyValue_boolFromBooleanTrue() { + assertBoolValueConvertedToByte(true, (byte) 1); + } + + @Test + public void testConvertThingModelPropertyValue_boolFromBooleanFalse() { + assertBoolValueConvertedToByte(false, (byte) 0); + } + + @Test + public void testConvertThingModelPropertyValue_boolFromStringTrue() { + assertBoolValueConvertedToByte("true", (byte) 1); + } + + @Test + public void testConvertThingModelPropertyValue_boolFromStringFalse() { + assertBoolValueConvertedToByte("false", (byte) 0); + } + + @Test + public void testConvertThingModelPropertyValue_boolFromNumberOne() { + assertBoolValueConvertedToByte(1, (byte) 1); + } + + @Test + public void testConvertThingModelPropertyValue_boolFromNumberZero() { + assertBoolValueConvertedToByte(0, (byte) 0); + } + + @Test + public void testConvertThingModelPropertyValue_boolInvalid() { + IotThingModelDO thingModel = buildThingModel("PowerSwitch", IotDataSpecsDataTypeEnum.BOOL.getDataType()); + + Object result = thingModelService.convertThingModelPropertyValue(thingModel, "yes"); + + assertNull(result); + } + + @Test + public void testConvertThingModelPropertyValue_enumFromStringNumber() { + IotThingModelDO thingModel = buildEnumThingModel("WorkMode", enumSpec("Auto", 1), enumSpec("Manual", 2)); + + Object result = thingModelService.convertThingModelPropertyValue(thingModel, "1"); + + assertEquals((byte) 1, result); + } + + @Test + public void testConvertThingModelPropertyValue_enumFromSpecName() { + IotThingModelDO thingModel = buildEnumThingModel("WorkMode", enumSpec("Auto", 1), enumSpec("Manual", 2)); + + Object result = thingModelService.convertThingModelPropertyValue(thingModel, "Manual"); + + assertEquals((byte) 2, result); + } + + @Test + public void testConvertThingModelPropertyValue_enumInvalidSpecValue() { + IotThingModelDO thingModel = buildEnumThingModel("WorkMode", enumSpec("Auto", 1), enumSpec("Manual", 2)); + + Object result = thingModelService.convertThingModelPropertyValue(thingModel, 3); + + assertNull(result); + } + + @Test + public void testConvertThingModelPropertyValue_enumOutOfTinyIntRange() { + IotThingModelDO thingModel = buildThingModel("WorkMode", IotDataSpecsDataTypeEnum.ENUM.getDataType()); + + Object result = thingModelService.convertThingModelPropertyValue(thingModel, 128); + + assertNull(result); + } + + @Test + public void testConvertThingModelPropertyValue_dateFromLocalDateTime() { + IotThingModelDO thingModel = buildThingModel("CollectTime", IotDataSpecsDataTypeEnum.DATE.getDataType()); + LocalDateTime collectTime = LocalDateTime.of(2025, 1, 2, 3, 4, 5); + + Object result = thingModelService.convertThingModelPropertyValue(thingModel, collectTime); + + assertEquals(LocalDateTimeUtil.toEpochMilli(collectTime), result); + } + + @Test + public void testConvertThingModelPropertyValue_dateFromString() { + IotThingModelDO thingModel = buildThingModel("CollectTime", IotDataSpecsDataTypeEnum.DATE.getDataType()); + LocalDateTime collectTime = LocalDateTime.of(2025, 1, 2, 3, 4, 5); + + Object result = thingModelService.convertThingModelPropertyValue(thingModel, "2025-01-02 03:04:05"); + + assertEquals(LocalDateTimeUtil.toEpochMilli(collectTime), result); + } + + @Test + public void testConvertThingModelPropertyValue_intInvalid() { + IotThingModelDO thingModel = buildThingModel("Temperature", IotDataSpecsDataTypeEnum.INT.getDataType()); + + Object result = thingModelService.convertThingModelPropertyValue(thingModel, "abc"); + + assertNull(result); + } + + @Test + public void testConvertThingModelPropertyValue_textOverLength() { + IotThingModelDO thingModel = buildTextThingModel("Remark", 3); + + Object result = thingModelService.convertThingModelPropertyValue(thingModel, "abcd"); + + assertNull(result); + } + + @Test + public void testConvertThingModelPropertyValue_structToJson() { + IotThingModelDO thingModel = buildThingModel("GeoLocation", IotDataSpecsDataTypeEnum.STRUCT.getDataType()); + Map value = new HashMap<>(); + value.put("Longitude", 120.1D); + + Object result = thingModelService.convertThingModelPropertyValue(thingModel, value); + + assertEquals(JsonUtils.toJsonString(value), result); + } + + private void assertBoolValueConvertedToByte(Object reportedValue, byte expected) { + IotThingModelDO thingModel = buildThingModel("PowerSwitch", IotDataSpecsDataTypeEnum.BOOL.getDataType()); + + Object result = thingModelService.convertThingModelPropertyValue(thingModel, reportedValue); + + assertEquals(expected, result); + } + + private IotThingModelDO buildThingModel(String identifier, String dataType) { + ThingModelProperty property = new ThingModelProperty(); + property.setIdentifier(identifier); + property.setDataType(dataType); + return IotThingModelDO.builder().identifier(identifier).property(property).build(); + } + + private IotThingModelDO buildEnumThingModel(String identifier, ThingModelBoolOrEnumDataSpecs... dataSpecsList) { + IotThingModelDO thingModel = buildThingModel(identifier, IotDataSpecsDataTypeEnum.ENUM.getDataType()); + thingModel.getProperty().setDataSpecsList(Arrays.asList(dataSpecsList)); + return thingModel; + } + + private ThingModelBoolOrEnumDataSpecs enumSpec(String name, Integer value) { + ThingModelBoolOrEnumDataSpecs dataSpecs = new ThingModelBoolOrEnumDataSpecs(); + dataSpecs.setName(name); + dataSpecs.setValue(value); + return dataSpecs; + } + + private IotThingModelDO buildTextThingModel(String identifier, Integer length) { + IotThingModelDO thingModel = buildThingModel(identifier, IotDataSpecsDataTypeEnum.TEXT.getDataType()); + ThingModelDateOrTextDataSpecs dataSpecs = new ThingModelDateOrTextDataSpecs(); + dataSpecs.setLength(length); + thingModel.getProperty().setDataSpecs(dataSpecs); + return thingModel; + } + +} diff --git a/yudao-module-mall/yudao-module-promotion-server/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java b/yudao-module-mall/yudao-module-promotion-server/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java index a2f28d3b8..ab215f603 100644 --- a/yudao-module-mall/yudao-module-promotion-server/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-server/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java @@ -77,7 +77,6 @@ public class CombinationRecordServiceImpl implements CombinationRecordService { @Resource public SocialClientApi socialClientApi; - // TODO @芋艿:在详细预览下; @Override public KeyValue validateCombinationRecord( Long userId, Long activityId, Long headId, Long skuId, Integer count) { @@ -97,7 +96,7 @@ public class CombinationRecordServiceImpl implements CombinationRecordService { } // 2. 父拼团是否存在,是否已经满了 - if (headId != null) { + if (isJoinCombination(headId)) { // 2.1. 查询进行中的父拼团 CombinationRecordDO record = combinationRecordMapper.selectByHeadId(headId, CombinationRecordStatusEnum.IN_PROGRESS.getStatus()); if (record == null) { @@ -124,12 +123,12 @@ public class CombinationRecordServiceImpl implements CombinationRecordService { throw exception(COMBINATION_JOIN_ACTIVITY_PRODUCT_NOT_EXISTS); } // 4.2 校验 sku 是否存在 - ProductSkuRespDTO sku = productSkuApi.getSku(skuId).getCheckedData(); + ProductSkuRespDTO sku = productSkuApi.getSku(skuId); if (sku == null) { throw exception(COMBINATION_JOIN_ACTIVITY_PRODUCT_NOT_EXISTS); } // 4.3 校验库存是否充足 - if (count >= sku.getStock()) { + if (count > sku.getStock()) { throw exception(COMBINATION_ACTIVITY_UPDATE_STOCK_FAIL); } @@ -153,6 +152,16 @@ public class CombinationRecordServiceImpl implements CombinationRecordService { return new KeyValue<>(activity, product); } + /** + * 是否加入已有拼团。 + * + * 前端开团时可能不传 headId,也可能传 {@link CombinationRecordDO#HEAD_ID_GROUP},都应视为新开团; + * 只有传入真实团长记录编号时,才需要按参团校验父拼团。 + */ + private static boolean isJoinCombination(Long headId) { + return headId != null && ObjUtil.notEqual(headId, CombinationRecordDO.HEAD_ID_GROUP); + } + @Override @Transactional(rollbackFor = Exception.class) public CombinationRecordDO createCombinationRecord(CombinationRecordCreateReqDTO reqDTO) { @@ -161,12 +170,12 @@ public class CombinationRecordServiceImpl implements CombinationRecordService { reqDTO.getActivityId(), reqDTO.getHeadId(), reqDTO.getSkuId(), reqDTO.getCount()); // 2. 组合数据创建拼团记录 - MemberUserRespDTO user = memberUserApi.getUser(reqDTO.getUserId()).getCheckedData(); - ProductSpuRespDTO spu = productSpuApi.getSpu(reqDTO.getSpuId()).getCheckedData(); - ProductSkuRespDTO sku = productSkuApi.getSku(reqDTO.getSkuId()).getCheckedData(); + MemberUserRespDTO user = memberUserApi.getUser(reqDTO.getUserId()); + ProductSpuRespDTO spu = productSpuApi.getSpu(reqDTO.getSpuId()); + ProductSkuRespDTO sku = productSkuApi.getSku(reqDTO.getSkuId()); CombinationRecordDO record = CombinationActivityConvert.INSTANCE.convert(reqDTO, keyValue.getKey(), user, spu, sku); // 2.1. 如果是团长需要设置 headId 为 CombinationRecordDO#HEAD_ID_GROUP - if (record.getHeadId() == null) { + if (!isJoinCombination(record.getHeadId())) { record.setStartTime(LocalDateTime.now()) .setExpireTime(LocalDateTime.now().plusHours(keyValue.getKey().getLimitDuration())) .setHeadId(CombinationRecordDO.HEAD_ID_GROUP); @@ -229,7 +238,7 @@ public class CombinationRecordServiceImpl implements CombinationRecordService { .setTemplateTitle(COMBINATION_SUCCESS) .setPage("pages/order/detail?id=" + record.getOrderId()) // 订单详情页 .addMessage("thing1", "商品拼团活动") // 活动标题 - .addMessage("thing2", "恭喜您拼团成功!我们将尽快为您发货。")).checkError(); // 温馨提示 + .addMessage("thing2", "恭喜您拼团成功!我们将尽快为您发货。")); // 温馨提示 } @Override @@ -336,7 +345,7 @@ public class CombinationRecordServiceImpl implements CombinationRecordService { CombinationRecordStatusEnum.FAILED); // 2. 订单取消 headAndRecords.forEach(item -> tradeOrderApi.cancelPaidOrder(item.getUserId(), item.getOrderId(), - TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getType()).checkError()); + TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getType())); } /** diff --git a/yudao-module-mall/yudao-module-promotion-server/src/test/resources/sql/clean.sql b/yudao-module-mall/yudao-module-promotion-server/src/test/resources/sql/clean.sql index 6a1a24252..54ec043c1 100644 --- a/yudao-module-mall/yudao-module-promotion-server/src/test/resources/sql/clean.sql +++ b/yudao-module-mall/yudao-module-promotion-server/src/test/resources/sql/clean.sql @@ -5,8 +5,9 @@ DELETE FROM "promotion_reward_activity"; DELETE FROM "promotion_discount_activity"; DELETE FROM "promotion_discount_product"; DELETE FROM "promotion_seckill_config"; +DELETE FROM "promotion_combination_record"; DELETE FROM "promotion_combination_activity"; DELETE FROM "promotion_article_category"; DELETE FROM "promotion_article"; DELETE FROM "promotion_diy_template"; -DELETE FROM "promotion_diy_page"; \ No newline at end of file +DELETE FROM "promotion_diy_page"; diff --git a/yudao-module-mall/yudao-module-promotion-server/src/test/resources/sql/create_tables.sql b/yudao-module-mall/yudao-module-promotion-server/src/test/resources/sql/create_tables.sql index 5b566ce5b..fae2e5225 100644 --- a/yudao-module-mall/yudao-module-promotion-server/src/test/resources/sql/create_tables.sql +++ b/yudao-module-mall/yudao-module-promotion-server/src/test/resources/sql/create_tables.sql @@ -203,6 +203,36 @@ CREATE TABLE IF NOT EXISTS "promotion_combination_activity" PRIMARY KEY ("id") ) COMMENT '拼团活动'; +CREATE TABLE IF NOT EXISTS "promotion_combination_record" +( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "activity_id" bigint NOT NULL, + "combination_price" int NOT NULL, + "spu_id" bigint NOT NULL, + "spu_name" varchar NOT NULL, + "pic_url" varchar, + "sku_id" bigint NOT NULL, + "count" int NOT NULL, + "user_id" bigint NOT NULL, + "nickname" varchar, + "avatar" varchar, + "head_id" bigint NOT NULL, + "status" int NOT NULL, + "order_id" bigint NOT NULL, + "user_size" int NOT NULL, + "user_count" int NOT NULL, + "virtual_group" bit NOT NULL, + "expire_time" datetime, + "start_time" datetime, + "end_time" datetime, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '拼团记录'; + CREATE TABLE IF NOT EXISTS "promotion_article_category" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, diff --git a/yudao-module-mall/yudao-module-trade-server/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java b/yudao-module-mall/yudao-module-trade-server/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java index 0e2380a46..3842bb424 100644 --- a/yudao-module-mall/yudao-module-trade-server/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java +++ b/yudao-module-mall/yudao-module-trade-server/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.trade.convert.order; import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; @@ -268,8 +269,8 @@ public interface TradeOrderConvert { .setTitle(StrUtil.format("{}成功购买{}", user.getNickname(), item.getSpuName())); if (BooleanUtil.isTrue(spu.getSubCommissionType())) { // 特殊:单独设置的佣金需要乘以购买数量。关联 https://gitee.com/yudaocode/yudao-mall-uniapp/issues/ICY7SJ - bo.setFirstFixedPrice(sku.getFirstBrokeragePrice() * item.getCount()) - .setSecondFixedPrice(sku.getSecondBrokeragePrice() * item.getCount()); + bo.setFirstFixedPrice(ObjectUtil.defaultIfNull(sku.getFirstBrokeragePrice(), 0) * item.getCount()) + .setSecondFixedPrice(ObjectUtil.defaultIfNull(sku.getSecondBrokeragePrice(), 0) * item.getCount()); } return bo; } diff --git a/yudao-module-mall/yudao-module-trade-server/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageRecordServiceImpl.java b/yudao-module-mall/yudao-module-trade-server/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageRecordServiceImpl.java index 5d97d70f8..845fea78c 100644 --- a/yudao-module-mall/yudao-module-trade-server/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageRecordServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-server/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageRecordServiceImpl.java @@ -38,7 +38,6 @@ import java.util.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getMaxValue; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getMinValue; -import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId; import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.BROKERAGE_WITHDRAW_USER_BALANCE_NOT_ENOUGH; /** @@ -143,7 +142,7 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService { int calculatePrice(Integer basePrice, Integer percent, Integer fixedPrice) { // 1. 优先使用固定佣金 if (fixedPrice != null && fixedPrice >= 0) { - return ObjectUtil.defaultIfNull(fixedPrice, 0); + return fixedPrice; } // 2. 根据比例计算佣金 if (basePrice != null && basePrice > 0 && percent != null && percent > 0) { @@ -329,32 +328,34 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService { return respVO; } // 2.2 校验用户是否有分销资格 - respVO.setEnabled(brokerageUserService.getUserBrokerageEnabled(getLoginUserId())); + respVO.setEnabled(brokerageUserService.getUserBrokerageEnabled(userId)); if (BooleanUtil.isFalse(respVO.getEnabled())) { return respVO; } // 2.3 校验商品是否存在 - ProductSpuRespDTO spu = productSpuApi.getSpu(spuId).getCheckedData(); + ProductSpuRespDTO spu = productSpuApi.getSpu(spuId); if (spu == null) { return respVO; } - // 3.1 商品单独分佣模式 - Integer fixedMinPrice = 0; - Integer fixedMaxPrice = 0; - Integer spuMinPrice = 0; - Integer spuMaxPrice = 0; - List skuList = productSkuApi.getSkuListBySpuId(ListUtil.of(spuId)).getCheckedData(); + // 3.1 获取商品 SKU 列表 + List skuList = productSkuApi.getSkuListBySpuId(ListUtil.of(spuId)); if (BooleanUtil.isTrue(spu.getSubCommissionType())) { - fixedMinPrice = getMinValue(skuList, ProductSkuRespDTO::getFirstBrokeragePrice); - fixedMaxPrice = getMaxValue(skuList, ProductSkuRespDTO::getFirstBrokeragePrice); - // 3.2 全局分佣模式(根据商品价格比例计算) + // 3.2.1 商品独立分销模式:直接取 SKU 固定佣金 + // 注意:固定佣金允许为 0,表示商家主动设为零佣金;为空时,也按 0 处理 + Integer fixedMinPrice = getMinValue(skuList, + sku -> ObjectUtil.defaultIfNull(sku.getFirstBrokeragePrice(), 0)); + Integer fixedMaxPrice = getMaxValue(skuList, + sku -> ObjectUtil.defaultIfNull(sku.getFirstBrokeragePrice(), 0)); + respVO.setBrokerageMinPrice(calculatePrice(null, tradeConfig.getBrokerageFirstPercent(), fixedMinPrice)) + .setBrokerageMaxPrice(calculatePrice(null, tradeConfig.getBrokerageFirstPercent(), fixedMaxPrice)); } else { - spuMinPrice = getMinValue(skuList, ProductSkuRespDTO::getPrice); - spuMaxPrice = getMaxValue(skuList, ProductSkuRespDTO::getPrice); + // 3.2.2 全局比例模式:固定佣金传 null,避免被默认值 0 提前拦截 + Integer spuMinPrice = getMinValue(skuList, ProductSkuRespDTO::getPrice); + Integer spuMaxPrice = getMaxValue(skuList, ProductSkuRespDTO::getPrice); + respVO.setBrokerageMinPrice(calculatePrice(spuMinPrice, tradeConfig.getBrokerageFirstPercent(), null)) + .setBrokerageMaxPrice(calculatePrice(spuMaxPrice, tradeConfig.getBrokerageFirstPercent(), null)); } - respVO.setBrokerageMinPrice(calculatePrice(spuMinPrice, tradeConfig.getBrokerageFirstPercent(), fixedMinPrice)); - respVO.setBrokerageMaxPrice(calculatePrice(spuMaxPrice, tradeConfig.getBrokerageFirstPercent(), fixedMaxPrice)); return respVO; } diff --git a/yudao-module-mall/yudao-module-trade-server/src/test/resources/sql/create_tables.sql b/yudao-module-mall/yudao-module-trade-server/src/test/resources/sql/create_tables.sql index 1d7ed24ee..61ed05809 100644 --- a/yudao-module-mall/yudao-module-trade-server/src/test/resources/sql/create_tables.sql +++ b/yudao-module-mall/yudao-module-trade-server/src/test/resources/sql/create_tables.sql @@ -173,7 +173,7 @@ CREATE TABLE IF NOT EXISTS "trade_brokerage_user" ) COMMENT '分销用户'; CREATE TABLE IF NOT EXISTS "trade_brokerage_record" ( - "id" int NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "user_id" bigint NOT NULL, "biz_id" varchar NOT NULL, "biz_type" varchar NOT NULL, @@ -184,6 +184,8 @@ CREATE TABLE IF NOT EXISTS "trade_brokerage_record" "status" varchar NOT NULL, "frozen_days" int NOT NULL, "unfreeze_time" varchar, + "source_user_level" int, + "source_user_id" bigint, "creator" varchar DEFAULT '', "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar DEFAULT '', @@ -232,4 +234,4 @@ CREATE TABLE IF NOT EXISTS "trade_delivery_express" "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") -) COMMENT '佣金提现'; \ No newline at end of file +) COMMENT '佣金提现'; diff --git a/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/ErrorCodeConstants.java b/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/ErrorCodeConstants.java index ee970a54c..c28fcfcf4 100644 --- a/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/ErrorCodeConstants.java +++ b/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/ErrorCodeConstants.java @@ -14,6 +14,7 @@ public interface ErrorCodeConstants { ErrorCode USER_MOBILE_NOT_EXISTS = new ErrorCode(1_004_001_001, "手机号未注册用户"); ErrorCode USER_MOBILE_USED = new ErrorCode(1_004_001_002, "修改手机失败,该手机号({})已经被使用"); ErrorCode USER_POINT_NOT_ENOUGH = new ErrorCode(1_004_001_003, "用户积分余额不足"); + ErrorCode USER_EMAIL_USED = new ErrorCode(1_004_001_004, "修改邮箱失败,该邮箱({})已经被使用"); // ========== AUTH 模块 1-004-003-000 ========== ErrorCode AUTH_LOGIN_BAD_CREDENTIALS = new ErrorCode(1_004_003_000, "登录失败,账号密码不正确"); diff --git a/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserBaseVO.java b/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserBaseVO.java index b24060261..01e4dd366 100644 --- a/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserBaseVO.java +++ b/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserBaseVO.java @@ -5,7 +5,9 @@ import lombok.Data; import org.hibernate.validator.constraints.URL; import org.springframework.format.annotation.DateTimeFormat; +import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import java.time.LocalDateTime; import java.util.List; @@ -26,6 +28,11 @@ public class MemberUserBaseVO { @NotNull(message = "状态不能为空") private Byte status; + @Schema(description = "邮箱", example = "member@iocoder.cn") + @Email(message = "邮箱格式不正确") + @Size(max = 50, message = "邮箱长度不能超过 50 个字符") + private String email; + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") @NotNull(message = "用户昵称不能为空") private String nickname; diff --git a/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserPageReqVO.java b/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserPageReqVO.java index abb94285e..299e6e253 100644 --- a/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserPageReqVO.java +++ b/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserPageReqVO.java @@ -21,6 +21,9 @@ public class MemberUserPageReqVO extends PageParam { @Schema(description = "手机号", example = "15601691300") private String mobile; + @Schema(description = "邮箱", example = "member@iocoder.cn") + private String email; + @Schema(description = "用户昵称", example = "李四") private String nickname; diff --git a/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/controller/app/user/vo/AppMemberUserInfoRespVO.java b/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/controller/app/user/vo/AppMemberUserInfoRespVO.java index 72e4fa3fa..fd9b90749 100644 --- a/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/controller/app/user/vo/AppMemberUserInfoRespVO.java +++ b/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/controller/app/user/vo/AppMemberUserInfoRespVO.java @@ -23,6 +23,9 @@ public class AppMemberUserInfoRespVO { @Schema(description = "用户手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300") private String mobile; + @Schema(description = "邮箱", example = "member@iocoder.cn") + private String email; + @Schema(description = "用户性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer sex; diff --git a/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/controller/app/user/vo/AppMemberUserUpdateReqVO.java b/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/controller/app/user/vo/AppMemberUserUpdateReqVO.java index cca08e926..4ddcdbdc7 100644 --- a/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/controller/app/user/vo/AppMemberUserUpdateReqVO.java +++ b/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/controller/app/user/vo/AppMemberUserUpdateReqVO.java @@ -1,8 +1,8 @@ package cn.iocoder.yudao.module.member.controller.app.user.vo; -import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.system.enums.common.SexEnum; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Size; import lombok.Data; import org.hibernate.validator.constraints.URL; @@ -17,6 +17,11 @@ public class AppMemberUserUpdateReqVO { @URL(message = "头像必须是 URL 格式") private String avatar; + @Schema(description = "邮箱", example = "member@iocoder.cn") + @Email(message = "邮箱格式不正确") + @Size(max = 50, message = "邮箱长度不能超过 50 个字符") + private String email; + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer sex; diff --git a/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/user/MemberUserDO.java b/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/user/MemberUserDO.java index 97ddc191d..11954c0ad 100644 --- a/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/user/MemberUserDO.java +++ b/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/user/MemberUserDO.java @@ -45,6 +45,10 @@ public class MemberUserDO extends TenantBaseDO { * 手机 */ private String mobile; + /** + * 邮箱 + */ + private String email; /** * 加密后的密码 * diff --git a/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/user/MemberUserMapper.java b/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/user/MemberUserMapper.java index 3f871020c..6200bf125 100644 --- a/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/user/MemberUserMapper.java +++ b/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/user/MemberUserMapper.java @@ -26,6 +26,10 @@ public interface MemberUserMapper extends BaseMapperX { return selectOne(MemberUserDO::getMobile, mobile); } + default MemberUserDO selectByEmail(String email) { + return selectOne(MemberUserDO::getEmail, email); + } + default List selectListByNicknameLike(String nickname) { return selectList(new LambdaQueryWrapperX() .likeIfPresent(MemberUserDO::getNickname, nickname)); @@ -42,6 +46,7 @@ public interface MemberUserMapper extends BaseMapperX { // 分页查询 return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(MemberUserDO::getMobile, reqVO.getMobile()) + .likeIfPresent(MemberUserDO::getEmail, reqVO.getEmail()) .betweenIfPresent(MemberUserDO::getLoginDate, reqVO.getLoginDate()) .likeIfPresent(MemberUserDO::getNickname, reqVO.getNickname()) .betweenIfPresent(MemberUserDO::getCreateTime, reqVO.getCreateTime()) diff --git a/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java b/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java index 7e4522236..7cfbeb569 100644 --- a/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java +++ b/yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java @@ -142,6 +142,12 @@ public class MemberUserServiceImpl implements MemberUserService { @Override public void updateUser(Long userId, AppMemberUserUpdateReqVO reqVO) { + // 1.1 检测用户是否存在 + validateUserExists(userId); + // 1.2 校验手机是否已经被绑定 + validateEmailUnique(userId, reqVO.getEmail()); + + // 2. 更新用户 MemberUserDO updateObj = BeanUtils.toBean(reqVO, MemberUserDO.class).setId(userId); memberUserMapper.updateById(updateObj); } @@ -158,11 +164,11 @@ public class MemberUserServiceImpl implements MemberUserService { // 补充说明:从安全性来说,老手机也校验 oldCode 验证码会更安全。但是由于 uni-app 商城界面暂时没做,所以这里不强制校验 if (StrUtil.isNotEmpty(reqVO.getOldCode())) { smsCodeApi.useSmsCode(new SmsCodeUseReqDTO().setMobile(user.getMobile()).setCode(reqVO.getOldCode()) - .setScene(SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene()).setUsedIp(getClientIP())).checkError(); + .setScene(SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene()).setUsedIp(getClientIP())); } // 2.2 使用新验证码 smsCodeApi.useSmsCode(new SmsCodeUseReqDTO().setMobile(reqVO.getMobile()).setCode(reqVO.getCode()) - .setScene(SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene()).setUsedIp(getClientIP())).checkError(); + .setScene(SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene()).setUsedIp(getClientIP())); // 3. 更新用户手机 memberUserMapper.updateById(MemberUserDO.builder().id(userId).mobile(reqVO.getMobile()).build()); @@ -172,7 +178,7 @@ public class MemberUserServiceImpl implements MemberUserService { public void updateUserMobileByWeixin(Long userId, AppMemberUserUpdateMobileByWeixinReqVO reqVO) { // 1.1 获得对应的手机号信息 SocialWxPhoneNumberInfoRespDTO phoneNumberInfo = socialClientApi.getWxMaPhoneNumberInfo( - UserTypeEnum.MEMBER.getValue(), reqVO.getCode()).getCheckedData(); + UserTypeEnum.MEMBER.getValue(), reqVO.getCode()); Assert.notNull(phoneNumberInfo, "获得手机信息失败,结果为空"); // 1.2 校验新手机是否已经被绑定 validateMobileUnique(userId, phoneNumberInfo.getPhoneNumber()); @@ -187,7 +193,7 @@ public class MemberUserServiceImpl implements MemberUserService { MemberUserDO user = validateUserExists(userId); // 校验验证码 smsCodeApi.useSmsCode(new SmsCodeUseReqDTO().setMobile(user.getMobile()).setCode(reqVO.getCode()) - .setScene(SmsSceneEnum.MEMBER_UPDATE_PASSWORD.getScene()).setUsedIp(getClientIP())).checkError(); + .setScene(SmsSceneEnum.MEMBER_UPDATE_PASSWORD.getScene()).setUsedIp(getClientIP())); // 更新用户密码 memberUserMapper.updateById(MemberUserDO.builder().id(userId) @@ -201,7 +207,7 @@ public class MemberUserServiceImpl implements MemberUserService { // 使用验证码 smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.MEMBER_RESET_PASSWORD, - getClientIP())).checkError(); + getClientIP())); // 更新密码 memberUserMapper.updateById(MemberUserDO.builder().id(user.getId()) @@ -238,6 +244,8 @@ public class MemberUserServiceImpl implements MemberUserService { validateUserExists(updateReqVO.getId()); // 校验手机唯一 validateMobileUnique(updateReqVO.getId(), updateReqVO.getMobile()); + // 校验邮箱唯一 + validateEmailUnique(updateReqVO.getId(), updateReqVO.getEmail()); // 更新 MemberUserDO updateObj = MemberUserConvert.INSTANCE.convert(updateReqVO); @@ -274,6 +282,24 @@ public class MemberUserServiceImpl implements MemberUserService { } } + @VisibleForTesting + void validateEmailUnique(Long id, String email) { + if (StrUtil.isBlank(email)) { + return; + } + MemberUserDO user = memberUserMapper.selectByEmail(email); + if (user == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的用户 + if (id == null) { + throw exception(USER_EMAIL_USED, email); + } + if (!user.getId().equals(id)) { + throw exception(USER_EMAIL_USED, email); + } + } + @Override public PageResult getUserPage(MemberUserPageReqVO pageReqVO) { return memberUserMapper.selectPage(pageReqVO); diff --git a/yudao-module-member/yudao-module-member-server/src/test/resources/sql/create_tables.sql b/yudao-module-member/yudao-module-member-server/src/test/resources/sql/create_tables.sql index 782a81810..9775a30e2 100644 --- a/yudao-module-member/yudao-module-member-server/src/test/resources/sql/create_tables.sql +++ b/yudao-module-member/yudao-module-member-server/src/test/resources/sql/create_tables.sql @@ -11,6 +11,7 @@ CREATE TABLE IF NOT EXISTS "member_user" "avatar" varchar(255) NOT NULL DEFAULT '' COMMENT '头像', "status" tinyint NOT NULL COMMENT '状态', "mobile" varchar(11) NOT NULL COMMENT '手机号', + "email" varchar(50) NULL COMMENT '邮箱', "password" varchar(100) NOT NULL DEFAULT '' COMMENT '密码', "register_ip" varchar(32) NOT NULL COMMENT '注册 IP', "login_ip" varchar(50) NULL DEFAULT '' COMMENT '最后登录IP', diff --git a/yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/andon/MesProAndonConfigController.java b/yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/andon/MesProAndonConfigController.java index e4d9df317..3b803e4a8 100644 --- a/yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/andon/MesProAndonConfigController.java +++ b/yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/andon/MesProAndonConfigController.java @@ -11,6 +11,8 @@ import cn.iocoder.yudao.module.mes.controller.admin.pro.andon.vo.config.MesProAn import cn.iocoder.yudao.module.mes.controller.admin.pro.andon.vo.config.MesProAndonConfigSaveReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.andon.MesProAndonConfigDO; import cn.iocoder.yudao.module.mes.service.pro.andon.MesProAndonConfigService; +import cn.iocoder.yudao.module.system.api.permission.RoleApi; +import cn.iocoder.yudao.module.system.api.permission.dto.RoleRespDTO; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import io.swagger.v3.oas.annotations.Operation; @@ -40,6 +42,8 @@ public class MesProAndonConfigController { @Resource private AdminUserApi adminUserApi; + @Resource + private RoleApi roleApi; @PostMapping("/create") @Operation(summary = "创建安灯呼叫配置") @@ -102,10 +106,15 @@ public class MesProAndonConfigController { // 1. 批量获取关联数据 Map userMap = adminUserApi.getUserMap( convertSet(list, MesProAndonConfigDO::getHandlerUserId)); + Map roleMap = roleApi.getRoleMap( + convertSet(list, MesProAndonConfigDO::getHandlerRoleId)); // 2. 拼接 VO - return BeanUtils.toBean(list, MesProAndonConfigRespVO.class, vo -> - MapUtils.findAndThen(userMap, vo.getHandlerUserId(), - user -> vo.setHandlerUserNickname(user.getNickname()))); + return BeanUtils.toBean(list, MesProAndonConfigRespVO.class, vo -> { + MapUtils.findAndThen(userMap, vo.getHandlerUserId(), + user -> vo.setHandlerUserNickname(user.getNickname())); + MapUtils.findAndThen(roleMap, vo.getHandlerRoleId(), + role -> vo.setHandlerRoleName(role.getName())); + }); } } diff --git a/yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/andon/vo/config/MesProAndonConfigRespVO.java b/yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/andon/vo/config/MesProAndonConfigRespVO.java index ed1e54130..dad699628 100644 --- a/yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/andon/vo/config/MesProAndonConfigRespVO.java +++ b/yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/andon/vo/config/MesProAndonConfigRespVO.java @@ -21,6 +21,9 @@ public class MesProAndonConfigRespVO { @Schema(description = "处置人角色编号", example = "10") private Long handlerRoleId; + @Schema(description = "处置人角色名称", example = "生产经理") + private String handlerRoleName; + @Schema(description = "处置人编号", example = "100") private Long handlerUserId; diff --git a/yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/defectrecord/MesQcDefectRecordController.java b/yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/defectrecord/MesQcDefectRecordController.java index 604b395d3..898d750cf 100644 --- a/yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/defectrecord/MesQcDefectRecordController.java +++ b/yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/defectrecord/MesQcDefectRecordController.java @@ -52,6 +52,15 @@ public class MesQcDefectRecordController { return success(true); } + @GetMapping("/get") + @Operation(summary = "获得质检缺陷记录") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('mes:qc-defect:query')") + public CommonResult getDefectRecord(@RequestParam("id") Long id) { + MesQcDefectRecordDO record = defectRecordService.getDefectRecord(id); + return success(BeanUtils.toBean(record, MesQcDefectRecordRespVO.class)); + } + @GetMapping("/page") @Operation(summary = "获得质检缺陷记录分页") @PreAuthorize("@ss.hasPermission('mes:qc-defect:query')") diff --git a/yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/qc/defectrecord/MesQcDefectRecordService.java b/yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/qc/defectrecord/MesQcDefectRecordService.java index 3d0acce18..2a9f8361e 100644 --- a/yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/qc/defectrecord/MesQcDefectRecordService.java +++ b/yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/qc/defectrecord/MesQcDefectRecordService.java @@ -35,6 +35,14 @@ public interface MesQcDefectRecordService { */ void deleteDefectRecord(Long id); + /** + * 获得质检缺陷记录 + * + * @param id 编号 + * @return 质检缺陷记录 + */ + MesQcDefectRecordDO getDefectRecord(Long id); + /** * 获得质检缺陷记录分页 * diff --git a/yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/qc/defectrecord/MesQcDefectRecordServiceImpl.java b/yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/qc/defectrecord/MesQcDefectRecordServiceImpl.java index 709528247..c0070803e 100644 --- a/yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/qc/defectrecord/MesQcDefectRecordServiceImpl.java +++ b/yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/qc/defectrecord/MesQcDefectRecordServiceImpl.java @@ -45,21 +45,18 @@ public class MesQcDefectRecordServiceImpl implements MesQcDefectRecordService { @Resource @Lazy private MesQcIqcLineService iqcLineService; - @Resource @Lazy private MesQcIpqcService ipqcService; @Resource @Lazy private MesQcIpqcLineService ipqcLineService; - @Resource @Lazy private MesQcOqcService oqcService; @Resource @Lazy private MesQcOqcLineService oqcLineService; - @Resource @Lazy private MesQcRqcService rqcService; @@ -109,6 +106,11 @@ public class MesQcDefectRecordServiceImpl implements MesQcDefectRecordService { recalculateDefectStats(record.getQcType(), record.getQcId()); } + @Override + public MesQcDefectRecordDO getDefectRecord(Long id) { + return defectRecordMapper.selectById(id); + } + @Override public PageResult getDefectRecordPage(MesQcDefectRecordPageReqVO pageReqVO) { return defectRecordMapper.selectPage(pageReqVO); diff --git a/yudao-module-pay/yudao-module-pay-server/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java b/yudao-module-pay/yudao-module-pay-server/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java index 3b54862a7..ab986efc5 100644 --- a/yudao-module-pay/yudao-module-pay-server/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java +++ b/yudao-module-pay/yudao-module-pay-server/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java @@ -74,6 +74,9 @@ public abstract class AbstractWxPayClient extends AbstractPayClient> getSimpleTemplateList() { - List list = mailTempleService.getMailTemplateList(); + @GetMapping({"/list-all-simple", "/simple-list"}) + @Operation(summary = "获得邮件模版精简列表", description = "只包含被开启的邮件模版,主要用于前端的下拉选项") + public CommonResult> getSimpleMailTemplateList() { + List list = mailTempleService.getMailTemplateListByStatus( + CommonStatusEnum.ENABLE.getStatus()); return success(BeanUtils.toBean(list, MailTemplateSimpleRespVO.class)); } diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/template/MailTemplateSimpleRespVO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/template/MailTemplateSimpleRespVO.java index 29833ffb7..a2003aab7 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/template/MailTemplateSimpleRespVO.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/template/MailTemplateSimpleRespVO.java @@ -13,4 +13,7 @@ public class MailTemplateSimpleRespVO { @Schema(description = "模版名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "哒哒哒") private String name; + @Schema(description = "模版编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "mail_001") + private String code; + } diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notify/NotifyTemplateController.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notify/NotifyTemplateController.java index a4a00034c..0aed50146 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notify/NotifyTemplateController.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notify/NotifyTemplateController.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.system.controller.admin.notify; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; @@ -8,6 +9,7 @@ import cn.iocoder.yudao.module.system.controller.admin.notify.vo.template.Notify import cn.iocoder.yudao.module.system.controller.admin.notify.vo.template.NotifyTemplateRespVO; import cn.iocoder.yudao.module.system.controller.admin.notify.vo.template.NotifyTemplateSaveReqVO; import cn.iocoder.yudao.module.system.controller.admin.notify.vo.template.NotifyTemplateSendReqVO; +import cn.iocoder.yudao.module.system.controller.admin.notify.vo.template.NotifyTemplateSimpleRespVO; import cn.iocoder.yudao.module.system.dal.dataobject.notify.NotifyTemplateDO; import cn.iocoder.yudao.module.system.service.notify.NotifySendService; import cn.iocoder.yudao.module.system.service.notify.NotifyTemplateService; @@ -86,6 +88,14 @@ public class NotifyTemplateController { return success(BeanUtils.toBean(pageResult, NotifyTemplateRespVO.class)); } + @GetMapping({"/list-all-simple", "/simple-list"}) + @Operation(summary = "获得站内信模版精简列表", description = "只包含被开启的站内信模版,主要用于前端的下拉选项") + public CommonResult> getSimpleNotifyTemplateList() { + List list = notifyTemplateService.getNotifyTemplateListByStatus( + CommonStatusEnum.ENABLE.getStatus()); + return success(BeanUtils.toBean(list, NotifyTemplateSimpleRespVO.class)); + } + @PostMapping("/send-notify") @Operation(summary = "发送站内信") @PreAuthorize("@ss.hasPermission('system:notify-template:send-notify')") diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notify/vo/template/NotifyTemplateSimpleRespVO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notify/vo/template/NotifyTemplateSimpleRespVO.java new file mode 100644 index 000000000..b48e6ca08 --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notify/vo/template/NotifyTemplateSimpleRespVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.system.controller.admin.notify.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 站内信模版的精简 Response VO") +@Data +public class NotifyTemplateSimpleRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "模版名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "系统通知") + private String name; + + @Schema(description = "模版编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "notify_001") + private String code; + +} diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsTemplateController.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsTemplateController.java index 8ffd4bb5c..0bd3bcc4a 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsTemplateController.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsTemplateController.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.system.controller.admin.sms; import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageResult; @@ -10,6 +11,7 @@ import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTempla import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateRespVO; import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateSaveReqVO; import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateSendReqVO; +import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateSimpleRespVO; import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO; import cn.iocoder.yudao.module.system.service.sms.SmsSendService; import cn.iocoder.yudao.module.system.service.sms.SmsTemplateService; @@ -88,6 +90,14 @@ public class SmsTemplateController { return success(BeanUtils.toBean(pageResult, SmsTemplateRespVO.class)); } + @GetMapping({"/list-all-simple", "/simple-list"}) + @Operation(summary = "获得短信模板精简列表", description = "只包含被开启的短信模板,主要用于前端的下拉选项") + public CommonResult> getSimpleSmsTemplateList() { + List list = smsTemplateService.getSmsTemplateListByStatus( + CommonStatusEnum.ENABLE.getStatus()); + return success(BeanUtils.toBean(list, SmsTemplateSimpleRespVO.class)); + } + @GetMapping("/export-excel") @Operation(summary = "导出短信模板 Excel") @PreAuthorize("@ss.hasPermission('system:sms-template:export')") diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateSimpleRespVO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateSimpleRespVO.java new file mode 100644 index 000000000..44cb8824d --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateSimpleRespVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.system.controller.admin.sms.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 短信模板的精简 Response VO") +@Data +public class SmsTemplateSimpleRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "验证码") + private String name; + + @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "sms_001") + private String code; + +} diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/mail/MailTemplateMapper.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/mail/MailTemplateMapper.java index 1730e58f9..24911dfc8 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/mail/MailTemplateMapper.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/mail/MailTemplateMapper.java @@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.system.controller.admin.mail.vo.template.MailTemp import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailTemplateDO; import org.apache.ibatis.annotations.Mapper; +import java.util.List; + @Mapper public interface MailTemplateMapper extends BaseMapperX { @@ -28,4 +30,8 @@ public interface MailTemplateMapper extends BaseMapperX { return selectOne(MailTemplateDO::getCode, code); } + default List selectListByStatus(Integer status) { + return selectList(MailTemplateDO::getStatus, status); + } + } diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/notify/NotifyTemplateMapper.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/notify/NotifyTemplateMapper.java index 1fcb8ee0d..d5ea599cd 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/notify/NotifyTemplateMapper.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/notify/NotifyTemplateMapper.java @@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.system.controller.admin.notify.vo.template.Notify import cn.iocoder.yudao.module.system.dal.dataobject.notify.NotifyTemplateDO; import org.apache.ibatis.annotations.Mapper; +import java.util.List; + @Mapper public interface NotifyTemplateMapper extends BaseMapperX { @@ -23,4 +25,8 @@ public interface NotifyTemplateMapper extends BaseMapperX { .orderByDesc(NotifyTemplateDO::getId)); } + default List selectListByStatus(Integer status) { + return selectList(NotifyTemplateDO::getStatus, status); + } + } diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsTemplateMapper.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsTemplateMapper.java index a9a1ebb99..af695976c 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsTemplateMapper.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsTemplateMapper.java @@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTempla import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO; import org.apache.ibatis.annotations.Mapper; +import java.util.List; + @Mapper public interface SmsTemplateMapper extends BaseMapperX { @@ -30,4 +32,8 @@ public interface SmsTemplateMapper extends BaseMapperX { return selectCount(SmsTemplateDO::getChannelId, channelId); } + default List selectListByStatus(Integer status) { + return selectList(SmsTemplateDO::getStatus, status); + } + } diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateService.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateService.java index 0ee9e2b25..49c846fda 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateService.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateService.java @@ -69,6 +69,14 @@ public interface MailTemplateService { */ List getMailTemplateList(); + /** + * 获取指定状态的邮件模版数组 + * + * @param status 状态 + * @return 邮件模版数组 + */ + List getMailTemplateListByStatus(Integer status); + /** * 从缓存中获取邮件模版 * diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImpl.java index 2ee46dbf7..2c3f4131e 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImpl.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImpl.java @@ -113,7 +113,9 @@ public class MailTemplateServiceImpl implements MailTemplateService { } @Override - public MailTemplateDO getMailTemplate(Long id) {return mailTemplateMapper.selectById(id);} + public MailTemplateDO getMailTemplate(Long id) { + return mailTemplateMapper.selectById(id); + } @Override @Cacheable(value = RedisKeyConstants.MAIL_TEMPLATE, key = "#code", unless = "#result == null") @@ -127,7 +129,14 @@ public class MailTemplateServiceImpl implements MailTemplateService { } @Override - public List getMailTemplateList() {return mailTemplateMapper.selectList();} + public List getMailTemplateList() { + return mailTemplateMapper.selectList(); + } + + @Override + public List getMailTemplateListByStatus(Integer status) { + return mailTemplateMapper.selectListByStatus(status); + } @Override public String formatMailTemplateContent(String content, Map params) { @@ -236,4 +245,4 @@ public class MailTemplateServiceImpl implements MailTemplateService { return ReUtil.findAllGroup1(PATTERN_PARAMS, content); } -} \ No newline at end of file +} diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/notify/NotifyTemplateService.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/notify/NotifyTemplateService.java index 2253ba978..0d9135bc9 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/notify/NotifyTemplateService.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/notify/NotifyTemplateService.java @@ -69,6 +69,14 @@ public interface NotifyTemplateService { */ PageResult getNotifyTemplatePage(NotifyTemplatePageReqVO pageReqVO); + /** + * 获得指定状态的站内信模版列表 + * + * @param status 状态 + * @return 站内信模版列表 + */ + List getNotifyTemplateListByStatus(Integer status); + /** * 格式化站内信内容 * diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/notify/NotifyTemplateServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/notify/NotifyTemplateServiceImpl.java index 45a832ddc..4b1b70d9f 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/notify/NotifyTemplateServiceImpl.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/notify/NotifyTemplateServiceImpl.java @@ -115,6 +115,11 @@ public class NotifyTemplateServiceImpl implements NotifyTemplateService { return notifyTemplateMapper.selectPage(pageReqVO); } + @Override + public List getNotifyTemplateListByStatus(Integer status) { + return notifyTemplateMapper.selectListByStatus(status); + } + @VisibleForTesting void validateNotifyTemplateCodeDuplicate(Long id, String code) { NotifyTemplateDO template = notifyTemplateMapper.selectByCode(code); diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateService.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateService.java index c73f70ff3..f26fe7b6e 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateService.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateService.java @@ -70,6 +70,14 @@ public interface SmsTemplateService { */ PageResult getSmsTemplatePage(SmsTemplatePageReqVO pageReqVO); + /** + * 获得指定状态的短信模板列表 + * + * @param status 状态 + * @return 短信模板列表 + */ + List getSmsTemplateListByStatus(Integer status); + /** * 获得指定短信渠道下的短信模板数量 * diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceImpl.java index 65d201797..bf1dbb398 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceImpl.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceImpl.java @@ -130,6 +130,11 @@ public class SmsTemplateServiceImpl implements SmsTemplateService { return smsTemplateMapper.selectPage(pageReqVO); } + @Override + public List getSmsTemplateListByStatus(Integer status) { + return smsTemplateMapper.selectListByStatus(status); + } + @Override public Long getSmsTemplateCountByChannelId(Long channelId) { return smsTemplateMapper.selectCountByChannelId(channelId); diff --git a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImplTest.java b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImplTest.java index fa9b709a7..af0211eda 100755 --- a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImplTest.java @@ -156,6 +156,23 @@ public class MailTemplateServiceImplTest extends BaseDbUnitTest { assertEquals(dbMailTemplate02, list.get(1)); } + @Test + public void testGetMailTemplateListByStatus() { + // mock 数据 + MailTemplateDO dbMailTemplate = randomPojo(MailTemplateDO.class, + o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); + mailTemplateMapper.insert(dbMailTemplate); + mailTemplateMapper.insert(cloneIgnoreId(dbMailTemplate, + o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + + // 调用 + List list = mailTemplateService.getMailTemplateListByStatus( + CommonStatusEnum.ENABLE.getStatus()); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbMailTemplate, list.get(0)); + } + @Test public void testGetMailTemplate() { // mock 数据 diff --git a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/notify/NotifyTemplateServiceImplTest.java b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/notify/NotifyTemplateServiceImplTest.java index ba1f98e22..6217a6ff8 100644 --- a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/notify/NotifyTemplateServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/notify/NotifyTemplateServiceImplTest.java @@ -12,6 +12,7 @@ import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; import java.util.HashMap; +import java.util.List; import java.util.Map; import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; @@ -136,6 +137,23 @@ public class NotifyTemplateServiceImplTest extends BaseDbUnitTest { assertPojoEquals(dbNotifyTemplate, pageResult.getList().get(0)); } + @Test + public void testGetNotifyTemplateListByStatus() { + // mock 数据 + NotifyTemplateDO dbNotifyTemplate = randomPojo(NotifyTemplateDO.class, + o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); + notifyTemplateMapper.insert(dbNotifyTemplate); + notifyTemplateMapper.insert(cloneIgnoreId(dbNotifyTemplate, + o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + + // 调用 + List list = notifyTemplateService.getNotifyTemplateListByStatus( + CommonStatusEnum.ENABLE.getStatus()); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbNotifyTemplate, list.get(0)); + } + @Test public void testGetNotifyTemplate() { // mock 数据 diff --git a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceImplTest.java b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceImplTest.java index 344f56684..5847b797e 100644 --- a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceImplTest.java @@ -244,6 +244,22 @@ public class SmsTemplateServiceImplTest extends BaseDbUnitTest { assertPojoEquals(dbSmsTemplate, pageResult.getList().get(0)); } + @Test + public void testGetSmsTemplateListByStatus() { + // mock 数据 + SmsTemplateDO dbSmsTemplate = randomSmsTemplateDO(o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); + smsTemplateMapper.insert(dbSmsTemplate); + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, + o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + + // 调用 + List list = smsTemplateService.getSmsTemplateListByStatus( + CommonStatusEnum.ENABLE.getStatus()); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbSmsTemplate, list.get(0)); + } + @Test public void testGetSmsTemplateCountByChannelId() { // mock 数据