diff --git a/.gitignore b/.gitignore index 09ec36308..49330ee16 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ rebel.xml application-my.yaml /yudao-ui-app/unpackage/ +**/.DS_Store diff --git a/README.md b/README.md index 935c0caad..f326bcadf 100644 --- a/README.md +++ b/README.md @@ -149,22 +149,45 @@ ### 工作流程 -| | 功能 | 描述 | -|----|-------|-----------------------------------------| -| 🚀 | 流程模型 | 配置工作流的流程模型,支持 BPMN 和仿钉钉/飞书设计器 | -| 🚀 | 流程表单 | 拖动表单元素生成相应的工作流表单,覆盖 Element UI 所有的表单组件 | -| 🚀 | 用户分组 | 自定义用户分组,可用于工作流的审批分组 | -| 🚀 | 我的流程 | 查看我发起的工作流程,支持新建、取消流程等操作,高亮流程图、审批时间线 | -| 🚀 | 待办任务 | 查看自己【未】审批的工作任务,支持通过、不通过、转派、委派、退回、加减签等操作 | -| 🚀 | 已办任务 | 查看自己【已】审批的工作任务,支持流程预测,展示未来审批人信息 | -| 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 | - ![功能图](/.image/common/bpm-feature.png) +基于 Flowable 构建,可支持信创(国产)数据库,满足中国特色流程操作: + | BPMN 设计器 | 钉钉/飞书设计器 | |------------------------------|--------------------------------| | ![](/.image/工作流设计器-bpmn.jpg) | ![](/.image/工作流设计器-simple.jpg) | +> 历经头部企业生产验证,工作流引擎须标配仿钉钉/飞书 + BPMN 双设计器!!! +> +> 前者支持轻量配置简单流程,后者实现复杂场景深度编排 + +| 功能列表 | 功能描述 | 是否完成 | +|------------|-------------------------------------------------------------------------------------|------| +| SIMPLE 设计器 | 仿钉钉/飞书设计器,支持拖拽搭建表单流程,10 分钟快速完成审批流程配置 | ✅ | +| BPMN 设计器 | 基于 BPMN 标准开发,适配复杂业务场景,满足多层级审批及流程自动化需求 | ✅ | +| 会签 | 同一个审批节点设置多个人(如 A、B、C 三人,三人会同时收到待办任务),需全部同意之后,审批才可到下一审批节点 | ✅ | +| 或签 | 同一个审批节点设置多个人,任意一个人处理后,就能进入下一个节点 | ✅ | +| 依次审批 | (顺序会签)同一个审批节点设置多个人(如 A、B、C 三人),三人按顺序依次收到待办,即 A 先审批,A 提交后 B 才能审批,需全部同意之后,审批才可到下一审批节点 | ✅ | +| 抄送 | 将审批结果通知给抄送人,同一个审批默认排重,不重复抄送给同一人 | ✅ | +| 驳回 | (退回)将审批重置发送给某节点,重新审批。可驳回至发起人、上一节点、任意节点 | ✅ | +| 转办 | A 转给其 B 审批,B 审批后,进入下一节点 | ✅ | +| 委派 | A 转给其 B 审批,B 审批后,转给 A,A 继续审批后进入下一节点 | ✅ | +| 加签 | 允许当前审批人根据需要,自行增加当前节点的审批人,支持向前、向后加签 | ✅ | +| 减签 | (取消加签)在当前审批人操作之前,减少审批人 | ✅ | +| 撤销 | (取消流程)流程发起人,可以对流程进行撤销处理 | ✅ | +| 终止 | 系统管理员,在任意节点终止流程实例 | ✅ | +| 表单权限 | 支持拖拉拽配置表单,每个审批节点可配置只读、编辑、隐藏权限 | ✅ | +| 超时审批 | 配置超时审批时间,超时后自动触发审批通过、不通过、驳回等操作 | ✅ | +| 自动提醒 | 配置提醒时间,到达时间后自动触发短信、邮箱、站内信等通知提醒,支持自定义重复提醒频次 | ✅ | +| 父子流程 | 主流程设置子流程节点,子流程节点会自动触发子流程。子流程结束后,主流程才会执行(继续往下下执行),支持同步子流程、异步子流程 | ✅ | +| 条件分支 | (排它分支)用于在流程中实现决策,即根据条件选择一个分支执行 | ✅ | +| 并行分支 | 允许将流程分成多条分支,不进行条件判断,所有分支都会执行 | ✅ | +| 包容分支 | (条件分支 + 并行分支的结合体)允许基于条件选择多条分支执行,但如果没有任何一个分支满足条件,则可以选择默认分支 | ✅ | +| 路由分支 | 根据条件选择一个分支执行(重定向到指定配置节点),也可以选择默认分支执行(继续往下执行) | ✅ | +| 触发节点 | 执行到该节点,触发 HTTP 请求、HTTP 回调、更新数据、删除数据等 | ✅ | +| 延迟节点 | 执行到该节点,审批等待一段时间再执行,支持固定时长、固定日期等 | ✅ | +| 拓展设置 | 流程前置/后置通知,节点(任务)前置、后置通知,流程报表,自动审批去重,自定流程编号、标题、摘要,流程报表等 | ✅ | + ### 支付系统 | | 功能 | 描述 | diff --git a/sql/mysql/ruoyi-vue-pro.sql b/sql/mysql/ruoyi-vue-pro.sql index b1a07fc61..bd9cd6d4a 100644 --- a/sql/mysql/ruoyi-vue-pro.sql +++ b/sql/mysql/ruoyi-vue-pro.sql @@ -11,7 +11,7 @@ Target Server Version : 80200 (8.2.0) File Encoding : 65001 - Date: 14/03/2025 22:52:31 + Date: 17/03/2025 13:14:16 */ SET NAMES utf8mb4; @@ -91,7 +91,7 @@ CREATE TABLE `infra_api_error_log` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 21417 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志'; +) ENGINE = InnoDB AUTO_INCREMENT = 21482 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志'; -- ---------------------------- -- Records of infra_api_error_log @@ -250,7 +250,7 @@ CREATE TABLE `infra_file` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1655 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表'; +) ENGINE = InnoDB AUTO_INCREMENT = 1657 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表'; -- ---------------------------- -- Records of infra_file @@ -444,7 +444,7 @@ CREATE TABLE `system_dict_data` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1694 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表'; +) ENGINE = InnoDB AUTO_INCREMENT = 3000 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表'; -- ---------------------------- -- Records of system_dict_data @@ -870,30 +870,6 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1591, 4, '七牛云', 'QINIU', 'system_sms_channel_code', 0, '', '', '', '1', '2024-08-31 08:45:03', '1', '2024-08-31 08:45:24', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1592, 3, '新人券', '3', 'promotion_coupon_take_type', 0, 'info', '', '新人注册后,自动发放', '1', '2024-09-03 11:57:16', '1', '2024-09-03 11:57:28', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1593, 5, '微信零钱', '5', 'brokerage_withdraw_type', 0, '', '', '自动打款', '1', '2024-10-13 11:06:48', '1', '2024-10-13 11:06:59', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1655, 0, '标准数据格式(JSON)', '0', 'iot_data_format', 0, 'default', '', '', '1', '2024-08-10 11:53:26', '1', '2024-09-06 14:31:02', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1656, 1, '透传/自定义', '1', 'iot_data_format', 0, 'default', '', '', '1', '2024-08-10 11:53:37', '1', '2024-09-06 14:30:54', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1657, 0, '直连设备', '0', 'iot_product_device_type', 0, 'default', '', '', '1', '2024-08-10 11:54:58', '1', '2024-09-06 21:57:01', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1658, 2, '网关设备', '2', 'iot_product_device_type', 0, 'default', '', '', '1', '2024-08-10 11:55:08', '1', '2024-09-06 21:56:46', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1659, 1, '网关子设备', '1', 'iot_product_device_type', 0, 'default', '', '', '1', '2024-08-10 11:55:20', '1', '2024-09-06 21:57:10', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1661, 1, '已发布', '1', 'iot_product_status', 0, 'success', '', '', '1', '2024-08-10 12:10:33', '1', '2024-09-06 22:06:22', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1663, 0, '开发中', '0', 'iot_product_status', 0, 'default', '', '', '1', '2024-08-10 14:19:18', '1', '2024-09-07 10:58:07', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1665, 0, '弱校验', '0', 'iot_validate_type', 0, '', '', '', '1', '2024-09-06 20:05:48', '1', '2024-09-06 22:02:44', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1666, 1, '免校验', '1', 'iot_validate_type', 0, '', '', '', '1', '2024-09-06 20:06:03', '1', '2024-09-06 22:02:51', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1667, 0, 'Wi-Fi', '0', 'iot_net_type', 0, '', '', '', '1', '2024-09-06 22:04:47', '1', '2024-09-06 22:04:47', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1668, 1, '蜂窝(2G / 3G / 4G / 5G)', '1', 'iot_net_type', 0, '', '', '', '1', '2024-09-06 22:05:14', '1', '2024-09-06 22:05:14', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1669, 2, '以太网', '2', 'iot_net_type', 0, '', '', '', '1', '2024-09-06 22:05:35', '1', '2024-09-06 22:05:35', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1670, 3, '其他', '3', 'iot_net_type', 0, '', '', '', '1', '2024-09-06 22:05:52', '1', '2024-09-06 22:05:52', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1671, 0, '自定义', '0', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:10', '1', '2024-09-06 22:26:10', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1672, 1, 'Modbus', '1', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:21', '1', '2024-09-06 22:26:21', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1673, 2, 'OPC UA', '2', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:31', '1', '2024-09-06 22:26:31', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1674, 3, 'ZigBee', '3', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:39', '1', '2024-09-06 22:26:39', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1675, 4, 'BLE', '4', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:48', '1', '2024-09-06 22:26:48', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1676, 0, '未激活', '0', 'iot_device_state', 0, '', '', '', '1', '2024-09-21 08:13:34', '1', '2025-01-28 16:09:27', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1677, 1, '在线', '1', 'iot_device_state', 0, '', '', '', '1', '2024-09-21 08:13:48', '1', '2025-01-28 16:09:26', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1678, 2, '离线', '2', 'iot_device_state', 0, '', '', '', '1', '2024-09-21 08:13:59', '1', '2025-01-28 16:09:25', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1680, 1, '属性', '1', 'iot_product_function_type', 0, '', '', '', '1', '2024-09-29 20:03:01', '1', '2024-09-29 20:09:41', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1681, 2, '服务', '2', 'iot_product_function_type', 0, '', '', '', '1', '2024-09-29 20:03:11', '1', '2024-09-29 20:08:23', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1682, 3, '事件', '3', 'iot_product_function_type', 0, '', '', '', '1', '2024-09-29 20:03:20', '1', '2024-09-29 20:08:20', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1683, 10, '字节豆包', 'DouBao', 'ai_platform', 0, '', '', '', '1', '2025-02-23 19:51:40', '1', '2025-02-23 19:52:02', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1684, 11, '腾讯混元', 'HunYuan', 'ai_platform', 0, '', '', '', '1', '2025-02-23 20:58:04', '1', '2025-02-23 20:58:04', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1685, 12, '硅基流动', 'SiliconFlow', 'ai_platform', 0, '', '', '', '1', '2025-02-24 20:19:09', '1', '2025-02-24 20:19:09', b'0'); @@ -905,6 +881,179 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1691, 6, '重排', '6', 'ai_model_type', 0, '', '', '', '1', '2025-03-03 12:28:26', '1', '2025-03-03 12:28:26', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1692, 14, 'MiniMax', 'MiniMax', 'ai_platform', 0, '', '', '', '1', '2025-03-11 20:04:51', '1', '2025-03-11 20:04:51', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1693, 15, '月之暗灭', 'Moonshot', 'ai_platform', 0, '', '', '', '1', '2025-03-11 20:05:08', '1', '2025-03-11 20:05:08', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2000, 0, '标准数据格式(JSON)', '0', 'iot_data_format', 0, 'default', '', '', '1', '2024-08-10 11:53:26', '1', '2025-03-17 09:28:16', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2001, 1, '透传/自定义', '1', 'iot_data_format', 0, 'default', '', '', '1', '2024-08-10 11:53:37', '1', '2025-03-17 09:28:19', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2002, 0, '直连设备', '0', 'iot_product_device_type', 0, 'default', '', '', '1', '2024-08-10 11:54:58', '1', '2025-03-17 09:28:22', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2003, 2, '网关设备', '2', 'iot_product_device_type', 0, 'default', '', '', '1', '2024-08-10 11:55:08', '1', '2025-03-17 09:28:28', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2004, 1, '网关子设备', '1', 'iot_product_device_type', 0, 'default', '', '', '1', '2024-08-10 11:55:20', '1', '2025-03-17 09:28:31', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2005, 1, '已发布', '1', 'iot_product_status', 0, 'success', '', '', '1', '2024-08-10 12:10:33', '1', '2025-03-17 09:28:34', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2006, 0, '开发中', '0', 'iot_product_status', 0, 'default', '', '', '1', '2024-08-10 14:19:18', '1', '2025-03-17 09:28:39', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2007, 0, '弱校验', '0', 'iot_validate_type', 0, '', '', '', '1', '2024-09-06 20:05:48', '1', '2025-03-17 09:28:41', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2008, 1, '免校验', '1', 'iot_validate_type', 0, '', '', '', '1', '2024-09-06 20:06:03', '1', '2025-03-17 09:28:44', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2009, 0, 'Wi-Fi', '0', 'iot_net_type', 0, '', '', '', '1', '2024-09-06 22:04:47', '1', '2025-03-17 09:28:47', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2010, 1, '蜂窝(2G / 3G / 4G / 5G)', '1', 'iot_net_type', 0, '', '', '', '1', '2024-09-06 22:05:14', '1', '2025-03-17 09:28:49', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2011, 2, '以太网', '2', 'iot_net_type', 0, '', '', '', '1', '2024-09-06 22:05:35', '1', '2025-03-17 09:28:51', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2012, 3, '其他', '3', 'iot_net_type', 0, '', '', '', '1', '2024-09-06 22:05:52', '1', '2025-03-17 09:28:54', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2013, 0, '自定义', '0', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:10', '1', '2025-03-17 09:28:56', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2014, 1, 'Modbus', '1', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:21', '1', '2025-03-17 09:28:58', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2015, 2, 'OPC UA', '2', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:31', '1', '2025-03-17 09:29:00', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2016, 3, 'ZigBee', '3', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:39', '1', '2025-03-17 09:29:04', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2017, 4, 'BLE', '4', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:48', '1', '2025-03-17 09:29:06', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2018, 0, '未激活', '0', 'iot_device_state', 0, '', '', '', '1', '2024-09-21 08:13:34', '1', '2025-03-17 09:29:09', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2019, 1, '在线', '1', 'iot_device_state', 0, '', '', '', '1', '2024-09-21 08:13:48', '1', '2025-03-17 09:29:12', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2020, 2, '离线', '2', 'iot_device_state', 0, '', '', '', '1', '2024-09-21 08:13:59', '1', '2025-03-17 09:29:14', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2021, 1, '属性', '1', 'iot_thing_model_type', 0, '', '', '', '1', '2024-09-29 20:03:01', '1', '2025-03-17 09:29:24', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2022, 2, '服务', '2', 'iot_thing_model_type', 0, '', '', '', '1', '2024-09-29 20:03:11', '1', '2025-03-17 09:29:27', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2023, 3, '事件', '3', 'iot_thing_model_type', 0, '', '', '', '1', '2024-09-29 20:03:20', '1', '2025-03-17 09:29:29', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2024, 1, 'JAR 部署', '0', 'iot_plugin_deploy_type', 0, '', '', '', '1', '2024-12-13 10:55:32', '1', '2025-03-17 09:29:32', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2025, 2, '独立部署', '1', 'iot_plugin_deploy_type', 0, '', '', '', '1', '2024-12-13 10:55:43', '1', '2025-03-17 09:29:34', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2026, 0, '停止', '0', 'iot_plugin_status', 0, 'danger', '', '', '1', '2024-12-13 11:07:37', '1', '2025-03-17 09:29:37', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2027, 1, '运行', '1', 'iot_plugin_status', 0, '', '', '', '1', '2024-12-13 11:07:45', '1', '2025-03-17 09:34:17', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2028, 0, '普通插件', '0', 'iot_plugin_type', 0, '', '', '', '1', '2024-12-13 11:08:32', '1', '2025-03-17 09:34:19', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2029, 1, '设备插件', '1', 'iot_plugin_type', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:34:22', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2030, 1, '升每分钟', 'L/min', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:34:24', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2031, 2, '毫克每千克', 'mg/kg', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:34:27', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2032, 3, '浊度', 'NTU', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:34:31', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2033, 4, 'PH值', 'pH', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:34:36', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2034, 5, '土壤EC值', 'dS/m', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:34:43', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2035, 6, '太阳总辐射', 'W/㎡', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:36:20', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2036, 7, '降雨量', 'mm/hour', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:36:24', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2037, 8, '乏', 'var', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:36:27', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2038, 9, '厘泊', 'cP', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:36:33', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2039, 10, '饱和度', 'aw', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37:11', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2040, 11, '个', 'pcs', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37:19', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2041, 12, '厘斯', 'cst', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37:22', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2042, 13, '巴', 'bar', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37:24', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2043, 14, '纳克每升', 'ppt', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37:27', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2044, 15, '微克每升', 'ppb', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37:31', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2045, 16, '微西每厘米', 'uS/cm', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37:34', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2046, 17, '牛顿每库仑', 'N/C', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37:38', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2047, 18, '伏特每米', 'V/m', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37:43', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2048, 19, '滴速', 'ml/min', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2049, 20, '毫米汞柱', 'mmHg', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37:48', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2050, 21, '血糖', 'mmol/L', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37:54', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2051, 22, '毫米每秒', 'mm/s', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:38:02', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2052, 23, '转每分钟', 'turn/m', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:38:07', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2053, 24, '次', 'count', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:38:09', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2054, 25, '档', 'gear', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:38:11', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2055, 26, '步', 'stepCount', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:38:13', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2056, 27, '标准立方米每小时', 'Nm3/h', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:38:15', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2057, 28, '千伏', 'kV', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:38:20', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2058, 29, '千伏安', 'kVA', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:38:24', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2060, 30, '千乏', 'kVar', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2061, 31, '微瓦每平方厘米', 'uw/cm2', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2062, 32, '只', '只', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2063, 33, '相对湿度', '%RH', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2064, 34, '立方米每秒', 'm³/s', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2065, 35, '公斤每秒', 'kg/s', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2066, 36, '转每分钟', 'r/min', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2067, 37, '吨每小时', 't/h', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2068, 38, '千卡每小时', 'KCL/h', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2069, 39, '升每秒', 'L/s', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2070, 40, '兆帕', 'Mpa', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2071, 41, '立方米每小时', 'm³/h', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2072, 42, '千乏时', 'kvarh', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2073, 43, '微克每升', 'μg/L', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2074, 44, '千卡路里', 'kcal', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2075, 45, '吉字节', 'GB', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2076, 46, '兆字节', 'MB', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2077, 47, '千字节', 'KB', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2078, 48, '字节', 'B', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2079, 49, '微克每平方分米每天', 'μg/(d㎡·d)', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2080, 50, '无', '', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2081, 51, '百万分率', 'ppm', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2082, 52, '像素', 'pixel', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2083, 53, '照度', 'Lux', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2084, 54, '重力加速度', 'grav', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2085, 55, '分贝', 'dB', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2086, 56, '百分比', '%', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2087, 57, '流明', 'lm', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2088, 58, '比特', 'bit', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2089, 59, '克每毫升', 'g/mL', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2090, 60, '克每升', 'g/L', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2091, 61, '毫克每升', 'mg/L', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2092, 62, '微克每立方米', 'μg/m³', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2093, 63, '毫克每立方米', 'mg/m³', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2094, 64, '克每立方米', 'g/m³', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2095, 65, '千克每立方米', 'kg/m³', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2096, 66, '纳法', 'nF', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2097, 67, '皮法', 'pF', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2098, 68, '微法', 'μF', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2099, 69, '法拉', 'F', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2100, 70, '欧姆', 'Ω', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2101, 71, '微安', 'μA', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2102, 72, '毫安', 'mA', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2103, 73, '千安', 'kA', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2104, 74, '安培', 'A', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2105, 75, '毫伏', 'mV', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2106, 76, '伏特', 'V', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2107, 77, '毫秒', 'ms', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2108, 78, '秒', 's', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2109, 79, '分钟', 'min', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2110, 80, '小时', 'h', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2111, 81, '日', 'day', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2112, 82, '周', 'week', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2113, 83, '月', 'month', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2114, 84, '年', 'year', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2115, 85, '节', 'kn', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2116, 86, '千米每小时', 'km/h', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2117, 87, '米每秒', 'm/s', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2118, 88, '秒', '″', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2119, 89, '分', '′', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2120, 90, '度', '°', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2121, 91, '弧度', 'rad', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2122, 92, '赫兹', 'Hz', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2123, 93, '微瓦', 'μW', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2124, 94, '毫瓦', 'mW', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2125, 95, '千瓦特', 'kW', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2126, 96, '瓦特', 'W', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2127, 97, '卡路里', 'cal', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2128, 98, '千瓦时', 'kW·h', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2129, 99, '瓦时', 'Wh', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2130, 100, '电子伏', 'eV', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2131, 101, '千焦', 'kJ', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2132, 102, '焦耳', 'J', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2133, 103, '华氏度', '℉', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2134, 104, '开尔文', 'K', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2135, 105, '吨', 't', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2136, 106, '摄氏度', '°C', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2137, 107, '毫帕', 'mPa', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2138, 108, '百帕', 'hPa', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2139, 109, '千帕', 'kPa', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2140, 110, '帕斯卡', 'Pa', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2141, 111, '毫克', 'mg', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2142, 112, '克', 'g', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2143, 113, '千克', 'kg', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2144, 114, '牛', 'N', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2145, 115, '毫升', 'mL', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2146, 116, '升', 'L', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2147, 117, '立方毫米', 'mm³', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2148, 118, '立方厘米', 'cm³', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2149, 119, '立方千米', 'km³', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2150, 120, '立方米', 'm³', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2151, 121, '公顷', 'h㎡', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2152, 122, '平方厘米', 'c㎡', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2153, 123, '平方毫米', 'm㎡', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2154, 124, '平方千米', 'k㎡', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2155, 125, '平方米', '㎡', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2156, 126, '纳米', 'nm', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2157, 127, '微米', 'μm', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2158, 128, '毫米', 'mm', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2159, 129, '厘米', 'cm', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2160, 130, '分米', 'dm', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2161, 131, '千米', 'km', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2162, 132, '米', 'm', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2163, 1, '输入', '1', 'iot_data_bridge_direction_enum', 0, 'primary', '', '', '1', '2025-03-09 12:38:24', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2164, 2, '输出', '2', 'iot_data_bridge_direction_enum', 0, 'primary', '', '', '1', '2025-03-09 12:38:36', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2165, 1, 'HTTP', '1', 'iot_data_bridge_type_enum', 0, 'primary', '', '', '1', '2025-03-09 12:39:54', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2166, 2, 'TCP', '2', 'iot_data_bridge_type_enum', 0, 'primary', '', '', '1', '2025-03-09 12:40:06', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2167, 3, 'WEBSOCKET', '3', 'iot_data_bridge_type_enum', 0, 'primary', '', '', '1', '2025-03-09 12:40:24', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2168, 10, 'MQTT', '10', 'iot_data_bridge_type_enum', 0, 'primary', '', '', '1', '2025-03-09 12:40:37', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2169, 20, 'DATABASE', '20', 'iot_data_bridge_type_enum', 0, 'primary', '', '', '1', '2025-03-09 12:41:05', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2170, 21, 'REDIS_STREAM', '21', 'iot_data_bridge_type_enum', 0, 'primary', '', '', '1', '2025-03-09 12:41:18', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2171, 30, 'ROCKETMQ', '30', 'iot_data_bridge_type_enum', 0, 'primary', '', '', '1', '2025-03-09 12:41:30', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2172, 31, 'RABBITMQ', '31', 'iot_data_bridge_type_enum', 0, 'primary', '', '', '1', '2025-03-09 12:41:47', '1', '2025-03-17 09:40:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2173, 32, 'KAFKA', '32', 'iot_data_bridge_type_enum', 0, 'primary', '', '', '1', '2025-03-09 12:41:59', '1', '2025-03-17 09:40:46', b'0'); COMMIT; -- ---------------------------- @@ -924,7 +1073,7 @@ CREATE TABLE `system_dict_type` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `deleted_time` datetime NULL DEFAULT NULL COMMENT '删除时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 641 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表'; +) ENGINE = InnoDB AUTO_INCREMENT = 2000 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表'; -- ---------------------------- -- Records of system_dict_type @@ -1023,15 +1172,21 @@ INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creat INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (627, '写作格式', 'ai_write_format', 0, '', '1', '2024-07-07 15:14:34', '1', '2024-07-07 15:14:34', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (628, 'AI 写作类型', 'ai_write_type', 0, '', '1', '2024-07-10 21:25:29', '1', '2024-07-10 21:25:29', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (629, 'BPM 流程模型类型', 'bpm_model_type', 0, '', '1', '2024-08-26 15:21:43', '1', '2024-08-26 15:21:43', b'0', '1970-01-01 00:00:00'); -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (630, 'IOT 接入网关协议', 'iot_protocol_type', 0, '', '1', '2024-09-06 22:20:17', '1', '2024-09-06 22:20:17', b'0', '1970-01-01 00:00:00'); -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (631, 'IOT 设备状态', 'iot_device_state', 0, '', '1', '2024-09-21 08:12:55', '1', '2025-01-28 16:09:42', b'0', '1970-01-01 00:00:00'); -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (632, 'IOT 物模型功能类型', 'iot_product_function_type', 0, '', '1', '2024-09-29 20:02:36', '1', '2024-09-29 20:09:26', b'0', '1970-01-01 00:00:00'); -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (634, 'IOT 数据格式', 'iot_data_format', 0, '', '1', '2024-08-10 11:52:58', '1', '2024-09-06 14:30:14', b'0', '1970-01-01 00:00:00'); -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (635, 'IOT 产品设备类型', 'iot_product_device_type', 0, '', '1', '2024-08-10 11:54:30', '1', '2024-08-10 04:06:56', b'0', '1970-01-01 00:00:00'); -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (637, 'IOT 产品状态', 'iot_product_status', 0, '', '1', '2024-08-10 12:06:09', '1', '2024-08-10 12:06:09', b'0', '1970-01-01 00:00:00'); -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (638, 'IOT 数据校验级别', 'iot_validate_type', 0, '', '1', '2024-09-06 20:05:13', '1', '2024-09-06 20:05:13', b'0', '1970-01-01 00:00:00'); -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (639, 'IOT 联网方式', 'iot_net_type', 0, '', '1', '2024-09-06 22:04:13', '1', '2024-09-06 22:04:13', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (640, 'AI 模型类型', 'ai_model_type', 0, '', '1', '2025-03-03 12:24:07', '1', '2025-03-03 12:24:07', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1000, 'IoT 数据格式', 'iot_data_format', 0, '', '1', '2024-08-10 11:52:58', '1', '2025-03-17 09:25:06', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1001, 'IoT 产品设备类型', 'iot_product_device_type', 0, '', '1', '2024-08-10 11:54:30', '1', '2025-03-17 09:25:08', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1002, 'IoT 产品状态', 'iot_product_status', 0, '', '1', '2024-08-10 12:06:09', '1', '2025-03-17 09:25:10', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1003, 'IoT 数据校验级别', 'iot_validate_type', 0, '', '1', '2024-09-06 20:05:13', '1', '2025-03-17 09:25:12', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1004, 'IoT 联网方式', 'iot_net_type', 0, '', '1', '2024-09-06 22:04:13', '1', '2025-03-17 09:25:14', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1005, 'IoT 接入网关协议', 'iot_protocol_type', 0, '', '1', '2024-09-06 22:20:17', '1', '2025-03-17 09:25:16', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1006, 'IoT 设备状态', 'iot_device_state', 0, '', '1', '2024-09-21 08:12:55', '1', '2025-03-17 09:25:19', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1007, 'IoT 物模型功能类型', 'iot_thing_model_type', 0, '', '1', '2024-09-29 20:02:36', '1', '2025-03-17 09:25:24', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1008, 'IoT 插件部署方式', 'iot_plugin_deploy_type', 0, '', '1', '2024-12-13 10:55:13', '1', '2025-03-17 09:25:27', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1009, 'IoT 插件状态', 'iot_plugin_status', 0, '', '1', '2024-12-13 11:05:34', '1', '2025-03-17 09:25:30', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1010, 'IoT 插件类型', 'iot_plugin_type', 0, '', '1', '2024-12-13 11:08:19', '1', '2025-03-17 09:25:32', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1011, 'IoT 物模型单位', 'iot_thing_model_unit', 0, '', '1', '2024-12-25 17:36:46', '1', '2025-03-17 09:25:35', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1012, 'IoT 数据桥接的方向枚举', 'iot_data_bridge_direction_enum', 0, '', '1', '2025-03-09 12:37:40', '1', '2025-03-17 09:25:39', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1013, 'IoT 数据桥梁的类型枚举', 'iot_data_bridge_type_enum', 0, '', '1', '2025-03-09 12:39:36', '1', '2025-03-17 09:25:43', b'0', '1970-01-01 00:00:00'); COMMIT; -- ---------------------------- @@ -1055,7 +1210,7 @@ CREATE TABLE `system_login_log` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 3442 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录'; +) ENGINE = InnoDB AUTO_INCREMENT = 3446 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录'; -- ---------------------------- -- Records of system_login_log @@ -1186,16 +1341,16 @@ CREATE TABLE `system_menu` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 2925 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表'; +) ENGINE = InnoDB AUTO_INCREMENT = 5000 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表'; -- ---------------------------- -- Records of system_menu -- ---------------------------- BEGIN; -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, '系统管理', '', 1, 10, 0, '/system', 'ep:tools', NULL, NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-06-18 01:19:41', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, '系统管理', '', 1, 10, 0, '/system', 'ep:tools', NULL, NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2025-03-15 21:30:27', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, '基础设施', '', 1, 20, 0, '/infra', 'ep:monitor', NULL, NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-03-01 08:28:40', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5, 'OA 示例', '', 1, 40, 1185, 'oa', 'fa:road', NULL, NULL, 0, b'1', b'1', b'1', 'admin', '2021-09-20 16:26:19', '1', '2024-02-29 12:38:13', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (100, '用户管理', 'system:user:list', 2, 1, 1, 'user', 'ep:avatar', 'system/user/index', 'SystemUser', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:02:04', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (100, '用户管理', 'system:user:list', 2, 1, 1, 'user', 'ep:avatar', 'system/user/index', 'SystemUser', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2025-03-15 21:30:41', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (101, '角色管理', '', 2, 2, 1, 'role', 'ep:user', 'system/role/index', 'SystemRole', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-05-01 18:35:29', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (102, '菜单管理', '', 2, 3, 1, 'menu', 'ep:menu', 'system/menu/index', 'SystemMenu', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:03:50', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (103, '部门管理', '', 2, 4, 1, 'dept', 'fa:address-card', 'system/dept/index', 'SystemDept', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:06:28', b'0'); @@ -1707,7 +1862,7 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2431, '回款计划更新', 'crm:receivable-plan:update', 3, 3, 2428, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:18:09', '', '2023-10-29 11:18:09', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2432, '回款计划删除', 'crm:receivable-plan:delete', 3, 4, 2428, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:18:09', '', '2023-10-29 11:18:09', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2433, '回款计划导出', 'crm:receivable-plan:export', 3, 5, 2428, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:18:09', '', '2023-10-29 11:18:09', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2435, '商城装修', '', 2, 20, 2030, 'diy-template', 'fa6-solid:brush', 'mall/promotion/diy/template/index', 'DiyTemplate', 0, b'1', b'1', b'1', '', '2023-10-29 14:19:25', '', '2023-10-29 14:19:25', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2435, '商城装修', '', 2, 20, 2030, 'diy-template', 'fa6-solid:brush', 'mall/promotion/diy/template/index', '', 0, b'1', b'1', b'1', '', '2023-10-29 14:19:25', '1', '2025-03-15 21:34:33', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2436, '装修模板', '', 2, 1, 2435, 'diy-template', 'fa6-solid:brush', 'mall/promotion/diy/template/index', 'DiyTemplate', 0, b'1', b'1', b'1', '', '2023-10-29 14:19:25', '', '2023-10-29 14:19:25', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2437, '装修模板查询', 'promotion:diy-template:query', 3, 1, 2436, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 14:19:25', '', '2023-10-29 14:19:25', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2438, '装修模板创建', 'promotion:diy-template:create', 3, 2, 2436, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 14:19:25', '', '2023-10-29 14:19:25', b'0'); @@ -2035,26 +2190,6 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2811, '积分商城活动更新', 'promotion:point-activity:update', 3, 3, 2808, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-21 05:36:42', '1', '2024-09-22 14:49:10', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2812, '积分商城活动删除', 'promotion:point-activity:delete', 3, 4, 2808, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-21 05:36:42', '1', '2024-09-22 14:49:12', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2813, '积分商城活动导出', 'promotion:point-activity:export', 3, 5, 2808, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-21 05:36:42', '1', '2024-09-22 14:49:27', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2892, 'IOT 物联网', '', 1, 500, 0, '/iot', 'fa-solid:hdd', '', '', 0, b'1', b'1', b'1', '1', '2024-08-10 09:55:29', '1', '2024-08-10 09:55:29', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2893, '设备接入', '', 1, 1, 2892, 'device', 'ep:platform', '', '', 0, b'1', b'1', b'1', '1', '2024-08-10 09:57:56', '1', '2024-10-20 18:57:43', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2894, '产品管理', '', 2, 0, 2893, 'product', '', 'iot/product/index', 'IoTProduct', 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '1', '2024-09-16 19:50:42', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2895, '产品查询', 'iot:product:query', 3, 1, 2894, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '', '2024-08-10 02:38:02', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2896, '产品创建', 'iot:product:create', 3, 2, 2894, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '', '2024-08-10 02:38:02', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2897, '产品更新', 'iot:product:update', 3, 3, 2894, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '', '2024-08-10 02:38:02', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2898, '产品删除', 'iot:product:delete', 3, 4, 2894, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '', '2024-08-10 02:38:02', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2899, '产品导出', 'iot:product:export', 3, 5, 2894, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '', '2024-08-10 02:38:02', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2900, '设备管理', '', 2, 0, 2893, 'device', '', 'iot/device/index', 'IoTDevice', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-09-16 19:50:53', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2901, '设备查询', 'iot:device:query', 3, 1, 2900, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-09-16 19:37:00', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2902, '设备创建', 'iot:device:create', 3, 2, 2900, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-09-16 19:37:09', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2903, '设备更新', 'iot:device:update', 3, 3, 2900, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-09-16 19:37:18', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2904, '设备删除', 'iot:device:delete', 3, 4, 2900, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-09-16 19:37:42', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2905, '设备导出', 'iot:device:export', 3, 5, 2900, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-09-16 19:37:49', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2906, 'IoT 产品物模型管理', '', 1, 0, 2893, 'think-model-function', '', '', '', 0, b'0', b'1', b'1', '', '2024-09-25 22:12:09', '1', '2024-09-29 20:52:12', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2907, 'IoT 产品物模型查询', 'iot:think-model-function:query', 3, 1, 2906, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-09-25 22:12:09', '', '2024-09-25 22:12:09', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2908, 'IoT 产品物模型创建', 'iot:think-model-function:create', 3, 2, 2906, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-09-25 22:12:09', '', '2024-09-25 22:12:09', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2909, 'IoT 产品物模型更新', 'iot:think-model-function:update', 3, 3, 2906, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-09-25 22:12:09', '', '2024-09-25 22:12:09', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2910, 'IoT 产品物模型删除', 'iot:think-model-function:delete', 3, 4, 2906, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-09-25 22:12:09', '', '2024-09-25 22:12:09', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2911, 'IoT 产品物模型导出', 'iot:think-model-function:export', 3, 5, 2906, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-09-25 22:12:09', '', '2024-09-25 22:12:09', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2912, '创建推广员', 'trade:brokerage-user:create', 3, 7, 2346, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-12-01 14:32:39', '1', '2024-12-01 14:32:39', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2913, '流程清理', 'bpm:model:clean', 3, 7, 1193, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-01-17 19:32:06', '1', '2025-01-17 19:32:06', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2914, '积分商城活动关闭', 'promotion:point-activity:close', 3, 6, 2808, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-01-23 20:23:34', '1', '2025-01-23 20:23:34', b'0'); @@ -2068,6 +2203,57 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2922, '工具创建', 'ai:tool:create', 3, 2, 2920, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-03-14 11:19:29', '', '2025-03-14 11:19:29', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2923, '工具更新', 'ai:tool:update', 3, 3, 2920, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-03-14 11:19:29', '', '2025-03-14 11:19:29', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2924, '工具删除', 'ai:tool:delete', 3, 4, 2920, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-03-14 11:19:29', '', '2025-03-14 11:19:29', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4000, 'IoT 物联网', '', 1, 500, 0, '/iot', 'fa-solid:hdd', '', '', 0, b'1', b'1', b'1', '1', '2024-08-10 09:55:28', '1', '2024-12-07 15:58:34', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4001, '设备接入', '', 1, 2, 4000, 'device', 'ep:platform', '', '', 0, b'1', b'1', b'1', '1', '2024-08-10 09:57:56', '1', '2025-02-27 08:39:49', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4002, '产品管理', '', 2, 2, 4001, 'product', 'fa-solid:tools', 'iot/product/product/index', 'IoTProduct', 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '1', '2024-12-07 18:47:53', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4003, '产品查询', 'iot:product:query', 3, 1, 4002, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '', '2024-12-07 15:55:00', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4004, '产品创建', 'iot:product:create', 3, 2, 4002, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '', '2024-12-07 15:55:03', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4005, '产品更新', 'iot:product:update', 3, 3, 4002, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '', '2024-12-07 15:55:05', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4006, '产品删除', 'iot:product:delete', 3, 4, 4002, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '', '2024-12-07 15:55:06', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4007, '产品导出', 'iot:product:export', 3, 5, 4002, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '', '2024-12-07 15:55:13', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4008, '设备管理', '', 2, 4, 4001, 'device', 'fa:mobile', 'iot/device/device/index', 'IoTDevice', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-12-14 11:39:30', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4009, '设备查询', 'iot:device:query', 3, 1, 4008, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-12-07 15:55:40', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4010, '设备创建', 'iot:device:create', 3, 2, 4008, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-12-07 15:55:41', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4011, '设备更新', 'iot:device:update', 3, 3, 4008, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-12-07 15:55:42', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4012, '设备删除', 'iot:device:delete', 3, 4, 4008, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-12-07 15:55:43', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4013, '设备导出', 'iot:device:export', 3, 5, 4008, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-12-07 15:55:44', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4014, '产品分类', '', 2, 1, 4001, 'product-category', 'ep:notebook', 'iot/product/category/index', 'IotProductCategory', 0, b'1', b'1', b'1', '', '2024-12-07 16:01:35', '1', '2024-12-07 16:31:52', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4015, '产品分类查询', 'iot:product-category:query', 3, 1, 4014, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-07 16:01:35', '', '2024-12-07 16:01:35', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4016, '产品分类创建', 'iot:product-category:create', 3, 2, 4014, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-07 16:01:35', '', '2024-12-07 16:01:35', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4017, '产品分类更新', 'iot:product-category:update', 3, 3, 4014, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-07 16:01:35', '', '2024-12-07 16:01:35', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4018, '产品分类删除', 'iot:product-category:delete', 3, 4, 4014, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-07 16:01:35', '', '2024-12-07 16:01:35', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4025, '插件管理', '', 2, 5, 4047, 'plugin-config', 'ep:folder-opened', 'iot/plugin/index', 'IoTPlugin', 0, b'1', b'1', b'1', '', '2024-12-09 21:25:06', '1', '2025-02-05 22:23:12', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4026, '插件查询', 'iot:plugin-config:query', 3, 1, 4025, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-09 21:25:06', '', '2025-02-05 21:23:20', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4027, '插件创建', 'iot:plugin-config:create', 3, 2, 4025, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-09 21:25:06', '', '2025-02-05 21:23:16', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4028, '插件更新', 'iot:plugin-config:update', 3, 3, 4025, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-09 21:25:06', '', '2025-02-05 21:23:12', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4029, '插件删除', 'iot:plugin-config:delete', 3, 4, 4025, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-09 21:25:06', '', '2025-02-05 21:23:09', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4030, '插件导出', 'iot:plugin-config:export', 3, 5, 4025, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-09 21:25:06', '', '2025-02-05 21:23:06', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4031, '设备分组', '', 2, 3, 4001, 'device-group', 'fa-solid:layer-group', 'iot/device/group/index', 'IotDeviceGroup', 0, b'1', b'1', b'1', '', '2024-12-14 17:08:29', '1', '2024-12-14 17:09:17', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4032, '设备分组查询', 'iot:device-group:query', 3, 1, 4031, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-14 17:08:29', '', '2024-12-14 17:08:29', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4033, '设备分组创建', 'iot:device-group:create', 3, 2, 4031, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-14 17:08:29', '', '2024-12-14 17:08:29', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4034, '设备分组更新', 'iot:device-group:update', 3, 3, 4031, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-14 17:08:29', '', '2024-12-14 17:08:29', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4035, '设备分组删除', 'iot:device-group:delete', 3, 4, 4031, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-14 17:08:29', '', '2024-12-14 17:08:29', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4036, '设备导入', 'iot:device:import', 3, 6, 4008, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-12-15 10:35:47', '1', '2024-12-15 10:35:47', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4037, '产品物模型', '', 2, 2, 4001, 'thing-model', 'ep:mostly-cloudy', 'iot/thingmodel/index', 'IoTThingModel', 0, b'0', b'0', b'0', '', '2024-12-16 17:17:50', '1', '2024-12-27 11:03:37', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4038, '产品物模型功能查询', 'iot:thing-model:query', 3, 1, 4037, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-16 17:17:51', '', '2025-03-17 09:14:54', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4039, '产品物模型功能创建', 'iot:thing-model:create', 3, 2, 4037, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-16 17:17:52', '', '2025-03-17 09:14:58', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4040, '产品物模型功能更新', 'iot:thing-model:update', 3, 3, 4037, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-16 17:17:52', '', '2025-03-17 09:15:03', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4041, '产品物模型功能删除', 'iot:thing-model:delete', 3, 4, 4037, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-16 17:17:52', '', '2025-03-17 09:15:06', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4042, '产品物模型功能导出', 'iot:thing-model:export', 3, 5, 4037, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-16 17:17:53', '', '2025-03-17 09:15:09', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4043, '设备上行', 'iot:device:upstream', 3, 7, 4008, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-01-28 04:40:16', '1', '2025-01-31 22:45:53', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4044, '设备属性查询', 'iot:device:property-query', 3, 10, 4008, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-01-28 11:52:54', '1', '2025-01-28 11:52:54', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4045, '设备日志查询', 'iot:device:log-query', 3, 11, 4008, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-01-28 11:53:22', '1', '2025-01-28 11:53:22', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4046, '设备下行', 'iot:device:downstream', 3, 8, 4008, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-01-31 22:46:11', '1', '2025-01-31 22:46:11', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4047, '运维管理', '', 1, 2, 4000, 'operations', 'fa:cog', '', '', 0, b'1', b'1', b'1', '1', '2025-02-05 22:21:37', '1', '2025-02-05 22:22:53', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4048, '规则引擎', '', 1, 3, 4000, 'rule', 'fa-solid:cogs', '', '', 0, b'1', b'1', b'1', '1', '2025-02-11 14:10:54', '1', '2025-02-11 14:10:54', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4049, '场景联动', '', 2, 1, 4048, 'scene', 'ep:link', 'iot/rule/scene/index', 'Scene', 0, b'1', b'1', b'1', '1', '2025-02-11 14:12:44', '1', '2025-02-12 10:15:36', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4050, 'IoT首页', '', 2, 1, 4000, 'home', 'ep:home-filled', 'iot/home/index', 'IotHome', 0, b'1', b'1', b'1', '1', '2025-02-27 08:39:35', '1', '2025-02-27 08:40:28', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4051, '数据桥梁', '', 2, 0, 4048, 'data-bridge', 'ep:guide', 'iot/rule/databridge/index', 'IotDataBridge', 0, b'1', b'1', b'1', '', '2025-03-09 13:47:11', '1', '2025-03-09 13:47:51', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4052, 'IoT 数据桥梁查询', 'iot:data-bridge:query', 3, 1, 4051, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-03-09 13:47:11', '', '2025-03-09 13:47:11', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4053, 'IoT 数据桥梁创建', 'iot:data-bridge:create', 3, 2, 4051, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-03-09 13:47:11', '', '2025-03-09 13:47:11', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4054, 'IoT 数据桥梁更新', 'iot:data-bridge:update', 3, 3, 4051, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-03-09 13:47:11', '', '2025-03-09 13:47:11', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4055, 'IoT 数据桥梁删除', 'iot:data-bridge:delete', 3, 4, 4051, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-03-09 13:47:12', '', '2025-03-09 13:47:12', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4056, 'IoT 数据桥梁导出', 'iot:data-bridge:export', 3, 5, 4051, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-03-09 13:47:12', '', '2025-03-09 13:47:12', b'0'); COMMIT; -- ---------------------------- @@ -2189,7 +2375,7 @@ CREATE TABLE `system_oauth2_access_token` ( PRIMARY KEY (`id`) USING BTREE, INDEX `idx_access_token`(`access_token` ASC) USING BTREE, INDEX `idx_refresh_token`(`refresh_token` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 13666 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌'; +) ENGINE = InnoDB AUTO_INCREMENT = 13787 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌'; -- ---------------------------- -- Records of system_oauth2_access_token @@ -2311,7 +2497,7 @@ CREATE TABLE `system_oauth2_refresh_token` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1732 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌'; +) ENGINE = InnoDB AUTO_INCREMENT = 1735 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌'; -- ---------------------------- -- Records of system_oauth2_refresh_token @@ -3326,7 +3512,7 @@ CREATE TABLE `system_sms_code` ( `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_mobile`(`mobile` ASC) USING BTREE COMMENT '手机号' -) ENGINE = InnoDB AUTO_INCREMENT = 646 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码'; +) ENGINE = InnoDB AUTO_INCREMENT = 649 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码'; -- ---------------------------- -- Records of system_sms_code @@ -3367,7 +3553,7 @@ CREATE TABLE `system_sms_log` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1255 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志'; +) ENGINE = InnoDB AUTO_INCREMENT = 1279 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志'; -- ---------------------------- -- Records of system_sms_log @@ -3397,7 +3583,7 @@ CREATE TABLE `system_sms_template` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信模板'; +) ENGINE = InnoDB AUTO_INCREMENT = 19 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信模板'; -- ---------------------------- -- Records of system_sms_template @@ -3416,6 +3602,7 @@ INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `cont INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (15, 1, 0, 'user-update-password', '会员用户 - 修改密码', '您的验证码{code},该验证码 5 分钟内有效,请勿泄漏于他人!', '[\"code\"]', '', 'null', 4, 'DEBUG_DING_TALK', '1', '2023-08-19 18:58:01', '1', '2023-08-19 11:34:18', b'0'); INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (16, 1, 0, 'user-reset-password', '会员用户 - 重置密码', '您的验证码{code},该验证码 5 分钟内有效,请勿泄漏于他人!', '[\"code\"]', '', 'null', 4, 'DEBUG_DING_TALK', '1', '2023-08-19 18:58:01', '1', '2023-12-02 22:35:27', b'0'); INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (17, 2, 0, 'bpm_task_timeout', '【工作流】任务审批超时', '您收到了一条超时的待办任务:{processInstanceName}-{taskName},处理链接:{detailUrl}', '[\"processInstanceName\",\"taskName\",\"detailUrl\"]', '', 'X', 4, 'DEBUG_DING_TALK', '1', '2024-08-16 21:59:15', '1', '2024-08-16 21:59:34', b'0'); +INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (18, 1, 0, 'admin-reset-password', '后台用户 - 忘记密码', '您的验证码{code},该验证码 5 分钟内有效,请勿泄漏于他人!', '[\"code\"]', '', 'null', 4, 'DEBUG_DING_TALK', '1', '2025-03-16 14:19:34', '1', '2025-03-16 14:19:45', b'0'); COMMIT; -- ---------------------------- @@ -3665,7 +3852,7 @@ CREATE TABLE `system_users` ( -- Records of system_users -- ---------------------------- BEGIN; -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1,2]', 'aoteman@126.com', '18818260277', 2, 'http://test.yudao.iocoder.cn/bf2002b38950c904243be7c825d3f82e29f25a44526583c3fde2ebdff3a87f75.png', 0, '0:0:0:0:0:0:0:1', '2025-03-13 12:44:59', 'admin', '2021-01-05 17:03:47', NULL, '2025-03-13 12:44:59', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$04$Q3WCEQJbSZ0zT/7ryYTb3OgtrhwIZXu4ah5RQ5/YQDQ7DpW7N7oNa', '芋道源码', '管理员', 103, '[1,2]', 'aoteman@126.com', '18818260277', 2, 'http://test.yudao.iocoder.cn/bf2002b38950c904243be7c825d3f82e29f25a44526583c3fde2ebdff3a87f75.png', 0, '0:0:0:0:0:0:0:1', '2025-03-16 14:20:16', 'admin', '2021-01-05 17:03:47', NULL, '2025-03-16 14:20:16', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$04$IgUse/ibRzAZ3rngCThmtemJeoh15Ux1TQ2hIMe4iwt/K3LcFHEda', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, '', 0, '0:0:0:0:0:0:0:1', '2024-11-02 14:00:46', '', '2021-01-07 09:07:17', NULL, '2024-11-02 14:00:46', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, 'yuanma', '$2a$04$fUBSmjKCPYAUmnMzOb6qE.eZCGPhHi1JmAKclODbfS/O7fHOl2bH6', '源码', NULL, 106, NULL, 'yuanma@iocoder.cn', '15601701300', 0, '', 0, '0:0:0:0:0:0:0:1', '2024-08-11 17:48:12', '', '2021-01-13 23:50:35', NULL, '2024-08-11 17:48:12', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$04$BrwaYn303hjA/6TnXqdGoOLhyHOAA0bVrAFu6.1dJKycqKUnIoRz2', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, '', 0, '0:0:0:0:0:0:0:1', '2025-01-04 10:40:49', '', '2021-01-21 02:13:53', NULL, '2025-01-04 10:40:49', b'0', 1); diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index 54252350d..d414e9415 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -32,8 +32,9 @@ 8.1.3.140 8.6.0 5.1.0 + 3.3.3 - 2.3.1 + 2.3.2 2.2.7 @@ -53,25 +54,27 @@ 1.6.3 5.8.35 6.0.0-M19 - 4.0.3 + 4.0.3 2.4.1 1.2.83 33.4.0-jre 2.14.5 3.11.1 0.1.55 - 3.1.0 + 3.1.0 2.7.0 3.0.6 4.1.116.Final 1.2.5 + 0.9.0 + 4.5.13 2.17.0 1.27.1 1.12.777 2.0.5 1.8.1 - 4.6.0 + 4.7.2.B @@ -263,6 +266,12 @@ ${kingbase.jdbc.version} + + com.taosdata.jdbc + taos-jdbcdriver + ${taos.version} + + cn.iocoder.boot @@ -276,7 +285,6 @@ yudao-spring-boot-starter-mq ${revision} - org.apache.rocketmq rocketmq-spring-boot-starter @@ -471,7 +479,7 @@ com.alibaba easyexcel - ${easyexcel.verion} + ${easyexcel.version} commons-io @@ -591,6 +599,36 @@ + + + org.pf4j + pf4j-spring + ${pf4j-spring.version} + + + org.slf4j + slf4j-log4j12 + + + + + + + io.vertx + vertx-core + ${vertx.version} + + + io.vertx + vertx-web + ${vertx.version} + + + io.vertx + vertx-mqtt + ${vertx.version} + + org.eclipse.paho diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/RpcConstants.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/RpcConstants.java new file mode 100644 index 000000000..b1c53dbfe --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/RpcConstants.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.framework.common.enums; + +/** + * RPC 相关的枚举 + * + * 虽然放在 yudao-spring-boot-starter-rpc 会相对合适,但是每个 API 模块需要使用到,所以暂时只好放在此处 + * + * @author 芋道源码 + */ +public class RpcConstants { + + /** + * RPC API 的前缀 + */ + public static final String RPC_API_PREFIX = "/rpc-api"; + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java index 9e75113d0..e1e0ac08e 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java @@ -7,13 +7,15 @@ import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpResponse; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; -import jakarta.servlet.http.HttpServletRequest; import java.net.URI; +import java.net.URLEncoder; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.Map; /** @@ -23,6 +25,16 @@ import java.util.Map; */ public class HttpUtils { + /** + * 编码 URL 参数 + * + * @param value 参数 + * @return 编码后的参数 + */ + public static String encodeUtf8(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } + @SuppressWarnings("unchecked") public static String replaceUrlQuery(String url, String key, String value) { UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset()); diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java index c928e2fcf..c2787f46f 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java @@ -1,9 +1,11 @@ package cn.iocoder.yudao.framework.common.util.number; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.StrUtil; import java.math.BigDecimal; +import java.util.List; /** * 数字的工具类,补全 {@link cn.hutool.core.util.NumberUtil} 的功能 @@ -20,6 +22,18 @@ public class NumberUtils { return StrUtil.isNotEmpty(str) ? Integer.valueOf(str) : null; } + public static boolean isAllNumber(List values) { + if (CollUtil.isEmpty(values)) { + return false; + } + for (String value : values) { + if (!NumberUtil.isNumber(value)) { + return false; + } + } + return true; + } + /** * 通过经纬度获取地球上两点之间的距离 * diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java index 12731edad..905f5d541 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java @@ -98,4 +98,8 @@ public class ServletUtils { return JakartaServletUtil.getParamMap(request); } + public static Map getHeaderMap(HttpServletRequest request) { + return JakartaServletUtil.getHeaderMap(request); + } + } diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java index 069e89db3..abb1cece8 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java @@ -97,12 +97,26 @@ public class SpringExpressionUtils { * @return 执行界面 */ public static Object parseExpression(String expressionString) { + return parseExpression(expressionString, null); + } + + /** + * 从 Bean 工厂,解析 EL 表达式的结果 + * + * @param expressionString EL 表达式 + * @param variables 变量 + * @return 执行界面 + */ + public static Object parseExpression(String expressionString, Map variables) { if (StrUtil.isBlank(expressionString)) { return null; } Expression expression = EXPRESSION_PARSER.parseExpression(expressionString); StandardEvaluationContext context = new StandardEvaluationContext(); context.setBeanResolver(new BeanFactoryResolver(SpringUtil.getApplicationContext())); + if (MapUtil.isNotEmpty(variables)) { + context.setVariables(variables); + } return expression.getValue(context); } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/util/TenantUtils.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/util/TenantUtils.java index 7ec9c69e3..b05b3c06b 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/util/TenantUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/util/TenantUtils.java @@ -45,6 +45,7 @@ public class TenantUtils { * * @param tenantId 租户编号 * @param callable 逻辑 + * @return 结果 */ public static V execute(Long tenantId, Callable callable) { Long oldTenantId = TenantContextHolder.getTenantId(); @@ -78,6 +79,25 @@ public class TenantUtils { } } + /** + * 忽略租户,执行对应的逻辑 + * + * @param callable 逻辑 + * @return 结果 + */ + public static V executeIgnore(Callable callable) { + Boolean oldIgnore = TenantContextHolder.isIgnore(); + try { + TenantContextHolder.setIgnore(true); + // 执行逻辑 + return callable.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + TenantContextHolder.setIgnore(oldIgnore); + } + } + /** * 将多租户编号,添加到 header 中 * diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java index f5c4b8313..eb037d9e1 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.framework.excel.core.util; +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; import cn.iocoder.yudao.framework.excel.core.handler.SelectSheetWriteHandler; import com.alibaba.excel.EasyExcel; import com.alibaba.excel.converters.longconverter.LongStringConverter; @@ -8,8 +9,6 @@ import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.util.List; /** @@ -40,7 +39,7 @@ public class ExcelUtils { .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度 .sheet(sheetName).doWrite(data); // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了 - response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name())); + response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); response.setContentType("application/vnd.ms-excel;charset=UTF-8"); } diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml b/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml index 1f3c9144b..5a619c511 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml @@ -63,6 +63,11 @@ opengauss-jdbc true + + com.taosdata.jdbc + taos-jdbcdriver + true + com.alibaba diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java index 01f214230..167a0fc4e 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java @@ -92,10 +92,36 @@ public interface BaseMapperX extends MPJBaseMapper { default T selectOne(SFunction field1, Object value1, SFunction field2, Object value2, SFunction field3, Object value3) { - return selectOne(new LambdaQueryWrapper().eq(field1, value1).eq(field2, value2) - .eq(field3, value3)); + return selectOne(new LambdaQueryWrapper().eq(field1, value1).eq(field2, value2).eq(field3, value3)); } + /** + * 获取满足条件的第 1 条记录 + * + * 目的:解决并发场景下,插入多条记录后,使用 selectOne 会报错的问题 + * + * @param field 字段名 + * @param value 字段值 + * @return 实体 + */ + default T selectFirstOne(SFunction field, Object value) { + // 如果明确使用 MySQL 等场景,可以考虑使用 LIMIT 1 进行优化 + List list = selectList(new LambdaQueryWrapper().eq(field, value)); + return CollUtil.getFirst(list); + } + + default T selectFirstOne(SFunction field1, Object value1, SFunction field2, Object value2) { + List list = selectList(new LambdaQueryWrapper().eq(field1, value1).eq(field2, value2)); + return CollUtil.getFirst(list); + } + + default T selectFirstOne(SFunction field1, Object value1, SFunction field2, Object value2, + SFunction field3, Object value3) { + List list = selectList(new LambdaQueryWrapper().eq(field1, value1).eq(field2, value2).eq(field3, value3)); + return CollUtil.getFirst(list); + } + + default Long selectCount() { return selectCount(new QueryWrapper<>()); } diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/EncryptTypeHandler.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/EncryptTypeHandler.java index 7ef0f4ece..9327ebbfe 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/EncryptTypeHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/EncryptTypeHandler.java @@ -13,7 +13,7 @@ import java.sql.ResultSet; import java.sql.SQLException; /** - * 字段字段的 TypeHandler 实现类,基于 {@link cn.hutool.crypto.symmetric.AES} 实现 + * 字段字段的 TypeHandler 实现类,基于 {@link AES} 实现 * 可通过 jasypt.encryptor.password 配置项,设置密钥 * * @author 芋道源码 diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/LongSetTypeHandler.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/LongSetTypeHandler.java new file mode 100644 index 000000000..58d82ecf3 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/LongSetTypeHandler.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.framework.mybatis.core.type; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; +import org.apache.ibatis.type.TypeHandler; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Set; + +/** + * Set 的类型转换器实现类,对应数据库的 varchar 类型 + * + * @author 芋道源码 + */ +@MappedJdbcTypes(JdbcType.VARCHAR) +@MappedTypes(List.class) +public class LongSetTypeHandler implements TypeHandler> { + + private static final String COMMA = ","; + + @Override + public void setParameter(PreparedStatement ps, int i, Set strings, JdbcType jdbcType) throws SQLException { + // 设置占位符 + ps.setString(i, CollUtil.join(strings, COMMA)); + } + + @Override + public Set getResult(ResultSet rs, String columnName) throws SQLException { + String value = rs.getString(columnName); + return getResult(value); + } + + @Override + public Set getResult(ResultSet rs, int columnIndex) throws SQLException { + String value = rs.getString(columnIndex); + return getResult(value); + } + + @Override + public Set getResult(CallableStatement cs, int columnIndex) throws SQLException { + String value = cs.getString(columnIndex); + return getResult(value); + } + + private Set getResult(String value) { + if (value == null) { + return null; + } + return StrUtils.splitToLongSet(value, COMMA); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/redis/RateLimiterRedisDAO.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/redis/RateLimiterRedisDAO.java index fc1378f3b..18c30682e 100644 --- a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/redis/RateLimiterRedisDAO.java +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/redis/RateLimiterRedisDAO.java @@ -44,6 +44,7 @@ public class RateLimiterRedisDAO { RateLimiterConfig config = rateLimiter.getConfig(); if (config == null) { rateLimiter.trySetRate(RateType.OVERALL, count, rateInterval, RateIntervalUnit.SECONDS); + rateLimiter.expire(rateInterval, TimeUnit.SECONDS); // 原因参见 https://t.zsxq.com/lcR0W return rateLimiter; } // 2. 如果存在,并且配置相同,则直接返回 @@ -54,6 +55,7 @@ public class RateLimiterRedisDAO { } // 3. 如果存在,并且配置不同,则进行新建 rateLimiter.setRate(RateType.OVERALL, count, rateInterval, RateIntervalUnit.SECONDS); + rateLimiter.expire(rateInterval, TimeUnit.SECONDS); // 原因参见 https://t.zsxq.com/lcR0W return rateLimiter; } diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java index c8b0dbd66..d07c4aaed 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java @@ -62,9 +62,9 @@ public class BannerApplicationRunner implements ApplicationRunner { if (isNotPresent("cn.iocoder.yudao.module.ai.framework.web.config.AiWebConfiguration")) { System.out.println("[AI 大模型 yudao-module-ai - 已禁用][参考 https://doc.iocoder.cn/ai/build/ 开启]"); } - // IOT 物联网 + // IoT 物联网 if (isNotPresent("cn.iocoder.yudao.module.iot.framework.web.config.IotWebConfiguration")) { - System.out.println("[IOT 物联网 yudao-module-iot - 已禁用][参考 https://doc.iocoder.cn/iot/build/ 开启]"); + System.out.println("[IoT 物联网 yudao-module-iot - 已禁用][参考 https://doc.iocoder.cn/iot/build/ 开启]"); } }); } diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java index 41c5cead6..073824815 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.framework.web.core.handler; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.exceptions.ExceptionUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjUtil; @@ -27,6 +28,7 @@ import org.springframework.security.access.AccessDeniedException; import org.springframework.util.Assert; import org.springframework.validation.BindException; import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingServletRequestParameterException; @@ -37,6 +39,7 @@ import org.springframework.web.servlet.NoHandlerFoundException; import org.springframework.web.servlet.resource.NoResourceFoundException; import java.time.LocalDateTime; +import java.util.List; import java.util.Map; import java.util.Set; @@ -135,9 +138,23 @@ public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public CommonResult methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) { log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex); + // 获取 errorMessage + String errorMessage = null; FieldError fieldError = ex.getBindingResult().getFieldError(); - assert fieldError != null; // 断言,避免告警 - return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); + if (fieldError == null) { + // 组合校验,参考自 https://t.zsxq.com/3HVTx + List allErrors = ex.getBindingResult().getAllErrors(); + if (CollUtil.isNotEmpty(allErrors)) { + errorMessage = allErrors.get(0).getDefaultMessage(); + } + } else { + errorMessage = fieldError.getDefaultMessage(); + } + // 转换 CommonResult + if (StrUtil.isEmpty(errorMessage)) { + return CommonResult.error(BAD_REQUEST); + } + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", errorMessage)); } /** @@ -378,11 +395,11 @@ public class GlobalExceptionHandler { return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]"); } - // 9. IOT 物联网 + // 9. IoT 物联网 if (message.contains("iot_")) { - log.error("[IOT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]"); + log.error("[IoT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[IOT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]"); + "[IoT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]"); } return null; } diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java index 8b235f9ac..8a8a38832 100644 --- a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java @@ -61,4 +61,8 @@ public interface ErrorCodeConstants { ErrorCode TOOL_NOT_EXISTS = new ErrorCode(1_040_010_000, "工具不存在"); ErrorCode TOOL_NAME_NOT_EXISTS = new ErrorCode(1_040_010_001, "工具({})找不到 Bean"); + // ========== AI 工作流 1-040-011-000 ========== + ErrorCode WORKFLOW_NOT_EXISTS = new ErrorCode(1_040_011_000, "工作流不存在"); + ErrorCode WORKFLOW_CODE_EXISTS = new ErrorCode(1_040_011_001, "工作流标识已存在"); + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/AiWorkflowController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/AiWorkflowController.java new file mode 100644 index 000000000..d558d9045 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/AiWorkflowController.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.ai.controller.admin.workflow; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.workflow.vo.*; +import cn.iocoder.yudao.module.ai.dal.dataobject.workflow.AiWorkflowDO; +import cn.iocoder.yudao.module.ai.service.workflow.AiWorkflowService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - AI 工作流") +@RestController +@RequestMapping("/ai/workflow") +@Slf4j +public class AiWorkflowController { + + @Resource + private AiWorkflowService workflowService; + + @PostMapping("/create") + @Operation(summary = "创建 AI 工作流") + @PreAuthorize("@ss.hasPermission('ai:workflow:create')") + public CommonResult createWorkflow(@Valid @RequestBody AiWorkflowSaveReqVO createReqVO) { + return success(workflowService.createWorkflow(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新 AI 工作流") + @PreAuthorize("@ss.hasPermission('ai:workflow:update')") + public CommonResult updateWorkflow(@Valid @RequestBody AiWorkflowSaveReqVO updateReqVO) { + workflowService.updateWorkflow(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除 AI 工作流") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('ai:workflow:delete')") + public CommonResult deleteWorkflow(@RequestParam("id") Long id) { + workflowService.deleteWorkflow(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得 AI 工作流") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('ai:workflow:query')") + public CommonResult getWorkflow(@RequestParam("id") Long id) { + AiWorkflowDO workflow = workflowService.getWorkflow(id); + return success(BeanUtils.toBean(workflow, AiWorkflowRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得 AI 工作流分页") + @PreAuthorize("@ss.hasPermission('ai:workflow:query')") + public CommonResult> getWorkflowPage(@Valid AiWorkflowPageReqVO pageReqVO) { + PageResult pageResult = workflowService.getWorkflowPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, AiWorkflowRespVO.class)); + } + + @PostMapping("/test") + @Operation(summary = "测试 AI 工作流") + @PreAuthorize("@ss.hasPermission('ai:workflow:test')") + public CommonResult testWorkflow(@Valid @RequestBody AiWorkflowTestReqVO testReqVO) { + return success(workflowService.testWorkflow(testReqVO)); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowPageReqVO.java new file mode 100644 index 000000000..e55b85ea9 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowPageReqVO.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.ai.controller.admin.workflow.vo; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - AI 工作流分页 Request VO") +@Data +public class AiWorkflowPageReqVO extends PageParam { + + @Schema(description = "名称", example = "工作流") + private String name; + + @Schema(description = "标识", example = "FLOW") + private String code; + + @Schema(description = "状态", example = "1") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowRespVO.java new file mode 100644 index 000000000..e3a28ad64 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowRespVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.ai.controller.admin.workflow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - AI 工作流 Response VO") +@Data +public class AiWorkflowRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "工作流标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "FLOW") + private String code; + + @Schema(description = "工作流名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "工作流") + private String name; + + @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "工作流") + private String remark; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "工作流模型 JSON", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}") + private String graph; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + private LocalDateTime createTime; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowSaveReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowSaveReqVO.java new file mode 100644 index 000000000..0a63c3773 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowSaveReqVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.ai.controller.admin.workflow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - AI 工作流新增/修改 Request VO") +@Data +public class AiWorkflowSaveReqVO { + + @Schema(description = "编号", example = "1") + private Long id; + + @Schema(description = "工作流标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "FLOW") + @NotEmpty(message = "工作流标识不能为空") + private String code; + + @Schema(description = "工作流名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "工作流") + @NotEmpty(message = "工作流名称不能为空") + private String name; + + @Schema(description = "备注", example = "FLOW") + private String remark; + + @Schema(description = "工作流模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}") + @NotEmpty(message = "工作流模型不能为空") + private String graph; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "FLOW") + @NotNull(message = "状态不能为空") + private Integer status; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowTestReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowTestReqVO.java new file mode 100644 index 000000000..4dc509e89 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowTestReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.ai.controller.admin.workflow.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.Map; + +@Schema(description = "管理后台 - AI 工作流测试 Request VO") +@Data +public class AiWorkflowTestReqVO { + + @Schema(description = "工作流模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}") + @NotEmpty(message = "工作流模型不能为空") + private String graph; + + @Schema(description = "参数", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}") + private Map params; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/workflow/AiWorkflowDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/workflow/AiWorkflowDO.java new file mode 100644 index 000000000..d844f7da2 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/workflow/AiWorkflowDO.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.ai.dal.dataobject.workflow; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * AI 工作流 DO + * + * @author lesan + */ +@TableName(value = "ai_workflow", autoResultMap = true) +@KeySequence("ai_workflow") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class AiWorkflowDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 工作流名称 + */ + private String name; + /** + * 工作流标识 + */ + private String code; + + /** + * 工作流模型 JSON 数据 + */ + private String graph; + + /** + * 备注 + */ + private String remark; + + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/workflow/AiWorkflowMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/workflow/AiWorkflowMapper.java new file mode 100644 index 000000000..3770dbf0b --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/workflow/AiWorkflowMapper.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.ai.dal.mysql.workflow; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.ai.controller.admin.workflow.vo.AiWorkflowPageReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.workflow.AiWorkflowDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * AI 工作流 Mapper + * + * @author lesan + */ +@Mapper +public interface AiWorkflowMapper extends BaseMapperX { + + default AiWorkflowDO selectByCode(String code) { + return selectOne(AiWorkflowDO::getCode, code); + } + + default PageResult selectPage(AiWorkflowPageReqVO pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .eqIfPresent(AiWorkflowDO::getStatus, pageReqVO.getStatus()) + .likeIfPresent(AiWorkflowDO::getName, pageReqVO.getName()) + .likeIfPresent(AiWorkflowDO::getCode, pageReqVO.getCode()) + .betweenIfPresent(AiWorkflowDO::getCreateTime, pageReqVO.getCreateTime())); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java index 60ca9ac99..c6c9fa43c 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java @@ -11,6 +11,7 @@ import cn.hutool.extra.spring.SpringUtil; import cn.hutool.http.HttpUtil; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowImageOptions; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO; @@ -144,7 +145,12 @@ public class AiImageServiceImpl implements AiImageService { .withStyle(MapUtil.getStr(draw.getOptions(), "style")) // 风格 .withResponseFormat("b64_json") .build(); - } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.STABLE_DIFFUSION.getPlatform())) { + } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.SILICON_FLOW.getPlatform())) { + // https://docs.siliconflow.cn/cn/api-reference/images/images-generations + return SiliconFlowImageOptions.builder().model(model.getModel()) + .height(draw.getHeight()).width(draw.getWidth()) + .build(); + } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.STABLE_DIFFUSION.getPlatform())) { // https://platform.stability.ai/docs/api-reference#tag/SDXL-and-SD1.6/operation/textToImage // https://platform.stability.ai/docs/api-reference#tag/Text-to-Image/operation/textToImage return StabilityAiImageOptions.builder().model(model.getModel()) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/workflow/AiWorkflowService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/workflow/AiWorkflowService.java new file mode 100644 index 000000000..51a3aea75 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/workflow/AiWorkflowService.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.ai.service.workflow; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.ai.controller.admin.workflow.vo.AiWorkflowPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.workflow.vo.AiWorkflowSaveReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.workflow.vo.AiWorkflowTestReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.workflow.AiWorkflowDO; +import jakarta.validation.Valid; + +/** + * AI 工作流 Service 接口 + * + * @author lesan + */ +public interface AiWorkflowService { + + /** + * 创建 AI 工作流 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createWorkflow(@Valid AiWorkflowSaveReqVO createReqVO); + + /** + * 更新 AI 工作流 + * + * @param updateReqVO 更新信息 + */ + void updateWorkflow(@Valid AiWorkflowSaveReqVO updateReqVO); + + /** + * 删除 AI 工作流 + * + * @param id 编号 + */ + void deleteWorkflow(Long id); + + /** + * 获得 AI 工作流 + * + * @param id 编号 + * @return AI 工作流 + */ + AiWorkflowDO getWorkflow(Long id); + + /** + * 获得 AI 工作流分页 + * + * @param pageReqVO 分页查询 + * @return AI 工作流分页 + */ + PageResult getWorkflowPage(AiWorkflowPageReqVO pageReqVO); + + /** + * 测试 AI 工作流 + * + * @param testReqVO 测试数据 + */ + Object testWorkflow(AiWorkflowTestReqVO testReqVO); + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/workflow/AiWorkflowServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/workflow/AiWorkflowServiceImpl.java new file mode 100644 index 000000000..70d28496c --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/workflow/AiWorkflowServiceImpl.java @@ -0,0 +1,150 @@ +package cn.iocoder.yudao.module.ai.service.workflow; + +import cn.hutool.core.util.ObjUtil; +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.ai.controller.admin.workflow.vo.AiWorkflowPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.workflow.vo.AiWorkflowSaveReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.workflow.vo.AiWorkflowTestReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.workflow.AiWorkflowDO; +import cn.iocoder.yudao.module.ai.dal.mysql.workflow.AiWorkflowMapper; +import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import dev.tinyflow.core.Tinyflow; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.WORKFLOW_CODE_EXISTS; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.WORKFLOW_NOT_EXISTS; + +/** + * AI 工作流 Service 实现类 + * + * @author lesan + */ +@Service +@Slf4j +public class AiWorkflowServiceImpl implements AiWorkflowService { + + @Resource + private AiWorkflowMapper workflowMapper; + + @Resource + private AiApiKeyService apiKeyService; + + @Override + public Long createWorkflow(AiWorkflowSaveReqVO createReqVO) { + validateWorkflowForCreateOrUpdate(null, createReqVO.getCode()); + AiWorkflowDO workflow = BeanUtils.toBean(createReqVO, AiWorkflowDO.class); + workflowMapper.insert(workflow); + return workflow.getId(); + } + + @Override + public void updateWorkflow(AiWorkflowSaveReqVO updateReqVO) { + validateWorkflowForCreateOrUpdate(updateReqVO.getId(), updateReqVO.getCode()); + AiWorkflowDO workflow = BeanUtils.toBean(updateReqVO, AiWorkflowDO.class); + workflowMapper.updateById(workflow); + } + + @Override + public void deleteWorkflow(Long id) { + validateWorkflowExists(id); + workflowMapper.deleteById(id); + } + + @Override + public AiWorkflowDO getWorkflow(Long id) { + return workflowMapper.selectById(id); + } + + @Override + public PageResult getWorkflowPage(AiWorkflowPageReqVO pageReqVO) { + return workflowMapper.selectPage(pageReqVO); + } + + @Override + public Object testWorkflow(AiWorkflowTestReqVO testReqVO) { + Map variables = testReqVO.getParams(); + Tinyflow tinyflow = parseFlowParam(testReqVO.getGraph()); + return tinyflow.toChain().executeForResult(variables); + } + + private void validateWorkflowForCreateOrUpdate(Long id, String code) { + validateWorkflowExists(id); + validateCodeUnique(id, code); + } + + private void validateWorkflowExists(Long id) { + if (ObjUtil.isNull(id)) { + return; + } + AiWorkflowDO workflow = workflowMapper.selectById(id); + if (ObjUtil.isNull(workflow)) { + throw exception(WORKFLOW_NOT_EXISTS); + } + } + + private void validateCodeUnique(Long id, String code) { + if (StrUtil.isBlank(code)) { + return; + } + AiWorkflowDO workflow = workflowMapper.selectByCode(code); + if (ObjUtil.isNull(workflow)) { + return; + } + if (ObjUtil.isNull(id)) { + throw exception(WORKFLOW_CODE_EXISTS); + } + if (ObjUtil.notEqual(workflow.getId(), id)) { + throw exception(WORKFLOW_CODE_EXISTS); + } + } + + private Tinyflow parseFlowParam(String graph) { + // TODO @lesan:可以使用 jackson 哇? + JSONObject json = JSONObject.parseObject(graph); + JSONArray nodeArr = json.getJSONArray("nodes"); + Tinyflow tinyflow = new Tinyflow(json.toJSONString()); + for (int i = 0; i < nodeArr.size(); i++) { + JSONObject node = nodeArr.getJSONObject(i); + switch (node.getString("type")) { + case "llmNode": + JSONObject data = node.getJSONObject("data"); + AiApiKeyDO apiKey = apiKeyService.getApiKey(data.getLong("llmId")); + switch (apiKey.getPlatform()) { + // TODO @lesan 需要讨论一下这里怎么弄 + // TODO @lesan llmId 对应 model 的编号如何?这样的话,就是 apiModelService 提供一个获取 LLM 的方法。然后,创建的方法,也在 AiModelFactory 提供。可以先接个 deepseek 先。deepseek yyds! + case "OpenAI": + break; + case "Ollama": + break; + case "YiYan": + break; + case "XingHuo": + break; + case "TongYi": + break; + case "DeepSeek": + break; + case "ZhiPu": + break; + } + break; + case "internalNode": + break; + default: + break; + } + } + return tinyflow; + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml index f37f3709c..3ed8724b7 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml @@ -15,6 +15,7 @@ AI 大模型拓展,接入国内外大模型 1.0.0-M6 + 1.0.0-rc.3 @@ -117,6 +118,13 @@ + + + dev.tinyflow + tinyflow-java-core + ${tinyflow.version} + + org.springframework.boot diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java index ef3314a48..a454e40e8 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java @@ -4,10 +4,12 @@ import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory; import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactoryImpl; +import cn.iocoder.yudao.framework.ai.core.model.baichuan.BaiChuanChatModel; import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel; import cn.iocoder.yudao.framework.ai.core.model.doubao.DouBaoChatModel; import cn.iocoder.yudao.framework.ai.core.model.hunyuan.HunYuanChatModel; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowApiConstants; import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowChatModel; import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel; @@ -113,11 +115,11 @@ public class YudaoAiAutoConfiguration { public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlowProperties properties) { if (StrUtil.isEmpty(properties.getModel())) { - properties.setModel(SiliconFlowChatModel.MODEL_DEFAULT); + properties.setModel(SiliconFlowApiConstants.MODEL_DEFAULT); } OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() .openAiApi(OpenAiApi.builder() - .baseUrl(SiliconFlowChatModel.BASE_URL) + .baseUrl(SiliconFlowApiConstants.DEFAULT_BASE_URL) .apiKey(properties.getApiKey()) .build()) .defaultOptions(OpenAiChatOptions.builder() @@ -192,6 +194,33 @@ public class YudaoAiAutoConfiguration { return new XingHuoChatModel(openAiChatModel); } + @Bean + @ConditionalOnProperty(value = "yudao.ai.baichuan.enable", havingValue = "true") + public BaiChuanChatModel baiChuanChatClient(YudaoAiProperties yudaoAiProperties) { + YudaoAiProperties.BaiChuanProperties properties = yudaoAiProperties.getBaichuan(); + return buildBaiChuanChatClient(properties); + } + + public BaiChuanChatModel buildBaiChuanChatClient(YudaoAiProperties.BaiChuanProperties properties) { + if (StrUtil.isEmpty(properties.getModel())) { + properties.setModel(BaiChuanChatModel.MODEL_DEFAULT); + } + OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(BaiChuanChatModel.BASE_URL) + .apiKey(properties.getApiKey()) + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model(properties.getModel()) + .temperature(properties.getTemperature()) + .maxTokens(properties.getMaxTokens()) + .topP(properties.getTopP()) + .build()) + .toolCallingManager(getToolCallingManager()) + .build(); + return new BaiChuanChatModel(openAiChatModel); + } + @Bean @ConditionalOnProperty(value = "yudao.ai.midjourney.enable", havingValue = "true") public MidjourneyApi midjourneyApi(YudaoAiProperties yudaoAiProperties) { diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java index 296e0af8b..86d1084cc 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java @@ -43,6 +43,12 @@ public class YudaoAiProperties { @SuppressWarnings("SpellCheckingInspection") private XingHuoProperties xinghuo; + /** + * 百川 + */ + @SuppressWarnings("SpellCheckingInspection") + private BaiChuanProperties baichuan; + /** * Midjourney 绘图 */ @@ -122,6 +128,19 @@ public class YudaoAiProperties { } + @Data + public static class BaiChuanProperties { + + private String enable; + private String apiKey; + + private String model; + private Double temperature; + private Integer maxTokens; + private Double topP; + + } + @Data public static class MidjourneyProperties { diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java index 5a8a5c453..be65f2986 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java @@ -27,6 +27,7 @@ public enum AiPlatformEnum implements ArrayValuable { SILICON_FLOW("SiliconFlow", "硅基流动"), // 硅基流动 MINI_MAX("MiniMax", "MiniMax"), // 稀宇科技 MOONSHOT("Moonshot", "月之暗灭"), // KIMI + BAI_CHUAN("BaiChuan", "百川智能"), // 百川智能 // ========== 国外平台 ========== diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java index 356715be2..6d664eb65 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -11,11 +11,15 @@ import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.ai.config.YudaoAiAutoConfiguration; import cn.iocoder.yudao.framework.ai.config.YudaoAiProperties; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import cn.iocoder.yudao.framework.ai.core.model.baichuan.BaiChuanChatModel; import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel; import cn.iocoder.yudao.framework.ai.core.model.doubao.DouBaoChatModel; import cn.iocoder.yudao.framework.ai.core.model.hunyuan.HunYuanChatModel; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowApiConstants; import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowChatModel; +import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowImageApi; +import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowImageModel; import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel; import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; @@ -42,6 +46,7 @@ import org.springframework.ai.autoconfigure.moonshot.MoonshotAutoConfiguration; import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration; import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration; import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration; +import org.springframework.ai.autoconfigure.stabilityai.StabilityAiImageAutoConfiguration; import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientConnectionDetails; import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientProperties; import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration; @@ -146,6 +151,8 @@ public class AiModelFactoryImpl implements AiModelFactory { return buildMoonshotChatModel(apiKey, url); case XING_HUO: return buildXingHuoChatModel(apiKey); + case BAI_CHUAN: + return buildBaiChuanChatModel(apiKey); case OPENAI: return buildOpenAiChatModel(apiKey, url); case AZURE_OPENAI: @@ -182,6 +189,8 @@ public class AiModelFactoryImpl implements AiModelFactory { return SpringUtil.getBean(MoonshotChatModel.class); case XING_HUO: return SpringUtil.getBean(XingHuoChatModel.class); + case BAI_CHUAN: + return SpringUtil.getBean(AzureOpenAiChatModel.class); case OPENAI: return SpringUtil.getBean(OpenAiChatModel.class); case AZURE_OPENAI: @@ -203,6 +212,8 @@ public class AiModelFactoryImpl implements AiModelFactory { return SpringUtil.getBean(QianFanImageModel.class); case ZHI_PU: return SpringUtil.getBean(ZhiPuAiImageModel.class); + case SILICON_FLOW: + return SpringUtil.getBean(SiliconFlowImageModel.class); case OPENAI: return SpringUtil.getBean(OpenAiImageModel.class); case STABLE_DIFFUSION: @@ -224,6 +235,8 @@ public class AiModelFactoryImpl implements AiModelFactory { return buildZhiPuAiImageModel(apiKey, url); case OPENAI: return buildOpenAiImageModel(apiKey, url); + case SILICON_FLOW: + return buildSiliconFlowImageModel(apiKey,url); case STABLE_DIFFUSION: return buildStabilityAiImageModel(apiKey, url); default: @@ -433,6 +446,15 @@ public class AiModelFactoryImpl implements AiModelFactory { return new YudaoAiAutoConfiguration().buildXingHuoChatClient(properties); } + /** + * 可参考 {@link YudaoAiAutoConfiguration#baiChuanChatClient(YudaoAiProperties)} + */ + private BaiChuanChatModel buildBaiChuanChatModel(String apiKey) { + YudaoAiProperties.BaiChuanProperties properties = new YudaoAiProperties.BaiChuanProperties() + .setApiKey(apiKey); + return new YudaoAiAutoConfiguration().buildBaiChuanChatClient(properties); + } + /** * 可参考 {@link OpenAiAutoConfiguration} 的 openAiChatModel 方法 */ @@ -468,6 +490,15 @@ public class AiModelFactoryImpl implements AiModelFactory { return new OpenAiImageModel(openAiApi); } + /** + * 创建 SiliconFlowImageModel 对象 + */ + private SiliconFlowImageModel buildSiliconFlowImageModel(String apiToken, String url) { + url = StrUtil.blankToDefault(url, SiliconFlowApiConstants.DEFAULT_BASE_URL); + SiliconFlowImageApi openAiApi = new SiliconFlowImageApi(url, apiToken); + return new SiliconFlowImageModel(openAiApi); + } + /** * 可参考 {@link OllamaAutoConfiguration} 的 ollamaApi 方法 */ @@ -476,6 +507,9 @@ public class AiModelFactoryImpl implements AiModelFactory { return OllamaChatModel.builder().ollamaApi(ollamaApi).toolCallingManager(getToolCallingManager()).build(); } + /** + * 可参考 {@link StabilityAiImageAutoConfiguration} 的 stabilityAiImageModel 方法 + */ private StabilityAiImageModel buildStabilityAiImageModel(String apiKey, String url) { url = StrUtil.blankToDefault(url, StabilityAiApi.DEFAULT_BASE_URL); StabilityAiApi stabilityAiApi = new StabilityAiApi(apiKey, StabilityAiApi.DEFAULT_IMAGE_MODEL, url); diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/baichuan/BaiChuanChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/baichuan/BaiChuanChatModel.java new file mode 100644 index 000000000..ac59b7026 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/baichuan/BaiChuanChatModel.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.framework.ai.core.model.baichuan; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import reactor.core.publisher.Flux; + +/** + * 百川 {@link ChatModel} 实现类 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class BaiChuanChatModel implements ChatModel { + + public static final String BASE_URL = "https://api.baichuan-ai.com"; + + public static final String MODEL_DEFAULT = "Baichuan4-Turbo"; + + /** + * 兼容 OpenAI 接口,进行复用 + */ + private final OpenAiChatModel openAiChatModel; + + @Override + public ChatResponse call(Prompt prompt) { + return openAiChatModel.call(prompt); + } + + @Override + public Flux stream(Prompt prompt) { + return openAiChatModel.stream(prompt); + } + + @Override + public ChatOptions getDefaultOptions() { + return openAiChatModel.getDefaultOptions(); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/siliconflow/SiliconFlowApiConstants.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/siliconflow/SiliconFlowApiConstants.java new file mode 100644 index 000000000..4df1b3f3d --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/siliconflow/SiliconFlowApiConstants.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.iocoder.yudao.framework.ai.core.model.siliconflow; + +/** + * SiliconFlow API 枚举类 + * + * @author zzt + */ +public final class SiliconFlowApiConstants { + + public static final String DEFAULT_BASE_URL = "https://api.siliconflow.cn"; + + public static final String MODEL_DEFAULT = "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B"; + + public static final String DEFAULT_IMAGE_MODEL = "Kwai-Kolors/Kolors"; + + public static final String PROVIDER_NAME = "Siiconflow"; + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/siliconflow/SiliconFlowChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/siliconflow/SiliconFlowChatModel.java index cada37987..cda2cb378 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/siliconflow/SiliconFlowChatModel.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/siliconflow/SiliconFlowChatModel.java @@ -20,10 +20,6 @@ import reactor.core.publisher.Flux; @RequiredArgsConstructor public class SiliconFlowChatModel implements ChatModel { - public static final String BASE_URL = "https://api.siliconflow.cn"; - - public static final String MODEL_DEFAULT = "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B"; - /** * 兼容 OpenAI 接口,进行复用 */ diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/siliconflow/SiliconFlowImageApi.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/siliconflow/SiliconFlowImageApi.java new file mode 100644 index 000000000..1408fbe2e --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/siliconflow/SiliconFlowImageApi.java @@ -0,0 +1,115 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.iocoder.yudao.framework.ai.core.model.siliconflow; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.ai.model.ApiKey; +import org.springframework.ai.model.NoopApiKey; +import org.springframework.ai.model.SimpleApiKey; +import org.springframework.ai.openai.api.OpenAiImageApi; +import org.springframework.ai.retry.RetryUtils; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestClient; + +import java.util.Map; + +/** + * 硅基流动 Image API + * + * @see Images + * + * @author zzt + */ +public class SiliconFlowImageApi { + + private final RestClient restClient; + + public SiliconFlowImageApi(String aiToken) { + this(SiliconFlowApiConstants.DEFAULT_BASE_URL, aiToken, RestClient.builder()); + } + + public SiliconFlowImageApi(String baseUrl, String openAiToken) { + this(baseUrl, openAiToken, RestClient.builder()); + } + + public SiliconFlowImageApi(String baseUrl, String openAiToken, RestClient.Builder restClientBuilder) { + this(baseUrl, openAiToken, restClientBuilder, RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER); + } + + public SiliconFlowImageApi(String baseUrl, String apiKey, RestClient.Builder restClientBuilder, + ResponseErrorHandler responseErrorHandler) { + this(baseUrl, apiKey, CollectionUtils.toMultiValueMap(Map.of()), restClientBuilder, responseErrorHandler); + } + + public SiliconFlowImageApi(String baseUrl, String apiKey, MultiValueMap headers, + RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) { + this(baseUrl, new SimpleApiKey(apiKey), headers, restClientBuilder, responseErrorHandler); + } + + public SiliconFlowImageApi(String baseUrl, ApiKey apiKey, MultiValueMap headers, + RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) { + + // @formatter:off + this.restClient = restClientBuilder.baseUrl(baseUrl) + .defaultHeaders(h -> { + if(!(apiKey instanceof NoopApiKey)) { + h.setBearerAuth(apiKey.getValue()); + } + h.setContentType(MediaType.APPLICATION_JSON); + h.addAll(headers); + }) + .defaultStatusHandler(responseErrorHandler) + .build(); + // @formatter:on + } + + public ResponseEntity createImage(SiliconflowImageRequest siliconflowImageRequest) { + Assert.notNull(siliconflowImageRequest, "Image request cannot be null."); + Assert.hasLength(siliconflowImageRequest.prompt(), "Prompt cannot be empty."); + + return this.restClient.post() + .uri("v1/images/generations") + .body(siliconflowImageRequest) + .retrieve() + .toEntity(OpenAiImageApi.OpenAiImageResponse.class); + } + + + // @formatter:off + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SiliconflowImageRequest ( + @JsonProperty("prompt") String prompt, + @JsonProperty("model") String model, + @JsonProperty("batch_size") Integer batchSize, + @JsonProperty("negative_prompt") String negativePrompt, + @JsonProperty("seed") Integer seed, + @JsonProperty("num_inference_steps") Integer numInferenceSteps, + @JsonProperty("guidance_scale") Float guidanceScale, + @JsonProperty("image") String image) { + + public SiliconflowImageRequest(String prompt, String model) { + this(prompt, model, null, null, null, null, null, null); + } + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/siliconflow/SiliconFlowImageModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/siliconflow/SiliconFlowImageModel.java new file mode 100644 index 000000000..235699ee6 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/siliconflow/SiliconFlowImageModel.java @@ -0,0 +1,159 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.iocoder.yudao.framework.ai.core.model.siliconflow; + +import io.micrometer.observation.ObservationRegistry; +import lombok.Setter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.image.*; +import org.springframework.ai.image.observation.DefaultImageModelObservationConvention; +import org.springframework.ai.image.observation.ImageModelObservationContext; +import org.springframework.ai.image.observation.ImageModelObservationConvention; +import org.springframework.ai.image.observation.ImageModelObservationDocumentation; +import org.springframework.ai.model.ModelOptionsUtils; +import org.springframework.ai.openai.OpenAiImageModel; +import org.springframework.ai.openai.api.OpenAiImageApi; +import org.springframework.ai.openai.metadata.OpenAiImageGenerationMetadata; +import org.springframework.ai.retry.RetryUtils; +import org.springframework.http.ResponseEntity; +import org.springframework.lang.Nullable; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.util.Assert; + +import java.util.List; + +/** + * 硅基流动 {@link ImageModel} 实现类 + * + * 参考 {@link OpenAiImageModel} 实现 + * + * @author zzt + */ +public class SiliconFlowImageModel implements ImageModel { + + private static final Logger logger = LoggerFactory.getLogger(SiliconFlowImageModel.class); + + private static final ImageModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultImageModelObservationConvention(); + + private final SiliconFlowImageOptions defaultOptions; + + private final RetryTemplate retryTemplate; + + private final SiliconFlowImageApi siliconFlowImageApi; + + private final ObservationRegistry observationRegistry; + + @Setter + private ImageModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION; + + public SiliconFlowImageModel(SiliconFlowImageApi siliconFlowImageApi) { + this(siliconFlowImageApi, SiliconFlowImageOptions.builder().build(), RetryUtils.DEFAULT_RETRY_TEMPLATE); + } + + public SiliconFlowImageModel(SiliconFlowImageApi siliconFlowImageApi, SiliconFlowImageOptions options, RetryTemplate retryTemplate) { + this(siliconFlowImageApi, options, retryTemplate, ObservationRegistry.NOOP); + } + + public SiliconFlowImageModel(SiliconFlowImageApi siliconFlowImageApi, SiliconFlowImageOptions options, RetryTemplate retryTemplate, + ObservationRegistry observationRegistry) { + Assert.notNull(siliconFlowImageApi, "OpenAiImageApi must not be null"); + Assert.notNull(options, "options must not be null"); + Assert.notNull(retryTemplate, "retryTemplate must not be null"); + Assert.notNull(observationRegistry, "observationRegistry must not be null"); + this.siliconFlowImageApi = siliconFlowImageApi; + this.defaultOptions = options; + this.retryTemplate = retryTemplate; + this.observationRegistry = observationRegistry; + } + + @Override + public ImageResponse call(ImagePrompt imagePrompt) { + SiliconFlowImageOptions requestImageOptions = mergeOptions(imagePrompt.getOptions(), this.defaultOptions); + SiliconFlowImageApi.SiliconflowImageRequest imageRequest = createRequest(imagePrompt, requestImageOptions); + + var observationContext = ImageModelObservationContext.builder() + .imagePrompt(imagePrompt) + .provider(SiliconFlowApiConstants.PROVIDER_NAME) + .requestOptions(imagePrompt.getOptions()) + .build(); + + return ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION + .observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, + this.observationRegistry) + .observe(() -> { + ResponseEntity imageResponseEntity = this.retryTemplate + .execute(ctx -> this.siliconFlowImageApi.createImage(imageRequest)); + + ImageResponse imageResponse = convertResponse(imageResponseEntity, imageRequest); + + observationContext.setResponse(imageResponse); + + return imageResponse; + }); + } + + private SiliconFlowImageApi.SiliconflowImageRequest createRequest(ImagePrompt imagePrompt, + SiliconFlowImageOptions requestImageOptions) { + String instructions = imagePrompt.getInstructions().get(0).getText(); + + SiliconFlowImageApi.SiliconflowImageRequest imageRequest = new SiliconFlowImageApi.SiliconflowImageRequest(instructions, + SiliconFlowApiConstants.DEFAULT_IMAGE_MODEL); + + return ModelOptionsUtils.merge(requestImageOptions, imageRequest, SiliconFlowImageApi.SiliconflowImageRequest.class); + } + + private ImageResponse convertResponse(ResponseEntity imageResponseEntity, + SiliconFlowImageApi.SiliconflowImageRequest siliconflowImageRequest) { + OpenAiImageApi.OpenAiImageResponse imageApiResponse = imageResponseEntity.getBody(); + if (imageApiResponse == null) { + logger.warn("No image response returned for request: {}", siliconflowImageRequest); + return new ImageResponse(List.of()); + } + + List imageGenerationList = imageApiResponse.data() + .stream() + .map(entry -> new ImageGeneration(new Image(entry.url(), entry.b64Json()), + new OpenAiImageGenerationMetadata(entry.revisedPrompt()))) + .toList(); + + ImageResponseMetadata openAiImageResponseMetadata = new ImageResponseMetadata(imageApiResponse.created()); + return new ImageResponse(imageGenerationList, openAiImageResponseMetadata); + } + + private SiliconFlowImageOptions mergeOptions(@Nullable ImageOptions runtimeOptions, SiliconFlowImageOptions defaultOptions) { + var runtimeOptionsForProvider = ModelOptionsUtils.copyToTarget(runtimeOptions, ImageOptions.class, + SiliconFlowImageOptions.class); + + if (runtimeOptionsForProvider == null) { + return defaultOptions; + } + + return SiliconFlowImageOptions.builder() + // Handle portable image options + .model(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getModel(), defaultOptions.getModel())) + .batchSize(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getN(), defaultOptions.getN())) + .width(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getWidth(), defaultOptions.getWidth())) + .height(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getHeight(), defaultOptions.getHeight())) + // Handle SiliconFlow specific image options + .negativePrompt(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getNegativePrompt(), defaultOptions.getNegativePrompt())) + .numInferenceSteps(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getNumInferenceSteps(), defaultOptions.getNumInferenceSteps())) + .guidanceScale(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getGuidanceScale(), defaultOptions.getGuidanceScale())) + .seed(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getSeed(), defaultOptions.getSeed())) + .build(); + } +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/siliconflow/SiliconFlowImageOptions.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/siliconflow/SiliconFlowImageOptions.java new file mode 100644 index 000000000..bdd82e9c8 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/siliconflow/SiliconFlowImageOptions.java @@ -0,0 +1,105 @@ +package cn.iocoder.yudao.framework.ai.core.model.siliconflow; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.ai.image.ImageOptions; + +/** + * 硅基流动 {@link ImageOptions} + * + * @author zzt + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SiliconFlowImageOptions implements ImageOptions { + + @JsonProperty("model") + private String model; + + @JsonProperty("negative_prompt") + private String negativePrompt; + + /** + * The number of images to generate. Must be between 1 and 4. + */ + @JsonProperty("image_size") + private String imageSize; + + /** + * The number of images to generate. Must be between 1 and 4. + */ + @JsonProperty("batch_size") + private Integer batchSize = 1; + + /** + * number of inference steps + */ + @JsonProperty("num_inference_steps") + private Integer numInferenceSteps = 25; + + /** + * This value is used to control the degree of match between the generated image and the given prompt. The higher the value, the more the generated image will tend to strictly match the text prompt. The lower the value, the more creative and diverse the generated image will be, potentially containing more unexpected elements. + * + * Required range: 0 <= x <= 20 + */ + @JsonProperty("guidance_scale") + private Float guidanceScale = 0.75F; + + /** + * 如果想要每次都生成固定的图片,可以把 seed 设置为固定值 + * + */ + @JsonProperty("seed") + private Integer seed = (int)(Math.random() * 1_000_000_000); + + /** + * The image that needs to be uploaded should be converted into base64 format. + */ + @JsonProperty("image") + private String image; + + /** + * 宽 + */ + private Integer width; + + /** + * 高 + */ + private Integer height; + + public void setHeight(Integer height) { + this.height = height; + if (this.width != null && this.height != null) { + this.imageSize = this.width + "x" + this.height; + } + } + + public void setWidth(Integer width) { + this.width = width; + if (this.width != null && this.height != null) { + this.imageSize = this.width + "x" + this.height; + } + } + + @Override + public Integer getN() { + return batchSize; + } + + @Override + public String getResponseFormat() { + return "url"; + } + + @Override + public String getStyle() { + return null; + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java index becc54ee4..09370c636 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.framework.ai.core.util; +import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; @@ -13,6 +14,7 @@ import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.qianfan.QianFanChatOptions; import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; +import java.util.Collections; import java.util.Set; /** @@ -28,6 +30,7 @@ public class AiUtils { public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens, Set toolNames) { + toolNames = ObjUtil.defaultIfNull(toolNames, Collections.emptySet()); // noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: @@ -50,6 +53,7 @@ public class AiUtils { case HUN_YUAN: // 复用 OpenAI 客户端 case XING_HUO: // 复用 OpenAI 客户端 case SILICON_FLOW: // 复用 OpenAI 客户端 + case BAI_CHUAN: // 复用 OpenAI 客户端 return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) .toolNames(toolNames).build(); case AZURE_OPENAI: diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/BaiChuanChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/BaiChuanChatModelTests.java new file mode 100644 index 000000000..9ae36dbb8 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/BaiChuanChatModelTests.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.framework.ai.chat; + +import cn.iocoder.yudao.framework.ai.core.model.baichuan.BaiChuanChatModel; +import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link BaiChuanChatModel} 集成测试 + * + * @author 芋道源码 + */ +public class BaiChuanChatModelTests { + + private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(BaiChuanChatModel.BASE_URL) + .apiKey("sk-61b6766a94c70786ed02673f5e16af3c") // apiKey + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model("Baichuan4-Turbo") // 模型(https://platform.baichuan-ai.com/docs/api) + .temperature(0.7) + .build()) + .build(); + + private final DeepSeekChatModel chatModel = new DeepSeekChatModel(openAiChatModel); + + @Test + @Disabled + public void testCall() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + } + + @Test + @Disabled + public void testStream() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(System.out::println).then().block(); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/SiliconFlowChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/SiliconFlowChatModelTests.java index 880795fe9..b6139b408 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/SiliconFlowChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/SiliconFlowChatModelTests.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.framework.ai.chat; +import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowApiConstants; import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -25,11 +26,11 @@ public class SiliconFlowChatModelTests { private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() .openAiApi(OpenAiApi.builder() - .baseUrl(SiliconFlowChatModel.BASE_URL) + .baseUrl(SiliconFlowApiConstants.DEFAULT_BASE_URL) .apiKey("sk-epsakfenqnyzoxhmbucsxlhkdqlcbnimslqoivkshalvdozz") // apiKey .build()) .defaultOptions(OpenAiChatOptions.builder() - .model(SiliconFlowChatModel.MODEL_DEFAULT) // 模型 + .model(SiliconFlowApiConstants.MODEL_DEFAULT) // 模型 // .model("deepseek-ai/DeepSeek-R1") // 模型(deepseek-ai/DeepSeek-R1)可用赠费 // .model("Pro/deepseek-ai/DeepSeek-R1") // 模型(Pro/deepseek-ai/DeepSeek-R1)需要付费 .temperature(0.7) diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/SiliconFlowImageModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/SiliconFlowImageModelTests.java new file mode 100644 index 000000000..323c4de51 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/SiliconFlowImageModelTests.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.ai.image; + +import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowImageApi; +import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowImageModel; +import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowImageOptions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.image.ImagePrompt; +import org.springframework.ai.image.ImageResponse; + +/** + * {@link SiliconFlowImageModel} 集成测试 + */ +public class SiliconFlowImageModelTests { + + private final SiliconFlowImageModel imageModel = new SiliconFlowImageModel( + new SiliconFlowImageApi("sk-epsakfenqnyzoxhmbucsxlhkdqlcbnimslqoivkshalvdozz") // 密钥 + ); + + @Test + @Disabled + public void testCall() { + // 准备参数 + SiliconFlowImageOptions imageOptions = SiliconFlowImageOptions.builder() + .model("Kwai-Kolors/Kolors") + .build(); + ImagePrompt prompt = new ImagePrompt("万里长城", imageOptions); + + // 方法调用 + ImageResponse response = imageModel.call(prompt); + // 打印结果 + System.out.println(response); + } + +} diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApi.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApi.java index e4eddf6eb..b8ccad64b 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApi.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApi.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.bpm.api.task; import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; - import jakarta.validation.Valid; /** @@ -20,4 +19,6 @@ public interface BpmProcessInstanceApi { */ String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO reqDTO); + + } diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessTaskApi.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessTaskApi.java new file mode 100644 index 000000000..8f6f4258c --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessTaskApi.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.bpm.api.task; + +import jakarta.validation.constraints.NotEmpty; + +/** + * 流程任务 Api 接口 + * + * @author jason + */ +public interface BpmProcessTaskApi { + + /** + * 触发流程任务的执行 + * + * @param processInstanceId 流程实例编号 + * @param taskDefineKey 任务 Key + */ + void triggerTask(@NotEmpty(message = "流程实例的编号不能为空") String processInstanceId, + @NotEmpty(message = "任务 Key 不能为空") String taskDefineKey); + +} diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java index 7fbc7ba22..65605142c 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java @@ -24,6 +24,7 @@ public interface ErrorCodeConstants { ErrorCode MODEL_DEPLOY_FAIL_BPMN_START_EVENT_NOT_EXISTS = new ErrorCode(1_009_002_005, "部署流程失败,原因:BPMN 流程图中,没有开始事件"); ErrorCode MODEL_DEPLOY_FAIL_BPMN_USER_TASK_NAME_NOT_EXISTS = new ErrorCode(1_009_002_006, "部署流程失败,原因:BPMN 流程图中,用户任务({})的名字不存在"); ErrorCode MODEL_UPDATE_FAIL_NOT_MANAGER = new ErrorCode(1_009_002_007, "操作流程失败,原因:你不是该流程({})的管理员"); + ErrorCode MODEL_DEPLOY_FAIL_FIRST_USER_TASK_CANDIDATE_STRATEGY_ERROR = new ErrorCode(1_009_002_008, "部署流程失败,原因:首个任务({})的审批人不能是【审批人自选】"); // ========== 流程定义 1-009-003-000 ========== ErrorCode PROCESS_DEFINITION_KEY_NOT_MATCH = new ErrorCode(1_009_003_000, "流程定义的标识期望是({}),当前是({}),请修改 BPMN 流程图"); @@ -39,6 +40,8 @@ public interface ErrorCodeConstants { ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS = new ErrorCode(1_009_004_004, "任务({})的候选人({})不存在"); ErrorCode PROCESS_INSTANCE_START_USER_CAN_START = new ErrorCode(1_009_004_005, "发起流程失败,你没有权限发起该流程"); ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW = new ErrorCode(1_009_004_005, "流程取消失败,该流程不允许取消"); + ErrorCode PROCESS_INSTANCE_HTTP_TRIGGER_CALL_ERROR = new ErrorCode(1_009_004_006, "流程 Http 触发器请求调用失败"); + ErrorCode PROCESS_INSTANCE_APPROVE_USER_SELECT_ASSIGNEES_NOT_CONFIG = new ErrorCode(1_009_004_007, "下一个任务({})的审批人未配置"); // ========== 流程任务 1-009-005-000 ========== ErrorCode TASK_OPERATE_FAIL_ASSIGN_NOT_SELF = new ErrorCode(1_009_005_001, "操作失败,原因:该任务的审批人不是你"); diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventTypeEnum.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventTypeEnum.java index a63804de3..69c551259 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventTypeEnum.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventTypeEnum.java @@ -14,7 +14,8 @@ import lombok.Getter; public enum BpmBoundaryEventTypeEnum { USER_TASK_TIMEOUT(1, "用户任务超时"), - DELAY_TIMER_TIMEOUT(2, "延迟器超时"); + DELAY_TIMER_TIMEOUT(2, "延迟器超时"), + CHILD_PROCESS_TIMEOUT(3, "子流程超时"); private final Integer type; private final String name; diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessMultiInstanceSourceTypeEnum.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessMultiInstanceSourceTypeEnum.java new file mode 100644 index 000000000..fab0ddfd7 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessMultiInstanceSourceTypeEnum.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * BPM 子流程多实例来源类型枚举 + * + * @author Lesan + */ +@Getter +@AllArgsConstructor +public enum BpmChildProcessMultiInstanceSourceTypeEnum implements ArrayValuable { + + FIXED_QUANTITY(1, "固定数量"), + NUMBER_FORM(2, "数字表单"), + MULTIPLE_FORM(3, "多选表单"); + + private final Integer type; + private final String name; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmChildProcessMultiInstanceSourceTypeEnum::getType).toArray(Integer[]::new); + + public static BpmChildProcessMultiInstanceSourceTypeEnum typeOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessStartUserEmptyTypeEnum.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessStartUserEmptyTypeEnum.java new file mode 100644 index 000000000..55e6e02e8 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessStartUserEmptyTypeEnum.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * BPM 当子流程发起人为空时类型枚举 + * + * @author Lesan + */ +@Getter +@AllArgsConstructor +public enum BpmChildProcessStartUserEmptyTypeEnum implements ArrayValuable { + + MAIN_PROCESS_START_USER(1, "同主流程发起人"), + CHILD_PROCESS_ADMIN(2, "子流程管理员"), + MAIN_PROCESS_ADMIN(3, "主流程管理员"); + + private final Integer type; + private final String name; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmChildProcessStartUserEmptyTypeEnum::getType).toArray(Integer[]::new); + + public static BpmChildProcessStartUserEmptyTypeEnum typeOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + + @Override + public Integer[] array() { + return ARRAYS; + } +} diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessStartUserTypeEnum.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessStartUserTypeEnum.java new file mode 100644 index 000000000..10d04ea4f --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessStartUserTypeEnum.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * BPM 子流程发起人类型枚举 + * + * @author Lesan + */ +@Getter +@AllArgsConstructor +public enum BpmChildProcessStartUserTypeEnum implements ArrayValuable { + + MAIN_PROCESS_START_USER(1, "同主流程发起人"), + FROM_FORM(2, "表单"); + + private final Integer type; + private final String name; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmChildProcessStartUserTypeEnum::getType).toArray(Integer[]::new); + + public static BpmChildProcessStartUserTypeEnum typeOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + + @Override + public Integer[] array() { + return ARRAYS; + } +} diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java index 238a9347c..e5ffa1202 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java @@ -25,10 +25,13 @@ public enum BpmSimpleModelNodeTypeEnum implements ArrayValuable { START_USER_NODE(10, "发起人", "userTask"), // 发起人节点。前端的开始节点,Id 固定 APPROVE_NODE(11, "审批人", "userTask"), COPY_NODE(12, "抄送人", "serviceTask"), + TRANSACTOR_NODE(13, "办理人", "userTask"), DELAY_TIMER_NODE(14, "延迟器", "receiveTask"), TRIGGER_NODE(15, "触发器", "serviceTask"), + CHILD_PROCESS(20, "子流程", "callActivity"), + // 50 ~ 条件分支 CONDITION_NODE(50, "条件", "sequenceFlow"), // 用于构建流转条件的表达式 CONDITION_BRANCH_NODE(51, "条件分支", "exclusiveGateway"), diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTriggerTypeEnum.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTriggerTypeEnum.java index 9a2f07372..13f997c7b 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTriggerTypeEnum.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTriggerTypeEnum.java @@ -16,8 +16,12 @@ import java.util.Arrays; @AllArgsConstructor public enum BpmTriggerTypeEnum implements ArrayValuable { - HTTP_REQUEST(1, "发起 HTTP 请求"), - UPDATE_NORMAL_FORM(2, "更新流程表单"); // TODO @jason:FORM_UPDATE + HTTP_REQUEST(1, "发起 HTTP 请求"), // BPM => 业务,流程继续执行,无需等待业务 + HTTP_CALLBACK(2, "接收 HTTP 回调"), // BPM => 业务 => BPM,流程卡主,等待业务回调 + + FORM_UPDATE(10, "更新流程表单数据"), + FORM_DELETE(11, "删除流程表单数据"), + ; /** * 触发器执行动作类型 @@ -39,5 +43,4 @@ public enum BpmTriggerTypeEnum implements ArrayValuable { public static BpmTriggerTypeEnum typeOf(Integer type) { return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); } - } diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java index d41c0c313..b0ade7529 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java @@ -26,6 +26,7 @@ public enum BpmReasonEnum { TIMEOUT_REJECT("审批超时,系统自动不通过"), ASSIGN_START_USER_APPROVE("审批人与提交人为同一人时,自动通过"), ASSIGN_START_USER_APPROVE_WHEN_SKIP("审批人与提交人为同一人时,自动通过"), + ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE("发起人节点首次自动通过"), // 目前仅“子流程”使用 ASSIGN_START_USER_APPROVE_WHEN_DEPT_LEADER_NOT_FOUND("审批人与提交人为同一人时,找不到部门负责人,自动通过"), ASSIGN_START_USER_TRANSFER_DEPT_LEADER("审批人与提交人为同一人时,转交给部门负责人审批"), ASSIGN_EMPTY_APPROVE("审批人为空,自动通过"), diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApiImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApiImpl.java index f8aea4d74..5ebb12f0f 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApiImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApiImpl.java @@ -2,11 +2,10 @@ package cn.iocoder.yudao.module.bpm.api.task; import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; -import org.springframework.stereotype.Service; -import org.springframework.validation.annotation.Validated; - import jakarta.annotation.Resource; import jakarta.validation.Valid; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; /** * Flowable 流程实例 Api 实现类 @@ -25,4 +24,5 @@ public class BpmProcessInstanceApiImpl implements BpmProcessInstanceApi { public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO reqDTO) { return processInstanceService.createProcessInstance(userId, reqDTO); } + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessTaskApiImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessTaskApiImpl.java new file mode 100644 index 000000000..d99aa03c6 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessTaskApiImpl.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.bpm.api.task; + +import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +/** + * 流程任务 Api 实现类 + * + * @author jason + */ +@Service +@Validated +public class BpmProcessTaskApiImpl implements BpmProcessTaskApi { + + @Resource + private BpmTaskService bpmTaskService; + + @Override + public void triggerTask(String processInstanceId, String taskDefineKey) { + bpmTaskService.triggerTask(processInstanceId, taskDefineKey); + } + +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java index 6483ee0e3..5578114f9 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java @@ -57,7 +57,7 @@ public class BpmModelController { @GetMapping("/list") @Operation(summary = "获得模型分页") @Parameter(name = "name", description = "模型名称", example = "芋艿") - public CommonResult> getModelPage(@RequestParam(value = "name", required = false) String name) { + public CommonResult> getModelList(@RequestParam(value = "name", required = false) String name) { List list = modelService.getModelList(name); if (CollUtil.isEmpty(list)) { return success(Collections.emptyList()); diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java index 28cbd0ab9..90c38886a 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java @@ -17,6 +17,7 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import org.flowable.bpmn.model.BpmnModel; +import org.flowable.common.engine.impl.db.SuspensionState; import org.flowable.engine.repository.Deployment; import org.flowable.engine.repository.ProcessDefinition; import org.springframework.security.access.prepost.PreAuthorize; @@ -31,6 +32,7 @@ import java.util.List; import java.util.Map; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @@ -99,6 +101,17 @@ public class BpmProcessDefinitionController { list, null, processDefinitionMap, null, null)); } + @GetMapping("/simple-list") + @Operation(summary = "获得流程定义精简列表", description = "只包含未挂起的流程,主要用于前端的下拉选项") + public CommonResult> getSimpleProcessDefinitionList() { + // 只查询未挂起的流程 + List list = processDefinitionService.getProcessDefinitionListBySuspensionState( + SuspensionState.ACTIVE.getStateCode()); + // 拼接 VO 返回,只返回 id、name、key + return success(convertList(list, definition -> new BpmProcessDefinitionRespVO() + .setId(definition.getId()).setName(definition.getName()).setKey(definition.getKey()))); + } + @GetMapping ("/get") @Operation(summary = "获得流程定义") @Parameter(name = "id", description = "流程编号", required = true, example = "1024") diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java index 7bdc963c8..cf9ca3e5f 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model; import cn.iocoder.yudao.framework.common.core.KeyValue; import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; import cn.iocoder.yudao.module.bpm.enums.definition.BpmAutoApproveTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum; @@ -27,8 +28,7 @@ import java.util.List; @Data public class BpmModelMetaInfoVO { - @Schema(description = "流程图标", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao.jpg") - @NotEmpty(message = "流程图标不能为空") + @Schema(description = "流程图标", example = "https://www.iocoder.cn/yudao.jpg") @URL(message = "流程图标格式不正确") private String icon; @@ -46,6 +46,7 @@ public class BpmModelMetaInfoVO { private Integer formType; @Schema(description = "表单编号", example = "1024") private Long formId; // formType 为 NORMAL 使用,必须非空 + @Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址", example = "/bpm/oa/leave/create") private String formCustomCreatePath; // 表单类型为 CUSTOM 时,必须非空 @Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址", example = "/bpm/oa/leave/view") @@ -81,6 +82,12 @@ public class BpmModelMetaInfoVO { @Schema(description = "摘要设置", example = "{}") private SummarySetting summarySetting; + @Schema(description = "流程前置通知设置", example = "{}") + private HttpRequestSetting processBeforeTriggerSetting; + + @Schema(description = "流程后置通知设置", example = "{}") + private HttpRequestSetting processAfterTriggerSetting; + @Schema(description = "流程 ID 规则") @Data @Valid @@ -133,4 +140,32 @@ public class BpmModelMetaInfoVO { } + @Schema(description = "http 请求通知设置", example = "{}") + @Data + public static class HttpRequestSetting { + + @Schema(description = "请求路径", example = "http://127.0.0.1") + @NotEmpty(message = "请求 URL 不能为空") + @URL(message = "请求 URL 格式不正确") + private String url; + + @Schema(description = "请求头参数设置", example = "[]") + @Valid + private List header; + + @Schema(description = "请求头参数设置", example = "[]") + @Valid + private List body; + + /** + * 请求返回处理设置,用于修改流程表单值 + *

+ * key:表示要修改的流程表单字段名(name) + * value:接口返回的字段名 + */ + @Schema(description = "请求返回处理设置", example = "[]") + private List> response; + + } + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelRespVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelRespVO.java index 278291483..275368c7c 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelRespVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelRespVO.java @@ -25,7 +25,7 @@ public class BpmModelRespVO extends BpmModelMetaInfoVO { @Schema(description = "流程图标", example = "https://www.iocoder.cn/yudao.jpg") private String icon; - @Schema(description = "流程分类编码", example = "1") + @Schema(description = "流程分类编号", example = "1") private String category; @Schema(description = "流程分类名字", example = "请假") private String categoryName; diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java index 291cf4d83..f002e6894 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java @@ -4,16 +4,19 @@ import cn.iocoder.yudao.framework.common.core.KeyValue; import cn.iocoder.yudao.framework.common.validation.InEnum; import cn.iocoder.yudao.module.bpm.enums.definition.*; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; +import org.flowable.bpmn.model.IOParameter; import org.hibernate.validator.constraints.URL; import java.util.List; import java.util.Map; +import java.util.Set; @Schema(description = "管理后台 - 仿钉钉流程设计模型节点 VO") @Data @@ -114,7 +117,8 @@ public class BpmSimpleModelNodeVO { @Schema(description = "路由分支组", example = "[]") private List routerGroups; - @Schema(description = "路由分支默认分支 ID", example = "Flow_xxx", hidden = true) // 由后端生成,所以 hidden = true + @Schema(description = "路由分支默认分支 ID", example = "Flow_xxx", hidden = true) // 由后端生成(不从前端传递),所以 hidden = true + @JsonIgnore private String routerDefaultFlowId; // 仅用于路由分支节点 BpmSimpleModelNodeType.ROUTER_BRANCH_NODE /** @@ -122,6 +126,15 @@ public class BpmSimpleModelNodeVO { */ private TriggerSetting triggerSetting; + @Schema(description = "附加节点 Id", example = "UserTask_xxx", hidden = true) // 由后端生成(不从前端传递),所以 hidden = true + @JsonIgnore + private String attachNodeId; // 目前用于触发器节点(HTTP 回调)。需要 UserTask 和 ReceiveTask(附加节点) 来完成 + + /** + * 子流程设置 + */ + private ChildProcessSetting childProcessSetting; + @Schema(description = "任务监听器") @Valid @Data @@ -345,12 +358,10 @@ public class BpmSimpleModelNodeVO { @Valid private HttpRequestTriggerSetting httpRequestSetting; - // TODO @jason:这个要不直接叫 formSetting,更好理解一点哈 - // TODO @jason:如果搞成 List,是不是可以做条件组了?微信讨论哈 /** * 流程表单触发器设置 */ - private NormalFormTriggerSetting normalFormSetting; + private List formSettings; @Schema(description = "http 请求触发器设置", example = "{}") @Data @@ -369,7 +380,6 @@ public class BpmSimpleModelNodeVO { @Valid private List body; - // TODO @json:可能未来看情况,搞个 HttpResponseParam;得看看有没别的业务需要,抽象统一 /** * 请求返回处理设置,用于修改流程表单值 *

@@ -379,15 +389,137 @@ public class BpmSimpleModelNodeVO { @Schema(description = "请求返回处理设置", example = "[]") private List> response; + /** + * Http 回调请求,需要指定回调任务 Key,用于回调执行 + */ + @Schema(description = "回调任务 Key", example = "xxx", hidden = true) + private String callbackTaskDefineKey; + } @Schema(description = "流程表单触发器设置", example = "{}") @Data - public static class NormalFormTriggerSetting { + public static class FormTriggerSetting { - @Schema(description = "修改的表单字段", example = "userName") + @Schema(description = "条件类型", example = "1") + @InEnum(BpmSimpleModeConditionTypeEnum.class) + private Integer conditionType; + + @Schema(description = "条件表达式", example = "${day>3}") + private String conditionExpression; + + @Schema(description = "条件组", example = "{}") + private ConditionGroups conditionGroups; + + @Schema(description = "修改的表单字段", example = "{}") private Map updateFormFields; + @Schema(description = "删除表单字段", example = "[]") + private Set deleteFields; + } + } + + @Schema(description = "子流程节点配置") + @Data + @Valid + public static class ChildProcessSetting { + + @Schema(description = "被调用流程", requiredMode = Schema.RequiredMode.REQUIRED, example = "xxx") + @NotEmpty(message = "被调用流程不能为空") + private String calledProcessDefinitionKey; + + @Schema(description = "被调用流程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "xxx") + @NotEmpty(message = "被调用流程名称不能为空") + private String calledProcessDefinitionName; + + @Schema(description = "是否异步", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "是否异步不能为空") + private Boolean async; + + @Schema(description = "输入参数(主->子)", example = "[]") + private List inVariables; + + @Schema(description = "输出参数(子->主)", example = "[]") + private List outVariables; + + @Schema(description = "是否自动跳过子流程发起节点", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "是否自动跳过子流程发起节点不能为空") + private Boolean skipStartUserNode; + + @Schema(description = "子流程发起人配置", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}") + @NotNull(message = "子流程发起人配置不能为空") + private StartUserSetting startUserSetting; + + @Schema(description = "超时设置", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}") + private TimeoutSetting timeoutSetting; + + @Schema(description = "多实例设置", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}") + private MultiInstanceSetting multiInstanceSetting; + + @Schema(description = "子流程发起人配置") + @Data + @Valid + public static class StartUserSetting { + + @Schema(description = "子流程发起人类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "子流程发起人类型") + @InEnum(BpmChildProcessStartUserTypeEnum.class) + private Integer type; + + @Schema(description = "表单", example = "xxx") + private String formField; + + @Schema(description = "当子流程发起人为空时类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "当子流程发起人为空时类型不能为空") + @InEnum(BpmChildProcessStartUserEmptyTypeEnum.class) + private Integer emptyType; + + } + + @Schema(description = "超时设置") + @Data + @Valid + public static class TimeoutSetting { + + @Schema(description = "是否开启超时设置", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "是否开启超时设置不能为空") + private Boolean enable; + + @Schema(description = "时间类型", example = "1") + @InEnum(BpmDelayTimerTypeEnum.class) + private Integer type; + + @Schema(description = "时间表达式", example = "PT1H,2025-01-01T00:00:00") + private String timeExpression; + + } + + @Schema(description = "多实例设置") + @Data + @Valid + public static class MultiInstanceSetting { + + @Schema(description = "是否开启多实例", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "是否开启多实例不能为空") + private Boolean enable; + + @Schema(description = "是否串行", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "是否串行不能为空") + private Boolean sequential; + + @Schema(description = "完成比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "完成比例不能为空") + private Integer approveRatio; + + @Schema(description = "多实例来源类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "多实例来源类型不能为空") + @InEnum(BpmChildProcessMultiInstanceSourceTypeEnum.class) + private Integer sourceType; + + @Schema(description = "多实例来源", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "多实例来源不能为空") + private String source; + } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionRespVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionRespVO.java index 1e9dfc820..0ec737174 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionRespVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionRespVO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -8,7 +9,7 @@ import java.util.List; @Schema(description = "管理后台 - 流程定义 Response VO") @Data -public class BpmProcessDefinitionRespVO { +public class BpmProcessDefinitionRespVO extends BpmModelMetaInfoVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private String id; @@ -19,15 +20,9 @@ public class BpmProcessDefinitionRespVO { @Schema(description = "流程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") private String name; - @Schema(description = "流程标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao") + @Schema(description = "流程标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "youdao") private String key; - @Schema(description = "流程图标", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao.jpg") - private String icon; - - @Schema(description = "流程描述", example = "我是描述") - private String description; - @Schema(description = "流程分类", example = "1") private String category; @Schema(description = "流程分类名字", example = "请假") @@ -36,22 +31,15 @@ public class BpmProcessDefinitionRespVO { @Schema(description = "流程模型的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") private Integer modelType; // 参见 BpmModelTypeEnum 枚举类 - @Schema(description = "表单类型-参见 bpm_model_form_type 数据字典", example = "1") - private Integer formType; - @Schema(description = "表单编号-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", example = "1024") - private Long formId; - @Schema(description = "表单名字", example = "请假表单") - private String formName; + @Schema(description = "流程模型的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "ABC") + private String modelId; + @Schema(description = "表单的配置-JSON 字符串。在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", requiredMode = Schema.RequiredMode.REQUIRED) private String formConf; @Schema(description = "表单项的数组-JSON 字符串的数组。在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", requiredMode = Schema.RequiredMode.REQUIRED) private List formFields; - @Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", - example = "/bpm/oa/leave/create") - private String formCustomCreatePath; - @Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", - example = "/bpm/oa/leave/view") - private String formCustomViewPath; + @Schema(description = "表单名字", example = "请假表单") + private String formName; @Schema(description = "中断状态-参见 SuspensionState 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer suspensionState; // 参见 SuspensionState 枚举 @@ -67,7 +55,7 @@ public class BpmProcessDefinitionRespVO { @Schema(description = "流程定义排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long sort; - + @Schema(description = "BPMN UserTask 用户任务") @Data public static class UserTask { diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java index 3a847ce4e..931626167 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java @@ -1,8 +1,10 @@ package cn.iocoder.yudao.module.bpm.controller.admin.task; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*; import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert; @@ -30,10 +32,10 @@ import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; +import java.util.Set; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @Tag(name = "管理后台 - 流程实例") // 流程实例,通过流程定义创建的一次“申请” @@ -76,8 +78,14 @@ public class BpmProcessInstanceController { convertSet(processDefinitionMap.values(), ProcessDefinition::getCategory)); Map processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap( convertSet(pageResult.getList(), HistoricProcessInstance::getProcessDefinitionId)); + Set userIds = convertSet(pageResult.getList(), processInstance -> NumberUtils.parseLong(processInstance.getStartUserId())); + userIds.addAll(convertSetByFlatMap(taskMap.values(), + tasks -> tasks.stream().map(Task::getAssignee).filter(StrUtil::isNotBlank).map(Long::parseLong))); + Map userMap = adminUserApi.getUserMap(userIds); + Map deptMap = deptApi.getDeptMap( + convertSet(userMap.values(), AdminUserRespDTO::getDeptId)); return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstancePage(pageResult, - processDefinitionMap, categoryMap, taskMap, null, null, processDefinitionInfoMap)); + processDefinitionMap, categoryMap, taskMap, userMap, deptMap, processDefinitionInfoMap)); } @GetMapping("/manager-page") @@ -140,6 +148,7 @@ public class BpmProcessInstanceController { processDefinition, processDefinitionInfo, startUser, dept)); } + // TODO @lesan:【子流程】子流程如果取消,主流程应该是通过、还是不通过哈?还是禁用掉子流程的取消? @DeleteMapping("/cancel-by-start-user") @Operation(summary = "用户取消流程实例", description = "取消发起的流程") @PreAuthorize("@ss.hasPermission('bpm:process-instance:cancel')") @@ -162,10 +171,25 @@ public class BpmProcessInstanceController { @Operation(summary = "获得审批详情") @Parameter(name = "id", description = "流程实例的编号", required = true) @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')") + @SuppressWarnings("unchecked") public CommonResult getApprovalDetail(@Valid BpmApprovalDetailReqVO reqVO) { + if (StrUtil.isNotEmpty(reqVO.getProcessVariablesStr())) { + reqVO.setProcessVariables(JsonUtils.parseObject(reqVO.getProcessVariablesStr(), Map.class)); + } return success(processInstanceService.getApprovalDetail(getLoginUserId(), reqVO)); } + @GetMapping("/get-next-approval-nodes") + @Operation(summary = "获取下一个执行的流程节点") + @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')") + @SuppressWarnings("unchecked") + public CommonResult> getNextApprovalNodes(@Valid BpmApprovalDetailReqVO reqVO) { + if (StrUtil.isNotEmpty(reqVO.getProcessVariablesStr())) { + reqVO.setProcessVariables(JsonUtils.parseObject(reqVO.getProcessVariablesStr(), Map.class)); + } + return success(processInstanceService.getNextApprovalNodes(getLoginUserId(), reqVO)); + } + @GetMapping("/get-bpmn-model-view") @Operation(summary = "获取流程实例的 BPMN 模型视图", description = "在【流程详细】界面中,进行调用") @Parameter(name = "id", description = "流程实例的编号", required = true) diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailReqVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailReqVO.java index 9121f1036..069ec2000 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailReqVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailReqVO.java @@ -18,6 +18,9 @@ public class BpmApprovalDetailReqVO { @Schema(description = "流程变量") private Map processVariables; // 使用场景:同 processDefinitionId,用于流程预测 + @Schema(description = "流程变量") + private String processVariablesStr; // 解决 GET 无法传递对象的问题,最终转换成 processVariables 变量 + @Schema(description = "流程实例的编号", example = "1024") private String processInstanceId; // 使用场景:流程已发起时候传流程实例 ID diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceRespVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceRespVO.java index 76dca606a..f73c490a5 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceRespVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceRespVO.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance; import cn.iocoder.yudao.framework.common.core.KeyValue; import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO; +import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -73,6 +74,13 @@ public class BpmProcessInstanceRespVO { @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") private String name; + @Schema(description = "任务分配人编号", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "2048") + @JsonIgnore // 不返回,只是方便后续读取,赋值给 assigneeUser + private Long assignee; + + @Schema(description = "任务分配人", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "2048") + private UserSimpleBaseVO assigneeUser; + } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java index 40df86efc..0969fda13 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; +import java.util.List; import java.util.Map; @Schema(description = "管理后台 - 通过流程任务的 Request VO") @@ -23,4 +24,7 @@ public class BpmTaskApproveReqVO { @Schema(description = "变量实例(动态表单)", requiredMode = Schema.RequiredMode.REQUIRED) private Map variables; + @Schema(description = "下一个节点审批人", example = "{nodeId:[1, 2]}") + private Map> nextAssignees; // 为什么是 Map,而不是 List 呢?因为下一个节点可能是多个,例如说并行网关的情况 + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskPageReqVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskPageReqVO.java index 11c59ce3e..f129e5a31 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskPageReqVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskPageReqVO.java @@ -18,6 +18,9 @@ public class BpmTaskPageReqVO extends PageParam { @Schema(description = "流程分类", example = "1") private String category; + @Schema(description = "流程定义的标识", example = "2048") + private String processDefinitionKey; // 精准匹配 + @Schema(description = "创建时间") @DateTimeFormat(pattern = DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java index 83812ee1a..fce72a490 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java @@ -85,6 +85,9 @@ public class BpmTaskRespVO { @Schema(description = "是否填写审批意见", example = "false") private Boolean reasonRequire; + @Schema(description = "节点类型", example = "10") + private Integer nodeType; // 参见 BpmSimpleModelNodeTypeEnum 枚举。 + @Data @Schema(description = "流程实例") public static class ProcessInstance { diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java index 6e798e77f..ccd7f06e7 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java @@ -76,6 +76,15 @@ public interface BpmProcessInstanceConvert { respVO.setStartUser(BeanUtils.toBean(startUser, UserSimpleBaseVO.class)); MapUtils.findAndThen(deptMap, startUser.getDeptId(), dept -> respVO.getStartUser().setDeptName(dept.getName())); } + if (CollUtil.isNotEmpty(respVO.getTasks())) { + respVO.getTasks().forEach(task -> { + AdminUserRespDTO assigneeUser = userMap.get(task.getAssignee()); + if (assigneeUser!= null) { + task.setAssigneeUser(BeanUtils.toBean(assigneeUser, UserSimpleBaseVO.class)); + MapUtils.findAndThen(deptMap, assigneeUser.getDeptId(), dept -> task.getAssigneeUser().setDeptName(dept.getName())); + } + }); + } } // 摘要 respVO.setSummary(FlowableUtils.getSummary(processDefinitionInfoMap.get(respVO.getProcessDefinitionId()), diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java index ccab7b043..86c83ed61 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.bpm.dal.dataobject.definition; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; -import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; import cn.iocoder.yudao.module.bpm.enums.definition.BpmAutoApproveTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum; @@ -60,6 +59,14 @@ public class BpmProcessDefinitionInfoDO extends BaseDO { */ private Integer modelType; + /** + * 流程分类的编码 + * + * 关联 {@link BpmCategoryDO#getCode()} + * + * 为什么要存储?原因是,{@link ProcessDefinition#getCategory()} 无法设置 + */ + private String category; /** * 图标 */ @@ -149,7 +156,7 @@ public class BpmProcessDefinitionInfoDO extends BaseDO { * * 关联 {@link AdminUserRespDTO#getId()} 字段的数组 */ - @TableField(typeHandler = StringListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤 + @TableField(typeHandler = LongListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤 private List managerUserIds; /** @@ -175,11 +182,21 @@ public class BpmProcessDefinitionInfoDO extends BaseDO { */ @TableField(typeHandler = JacksonTypeHandler.class) private BpmModelMetaInfoVO.TitleSetting titleSetting; - /** * 摘要设置 */ @TableField(typeHandler = JacksonTypeHandler.class) private BpmModelMetaInfoVO.SummarySetting summarySetting; + /** + * 流程前置通知设置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private BpmModelMetaInfoVO.HttpRequestSetting processBeforeTriggerSetting; + /** + * 流程后置通知设置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private BpmModelMetaInfoVO.HttpRequestSetting processAfterTriggerSetting; + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java index d239dbe3f..57f4d393f 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java @@ -1,15 +1,21 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior; import cn.hutool.core.collection.CollUtil; -import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; -import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessMultiInstanceSourceTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import lombok.Setter; import org.flowable.bpmn.model.Activity; +import org.flowable.bpmn.model.CallActivity; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.bpmn.model.UserTask; import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior; +import java.util.List; import java.util.Set; /** @@ -42,27 +48,44 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav */ @Override protected int resolveNrOfInstances(DelegateExecution execution) { - // 第一步,设置 collectionVariable 和 CollectionVariable - // 从 execution.getVariable() 读取所有任务处理人的 key - super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的 - super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId()); - // 从 execution.getVariable() 读取当前所有任务处理的人的 key - super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId()); + // 情况一:UserTask 节点 + if (execution.getCurrentFlowElement() instanceof UserTask) { + // 第一步,设置 collectionVariable 和 CollectionVariable + // 从 execution.getVariable() 读取所有任务处理人的 key + super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的 + super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId()); + // 从 execution.getVariable() 读取当前所有任务处理的人的 key + super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId()); - // 第二步,获取任务的所有处理人 - @SuppressWarnings("unchecked") - Set assigneeUserIds = (Set) execution.getVariable(super.collectionVariable, Set.class); - if (assigneeUserIds == null) { - assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution); - if (CollUtil.isEmpty(assigneeUserIds)) { - // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过! - // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务 - // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时 - assigneeUserIds = SetUtils.asSet((Long) null); + // 第二步,获取任务的所有处理人 + @SuppressWarnings("unchecked") + Set assigneeUserIds = (Set) execution.getVariable(super.collectionVariable, Set.class); + if (assigneeUserIds == null) { + assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution); + if (CollUtil.isEmpty(assigneeUserIds)) { + // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过! + // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务 + // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时 + assigneeUserIds = SetUtils.asSet((Long) null); + } + execution.setVariableLocal(super.collectionVariable, assigneeUserIds); } - execution.setVariableLocal(super.collectionVariable, assigneeUserIds); + return assigneeUserIds.size(); } - return assigneeUserIds.size(); + + // 情况二:CallActivity 节点 + if (execution.getCurrentFlowElement() instanceof CallActivity) { + FlowElement flowElement = execution.getCurrentFlowElement(); + Integer sourceType = BpmnModelUtils.parseMultiInstanceSourceType(flowElement); + if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType())) { + return execution.getVariable(super.collectionExpression.getExpressionText(), Integer.class); + } + if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) { + return execution.getVariable(super.collectionExpression.getExpressionText(), List.class).size(); + } + } + + return super.resolveNrOfInstances(execution); } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java index b3a3a24f8..cb748182e 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java @@ -2,14 +2,20 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessMultiInstanceSourceTypeEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import lombok.Setter; import org.flowable.bpmn.model.Activity; +import org.flowable.bpmn.model.CallActivity; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.bpmn.model.UserTask; import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior; +import java.util.List; import java.util.Set; /** @@ -35,28 +41,45 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB */ @Override protected int resolveNrOfInstances(DelegateExecution execution) { - // 第一步,设置 collectionVariable 和 CollectionVariable - // 从 execution.getVariable() 读取所有任务处理人的 key - super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的 - super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId()); - // 从 execution.getVariable() 读取当前所有任务处理的人的 key - super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId()); + // 情况一:UserTask 节点 + if (execution.getCurrentFlowElement() instanceof UserTask) { + // 第一步,设置 collectionVariable 和 CollectionVariable + // 从 execution.getVariable() 读取所有任务处理人的 key + super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的 + super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId()); + // 从 execution.getVariable() 读取当前所有任务处理的人的 key + super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId()); - // 第二步,获取任务的所有处理人 - // 不使用 execution.getVariable 原因:目前依次审批任务回退后 collectionVariable 变量没有清理, 如果重新进入该任务不会重新分配审批人 - @SuppressWarnings("unchecked") - Set assigneeUserIds = (Set) execution.getVariableLocal(super.collectionVariable, Set.class); - if (assigneeUserIds == null) { - assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution); - if (CollUtil.isEmpty(assigneeUserIds)) { - // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过! - // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务 - // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时 - assigneeUserIds = SetUtils.asSet((Long) null); + // 第二步,获取任务的所有处理人 + // 不使用 execution.getVariable 原因:目前依次审批任务回退后 collectionVariable 变量没有清理, 如果重新进入该任务不会重新分配审批人 + @SuppressWarnings("unchecked") + Set assigneeUserIds = (Set) execution.getVariableLocal(super.collectionVariable, Set.class); + if (assigneeUserIds == null) { + assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution); + if (CollUtil.isEmpty(assigneeUserIds)) { + // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过! + // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务 + // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时 + assigneeUserIds = SetUtils.asSet((Long) null); + } + execution.setVariableLocal(super.collectionVariable, assigneeUserIds); } - execution.setVariableLocal(super.collectionVariable, assigneeUserIds); + return assigneeUserIds.size(); } - return assigneeUserIds.size(); + + // 情况二:CallActivity 节点 + if (execution.getCurrentFlowElement() instanceof CallActivity) { + FlowElement flowElement = execution.getCurrentFlowElement(); + Integer sourceType = BpmnModelUtils.parseMultiInstanceSourceType(flowElement); + if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType())) { + return execution.getVariable(super.collectionExpression.getExpressionText(), Integer.class); + } + if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) { + return execution.getVariable(super.collectionExpression.getExpressionText(), List.class).size(); + } + } + + return super.resolveNrOfInstances(execution); } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmUserTaskActivityBehavior.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmUserTaskActivityBehavior.java index cba5187b3..c4c8167c8 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmUserTaskActivityBehavior.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmUserTaskActivityBehavior.java @@ -10,7 +10,10 @@ import org.flowable.common.engine.impl.el.ExpressionManager; import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.impl.bpmn.behavior.UserTaskActivityBehavior; import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; +import org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntity; +import org.flowable.engine.impl.util.CommandContextUtil; import org.flowable.engine.impl.util.TaskHelper; +import org.flowable.engine.interceptor.CreateUserTaskBeforeContext; import org.flowable.task.service.TaskService; import org.flowable.task.service.impl.persistence.entity.TaskEntity; import org.springframework.transaction.annotation.Transactional; @@ -69,4 +72,15 @@ public class BpmUserTaskActivityBehavior extends UserTaskActivityBehavior { return CollUtil.get(candidateUserIds, index); } + @Override + protected void handleCategory(CreateUserTaskBeforeContext beforeContext, ExpressionManager expressionManager, + TaskEntity task, DelegateExecution execution) { + ProcessDefinitionEntity processDefinitionEntity = CommandContextUtil.getProcessDefinitionEntityManager().findById(execution.getProcessDefinitionId()); + if (processDefinitionEntity == null) { + log.warn("[handleCategory][任务编号({}) 找不到流程定义({})]", task.getId(), execution.getProcessDefinitionId()); + return; + } + task.setCategory(processDefinitionEntity.getCategory()); + } + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java index 952f0f1be..7f66b29d3 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java @@ -19,6 +19,7 @@ import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.CallActivity; import org.flowable.bpmn.model.FlowElement; import org.flowable.bpmn.model.UserTask; import org.flowable.engine.delegate.DelegateExecution; @@ -129,8 +130,12 @@ public class BpmTaskCandidateInvoker { public Set calculateUsersByActivity(BpmnModel bpmnModel, String activityId, Long startUserId, String processDefinitionId, Map processVariables) { - // 审批类型非人工审核时,不进行计算候选人。原因是:后续会自动通过、不通过 + // 如果是 CallActivity 子流程,不进行计算候选人 FlowElement flowElement = BpmnModelUtils.getFlowElementById(bpmnModel, activityId); + if (flowElement instanceof CallActivity) { + return new HashSet<>(); + } + // 审批类型非人工审核时,不进行计算候选人。原因是:后续会自动通过、不通过 Integer approveType = BpmnModelUtils.parseApproveType(flowElement); if (ObjectUtils.equalsAny(approveType, BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType(), diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateApproveUserSelectStrategy.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateApproveUserSelectStrategy.java new file mode 100644 index 000000000..a31692565 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateApproveUserSelectStrategy.java @@ -0,0 +1,78 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user.BpmTaskCandidateUserStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import com.google.common.collect.Sets; +import jakarta.annotation.Resource; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; + +/** + * 审批人自选 {@link BpmTaskCandidateUserStrategy} 实现类 + * 审批人在审批时选择下一个节点的审批人 + * + * @author smallNorthLee + */ +@Component +public class BpmTaskCandidateApproveUserSelectStrategy extends AbstractBpmTaskCandidateDeptLeaderStrategy { + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private BpmProcessInstanceService processInstanceService; + + @Override + public BpmTaskCandidateStrategyEnum getStrategy() { + return BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT; + } + + @Override + public void validateParam(String param) {} + + @Override + public boolean isParamRequired() { + return false; + } + + @Override + public LinkedHashSet calculateUsersByTask(DelegateExecution execution, String param) { + ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId()); + Assert.notNull(processInstance, "流程实例({})不能为空", execution.getProcessInstanceId()); + Map> approveUserSelectAssignees = FlowableUtils.getApproveUserSelectAssignees(processInstance); + Assert.notNull(approveUserSelectAssignees, "流程实例({}) 的下一个执行节点审批人不能为空", + execution.getProcessInstanceId()); + if (approveUserSelectAssignees == null) { + return Sets.newLinkedHashSet(); + } + // 获得审批人 + List assignees = approveUserSelectAssignees.get(execution.getCurrentActivityId()); + return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet(); + } + + @Override + public LinkedHashSet calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param, + Long startUserId, String processDefinitionId, Map processVariables) { + if (processVariables == null) { + return Sets.newLinkedHashSet(); + } + // 流程预测时会使用,允许审批人为空,如果为空前端会弹出提示选择下一个节点审批人,避免流程无法进行,审批时会真正校验节点是否配置审批人 + Map> approveUserSelectAssignees = FlowableUtils.getApproveUserSelectAssignees(processVariables); + if (approveUserSelectAssignees == null) { + return Sets.newLinkedHashSet(); + } + // 获得审批人 + List assignees = approveUserSelectAssignees.get(activityId); + return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet(); + } + +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategy.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategy.java index 9fd14d6de..9304d288a 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategy.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategy.java @@ -2,24 +2,21 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.d import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.ObjectUtil; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user.BpmTaskCandidateUserStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; -import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; import com.google.common.collect.Sets; import jakarta.annotation.Resource; import org.flowable.bpmn.model.BpmnModel; -import org.flowable.bpmn.model.ServiceTask; -import org.flowable.bpmn.model.Task; -import org.flowable.bpmn.model.UserTask; import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.runtime.ProcessInstance; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; -import java.util.*; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; /** * 发起人自选 {@link BpmTaskCandidateUserStrategy} 实现类 @@ -55,7 +52,7 @@ public class BpmTaskCandidateStartUserSelectStrategy extends AbstractBpmTaskCand execution.getProcessInstanceId()); // 获得审批人 List assignees = startUserSelectAssignees.get(execution.getCurrentActivityId()); - return new LinkedHashSet<>(assignees); + return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet(); } @Override @@ -70,28 +67,7 @@ public class BpmTaskCandidateStartUserSelectStrategy extends AbstractBpmTaskCand } // 获得审批人 List assignees = startUserSelectAssignees.get(activityId); - return new LinkedHashSet<>(assignees); - } - - /** - * 获得发起人自选审批人或抄送人的 Task 列表 - * - * @param bpmnModel BPMN 模型 - * @return Task 列表 - */ - public static List getStartUserSelectTaskList(BpmnModel bpmnModel) { - if (bpmnModel == null) { - return Collections.emptyList(); - } - List tasks = new ArrayList<>(); - tasks.addAll(BpmnModelUtils.getBpmnModelElements(bpmnModel, UserTask.class)); - tasks.addAll(BpmnModelUtils.getBpmnModelElements(bpmnModel, ServiceTask.class)); - if (CollUtil.isEmpty(tasks)) { - return Collections.emptyList(); - } - tasks.removeIf(task -> ObjectUtil.notEqual(BpmnModelUtils.parseCandidateStrategy(task), - BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy())); - return tasks; + return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet(); } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmTaskCandidateStrategyEnum.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmTaskCandidateStrategyEnum.java index 990bc6303..2a45e3a10 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmTaskCandidateStrategyEnum.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmTaskCandidateStrategyEnum.java @@ -24,7 +24,8 @@ public enum BpmTaskCandidateStrategyEnum implements ArrayValuable { MULTI_DEPT_LEADER_MULTI(23, "连续多级部门的负责人"), POST(22, "岗位"), USER(30, "用户"), - START_USER_SELECT(35, "发起人自选"), // 申请人自己,可在提交申请时选择此节点的审批人 + APPROVE_USER_SELECT(34, "审批人自身"), // 当前审批人,可在审批时,选择下一个节点的审批人 + START_USER_SELECT(35, "发起人自选"), // 申请人自己,可在提交申请时,选择此节点的审批人 START_USER(36, "发起人自己"), // 申请人自己, 一般紧挨开始节点,常用于发起人信息审核场景 START_USER_DEPT_LEADER(37, "发起人部门负责人"), START_USER_DEPT_LEADER_MULTI(38, "发起人连续多级部门的负责人"), diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java index 9dce8a2ea..e416428c4 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java @@ -1,5 +1,7 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.enums; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum; + /** * BPMN XML 常量信息 * @@ -66,6 +68,11 @@ public interface BpmnModelConstants { */ String USER_TASK_APPROVE_METHOD = "approveMethod"; + /** + * BPMN Child Process 的扩展属性,用于标记多实例来源类型 + */ + String CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE = "childProcessMultiInstanceSourceType"; + /** * BPMN ExtensionElement 流程表单字段权限元素, 用于标记字段权限 */ @@ -129,4 +136,11 @@ public interface BpmnModelConstants { */ String REASON_REQUIRE = "reasonRequire"; + /** + * 节点类型 + * + * 目前只有 {@link BpmModelTypeEnum#SIMPLE} 的 UserTask 节点会设置该属性,用于区分是审批节点、还是办理节点 + */ + String NODE_TYPE = "nodeType"; + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java index 8172cf59a..893c4d053 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java @@ -27,8 +27,16 @@ public class BpmnVariableConstants { * 流程实例的变量 - 发起用户选择的审批人 Map * * @see ProcessInstance#getProcessVariables() + * @see BpmTaskCandidateStrategyEnum#START_USER_SELECT */ public static final String PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES = "PROCESS_START_USER_SELECT_ASSIGNEES"; + /** + * 流程实例的变量 - 审批人选择的审批人 Map + * + * @see ProcessInstance#getProcessVariables() + * @see BpmTaskCandidateStrategyEnum#APPROVE_USER_SELECT + */ + public static final String PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES = "PROCESS_APPROVE_USER_SELECT_ASSIGNEES"; /** * 流程实例的变量 - 发起用户 ID * @@ -51,6 +59,13 @@ public class BpmnVariableConstants { */ public static final String PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED = "_FLOWABLE_SKIP_EXPRESSION_ENABLED"; + /** + * 流程实例的变量 - 用于判断流程是否需要跳过发起人节点 + * + * @see ProcessInstance#getProcessVariables() + */ + public static final String PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE = "PROCESS_SKIP_START_USER_NODE"; + /** * 流程实例的变量 - 流程开始时间 * diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java index 5251aea64..ba2aaa6bc 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java @@ -22,6 +22,7 @@ import java.util.Set; public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEventListener { public static final Set PROCESS_INSTANCE_EVENTS = ImmutableSet.builder() + .add(FlowableEngineEventType.PROCESS_CREATED) .add(FlowableEngineEventType.PROCESS_COMPLETED) .add(FlowableEngineEventType.PROCESS_CANCELLED) .build(); @@ -34,6 +35,11 @@ public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEvent super(PROCESS_INSTANCE_EVENTS); } + @Override + protected void processCreated(FlowableEngineEntityEvent event) { + processInstanceService.processProcessInstanceCreated((ProcessInstance)event.getEntity()); + } + @Override protected void processCompleted(FlowableEngineEntityEvent event) { processInstanceService.processProcessInstanceCompleted((ProcessInstance)event.getEntity()); diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java index ecef8fdb4..329241f79 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java @@ -109,7 +109,11 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener { // 2.2 延迟器超时处理 } else if (ObjectUtil.equal(bpmTimerBoundaryEventType, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT)) { String taskKey = boundaryEvent.getAttachedToRefId(); - taskService.processDelayTimerTimeout(event.getProcessInstanceId(), taskKey); + taskService.triggerTask(event.getProcessInstanceId(), taskKey); + // 2.3 子流程超时处理 + } else if (ObjectUtil.equal(bpmTimerBoundaryEventType, BpmBoundaryEventTypeEnum.CHILD_PROCESS_TIMEOUT)) { + String taskKey = boundaryEvent.getAttachedToRefId(); + taskService.processChildProcessTimeout(event.getProcessInstanceId(), taskKey); } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java new file mode 100644 index 000000000..2503c0fff --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java @@ -0,0 +1,158 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmHttpRequestParamTypeEnum; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import com.fasterxml.jackson.core.type.TypeReference; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_INSTANCE_HTTP_TRIGGER_CALL_ERROR; + +/** + * 工作流发起 HTTP 请求工具类 + * + * @author 芋道源码 + */ +@Slf4j +public class BpmHttpRequestUtils { + + public static void executeBpmHttpRequest(ProcessInstance processInstance, + String url, + List headerParams, + List bodyParams, + Boolean handleResponse, + List> response) { + RestTemplate restTemplate = SpringUtils.getBean(RestTemplate.class); + BpmProcessInstanceService processInstanceService = SpringUtils.getBean(BpmProcessInstanceService.class); + + // 1.1 设置请求头 + MultiValueMap headers = buildHttpHeaders(processInstance, headerParams); + // 1.2 设置请求体 + MultiValueMap body = buildHttpBody(processInstance, bodyParams); + + // 2. 发起请求 + ResponseEntity responseEntity = sendHttpRequest(url, headers, body, restTemplate); + + // 3. 处理返回 + if (Boolean.FALSE.equals(handleResponse)) { + return; + } + // 3.1 判断是否需要解析返回值 + if (responseEntity == null + || StrUtil.isEmpty(responseEntity.getBody()) + || !responseEntity.getStatusCode().is2xxSuccessful() + || CollUtil.isEmpty(response)) { + return; + } + // 3.2 解析返回值, 返回值必须符合 CommonResult 规范。 + CommonResult> respResult = JsonUtils.parseObjectQuietly(responseEntity.getBody(), + new TypeReference<>() {}); + if (respResult == null || !respResult.isSuccess()) { + return; + } + // 3.3 获取需要更新的流程变量 + Map updateVariables = getNeedUpdatedVariablesFromResponse(respResult.getData(), response); + // 3.4 更新流程变量 + if (CollUtil.isNotEmpty(updateVariables)) { + processInstanceService.updateProcessInstanceVariables(processInstance.getId(), updateVariables); + } + } + + public static ResponseEntity sendHttpRequest(String url, + MultiValueMap headers, + MultiValueMap body, + RestTemplate restTemplate) { + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + ResponseEntity responseEntity; + try { + responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); + log.info("[sendHttpRequest][HTTP 触发器,请求头:{},请求体:{},响应结果:{}]", headers, body, responseEntity); + } catch (RestClientException e) { + log.error("[sendHttpRequest][HTTP 触发器,请求头:{},请求体:{},请求出错:{}]", headers, body, e.getMessage()); + throw exception(PROCESS_INSTANCE_HTTP_TRIGGER_CALL_ERROR); + } + return responseEntity; + } + + public static MultiValueMap buildHttpHeaders(ProcessInstance processInstance, + List headerSettings) { + Map processVariables = processInstance.getProcessVariables(); + MultiValueMap headers = new LinkedMultiValueMap<>(); + headers.add(HEADER_TENANT_ID, processInstance.getTenantId()); + addHttpRequestParam(headers, headerSettings, processVariables); + return headers; + } + + public static MultiValueMap buildHttpBody(ProcessInstance processInstance, + List bodySettings) { + Map processVariables = processInstance.getProcessVariables(); + MultiValueMap body = new LinkedMultiValueMap<>(); + addHttpRequestParam(body, bodySettings, processVariables); + body.add("processInstanceId", processInstance.getId()); + return body; + } + + /** + * 从请求返回值获取需要更新的流程变量 + * + * @param result 请求返回结果 + * @param responseSettings 返回设置 + * @return 需要更新的流程变量 + */ + public static Map getNeedUpdatedVariablesFromResponse(Map result, + List> responseSettings) { + Map updateVariables = new HashMap<>(); + if (CollUtil.isEmpty(result)) { + return updateVariables; + } + responseSettings.forEach(responseSetting -> { + if (StrUtil.isNotEmpty(responseSetting.getKey()) && result.containsKey(responseSetting.getValue())) { + updateVariables.put(responseSetting.getKey(), result.get(responseSetting.getValue())); + } + }); + return updateVariables; + } + + /** + * 添加 HTTP 请求参数。请求头或者请求体 + * + * @param params HTTP 请求参数 + * @param paramSettings HTTP 请求参数设置 + * @param processVariables 流程变量 + */ + public static void addHttpRequestParam(MultiValueMap params, + List paramSettings, + Map processVariables) { + if (CollUtil.isEmpty(paramSettings)) { + return; + } + paramSettings.forEach(item -> { + if (item.getType().equals(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType())) { + params.add(item.getKey(), item.getValue()); + } else if (item.getType().equals(BpmHttpRequestParamTypeEnum.FROM_FORM.getType())) { + params.add(item.getKey(), processVariables.get(item.getValue()).toString()); + } + }); + } + +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java index 3f22c7c25..1cccf18f0 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java @@ -158,6 +158,17 @@ public class BpmnModelUtils { return NumberUtils.parseInt(parseExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_TYPE)); } + /** + * 解析子流程多实例来源类型 + * + * @see BpmChildProcessMultiInstanceSourceTypeEnum + * @param element 任务节点 + * @return 多实例来源类型 + */ + public static Integer parseMultiInstanceSourceType(FlowElement element) { + return NumberUtils.parseInt(parseExtensionElement(element, BpmnModelConstants.CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE)); + } + /** * 添加任务拒绝处理元素 * @@ -410,6 +421,26 @@ public class BpmnModelUtils { return parseExtensionElement(flowElement, TRIGGER_PARAM); } + /** + * 给节点添加节点类型 + * + * @param nodeType 节点类型 + * @param flowElement 节点 + */ + public static void addNodeType(Integer nodeType, FlowElement flowElement) { + addExtensionElement(flowElement, BpmnModelConstants.NODE_TYPE, nodeType); + } + + /** + * 解析节点类型 + * + * @param flowElement 节点 + * @return 节点类型 + */ + public static Integer parseNodeType(FlowElement flowElement) { + return NumberUtils.parseInt(parseExtensionElement(flowElement, BpmnModelConstants.NODE_TYPE)); + } + // ========== BPM 简单查找相关的方法 ========== /** @@ -777,71 +808,206 @@ public class BpmnModelUtils { // 情况:ExclusiveGateway 排它,只有一个满足条件的。如果没有,就走默认的 if (currentElement instanceof ExclusiveGateway) { // 查找满足条件的 SequenceFlow 路径 - Gateway gateway = (Gateway) currentElement; - SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), - flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()) - && evalConditionExpress(variables, flow.getConditionExpression())); - if (matchSequenceFlow == null) { - matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), - flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId())); - // 特殊:没有默认的情况下,并且只有 1 个条件,则认为它是默认的 - if (matchSequenceFlow == null && gateway.getOutgoingFlows().size() == 1) { - matchSequenceFlow = gateway.getOutgoingFlows().get(0); - } - } + SequenceFlow matchSequenceFlow = findMatchSequenceFlowByExclusiveGateway((Gateway) currentElement, variables); // 遍历满足条件的 SequenceFlow 路径 if (matchSequenceFlow != null) { simulateNextFlowElements(matchSequenceFlow.getTargetFlowElement(), variables, resultElements, visitElements); } - return; } - // 情况:InclusiveGateway 包容,多个满足条件的。如果没有,就走默认的 - if (currentElement instanceof InclusiveGateway) { + else if (currentElement instanceof InclusiveGateway) { // 查找满足条件的 SequenceFlow 路径 - Gateway gateway = (Gateway) currentElement; - Collection matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(), - flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()) - && evalConditionExpress(variables, flow.getConditionExpression())); - if (CollUtil.isEmpty(matchSequenceFlows)) { - matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(), - flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId())); - // 特殊:没有默认的情况下,并且只有 1 个条件,则认为它是默认的 - if (CollUtil.isEmpty(matchSequenceFlows) && gateway.getOutgoingFlows().size() == 1) { - matchSequenceFlows = gateway.getOutgoingFlows(); - } - } + Collection matchSequenceFlows = findMatchSequenceFlowsByInclusiveGateway((Gateway) currentElement, variables); // 遍历满足条件的 SequenceFlow 路径 matchSequenceFlows.forEach( flow -> simulateNextFlowElements(flow.getTargetFlowElement(), variables, resultElements, visitElements)); } - // 情况:ParallelGateway 并行,都满足,都走 - if (currentElement instanceof ParallelGateway) { + else if (currentElement instanceof ParallelGateway) { Gateway gateway = (Gateway) currentElement; // 遍历子节点 gateway.getOutgoingFlows().forEach( nextElement -> simulateNextFlowElements(nextElement.getTargetFlowElement(), variables, resultElements, visitElements)); - return; } } + /** + * 根据当前节点,获取下一个节点 + * + * @param currentElement 当前节点 + * @param bpmnModel BPMN模型 + * @param variables 流程变量 + */ + @SuppressWarnings("PatternVariableCanBeUsed") + public static List getNextFlowNodes(FlowElement currentElement, BpmnModel bpmnModel, + Map variables){ + List nextFlowNodes = new ArrayList<>(); // 下一个执行的流程节点集合 + FlowNode currentNode = (FlowNode) currentElement; // 当前执行节点的基本属性 + List outgoingFlows = currentNode.getOutgoingFlows(); // 当前节点的关联节点 + if (CollUtil.isEmpty(outgoingFlows)) { + log.warn("[getNextFlowNodes][当前节点({}) 的 outgoingFlows 为空]", currentNode.getId()); + return nextFlowNodes; + } + + // 遍历每个出口流 + for (SequenceFlow outgoingFlow : outgoingFlows) { + // 获取目标节点的基本属性 + FlowElement targetElement = bpmnModel.getFlowElement(outgoingFlow.getTargetRef()); + if (targetElement == null) { + continue; + } + // 如果是结束节点,直接返回 + if (targetElement instanceof EndEvent) { + break; + } + // 情况一:处理不同类型的网关 + if (targetElement instanceof Gateway) { + Gateway gateway = (Gateway) targetElement; + if (gateway instanceof ExclusiveGateway) { + handleExclusiveGateway(gateway, bpmnModel, variables, nextFlowNodes); + } else if (gateway instanceof InclusiveGateway) { + handleInclusiveGateway(gateway, bpmnModel, variables, nextFlowNodes); + } else if (gateway instanceof ParallelGateway) { + handleParallelGateway(gateway, bpmnModel, variables, nextFlowNodes); + } + } else { + // 情况二:如果不是网关,直接添加到下一个节点列表 + nextFlowNodes.add((FlowNode) targetElement); + } + } + return nextFlowNodes; + } + + /** + * 处理排它网关 + * + * @param gateway 排他网关 + * @param bpmnModel BPMN模型 + * @param variables 流程变量 + * @param nextFlowNodes 下一个执行的流程节点集合 + */ + private static void handleExclusiveGateway(Gateway gateway, BpmnModel bpmnModel, + Map variables, List nextFlowNodes) { + // 查找满足条件的 SequenceFlow 路径 + SequenceFlow matchSequenceFlow = findMatchSequenceFlowByExclusiveGateway(gateway, variables); + // 遍历满足条件的 SequenceFlow 路径 + if (matchSequenceFlow != null) { + FlowElement targetElement = bpmnModel.getFlowElement(matchSequenceFlow.getTargetRef()); + if (targetElement instanceof FlowNode) { + nextFlowNodes.add((FlowNode) targetElement); + } + } + } + + /** + * 处理排它网关(Exclusive Gateway),选择符合条件的路径 + * + * @param gateway 排他网关 + * @param variables 流程变量 + * @return 符合条件的路径 + */ + private static SequenceFlow findMatchSequenceFlowByExclusiveGateway(Gateway gateway, Map variables) { + SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), + flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()) + && (evalConditionExpress(variables, flow.getConditionExpression()))); + if (matchSequenceFlow == null) { + matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), + flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId())); + // 特殊:没有默认的情况下,并且只有 1 个条件,则认为它是默认的 + if (matchSequenceFlow == null && gateway.getOutgoingFlows().size() == 1) { + matchSequenceFlow = gateway.getOutgoingFlows().get(0); + } + } + return matchSequenceFlow; + } + + /** + * 处理包容网关 + * + * @param gateway 排他网关 + * @param bpmnModel BPMN模型 + * @param variables 流程变量 + * @param nextFlowNodes 下一个执行的流程节点集合 + */ + private static void handleInclusiveGateway(Gateway gateway, BpmnModel bpmnModel, + Map variables, List nextFlowNodes) { + // 查找满足条件的 SequenceFlow 路径集合 + Collection matchSequenceFlows = findMatchSequenceFlowsByInclusiveGateway(gateway, variables); + // 遍历满足条件的 SequenceFlow 路径,获取目标节点 + matchSequenceFlows.forEach(flow -> { + FlowElement targetElement = bpmnModel.getFlowElement(flow.getTargetRef()); + if (targetElement instanceof FlowNode) { + nextFlowNodes.add((FlowNode) targetElement); + } + }); + } + + /** + * 处理排它网关(Inclusive Gateway),选择符合条件的路径 + * + * @param gateway 排他网关 + * @param variables 流程变量 + * @return 符合条件的路径 + */ + private static Collection findMatchSequenceFlowsByInclusiveGateway(Gateway gateway, Map variables) { + // 查找满足条件的 SequenceFlow 路径 + Collection matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(), + flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()) + && evalConditionExpress(variables, flow.getConditionExpression())); + if (CollUtil.isEmpty(matchSequenceFlows)) { + matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(), + flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId())); + // 特殊:没有默认的情况下,并且只有 1 个条件,则认为它是默认的 + if (CollUtil.isEmpty(matchSequenceFlows) && gateway.getOutgoingFlows().size() == 1) { + matchSequenceFlows = gateway.getOutgoingFlows(); + } + } + return matchSequenceFlows; + } + + + /** + * 处理并行网关 + * + * @param gateway 排他网关 + * @param bpmnModel BPMN模型 + * @param variables 流程变量 + * @param nextFlowNodes 下一个执行的流程节点集合 + */ + private static void handleParallelGateway(Gateway gateway, BpmnModel bpmnModel, + Map variables, List nextFlowNodes) { + // 并行网关,遍历所有出口路径,获取目标节点 + gateway.getOutgoingFlows().forEach(flow -> { + FlowElement targetElement = bpmnModel.getFlowElement(flow.getTargetRef()); + if (targetElement instanceof FlowNode) { + nextFlowNodes.add((FlowNode) targetElement); + } + }); + } + /** * 计算条件表达式是否为 true 满足条件 * * @param variables 流程实例 - * @param express 条件表达式 + * @param expression 条件表达式 * @return 是否满足条件 */ - public static boolean evalConditionExpress(Map variables, String express) { - if (express == null) { + public static boolean evalConditionExpress(Map variables, String expression) { + if (expression == null) { return Boolean.FALSE; } + // 如果 variables 为空,则创建一个的原因?可能 expression 的计算,不依赖于 variables + if (variables == null) { + variables = new HashMap<>(); + } + + // 执行计算 try { - Object result = FlowableUtils.getExpressionValue(variables, express); + Object result = FlowableUtils.getExpressionValue(variables, expression); return Boolean.TRUE.equals(result); } catch (FlowableException ex) { - log.error("[evalConditionExpress][条件表达式({}) 变量({}) 解析报错]", express, variables, ex); + // 为什么使用 info 日志?原因是,expression 如果从 variables 取不到值,会报错。实际这种情况下,可以忽略 + log.info("[evalConditionExpress][条件表达式({}) 变量({}) 解析报错]", expression, variables, ex); return Boolean.FALSE; } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java index a458567d8..67c24bb9f 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.util; +import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.core.KeyValue; @@ -24,7 +25,10 @@ import org.flowable.engine.impl.util.CommandContextUtil; import org.flowable.engine.runtime.ProcessInstance; import org.flowable.task.api.TaskInfo; -import java.util.*; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.Callable; import java.util.stream.Collectors; @@ -190,12 +194,37 @@ public class FlowableUtils { @SuppressWarnings("unchecked") public static Map> getStartUserSelectAssignees(Map processVariables) { if (processVariables == null) { - return null; + return new HashMap<>(); } return (Map>) processVariables.get( BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES); } + /** + * 获得流程实例的审批用户选择的下一个节点的审批人 Map + * + * @param processInstance 流程实例 + * @return 审批用户选择的下一个节点的审批人Map + */ + public static Map> getApproveUserSelectAssignees(ProcessInstance processInstance) { + return processInstance != null ? getApproveUserSelectAssignees(processInstance.getProcessVariables()) : null; + } + + /** + * 获得流程实例的审批用户选择的下一个节点的审批人 Map + * + * @param processVariables 流程变量 + * @return 审批用户选择的下一个节点的审批人Map Map + */ + @SuppressWarnings("unchecked") + public static Map> getApproveUserSelectAssignees(Map processVariables) { + if (processVariables == null) { + return new HashMap<>(); + } + return (Map>) processVariables.get( + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES); + } + /** * 获得流程实例的摘要 * @@ -240,7 +269,7 @@ public class FlowableUtils { return formFieldsMap.entrySet().stream() .limit(3) .map(entry -> new KeyValue<>(entry.getValue().getTitle(), - processVariables.getOrDefault(entry.getValue().getField(), "").toString())) + MapUtil.getStr(processVariables, entry.getValue().getField(), ""))) .collect(Collectors.toList()); } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java index 309c11cf8..3b5bad52c 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java @@ -5,25 +5,27 @@ import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.*; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.ConditionGroups; import cn.iocoder.yudao.module.bpm.enums.definition.*; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; -import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; import cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmCopyTaskDelegate; import cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmTriggerTaskDelegate; +import cn.iocoder.yudao.module.bpm.service.task.listener.BpmCallActivityListener; +import cn.iocoder.yudao.module.bpm.service.task.listener.BpmUserTaskListener; import org.flowable.bpmn.BpmnAutoLayout; import org.flowable.bpmn.constants.BpmnXMLConstants; import org.flowable.bpmn.model.Process; import org.flowable.bpmn.model.*; +import org.flowable.engine.delegate.ExecutionListener; import org.flowable.engine.delegate.TaskListener; -import org.springframework.util.MultiValueMap; import java.util.*; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.*; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*; -import static cn.iocoder.yudao.module.bpm.service.task.listener.BpmUserTaskListener.DELEGATE_EXPRESSION; import static java.util.Arrays.asList; /** @@ -40,9 +42,10 @@ public class SimpleModelUtils { static { List converts = asList(new StartNodeConvert(), new EndNodeConvert(), - new StartUserNodeConvert(), new ApproveNodeConvert(), new CopyNodeConvert(), + new StartUserNodeConvert(), new ApproveNodeConvert(), new CopyNodeConvert(), new TransactorNodeConvert(), new DelayTimerNodeConvert(), new TriggerNodeConvert(), - new ConditionBranchNodeConvert(), new ParallelBranchNodeConvert(), new InclusiveBranchNodeConvert(), new RouteBranchNodeConvert()); + new ConditionBranchNodeConvert(), new ParallelBranchNodeConvert(), new InclusiveBranchNodeConvert(), new RouteBranchNodeConvert(), + new ChildProcessConvert()); converts.forEach(convert -> NODE_CONVERTS.put(convert.getType(), convert)); } @@ -78,7 +81,7 @@ public class SimpleModelUtils { traverseNodeToBuildFlowNode(startNode, process); // 3. 构建并添加节点之间的连线 Sequence Flow - EndEvent endEvent = BpmnModelUtils.getEndEvent(bpmnModel); + EndEvent endEvent = getEndEvent(bpmnModel); traverseNodeToBuildSequenceFlow(process, startNode, endEvent.getId()); // 4. 自动布局 @@ -164,8 +167,16 @@ public class SimpleModelUtils { // 情况一:有“子”节点,则建立连线 // 情况二:没有“子节点”,则直接跟 targetNodeId 建立连线。例如说,结束节点、条件分支(分支节点的孩子节点或聚合节点)的最后一个节点 String finalTargetNodeId = isChildNodeValid ? childNode.getId() : targetNodeId; - SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), finalTargetNodeId); - process.addFlowElement(sequenceFlow); + + // 如果没有附加节点:则直接建立连线 + if (StrUtil.isEmpty(node.getAttachNodeId())) { + SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), finalTargetNodeId); + process.addFlowElement(sequenceFlow); + } else { + // 如果有附加节点:需要先建立和附加节点的连线,再建立附加节点和目标节点的连线。例如说,触发器节点(HTTP 回调) + List sequenceFlows = buildAttachNodeSequenceFlow(node.getId(), node.getAttachNodeId(), finalTargetNodeId); + sequenceFlows.forEach(process::addFlowElement); + } // 因为有子节点,递归调用后续子节点 if (isChildNodeValid) { @@ -173,6 +184,19 @@ public class SimpleModelUtils { } } + /** + * 构建有附加节点的连线 + * + * @param nodeId 当前节点 ID + * @param attachNodeId 附属节点 ID + * @param targetNodeId 目标节点 ID + */ + private static List buildAttachNodeSequenceFlow(String nodeId, String attachNodeId, String targetNodeId) { + SequenceFlow sequenceFlow = buildBpmnSequenceFlow(nodeId, attachNodeId, null, null, null); + SequenceFlow attachSequenceFlow = buildBpmnSequenceFlow(attachNodeId, targetNodeId, null, null, null); + return CollUtil.newArrayList(sequenceFlow, attachSequenceFlow); + } + /** * 遍历条件节点,构建 SequenceFlow 元素 * @@ -337,7 +361,7 @@ public class SimpleModelUtils { userTask.setName(node.getName()); // 人工审批 - addExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_TYPE, BpmUserTaskApproveTypeEnum.USER.getType()); + addExtensionElement(userTask, USER_TASK_APPROVE_TYPE, BpmUserTaskApproveTypeEnum.USER.getType()); // 候选人策略为发起人自己 addCandidateElements(BpmTaskCandidateStrategyEnum.START_USER.getStrategy(), null, userTask); // 添加表单字段权限属性元素 @@ -388,24 +412,17 @@ public class SimpleModelUtils { */ private BoundaryEvent buildUserTaskTimeoutBoundaryEvent(UserTask userTask, BpmSimpleModelNodeVO.TimeoutHandler timeoutHandler) { - // 1.1 定时器边界事件 - BoundaryEvent boundaryEvent = new BoundaryEvent(); - boundaryEvent.setId("Event-" + IdUtil.fastUUID()); - boundaryEvent.setCancelActivity(false); // 设置关联的任务为不会被中断 - boundaryEvent.setAttachedToRef(userTask); - // 1.2 定义超时时间、最大提醒次数 - TimerEventDefinition eventDefinition = new TimerEventDefinition(); - eventDefinition.setTimeDuration(timeoutHandler.getTimeDuration()); + // 1. 创建 Timeout Boundary Event + String timeCycle = null; if (Objects.equals(BpmUserTaskTimeoutHandlerTypeEnum.REMINDER.getType(), timeoutHandler.getType()) && timeoutHandler.getMaxRemindCount() != null && timeoutHandler.getMaxRemindCount() > 1) { - eventDefinition.setTimeCycle(String.format("R%d/%s", - timeoutHandler.getMaxRemindCount(), timeoutHandler.getTimeDuration())); + timeCycle = String.format("R%d/%s", + timeoutHandler.getMaxRemindCount(), timeoutHandler.getTimeDuration()); } - boundaryEvent.addEventDefinition(eventDefinition); + BoundaryEvent boundaryEvent = buildTimeoutBoundaryEvent(userTask, BpmBoundaryEventTypeEnum.USER_TASK_TIMEOUT.getType(), + timeoutHandler.getTimeDuration(), timeCycle, null); - // 2.1 添加定时器边界事件类型 - addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, BpmBoundaryEventTypeEnum.USER_TASK_TIMEOUT.getType()); - // 2.2 添加超时执行动作元素 + // 2 添加超时执行动作元素 addExtensionElement(boundaryEvent, USER_TASK_TIMEOUT_HANDLER_TYPE, timeoutHandler.getType()); return boundaryEvent; } @@ -445,6 +462,8 @@ public class SimpleModelUtils { addSignEnable(node.getSignEnable(), userTask); // 审批意见 addReasonRequire(node.getReasonRequire(), userTask); + // 节点类型 + addNodeType(node.getType(), userTask); return userTask; } @@ -455,7 +474,7 @@ public class SimpleModelUtils { FlowableListener flowableListener = new FlowableListener(); flowableListener.setEvent(TaskListener.EVENTNAME_CREATE); flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); - flowableListener.setImplementation(DELEGATE_EXPRESSION); + flowableListener.setImplementation(BpmUserTaskListener.DELEGATE_EXPRESSION); addListenerConfig(flowableListener, node.getTaskCreateListener()); flowableListeners.add(flowableListener); } @@ -464,7 +483,7 @@ public class SimpleModelUtils { FlowableListener flowableListener = new FlowableListener(); flowableListener.setEvent(TaskListener.EVENTNAME_ASSIGNMENT); flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); - flowableListener.setImplementation(DELEGATE_EXPRESSION); + flowableListener.setImplementation(BpmUserTaskListener.DELEGATE_EXPRESSION); addListenerConfig(flowableListener, node.getTaskAssignListener()); flowableListeners.add(flowableListener); } @@ -473,7 +492,7 @@ public class SimpleModelUtils { FlowableListener flowableListener = new FlowableListener(); flowableListener.setEvent(TaskListener.EVENTNAME_COMPLETE); flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); - flowableListener.setImplementation(DELEGATE_EXPRESSION); + flowableListener.setImplementation(BpmUserTaskListener.DELEGATE_EXPRESSION); addListenerConfig(flowableListener, node.getTaskCompleteListener()); flowableListeners.add(flowableListener); } @@ -486,7 +505,7 @@ public class SimpleModelUtils { BpmUserTaskApproveMethodEnum approveMethodEnum = BpmUserTaskApproveMethodEnum.valueOf(approveMethod); Assert.notNull(approveMethodEnum, "审批方式({})不能为空", approveMethodEnum); // 添加审批方式的扩展属性 - addExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_METHOD, approveMethod); + addExtensionElement(userTask, USER_TASK_APPROVE_METHOD, approveMethod); if (approveMethodEnum == BpmUserTaskApproveMethodEnum.RANDOM) { // 随机审批,不需要设置多实例属性 return; @@ -514,6 +533,15 @@ public class SimpleModelUtils { } + private static class TransactorNodeConvert extends ApproveNodeConvert { + + @Override + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.TRANSACTOR_NODE; + } + + } + private static class CopyNodeConvert implements NodeConvert { @Override @@ -684,20 +712,16 @@ public class SimpleModelUtils { // 2. 添加接收任务的 Timer Boundary Event if (node.getDelaySetting() != null) { - // 2.1 定时器边界事件 - BoundaryEvent boundaryEvent = new BoundaryEvent(); - boundaryEvent.setId("Event-" + IdUtil.fastUUID()); - boundaryEvent.setCancelActivity(false); - boundaryEvent.setAttachedToRef(receiveTask); - // 2.2 定义超时时间 - TimerEventDefinition eventDefinition = new TimerEventDefinition(); + BoundaryEvent boundaryEvent = null; if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())) { - eventDefinition.setTimeDuration(node.getDelaySetting().getDelayTime()); + boundaryEvent = buildTimeoutBoundaryEvent(receiveTask, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(), + node.getDelaySetting().getDelayTime(), null, null); } else if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())) { - eventDefinition.setTimeDate(node.getDelaySetting().getDelayTime()); + boundaryEvent = buildTimeoutBoundaryEvent(receiveTask, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(), + null, null, node.getDelaySetting().getDelayTime()); + } else { + throw new UnsupportedOperationException("不支持的延迟类型:" + node.getDelaySetting()); } - boundaryEvent.addEventDefinition(eventDefinition); - addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType()); flowElements.add(boundaryEvent); } return flowElements; @@ -712,23 +736,36 @@ public class SimpleModelUtils { public static class TriggerNodeConvert implements NodeConvert { @Override - public ServiceTask convert(BpmSimpleModelNodeVO node) { + public List convertList(BpmSimpleModelNodeVO node) { + Assert.notNull(node.getTriggerSetting(), "触发器节点设置不能为空"); + List flowElements = new ArrayList<>(2); + // HTTP 回调请求。需要附加一个 ReceiveTask、发起请求后、等待回调执行 + if (BpmTriggerTypeEnum.HTTP_CALLBACK.getType().equals(node.getTriggerSetting().getType())) { + Assert.notNull(node.getTriggerSetting().getHttpRequestSetting(), "触发器 HTTP 回调请求设置不能为空"); + ReceiveTask receiveTask = new ReceiveTask(); + receiveTask.setId("Activity_" + IdUtil.fastUUID()); + receiveTask.setName("HTTP 回调"); + node.setAttachNodeId(receiveTask.getId()); + flowElements.add(receiveTask); + // 重要:设置 callbackTaskDefineKey,用于 HTTP 回调 + node.getTriggerSetting().getHttpRequestSetting().setCallbackTaskDefineKey(receiveTask.getId()); + } + // 触发器使用 ServiceTask 来实现 ServiceTask serviceTask = new ServiceTask(); serviceTask.setId(node.getId()); serviceTask.setName(node.getName()); serviceTask.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); serviceTask.setImplementation("${" + BpmTriggerTaskDelegate.BEAN_NAME + "}"); - if (node.getTriggerSetting() != null) { - addExtensionElement(serviceTask, TRIGGER_TYPE, node.getTriggerSetting().getType()); - if (node.getTriggerSetting().getHttpRequestSetting() != null) { - addExtensionElementJson(serviceTask, TRIGGER_PARAM, node.getTriggerSetting().getHttpRequestSetting()); - } - if (node.getTriggerSetting().getNormalFormSetting() != null) { - addExtensionElementJson(serviceTask, TRIGGER_PARAM, node.getTriggerSetting().getNormalFormSetting()); - } + addExtensionElement(serviceTask, TRIGGER_TYPE, node.getTriggerSetting().getType()); + if (node.getTriggerSetting().getHttpRequestSetting() != null) { + addExtensionElementJson(serviceTask, TRIGGER_PARAM, node.getTriggerSetting().getHttpRequestSetting()); } - return serviceTask; + if (node.getTriggerSetting().getFormSettings() != null) { + addExtensionElementJson(serviceTask, TRIGGER_PARAM, node.getTriggerSetting().getFormSettings()); + } + flowElements.add(serviceTask); + return flowElements; } @Override @@ -762,10 +799,131 @@ public class SimpleModelUtils { } + private static class ChildProcessConvert implements NodeConvert { + + @Override + public List convertList(BpmSimpleModelNodeVO node) { + List flowElements = new ArrayList<>(2); + BpmSimpleModelNodeVO.ChildProcessSetting childProcessSetting = node.getChildProcessSetting(); + List inVariables = childProcessSetting.getInVariables() == null ? + new ArrayList<>() : new ArrayList<>(childProcessSetting.getInVariables()); + CallActivity callActivity = new CallActivity(); + callActivity.setId(node.getId()); + callActivity.setName(node.getName()); + callActivity.setCalledElementType("key"); + // 1. 是否异步 + if (node.getChildProcessSetting().getAsync()) { + // TODO @lesan: 这里目前测试没有跳过执行call activity 后面的节点 + callActivity.setAsynchronous(true); + } + + // 2. 调用的子流程 + callActivity.setCalledElement(childProcessSetting.getCalledProcessDefinitionKey()); + callActivity.setProcessInstanceName(childProcessSetting.getCalledProcessDefinitionName()); + + // 3. 是否自动跳过子流程发起节点 + IOParameter ioParameter = new IOParameter(); + ioParameter.setSourceExpression(childProcessSetting.getSkipStartUserNode().toString()); + ioParameter.setTarget(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE); + inVariables.add(ioParameter); + + // 4. 【默认需要传递的一些变量】流程状态 + ioParameter = new IOParameter(); + ioParameter.setSource(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS); + ioParameter.setTarget(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS); + inVariables.add(ioParameter); + + // 5. 主→子变量传递、子->主变量传递 + callActivity.setInParameters(inVariables); + if (ArrayUtil.isNotEmpty(childProcessSetting.getOutVariables()) && ObjUtil.notEqual(childProcessSetting.getAsync(), Boolean.TRUE)) { + callActivity.setOutParameters(childProcessSetting.getOutVariables()); + } + + // 6. 子流程发起人配置 + List executionListeners = new ArrayList<>(); + FlowableListener flowableListener = new FlowableListener(); + flowableListener.setEvent(ExecutionListener.EVENTNAME_START); + flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); + flowableListener.setImplementation(BpmCallActivityListener.DELEGATE_EXPRESSION); + FieldExtension fieldExtension = new FieldExtension(); + fieldExtension.setFieldName("listenerConfig"); + fieldExtension.setStringValue(JsonUtils.toJsonString(childProcessSetting.getStartUserSetting())); + flowableListener.getFieldExtensions().add(fieldExtension); + executionListeners.add(flowableListener); + callActivity.setExecutionListeners(executionListeners); + + // 7. 超时设置 + if (childProcessSetting.getTimeoutSetting() != null && Boolean.TRUE.equals(childProcessSetting.getTimeoutSetting().getEnable())) { + BoundaryEvent boundaryEvent = null; + if (childProcessSetting.getTimeoutSetting().getType().equals(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())) { + boundaryEvent = buildTimeoutBoundaryEvent(callActivity, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(), + childProcessSetting.getTimeoutSetting().getTimeExpression(), null, null); + } else if (childProcessSetting.getTimeoutSetting().getType().equals(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())) { + boundaryEvent = buildTimeoutBoundaryEvent(callActivity, BpmBoundaryEventTypeEnum.CHILD_PROCESS_TIMEOUT.getType(), + null, null, childProcessSetting.getTimeoutSetting().getTimeExpression()); + } + flowElements.add(boundaryEvent); + } + + // 8. 多实例 + if (childProcessSetting.getMultiInstanceSetting() != null && Boolean.TRUE.equals(childProcessSetting.getMultiInstanceSetting().getEnable())) { + MultiInstanceLoopCharacteristics multiInstanceCharacteristics = new MultiInstanceLoopCharacteristics(); + multiInstanceCharacteristics.setSequential(childProcessSetting.getMultiInstanceSetting().getSequential()); + if (childProcessSetting.getMultiInstanceSetting().getSourceType().equals(BpmChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY.getType())) { + multiInstanceCharacteristics.setLoopCardinality(childProcessSetting.getMultiInstanceSetting().getSource()); + } + if (childProcessSetting.getMultiInstanceSetting().getSourceType().equals(BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType()) || + childProcessSetting.getMultiInstanceSetting().getSourceType().equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) { + multiInstanceCharacteristics.setInputDataItem(childProcessSetting.getMultiInstanceSetting().getSource()); + } + multiInstanceCharacteristics.setCompletionCondition(String.format(BpmUserTaskApproveMethodEnum.RATIO.getCompletionCondition(), + String.format("%.2f", childProcessSetting.getMultiInstanceSetting().getApproveRatio() / 100D))); + callActivity.setLoopCharacteristics(multiInstanceCharacteristics); + addExtensionElement(callActivity, CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE, childProcessSetting.getMultiInstanceSetting().getSourceType()); + } + + // 添加节点类型 + addNodeType(node.getType(), callActivity); + flowElements.add(callActivity); + return flowElements; + } + + @Override + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.CHILD_PROCESS; + } + + } + private static String buildGatewayJoinId(String id) { return id + "_join"; } + private static BoundaryEvent buildTimeoutBoundaryEvent(Activity attachedToRef, Integer type, + String timeDuration, String timeCycle, String timeDate) { + // 1.1 定时器边界事件 + BoundaryEvent boundaryEvent = new BoundaryEvent(); + boundaryEvent.setId("Event-" + IdUtil.fastUUID()); + boundaryEvent.setCancelActivity(false); // 设置关联的任务为不会被中断 + boundaryEvent.setAttachedToRef(attachedToRef); + // 1.2 定义超时时间表达式 + TimerEventDefinition eventDefinition = new TimerEventDefinition(); + if (ObjUtil.isNotNull(timeDuration)) { + eventDefinition.setTimeDuration(timeDuration); + } + if (ObjUtil.isNotNull(timeDuration)) { + eventDefinition.setTimeCycle(timeCycle); + } + if (ObjUtil.isNotNull(timeDate)) { + eventDefinition.setTimeDate(timeDate); + } + boundaryEvent.addEventDefinition(eventDefinition); + + // 2. 添加定时器边界事件类型 + addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, type); + return boundaryEvent; + } + // ========== SIMPLE 流程预测相关的方法 ========== public static List simulateProcess(BpmSimpleModelNodeVO rootNode, Map variables) { @@ -785,11 +943,13 @@ public class SimpleModelUtils { BpmSimpleModelNodeTypeEnum nodeType = BpmSimpleModelNodeTypeEnum.valueOf(currentNode.getType()); Assert.notNull(nodeType, "模型节点类型不支持"); - // 情况:START_NODE/START_USER_NODE/APPROVE_NODE/COPY_NODE/END_NODE + // 情况:START_NODE/START_USER_NODE/APPROVE_NODE/COPY_NODE/END_NODE/TRANSACTOR_NODE if (nodeType == BpmSimpleModelNodeTypeEnum.START_NODE || nodeType == BpmSimpleModelNodeTypeEnum.START_USER_NODE || nodeType == BpmSimpleModelNodeTypeEnum.APPROVE_NODE + || nodeType == BpmSimpleModelNodeTypeEnum.TRANSACTOR_NODE || nodeType == BpmSimpleModelNodeTypeEnum.COPY_NODE + || nodeType == BpmSimpleModelNodeTypeEnum.CHILD_PROCESS || nodeType == BpmSimpleModelNodeTypeEnum.END_NODE) { // 添加元素 resultNodes.add(currentNode); @@ -799,8 +959,8 @@ public class SimpleModelUtils { if (nodeType == BpmSimpleModelNodeTypeEnum.CONDITION_BRANCH_NODE) { // 查找满足条件的 BpmSimpleModelNodeVO 节点 BpmSimpleModelNodeVO matchConditionNode = CollUtil.findOne(currentNode.getConditionNodes(), - conditionNode -> !BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow()) - && evalConditionExpress(variables, conditionNode.getConditionSetting())); + conditionNode -> !BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow()) + && evalConditionExpress(variables, conditionNode.getConditionSetting())); if (matchConditionNode == null) { matchConditionNode = CollUtil.findOne(currentNode.getConditionNodes(), conditionNode -> BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow())); @@ -814,8 +974,8 @@ public class SimpleModelUtils { if (nodeType == BpmSimpleModelNodeTypeEnum.INCLUSIVE_BRANCH_NODE) { // 查找满足条件的 BpmSimpleModelNodeVO 节点 Collection matchConditionNodes = CollUtil.filterNew(currentNode.getConditionNodes(), - conditionNode -> !BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow()) - && evalConditionExpress(variables, conditionNode.getConditionSetting())); + conditionNode -> !BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow()) + && evalConditionExpress(variables, conditionNode.getConditionSetting())); if (CollUtil.isEmpty(matchConditionNodes)) { matchConditionNodes = CollUtil.filterNew(currentNode.getConditionNodes(), conditionNode -> BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow())); @@ -841,27 +1001,4 @@ public class SimpleModelUtils { return BpmnModelUtils.evalConditionExpress(variables, buildConditionExpression(conditionSetting)); } - // TODO @芋艿:【高】要不要优化下,抽个 HttpUtils - - /** - * 添加 HTTP 请求参数。请求头或者请求体 - * - * @param params HTTP 请求参数 - * @param paramSettings HTTP 请求参数设置 - * @param processVariables 流程变量 - */ - public static void addHttpRequestParam(MultiValueMap params, - List paramSettings, - Map processVariables) { - if (CollUtil.isEmpty(paramSettings)) { - return; - } - paramSettings.forEach(item -> { - if (item.getType().equals(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType())) { - params.add(item.getKey(), item.getValue()); - } else if (item.getType().equals(BpmHttpRequestParamTypeEnum.FROM_FORM.getType())) { - params.add(item.getKey(), processVariables.get(item.getValue()).toString()); - } - }); - } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java index 9ccc2f2c9..e8e90006f 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java @@ -16,6 +16,7 @@ import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils; @@ -23,9 +24,7 @@ import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceCopyService; import jakarta.annotation.Resource; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; -import org.flowable.bpmn.model.BpmnModel; -import org.flowable.bpmn.model.StartEvent; -import org.flowable.bpmn.model.UserTask; +import org.flowable.bpmn.model.*; import org.flowable.common.engine.impl.db.SuspensionState; import org.flowable.engine.HistoryService; import org.flowable.engine.RepositoryService; @@ -41,13 +40,12 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseCandidateStrategy; /** * 流程模型实现:主要进行 Flowable {@link Model} 的维护 @@ -209,11 +207,11 @@ public class BpmModelServiceImpl implements BpmModelService { public void deployModel(Long userId, String id) { // 1.1 校验流程模型存在 Model model = validateModelManager(id, userId); + BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model); // 1.2 校验流程图 byte[] bpmnBytes = getModelBpmnXML(model.getId()); - validateBpmnXml(bpmnBytes); + validateBpmnXml(bpmnBytes, metaInfo.getType()); // 1.3 校验表单已配 - BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model); BpmFormDO form = validateFormConfig(metaInfo); // 1.4 校验任务分配规则已配置 taskCandidateInvoker.validateBpmnConfig(bpmnBytes); @@ -233,7 +231,7 @@ public class BpmModelServiceImpl implements BpmModelService { repositoryService.saveModel(model); } - private void validateBpmnXml(byte[] bpmnBytes) { + private void validateBpmnXml(byte[] bpmnBytes, Integer type) { BpmnModel bpmnModel = BpmnModelUtils.getBpmnModel(bpmnBytes); if (bpmnModel == null) { throw exception(MODEL_NOT_EXISTS); @@ -250,6 +248,15 @@ public class BpmModelServiceImpl implements BpmModelService { throw exception(MODEL_DEPLOY_FAIL_BPMN_USER_TASK_NAME_NOT_EXISTS, userTask.getId()); } }); + // 3. 校验第一个用户任务节点的规则类型是否为“审批人自选”,BPMN 设计器,校验第一个用户任务节点,SIMPLE 设计器,第一个节点固定为发起人所以校验第二个用户任务节点 + UserTask firUserTask = CollUtil.get(userTasks, BpmModelTypeEnum.BPMN.getType().equals(type) ? 0 : 1); + if (firUserTask == null) { + return; + } + Integer candidateStrategy = parseCandidateStrategy(firUserTask); + if (Objects.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy())) { + throw exception(MODEL_DEPLOY_FAIL_FIRST_USER_TASK_CANDIDATE_STRATEGY_ERROR, firUserTask.getName()); + } } @Override diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java index c6a178c6c..14ee08406 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java @@ -28,8 +28,7 @@ import java.util.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.addIfNotNull; -import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_DEFINITION_KEY_NOT_MATCH; -import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_DEFINITION_NAME_NOT_MATCH; +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; import static java.util.Collections.emptyList; /** @@ -144,9 +143,8 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ // 插入拓展表 BpmProcessDefinitionInfoDO definitionDO = BeanUtils.toBean(modelMetaInfo, BpmProcessDefinitionInfoDO.class) - .setModelId(model.getId()).setProcessDefinitionId(definition.getId()) + .setModelId(model.getId()).setCategory(model.getCategory()).setProcessDefinitionId(definition.getId()) .setModelType(modelMetaInfo.getType()).setSimpleModel(simpleJson); - if (form != null) { definitionDO.setFormFields(form.getFields()).setFormConf(form.getConf()); } @@ -156,16 +154,25 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ @Override public void updateProcessDefinitionState(String id, Integer state) { + ProcessDefinition processDefinition = repositoryService.getProcessDefinition(id); + if (processDefinition == null) { + throw exception(PROCESS_DEFINITION_NOT_EXISTS); + } + // 激活 if (Objects.equals(SuspensionState.ACTIVE.getStateCode(), state)) { - repositoryService.activateProcessDefinitionById(id, false, null); + if (processDefinition.isSuspended()) { + repositoryService.activateProcessDefinitionById(id, false, null); + } return; } // 挂起 if (Objects.equals(SuspensionState.SUSPENDED.getStateCode(), state)) { // suspendProcessInstances = false,进行中的任务,不进行挂起。 // 原因:只要新的流程不允许发起即可,老流程继续可以执行。 - repositoryService.suspendProcessDefinitionById(id, false, null); + if (!processDefinition.isSuspended()) { + repositoryService.suspendProcessDefinitionById(id, false, null); + } return; } log.error("[updateProcessDefinitionState][流程定义({}) 修改未知状态({})]", id, state); diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java index 55b86a551..abba2245e 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java @@ -7,6 +7,7 @@ import jakarta.validation.Valid; import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.runtime.ProcessInstance; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; @@ -84,7 +85,6 @@ public interface BpmProcessInstanceService { PageResult getProcessInstancePage(Long userId, @Valid BpmProcessInstancePageReqVO pageReqVO); - // TODO @芋艿:重点在 review 下 /** * 获取审批详情。 *

@@ -96,6 +96,15 @@ public interface BpmProcessInstanceService { */ BpmApprovalDetailRespVO getApprovalDetail(Long loginUserId, @Valid BpmApprovalDetailReqVO reqVO); + /** + * 获取下一个执行节点信息 + * + * @param loginUserId 登录人的用户编号 + * @param reqVO 请求信息 + * @return 下一个执行节点信息 + */ + List getNextApprovalNodes(Long loginUserId, @Valid BpmApprovalDetailReqVO reqVO); + /** * 获取流程实例的 BPMN 模型视图 * @@ -148,6 +157,22 @@ public interface BpmProcessInstanceService { */ void updateProcessInstanceReject(ProcessInstance processInstance, String reason); + /** + * 更新 ProcessInstance 的变量 + * + * @param id 流程编号 + * @param variables 流程变量 + */ + void updateProcessInstanceVariables(String id, Map variables); + + /** + * 删除 ProcessInstance 的变量 + * + * @param id 流程编号 + * @param variableNames 流程变量名 + */ + void removeProcessInstanceVariables(String id, Collection variableNames); + // ========== Event 事件相关方法 ========== /** @@ -158,11 +183,9 @@ public interface BpmProcessInstanceService { void processProcessInstanceCompleted(ProcessInstance instance); /** - * 更新 ProcessInstance 的变量 + * 处理 ProcessInstance 开始事件,例如说:流程前置通知 * - * @param id 流程编号 - * @param variables 流程变量 + * @param instance 流程任务 */ - void updateProcessInstanceVariables(String id, Map variables); - + void processProcessInstanceCreated(ProcessInstance instance); } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java index ca50baef8..1e0a777d0 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java @@ -5,6 +5,7 @@ import cn.hutool.core.collection.ListUtil; import cn.hutool.core.date.DateUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; @@ -13,6 +14,7 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.framework.common.util.object.PageUtils; import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*; @@ -28,10 +30,11 @@ import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; -import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept.BpmTaskCandidateStartUserSelectStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; import cn.iocoder.yudao.module.bpm.framework.flowable.core.event.BpmProcessInstanceEventPublisher; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils; @@ -54,11 +57,13 @@ import org.flowable.engine.history.HistoricProcessInstanceQuery; import org.flowable.engine.repository.ProcessDefinition; import org.flowable.engine.runtime.ProcessInstance; import org.flowable.engine.runtime.ProcessInstanceBuilder; +import org.flowable.task.api.Task; import org.flowable.task.api.history.HistoricTaskInstance; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; +import org.springframework.web.client.RestTemplate; import java.util.*; @@ -67,6 +72,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils. import static cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ActivityNode; import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID; +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseNodeType; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.flowable.bpmn.constants.BpmnXMLConstants.*; @@ -144,7 +150,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService } private Map getFormFieldsPermission(BpmnModel bpmnModel, - String activityId, String taskId) { + String activityId, String taskId) { // 1. 获取流程活动编号。流程活动 Id 为空事,从流程任务中获取流程活动 Id if (StrUtil.isEmpty(activityId) && StrUtil.isNotEmpty(taskId)) { activityId = Optional.ofNullable(taskService.getHistoricTask(taskId)) @@ -164,7 +170,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService Long startUserId = loginUserId; // 流程发起人 HistoricProcessInstance historicProcessInstance = null; // 流程实例 Integer processInstanceStatus = BpmProcessInstanceStatusEnum.NOT_START.getStatus(); // 流程状态 - Map processVariables = reqVO.getProcessVariables(); // 流程变量 + Map processVariables = new HashMap<>(); // 流程变量 // 1.2 如果是流程已发起的场景,则使用流程实例的数据 if (reqVO.getProcessInstanceId() != null) { historicProcessInstance = getHistoricProcessInstance(reqVO.getProcessInstanceId()); @@ -173,7 +179,13 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService } startUserId = Long.valueOf(historicProcessInstance.getStartUserId()); processInstanceStatus = FlowableUtils.getProcessInstanceStatus(historicProcessInstance); - processVariables = historicProcessInstance.getProcessVariables(); + // 合并 DB 和前端传递的流量变量,以前端的为主 + if (CollUtil.isNotEmpty(historicProcessInstance.getProcessVariables())) { + processVariables.putAll(historicProcessInstance.getProcessVariables()); + } + } + if (CollUtil.isNotEmpty(reqVO.getProcessVariables())) { + processVariables.putAll(reqVO.getProcessVariables()); } // 1.3 读取其它相关数据 ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition( @@ -205,20 +217,78 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService } // 3.1 计算当前登录用户的待办任务 - // TODO @jason:有一个极端情况,如果一个用户有 2 个 task A 和 B,A 已经通过,B 需要审核。这个时,通过 A 进来,todo 拿到 - // B,会不会表单权限不一致哈。 - BpmTaskRespVO todoTask = taskService.getFirstTodoTask(loginUserId, reqVO.getProcessInstanceId()); - + BpmTaskRespVO todoTask = taskService.getTodoTask(loginUserId, reqVO.getTaskId(), reqVO.getProcessInstanceId()); // 3.2 预测未运行节点的审批信息 List simulateActivityNodes = getSimulateApproveNodeList(startUserId, bpmnModel, processDefinitionInfo, processVariables, activities); + // 3.3 如果是发起动作,activityId 为开始节点,不校验审批人自选节点 + if (ObjUtil.equals(reqVO.getActivityId(), BpmnModelConstants.START_USER_NODE_ID)) { + simulateActivityNodes.removeIf(node -> + BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy().equals(node.getCandidateStrategy())); + } // 4. 拼接最终数据 return buildApprovalDetail(reqVO, bpmnModel, processDefinition, processDefinitionInfo, historicProcessInstance, processInstanceStatus, endActivityNodes, runActivityNodes, simulateActivityNodes, todoTask); } + @Override + public List getNextApprovalNodes(Long loginUserId, BpmApprovalDetailReqVO reqVO) { + // 1.1 校验任务存在,且是当前用户的 + Task task = taskService.validateTask(loginUserId, reqVO.getTaskId()); + // 1.2 校验流程实例存在 + ProcessInstance instance = getProcessInstance(task.getProcessInstanceId()); + if (instance == null) { + throw exception(PROCESS_INSTANCE_NOT_EXISTS); + } + HistoricProcessInstance historicProcessInstance = getHistoricProcessInstance(task.getProcessInstanceId()); + if (historicProcessInstance == null) { + throw exception(ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS); + } + // 1.3 校验BpmnModel + BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(task.getProcessDefinitionId()); + if (bpmnModel == null) { + return null; + } + + // 2. 设置流程变量 + Map processVariables = new HashMap<>(); + // 2.1 获取历史中流程变量 + if (CollUtil.isNotEmpty(historicProcessInstance.getProcessVariables())) { + processVariables.putAll(historicProcessInstance.getProcessVariables()); + } + // 2.2 合并前端传递的流程变量,以前端为准 + if (CollUtil.isNotEmpty(reqVO.getProcessVariables())) { + processVariables.putAll(reqVO.getProcessVariables()); + } + + // 3 获取当前任务节点的信息 + // 3.1 获取下一个将要执行的节点集合 + FlowElement flowElement = bpmnModel.getFlowElement(task.getTaskDefinitionKey()); + List nextFlowNodes = BpmnModelUtils.getNextFlowNodes(flowElement, bpmnModel, processVariables); + return convertList(nextFlowNodes, node -> { + List candidateUserIds = getTaskCandidateUserList(bpmnModel, node.getId(), + loginUserId, historicProcessInstance.getProcessDefinitionId(), processVariables); + // 3.2 获取节点的审批人信息 + Map userMap = adminUserApi.getUserMap(candidateUserIds); + // 3.3 获取节点的审批人部门信息 + Map deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId)); + // 3.4 存在一个节点多人审批的情况,组装审批人信息 + List candidateUsers = new ArrayList<>(); + userMap.forEach((key, value) -> candidateUsers.add(BpmProcessInstanceConvert.INSTANCE.buildUser(key, userMap, deptMap))); + return new ActivityNode().setNodeType(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType()) + .setId(node.getId()) + .setName(node.getName()) + .setStatus(BpmTaskStatusEnum.RUNNING.getStatus()) + .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(node)) + // TODO @小北:先把 candidateUserIds 设置完,然后最后拼接 candidateUsers 信息。这样,如果有多个节点,就不用重复查询啦;类似 buildApprovalDetail 思路; + // TODO 先拼接处 List ActivityNode + // TODO 接着,再起一段,处理 adminUserApi.getUserMap(candidateUserIds)、deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId)) + .setCandidateUsers(candidateUsers); + }); + } + @Override @SuppressWarnings("unchecked") public PageResult getProcessInstancePage(Long userId, @@ -283,15 +353,15 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService * 主要是,拼接审批人的用户信息、部门信息 */ private BpmApprovalDetailRespVO buildApprovalDetail(BpmApprovalDetailReqVO reqVO, - BpmnModel bpmnModel, - ProcessDefinition processDefinition, - BpmProcessDefinitionInfoDO processDefinitionInfo, - HistoricProcessInstance processInstance, - Integer processInstanceStatus, - List endApprovalNodeInfos, - List runningApprovalNodeInfos, - List simulateApprovalNodeInfos, - BpmTaskRespVO todoTask) { + BpmnModel bpmnModel, + ProcessDefinition processDefinition, + BpmProcessDefinitionInfoDO processDefinitionInfo, + HistoricProcessInstance processInstance, + Integer processInstanceStatus, + List endApprovalNodeInfos, + List runningApprovalNodeInfos, + List simulateApprovalNodeInfos, + BpmTaskRespVO todoTask) { // 1. 获取所有需要读取用户信息的 userIds List approveNodes = newArrayList( asList(endApprovalNodeInfos, runningApprovalNodeInfos, simulateApprovalNodeInfos)); @@ -313,19 +383,21 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService * 获得【已结束】的活动节点们 */ private List getEndActivityNodeList(Long startUserId, BpmnModel bpmnModel, - BpmProcessDefinitionInfoDO processDefinitionInfo, - HistoricProcessInstance historicProcessInstance, Integer processInstanceStatus, - List activities, List tasks) { + BpmProcessDefinitionInfoDO processDefinitionInfo, + HistoricProcessInstance historicProcessInstance, Integer processInstanceStatus, + List activities, List tasks) { // 遍历 tasks 列表,只处理已结束的 UserTask - // 为什么不通过 activities 呢?因为,加签场景下,它只存在于 tasks,没有 activities,导致如果遍历 activities - // 的话,它无法成为一个节点 + // 为什么不通过 activities 呢?因为,加签场景下,它只存在于 tasks,没有 activities,导致如果遍历 activities 的话,它无法成为一个节点 + // TODO @芋艿:子流程只有activity,这里获取不到已结束的子流程; + // TODO @lesan:【子流程】基于 activities 查询出 usertask、callactivity,然后拼接?如果是子流程,就是可以点击过去? List endTasks = filterList(tasks, task -> task.getEndTime() != null); List approvalNodes = convertList(endTasks, task -> { FlowElement flowNode = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey()); ActivityNode activityNode = new ActivityNode().setId(task.getTaskDefinitionKey()).setName(task.getName()) .setNodeType(START_USER_NODE_ID.equals(task.getTaskDefinitionKey()) ? BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType() - : BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType()) + : ObjUtil.defaultIfNull(parseNodeType(flowNode), // 目的:解决“办理节点”的识别 + BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())) .setStatus(FlowableUtils.getTaskStatus(task)) .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode)) .setStartTime(DateUtils.of(task.getCreateTime())).setEndTime(DateUtils.of(task.getEndTime())) @@ -381,18 +453,19 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService * 获得【进行中】的活动节点们 */ private List getRunApproveNodeList(Long startUserId, - BpmnModel bpmnModel, - ProcessDefinition processDefinition, - Map processVariables, - List activities, - List tasks) { - // 构建运行中的任务,基于 activityId 分组 + BpmnModel bpmnModel, + ProcessDefinition processDefinition, + Map processVariables, + List activities, + List tasks) { + // 构建运行中的任务、子流程,基于 activityId 分组 List runActivities = filterList(activities, activity -> activity.getEndTime() == null - && (StrUtil.equalsAny(activity.getActivityType(), ELEMENT_TASK_USER))); + && (StrUtil.equalsAny(activity.getActivityType(), ELEMENT_TASK_USER, ELEMENT_CALL_ACTIVITY))); Map> runningTaskMap = convertMultiMap(runActivities, HistoricActivityInstance::getActivityId); // 按照 activityId 分组,构建 ApprovalNodeInfo 节点 + // TODO @lesan:【子流程】在子流程进行审批的时候,HistoricActivityInstance 里面可以拿到 runActivities.get(0).getCalledProcessInstanceId()。要不要支持跳转??? Map taskMap = convertMap(tasks, HistoricTaskInstance::getId); return convertList(runningTaskMap.entrySet(), entry -> { String activityId = entry.getKey(); @@ -402,7 +475,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService HistoricActivityInstance firstActivity = CollUtil.getFirst(taskActivities); // 取第一个任务,会签/或签的任务,开始时间相同 ActivityNode activityNode = new ActivityNode().setId(firstActivity.getActivityId()) .setName(firstActivity.getActivityName()) - .setNodeType(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType()) + .setNodeType(ObjUtil.defaultIfNull(parseNodeType(flowNode), // 目的:解决“办理节点”和"子流程"的识别 + BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())) .setStatus(BpmTaskStatusEnum.RUNNING.getStatus()) .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode)) .setStartTime(DateUtils.of(CollUtil.getFirst(taskActivities).getStartTime())) @@ -410,6 +484,11 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService // 处理每个任务的 tasks 属性 for (HistoricActivityInstance activity : taskActivities) { HistoricTaskInstance task = taskMap.get(activity.getTaskId()); + // 特殊情况:子流程节点 ChildProcess 仅存在于 activity 中,并且没有自身的 task,需要跳过执行 + // TODO @芋艿:后续看看怎么优化! + if (task == null) { + continue; + } activityNode.getTasks().add(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task)); // 加签子任务,需要过滤掉已经完成的加签子任务 List childrenTasks = filterList( @@ -440,9 +519,9 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService * 获得【预测(未来)】的活动节点们 */ private List getSimulateApproveNodeList(Long startUserId, BpmnModel bpmnModel, - BpmProcessDefinitionInfoDO processDefinitionInfo, - Map processVariables, - List activities) { + BpmProcessDefinitionInfoDO processDefinitionInfo, + Map processVariables, + List activities) { // TODO @芋艿:【可优化】在驳回场景下,未来的预测准确性不高。原因是,驳回后,HistoricActivityInstance // 包括了历史的操作,不是只有 startEvent 到当前节点的记录 Set runActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId); @@ -464,8 +543,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService } private ActivityNode buildNotRunApproveNodeForSimple(Long startUserId, BpmnModel bpmnModel, - BpmProcessDefinitionInfoDO processDefinitionInfo, Map processVariables, - BpmSimpleModelNodeVO node, Set runActivityIds) { + BpmProcessDefinitionInfoDO processDefinitionInfo, Map processVariables, + BpmSimpleModelNodeVO node, Set runActivityIds) { // TODO @芋艿:【可优化】在驳回场景下,未来的预测准确性不高。原因是,驳回后,HistoricActivityInstance // 包括了历史的操作,不是只有 startEvent 到当前节点的记录 if (runActivityIds.contains(node.getId())) { @@ -479,7 +558,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService // 1. 开始节点/审批节点 if (ObjectUtils.equalsAny(node.getType(), BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType(), - BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())) { + BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType(), + BpmSimpleModelNodeTypeEnum.TRANSACTOR_NODE.getType())) { List candidateUserIds = getTaskCandidateUserList(bpmnModel, node.getId(), startUserId, processDefinitionInfo.getProcessDefinitionId(), processVariables); activityNode.setCandidateUserIds(candidateUserIds); @@ -494,14 +574,22 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService // 3. 抄送节点 if (CollUtil.isEmpty(runActivityIds) && // 流程发起时:需要展示抄送节点,用于选择抄送人 BpmSimpleModelNodeTypeEnum.COPY_NODE.getType().equals(node.getType())) { + List candidateUserIds = getTaskCandidateUserList(bpmnModel, node.getId(), + startUserId, processDefinitionInfo.getProcessDefinitionId(), processVariables); + activityNode.setCandidateUserIds(candidateUserIds); + return activityNode; + } + + // 4. 子流程节点 + if (BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType().equals(node.getType())) { return activityNode; } return null; } private ActivityNode buildNotRunApproveNodeForBpmn(Long startUserId, BpmnModel bpmnModel, - BpmProcessDefinitionInfoDO processDefinitionInfo, Map processVariables, - FlowElement node, Set runActivityIds) { + BpmProcessDefinitionInfoDO processDefinitionInfo, Map processVariables, + FlowElement node, Set runActivityIds) { if (runActivityIds.contains(node.getId())) { return null; } @@ -532,7 +620,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService } private List getTaskCandidateUserList(BpmnModel bpmnModel, String activityId, - Long startUserId, String processDefinitionId, Map processVariables) { + Long startUserId, String processDefinitionId, Map processVariables) { Set userIds = taskCandidateInvoker.calculateUsersByActivity(bpmnModel, activityId, startUserId, processDefinitionId, processVariables); return new ArrayList<>(userIds); @@ -568,11 +656,11 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService Set finishedTaskActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId, activityInstance -> activityInstance.getEndTime() != null && ObjectUtil.notEqual(activityInstance.getActivityType(), - BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW)); + BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW)); Set finishedSequenceFlowActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId, activityInstance -> activityInstance.getEndTime() != null && ObjectUtil.equals(activityInstance.getActivityType(), - BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW)); + BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW)); // 特殊:会签情况下,会有部分已完成(审批)、部分未完成(待审批),此时需要 finishedTaskActivityIds 移除掉 finishedTaskActivityIds.removeAll(unfinishedTaskActivityIds); // 特殊:如果流程实例被拒绝,则需要计算是哪个活动节点。 @@ -624,8 +712,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService } private String createProcessInstance0(Long userId, ProcessDefinition definition, - Map variables, String businessKey, - Map> startUserSelectAssignees) { + Map variables, String businessKey, + Map> startUserSelectAssignees) { // 1.1 校验流程定义 if (definition == null) { throw exception(PROCESS_DEFINITION_NOT_EXISTS); @@ -643,7 +731,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService throw exception(PROCESS_INSTANCE_START_USER_CAN_START); } // 1.3 校验发起人自选审批人 - validateStartUserSelectAssignees(definition, startUserSelectAssignees); + validateStartUserSelectAssignees(userId, definition, startUserSelectAssignees, variables); // 2. 创建流程实例 if (variables == null) { @@ -653,10 +741,9 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, userId); // 设置流程变量,发起人 ID variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, // 流程实例状态:审批中 BpmProcessInstanceStatusEnum.RUNNING.getStatus()); - variables.put(BpmnVariableConstants.PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED, true); // 跳过表达式需要添加此变量为 - // true,不影响没配置 - // skipExpression 的节点 + variables.put(BpmnVariableConstants.PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED, true); // 跳过表达式需要添加此变量为 true,不影响没配置 skipExpression 的节点 if (CollUtil.isNotEmpty(startUserSelectAssignees)) { + // 设置流程变量,发起人自选审批人 variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, startUserSelectAssignees); } @@ -688,17 +775,23 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService return instance.getId(); } - private void validateStartUserSelectAssignees(ProcessDefinition definition, - Map> startUserSelectAssignees) { - // 1. 获得发起人自选审批人的 UserTask/ServiceTask 列表 - BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(definition.getId()); - List tasks = BpmTaskCandidateStartUserSelectStrategy.getStartUserSelectTaskList(bpmnModel); - if (CollUtil.isEmpty(tasks)) { + private void validateStartUserSelectAssignees(Long userId, ProcessDefinition definition, + Map> startUserSelectAssignees, + Map variables) { + // 1. 获取预测的节点信息 + BpmApprovalDetailRespVO detailRespVO = getApprovalDetail(userId, new BpmApprovalDetailReqVO() + .setProcessDefinitionId(definition.getId()) + .setProcessVariables(variables)); + List activityNodes = detailRespVO.getActivityNodes(); + if (CollUtil.isEmpty(activityNodes)) { return; } - // 2. 校验发起人自选审批人的审批人和抄送人是否都配置了 - tasks.forEach(task -> { + // 2.1 移除掉不是发起人自选审批人节点 + activityNodes.removeIf(task -> + ObjectUtil.notEqual(BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy(), task.getCandidateStrategy())); + // 2.2 流程发起时要先获取当前流程的预测走向节点,发起时只校验预测的节点发起人自选审批人的审批人和抄送人是否都配置了 + activityNodes.forEach(task -> { List assignees = startUserSelectAssignees != null ? startUserSelectAssignees.get(task.getId()) : null; if (CollUtil.isEmpty(assignees)) { throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, task.getName()); @@ -771,6 +864,16 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService BpmReasonEnum.REJECT_TASK.format(reason)); } + @Override + public void updateProcessInstanceVariables(String id, Map variables) { + runtimeService.setVariables(id, variables); + } + + @Override + public void removeProcessInstanceVariables(String id, Collection variableNames) { + runtimeService.removeVariables(id, variableNames); + } + // ========== Event 事件相关方法 ========== @Override @@ -802,12 +905,43 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService // 3. 发送流程实例的状态事件 processInstanceEventPublisher.sendProcessInstanceResultEvent( BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceStatusEvent(this, instance, status)); + + // 4. 流程后置通知 + if (Objects.equals(status, BpmProcessInstanceStatusEnum.APPROVE.getStatus())) { + BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService. + getProcessDefinitionInfo(instance.getProcessDefinitionId()); + if (ObjUtil.isNotNull(processDefinitionInfo) && + ObjUtil.isNotNull(processDefinitionInfo.getProcessAfterTriggerSetting())) { + BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getProcessAfterTriggerSetting(); + + BpmHttpRequestUtils.executeBpmHttpRequest(instance, + setting.getUrl(), + setting.getHeader(), + setting.getBody(), + true, setting.getResponse()); + } + } }); } @Override - public void updateProcessInstanceVariables(String id, Map variables) { - runtimeService.setVariables(id, variables); + public void processProcessInstanceCreated(ProcessInstance instance) { + // 注意:需要基于 instance 设置租户编号,避免 Flowable 内部异步时,丢失租户编号 + FlowableUtils.execute(instance.getTenantId(), () -> { + // 流程前置通知 + BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService. + getProcessDefinitionInfo(instance.getProcessDefinitionId()); + if (ObjUtil.isNull(processDefinitionInfo) || + ObjUtil.isNull(processDefinitionInfo.getProcessBeforeTriggerSetting())) { + return; + } + BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getProcessBeforeTriggerSetting(); + BpmHttpRequestUtils.executeBpmHttpRequest(instance, + setting.getUrl(), + setting.getHeader(), + setting.getBody(), + true, setting.getResponse()); + }); } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java index bff859b06..e99d97435 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java @@ -35,13 +35,16 @@ public interface BpmTaskService { PageResult getTaskTodoPage(Long userId, BpmTaskPageReqVO pageReqVO); /** - * 获得用户在指定流程下,首个需要处理(待办)的任务 + * 获得用户(待办)的任务: + * 1. 根据 taskId 查询待办任务 + * 2. 如果任务不存在(或者已审核),获取指定流程下,首个需要处理任务 * * @param userId 用户编号 + * @param taskId 任务编号 * @param processInstanceId 流程实例编号 * @return 待办任务 */ - BpmTaskRespVO getFirstTodoTask(Long userId, String processInstanceId); + BpmTaskRespVO getTodoTask(Long userId, String taskId, String processInstanceId); /** * 获得已办的流程任务分页 @@ -89,6 +92,14 @@ public interface BpmTaskService { */ List getTaskListByProcessInstanceId(String processInstanceId, Boolean asc); + /** + * 校验任务是否存在,并且是否是分配给自己的任务 + * + * @param userId 用户 id + * @param taskId task id + */ + Task validateTask(Long userId, String taskId); + /** * 获取任务 * @@ -277,11 +288,22 @@ public interface BpmTaskService { void processTaskTimeout(String processInstanceId, String taskDefineKey, Integer handlerType); /** - * 处理 延迟器 超时事件 + * 处理 ChildProcess 子流程的审批超时事件 * * @param processInstanceId 流程示例编号 * @param taskDefineKey 任务 Key */ - void processDelayTimerTimeout(String processInstanceId, String taskDefineKey); + void processChildProcessTimeout(String processInstanceId, String taskDefineKey); + + /** + * 触发流程任务 (ReceiveTask) 的执行 + *

+ * 1. Simple 模型 HTTP 回调请求触发器节点的回调,触发流程继续执行 + * 2. Simple 模型延迟器节点,到时触发流程继续执行 + * + * @param processInstanceId 流程示例编号 + * @param taskDefineKey 任务 Key + */ + void triggerTask(String processInstanceId, String taskDefineKey); } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java index 76c777103..6263e21bc 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.bpm.service.task; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.*; import cn.hutool.extra.spring.SpringUtil; @@ -9,6 +10,7 @@ import cn.iocoder.yudao.framework.common.util.date.DateUtils; import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.framework.common.util.object.PageUtils; +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*; import cn.iocoder.yudao.module.bpm.convert.task.BpmTaskConvert; @@ -19,6 +21,7 @@ import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskSignTypeEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; @@ -40,6 +43,7 @@ import org.flowable.engine.ManagementService; import org.flowable.engine.RuntimeService; import org.flowable.engine.TaskService; import org.flowable.engine.history.HistoricActivityInstance; +import org.flowable.engine.runtime.ActivityInstance; import org.flowable.engine.runtime.Execution; import org.flowable.engine.runtime.ProcessInstance; import org.flowable.task.api.DelegationState; @@ -61,7 +65,9 @@ import java.util.stream.Stream; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG; +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*; /** @@ -116,6 +122,9 @@ public class BpmTaskServiceImpl implements BpmTaskService { if (StrUtil.isNotEmpty(pageVO.getCategory())) { taskQuery.taskCategory(pageVO.getCategory()); } + if (StrUtil.isNotEmpty(pageVO.getProcessDefinitionKey())) { + taskQuery.processDefinitionKey(pageVO.getProcessDefinitionKey()); + } if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); @@ -129,7 +138,67 @@ public class BpmTaskServiceImpl implements BpmTaskService { } @Override - public BpmTaskRespVO getFirstTodoTask(Long userId, String processInstanceId) { + public BpmTaskRespVO getTodoTask(Long userId, String taskId, String processInstanceId) { + // 1.1 获取指定的用户待办任务 + Task todoTask = getMyTodoTask(userId, taskId); + // 1.2 获取不到,则获取该流程实例下,第一个用户的待办任务 + if (todoTask == null) { + todoTask = getMyFirstTodoTask(userId, processInstanceId); + } + if (todoTask == null) { + return null; + } + + // 2. 查询该任务的子任务 + List childrenTasks = getAllChildrenTaskListByParentTaskId(todoTask.getId(), CollUtil.newArrayList(todoTask)); + + // 3. 转换返回 + BpmnModel bpmnModel = bpmProcessDefinitionService.getProcessDefinitionBpmnModel(todoTask.getProcessDefinitionId()); + Map buttonsSetting = BpmnModelUtils.parseButtonsSetting( + bpmnModel, todoTask.getTaskDefinitionKey()); + Boolean signEnable = parseSignEnable(bpmnModel, todoTask.getTaskDefinitionKey()); + Boolean reasonRequire = parseReasonRequire(bpmnModel, todoTask.getTaskDefinitionKey()); + Integer nodeType = parseNodeType(BpmnModelUtils.getFlowElementById(bpmnModel, todoTask.getTaskDefinitionKey())); + + // 4. 任务表单 + BpmFormDO taskForm = null; + if (StrUtil.isNotBlank(todoTask.getFormKey())) { + taskForm = formService.getForm(NumberUtils.parseLong(todoTask.getFormKey())); + } + + return BpmTaskConvert.INSTANCE.buildTodoTask(todoTask, childrenTasks, buttonsSetting, taskForm) + .setNodeType(nodeType).setSignEnable(signEnable).setReasonRequire(reasonRequire); + } + + /** + * 获得用户指定 taskId 任务编号的“待办”(未审批、且可审核)的任务 + * + * @param userId 用户编号 + * @param taskId 任务编号 + * @return 任务 + */ + private Task getMyTodoTask(Long userId, String taskId) { + if (StrUtil.isEmpty(taskId)) { + return null; + } + Task task = getTask(taskId); + if (task == null) { + return null; + } + if (!isAssignUserTask(userId, task) && !isAddSignUserTask(userId, task)) { + return null; + } + return task; + } + + /** + * 获得用户指定 processInstanceId 流程编号下的首个“待办”(未审批、且可审核)的任务 + * + * @param userId 用户编号 + * @param processInstanceId 流程编号 + * @return 任务 + */ + private Task getMyFirstTodoTask(Long userId, String processInstanceId) { if (processInstanceId == null) { return null; } @@ -141,37 +210,12 @@ public class BpmTaskServiceImpl implements BpmTaskService { .includeProcessVariables() .orderByTaskCreateTime().asc() // 按创建时间升序 .list(); - if (CollUtil.isEmpty(tasks)) { - return null; - } - // 2.1 查询我的首个任务 - Task todoTask = CollUtil.findOne(tasks, task -> { + // 2. 查询我的首个任务 + return CollUtil.findOne(tasks, task -> { return isAssignUserTask(userId, task) // 当前用户为审批人 || isAddSignUserTask(userId, task); // 当前用户为加签人(为了减签) }); - if (todoTask == null) { - return null; - } - // 2.2 查询该任务的子任务 - List childrenTasks = getAllChildrenTaskListByParentTaskId(todoTask.getId(), tasks); - - // 3. 转换返回 - BpmnModel bpmnModel = bpmProcessDefinitionService.getProcessDefinitionBpmnModel(todoTask.getProcessDefinitionId()); - Map buttonsSetting = BpmnModelUtils.parseButtonsSetting( - bpmnModel, todoTask.getTaskDefinitionKey()); - Boolean signEnable = parseSignEnable(bpmnModel, todoTask.getTaskDefinitionKey()); - Boolean reasonRequire = parseReasonRequire(bpmnModel, todoTask.getTaskDefinitionKey()); - - // 4. 任务表单 - BpmFormDO taskForm = null; - if (StrUtil.isNotBlank(todoTask.getFormKey())) { - taskForm = formService.getForm(NumberUtils.parseLong(todoTask.getFormKey())); - } - - return BpmTaskConvert.INSTANCE.buildTodoTask(todoTask, childrenTasks, buttonsSetting, taskForm) - .setSignEnable(signEnable) - .setReasonRequire(reasonRequire); } @Override @@ -194,6 +238,10 @@ public class BpmTaskServiceImpl implements BpmTaskService { return PageResult.empty(); } List tasks = taskQuery.listPage(PageUtils.getStart(pageVO), pageVO.getPageSize()); + + // 特殊:强制移除自动完成的“发起人”节点 + // 补充说明:由于 taskQuery 无法方面的过滤,所以暂时通过内存过滤 + tasks.removeIf(task -> task.getTaskDefinitionKey().equals(START_USER_NODE_ID)); return new PageResult<>(tasks, count); } @@ -243,13 +291,8 @@ public class BpmTaskServiceImpl implements BpmTaskService { return query.list(); } - /** - * 校验任务是否存在,并且是否是分配给自己的任务 - * - * @param userId 用户 id - * @param taskId task id - */ - private Task validateTask(Long userId, String taskId) { + @Override + public Task validateTask(Long userId, String taskId) { Task task = validateTaskExist(taskId); // 为什么判断 assignee 非空的情况下? // 例如说:在审批人为空时,我们会有“自动审批通过”的策略,此时 userId 为 null,允许通过 @@ -515,21 +558,91 @@ public class BpmTaskServiceImpl implements BpmTaskService { // 2.2 添加评论 taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.APPROVE.getType(), BpmCommentTypeEnum.APPROVE.formatComment(reqVO.getReason())); - // 2.3 调用 BPM complete 去完成任务 - // 其中,variables 是存储动态表单到 local 任务级别。过滤一下,避免 ProcessInstance 系统级的变量被占用 - if (CollUtil.isNotEmpty(reqVO.getVariables())) { - Map variables = FlowableUtils.filterTaskFormVariable(reqVO.getVariables()); - // 修改表单的值需要存储到 ProcessInstance 变量 - runtimeService.setVariables(task.getProcessInstanceId(), variables); - taskService.complete(task.getId(), variables, true); - } else { - taskService.complete(task.getId()); + + // 3. 设置流程变量。如果流程变量前端传空,需要从历史实例中获取,原因:前端表单如果在当前节点无可编辑的字段时 variables 一定会为空 + // 场景一:A 节点发起,B 节点表单无可编辑字段,审批通过时,C 节点需要流程变量获取下一个执行节点,但因为 B 节点无可编辑的字段,variables 为空,流程可能出现问题。 + // 场景二:A 节点发起,B 节点只有某一个字段可编辑(比如 day),但 C 节点需要多个节点。 + // (比如 work + day 变量,在发起时填写,因为 B 节点只有 day 的编辑权限,在审批后,variables 会缺少 work 的值) + Map processVariables = new HashMap<>(); + if (CollUtil.isNotEmpty(instance.getProcessVariables())) { // 获取历史中流程变量 + processVariables.putAll(instance.getProcessVariables()); } + if (CollUtil.isNotEmpty(reqVO.getVariables())) { // 合并前端传递的流程变量,以前端为准 + processVariables.putAll(reqVO.getVariables()); + } + + // 4. 校验并处理 APPROVE_USER_SELECT 当前审批人,选择下一节点审批人的逻辑 + Map variables = validateAndSetNextAssignees(task.getTaskDefinitionKey(), processVariables, + bpmnModel, reqVO.getNextAssignees(), instance); + runtimeService.setVariables(task.getProcessInstanceId(), variables); + + // 5. 调用 BPM complete 去完成任务 + taskService.complete(task.getId(), variables, true); // 【加签专属】处理加签任务 handleParentTaskIfSign(task.getParentTaskId()); } + /** + * 校验选择的下一个节点的审批人,是否合法 + * + * 1. 是否有漏选:没有选择审批人 + * 2. 是否有多选:非下一个节点 + * + * @param taskDefinitionKey 当前任务节点标识 + * @param variables 流程变量 + * @param bpmnModel 流程模型 + * @param nextAssignees 下一个节点审批人集合(参数) + * @param processInstance 流程实例 + */ + private Map validateAndSetNextAssignees(String taskDefinitionKey, Map variables, BpmnModel bpmnModel, + Map> nextAssignees, ProcessInstance processInstance) { + // 1. 获取下一个将要执行的节点集合 + FlowElement flowElement = bpmnModel.getFlowElement(taskDefinitionKey); + List nextFlowNodes = getNextFlowNodes(flowElement, bpmnModel, variables); + + // 2. 校验选择的下一个节点的审批人,是否合法 + Map> processVariables; + for (FlowNode nextFlowNode : nextFlowNodes) { + Integer candidateStrategy = parseCandidateStrategy(nextFlowNode); + // 2.1 情况一:如果节点中的审批人策略为 发起人自选 + if (ObjUtil.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy())) { + // 如果节点存在,但未配置审批人 + List assignees = nextAssignees != null ? nextAssignees.get(nextFlowNode.getId()) : null; + if (CollUtil.isEmpty(assignees)) { + throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, nextFlowNode.getName()); + } + processVariables = FlowableUtils.getStartUserSelectAssignees(processInstance.getProcessVariables()); + // 特殊:如果当前节点已经存在审批人,则不允许覆盖 + if (processVariables != null && CollUtil.isNotEmpty(processVariables.get(nextFlowNode.getId()))) { + continue; + } + // 设置 PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES + if (processVariables == null) { + processVariables = new HashMap<>(); + } + processVariables.put(nextFlowNode.getId(), assignees); + variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, processVariables); + } + // 2.2 情况二:如果节点中的审批人策略为 审批人,在审批时选择下一个节点的审批人,并且该节点的审批人为空 + if (ObjUtil.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy())) { + // 如果节点存在,但未配置审批人 + List assignees = nextAssignees != null ? nextAssignees.get(nextFlowNode.getId()) : null; + if (CollUtil.isEmpty(assignees)) { + throw exception(PROCESS_INSTANCE_APPROVE_USER_SELECT_ASSIGNEES_NOT_CONFIG, nextFlowNode.getName()); + } + processVariables = FlowableUtils.getApproveUserSelectAssignees(processInstance.getProcessVariables()); + if (processVariables == null) { + processVariables = new HashMap<>(); + } + // 设置 PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES + processVariables.put(nextFlowNode.getId(), assignees); + variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES, processVariables); + } + } + return variables; + } + /** * 审批通过存在“后加签”的任务。 *

@@ -858,7 +971,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(taskList.get(0).getProcessDefinitionId()); List activityIds = CollUtil.newArrayList(convertSet(taskList, Task::getTaskDefinitionKey)); EndEvent endEvent = BpmnModelUtils.getEndEvent(bpmnModel); - Assert.notNull(endEvent, "结束节点不能未空"); + Assert.notNull(endEvent, "结束节点不能为空"); runtimeService.createChangeActivityStateBuilder() .processInstanceId(processInstanceId) .moveActivityIdsToSingleActivityId(activityIds, endEvent.getId()) @@ -991,6 +1104,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { @Override @Transactional(rollbackFor = Exception.class) + @SuppressWarnings("DataFlowIssue") public void deleteSignTask(Long userId, BpmTaskSignDeleteReqVO reqVO) { // 1.1 校验 task 可以被减签 Task task = validateTaskCanSignDelete(reqVO.getId()); @@ -1142,6 +1256,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { } @Override + @DataPermission(enable = false) // 忽略数据权限,避免因为过滤,导致找不到候选人 public void processTaskAssigned(Task task) { // 发送通知。在事务提交时,批量执行操作,所以直接查询会无法查询到 ProcessInstance,所以这里是通过监听事务的提交来实现。 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @@ -1207,19 +1322,31 @@ public class BpmTaskServiceImpl implements BpmTaskService { } } - // 审批人与提交人为同一人时,根据 BpmUserTaskAssignStartUserHandlerTypeEnum 策略进行处理 - if (StrUtil.equals(task.getAssignee(), processInstance.getStartUserId())) { - // 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略 - // TODO 芋艿:【优化】未来有没更好的判断方式?!另外,还要考虑清理机制。就是说,下次处理了之后,就移除这个标识 - Boolean returnTaskFlag = runtimeService.getVariable(processInstance.getProcessInstanceId(), - String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class); + // 获取发起人节点 + BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId()); + if (bpmnModel == null) { + log.error("[processTaskAssigned][taskId({}) 没有找到流程模型]", task.getId()); + return; + } + FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey()); + // 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略 + // TODO 芋艿:【优化】未来有没更好的判断方式?!另外,还要考虑清理机制。就是说,下次处理了之后,就移除这个标识 + Boolean returnTaskFlag = runtimeService.getVariable(processInstance.getProcessInstanceId(), + String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class); + Boolean skipStartUserNodeFlag = Convert.toBool(runtimeService.getVariable(processInstance.getProcessInstanceId(), + PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class)); + if (userTaskElement.getId().equals(START_USER_NODE_ID) + && (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核 + || Boolean.TRUE.equals(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核 + && ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) { + getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) + .setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason())); + return; + } + // 当不为发起人节点时,审批人与提交人为同一人时,根据 BpmUserTaskAssignStartUserHandlerTypeEnum 策略进行处理 + if (ObjectUtil.notEqual(userTaskElement.getId(), START_USER_NODE_ID) + && StrUtil.equals(task.getAssignee(), processInstance.getStartUserId())) { if (ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) { - BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId()); - if (bpmnModel == null) { - log.error("[processTaskAssigned][taskId({}) 没有找到流程模型]", task.getId()); - return; - } - FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey()); Integer assignStartUserHandlerType = BpmnModelUtils.parseAssignStartUserHandlerType(userTaskElement); // 情况一:自动跳过 @@ -1304,14 +1431,23 @@ public class BpmTaskServiceImpl implements BpmTaskService { } @Override - public void processDelayTimerTimeout(String processInstanceId, String taskDefineKey) { + @Transactional(rollbackFor = Exception.class) + public void processChildProcessTimeout(String processInstanceId, String taskDefineKey) { + List activityInstances = runtimeService.createActivityInstanceQuery() + .processInstanceId(processInstanceId) + .activityId(taskDefineKey).list(); + activityInstances.forEach(activityInstance -> FlowableUtils.execute(activityInstance.getTenantId(), + () -> moveTaskToEnd(activityInstance.getCalledProcessInstanceId(), BpmReasonEnum.TIMEOUT_APPROVE.getReason()))); + } + + @Override + public void triggerTask(String processInstanceId, String taskDefineKey) { Execution execution = runtimeService.createExecutionQuery() .processInstanceId(processInstanceId) .activityId(taskDefineKey) .singleResult(); if (execution == null) { - log.error("[processDelayTimerTimeout][processInstanceId({}) activityId({}) 没有找到执行活动]", - processInstanceId, taskDefineKey); + log.error("[triggerTask][processInstanceId({}) activityId({}) 没有找到执行活动]", processInstanceId, taskDefineKey); return; } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmCallActivityListener.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmCallActivityListener.java new file mode 100644 index 000000000..40313a966 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmCallActivityListener.java @@ -0,0 +1,96 @@ +package cn.iocoder.yudao.module.bpm.service.task.listener; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessStartUserEmptyTypeEnum; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessStartUserTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; +import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import jakarta.annotation.Resource; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.delegate.ExecutionListener; +import org.flowable.engine.impl.el.FixedValue; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * BPM 子流程监听器:设置流程的发起人 + * + * @author Lesan + */ +@Component +@Slf4j +public class BpmCallActivityListener implements ExecutionListener { + + public static final String DELEGATE_EXPRESSION = "${bpmCallActivityListener}"; + + @Setter + private FixedValue listenerConfig; + + @Resource + private BpmProcessDefinitionService processDefinitionService; + + @Resource + private BpmProcessInstanceService processInstanceService; + + @Override + public void notify(DelegateExecution execution) { + String expressionText = listenerConfig.getExpressionText(); + Assert.notNull(expressionText, "监听器扩展字段({})不能为空", expressionText); + BpmSimpleModelNodeVO.ChildProcessSetting.StartUserSetting startUserSetting = JsonUtils.parseObject( + expressionText, BpmSimpleModelNodeVO.ChildProcessSetting.StartUserSetting.class); + ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getRootProcessInstanceId()); + + // 1. 当发起人来源为主流程发起人时,并兜底 startUserSetting 为空时 + if (startUserSetting == null + || startUserSetting.getType().equals(BpmChildProcessStartUserTypeEnum.MAIN_PROCESS_START_USER.getType())) { + FlowableUtils.setAuthenticatedUserId(Long.parseLong(processInstance.getStartUserId())); + return; + } + + // 2. 当发起人来源为表单时 + if (startUserSetting.getType().equals(BpmChildProcessStartUserTypeEnum.FROM_FORM.getType())) { + String formFieldValue = MapUtil.getStr(processInstance.getProcessVariables(), startUserSetting.getFormField()); + // 2.1 当表单值为空时 + if (StrUtil.isEmpty(formFieldValue)) { + // 2.1.1 来自主流程发起人 + if (startUserSetting.getEmptyType().equals(BpmChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER.getType())) { + FlowableUtils.setAuthenticatedUserId(Long.parseLong(processInstance.getStartUserId())); + return; + } + // 2.1.2 来自子流程管理员 + if (startUserSetting.getEmptyType().equals(BpmChildProcessStartUserEmptyTypeEnum.CHILD_PROCESS_ADMIN.getType())) { + BpmProcessDefinitionInfoDO processDefinition = processDefinitionService.getProcessDefinitionInfo(execution.getProcessDefinitionId()); + List managerUserIds = processDefinition.getManagerUserIds(); + FlowableUtils.setAuthenticatedUserId(managerUserIds.get(0)); + return; + } + // 2.1.3 来自主流程管理员 + if (startUserSetting.getEmptyType().equals(BpmChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_ADMIN.getType())) { + BpmProcessDefinitionInfoDO processDefinition = processDefinitionService.getProcessDefinitionInfo(processInstance.getProcessDefinitionId()); + List managerUserIds = processDefinition.getManagerUserIds(); + FlowableUtils.setAuthenticatedUserId(managerUserIds.get(0)); + return; + } + } + // 2.2 使用表单值,并兜底字符串转 Long 失败时使用主流程发起人 + try { + FlowableUtils.setAuthenticatedUserId(Long.parseLong(formFieldValue)); + } catch (Exception e) { + log.error("[notify][监听器:{},子流程监听器设置流程的发起人字符串转 Long 失败,字符串:{}]", + DELEGATE_EXPRESSION, formFieldValue); + FlowableUtils.setAuthenticatedUserId(Long.parseLong(processInstance.getStartUserId())); + } + } + } + +} \ No newline at end of file diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java index 97c103c14..54fa9b12c 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java @@ -1,29 +1,20 @@ package cn.iocoder.yudao.module.bpm.service.task.listener; -import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; -import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmHttpRequestParamTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; import jakarta.annotation.Resource; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.flowable.engine.delegate.TaskListener; -import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.impl.el.FixedValue; +import org.flowable.engine.runtime.ProcessInstance; import org.flowable.task.service.delegate.DelegateTask; import org.springframework.context.annotation.Scope; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; -import java.util.Map; - -import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseListenerConfig; // TODO @芋艿:可能会想换个包地址 @@ -42,55 +33,31 @@ public class BpmUserTaskListener implements TaskListener { @Resource private BpmProcessInstanceService processInstanceService; - @Resource - private RestTemplate restTemplate; - @Setter private FixedValue listenerConfig; @Override public void notify(DelegateTask delegateTask) { // 1. 获取所需基础信息 - HistoricProcessInstance processInstance = processInstanceService.getHistoricProcessInstance(delegateTask.getProcessInstanceId()); + ProcessInstance processInstance = processInstanceService.getProcessInstance(delegateTask.getProcessInstanceId()); BpmSimpleModelNodeVO.ListenerHandler listenerHandler = parseListenerConfig(listenerConfig); - // 2. 获取请求头和请求体 - Map processVariables = processInstance.getProcessVariables(); - MultiValueMap headers = new LinkedMultiValueMap<>(); - MultiValueMap body = new LinkedMultiValueMap<>(); - SimpleModelUtils.addHttpRequestParam(headers, listenerHandler.getHeader(), processVariables); - SimpleModelUtils.addHttpRequestParam(body, listenerHandler.getBody(), processVariables); - // 2.1 请求头默认参数 - if (StrUtil.isNotEmpty(delegateTask.getTenantId())) { - headers.add(HEADER_TENANT_ID, delegateTask.getTenantId()); - } - // 2.2 请求体默认参数 + // 2. 发起请求 // TODO @芋艿:哪些默认参数,后续再调研下;感觉可以搞个 task 字段,把整个 delegateTask 放进去; - body.add("processInstanceId", delegateTask.getProcessInstanceId()); - body.add("assignee", delegateTask.getAssignee()); - body.add("taskDefinitionKey", delegateTask.getTaskDefinitionKey()); - body.add("taskId", delegateTask.getId()); + listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("processInstanceId") + .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getProcessInstanceId())); + listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("assignee") + .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getAssignee())); + listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("taskDefinitionKey") + .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getTaskDefinitionKey())); + listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("taskId") + .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getId())); + BpmHttpRequestUtils.executeBpmHttpRequest(processInstance, + listenerHandler.getPath(), + listenerHandler.getHeader(), + listenerHandler.getBody(), + false, null); - // 3. 异步发起请求 - // TODO @芋艿:确认要同步,还是异步 - HttpEntity> requestEntity = new HttpEntity<>(body, headers); - try { - ResponseEntity responseEntity = restTemplate.exchange(listenerHandler.getPath(), HttpMethod.POST, - requestEntity, String.class); - log.info("[notify][监听器:{},事件类型:{},请求头:{},请求体:{},响应结果:{}]", - DELEGATE_EXPRESSION, - delegateTask.getEventName(), - headers, - body, - responseEntity); - } catch (RestClientException e) { - log.error("[error][监听器:{},事件类型:{},请求头:{},请求体:{},请求出错:{}]", - DELEGATE_EXPRESSION, - delegateTask.getEventName(), - headers, - body, - e.getMessage()); - } - // 4. 是否需要后续操作?TODO 芋艿:待定! + // 3. 是否需要后续操作?TODO 芋艿:待定! } } \ No newline at end of file diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmHttpRequestTrigger.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmHttpRequestTrigger.java deleted file mode 100644 index 52936f93f..000000000 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmHttpRequestTrigger.java +++ /dev/null @@ -1,124 +0,0 @@ -package cn.iocoder.yudao.module.bpm.service.task.trigger; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.core.KeyValue; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting; -import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum; -import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils; -import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; -import com.fasterxml.jackson.core.type.TypeReference; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.flowable.engine.runtime.ProcessInstance; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Component; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.client.RestClientException; -import org.springframework.web.client.RestTemplate; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; - -/** - * BPM 发送 HTTP 请求触发器 - * - * @author jason - */ -@Component -@Slf4j -public class BpmHttpRequestTrigger implements BpmTrigger { - - @Resource - private BpmProcessInstanceService processInstanceService; - - @Resource - private RestTemplate restTemplate; - - @Override - public BpmTriggerTypeEnum getType() { - return BpmTriggerTypeEnum.HTTP_REQUEST; - } - - @Override - public void execute(String processInstanceId, String param) { - // 1. 解析 http 请求配置 - HttpRequestTriggerSetting setting = JsonUtils.parseObject(param, HttpRequestTriggerSetting.class); - if (setting == null) { - log.error("[execute][流程({}) HTTP 触发器请求配置为空]", processInstanceId); - return; - } - // 2.1 设置请求头 - ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId); - Map processVariables = processInstance.getProcessVariables(); - MultiValueMap headers = new LinkedMultiValueMap<>(); - headers.add(HEADER_TENANT_ID, processInstance.getTenantId()); - SimpleModelUtils.addHttpRequestParam(headers, setting.getHeader(), processVariables); - // 2.2 设置请求体 - MultiValueMap body = new LinkedMultiValueMap<>(); - SimpleModelUtils.addHttpRequestParam(body, setting.getBody(), processVariables); - body.add("processInstanceId", processInstanceId); - - // TODO @芋艿:要不要抽象一个 Http 请求的工具类,方便复用呢? - // 3. 发起请求 - HttpEntity> requestEntity = new HttpEntity<>(body, headers); - ResponseEntity responseEntity; - try { - responseEntity = restTemplate.exchange(setting.getUrl(), HttpMethod.POST, - requestEntity, String.class); - log.info("[execute][HTTP 触发器,请求头:{},请求体:{},响应结果:{}]", headers, body, responseEntity); - } catch (RestClientException e) { - log.error("[execute][HTTP 触发器,请求头:{},请求体:{},请求出错:{}]", headers, body, e.getMessage()); - return; - } - - // 4.1 判断是否需要解析返回值 - if (StrUtil.isEmpty(responseEntity.getBody()) - || !responseEntity.getStatusCode().is2xxSuccessful() - || CollUtil.isEmpty(setting.getResponse())) { - return; - } - // 4.2 解析返回值, 返回值必须符合 CommonResult 规范。 - CommonResult> respResult = JsonUtils.parseObjectQuietly( - responseEntity.getBody(), new TypeReference<>() {}); - if (respResult == null || !respResult.isSuccess()){ - return; - } - // 4.3 获取需要更新的流程变量 - Map updateVariables = getNeedUpdatedVariablesFromResponse(respResult.getData(), setting.getResponse()); - // 4.4 更新流程变量 - if (CollUtil.isNotEmpty(updateVariables)) { - processInstanceService.updateProcessInstanceVariables(processInstanceId, updateVariables); - } - } - - /** - * 从请求返回值获取需要更新的流程变量 - * - * @param result 请求返回结果 - * @param responseSettings 返回设置 - * @return 需要更新的流程变量 - */ - private Map getNeedUpdatedVariablesFromResponse(Map result, - List> responseSettings) { - Map updateVariables = new HashMap<>(); - if (CollUtil.isEmpty(result)) { - return updateVariables; - } - responseSettings.forEach(responseSetting -> { - if (StrUtil.isNotEmpty(responseSetting.getKey()) && result.containsKey(responseSetting.getValue())) { - updateVariables.put(responseSetting.getKey(), result.get(responseSetting.getValue())); - } - }); - return updateVariables; - } - -} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmUpdateNormalFormTrigger.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmUpdateNormalFormTrigger.java deleted file mode 100644 index deab1f5e3..000000000 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmUpdateNormalFormTrigger.java +++ /dev/null @@ -1,44 +0,0 @@ -package cn.iocoder.yudao.module.bpm.service.task.trigger; - -import cn.hutool.core.collection.CollUtil; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.TriggerSetting.NormalFormTriggerSetting; -import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum; -import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -// TODO @jason:改成 BpmFormUpdateTrigger -/** - * BPM 更新流程表单触发器 - * - * @author jason - */ -@Component -@Slf4j -public class BpmUpdateNormalFormTrigger implements BpmTrigger { - - @Resource - private BpmProcessInstanceService processInstanceService; - - @Override - public BpmTriggerTypeEnum getType() { - return BpmTriggerTypeEnum.UPDATE_NORMAL_FORM; - } - - @Override - public void execute(String processInstanceId, String param) { - // 1. 解析更新流程表单配置 - NormalFormTriggerSetting setting = JsonUtils.parseObject(param, NormalFormTriggerSetting.class); - if (setting == null) { - log.error("[execute][流程({}) 更新流程表单触发器配置为空]", processInstanceId); - return; - } - // 2.更新流程变量 - if (CollUtil.isNotEmpty(setting.getUpdateFormFields())) { - processInstanceService.updateProcessInstanceVariables(processInstanceId, setting.getUpdateFormFields()); - } - } - -} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/form/BpmFormDeleteTrigger.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/form/BpmFormDeleteTrigger.java new file mode 100644 index 000000000..c441c5f9b --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/form/BpmFormDeleteTrigger.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.bpm.service.task.trigger.form; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import cn.iocoder.yudao.module.bpm.service.task.trigger.BpmTrigger; +import com.fasterxml.jackson.core.type.TypeReference; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * BPM 删除流程表单数据触发器 + * + * @author jason + */ +@Component +@Slf4j +public class BpmFormDeleteTrigger implements BpmTrigger { + + @Resource + private BpmProcessInstanceService processInstanceService; + + @Override + public BpmTriggerTypeEnum getType() { + return BpmTriggerTypeEnum.FORM_DELETE; + } + + @Override + public void execute(String processInstanceId, String param) { + // 1. 解析删除流程表单数据配置 + List settings = JsonUtils.parseObject(param, new TypeReference<>() {}); + if (CollUtil.isEmpty(settings)) { + log.error("[execute][流程({}) 删除流程表单数据触发器配置为空]", processInstanceId); + return; + } + + // 2. 获取流程变量 + Map processVariables = processInstanceService.getProcessInstance(processInstanceId).getProcessVariables(); + + // 3.1 获取需要删除的表单字段 + Set deleteFields = new HashSet<>(); + settings.forEach(setting -> { + if (CollUtil.isEmpty(setting.getDeleteFields())) { + return; + } + // 配置了条件,判断条件是否满足 + boolean isFieldDeletedNeeded = true; + if (setting.getConditionType() != null) { + String conditionExpression = SimpleModelUtils.buildConditionExpression( + setting.getConditionType(), setting.getConditionExpression(), setting.getConditionGroups()); + isFieldDeletedNeeded = BpmnModelUtils.evalConditionExpress(processVariables, conditionExpression); + } + if (isFieldDeletedNeeded) { + deleteFields.addAll(setting.getDeleteFields()); + } + }); + + // 3.2 删除流程变量 + if (CollUtil.isNotEmpty(deleteFields)) { + processInstanceService.removeProcessInstanceVariables(processInstanceId, deleteFields); + } + } +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/form/BpmFormUpdateTrigger.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/form/BpmFormUpdateTrigger.java new file mode 100644 index 000000000..4940d697d --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/form/BpmFormUpdateTrigger.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.module.bpm.service.task.trigger.form; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.TriggerSetting.FormTriggerSetting; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import cn.iocoder.yudao.module.bpm.service.task.trigger.BpmTrigger; +import com.fasterxml.jackson.core.type.TypeReference; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * BPM 更新流程表单触发器 + * + * @author jason + */ +@Component +@Slf4j +public class BpmFormUpdateTrigger implements BpmTrigger { + + @Resource + private BpmProcessInstanceService processInstanceService; + + @Override + public BpmTriggerTypeEnum getType() { + return BpmTriggerTypeEnum.FORM_UPDATE; + } + + @Override + public void execute(String processInstanceId, String param) { + // 1. 解析更新流程表单配置 + List settings = JsonUtils.parseObject(param, new TypeReference<>() {}); + if (CollUtil.isEmpty(settings)) { + log.error("[execute][流程({}) 更新流程表单触发器配置为空]", processInstanceId); + return; + } + + // 2. 获取流程变量 + Map processVariables = processInstanceService.getProcessInstance(processInstanceId).getProcessVariables(); + + // 3. 更新流程变量 + for (FormTriggerSetting setting : settings) { + if (CollUtil.isEmpty(setting.getUpdateFormFields())) { + continue; + } + // 配置了条件,判断条件是否满足 + boolean isFormUpdateNeeded = true; + if (setting.getConditionType() != null) { + String conditionExpression = SimpleModelUtils.buildConditionExpression( + setting.getConditionType(), setting.getConditionExpression(), setting.getConditionGroups()); + isFormUpdateNeeded = BpmnModelUtils.evalConditionExpress(processVariables, conditionExpression); + } + // 更新流程表单 + if (isFormUpdateNeeded) { + processInstanceService.updateProcessInstanceVariables(processInstanceId, setting.getUpdateFormFields()); + } + } + } +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmAbstractHttpRequestTrigger.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmAbstractHttpRequestTrigger.java new file mode 100644 index 000000000..b1d81bc14 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmAbstractHttpRequestTrigger.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.module.bpm.service.task.trigger.http; + +import cn.iocoder.yudao.module.bpm.service.task.trigger.BpmTrigger; +import lombok.extern.slf4j.Slf4j; + +/** + * BPM 发送 HTTP 请求触发器抽象类 + * + * @author jason + */ +@Slf4j +public abstract class BpmAbstractHttpRequestTrigger implements BpmTrigger { + +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmHttpCallbackTrigger.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmHttpCallbackTrigger.java new file mode 100644 index 000000000..351b57ddd --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmHttpCallbackTrigger.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.bpm.service.task.trigger.http; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmHttpRequestParamTypeEnum; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +/** + * BPM HTTP 回调触发器 + * + * @author jason + */ +@Component +@Slf4j +public class BpmHttpCallbackTrigger extends BpmAbstractHttpRequestTrigger { + + @Resource + private BpmProcessInstanceService processInstanceService; + + @Override + public BpmTriggerTypeEnum getType() { + return BpmTriggerTypeEnum.HTTP_CALLBACK; + } + + @Override + public void execute(String processInstanceId, String param) { + // 1. 解析 http 请求配置 + BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting setting = JsonUtils.parseObject(param, + BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting.class); + if (setting == null) { + log.error("[execute][流程({}) HTTP 回调触发器配置为空]", processInstanceId); + return; + } + + // 2. 发起请求 + ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId); + setting.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam() + .setKey("taskDefineKey") // 重要:回调请求 taskDefineKey 需要传给被调用方,用于回调执行 + .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(setting.getCallbackTaskDefineKey())); + BpmHttpRequestUtils.executeBpmHttpRequest(processInstance, + setting.getUrl(), + setting.getHeader(), + setting.getBody(), + false, null); + } + +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmSyncHttpRequestTrigger.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmSyncHttpRequestTrigger.java new file mode 100644 index 000000000..2ac04117e --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmSyncHttpRequestTrigger.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.bpm.service.task.trigger.http; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +/** + * BPM 发送同步 HTTP 请求触发器 + * + * @author jason + */ +@Component +@Slf4j +public class BpmSyncHttpRequestTrigger extends BpmAbstractHttpRequestTrigger { + + @Resource + private BpmProcessInstanceService processInstanceService; + + @Override + public BpmTriggerTypeEnum getType() { + return BpmTriggerTypeEnum.HTTP_REQUEST; + } + + @Override + public void execute(String processInstanceId, String param) { + // 1. 解析 http 请求配置 + HttpRequestTriggerSetting setting = JsonUtils.parseObject(param, HttpRequestTriggerSetting.class); + if (setting == null) { + log.error("[execute][流程({}) HTTP 触发器请求配置为空]", processInstanceId); + return; + } + + // 2. 发起请求 + ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId); + BpmHttpRequestUtils.executeBpmHttpRequest(processInstance, + setting.getUrl(), + setting.getHeader(), + setting.getBody(), + true, setting.getResponse()); + } + +} diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java index 269cac6cc..c24614ad5 100644 --- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java +++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java @@ -66,7 +66,9 @@ public class CrmReceivableDO extends BaseDO { */ private LocalDateTime returnTime; /** - * 回款方式,关联枚举{@link CrmReceivableReturnTypeEnum} + * 回款方式 + * + * 枚举 {@link CrmReceivableReturnTypeEnum} */ private Integer returnType; /** diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java index 8970c004c..b25870fe8 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java @@ -2,13 +2,13 @@ package cn.iocoder.yudao.module.infra.framework.file.core.utils; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; import com.alibaba.ttl.TransmittableThreadLocal; import jakarta.servlet.http.HttpServletResponse; import lombok.SneakyThrows; import org.apache.tika.Tika; import java.io.IOException; -import java.net.URLEncoder; /** * 文件类型 Utils @@ -60,7 +60,7 @@ public class FileTypeUtils { */ public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException { // 设置 header 和 contentType - response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8")); + response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); String contentType = getMineType(content, filename); response.setContentType(contentType); // 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题 diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImpl.java index 9483e8f73..4433fc2a7 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImpl.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImpl.java @@ -1,8 +1,8 @@ package cn.iocoder.yudao.module.infra.service.logger; -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.framework.common.util.string.StrUtils; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; @@ -35,8 +35,8 @@ public class ApiAccessLogServiceImpl implements ApiAccessLogService { @Override public void createApiAccessLog(ApiAccessLogCreateReqDTO createDTO) { ApiAccessLogDO apiAccessLog = BeanUtils.toBean(createDTO, ApiAccessLogDO.class); - apiAccessLog.setRequestParams(StrUtil.maxLength(apiAccessLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH)); - apiAccessLog.setResultMsg(StrUtil.maxLength(apiAccessLog.getResultMsg(), RESULT_MSG_MAX_LENGTH)); + apiAccessLog.setRequestParams(StrUtils.maxLength(apiAccessLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH)); + apiAccessLog.setResultMsg(StrUtils.maxLength(apiAccessLog.getResultMsg(), RESULT_MSG_MAX_LENGTH)); if (TenantContextHolder.getTenantId() != null) { apiAccessLogMapper.insert(apiAccessLog); } else { diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java index 747b220b5..1fcbc8dd7 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java @@ -1,8 +1,8 @@ package cn.iocoder.yudao.module.infra.service.logger; -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.framework.common.util.string.StrUtils; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; @@ -39,7 +39,7 @@ public class ApiErrorLogServiceImpl implements ApiErrorLogService { public void createApiErrorLog(ApiErrorLogCreateReqDTO createDTO) { ApiErrorLogDO apiErrorLog = BeanUtils.toBean(createDTO, ApiErrorLogDO.class) .setProcessStatus(ApiErrorLogProcessStatusEnum.INIT.getStatus()); - apiErrorLog.setRequestParams(StrUtil.maxLength(apiErrorLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH)); + apiErrorLog.setRequestParams(StrUtils.maxLength(apiErrorLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH)); if (TenantContextHolder.getTenantId() != null) { apiErrorLogMapper.insert(apiErrorLog); } else { diff --git a/yudao-module-iot/pom.xml b/yudao-module-iot/pom.xml index 069af1699..0422c5d6c 100644 --- a/yudao-module-iot/pom.xml +++ b/yudao-module-iot/pom.xml @@ -10,6 +10,7 @@ yudao-module-iot-api yudao-module-iot-biz + yudao-module-iot-plugins 4.0.0 diff --git a/yudao-module-iot/yudao-module-iot-api/pom.xml b/yudao-module-iot/yudao-module-iot-api/pom.xml index 8a75b09bf..4a31c9bf5 100644 --- a/yudao-module-iot/yudao-module-iot-api/pom.xml +++ b/yudao-module-iot/yudao-module-iot-api/pom.xml @@ -12,6 +12,7 @@ jar ${project.artifactId} + 物联网 模块 API,暴露给其它模块调用 @@ -21,6 +22,32 @@ cn.iocoder.boot yudao-common + + + + org.springframework + spring-web + provided + + + + + com.fasterxml.jackson.core + jackson-databind + provided + + + + org.pf4j + pf4j-spring + + + + + org.springframework.boot + spring-boot-starter-validation + true + diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/IotDeviceUpstreamApi.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/IotDeviceUpstreamApi.java new file mode 100644 index 000000000..e88706ac5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/IotDeviceUpstreamApi.java @@ -0,0 +1,93 @@ +package cn.iocoder.yudao.module.iot.api.device; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; +import cn.iocoder.yudao.module.iot.enums.ApiConstants; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +/** + * 设备数据 Upstream 上行 API + * + * 目的:设备 -> 插件 -> 服务端 + * + * @author haohao + */ +public interface IotDeviceUpstreamApi { + + String PREFIX = ApiConstants.PREFIX + "/device/upstream"; + + // ========== 设备相关 ========== + + /** + * 更新设备状态 + * + * @param updateReqDTO 更新设备状态 DTO + */ + @PostMapping(PREFIX + "/update-state") + CommonResult updateDeviceState(@Valid @RequestBody IotDeviceStateUpdateReqDTO updateReqDTO); + + /** + * 上报设备属性数据 + * + * @param reportReqDTO 上报设备属性数据 DTO + */ + @PostMapping(PREFIX + "/report-property") + CommonResult reportDeviceProperty(@Valid @RequestBody IotDevicePropertyReportReqDTO reportReqDTO); + + /** + * 上报设备事件数据 + * + * @param reportReqDTO 设备事件 + */ + @PostMapping(PREFIX + "/report-event") + CommonResult reportDeviceEvent(@Valid @RequestBody IotDeviceEventReportReqDTO reportReqDTO); + + // TODO @芋艿:这个需要 plugins 接入下 + /** + * 注册设备 + * + * @param registerReqDTO 注册设备 DTO + */ + @PostMapping(PREFIX + "/register") + CommonResult registerDevice(@Valid @RequestBody IotDeviceRegisterReqDTO registerReqDTO); + + // TODO @芋艿:这个需要 plugins 接入下 + /** + * 注册子设备 + * + * @param registerReqDTO 注册子设备 DTO + */ + @PostMapping(PREFIX + "/register-sub") + CommonResult registerSubDevice(@Valid @RequestBody IotDeviceRegisterSubReqDTO registerReqDTO); + + // TODO @芋艿:这个需要 plugins 接入下 + /** + * 注册设备拓扑 + * + * @param addReqDTO 注册设备拓扑 DTO + */ + @PostMapping(PREFIX + "/add-topology") + CommonResult addDeviceTopology(@Valid @RequestBody IotDeviceTopologyAddReqDTO addReqDTO); + + // TODO @芋艿:考虑 http 认证 + /** + * 认证 Emqx 连接 + * + * @param authReqDTO 认证 Emqx 连接 DTO + */ + @PostMapping(PREFIX + "/authenticate-emqx-connection") + CommonResult authenticateEmqxConnection(@Valid @RequestBody IotDeviceEmqxAuthReqDTO authReqDTO); + + // ========== 插件相关 ========== + + /** + * 心跳插件实例 + * + * @param heartbeatReqDTO 心跳插件实例 DTO + */ + @PostMapping(PREFIX + "/heartbeat-plugin-instance") + CommonResult heartbeatPluginInstance(@Valid @RequestBody IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceConfigSetReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceConfigSetReqDTO.java new file mode 100644 index 000000000..9624b671e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceConfigSetReqDTO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.Map; + +/** + * IoT 设备【配置】设置 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotDeviceConfigSetReqDTO extends IotDeviceDownstreamAbstractReqDTO { + + /** + * 配置 + */ + @NotNull(message = "配置不能为空") + private Map config; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceDownstreamAbstractReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceDownstreamAbstractReqDTO.java new file mode 100644 index 000000000..e78bea6fb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceDownstreamAbstractReqDTO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +/** + * IoT 设备下行的抽象 Request DTO + * + * @author 芋道源码 + */ +@Data +public abstract class IotDeviceDownstreamAbstractReqDTO { + + /** + * 请求编号 + */ + private String requestId; + + /** + * 产品标识 + */ + @NotEmpty(message = "产品标识不能为空") + private String productKey; + /** + * 设备名称 + */ + @NotEmpty(message = "设备名称不能为空") + private String deviceName; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceOtaUpgradeReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceOtaUpgradeReqDTO.java new file mode 100644 index 000000000..8eccec42e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceOtaUpgradeReqDTO.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream; + +import cn.hutool.core.map.MapUtil; +import lombok.Data; + +import java.util.Map; + +/** + * IoT 设备【OTA】升级下发 Request DTO(更新固件消息) + * + * @author 芋道源码 + */ +@Data +public class IotDeviceOtaUpgradeReqDTO extends IotDeviceDownstreamAbstractReqDTO { + + /** + * 固件编号 + */ + private Long firmwareId; + /** + * 固件版本 + */ + private String version; + + /** + * 签名方式 + * + * 例如说:MD5、SHA256 + */ + private String signMethod; + /** + * 固件文件签名 + */ + private String fileSign; + /** + * 固件文件大小 + */ + private Long fileSize; + /** + * 固件文件 URL + */ + private String fileUrl; + + /** + * 自定义信息,建议使用 JSON 格式 + */ + private String information; + + public static IotDeviceOtaUpgradeReqDTO build(Map map) { + return new IotDeviceOtaUpgradeReqDTO() + .setFirmwareId(MapUtil.getLong(map, "firmwareId")).setVersion((String) map.get("version")) + .setSignMethod((String) map.get("signMethod")).setFileSign((String) map.get("fileSign")) + .setFileSize(MapUtil.getLong(map, "fileSize")).setFileUrl((String) map.get("fileUrl")) + .setInformation((String) map.get("information")); + } + + public static Map build(IotDeviceOtaUpgradeReqDTO dto) { + return MapUtil.builder() + .put("firmwareId", dto.getFirmwareId()).put("version", dto.getVersion()) + .put("signMethod", dto.getSignMethod()).put("fileSign", dto.getFileSign()) + .put("fileSize", dto.getFileSize()).put("fileUrl", dto.getFileUrl()) + .put("information", dto.getInformation()) + .build(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDevicePropertyGetReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDevicePropertyGetReqDTO.java new file mode 100644 index 000000000..d9ae96321 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDevicePropertyGetReqDTO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; + +// TODO @芋艿:从 server => plugin => device 是否有必要?从阿里云 iot 来看,没有这个功能?! +// TODO @芋艿:是不是改成 read 更好?在看看阿里云的 topic 设计 +/** + * IoT 设备【属性】获取 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotDevicePropertyGetReqDTO extends IotDeviceDownstreamAbstractReqDTO { + + /** + * 属性标识数组 + */ + @NotEmpty(message = "属性标识数组不能为空") + private List identifiers; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDevicePropertySetReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDevicePropertySetReqDTO.java new file mode 100644 index 000000000..170fe80f6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDevicePropertySetReqDTO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.Map; + +/** + * IoT 设备【属性】设置 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotDevicePropertySetReqDTO extends IotDeviceDownstreamAbstractReqDTO { + + /** + * 属性参数 + */ + @NotEmpty(message = "属性参数不能为空") + private Map properties; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceServiceInvokeReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceServiceInvokeReqDTO.java new file mode 100644 index 000000000..0a2b3f0bf --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceServiceInvokeReqDTO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.Map; + +/** + * IoT 设备【服务】调用 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotDeviceServiceInvokeReqDTO extends IotDeviceDownstreamAbstractReqDTO { + + /** + * 服务标识 + */ + @NotEmpty(message = "服务标识不能为空") + private String identifier; + /** + * 调用参数 + */ + private Map params; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEmqxAuthReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEmqxAuthReqDTO.java new file mode 100644 index 000000000..8762aae5b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEmqxAuthReqDTO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +// TODO @芋艿:要不要继承 IotDeviceUpstreamAbstractReqDTO +// TODO @芋艿:@haohao:后续其它认证的设计 +/** + * IoT 认证 Emqx 连接 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotDeviceEmqxAuthReqDTO { + + /** + * 客户端 ID + */ + @NotEmpty(message = "客户端 ID 不能为空") + private String clientId; + + /** + * 用户名 + */ + @NotEmpty(message = "用户名不能为空") + private String username; + + /** + * 密码 + */ + @NotEmpty(message = "密码不能为空") + private String password; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEventReportReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEventReportReqDTO.java new file mode 100644 index 000000000..34e6283d9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEventReportReqDTO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.Map; + +/** + * IoT 设备【事件】上报 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotDeviceEventReportReqDTO extends IotDeviceUpstreamAbstractReqDTO { + + /** + * 事件标识 + */ + @NotEmpty(message = "事件标识不能为空") + private String identifier; + /** + * 事件参数 + */ + private Map params; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaProgressReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaProgressReqDTO.java new file mode 100644 index 000000000..a88a72e91 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaProgressReqDTO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +import lombok.Data; + +// TODO @芋艿:待实现:/ota/${productKey}/${deviceName}/progress +/** + * IoT 设备【OTA】升级进度 Request DTO(上报更新固件进度) + * + * @author 芋道源码 + */ +@Data +public class IotDeviceOtaProgressReqDTO extends IotDeviceUpstreamAbstractReqDTO { + + /** + * 固件编号 + */ + private Long firmwareId; + + /** + * 升级状态 + * + * 枚举 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum} + */ + private Integer status; + /** + * 升级进度,百分比 + */ + private Integer progress; + + /** + * 升级进度描述 + */ + private String description; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaPullReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaPullReqDTO.java new file mode 100644 index 000000000..6328704e5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaPullReqDTO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +// TODO @芋艿:待实现:/ota/${productKey}/${deviceName}/pull +/** + * IoT 设备【OTA】升级下拉 Request DTO(拉取固件更新) + * + * @author 芋道源码 + */ +public class IotDeviceOtaPullReqDTO { + + /** + * 固件编号 + */ + private Long firmwareId; + + /** + * 固件版本 + */ + private String version; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaReportReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaReportReqDTO.java new file mode 100644 index 000000000..2b3b91c98 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaReportReqDTO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +// TODO @芋艿:待实现:/ota/${productKey}/${deviceName}/report +/** + * IoT 设备【OTA】上报 Request DTO(上报固件版本) + * + * @author 芋道源码 + */ +public class IotDeviceOtaReportReqDTO { + + /** + * 固件编号 + */ + private Long firmwareId; + + /** + * 固件版本 + */ + private String version; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDevicePropertyReportReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDevicePropertyReportReqDTO.java new file mode 100644 index 000000000..4a276bd22 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDevicePropertyReportReqDTO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.Map; + +/** + * IoT 设备【属性】上报 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotDevicePropertyReportReqDTO extends IotDeviceUpstreamAbstractReqDTO { + + /** + * 属性参数 + */ + @NotEmpty(message = "属性参数不能为空") + private Map properties; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceRegisterReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceRegisterReqDTO.java new file mode 100644 index 000000000..cab55e832 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceRegisterReqDTO.java @@ -0,0 +1,12 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +import lombok.Data; + +/** + * IoT 设备【注册】自己 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotDeviceRegisterReqDTO extends IotDeviceUpstreamAbstractReqDTO { +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceRegisterSubReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceRegisterSubReqDTO.java new file mode 100644 index 000000000..0b826fbb1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceRegisterSubReqDTO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; + +/** + * IoT 设备【注册】子设备 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotDeviceRegisterSubReqDTO extends IotDeviceUpstreamAbstractReqDTO { + + // TODO @芋艿:看看要不要优化命名 + /** + * 子设备数组 + */ + @NotEmpty(message = "子设备不能为空") + private List params; + + /** + * 设备信息 + */ + @Data + public static class Device { + + /** + * 产品标识 + */ + @NotEmpty(message = "产品标识不能为空") + private String productKey; + + /** + * 设备名称 + */ + @NotEmpty(message = "设备名称不能为空") + private String deviceName; + + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceStateUpdateReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceStateUpdateReqDTO.java new file mode 100644 index 000000000..38c479a57 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceStateUpdateReqDTO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * IoT 设备【状态】更新 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotDeviceStateUpdateReqDTO extends IotDeviceUpstreamAbstractReqDTO { + + /** + * 设备状态 + */ + @NotNull(message = "设备状态不能为空") + @InEnum(IotDeviceStateEnum.class) // 只使用:在线、离线 + private Integer state; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceTopologyAddReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceTopologyAddReqDTO.java new file mode 100644 index 000000000..18efe7d48 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceTopologyAddReqDTO.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; + +// TODO @芋艿:要写清楚,是来自设备网关,还是设备。 +/** + * IoT 设备【拓扑】添加 Request DTO + */ +@Data +public class IotDeviceTopologyAddReqDTO extends IotDeviceUpstreamAbstractReqDTO { + + // TODO @芋艿:看看要不要优化命名 + /** + * 子设备数组 + */ + @NotEmpty(message = "子设备不能为空") + private List params; + + /** + * 设备信息 + */ + @Data + public static class Device { + + /** + * 产品标识 + */ + @NotEmpty(message = "产品标识不能为空") + private String productKey; + + /** + * 设备名称 + */ + @NotEmpty(message = "设备名称不能为空") + private String deviceName; + + // TODO @芋艿:阿里云还有 sign 签名 + + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceUpstreamAbstractReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceUpstreamAbstractReqDTO.java new file mode 100644 index 000000000..a0c8ce92a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceUpstreamAbstractReqDTO.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeSerializer; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * IoT 设备上行的抽象 Request DTO + * + * @author 芋道源码 + */ +@Data +public abstract class IotDeviceUpstreamAbstractReqDTO { + + /** + * 请求编号 + */ + private String requestId; + + /** + * 插件实例的进程编号 + */ + private String processId; + + /** + * 产品标识 + */ + @NotEmpty(message = "产品标识不能为空") + private String productKey; + /** + * 设备名称 + */ + @NotEmpty(message = "设备名称不能为空") + private String deviceName; + + /** + * 上报时间 + */ + @JsonSerialize(using = TimestampLocalDateTimeSerializer.class) // 解决 iot plugins 序列化 LocalDateTime 是数组,导致无法解析的问题 + private LocalDateTime reportTime; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotPluginInstanceHeartbeatReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotPluginInstanceHeartbeatReqDTO.java new file mode 100644 index 000000000..9125b5f24 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotPluginInstanceHeartbeatReqDTO.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * IoT 插件实例心跳 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotPluginInstanceHeartbeatReqDTO { + + /** + * 请求编号 + */ + @NotEmpty(message = "请求编号不能为空") + private String processId; + + /** + * 插件包标识符 + */ + @NotEmpty(message = "插件包标识符不能为空") + private String pluginKey; + + /** + * 插件实例所在 IP + */ + @NotEmpty(message = "插件实例所在 IP 不能为空") + private String hostIp; + /** + * 插件实例的进程编号 + */ + @NotNull(message = "插件实例的进程编号不能为空") + private Integer downstreamPort; + + /** + * 是否在线 + */ + @NotNull(message = "是否在线不能为空") + private Boolean online; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/package-info.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/package-info.java new file mode 100644 index 000000000..cb946cd89 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/package-info.java @@ -0,0 +1,4 @@ +/** + * TODO 芋艿:占位 + */ +package cn.iocoder.yudao.module.iot.api.device.dto; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ApiConstants.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ApiConstants.java new file mode 100644 index 000000000..2c4147be1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ApiConstants.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.iot.enums; + +import cn.iocoder.yudao.framework.common.enums.RpcConstants; + +/** + * API 相关的枚举 + * + * @author 芋道源码 + */ +public class ApiConstants { + + public static final String PREFIX = RpcConstants.RPC_API_PREFIX + "/iot"; + + public static final String VERSION = "1.0.0"; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java new file mode 100644 index 000000000..d8f0cc60d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.iot.enums; + +/** + * IoT 字典类型的枚举类 + * + * @author 芋道源码 + */ +public class DictTypeConstants { + + public static final String PRODUCT_STATUS = "iot_product_status"; + public static final String PRODUCT_DEVICE_TYPE = "iot_product_device_type"; + public static final String NET_TYPE = "iot_net_type"; + public static final String PROTOCOL_TYPE = "iot_protocol_type"; + public static final String DATA_FORMAT = "iot_data_format"; + public static final String VALIDATE_TYPE = "iot_validate_type"; + + public static final String DEVICE_STATE = "iot_device_state"; + + public static final String IOT_DATA_BRIDGE_DIRECTION_ENUM = "iot_data_bridge_direction_enum"; + public static final String IOT_DATA_BRIDGE_TYPE_ENUM = "iot_data_bridge_type_enum"; + +} 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 31702e1e6..230baca3f 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 @@ -9,24 +9,67 @@ import cn.iocoder.yudao.framework.common.exception.ErrorCode; */ public interface ErrorCodeConstants { - // ========== IoT 产品相关 1-050-001-000 ============ + // ========== 产品相关 1-050-001-000 ============ ErrorCode PRODUCT_NOT_EXISTS = new ErrorCode(1_050_001_000, "产品不存在"); - ErrorCode PRODUCT_IDENTIFICATION_EXISTS = new ErrorCode(1_050_001_001, "产品标识已经存在"); + ErrorCode PRODUCT_KEY_EXISTS = new ErrorCode(1_050_001_001, "产品标识已经存在"); ErrorCode PRODUCT_STATUS_NOT_DELETE = new ErrorCode(1_050_001_002, "产品状是发布状态,不允许删除"); + ErrorCode PRODUCT_STATUS_NOT_ALLOW_THING_MODEL = new ErrorCode(1_050_001_003, "产品状是发布状态,不允许操作物模型"); - // ========== IoT 产品物模型 1-050-002-000 ============ - ErrorCode THINK_MODEL_FUNCTION_NOT_EXISTS = new ErrorCode(1_050_002_000, "产品物模型不存在"); - ErrorCode THINK_MODEL_FUNCTION_EXISTS_BY_PRODUCT_KEY = new ErrorCode(1_050_002_001, "ProductKey 对应的产品物模型已存在"); - ErrorCode THINK_MODEL_FUNCTION_IDENTIFIER_EXISTS = new ErrorCode(1_050_002_002, "存在重复的功能标识符。"); - ErrorCode THINK_MODEL_FUNCTION_NAME_EXISTS = new ErrorCode(1_050_002_003, "存在重复的功能名称。"); - ErrorCode THINK_MODEL_FUNCTION_IDENTIFIER_INVALID = new ErrorCode(1_050_002_003, "产品物模型标识无效"); + // ========== 产品物模型 1-050-002-000 ============ + ErrorCode THING_MODEL_NOT_EXISTS = new ErrorCode(1_050_002_000, "产品物模型不存在"); + ErrorCode THING_MODEL_EXISTS_BY_PRODUCT_KEY = new ErrorCode(1_050_002_001, "ProductKey 对应的产品物模型已存在"); + ErrorCode THING_MODEL_IDENTIFIER_EXISTS = new ErrorCode(1_050_002_002, "存在重复的功能标识符。"); + ErrorCode THING_MODEL_NAME_EXISTS = new ErrorCode(1_050_002_003, "存在重复的功能名称。"); + ErrorCode THING_MODEL_IDENTIFIER_INVALID = new ErrorCode(1_050_002_003, "产品物模型标识无效"); - // ========== IoT 设备 1-050-003-000 ============ + // ========== 设备 1-050-003-000 ============ ErrorCode DEVICE_NOT_EXISTS = new ErrorCode(1_050_003_000, "设备不存在"); ErrorCode DEVICE_NAME_EXISTS = new ErrorCode(1_050_003_001, "设备名称在同一产品下必须唯一"); ErrorCode DEVICE_HAS_CHILDREN = new ErrorCode(1_050_003_002, "有子设备,不允许删除"); - ErrorCode DEVICE_NAME_CANNOT_BE_MODIFIED = new ErrorCode(1_050_003_003, "设备名称不能修改"); - ErrorCode DEVICE_PRODUCT_CANNOT_BE_MODIFIED = new ErrorCode(1_050_003_004, "产品不能修改"); - ErrorCode DEVICE_INVALID_DEVICE_STATUS = new ErrorCode(1_050_003_005, "无效的设备状态"); + ErrorCode DEVICE_KEY_EXISTS = new ErrorCode(1_050_003_003, "设备标识已经存在"); + ErrorCode DEVICE_GATEWAY_NOT_EXISTS = new ErrorCode(1_050_003_004, "网关设备不存在"); + ErrorCode DEVICE_NOT_GATEWAY = new ErrorCode(1_050_003_005, "设备不是网关设备"); + ErrorCode DEVICE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_050_003_006, "导入设备数据不能为空!"); + ErrorCode DEVICE_DOWNSTREAM_FAILED = new ErrorCode(1_050_003_007, "执行失败,原因:{}"); -} + // ========== 产品分类 1-050-004-000 ========== + ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在"); + + // ========== 设备分组 1-050-005-000 ========== + ErrorCode DEVICE_GROUP_NOT_EXISTS = new ErrorCode(1_050_005_000, "设备分组不存在"); + ErrorCode DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS = new ErrorCode(1_050_005_001, "设备分组下存在设备,不允许删除"); + + // ========== 插件配置 1-050-006-000 ========== + ErrorCode PLUGIN_CONFIG_NOT_EXISTS = new ErrorCode(1_050_006_000, "插件配置不存在"); + ErrorCode PLUGIN_INSTALL_FAILED = new ErrorCode(1_050_006_001, "插件安装失败"); + ErrorCode PLUGIN_INSTALL_FAILED_FILE_NAME_NOT_MATCH = new ErrorCode(1_050_006_002, "插件安装失败,文件名与原插件id不匹配"); + ErrorCode PLUGIN_CONFIG_DELETE_FAILED_RUNNING = new ErrorCode(1_050_006_003, "请先停止插件"); + ErrorCode PLUGIN_STATUS_INVALID = new ErrorCode(1_050_006_004, "插件状态无效"); + ErrorCode PLUGIN_CONFIG_KEY_DUPLICATE = new ErrorCode(1_050_006_005, "插件标识已存在"); + ErrorCode PLUGIN_START_FAILED = new ErrorCode(1_050_006_006, "插件启动失败"); + ErrorCode PLUGIN_STOP_FAILED = new ErrorCode(1_050_006_007, "插件停止失败"); + + // ========== 插件实例 1-050-007-000 ========== + + // ========== 固件相关 1-050-008-000 ========== + + ErrorCode OTA_FIRMWARE_NOT_EXISTS = new ErrorCode(1_050_008_000, "固件信息不存在"); + ErrorCode OTA_FIRMWARE_PRODUCT_VERSION_DUPLICATE = new ErrorCode(1_050_008_001, "产品版本号重复"); + + ErrorCode OTA_UPGRADE_TASK_NOT_EXISTS = new ErrorCode(1_050_008_100, "升级任务不存在"); + ErrorCode OTA_UPGRADE_TASK_NAME_DUPLICATE = new ErrorCode(1_050_008_101, "升级任务名称重复"); + ErrorCode OTA_UPGRADE_TASK_DEVICE_IDS_EMPTY = new ErrorCode(1_050_008_102, "设备编号列表不能为空"); + ErrorCode OTA_UPGRADE_TASK_DEVICE_LIST_EMPTY = new ErrorCode(1_050_008_103, "设备列表不能为空"); + ErrorCode OTA_UPGRADE_TASK_CANNOT_CANCEL = new ErrorCode(1_050_008_104, "升级任务不能取消"); + + ErrorCode OTA_UPGRADE_RECORD_NOT_EXISTS = new ErrorCode(1_050_008_200, "升级记录不存在"); + ErrorCode OTA_UPGRADE_RECORD_DUPLICATE = new ErrorCode(1_050_008_201, "升级记录重复"); + ErrorCode OTA_UPGRADE_RECORD_CANNOT_RETRY = new ErrorCode(1_050_008_202, "升级记录不能重试"); + + // ========== MQTT 通信相关 1-050-009-000 ========== + ErrorCode MQTT_TOPIC_ILLEGAL = new ErrorCode(1_050_009_000, "topic illegal"); + + // ========== IoT 数据桥梁 1-050-010-000 ========== + ErrorCode DATA_BRIDGE_NOT_EXISTS = new ErrorCode(1_050_010_000, "IoT 数据桥梁不存在"); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageIdentifierEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageIdentifierEnum.java new file mode 100644 index 000000000..6de9359ba --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageIdentifierEnum.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.iot.enums.device; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +// TODO @芋艿:需要添加对应的 DTO,以及上下行的链路,网关、网关服务、设备等 +/** + * IoT 设备消息标识符枚举 + */ +@Getter +@RequiredArgsConstructor +public enum IotDeviceMessageIdentifierEnum { + + PROPERTY_GET("get"), // 下行 TODO 芋艿:【讨论】貌似这个“上行”更合理?device 主动拉取配置。和 IotDevicePropertyGetReqDTO 一样的配置 + PROPERTY_SET("set"), // 下行 + PROPERTY_REPORT("report"), // 上行 + + STATE_ONLINE("online"), // 上行 + STATE_OFFLINE("offline"), // 上行 + + CONFIG_GET("get"), // 上行 TODO 芋艿:【讨论】暂时没有上行的场景 + CONFIG_SET("set"), // 下行 + + SERVICE_INVOKE("${identifier}"), // 下行 + SERVICE_REPLY_SUFFIX("_reply"), // 芋艿:TODO 芋艿:【讨论】上行 or 下行 + + OTA_UPGRADE("upgrade"), // 下行 + OTA_PULL("pull"), // 上行 + OTA_PROGRESS("progress"), // 上行 + OTA_REPORT("report"), // 上行 + + REGISTER_REGISTER("register"), // 上行 + REGISTER_REGISTER_SUB("register_sub"), // 上行 + REGISTER_UNREGISTER_SUB("unregister_sub"), // 下行 + + TOPOLOGY_ADD("topology_add"), // 下行; + ; + + /** + * 标志符 + */ + private final String identifier; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageTypeEnum.java new file mode 100644 index 000000000..0354157ed --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageTypeEnum.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.enums.device; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 设备消息类型枚举 + */ +@Getter +@RequiredArgsConstructor +public enum IotDeviceMessageTypeEnum implements ArrayValuable { + + STATE("state"), // 设备状态 + PROPERTY("property"), // 设备属性:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务 + EVENT("event"), // 设备事件:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务 + SERVICE("service"), // 设备服务:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务 + CONFIG("config"), // 设备配置:可参考 https://help.aliyun.com/zh/iot/user-guide/remote-configuration-1 远程配置 + OTA("ota"), // 设备 OTA:可参考 https://help.aliyun.com/zh/iot/user-guide/ota-update OTA 升级 + REGISTER("register"), // 设备注册:可参考 https://help.aliyun.com/zh/iot/user-guide/register-devices 设备身份注册 + TOPOLOGY("topology"),; // 设备拓扑:可参考 https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships 设备拓扑 + + public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageTypeEnum::getType).toArray(String[]::new); + + /** + * 属性 + */ + private final String type; + + @Override + public String[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceStateEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceStateEnum.java new file mode 100644 index 000000000..6ce2677db --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceStateEnum.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.iot.enums.device; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 设备状态枚举 + * + * @author haohao + */ +@RequiredArgsConstructor +@Getter +public enum IotDeviceStateEnum implements ArrayValuable { + + INACTIVE(0, "未激活"), + ONLINE(1, "在线"), + OFFLINE(2, "离线"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotDeviceStateEnum::getState).toArray(Integer[]::new); + + /** + * 状态 + */ + private final Integer state; + /** + * 状态名 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static boolean isOnline(Integer state) { + return ONLINE.getState().equals(state); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceStatusEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceStatusEnum.java deleted file mode 100644 index 0ec2ac776..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceStatusEnum.java +++ /dev/null @@ -1,55 +0,0 @@ -package cn.iocoder.yudao.module.iot.enums.device; - -import cn.iocoder.yudao.framework.common.core.ArrayValuable; -import lombok.Getter; - -import java.util.Arrays; - -/** - * IoT 设备状态枚举 - * - * @author haohao - */ -@Getter -public enum IotDeviceStatusEnum implements ArrayValuable { - - INACTIVE(0, "未激活"), - ONLINE(1, "在线"), - OFFLINE(2, "离线"), - DISABLED(3, "已禁用"); - - public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotDeviceStatusEnum::getStatus).toArray(Integer[]::new); - - /** - * 状态 - */ - private final Integer status; - /** - * 状态名 - */ - private final String name; - - IotDeviceStatusEnum(Integer status, String name) { - this.status = status; - this.name = name; - } - - public static IotDeviceStatusEnum fromStatus(Integer status) { - for (IotDeviceStatusEnum value : values()) { - if (value.getStatus().equals(status)) { - return value; - } - } - return null; - } - - public static boolean isValidStatus(Integer status) { - return fromStatus(status) != null; - } - - @Override - public Integer[] array() { - return ARRAYS; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeRecordStatusEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeRecordStatusEnum.java new file mode 100644 index 000000000..e809a7e5b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeRecordStatusEnum.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.iot.enums.ota; + + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT OTA 升级记录的范围枚举 + * + * @author haohao + */ +@RequiredArgsConstructor +@Getter +public enum IotOtaUpgradeRecordStatusEnum implements ArrayValuable { + + PENDING(0), // 待推送 + PUSHED(10), // 已推送 + UPGRADING(20), // 升级中 + SUCCESS(30), // 升级成功 + FAILURE(40), // 升级失败 + CANCELED(50),; // 已取消 + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotOtaUpgradeRecordStatusEnum::getStatus).toArray(Integer[]::new); + + /** + * 范围 + */ + private final Integer status; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeTaskScopeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeTaskScopeEnum.java new file mode 100644 index 000000000..6dccbb041 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeTaskScopeEnum.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.iot.enums.ota; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT OTA 升级任务的范围枚举 + * + * @author haohao + */ +@RequiredArgsConstructor +@Getter +public enum IotOtaUpgradeTaskScopeEnum implements ArrayValuable { + + ALL(1), // 全部设备:只包括当前产品下的设备,不包括未来创建的设备 + SELECT(2); // 指定设备 + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotOtaUpgradeTaskScopeEnum::getScope).toArray(Integer[]::new); + + /** + * 范围 + */ + private final Integer scope; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeTaskStatusEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeTaskStatusEnum.java new file mode 100644 index 000000000..78af16cb2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeTaskStatusEnum.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.enums.ota; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT OTA 升级任务的范围枚举 + * + * @author haohao + */ +@RequiredArgsConstructor +@Getter +public enum IotOtaUpgradeTaskStatusEnum implements ArrayValuable { + + IN_PROGRESS(10), // 进行中:升级中 + COMPLETED(20), // 已完成:已结束,全部升级完成 + INCOMPLETE(21), // 未完成:已结束,部分升级完成 + CANCELED(30),; // 已取消:一般是主动取消任务 + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotOtaUpgradeTaskStatusEnum::getStatus).toArray(Integer[]::new); + + /** + * 范围 + */ + private final Integer status; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginDeployTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginDeployTypeEnum.java new file mode 100644 index 000000000..b6ef4f0cc --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginDeployTypeEnum.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.enums.plugin; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 部署方式枚举 + * + * @author haohao + */ +@RequiredArgsConstructor +@Getter +public enum IotPluginDeployTypeEnum implements ArrayValuable { + + JAR(0, "JAR 部署"), + STANDALONE(1, "独立部署"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotPluginDeployTypeEnum::getDeployType).toArray(Integer[]::new); + + /** + * 部署方式 + */ + private final Integer deployType; + /** + * 部署方式名 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginStatusEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginStatusEnum.java new file mode 100644 index 000000000..7e3fa657e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginStatusEnum.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.enums.plugin; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 插件状态枚举 + * + * @author haohao + */ +@RequiredArgsConstructor +@Getter +public enum IotPluginStatusEnum implements ArrayValuable { + + STOPPED(0, "停止"), + RUNNING(1, "运行"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotPluginStatusEnum::getStatus).toArray(Integer[]::new); + + /** + * 状态 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginTypeEnum.java new file mode 100644 index 000000000..ec0b72f9f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginTypeEnum.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.enums.plugin; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * IoT 插件类型枚举 + * + * @author haohao + */ +@AllArgsConstructor +@Getter +public enum IotPluginTypeEnum implements ArrayValuable { + + NORMAL(0, "普通插件"), + DEVICE(1, "设备插件"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotPluginTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 类型名 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotAccessModeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotAccessModeEnum.java deleted file mode 100644 index 64ece99ca..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotAccessModeEnum.java +++ /dev/null @@ -1,21 +0,0 @@ -package cn.iocoder.yudao.module.iot.enums.product; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * IOT 访问方式枚举类 - * - * @author ahh - */ -@AllArgsConstructor -@Getter -public enum IotAccessModeEnum { - - READ("r"), - WRITE("w"), - READ_WRITE("rw"); - - private final String mode; - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotNetTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotNetTypeEnum.java index 561bc66f9..2a54e489f 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotNetTypeEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotNetTypeEnum.java @@ -7,7 +7,7 @@ import lombok.Getter; import java.util.Arrays; /** - * IOT 联网方式枚举类 + * IoT 联网方式枚举类 * * @author ahh */ diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductDeviceTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductDeviceTypeEnum.java index 3d8ca10b3..7910f1b2d 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductDeviceTypeEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductDeviceTypeEnum.java @@ -7,7 +7,7 @@ import lombok.Getter; import java.util.Arrays; /** - * IOT 产品的设备类型 + * IoT 产品的设备类型 * * @author ahh */ @@ -16,7 +16,7 @@ import java.util.Arrays; public enum IotProductDeviceTypeEnum implements ArrayValuable { DIRECT(0, "直连设备"), - GATEWAY_CHILD(1, "网关子设备"), + GATEWAY_SUB(1, "网关子设备"), GATEWAY(2, "网关设备"); /** @@ -36,4 +36,24 @@ public enum IotProductDeviceTypeEnum implements ArrayValuable { return ARRAYS; } + /** + * 判断是否是网关 + * + * @param type 类型 + * @return 是否是网关 + */ + public static boolean isGateway(Integer type) { + return GATEWAY.getType().equals(type); + } + + /** + * 判断是否是网关子设备 + * + * @param type 类型 + * @return 是否是网关子设备 + */ + public static boolean isGatewaySub(Integer type) { + return GATEWAY_SUB.getType().equals(type); + } + } diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductStatusEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductStatusEnum.java index 32daf1671..b9bbbeec7 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductStatusEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductStatusEnum.java @@ -7,7 +7,7 @@ import lombok.Getter; import java.util.Arrays; /** - * IOT 产品的状态枚举类 + * IoT 产品的状态枚举类 * * @author ahh */ @@ -18,12 +18,12 @@ public enum IotProductStatusEnum implements ArrayValuable { UNPUBLISHED(0, "开发中"), PUBLISHED(1, "已发布"); - public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotProductStatusEnum::getType).toArray(Integer[]::new); + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotProductStatusEnum::getStatus).toArray(Integer[]::new); /** * 类型 */ - private final Integer type; + private final Integer status; /** * 描述 */ diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProtocolTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProtocolTypeEnum.java index 9eb57044f..d24dea92e 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProtocolTypeEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProtocolTypeEnum.java @@ -7,7 +7,7 @@ import lombok.Getter; import java.util.Arrays; /** - * IOT 接入网关协议枚举类 + * IoT 接入网关协议枚举类 * * @author ahh */ diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotValidateTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotValidateTypeEnum.java index 11604b4dd..2a15d16a4 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotValidateTypeEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotValidateTypeEnum.java @@ -7,7 +7,7 @@ import lombok.Getter; import java.util.Arrays; /** - * IOT 数据校验级别枚举类 + * IoT 数据校验级别枚举类 * * @author ahh */ diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotAlertConfigReceiveTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotAlertConfigReceiveTypeEnum.java new file mode 100644 index 000000000..3fdd53234 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotAlertConfigReceiveTypeEnum.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.enums.rule; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 告警配置的接收方式枚举 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum IotAlertConfigReceiveTypeEnum implements ArrayValuable { + + SMS(1), // 短信 + MAIL(2), // 邮箱 + NOTIFY(3); // 通知 + + private final Integer type; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotAlertConfigReceiveTypeEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataBridgeDirectionEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataBridgeDirectionEnum.java new file mode 100644 index 000000000..a9d445fd2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataBridgeDirectionEnum.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.enums.rule; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 数据桥接的方向枚举 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum IotDataBridgeDirectionEnum implements ArrayValuable { + + INPUT(1), // 输入 + OUTPUT(2); // 输出 + + private final Integer type; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotDataBridgeDirectionEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataBridgeTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataBridgeTypeEnum.java new file mode 100644 index 000000000..78fc8452e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataBridgeTypeEnum.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.iot.enums.rule; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 数据桥接的类型枚举 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum IotDataBridgeTypeEnum implements ArrayValuable { + + HTTP(1, "HTTP"), + TCP(2, "TCP"), + WEBSOCKET(3, "WEBSOCKET"), + + MQTT(10, "MQTT"), + + DATABASE(20, "DATABASE"), + REDIS_STREAM(21, "REDIS_STREAM"), + + ROCKETMQ(30, "ROCKETMQ"), + RABBITMQ(31, "RABBITMQ"), + KAFKA(32, "KAFKA"); + + private final Integer type; + + private final String name; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotDataBridgeTypeEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneActionTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneActionTypeEnum.java new file mode 100644 index 000000000..2bdf7d0ed --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneActionTypeEnum.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.enums.rule; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 规则场景的触发类型枚举 + * + * 设备触发,定时触发 + */ +@RequiredArgsConstructor +@Getter +public enum IotRuleSceneActionTypeEnum implements ArrayValuable { + + DEVICE_CONTROL(1), // 设备执行 + ALERT(2), // 告警执行 + DATA_BRIDGE(3); // 桥接执行 + + private final Integer type; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotRuleSceneActionTypeEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerConditionParameterOperatorEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerConditionParameterOperatorEnum.java new file mode 100644 index 000000000..5ed90ccae --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerConditionParameterOperatorEnum.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.iot.enums.rule; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 场景触发条件参数的操作符枚举 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum IotRuleSceneTriggerConditionParameterOperatorEnum implements ArrayValuable { + + EQUALS("=", "#source == #value"), + NOT_EQUALS("!=", "!(#source == #value)"), + + GREATER_THAN(">", "#source > #value"), + GREATER_THAN_OR_EQUALS(">=", "#source >= #value"), + + LESS_THAN("<", "#source < #value"), + LESS_THAN_OR_EQUALS("<=", "#source <= #value"), + + IN("in", "#values.contains(#source)"), + NOT_IN("not in", "!(#values.contains(#source))"), + + BETWEEN("between", "(#source >= #values.get(0)) && (#source <= #values.get(1))"), + NOT_BETWEEN("not between", "(#source < #values.get(0)) || (#source > #values.get(1))"), + + LIKE("like", "#source.contains(#value)"), // 字符串匹配 + NOT_NULL("not null", "#source != null && #source.length() > 0"); // 非空 + + private final String operator; + private final String springExpression; + + public static final String[] ARRAYS = Arrays.stream(values()).map(IotRuleSceneTriggerConditionParameterOperatorEnum::getOperator).toArray(String[]::new); + + /** + * Spring 表达式 - 原始值 + */ + public static final String SPRING_EXPRESSION_SOURCE = "source"; + /** + * Spring 表达式 - 目标值 + */ + public static final String SPRING_EXPRESSION_VALUE = "value"; + /** + * Spring 表达式 - 目标值数组 + */ + public static final String SPRING_EXPRESSION_VALUE_List = "values"; + + public static IotRuleSceneTriggerConditionParameterOperatorEnum operatorOf(String operator) { + return ArrayUtil.firstMatch(item -> item.getOperator().equals(operator), values()); + } + + @Override + public String[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerTypeEnum.java new file mode 100644 index 000000000..a420a21d5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerTypeEnum.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.enums.rule; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 场景流转的触发类型枚举 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum IotRuleSceneTriggerTypeEnum implements ArrayValuable { + + DEVICE(1), // 设备触发 + TIMER(2); // 定时触发 + + private final Integer type; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotRuleSceneTriggerTypeEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotDataSpecsDataTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotDataSpecsDataTypeEnum.java new file mode 100644 index 000000000..5524fdeb4 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotDataSpecsDataTypeEnum.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.enums.thingmodel; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * IoT 数据定义的数据类型枚举类 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum IotDataSpecsDataTypeEnum implements ArrayValuable { + + INT("int"), + FLOAT("float"), + DOUBLE("double"), + ENUM("enum"), + BOOL("bool"), + TEXT("text"), + DATE("date"), + STRUCT("struct"), + ARRAY("array"); + + public static final String[] ARRAYS = Arrays.stream(values()).map(IotDataSpecsDataTypeEnum::getDataType).toArray(String[]::new); + + private final String dataType; + + @Override + public String[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelAccessModeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelAccessModeEnum.java new file mode 100644 index 000000000..c0a2b329b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelAccessModeEnum.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.enums.thingmodel; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * IoT 产品物模型属性读取类型枚举 + * + * @author ahh + */ +@AllArgsConstructor +@Getter +public enum IotThingModelAccessModeEnum implements ArrayValuable { + + READ_ONLY("r"), + READ_WRITE("rw"); + + public static final String[] ARRAYS = Arrays.stream(values()).map(IotThingModelAccessModeEnum::getMode).toArray(String[]::new); + + private final String mode; + + @Override + public String[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelParamDirectionEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelParamDirectionEnum.java new file mode 100644 index 000000000..4f06cefce --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelParamDirectionEnum.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.enums.thingmodel; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + + +/** + * IoT 产品物模型参数是输入参数还是输出参数枚举 + * + * @author HUIHUI + */ +@AllArgsConstructor +@Getter +public enum IotThingModelParamDirectionEnum implements ArrayValuable { + + INPUT("input"), // 输入参数 + OUTPUT("output"); // 输出参数 + + public static final String[] ARRAYS = Arrays.stream(values()).map(IotThingModelParamDirectionEnum::getDirection).toArray(String[]::new); + + private final String direction; + + @Override + public String[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelServiceCallTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelServiceCallTypeEnum.java new file mode 100644 index 000000000..376db6b4a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelServiceCallTypeEnum.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.enums.thingmodel; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * IoT 产品物模型服务调用方式枚举 + * + * @author HUIHUI + */ +@AllArgsConstructor +@Getter +public enum IotThingModelServiceCallTypeEnum implements ArrayValuable { + + ASYNC("async"), // 异步调用 + SYNC("sync"); // 同步调用 + + public static final String[] ARRAYS = Arrays.stream(values()).map(IotThingModelServiceCallTypeEnum::getType).toArray(String[]::new); + + private final String type; + + @Override + public String[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelServiceEventTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelServiceEventTypeEnum.java new file mode 100644 index 000000000..c7c74092a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelServiceEventTypeEnum.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.enums.thingmodel; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * IoT 产品物模型事件类型枚举 + * + * @author HUIHUI + */ +@AllArgsConstructor +@Getter +public enum IotThingModelServiceEventTypeEnum implements ArrayValuable { + + INFO("info"), // 信息 + ALERT("alert"), // 告警 + ERROR("error"); // 故障 + + public static final String[] ARRAYS = Arrays.stream(values()).map(IotThingModelServiceEventTypeEnum::getType).toArray(String[]::new); + + private final String type; + + @Override + public String[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductFunctionTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelTypeEnum.java similarity index 69% rename from yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductFunctionTypeEnum.java rename to yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelTypeEnum.java index 1e95c6b78..e0097cfe9 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductFunctionTypeEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelTypeEnum.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.enums.product; +package cn.iocoder.yudao.module.iot.enums.thingmodel; import cn.iocoder.yudao.framework.common.core.ArrayValuable; import lombok.AllArgsConstructor; @@ -7,19 +7,19 @@ import lombok.Getter; import java.util.Arrays; /** - * IOT 产品功能(物模型)类型枚举类 + * IoT 产品功能(物模型)类型枚举类 * * @author ahh */ @AllArgsConstructor @Getter -public enum IotProductFunctionTypeEnum implements ArrayValuable { +public enum IotThingModelTypeEnum implements ArrayValuable { PROPERTY(1, "属性"), SERVICE(2, "服务"), EVENT(3, "事件"); - public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotProductFunctionTypeEnum::getType).toArray(Integer[]::new); + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotThingModelTypeEnum::getType).toArray(Integer[]::new); /** * 类型 diff --git a/yudao-module-iot/yudao-module-iot-biz/pom.xml b/yudao-module-iot/yudao-module-iot-biz/pom.xml index e3f93086a..8721e4de9 100644 --- a/yudao-module-iot/yudao-module-iot-biz/pom.xml +++ b/yudao-module-iot/yudao-module-iot-biz/pom.xml @@ -25,6 +25,11 @@ ${revision} + + cn.iocoder.boot + yudao-spring-boot-starter-biz-tenant + + cn.iocoder.boot @@ -37,11 +42,21 @@ + + com.taosdata.jdbc + taos-jdbcdriver + + cn.iocoder.boot yudao-spring-boot-starter-mybatis + + cn.iocoder.boot + yudao-spring-boot-starter-redis + + cn.iocoder.boot @@ -54,11 +69,64 @@ yudao-spring-boot-starter-excel - + - org.eclipse.paho - org.eclipse.paho.client.mqttv3 + org.apache.rocketmq + rocketmq-spring-boot-starter + true + + org.springframework.kafka + spring-kafka + true + + + org.springframework.boot + spring-boot-starter-amqp + true + + + + org.pf4j + pf4j-spring + + + + + org.apache.groovy + groovy-all + 4.0.25 + pom + + + + + org.graalvm.js + js + 24.1.2 + pom + + + org.graalvm.js + js-scriptengine + 24.1.2 + + + + + + + + + + + + + + + + + diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/ScriptTest.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/ScriptTest.java new file mode 100644 index 000000000..9f54d60e8 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/ScriptTest.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.module.iot; + +import cn.hutool.script.ScriptUtil; +import javax.script.Bindings; +import javax.script.ScriptEngine; +import javax.script.ScriptException; + +/** + * TODO 芋艿:测试脚本的接入 + */ +public class ScriptTest { + + public static void main2(String[] args) { + // 创建一个 Groovy 脚本引擎 + ScriptEngine engine = ScriptUtil.createGroovyEngine(); + + // 创建绑定参数 + Bindings bindings = engine.createBindings(); + bindings.put("name", "Alice"); + bindings.put("age", 30); + + // 定义一个稍微复杂的 Groovy 脚本 + String script = "def greeting = 'Hello, ' + name + '!';\n" + + "def ageInFiveYears = age + 5;\n" + + "def message = greeting + ' In five years, you will be ' + ageInFiveYears + ' years old.';\n" + + "return message.toUpperCase();\n"; + + try { + // 执行脚本并获取结果 + Object result = engine.eval(script, bindings); + System.out.println(result); // 输出: HELLO, ALICE! IN FIVE YEARS, YOU WILL BE 35 YEARS OLD. + } catch (ScriptException e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) { + // 创建一个 JavaScript 脚本引擎 + ScriptEngine jsEngine = ScriptUtil.createJsEngine(); + + // 创建绑定参数 + Bindings jsBindings = jsEngine.createBindings(); + jsBindings.put("name", "Bob"); + jsBindings.put("age", 25); + + // 定义一个简单的 JavaScript 脚本 + String jsScript = "var greeting = 'Hello, ' + name + '!';\n" + + "var ageInTenYears = age + 10;\n" + + "var message = greeting + ' In ten years, you will be ' + ageInTenYears + ' years old.';\n" + + "message.toUpperCase();\n"; + + try { + // 执行脚本并获取结果 + Object jsResult = jsEngine.eval(jsScript, jsBindings); + System.out.println(jsResult); // 输出: HELLO, BOB! IN TEN YEARS, YOU WILL BE 35 YEARS OLD. + } catch (ScriptException e) { + e.printStackTrace(); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceUpstreamApiImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceUpstreamApiImpl.java new file mode 100644 index 000000000..25faa1a6b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceUpstreamApiImpl.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.iot.api.device; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; +import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceUpstreamService; +import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +/** + * * 设备数据 Upstream 上行 API 实现类 + */ +@RestController +@Validated +public class IoTDeviceUpstreamApiImpl implements IotDeviceUpstreamApi { + + @Resource + private IotDeviceUpstreamService deviceUpstreamService; + @Resource + private IotPluginInstanceService pluginInstanceService; + + // ========== 设备相关 ========== + + @Override + public CommonResult updateDeviceState(IotDeviceStateUpdateReqDTO updateReqDTO) { + deviceUpstreamService.updateDeviceState(updateReqDTO); + return success(true); + } + + @Override + public CommonResult reportDeviceProperty(IotDevicePropertyReportReqDTO reportReqDTO) { + deviceUpstreamService.reportDeviceProperty(reportReqDTO); + return success(true); + } + + @Override + public CommonResult reportDeviceEvent(IotDeviceEventReportReqDTO reportReqDTO) { + deviceUpstreamService.reportDeviceEvent(reportReqDTO); + return success(true); + } + + @Override + public CommonResult registerDevice(IotDeviceRegisterReqDTO registerReqDTO) { + deviceUpstreamService.registerDevice(registerReqDTO); + return success(true); + } + + @Override + public CommonResult registerSubDevice(IotDeviceRegisterSubReqDTO registerReqDTO) { + deviceUpstreamService.registerSubDevice(registerReqDTO); + return success(true); + } + + @Override + public CommonResult addDeviceTopology(IotDeviceTopologyAddReqDTO addReqDTO) { + deviceUpstreamService.addDeviceTopology(addReqDTO); + return success(true); + } + + @Override + public CommonResult authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) { + boolean result = deviceUpstreamService.authenticateEmqxConnection(authReqDTO); + return success(result); + } + + // ========== 插件相关 ========== + + @Override + public CommonResult heartbeatPluginInstance(IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO) { + pluginInstanceService.heartbeatPluginInstance(heartbeatReqDTO); + return success(true); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java new file mode 100644 index 000000000..07852180d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java @@ -0,0 +1,6 @@ +/** + * 占位 + * + * TODO 芋艿:后续删除 + */ +package cn.iocoder.yudao.module.iot.api; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.http b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.http new file mode 100644 index 000000000..c1190cec1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.http @@ -0,0 +1,75 @@ +### 请求 /iot/device/downstream 接口(服务调用) => 成功 +POST {{baseUrl}}/iot/device/downstream +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +{ + "id": 25, + "type": "service", + "identifier": "temperature", + "data": { + "xx": "yy" + } +} + +### 请求 /iot/device/downstream 接口(属性设置) => 成功 +POST {{baseUrl}}/iot/device/downstream +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +{ + "id": 25, + "type": "property", + "identifier": "set", + "data": { + "xx": "yy" + } +} + +### 请求 /iot/device/downstream 接口(属性获取) => 成功 +POST {{baseUrl}}/iot/device/downstream +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +{ + "id": 25, + "type": "property", + "identifier": "get", + "data": ["xx", "yy"] +} + +### 请求 /iot/device/downstream 接口(配置设置) => 成功 +POST {{baseUrl}}/iot/device/downstream +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +{ + "id": 25, + "type": "config", + "identifier": "set" +} + +### 请求 /iot/device/downstream 接口(OTA 升级) => 成功 +POST {{baseUrl}}/iot/device/downstream +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +{ + "id": 25, + "type": "ota", + "identifier": "upgrade", + "data": { + "firmwareId": 1, + "version": "1.0.0", + "signMethod": "MD5", + "fileSign": "d41d8cd98f00b204e9800998ecf8427e", + "fileSize": 1024, + "fileUrl": "http://example.com/firmware.bin", + "information": "{\"desc\":\"升级到最新版本\"}" + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java index 6d75f1cdd..08fc244b1 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java @@ -1,24 +1,37 @@ package cn.iocoder.yudao.module.iot.controller.admin.device; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; 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.device.vo.IotDevicePageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDeviceRespVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDeviceSaveReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDeviceStatusUpdateReqVO; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceDownstreamReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceUpstreamReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceDownstreamService; +import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceUpstreamService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; @Tag(name = "管理后台 - IoT 设备") @RestController @@ -28,6 +41,10 @@ public class IotDeviceController { @Resource private IotDeviceService deviceService; + @Resource + private IotDeviceUpstreamService deviceUpstreamService; + @Resource + private IotDeviceDownstreamService deviceDownstreamService; @PostMapping("/create") @Operation(summary = "创建设备") @@ -36,14 +53,6 @@ public class IotDeviceController { return success(deviceService.createDevice(createReqVO)); } - @PutMapping("/update-status") - @Operation(summary = "更新设备状态") - @PreAuthorize("@ss.hasPermission('iot:device:update')") - public CommonResult updateDeviceStatus(@Valid @RequestBody IotDeviceStatusUpdateReqVO updateReqVO) { - deviceService.updateDeviceStatus(updateReqVO); - return success(true); - } - @PutMapping("/update") @Operation(summary = "更新设备") @PreAuthorize("@ss.hasPermission('iot:device:update')") @@ -52,8 +61,18 @@ public class IotDeviceController { return success(true); } + // TODO @芋艿:参考阿里云:1)绑定网关;2)解绑网关 + + @PutMapping("/update-group") + @Operation(summary = "更新设备分组") + @PreAuthorize("@ss.hasPermission('iot:device:update')") + public CommonResult updateDeviceGroup(@Valid @RequestBody IotDeviceUpdateGroupReqVO updateReqVO) { + deviceService.updateDeviceGroup(updateReqVO); + return success(true); + } + @DeleteMapping("/delete") - @Operation(summary = "删除设备") + @Operation(summary = "删除单个设备") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('iot:device:delete')") public CommonResult deleteDevice(@RequestParam("id") Long id) { @@ -61,6 +80,15 @@ public class IotDeviceController { return success(true); } + @DeleteMapping("/delete-list") + @Operation(summary = "删除多个设备") + @Parameter(name = "ids", description = "编号数组", required = true) + @PreAuthorize("@ss.hasPermission('iot:device:delete')") + public CommonResult deleteDeviceList(@RequestParam("ids") Collection ids) { + deviceService.deleteDeviceList(ids); + return success(true); + } + @GetMapping("/get") @Operation(summary = "获得设备") @Parameter(name = "id", description = "编号", required = true, example = "1024") @@ -78,6 +106,19 @@ public class IotDeviceController { return success(BeanUtils.toBean(pageResult, IotDeviceRespVO.class)); } + @GetMapping("/export-excel") + @Operation(summary = "导出设备 Excel") + @PreAuthorize("@ss.hasPermission('iot:device:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportDeviceExcel(@Valid IotDevicePageReqVO exportReqVO, + HttpServletResponse response) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + CommonResult> result = getDevicePage(exportReqVO); + // 导出 Excel + ExcelUtils.write(response, "设备.xls", "数据", IotDeviceRespVO.class, + result.getData().getList()); + } + @GetMapping("/count") @Operation(summary = "获得设备数量") @Parameter(name = "productId", description = "产品编号", example = "1") @@ -86,4 +127,62 @@ public class IotDeviceController { return success(deviceService.getDeviceCountByProductId(productId)); } + @GetMapping("/simple-list") + @Operation(summary = "获取设备的精简信息列表", description = "主要用于前端的下拉选项") + @Parameter(name = "deviceType", description = "设备类型", example = "1") + public CommonResult> getSimpleDeviceList( + @RequestParam(value = "deviceType", required = false) Integer deviceType) { + List list = deviceService.getDeviceListByDeviceType(deviceType); + return success(convertList(list, device -> // 只返回 id、name 字段 + new IotDeviceRespVO().setId(device.getId()).setDeviceName(device.getDeviceName()))); + } + + @PostMapping("/import") + @Operation(summary = "导入设备") + @PreAuthorize("@ss.hasPermission('iot:device:import')") + public CommonResult importDevice( + @RequestParam("file") MultipartFile file, + @RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport) + throws Exception { + List list = ExcelUtils.read(file, IotDeviceImportExcelVO.class); + return success(deviceService.importDevice(list, updateSupport)); + } + + @GetMapping("/get-import-template") + @Operation(summary = "获得导入设备模板") + public void importTemplate(HttpServletResponse response) throws IOException { + // 手动创建导出 demo + List list = Arrays.asList( + IotDeviceImportExcelVO.builder().deviceName("温度传感器001").parentDeviceName("gateway110") + .productKey("1de24640dfe").groupNames("灰度分组,生产分组").build(), + IotDeviceImportExcelVO.builder().deviceName("biubiu") + .productKey("YzvHxd4r67sT4s2B").groupNames("").build()); + // 输出 + ExcelUtils.write(response, "设备导入模板.xls", "数据", IotDeviceImportExcelVO.class, list); + } + + @PostMapping("/upstream") + @Operation(summary = "设备上行", description = "可用于设备模拟") + @PreAuthorize("@ss.hasPermission('iot:device:upstream')") + public CommonResult upstreamDevice(@Valid @RequestBody IotDeviceUpstreamReqVO upstreamReqVO) { + deviceUpstreamService.upstreamDevice(upstreamReqVO); + return success(true); + } + + @PostMapping("/downstream") + @Operation(summary = "设备下行", description = "可用于设备模拟") + @PreAuthorize("@ss.hasPermission('iot:device:downstream')") + public CommonResult downstreamDevice(@Valid @RequestBody IotDeviceDownstreamReqVO downstreamReqVO) { + deviceDownstreamService.downstreamDevice(downstreamReqVO); + return success(true); + } + + // TODO @haohao:是不是默认详情接口,不返回 secret,然后这个接口,用于统一返回。然后接口名可以更通用一点。 + @GetMapping("/mqtt-connection-params") + @Operation(summary = "获取 MQTT 连接参数") + @PreAuthorize("@ss.hasPermission('iot:device:mqtt-connection-params')") + public CommonResult getMqttConnectionParams(@RequestParam("deviceId") Long deviceId) { + return success(deviceService.getMqttConnectionParams(deviceId)); + } + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceGroupController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceGroupController.java new file mode 100644 index 000000000..d19cf7fc9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceGroupController.java @@ -0,0 +1,88 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +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.device.vo.group.IotDeviceGroupPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.group.IotDeviceGroupRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.group.IotDeviceGroupSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceGroupService; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - IoT 设备分组") +@RestController +@RequestMapping("/iot/device-group") +@Validated +public class IotDeviceGroupController { + + @Resource + private IotDeviceGroupService deviceGroupService; + @Resource + private IotDeviceService deviceService; + + @PostMapping("/create") + @Operation(summary = "创建设备分组") + @PreAuthorize("@ss.hasPermission('iot:device-group:create')") + public CommonResult createDeviceGroup(@Valid @RequestBody IotDeviceGroupSaveReqVO createReqVO) { + return success(deviceGroupService.createDeviceGroup(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新设备分组") + @PreAuthorize("@ss.hasPermission('iot:device-group:update')") + public CommonResult updateDeviceGroup(@Valid @RequestBody IotDeviceGroupSaveReqVO updateReqVO) { + deviceGroupService.updateDeviceGroup(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除设备分组") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:device-group:delete')") + public CommonResult deleteDeviceGroup(@RequestParam("id") Long id) { + deviceGroupService.deleteDeviceGroup(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得设备分组") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:device-group:query')") + public CommonResult getDeviceGroup(@RequestParam("id") Long id) { + IotDeviceGroupDO deviceGroup = deviceGroupService.getDeviceGroup(id); + return success(BeanUtils.toBean(deviceGroup, IotDeviceGroupRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得设备分组分页") + @PreAuthorize("@ss.hasPermission('iot:device-group:query')") + public CommonResult> getDeviceGroupPage(@Valid IotDeviceGroupPageReqVO pageReqVO) { + PageResult pageResult = deviceGroupService.getDeviceGroupPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotDeviceGroupRespVO.class, + group -> group.setDeviceCount(deviceService.getDeviceCountByGroupId(group.getId())))); + } + + @GetMapping("/simple-list") + @Operation(summary = "获取设备分组的精简信息列表", description = "只包含被开启的分组,主要用于前端的下拉选项") + public CommonResult> getSimpleDeviceGroupList() { + List list = deviceGroupService.getDeviceGroupListByStatus(CommonStatusEnum.ENABLE.getStatus()); + return success(convertList(list, group -> // 只返回 id、name 字段 + new IotDeviceGroupRespVO().setId(group.getId()).setName(group.getName()))); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceLogController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceLogController.java new file mode 100644 index 000000000..81d1bff94 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceLogController.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +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.device.vo.data.IotDeviceLogPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDeviceLogRespVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO; +import cn.iocoder.yudao.module.iot.service.device.data.IotDeviceLogService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT 设备日志") +@RestController +@RequestMapping("/iot/device/log") +@Validated +public class IotDeviceLogController { + + @Resource + private IotDeviceLogService deviceLogService; + + @GetMapping("/page") + @Operation(summary = "获得设备日志分页") + @PreAuthorize("@ss.hasPermission('iot:device:log-query')") + public CommonResult> getDeviceLogPage(@Valid IotDeviceLogPageReqVO pageReqVO) { + PageResult pageResult = deviceLogService.getDeviceLogPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotDeviceLogRespVO.class)); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDevicePropertyController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDevicePropertyController.java new file mode 100644 index 000000000..47bf325dd --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDevicePropertyController.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyHistoryPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyRespVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.data.IotDevicePropertyService; +import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - IoT 设备属性") +@RestController +@RequestMapping("/iot/device/property") +@Validated +public class IotDevicePropertyController { + + @Resource + private IotDevicePropertyService devicePropertyService; + @Resource + private IotThingModelService thingModelService; + @Resource + private IotDeviceService deviceService; + + @GetMapping("/latest") + @Operation(summary = "获取设备属性最新属性") + @Parameters({ + @Parameter(name = "deviceId", description = "设备编号", required = true), + @Parameter(name = "identifier", description = "标识符"), + @Parameter(name = "name", description = "名称") + }) + @PreAuthorize("@ss.hasPermission('iot:device:property-query')") + public CommonResult> getLatestDeviceProperties( + @RequestParam("deviceId") Long deviceId, + @RequestParam(value = "identifier", required = false) String identifier, + @RequestParam(value = "name", required = false) String name) { + Map properties = devicePropertyService.getLatestDeviceProperties(deviceId); + + // 拼接数据 + IotDeviceDO device = deviceService.getDevice(deviceId); + Assert.notNull(device, "设备不存在"); + List thingModels = thingModelService.getThingModelListByProductId(device.getProductId()); + return success(convertList(properties.entrySet(), entry -> { + IotThingModelDO thingModel = CollUtil.findOne(thingModels, + item -> item.getIdentifier().equals(entry.getKey())); + if (thingModel == null || thingModel.getProperty() == null) { + return null; + } + if (StrUtil.isNotEmpty(identifier) && !StrUtil.contains(thingModel.getIdentifier(), identifier)) { + return null; + } + if (StrUtil.isNotEmpty(name) && !StrUtil.contains(thingModel.getName(), name)) { + return null; + } + // 构建对象 + IotDevicePropertyDO property = entry.getValue(); + return new IotDevicePropertyRespVO().setProperty(thingModel.getProperty()) + .setValue(property.getValue()).setUpdateTime(LocalDateTimeUtil.toEpochMilli(property.getUpdateTime())); + })); + } + + @GetMapping("/history-page") + @Operation(summary = "获取设备属性历史数据") + @PreAuthorize("@ss.hasPermission('iot:device:property-query')") + public CommonResult> getHistoryDevicePropertyPage( + @Valid IotDevicePropertyHistoryPageReqVO pageReqVO) { + Assert.notEmpty(pageReqVO.getIdentifier(), "标识符不能为空"); + return success(devicePropertyService.getHistoryDevicePropertyPage(pageReqVO)); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDevicePageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDevicePageReqVO.java deleted file mode 100644 index 26bdaca05..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDevicePageReqVO.java +++ /dev/null @@ -1,87 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.device.vo; - -import cn.iocoder.yudao.framework.common.pojo.PageParam; -import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStatusEnum; -import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import org.springframework.format.annotation.DateTimeFormat; - -import java.math.BigDecimal; -import java.time.LocalDateTime; - -import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; - -@Schema(description = "管理后台 - IoT 设备分页 Request VO") -@Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) -public class IotDevicePageReqVO extends PageParam { - - // TODO @芋艿:需要去掉一些多余的字段; - - @Schema(description = "设备唯一标识符", example = "24602") - private String deviceKey; - - @Schema(description = "设备名称", example = "王五") - private String deviceName; - - @Schema(description = "备注名称", example = "张三") - private String nickname; - - @Schema(description = "产品编号", example = "26202") - private Long productId; - - @Schema(description = "产品标识") - private String productKey; - - @Schema(description = "设备类型", example = "1") - @InEnum(IotProductDeviceTypeEnum.class) - private Integer deviceType; - - @Schema(description = "网关设备 ID", example = "16380") - private Long gatewayId; - - @Schema(description = "设备状态", example = "1") - @InEnum(IotDeviceStatusEnum.class) - private Integer status; - - @Schema(description = "设备状态最后更新时间") - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) - private LocalDateTime[] statusLastUpdateTime; - - @Schema(description = "最后上线时间") - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) - private LocalDateTime[] lastOnlineTime; - - @Schema(description = "最后离线时间") - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) - private LocalDateTime[] lastOfflineTime; - - @Schema(description = "设备激活时间") - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) - private LocalDateTime[] activeTime; - - @Schema(description = "设备密钥,用于设备认证,需安全存储") - private String deviceSecret; - - @Schema(description = "MQTT 客户端 ID", example = "24602") - private String mqttClientId; - - @Schema(description = "MQTT 用户名", example = "芋艿") - private String mqttUsername; - - @Schema(description = "MQTT 密码") - private String mqttPassword; - - @Schema(description = "认证类型(如一机一密、动态注册)", example = "2") - private String authType; - - @Schema(description = "创建时间") - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) - private LocalDateTime[] createTime; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceSaveReqVO.java deleted file mode 100644 index 620e5310f..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceSaveReqVO.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.device.vo; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Schema(description = "管理后台 - IoT 设备新增/修改 Request VO") -@Data -public class IotDeviceSaveReqVO { - - @Schema(description = "设备编号", example = "177") - private Long id; - - @Schema(description = "设备名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") - private String deviceName; - - @Schema(description = "备注名称", example = "张三") - private String nickname; - - @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "26202") - private Long productId; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceStatusUpdateReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceStatusUpdateReqVO.java deleted file mode 100644 index a91a58690..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceStatusUpdateReqVO.java +++ /dev/null @@ -1,21 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.device.vo; - -import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStatusEnum; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -@Schema(description = "管理后台 - IoT 设备状态更新 Request VO") -@Data -public class IotDeviceStatusUpdateReqVO { - - @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @NotNull(message = "设备编号不能为空") - private Long id; - - @Schema(description = "设备状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @NotNull(message = "设备状态不能为空") - @InEnum(IotDeviceStatusEnum.class) - private Integer status; -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/control/IotDeviceDownstreamReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/control/IotDeviceDownstreamReqVO.java new file mode 100644 index 000000000..eefaeffeb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/control/IotDeviceDownstreamReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.control; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 设备下行 Request VO") // 服务调用、属性设置、属性获取等 +@Data +public class IotDeviceDownstreamReqVO { + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "177") + @NotNull(message = "设备编号不能为空") + private Long id; + + @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "property") + @NotEmpty(message = "消息类型不能为空") + @InEnum(IotDeviceMessageTypeEnum.class) + private String type; + + @Schema(description = "标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "report") + @NotEmpty(message = "标识符不能为空") + private String identifier; // 参见 IotDeviceMessageIdentifierEnum 枚举类 + + @Schema(description = "请求参数", requiredMode = Schema.RequiredMode.REQUIRED) + private Object data; // 例如说:服务调用的 params、属性设置的 properties + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/control/IotDeviceUpstreamReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/control/IotDeviceUpstreamReqVO.java new file mode 100644 index 000000000..778d75bba --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/control/IotDeviceUpstreamReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.control; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 设备上行 Request VO") // 属性上报、事件上报、状态变更等 +@Data +public class IotDeviceUpstreamReqVO { + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "177") + @NotNull(message = "设备编号不能为空") + private Long id; + + @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "property") + @NotEmpty(message = "消息类型不能为空") + @InEnum(IotDeviceMessageTypeEnum.class) + private String type; + + @Schema(description = "标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "report") + @NotEmpty(message = "标识符不能为空") + private String identifier; // 参见 IotDeviceMessageIdentifierEnum 枚举类 + + @Schema(description = "请求参数", requiredMode = Schema.RequiredMode.REQUIRED) + private Object data; // 例如说:属性上报的 properties、事件上报的 params + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDeviceLogPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDeviceLogPageReqVO.java new file mode 100644 index 000000000..fcf36994f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDeviceLogPageReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.data; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 设备日志分页查询 Request VO") +@Data +public class IotDeviceLogPageReqVO extends PageParam { + + @Schema(description = "设备标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "device123") + @NotEmpty(message = "设备标识不能为空") + private String deviceKey; + + @Schema(description = "消息类型", example = "property") + private String type; // 参见 IotDeviceMessageTypeEnum 枚举,精准匹配 + + @Schema(description = "标识符", example = "temperature") + private String identifier; // 参见 IotDeviceMessageIdentifierEnum 枚举,模糊匹配 + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDeviceLogRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDeviceLogRespVO.java new file mode 100644 index 000000000..6e6639ede --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDeviceLogRespVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.data; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT 设备日志 Response VO") +@Data +public class IotDeviceLogRespVO { + + @Schema(description = "日志编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String id; + + @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "product123") + private String productKey; + + @Schema(description = "设备标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "device123") + private String deviceKey; + + @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "property") + private String type; + + @Schema(description = "标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "temperature") + private String identifier; + + @Schema(description = "日志内容", requiredMode = Schema.RequiredMode.REQUIRED) + private String content; + + @Schema(description = "上报时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime reportTime; + + @Schema(description = "记录时间戳", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime ts; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDevicePropertyHistoryPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDevicePropertyHistoryPageReqVO.java new file mode 100644 index 000000000..0de45e4a7 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDevicePropertyHistoryPageReqVO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.data; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - IoT 设备属性历史分页 Request VO") +@Data +public class IotDevicePropertyHistoryPageReqVO extends PageParam { + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "177") + @NotNull(message = "设备编号不能为空") + private Long deviceId; + + @Schema(description = "设备 Key", hidden = true) + private String deviceKey; // 非前端传递,后端自己查询设置 + + @Schema(description = "属性标识符", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "属性标识符不能为空") + private String identifier; + + @Schema(description = "时间范围", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Size(min = 2, max = 2, message = "请选择时间范围") + private LocalDateTime[] times; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDevicePropertyRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDevicePropertyRespVO.java new file mode 100644 index 000000000..dd7a0d6ad --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDevicePropertyRespVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.data; + +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 设备属性 Response VO") +@Data +public class IotDevicePropertyRespVO { + + @Schema(description = "属性定义", requiredMode = Schema.RequiredMode.REQUIRED) + private ThingModelProperty property; + + @Schema(description = "最新值", requiredMode = Schema.RequiredMode.REQUIRED) + private Object value; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private Long updateTime; // 由于从 TDengine 查询出来的是 Long 类型,所以这里也使用 Long 类型 + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java new file mode 100644 index 000000000..710e74263 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; + +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * 设备 Excel 导入 VO + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Accessors(chain = false) // 设置 chain = false,避免设备导入有问题 +public class IotDeviceImportExcelVO { + + @ExcelProperty("设备名称") + @NotEmpty(message = "设备名称不能为空") + private String deviceName; + + @ExcelProperty("父设备名称") + @Schema(description = "父设备名称", example = "网关001") + private String parentDeviceName; + + @ExcelProperty("产品标识") + @NotEmpty(message = "产品标识不能为空") + private String productKey; + + @ExcelProperty("设备分组") + private String groupNames; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportRespVO.java new file mode 100644 index 000000000..bf52b123f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportRespVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - IoT 设备导入 Response VO") +@Data +@Builder +public class IotDeviceImportRespVO { + + @Schema(description = "创建成功的设备名称数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List createDeviceNames; + + @Schema(description = "更新成功的设备名称数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List updateDeviceNames; + + @Schema(description = "导入失败的设备集合,key为设备名称,value为失败原因", requiredMode = Schema.RequiredMode.REQUIRED) + private Map failureDeviceNames; +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceMqttConnectionParamsRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceMqttConnectionParamsRespVO.java new file mode 100644 index 000000000..5ce68c0fe --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceMqttConnectionParamsRespVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; + +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 设备 MQTT 连接参数 Response VO") +@Data +@ExcelIgnoreUnannotated +public class IotDeviceMqttConnectionParamsRespVO { + + @Schema(description = "MQTT 客户端 ID", example = "24602") + @ExcelProperty("MQTT 客户端 ID") + private String mqttClientId; + + @Schema(description = "MQTT 用户名", example = "芋艿") + @ExcelProperty("MQTT 用户名") + private String mqttUsername; + + @Schema(description = "MQTT 密码") + @ExcelProperty("MQTT 密码") + private String mqttPassword; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java new file mode 100644 index 000000000..686267732 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 设备分页 Request VO") +@Data +public class IotDevicePageReqVO extends PageParam { + + @Schema(description = "设备名称", example = "王五") + private String deviceName; + + @Schema(description = "备注名称", example = "张三") + private String nickname; + + @Schema(description = "产品编号", example = "26202") + private Long productId; + + @Schema(description = "设备类型", example = "1") + @InEnum(IotProductDeviceTypeEnum.class) + private Integer deviceType; + + @Schema(description = "设备状态", example = "1") + @InEnum(IotDeviceStateEnum.class) + private Integer status; + + @Schema(description = "设备分组编号", example = "1024") + private Long groupId; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java similarity index 70% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceRespVO.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java index 488f6b907..8404ca922 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceRespVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java @@ -1,12 +1,16 @@ -package cn.iocoder.yudao.module.iot.controller.admin.device.vo; +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -import java.math.BigDecimal; import java.time.LocalDateTime; +import java.util.Set; + +import static cn.iocoder.yudao.module.iot.enums.DictTypeConstants.DEVICE_STATE; @Schema(description = "管理后台 - IoT 设备 Response VO") @Data @@ -21,9 +25,24 @@ public class IotDeviceRespVO { private String deviceKey; @Schema(description = "设备名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") - @ExcelProperty("设备名称备") + @ExcelProperty("设备名称") private String deviceName; + @Schema(description = "设备备注名称", example = "张三") + @ExcelProperty("设备备注名称") + private String nickname; + + @Schema(description = "设备序列号", example = "1024") + @ExcelProperty("设备序列号") + private String serialNumber; + + @Schema(description = "设备图片", example = "我是一名码农") + @ExcelProperty("设备图片") + private String picUrl; + + @Schema(description = "设备分组编号数组", example = "1,2") + private Set groupIds; + @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "26202") @ExcelProperty("产品编号") private Long productId; @@ -36,28 +55,21 @@ public class IotDeviceRespVO { @ExcelProperty("设备类型") private Integer deviceType; - @Schema(description = "设备备注名称", example = "张三") - @ExcelProperty("设备备注名称") - private String nickname; - @Schema(description = "网关设备 ID", example = "16380") private Long gatewayId; @Schema(description = "设备状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @ExcelProperty("设备状态") - private Integer status; - - @Schema(description = "设备状态最后更新时间") - @ExcelProperty("设备状态最后更新时间") - private LocalDateTime statusLastUpdateTime; + @ExcelProperty(value = "设备状态", converter = DictConvert.class) + @DictFormat(DEVICE_STATE) + private Integer state; @Schema(description = "最后上线时间") @ExcelProperty("最后上线时间") - private LocalDateTime lastOnlineTime; + private LocalDateTime onlineTime; @Schema(description = "最后离线时间") @ExcelProperty("最后离线时间") - private LocalDateTime lastOfflineTime; + private LocalDateTime offlineTime; @Schema(description = "设备激活时间") @ExcelProperty("设备激活时间") @@ -67,22 +79,13 @@ public class IotDeviceRespVO { @ExcelProperty("设备密钥") private String deviceSecret; - @Schema(description = "MQTT 客户端 ID", example = "24602") - @ExcelProperty("MQTT 客户端 ID") - private String mqttClientId; - - @Schema(description = "MQTT 用户名", example = "芋艿") - @ExcelProperty("MQTT 用户名") - private String mqttUsername; - - @Schema(description = "MQTT 密码") - @ExcelProperty("MQTT 密码") - private String mqttPassword; - @Schema(description = "认证类型(如一机一密、动态注册)", example = "2") @ExcelProperty("认证类型(如一机一密、动态注册)") private String authType; + @Schema(description = "设备配置", example = "{\"abc\": \"efg\"}") + private String config; + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("创建时间") private LocalDateTime createTime; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceSaveReqVO.java new file mode 100644 index 000000000..b9ea9b99f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceSaveReqVO.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.Set; + +@Schema(description = "管理后台 - IoT 设备新增/修改 Request VO") +@Data +public class IotDeviceSaveReqVO { + + @Schema(description = "设备编号", example = "177") + private Long id; + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.AUTO, example = "177") + @Size(max = 50, message = "设备编号长度不能超过 50 个字符") + private String deviceKey; + + @Schema(description = "设备名称", requiredMode = Schema.RequiredMode.AUTO, example = "王五") + private String deviceName; + + @Schema(description = "备注名称", example = "张三") + private String nickname; + + @Schema(description = "设备序列号", example = "123456") + private String serialNumber; + + @Schema(description = "设备图片", example = "https://iocoder.cn/1.png") + private String picUrl; + + @Schema(description = "设备分组编号数组", example = "1,2") + private Set groupIds; + + @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "26202") + private Long productId; + + @Schema(description = "网关设备 ID", example = "16380") + private Long gatewayId; + + @Schema(description = "设备配置", example = "{\"abc\": \"efg\"}") + private String config; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUpdateGroupReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUpdateGroupReqVO.java new file mode 100644 index 000000000..bf66fbf98 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUpdateGroupReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.Set; + +@Schema(description = "管理后台 - IoT 设备更新分组 Request VO") +@Data +public class IotDeviceUpdateGroupReqVO { + + @Schema(description = "设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") + @NotEmpty(message = "设备编号列表不能为空") + private Set ids; + + @Schema(description = "分组编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") + @NotEmpty(message = "分组编号列表不能为空") + private Set groupIds; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupPageReqVO.java new file mode 100644 index 000000000..93b1a1ead --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupPageReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.group; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - IoT 设备分组分页 Request VO") +@Data +public class IotDeviceGroupPageReqVO extends PageParam { + + @Schema(description = "分组名字", example = "李四") + private String name; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupRespVO.java new file mode 100644 index 000000000..4fd541502 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupRespVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.group; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT 设备分组 Response VO") +@Data +public class IotDeviceGroupRespVO { + + @Schema(description = "分组 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "3583") + private Long id; + + @Schema(description = "分组名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + private String name; + + @Schema(description = "分组状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "分组描述", example = "你说的对") + private String description; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "设备数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Long deviceCount; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupSaveReqVO.java new file mode 100644 index 000000000..491cd9366 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupSaveReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.group; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 设备分组新增/修改 Request VO") +@Data +public class IotDeviceGroupSaveReqVO { + + @Schema(description = "分组 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "3583") + private Long id; + + @Schema(description = "分组名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + @NotEmpty(message = "分组名字不能为空") + private String name; + + @Schema(description = "分组状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "分组状态不能为空") + private Integer status; + + @Schema(description = "分组描述", example = "你说的对") + private String description; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaFirmwareController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaFirmwareController.java new file mode 100644 index 000000000..6cc3918e8 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaFirmwareController.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +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.ota.vo.firmware.IotOtaFirmwarePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareCreateReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareUpdateReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import cn.iocoder.yudao.module.iot.service.ota.IotOtaFirmwareService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT OTA 固件") +@RestController +@RequestMapping("/iot/ota-firmware") +@Validated +public class IotOtaFirmwareController { + + @Resource + private IotOtaFirmwareService otaFirmwareService; + + @PostMapping("/create") + @Operation(summary = "创建 OTA 固件") + @PreAuthorize("@ss.hasPermission('iot:ota-firmware:create')") + public CommonResult createOtaFirmware(@Valid @RequestBody IotOtaFirmwareCreateReqVO createReqVO) { + return success(otaFirmwareService.createOtaFirmware(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新 OTA 固件") + @PreAuthorize("@ss.hasPermission('iot:ota-firmware:update')") + public CommonResult updateOtaFirmware(@Valid @RequestBody IotOtaFirmwareUpdateReqVO updateReqVO) { + otaFirmwareService.updateOtaFirmware(updateReqVO); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得 OTA 固件") + @PreAuthorize("@ss.hasPermission('iot:ota-firmware:query')") + public CommonResult getOtaFirmware(@RequestParam("id") Long id) { + IotOtaFirmwareDO otaFirmware = otaFirmwareService.getOtaFirmware(id); + return success(BeanUtils.toBean(otaFirmware, IotOtaFirmwareRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得 OTA 固件分页") + @PreAuthorize("@ss.hasPermission('iot:ota-firmware:query')") + public CommonResult> getOtaFirmwarePage(@Valid IotOtaFirmwarePageReqVO pageReqVO) { + PageResult pageResult = otaFirmwareService.getOtaFirmwarePage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotOtaFirmwareRespVO.class)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeRecordController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeRecordController.java new file mode 100644 index 000000000..f6bc526ac --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeRecordController.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +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.ota.vo.upgrade.record.IotOtaUpgradeRecordPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record.IotOtaUpgradeRecordRespVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeRecordDO; +import cn.iocoder.yudao.module.iot.service.ota.IotOtaUpgradeRecordService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT OTA 升级记录") +@RestController +@RequestMapping("/iot/ota-upgrade-record") +@Validated +public class IotOtaUpgradeRecordController { + + @Resource + private IotOtaUpgradeRecordService upgradeRecordService; + + @GetMapping("/get-statistics") + @Operation(summary = "固件升级设备统计") + @PreAuthorize("@ss.hasPermission('iot:ota-upgrade-record:query')") + @Parameter(name = "firmwareId", description = "固件编号", required = true, example = "1024") + public CommonResult> getOtaUpgradeRecordStatistics(@RequestParam(value = "firmwareId") Long firmwareId) { + return success(upgradeRecordService.getOtaUpgradeRecordStatistics(firmwareId)); + } + + @GetMapping("/get-count") + @Operation(summary = "获得升级记录分页 tab 数量") + @PreAuthorize("@ss.hasPermission('iot:ota-upgrade-record:query')") + public CommonResult> getOtaUpgradeRecordCount( + @Valid IotOtaUpgradeRecordPageReqVO pageReqVO) { + return success(upgradeRecordService.getOtaUpgradeRecordCount(pageReqVO)); + } + + @GetMapping("/page") + @Operation(summary = "获得升级记录分页") + @PreAuthorize("@ss.hasPermission('iot:ota-upgrade-record:query')") + public CommonResult> getUpgradeRecordPage( + @Valid IotOtaUpgradeRecordPageReqVO pageReqVO) { + PageResult pageResult = upgradeRecordService.getUpgradeRecordPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotOtaUpgradeRecordRespVO.class)); + } + + @GetMapping("/get") + @Operation(summary = "获得升级记录") + @PreAuthorize("@ss.hasPermission('iot:ota-upgrade-record:query')") + @Parameter(name = "id", description = "升级记录编号", required = true, example = "1024") + public CommonResult getUpgradeRecord(@RequestParam("id") Long id) { + IotOtaUpgradeRecordDO upgradeRecord = upgradeRecordService.getUpgradeRecord(id); + return success(BeanUtils.toBean(upgradeRecord, IotOtaUpgradeRecordRespVO.class)); + } + + @PutMapping("/retry") + @Operation(summary = "重试升级记录") + @PreAuthorize("@ss.hasPermission('iot:ota-upgrade-record:retry')") + @Parameter(name = "id", description = "升级记录编号", required = true, example = "1024") + public CommonResult retryUpgradeRecord(@RequestParam("id") Long id) { + upgradeRecordService.retryUpgradeRecord(id); + return success(true); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeTaskController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeTaskController.java new file mode 100644 index 000000000..e248e8027 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeTaskController.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +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.ota.vo.upgrade.task.IotOtaUpgradeTaskPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO; +import cn.iocoder.yudao.module.iot.service.ota.IotOtaUpgradeTaskService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT OTA 升级任务") +@RestController +@RequestMapping("/iot/ota-upgrade-task") +@Validated +public class IotOtaUpgradeTaskController { + + @Resource + private IotOtaUpgradeTaskService upgradeTaskService; + + @PostMapping("/create") + @Operation(summary = "创建升级任务") + @PreAuthorize(value = "@ss.hasPermission('iot:ota-upgrade-task:create')") + public CommonResult createUpgradeTask(@Valid @RequestBody IotOtaUpgradeTaskSaveReqVO createReqVO) { + return success(upgradeTaskService.createUpgradeTask(createReqVO)); + } + + @PostMapping("/cancel") + @Operation(summary = "取消升级任务") + @Parameter(name = "id", description = "升级任务编号", required = true) + @PreAuthorize(value = "@ss.hasPermission('iot:ota-upgrade-task:cancel')") + public CommonResult cancelUpgradeTask(@RequestParam("id") Long id) { + upgradeTaskService.cancelUpgradeTask(id); + return success(true); + } + + @GetMapping("/page") + @Operation(summary = "获得升级任务分页") + @PreAuthorize(value = "@ss.hasPermission('iot:ota-upgrade-task:query')") + public CommonResult> getUpgradeTaskPage(@Valid IotOtaUpgradeTaskPageReqVO pageReqVO) { + PageResult pageResult = upgradeTaskService.getUpgradeTaskPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotOtaUpgradeTaskRespVO.class)); + } + + @GetMapping("/get") + @Operation(summary = "获得升级任务") + @Parameter(name = "id", description = "升级任务编号", required = true, example = "1024") + @PreAuthorize(value = "@ss.hasPermission('iot:ota-upgrade-task:query')") + public CommonResult getUpgradeTask(@RequestParam("id") Long id) { + IotOtaUpgradeTaskDO upgradeTask = upgradeTaskService.getUpgradeTask(id); + return success(BeanUtils.toBean(upgradeTask, IotOtaUpgradeTaskRespVO.class)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareCreateReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareCreateReqVO.java new file mode 100644 index 000000000..50c2ece15 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareCreateReqVO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Schema(description = "管理后台 - IoT OTA 固件创建 Request VO") +@Data +public class IotOtaFirmwareCreateReqVO { + + @Schema(description = "固件名称", requiredMode = REQUIRED, example = "智能开关固件") + @NotEmpty(message = "固件名称不能为空") + private String name; + + @Schema(description = "固件描述", example = "某品牌型号固件,测试用") + private String description; + + @Schema(description = "版本号", requiredMode = REQUIRED, example = "1.0.0") + @NotEmpty(message = "版本号不能为空") + private String version; + + @Schema(description = "产品编号", requiredMode = REQUIRED, example = "1024") + @NotNull(message = "产品编号不能为空") + private String productId; + + @Schema(description = "签名方式", example = "MD5") + // TODO @li:是不是必传哈 + private String signMethod; + + @Schema(description = "固件文件 URL", requiredMode = REQUIRED, example = "https://www.iocoder.cn/yudao-firmware.zip") + @NotEmpty(message = "固件文件 URL 不能为空") + private String fileUrl; + + @Schema(description = "自定义信息,建议使用 JSON 格式", example = "{\"key1\":\"value1\",\"key2\":\"value2\"}") + private String information; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwarePageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwarePageReqVO.java new file mode 100644 index 000000000..baa741029 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwarePageReqVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "管理后台 - IoT OTA 固件分页 Request VO") +public class IotOtaFirmwarePageReqVO extends PageParam { + + /** + * 固件名称 + */ + @Schema(description = "固件名称", example = "智能开关固件") + private String name; + + /** + * 产品标识 + */ + @Schema(description = "产品标识", example = "1024") + private String productId; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareRespVO.java new file mode 100644 index 000000000..735618781 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareRespVO.java @@ -0,0 +1,85 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware; + +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import com.fhs.core.trans.anno.Trans; +import com.fhs.core.trans.constant.TransType; +import com.fhs.core.trans.vo.VO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Data +@Schema(description = "管理后台 - IoT OTA 固件 Response VO") +public class IotOtaFirmwareRespVO implements VO { + + /** + * 固件编号 + */ + @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") + private Long id; + /** + * 固件名称 + */ + @Schema(description = "固件名称", requiredMode = REQUIRED, example = "OTA固件") + private String name; + /** + * 固件描述 + */ + @Schema(description = "固件描述") + private String description; + /** + * 版本号 + */ + @Schema(description = "版本号", requiredMode = REQUIRED, example = "1.0.0") + private String version; + + /** + * 产品编号 + *

+ * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()} + */ + @Schema(description = "产品编号", requiredMode = REQUIRED, example = "1024") + @Trans(type = TransType.SIMPLE, target = IotProductDO.class, fields = {"name"}, refs = {"productName"}) + private String productId; + /** + * 产品标识 + *

+ * 冗余 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getProductKey()} + */ + @Schema(description = "产品标识", requiredMode = REQUIRED, example = "iot-product-key") + private String productKey; + /** + * 产品名称 + */ + @Schema(description = "产品名称", requiredMode = REQUIRED, example = "OTA产品") + private String productName; + /** + * 签名方式 + *

+ * 例如说:MD5、SHA256 + */ + @Schema(description = "签名方式", example = "MD5") + private String signMethod; + /** + * 固件文件签名 + */ + @Schema(description = "固件文件签名", example = "1024") + private String fileSign; + /** + * 固件文件大小 + */ + @Schema(description = "固件文件大小", requiredMode = REQUIRED, example = "1024") + private Long fileSize; + /** + * 固件文件 URL + */ + @Schema(description = "固件文件 URL", requiredMode = REQUIRED, example = "https://www.iocoder.cn") + private String fileUrl; + /** + * 自定义信息,建议使用 JSON 格式 + */ + @Schema(description = "自定义信息,建议使用 JSON 格式") + private String information; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareUpdateReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareUpdateReqVO.java new file mode 100644 index 000000000..aa134bcee --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareUpdateReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Schema(description = "管理后台 - IoT OTA 固件更新 Request VO") +@Data +public class IotOtaFirmwareUpdateReqVO { + + @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") + @NotNull(message = "固件编号不能为空") + private Long id; + + // TODO @li:name 是不是可以飞必传哈 + @Schema(description = "固件名称", requiredMode = REQUIRED, example = "智能开关固件") + @NotEmpty(message = "固件名称不能为空") + private String name; + + @Schema(description = "固件描述", example = "某品牌型号固件,测试用") + private String description; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordPageReqVO.java new file mode 100644 index 000000000..2b21b3079 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordPageReqVO.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Data +@Schema(description = "管理后台 - IoT OTA 升级记录分页 Request VO") +public class IotOtaUpgradeRecordPageReqVO extends PageParam { + + // TODO @li:已经有注解,不用重复注释 + /** + * 升级任务编号字段。 + *

+ * 该字段用于标识升级任务的唯一编号,不能为空。 + */ + @Schema(description = "升级任务编号", requiredMode = REQUIRED, example = "1024") + @NotNull(message = "升级任务编号不能为空") + private Long taskId; + + /** + * 设备标识字段。 + *

+ * 该字段用于标识设备的名称,通常用于区分不同的设备。 + */ + @Schema(description = "设备标识", requiredMode = REQUIRED, example = "摄像头A1-1") + private String deviceName; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordRespVO.java new file mode 100644 index 000000000..db6737feb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordRespVO.java @@ -0,0 +1,109 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record; + +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO; +import com.fhs.core.trans.anno.Trans; +import com.fhs.core.trans.constant.TransType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Data +@Schema(description = "管理后台 - IoT OTA 升级记录 Response VO") +public class IotOtaUpgradeRecordRespVO { + + /** + * 升级记录编号 + */ + @Schema(description = "升级记录编号", requiredMode = REQUIRED, example = "1024") + private Long id; + /** + * 固件编号 + *

+ * 关联 {@link IotOtaFirmwareDO#getId()} + */ + @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") + @Trans(type = TransType.SIMPLE, target = IotOtaFirmwareDO.class, fields = {"version"}, refs = {"firmwareVersion"}) + private Long firmwareId; + /** + * 固件版本 + */ + @Schema(description = "固件版本", requiredMode = REQUIRED, example = "v1.0.0") + private String firmwareVersion; + /** + * 任务编号 + *

+ * 关联 {@link IotOtaUpgradeTaskDO#getId()} + */ + @Schema(description = "任务编号", requiredMode = REQUIRED, example = "1024") + private Long taskId; + /** + * 产品标识 + *

+ * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()} + */ + @Schema(description = "产品标识", requiredMode = REQUIRED, example = "iot") + private String productKey; + /** + * 设备名称 + *

+ * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()} + */ + @Schema(description = "设备名称", requiredMode = REQUIRED, example = "iot") + private String deviceName; + /** + * 设备编号 + *

+ * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()} + */ + @Schema(description = "设备编号", requiredMode = REQUIRED, example = "1024") + private String deviceId; + /** + * 来源的固件编号 + *

+ * 关联 {@link IotDeviceDO#getFirmwareId()} + */ + @Schema(description = "来源的固件编号", requiredMode = REQUIRED, example = "1024") + @Trans(type = TransType.SIMPLE, target = IotOtaFirmwareDO.class, fields = {"version"}, refs = {"fromFirmwareVersion"}) + private Long fromFirmwareId; + /** + * 来源的固件版本 + */ + @Schema(description = "来源的固件版本", requiredMode = REQUIRED, example = "v1.0.0") + private String fromFirmwareVersion; + /** + * 升级状态 + *

+ * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum} + */ + @Schema(description = "升级状态", requiredMode = REQUIRED, allowableValues = {"0", "10", "20", "30", "40", "50"}) + private Integer status; + /** + * 升级进度,百分比 + */ + @Schema(description = "升级进度,百分比", requiredMode = REQUIRED, example = "10") + private Integer progress; + /** + * 升级进度描述 + *

+ * 注意,只记录设备最后一次的升级进度描述 + * 如果想看历史记录,可以查看 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO} 设备日志 + */ + @Schema(description = "升级进度描述", requiredMode = REQUIRED, example = "10") + private String description; + /** + * 升级开始时间 + */ + @Schema(description = "升级开始时间", requiredMode = REQUIRED, example = "2022-07-08 07:30:00") + private LocalDateTime startTime; + /** + * 升级结束时间 + */ + @Schema(description = "升级结束时间", requiredMode = REQUIRED, example = "2022-07-08 07:30:00") + private LocalDateTime endTime; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskPageReqVO.java new file mode 100644 index 000000000..d2b1926aa --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskPageReqVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Data +@Schema(description = "管理后台 - IoT OTA 升级任务分页 Request VO") +public class IotOtaUpgradeTaskPageReqVO extends PageParam { + + /** + * 任务名称字段,用于描述任务的名称 + */ + @Schema(description = "任务名称", example = "升级任务") + private String name; + + /** + * 固件编号字段,用于唯一标识固件,不能为空 + */ + @NotNull(message = "固件编号不能为空") + @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") + private Long firmwareId; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskRespVO.java new file mode 100644 index 000000000..dbc29618f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskRespVO.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task; + +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import com.fhs.core.trans.vo.VO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Data +@Schema(description = "管理后台 - IoT OTA 升级任务 Response VO") +public class IotOtaUpgradeTaskRespVO implements VO { + + /** + * 任务编号 + */ + @Schema(description = "任务编号", requiredMode = REQUIRED, example = "1024") + private Long id; + /** + * 任务名称 + */ + @Schema(description = "任务名称", requiredMode = REQUIRED, example = "升级任务") + private String name; + /** + * 任务描述 + */ + @Schema(description = "任务描述", example = "升级任务") + private String description; + /** + * 固件编号 + *

+ * 关联 {@link IotOtaFirmwareDO#getId()} + */ + @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") + private Long firmwareId; + /** + * 任务状态 + *

+ * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskStatusEnum} + */ + @Schema(description = "任务状态", requiredMode = REQUIRED, allowableValues = {"10", "20", "21", "30"}) + private Integer status; + /** + * 任务状态名称 + */ + @Schema(description = "任务状态名称", requiredMode = REQUIRED, example = "进行中") + private String statusName; + /** + * 升级范围 + *

+ * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum} + */ + @Schema(description = "升级范围", requiredMode = REQUIRED, allowableValues = {"1", "2"}) + private Integer scope; + /** + * 设备数量 + */ + @Schema(description = "设备数量", requiredMode = REQUIRED, example = "1024") + private Long deviceCount; + /** + * 选中的设备编号数组 + *

+ * 关联 {@link IotDeviceDO#getId()} + */ + @Schema(description = "选中的设备编号数组", example = "1024") + private List deviceIds; + /** + * 选中的设备名字数组 + *

+ * 关联 {@link IotDeviceDO#getDeviceName()} + */ + @Schema(description = "选中的设备名字数组", example = "1024") + private List deviceNames; + /** + * 创建时间 + */ + @Schema(description = "创建时间", requiredMode = REQUIRED, example = "2022-07-08 07:30:00") + private LocalDateTime createTime; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskSaveReqVO.java new file mode 100644 index 000000000..0ace17a04 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskSaveReqVO.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Data +@Schema(description = "管理后台 - IoT OTA 升级任务创建/修改 Request VO") +public class IotOtaUpgradeTaskSaveReqVO { + + // TODO @li:已经有注解,不用重复注释 + // TODO @li: @Schema 写在参数校验前面。先有定义;其他的,也检查下; + + /** + * 任务名称 + */ + @NotEmpty(message = "任务名称不能为空") + @Schema(description = "任务名称", requiredMode = REQUIRED, example = "升级任务") + private String name; + + /** + * 任务描述 + */ + @Schema(description = "任务描述", example = "升级任务") + private String description; + + /** + * 固件编号 + *

+ * 关联 {@link IotOtaFirmwareDO#getId()} + */ + @NotNull(message = "固件编号不能为空") + @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") + private Long firmwareId; + + /** + * 升级范围 + *

+ * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum} + */ + @NotNull(message = "升级范围不能为空") + @InEnum(value = IotOtaUpgradeTaskScopeEnum.class) + @Schema(description = "升级范围", requiredMode = REQUIRED, example = "1") + private Integer scope; + + /** + * 选中的设备编号数组 + *

+ * 关联 {@link IotDeviceDO#getId()} + */ + @Schema(description = "选中的设备编号数组", requiredMode = REQUIRED, example = "[1,2,3,4]") + private List deviceIds; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/PluginConfigController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/PluginConfigController.java new file mode 100644 index 000000000..e21b10241 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/PluginConfigController.java @@ -0,0 +1,90 @@ +package cn.iocoder.yudao.module.iot.controller.admin.plugin; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +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.plugin.vo.config.PluginConfigImportReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigSaveReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigStatusReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO; +import cn.iocoder.yudao.module.iot.service.plugin.IotPluginConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT 插件配置") +@RestController +@RequestMapping("/iot/plugin-config") +@Validated +public class PluginConfigController { + + @Resource + private IotPluginConfigService pluginConfigService; + + @PostMapping("/create") + @Operation(summary = "创建插件配置") + @PreAuthorize("@ss.hasPermission('iot:plugin-config:create')") + public CommonResult createPluginConfig(@Valid @RequestBody PluginConfigSaveReqVO createReqVO) { + return success(pluginConfigService.createPluginConfig(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新插件配置") + @PreAuthorize("@ss.hasPermission('iot:plugin-config:update')") + public CommonResult updatePluginConfig(@Valid @RequestBody PluginConfigSaveReqVO updateReqVO) { + pluginConfigService.updatePluginConfig(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除插件配置") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:plugin-config:delete')") + public CommonResult deletePluginConfig(@RequestParam("id") Long id) { + pluginConfigService.deletePluginConfig(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得插件配置") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:plugin-config:query')") + public CommonResult getPluginConfig(@RequestParam("id") Long id) { + IotPluginConfigDO pluginConfig = pluginConfigService.getPluginConfig(id); + return success(BeanUtils.toBean(pluginConfig, PluginConfigRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得插件配置分页") + @PreAuthorize("@ss.hasPermission('iot:plugin-config:query')") + public CommonResult> getPluginConfigPage(@Valid PluginConfigPageReqVO pageReqVO) { + PageResult pageResult = pluginConfigService.getPluginConfigPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, PluginConfigRespVO.class)); + } + + @PostMapping("/upload-file") + @Operation(summary = "上传插件文件") + @PreAuthorize("@ss.hasPermission('iot:plugin-config:update')") + public CommonResult uploadFile(@Valid PluginConfigImportReqVO reqVO) { + pluginConfigService.uploadFile(reqVO.getId(), reqVO.getFile()); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "修改插件状态") + @PreAuthorize("@ss.hasPermission('iot:plugin-config:update')") + public CommonResult updatePluginConfigStatus(@Valid @RequestBody PluginConfigStatusReqVO reqVO) { + pluginConfigService.updatePluginStatus(reqVO.getId(), reqVO.getStatus()); + return success(true); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigImportReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigImportReqVO.java new file mode 100644 index 000000000..b9b277a54 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigImportReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.web.multipart.MultipartFile; + +@Schema(description = "管理后台 - IoT 插件上传 Request VO") +@Data +public class PluginConfigImportReqVO { + + @Schema(description = "主键 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546") + private Long id; + + @Schema(description = "插件文件", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "插件文件不能为空") + private MultipartFile file; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigPageReqVO.java new file mode 100644 index 000000000..1666d5d6b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigPageReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 插件配置分页 Request VO") +@Data +public class PluginConfigPageReqVO extends PageParam { + + @Schema(description = "插件名称", example = "http") + private String name; + + @Schema(description = "状态", example = "1") + @InEnum(IotPluginStatusEnum.class) + private Integer status; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigRespVO.java new file mode 100644 index 000000000..2b8c4dcde --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigRespVO.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT 插件配置 Response VO") +@Data +public class PluginConfigRespVO { + + @Schema(description = "主键 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546") + private Long id; + + @Schema(description = "插件包标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "24627") + private String pluginKey; + + @Schema(description = "插件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") + private String name; + + @Schema(description = "描述", example = "你猜") + private String description; + + @Schema(description = "部署方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer deployType; + + @Schema(description = "插件包文件名", requiredMode = Schema.RequiredMode.REQUIRED) + private String fileName; + + @Schema(description = "插件版本", requiredMode = Schema.RequiredMode.REQUIRED) + private String version; + + @Schema(description = "插件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer type; + + @Schema(description = "设备插件协议类型") + private String protocol; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer status; + + @Schema(description = "插件配置项描述信息") + private String configSchema; + + @Schema(description = "插件配置信息") + private String config; + + @Schema(description = "插件脚本") + private String script; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigSaveReqVO.java new file mode 100644 index 000000000..e48869d64 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigSaveReqVO.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 插件配置新增/修改 Request VO") +@Data +public class PluginConfigSaveReqVO { + + // TODO @haohao:新增的字段有点多,每个都需要哇? + + // TODO @haohao:一些枚举字段,需要加枚举校验。例如说,deployType、status、type 等 + + @Schema(description = "主键编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546") + private Long id; + + @Schema(description = "插件包标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "24627") + private String pluginKey; + + @Schema(description = "插件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") + private String name; + + @Schema(description = "描述", example = "你猜") + private String description; + + @Schema(description = "部署方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer deployType; + + @Schema(description = "插件包文件名", requiredMode = Schema.RequiredMode.REQUIRED) + private String fileName; + + @Schema(description = "插件版本", requiredMode = Schema.RequiredMode.REQUIRED) + private String version; + + @Schema(description = "插件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer type; + + @Schema(description = "设备插件协议类型") + private String protocol; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED) + @InEnum(IotPluginStatusEnum.class) + private Integer status; + + @Schema(description = "插件配置项描述信息") + private String configSchema; + + @Schema(description = "插件配置信息") + private String config; + + @Schema(description = "插件脚本") + private String script; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigStatusReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigStatusReqVO.java new file mode 100644 index 000000000..eae4aa0a2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigStatusReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 插件配置状态 Request VO") +@Data +public class PluginConfigStatusReqVO { + + @Schema(description = "主键编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546") + private Long id; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED) + @InEnum(IotPluginStatusEnum.class) + private Integer status; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/instance/PluginInstancePageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/instance/PluginInstancePageReqVO.java new file mode 100644 index 000000000..e58b88856 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/instance/PluginInstancePageReqVO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.instance; + +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +// TODO @haohao:后续需要使用下 +@Schema(description = "管理后台 - IoT 插件实例分页 Request VO") +@Data +public class PluginInstancePageReqVO extends PageParam { + + @Schema(description = "插件主程序编号", example = "23738") + private String mainId; + + @Schema(description = "插件id", example = "26498") + private Long pluginId; + + @Schema(description = "插件主程序所在ip") + private String ip; + + @Schema(description = "插件主程序端口") + private Integer port; + + @Schema(description = "心跳时间,心路时间超过30秒需要剔除") + private Long heartbeatAt; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/instance/PluginInstanceRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/instance/PluginInstanceRespVO.java new file mode 100644 index 000000000..cba59fdaf --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/instance/PluginInstanceRespVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.instance; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +// TODO @haohao:后续需要使用下 +@Schema(description = "管理后台 - IoT 插件实例 Response VO") +@Data +public class PluginInstanceRespVO { + + @Schema(description = "主键编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23864") + private Long id; + + @Schema(description = "插件主程序id", requiredMode = Schema.RequiredMode.REQUIRED, example = "23738") + private String mainId; + + @Schema(description = "插件id", requiredMode = Schema.RequiredMode.REQUIRED, example = "26498") + private Long pluginId; + + @Schema(description = "插件主程序所在ip", requiredMode = Schema.RequiredMode.REQUIRED) + private String ip; + + @Schema(description = "插件主程序端口", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer port; + + @Schema(description = "心跳时间,心路时间超过30秒需要剔除", requiredMode = Schema.RequiredMode.REQUIRED) + private Long heartbeatAt; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductCategoryController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductCategoryController.java new file mode 100644 index 000000000..bc1c1fbf2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductCategoryController.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.module.iot.controller.admin.product; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +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.product.vo.category.IotProductCategoryPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.category.IotProductCategoryRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.category.IotProductCategorySaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductCategoryDO; +import cn.iocoder.yudao.module.iot.service.product.IotProductCategoryService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - IoT 产品分类") +@RestController +@RequestMapping("/iot/product-category") +@Validated +public class IotProductCategoryController { + + @Resource + private IotProductCategoryService productCategoryService; + + @PostMapping("/create") + @Operation(summary = "创建产品分类") + @PreAuthorize("@ss.hasPermission('iot:product-category:create')") + public CommonResult createProductCategory(@Valid @RequestBody IotProductCategorySaveReqVO createReqVO) { + return success(productCategoryService.createProductCategory(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新产品分类") + @PreAuthorize("@ss.hasPermission('iot:product-category:update')") + public CommonResult updateProductCategory(@Valid @RequestBody IotProductCategorySaveReqVO updateReqVO) { + productCategoryService.updateProductCategory(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除产品分类") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:product-category:delete')") + public CommonResult deleteProductCategory(@RequestParam("id") Long id) { + productCategoryService.deleteProductCategory(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得产品分类") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:product-category:query')") + public CommonResult getProductCategory(@RequestParam("id") Long id) { + IotProductCategoryDO productCategory = productCategoryService.getProductCategory(id); + return success(BeanUtils.toBean(productCategory, IotProductCategoryRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得产品分类分页") + @PreAuthorize("@ss.hasPermission('iot:product-category:query')") + public CommonResult> getProductCategoryPage(@Valid IotProductCategoryPageReqVO pageReqVO) { + PageResult pageResult = productCategoryService.getProductCategoryPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotProductCategoryRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得所有产品分类列表") + @PreAuthorize("@ss.hasPermission('iot:product-category:query')") + public CommonResult> getSimpleProductCategoryList() { + List list = productCategoryService.getProductCategoryListByStatus( + CommonStatusEnum.ENABLE.getStatus()); + return success(convertList(list, category -> + new IotProductCategoryRespVO().setId(category.getId()).setName(category.getName()))); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java index 5b0ecb27a..2d8c85640 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java @@ -1,26 +1,36 @@ package cn.iocoder.yudao.module.iot.controller.admin.product; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.iot.controller.admin.product.vo.IotProductPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.product.vo.IotProductRespVO; -import cn.iocoder.yudao.module.iot.controller.admin.product.vo.IotProductSaveReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.product.vo.IotProductSimpleRespVO; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductCategoryDO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.service.product.IotProductCategoryService; import cn.iocoder.yudao.module.iot.service.product.IotProductService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.io.IOException; import java.util.List; +import java.util.Map; +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; @Tag(name = "管理后台 - IoT 产品") @RestController @@ -30,6 +40,8 @@ public class IotProductController { @Resource private IotProductService productService; + @Resource + private IotProductCategoryService categoryService; @PostMapping("/create") @Operation(summary = "创建产品") @@ -72,7 +84,13 @@ public class IotProductController { @PreAuthorize("@ss.hasPermission('iot:product:query')") public CommonResult getProduct(@RequestParam("id") Long id) { IotProductDO product = productService.getProduct(id); - return success(BeanUtils.toBean(product, IotProductRespVO.class)); + // 拼接数据 + IotProductCategoryDO category = categoryService.getProductCategory(product.getCategoryId()); + return success(BeanUtils.toBean(product, IotProductRespVO.class, bean -> { + if (category != null) { + bean.setCategoryName(category.getName()); + } + })); } @GetMapping("/page") @@ -80,16 +98,35 @@ public class IotProductController { @PreAuthorize("@ss.hasPermission('iot:product:query')") public CommonResult> getProductPage(@Valid IotProductPageReqVO pageReqVO) { PageResult pageResult = productService.getProductPage(pageReqVO); - return success(BeanUtils.toBean(pageResult, IotProductRespVO.class)); + // 拼接数据 + Map categoryMap = categoryService.getProductCategoryMap( + convertList(pageResult.getList(), IotProductDO::getCategoryId)); + return success(BeanUtils.toBean(pageResult, IotProductRespVO.class, bean -> { + MapUtils.findAndThen(categoryMap, bean.getCategoryId(), + category -> bean.setCategoryName(category.getName())); + })); } - // TODO @haohao:改成 simple-list 哈 - @GetMapping("/list-all-simple") - @Operation(summary = "获得所有产品列表") - @PreAuthorize("@ss.hasPermission('iot:product:query')") - public CommonResult> listAllSimpleProducts() { + @GetMapping("/export-excel") + @Operation(summary = "导出产品 Excel") + @PreAuthorize("@ss.hasPermission('iot:product:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportProductExcel(@Valid IotProductPageReqVO exportReqVO, + HttpServletResponse response) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + CommonResult> result = getProductPage(exportReqVO); + // 导出 Excel + ExcelUtils.write(response, "产品.xls", "数据", IotProductRespVO.class, + result.getData().getList()); + } + + @GetMapping("/simple-list") + @Operation(summary = "获取产品的精简信息列表", description = "主要用于前端的下拉选项") + public CommonResult> getSimpleProductList() { List list = productService.getProductList(); - return success(BeanUtils.toBean(list, IotProductSimpleRespVO.class)); + return success(convertList(list, product -> // 只返回 id、name 字段 + new IotProductRespVO().setId(product.getId()).setName(product.getName()) + .setDeviceType(product.getDeviceType()))); } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductSimpleRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductSimpleRespVO.java deleted file mode 100644 index 83855eaaf..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductSimpleRespVO.java +++ /dev/null @@ -1,16 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.product.vo; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Schema(description = "管理后台 - IoT 产品 Response VO") -@Data -public class IotProductSimpleRespVO { - - @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "26087") - private Long id; - - @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") - private String name; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategoryPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategoryPageReqVO.java new file mode 100644 index 000000000..f1c12bf7c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategoryPageReqVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.iot.controller.admin.product.vo.category; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - IoT 产品分类分页 Request VO") +@Data +public class IotProductCategoryPageReqVO extends PageParam { + + @Schema(description = "分类名字", example = "王五") + private String name; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategoryRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategoryRespVO.java new file mode 100644 index 000000000..d684b0215 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategoryRespVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.iot.controller.admin.product.vo.category; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.module.system.enums.DictTypeConstants; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT 产品分类 Response VO") +@Data +public class IotProductCategoryRespVO { + + @Schema(description = "分类 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "25284") + private Long id; + + @Schema(description = "分类名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + private String name; + + @Schema(description = "分类排序") + private Integer sort; + + @Schema(description = "分类状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + + @Schema(description = "分类描述", example = "随便") + private String description; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategorySaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategorySaveReqVO.java new file mode 100644 index 000000000..a7b2fe427 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategorySaveReqVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.iot.controller.admin.product.vo.category; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 产品分类新增/修改 Request VO") +@Data +public class IotProductCategorySaveReqVO { + + @Schema(description = "分类 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "25284") + private Long id; + + @Schema(description = "分类名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + @NotEmpty(message = "分类名字不能为空") + private String name; + + @Schema(description = "分类排序") + private Integer sort; + + @Schema(description = "分类状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "分类状态不能为空") + private Integer status; + + @Schema(description = "分类描述", example = "随便") + private String description; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductPageReqVO.java similarity index 78% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductPageReqVO.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductPageReqVO.java index 3437f563f..18c69c4ce 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductPageReqVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductPageReqVO.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.controller.admin.product.vo; +package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product; import cn.iocoder.yudao.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; @@ -8,8 +8,6 @@ import lombok.ToString; @Schema(description = "管理后台 - IoT 产品分页 Request VO") @Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) public class IotProductPageReqVO extends PageParam { @Schema(description = "产品名称", example = "李四") diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java similarity index 56% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductRespVO.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java index 0958b3e84..f674651d5 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductRespVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java @@ -1,5 +1,8 @@ -package cn.iocoder.yudao.module.iot.controller.admin.product.vo; +package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.iot.enums.DictTypeConstants; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; @@ -20,48 +23,65 @@ public class IotProductRespVO { @ExcelProperty("产品名称") private String name; - @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) - @ExcelProperty("创建时间") - private LocalDateTime createTime; - @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("产品标识") private String productKey; + @Schema(description = "产品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long categoryId; + + @Schema(description = "产品分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty("产品分类") + private String categoryName; + + @Schema(description = "产品图标", example = "https://iocoder.cn/1.svg") + @ExcelProperty("产品图标") + private String icon; + + @Schema(description = "产品图片", example = "https://iocoder.cn/1.png") + @ExcelProperty("产品图片") + private String picUrl; + + @Schema(description = "产品描述", example = "你猜") + @ExcelProperty("产品描述") + private String description; + + @Schema(description = "产品状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "产品状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.PRODUCT_STATUS) + private Integer status; + + @Schema(description = "设备类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @ExcelProperty(value = "设备类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.PRODUCT_DEVICE_TYPE) + private Integer deviceType; + + @Schema(description = "联网方式", example = "2") + @ExcelProperty(value = "联网方式", converter = DictConvert.class) + @DictFormat(DictTypeConstants.NET_TYPE) + private Integer netType; + @Schema(description = "接入网关协议", example = "2") - @ExcelProperty("接入网关协议") + @ExcelProperty(value = "接入网关协议", converter = DictConvert.class) + @DictFormat(DictTypeConstants.PROTOCOL_TYPE) private Integer protocolType; @Schema(description = "协议编号(脚本解析 id)", requiredMode = Schema.RequiredMode.REQUIRED, example = "13177") @ExcelProperty("协议编号(脚本解析 id)") private Long protocolId; - @Schema(description = "产品所属品类标识符", example = "14237") - @ExcelProperty("产品所属品类标识符") - private Long categoryId; - - @Schema(description = "产品描述", example = "你猜") - @ExcelProperty("产品描述") - private String description; - - @Schema(description = "数据校验级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @ExcelProperty("数据校验级别") - private Integer validateType; - - @Schema(description = "产品状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @ExcelProperty("产品状态") - private Integer status; - - @Schema(description = "设备类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") - @ExcelProperty("设备类型") - private Integer deviceType; - - @Schema(description = "联网方式", example = "2") - @ExcelProperty("联网方式") - private Integer netType; - @Schema(description = "数据格式") - @ExcelProperty("数据格式") + @ExcelProperty(value = "数据格式", converter = DictConvert.class) + @DictFormat(DictTypeConstants.DATA_FORMAT) private Integer dataFormat; + @Schema(description = "数据校验级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "数据校验级别", converter = DictConvert.class) + @DictFormat(DictTypeConstants.VALIDATE_TYPE) + private Integer validateType; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java similarity index 78% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductSaveReqVO.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java index 254b6b9da..268ab7c6f 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductSaveReqVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.controller.admin.product.vo; +package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product; import cn.iocoder.yudao.framework.common.validation.InEnum; import cn.iocoder.yudao.module.iot.enums.product.*; @@ -14,13 +14,26 @@ public class IotProductSaveReqVO { @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.AUTO, example = "1") private Long id; - @Schema(description = "产品Key", requiredMode = Schema.RequiredMode.AUTO, example = "12345abc") - private String productKey; - @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "温湿度") @NotEmpty(message = "产品名称不能为空") private String name; + @Schema(description = "产品 Key", requiredMode = Schema.RequiredMode.AUTO, example = "12345abc") + private String productKey; + + @Schema(description = "产品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "产品分类编号不能为空") + private Long categoryId; + + @Schema(description = "产品图标", example = "https://iocoder.cn/1.svg") + private String icon; + + @Schema(description = "产品图片", example = "https://iocoder.cn/1.png") + private String picUrl; + + @Schema(description = "产品描述", example = "描述") + private String description; + @Schema(description = "设备类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") @InEnum(value = IotProductDeviceTypeEnum.class, message = "设备类型必须是 {value}") @NotNull(message = "设备类型不能为空") @@ -44,7 +57,4 @@ public class IotProductSaveReqVO { @NotNull(message = "数据校验级别不能为空") private Integer validateType; - @Schema(description = "产品描述", example = "描述") - private String description; - } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotDataBridgeController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotDataBridgeController.java new file mode 100644 index 000000000..95e50a4a2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotDataBridgeController.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +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.rule.vo.databridge.IotDataBridgePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.IotDataBridgeRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.IotDataBridgeSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; +import cn.iocoder.yudao.module.iot.service.rule.IotDataBridgeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT 数据桥梁") +@RestController +@RequestMapping("/iot/data-bridge") +@Validated +public class IotDataBridgeController { + + @Resource + private IotDataBridgeService dataBridgeService; + + @PostMapping("/create") + @Operation(summary = "创建数据桥梁") + @PreAuthorize("@ss.hasPermission('iot:data-bridge:create')") + public CommonResult createDataBridge(@Valid @RequestBody IotDataBridgeSaveReqVO createReqVO) { + return success(dataBridgeService.createDataBridge(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新数据桥梁") + @PreAuthorize("@ss.hasPermission('iot:data-bridge:update')") + public CommonResult updateDataBridge(@Valid @RequestBody IotDataBridgeSaveReqVO updateReqVO) { + dataBridgeService.updateDataBridge(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除数据桥梁") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:data-bridge:delete')") + public CommonResult deleteDataBridge(@RequestParam("id") Long id) { + dataBridgeService.deleteDataBridge(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得数据桥梁") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:data-bridge:query')") + public CommonResult getDataBridge(@RequestParam("id") Long id) { + IotDataBridgeDO dataBridge = dataBridgeService.getDataBridge(id); + return success(BeanUtils.toBean(dataBridge, IotDataBridgeRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得数据桥梁分页") + @PreAuthorize("@ss.hasPermission('iot:data-bridge:query')") + public CommonResult> getDataBridgePage(@Valid IotDataBridgePageReqVO pageReqVO) { + PageResult pageResult = dataBridgeService.getDataBridgePage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotDataBridgeRespVO.class)); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotRuleSceneController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotRuleSceneController.java new file mode 100644 index 000000000..04e2f4570 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotRuleSceneController.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule; + +import cn.iocoder.yudao.module.iot.service.rule.IotRuleSceneService; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "管理后台 - IoT 规则场景") +@RestController +@RequestMapping("/iot/rule-scene") +@Validated +public class IotRuleSceneController { + + @Resource + private IotRuleSceneService ruleSceneService; + + @GetMapping("/test") + @PermitAll + public void test() { + ruleSceneService.test(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgePageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgePageReqVO.java new file mode 100644 index 000000000..e4dc36ef9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgePageReqVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - IoT 数据桥梁分页 Request VO") +@Data +public class IotDataBridgePageReqVO extends PageParam { + + @Schema(description = "桥梁名称", example = "赵六") + private String name; + + @Schema(description = "桥梁状态", example = "1") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgeRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgeRespVO.java new file mode 100644 index 000000000..38e04b2eb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgeRespVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge; + +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeAbstractConfig; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT 数据桥梁 Response VO") +@Data +public class IotDataBridgeRespVO { + + @Schema(description = "桥梁编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18564") + private Long id; + + @Schema(description = "桥梁名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") + private String name; + + @Schema(description = "桥梁描述", example = "随便") + private String description; + + @Schema(description = "桥梁状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "桥梁方向", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer direction; + + @Schema(description = "桥梁类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "桥梁配置") + private IotDataBridgeAbstractConfig config; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgeSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgeSaveReqVO.java new file mode 100644 index 000000000..8441701af --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgeSaveReqVO.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeAbstractConfig; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeDirectionEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 数据桥梁新增/修改 Request VO") +@Data +public class IotDataBridgeSaveReqVO { + + @Schema(description = "桥梁编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18564") + private Long id; + + @Schema(description = "桥梁名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") + @NotEmpty(message = "桥梁名称不能为空") + private String name; + + @Schema(description = "桥梁描述", example = "随便") + private String description; + + @Schema(description = "桥梁状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "桥梁状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "桥梁方向", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "桥梁方向不能为空") + @InEnum(IotDataBridgeDirectionEnum.class) + private Integer direction; + + @Schema(description = "桥梁类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "桥梁类型不能为空") + @InEnum(IotDataBridgeTypeEnum.class) + private Integer type; + + @Schema(description = "桥梁配置") + @NotNull(message = "桥梁配置不能为空") + private IotDataBridgeAbstractConfig config; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeAbstractConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeAbstractConfig.java new file mode 100644 index 000000000..527e79b35 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeAbstractConfig.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config; + +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import lombok.Data; + +/** + * IoT IotDataBridgeConfig 抽象类 + * + * 用于表示数据桥梁配置数据的通用类型,根据具体的 "type" 字段动态映射到对应的子类 + * 提供多态支持,适用于不同类型的数据结构序列化和反序列化场景。 + * + * @author HUIHUI + */ +@Data +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = IotDataBridgeHttpConfig.class, name = "1"), + @JsonSubTypes.Type(value = IotDataBridgeMqttConfig.class, name = "10"), + @JsonSubTypes.Type(value = IotDataBridgeRedisStreamMQConfig.class, name = "21"), + @JsonSubTypes.Type(value = IotDataBridgeRocketMQConfig.class, name = "30"), + @JsonSubTypes.Type(value = IotDataBridgeRabbitMQConfig.class, name = "31"), + @JsonSubTypes.Type(value = IotDataBridgeKafkaMQConfig.class, name = "32"), +}) +public abstract class IotDataBridgeAbstractConfig { + + /** + * 配置类型 + * + * 枚举 {@link IotDataBridgeTypeEnum#getType()} + */ + private String type; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeHttpConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeHttpConfig.java new file mode 100644 index 000000000..eca35c76e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeHttpConfig.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config; + +import lombok.Data; + +import java.util.Map; + +/** + * IoT HTTP 配置 {@link IotDataBridgeAbstractConfig} 实现类 + * + * @author HUIHUI + */ +@Data +public class IotDataBridgeHttpConfig extends IotDataBridgeAbstractConfig { + + /** + * 请求 URL + */ + private String url; + /** + * 请求方法 + */ + private String method; + /** + * 请求头 + */ + private Map headers; + /** + * 请求参数 + */ + private Map query; + /** + * 请求体 + */ + private String body; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeKafkaMQConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeKafkaMQConfig.java new file mode 100644 index 000000000..1201214d1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeKafkaMQConfig.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config; + +import lombok.Data; + +/** + * IoT Kafka 配置 {@link IotDataBridgeAbstractConfig} 实现类 + * + * @author HUIHUI + */ +@Data +public class IotDataBridgeKafkaMQConfig extends IotDataBridgeAbstractConfig { + + /** + * Kafka 服务器地址 + */ + private String bootstrapServers; + /** + * 用户名 + */ + private String username; + /** + * 密码 + */ + private String password; + /** + * 是否启用 SSL + */ + private Boolean ssl; + + /** + * 主题 + */ + private String topic; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeMqttConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeMqttConfig.java new file mode 100644 index 000000000..448b21501 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeMqttConfig.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config; + +import lombok.Data; + +/** + * IoT MQTT 配置 {@link IotDataBridgeAbstractConfig} 实现类 + * + * @author HUIHUI + */ +@Data +public class IotDataBridgeMqttConfig extends IotDataBridgeAbstractConfig { + + /** + * MQTT 服务器地址 + */ + private String url; + /** + * 用户名 + */ + private String username; + /** + * 密码 + */ + private String password; + /** + * 客户端编号 + */ + private String clientId; + /** + * 主题 + */ + private String topic; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRabbitMQConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRabbitMQConfig.java new file mode 100644 index 000000000..2c247d1d5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRabbitMQConfig.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config; + +import lombok.Data; + +/** + * IoT RabbitMQ 配置 {@link IotDataBridgeAbstractConfig} 实现类 + * + * @author HUIHUI + */ +@Data +public class IotDataBridgeRabbitMQConfig extends IotDataBridgeAbstractConfig { + + /** + * RabbitMQ 服务器地址 + */ + private String host; + /** + * 端口 + */ + private Integer port; + /** + * 虚拟主机 + */ + private String virtualHost; + /** + * 用户名 + */ + private String username; + /** + * 密码 + */ + private String password; + + /** + * 交换机名称 + */ + private String exchange; + /** + * 路由键 + */ + private String routingKey; + /** + * 队列名称 + */ + private String queue; +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRedisStreamMQConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRedisStreamMQConfig.java new file mode 100644 index 000000000..3c9bb330f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRedisStreamMQConfig.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config; + +import lombok.Data; + +// TODO @puhui999:MQ 可以去掉哈。stream 更精准 +/** + * IoT Redis Stream 配置 {@link IotDataBridgeAbstractConfig} 实现类 + * + * @author HUIHUI + */ +@Data +public class IotDataBridgeRedisStreamMQConfig extends IotDataBridgeAbstractConfig { + + /** + * Redis 服务器地址 + */ + private String host; + /** + * 端口 + */ + private Integer port; + /** + * 密码 + */ + private String password; + /** + * 数据库索引 + */ + private Integer database; + + /** + * 主题 + */ + private String topic; +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRocketMQConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRocketMQConfig.java new file mode 100644 index 000000000..e23e3061a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRocketMQConfig.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config; + +import lombok.Data; + +/** + * IoT RocketMQ 配置 {@link IotDataBridgeAbstractConfig} 实现类 + * + * @author HUIHUI + */ +@Data +public class IotDataBridgeRocketMQConfig extends IotDataBridgeAbstractConfig { + + /** + * RocketMQ 名称服务器地址 + */ + private String nameServer; + /** + * 访问密钥 + */ + private String accessKey; + /** + * 秘密钥匙 + */ + private String secretKey; + + /** + * 生产者组 + */ + private String group; + /** + * 主题 + */ + private String topic; + /** + * 标签 + */ + private String tags; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/package-info.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/package-info.java new file mode 100644 index 000000000..f397e0acd --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/package-info.java @@ -0,0 +1,2 @@ +// TODO @芋艿:占位 +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java new file mode 100644 index 000000000..a9c195656 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.module.iot.controller.admin.statistics; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageSummaryRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsSummaryRespVO; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.data.IotDeviceLogService; +import cn.iocoder.yudao.module.iot.service.product.IotProductCategoryService; +import cn.iocoder.yudao.module.iot.service.product.IotProductService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDateTime; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.*; + +@Tag(name = "管理后台 - IoT 数据统计") +@RestController +@RequestMapping("/iot/statistics") +@Validated +public class IotStatisticsController { + + @Resource + private IotDeviceService deviceService; + @Resource + private IotProductCategoryService productCategoryService; + @Resource + private IotProductService productService; + @Resource + private IotDeviceLogService deviceLogService; + + @GetMapping("/get-summary") + @Operation(summary = "获取 IoT 数据统计") + public CommonResult getIotStatisticsSummary(){ + IotStatisticsSummaryRespVO respVO = new IotStatisticsSummaryRespVO(); + // 1.1 获取总数 + respVO.setProductCategoryCount(productCategoryService.getProductCategoryCount(null)); + respVO.setProductCount(productService.getProductCount(null)); + respVO.setDeviceCount(deviceService.getDeviceCount(null)); + respVO.setDeviceMessageCount(deviceLogService.getDeviceLogCount(null)); + // 1.2 获取今日新增数量 + // TODO @super:使用 LocalDateTimeUtils.getToday() + LocalDateTime todayStart = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0); + respVO.setProductCategoryTodayCount(productCategoryService.getProductCategoryCount(todayStart)); + respVO.setProductTodayCount(productService.getProductCount(todayStart)); + respVO.setDeviceTodayCount(deviceService.getDeviceCount(todayStart)); + respVO.setDeviceMessageTodayCount(deviceLogService.getDeviceLogCount(todayStart)); + + // 2. 获取各个品类下设备数量统计 + respVO.setProductCategoryDeviceCounts(productCategoryService.getProductCategoryDeviceCountMap()); + + // 3. 获取设备状态数量统计 + Map deviceCountMap = deviceService.getDeviceCountMapByState(); + respVO.setDeviceOnlineCount(deviceCountMap.getOrDefault(IotDeviceStateEnum.ONLINE.getState(), 0L)); + respVO.setDeviceOfflineCount(deviceCountMap.getOrDefault(IotDeviceStateEnum.OFFLINE.getState(), 0L)); + respVO.setDeviceInactiveCount(deviceCountMap.getOrDefault(IotDeviceStateEnum.INACTIVE.getState(), 0L)); + return success(respVO); + } + + // TODO @super:要不干掉 IotStatisticsReqVO 参数,直接使用 @RequestParam 接收,简单一些。 + @GetMapping("/get-log-summary") + @Operation(summary = "获取 IoT 设备上下行消息数据统计") + public CommonResult getIotStatisticsDeviceMessageSummary( + @Valid IotStatisticsReqVO reqVO) { + return success(new IotStatisticsDeviceMessageSummaryRespVO() + .setDownstreamCounts(deviceLogService.getDeviceLogUpCountByHour(null, reqVO.getStartTime(), reqVO.getEndTime())) + .setDownstreamCounts((deviceLogService.getDeviceLogDownCountByHour(null, reqVO.getStartTime(), reqVO.getEndTime())))); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsDeviceMessageSummaryRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsDeviceMessageSummaryRespVO.java new file mode 100644 index 000000000..15d2abccc --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsDeviceMessageSummaryRespVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.iot.controller.admin.statistics.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - IoT 设备上下行消息数量统计 Response VO") +@Data +public class IotStatisticsDeviceMessageSummaryRespVO { + + @Schema(description = "每小时上行数据数量统计") + private List> upstreamCounts; + + @Schema(description = "每小时下行数据数量统计") + private List> downstreamCounts; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsReqVO.java new file mode 100644 index 000000000..741f77f3a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.iot.controller.admin.statistics.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 统计 Request VO") +@Data +public class IotStatisticsReqVO { + + // TODO @super:前端传递的时候,还是通过 startTime 和 endTime 传递。后端转成 Long + + @Schema(description = "查询起始时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "1658486600000") + @NotNull(message = "查询起始时间不能为空") + private Long startTime; + + @Schema(description = "查询结束时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "1758486600000") + @NotNull(message = "查询结束时间不能为空") + private Long endTime; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsSummaryRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsSummaryRespVO.java new file mode 100644 index 000000000..21745c4ab --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsSummaryRespVO.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.iot.controller.admin.statistics.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Map; + +/** + * 管理后台 - IoT 统计 Response VO + */ +@Schema(description = "管理后台 - IoT 统计 Response VO") +@Data +public class IotStatisticsSummaryRespVO { + + @Schema(description = "品类数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Long productCategoryCount; + + @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Long productCount; + + @Schema(description = "设备数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Long deviceCount; + + @Schema(description = "上报数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private Long deviceMessageCount; + + @Schema(description = "今日新增品类数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Long productCategoryTodayCount; + + @Schema(description = "今日新增产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Long productTodayCount; + + @Schema(description = "今日新增设备数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Long deviceTodayCount; + + @Schema(description = "今日新增上报数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private Long deviceMessageTodayCount; + + @Schema(description = "在线数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "80") + private Long deviceOnlineCount; + + @Schema(description = "离线数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "15") + private Long deviceOfflineCount; + + @Schema(description = "待激活设备数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + private Long deviceInactiveCount; + + @Schema(description = "按品类统计的设备数量") + private Map productCategoryDeviceCounts; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.http b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.http new file mode 100644 index 000000000..1e1f72103 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.http @@ -0,0 +1,181 @@ +### 请求 /iot/product-thing-model/create 接口 => 成功 +POST {{baseUrl}}/iot/product-thing-model/create +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +{ + "productId": 12, + "productKey": "CJVS54fObwZJ9Qe5CJVS54fObwZJ9Qe5", + "identifier": "Temperature", + "name": "温度", + "description": "当前温度值", + "type": 1, + "property": { + "identifier": "Temperature", + "name": "温度", + "accessMode": "r", + "required": true, + "dataType": "int", + "dataSpecs": { + "dataType": "int", + "max": "200", + "min": "0", + "step": "10", + "defaultValue": "30", + "unit": "%", + "unitName": "百分比" + } + } +} + +### 请求 /iot/product-thing-model/create 接口 => 成功 +POST {{baseUrl}}/iot/product-thing-model/create +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +{ + "productId": 12, + "productKey": "CJVS54fObwZJ9Qe5CJVS54fObwZJ9Qe5", + "identifier": "switch", + "name": "开关", + "description": "温度计开关", + "type": 1, + "property": { + "identifier": "switch", + "name": "开关", + "accessMode": "rw", + "required": true, + "dataType": "bool", + "dataSpecsList": [ + { + "dataType": "bool", + "name": "关", + "value": 0 + }, + { + "dataType": "bool", + "name": "开", + "value": 1 + } + ] + } +} + +### 请求 /iot/product-thing-model/create 接口 => 成功 +POST {{baseUrl}}/iot/product-thing-model/create +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +{ + "productId": 12, + "productKey": "CJVS54fObwZJ9Qe5CJVS54fObwZJ9Qe5", + "identifier": "argb", + "name": "温度计 argb 颜色", + "description": "温度计 argb 颜色", + "type": 1, + "property": { + "identifier": "argb", + "name": "温度计 argb 颜色", + "accessMode": "rw", + "required": true, + "dataType": "array", + "dataSpecs": { + "dataType": "array", + "size": 10, + "childDataType": "struct", + "dataSpecsList": [ + { + "identifier": "switch", + "name": "开关", + "accessMode": "rw", + "required": true, + "dataType": "struct", + "childDataType": "bool", + "dataSpecsList": [ + { + "dataType": "bool", + "name": "关", + "value": 0 + }, + { + "dataType": "bool", + "name": "开", + "value": 1 + } + ] + }, + { + "identifier": "Temperature", + "name": "温度", + "accessMode": "r", + "required": true, + "dataType": "struct", + "childDataType": "int", + "dataSpecs": { + "dataType": "int", + "max": "200", + "min": "0", + "step": "10", + "defaultValue": "30", + "unit": "%", + "unitName": "百分比" + } + } + ] + } + } +} + +### 请求 /iot/product-thing-model/update 接口 => 成功 +PUT {{baseUrl}}/iot/product-thing-model/update +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +{ + "id": 33, + "productId": 12, + "productKey": "CJVS54fObwZJ9Qe5CJVS54fObwZJ9Qe5", + "identifier": "switch", + "name": "开关", + "description": "温度计开关", + "type": 1, + "property": { + "identifier": "switch", + "name": "开关", + "accessMode": "r", + "required": true, + "dataType": "bool", + "dataSpecsList": [ + { + "dataType": "bool", + "name": "关", + "value": 0 + }, + { + "dataType": "bool", + "name": "开", + "value": 1 + } + ] + } +} + +### 请求 /iot/product-thing-model/delete 接口 => 成功 +DELETE {{baseUrl}}/iot/product-thing-model/delete?id=36 +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +### 请求 /iot/product-thing-model/get 接口 => 成功 +GET {{baseUrl}}/iot/product-thing-model/get?id=67 +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + + +### 请求 /iot/product-thing-model/list-by-product-id 接口 => 成功 +GET {{baseUrl}}/iot/product-thing-model/list-by-product-id?productId=1001 +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.java new file mode 100644 index 000000000..382940fc4 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.java @@ -0,0 +1,93 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +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.thingmodel.vo.IotThingModelListReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT 产品物模型") +@RestController +@RequestMapping("/iot/thing-model") +@Validated +public class IotThingModelController { + + @Resource + private IotThingModelService thingModelService; + + @PostMapping("/create") + @Operation(summary = "创建产品物模型") + @PreAuthorize("@ss.hasPermission('iot:thing-model:create')") + public CommonResult createThingModel(@Valid @RequestBody IotThingModelSaveReqVO createReqVO) { + return success(thingModelService.createThingModel(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新产品物模型") + @PreAuthorize("@ss.hasPermission('iot:thing-model:update')") + public CommonResult updateThingModel(@Valid @RequestBody IotThingModelSaveReqVO updateReqVO) { + thingModelService.updateThingModel(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除产品物模型") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:thing-model:delete')") + public CommonResult deleteThingModel(@RequestParam("id") Long id) { + thingModelService.deleteThingModel(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得产品物模型") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:thing-model:query')") + public CommonResult getThingModel(@RequestParam("id") Long id) { + IotThingModelDO thingModel = thingModelService.getThingModel(id); + return success(BeanUtils.toBean(thingModel, IotThingModelRespVO.class)); + } + + @GetMapping("/list-by-product-id") + @Operation(summary = "获得产品物模型") + @Parameter(name = "productId", description = "产品ID", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:thing-model:query')") + public CommonResult> getThingModelListByProductId(@RequestParam("productId") Long productId) { + List list = thingModelService.getThingModelListByProductId(productId); + return success(BeanUtils.toBean(list, IotThingModelRespVO.class)); + } + + // TODO @puhui @super:getThingModelListByProductId 和 getThingModelListByProductId 可以融合么? + @GetMapping("/list") + @Operation(summary = "获得产品物模型列表") + @PreAuthorize("@ss.hasPermission('iot:thing-model:query')") + public CommonResult> getThingModelListByProductId(@Valid IotThingModelListReqVO reqVO) { + List list = thingModelService.getThingModelList(reqVO); + return success(BeanUtils.toBean(list, IotThingModelRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得产品物模型分页") + @PreAuthorize("@ss.hasPermission('iot:thing-model:query')") + public CommonResult> getThingModelPage(@Valid IotThingModelPageReqVO pageReqVO) { + PageResult pageResult = thingModelService.getProductThingModelPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotThingModelRespVO.class)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelEvent.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelEvent.java new file mode 100644 index 000000000..06cc43809 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelEvent.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelServiceEventTypeEnum; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +import java.util.List; + +/** + * IoT 物模型中的事件 + * + * @author HUIHUI + */ +@Data +public class ThingModelEvent { + + /** + * 事件标识符 + */ + @NotEmpty(message = "事件标识符不能为空") + @Pattern(regexp = "^[a-zA-Z][a-zA-Z0-9_]{0,31}$", message = "事件标识符只能由字母、数字和下划线组成,必须以字母开头,长度不超过 32 个字符") + private String identifier; + /** + * 事件名称 + */ + @NotEmpty(message = "事件名称不能为空") + private String name; + /** + * 是否是标准品类的必选事件 + */ + private Boolean required; + /** + * 事件类型 + * + * 枚举 {@link IotThingModelServiceEventTypeEnum} + */ + @NotEmpty(message = "事件类型不能为空") + @InEnum(IotThingModelServiceEventTypeEnum.class) + private String type; + /** + * 事件的输出参数 + * + * 输出参数定义事件调用后返回的结果或反馈信息,用于确认操作结果或提供额外的信息。 + */ + @Valid + private List outputParams; + /** + * 标识设备需要执行的具体操作 + */ + private String method; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelParam.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelParam.java new file mode 100644 index 000000000..2afad898b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelParam.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType.ThingModelDataSpecs; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelParamDirectionEnum; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +import java.util.List; + +/** + * IoT 产品物模型中的参数 + * + * @author HUIHUI + */ +@Data +public class ThingModelParam { + + /** + * 参数标识符 + */ + @NotEmpty(message = "参数标识符不能为空") + @Pattern(regexp = "^[a-zA-Z][a-zA-Z0-9_]{0,31}$", message = "参数标识符只能由字母、数字和下划线组成,必须以字母开头,长度不超过 32 个字符") + private String identifier; + /** + * 参数名称 + */ + @NotEmpty(message = "参数名称不能为空") + private String name; + /** + * 用于区分输入或输出参数 + * + * 枚举 {@link IotThingModelParamDirectionEnum} + */ + @NotEmpty(message = "参数方向不能为空") + @InEnum(IotThingModelParamDirectionEnum.class) + private String direction; + /** + * 参数的序号。从 0 开始排序,且不能重复。 + * + * TODO 考虑要不要序号,感觉是要的, 先留一手看看 + */ + private Integer paraOrder; + /** + * 参数值的数据类型,与 dataSpecs 的 dataType 保持一致 + * + * 枚举 {@link IotDataSpecsDataTypeEnum} + */ + @NotEmpty(message = "数据类型不能为空") + @InEnum(IotDataSpecsDataTypeEnum.class) + private String dataType; + /** + * 参数值的数据类型(dataType)为非列表型(int、float、double、text、date、array)的数据规范存储在 dataSpecs 中 + */ + private ThingModelDataSpecs dataSpecs; + /** + * 参数值的数据类型(dataType)为列表型(enum、bool、struct)的数据规范存储在 dataSpecsList 中 + */ + private List dataSpecsList; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelProperty.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelProperty.java new file mode 100644 index 000000000..4b9a05a0e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelProperty.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType.ThingModelDataSpecs; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelAccessModeEnum; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +import java.util.List; + +/** + * IoT 物模型中的属性 + * + * dataSpecs 和 dataSpecsList 之中必须传入且只能传入一个 + * + * @author HUIHUI + */ +@Data +public class ThingModelProperty { + + /** + * 属性标识符 + */ + @NotEmpty(message = "属性标识符不能为空") + @Pattern(regexp = "^[a-zA-Z][a-zA-Z0-9_]{0,31}$", message = "属性标识符只能由字母、数字和下划线组成,必须以字母开头,长度不超过 32 个字符") + private String identifier; + /** + * 属性名称 + */ + @NotEmpty(message = "属性名称不能为空") + private String name; + /** + * 云端可以对该属性进行的操作类型 + * + * 枚举 {@link IotThingModelAccessModeEnum} + */ + @NotEmpty(message = "操作类型不能为空") + @InEnum(IotThingModelAccessModeEnum.class) + private String accessMode; + /** + * 是否是标准品类的必选服务 + */ + private Boolean required; + /** + * 参数值的数据类型,与 dataSpecs 的 dataType 保持一致 + * + * 枚举 {@link IotDataSpecsDataTypeEnum} + */ + @NotEmpty(message = "数据类型不能为空") + @InEnum(IotDataSpecsDataTypeEnum.class) + private String dataType; + /** + * 数据类型(dataType)为非列表型(int、float、double、text、date、array)的数据规范存储在 dataSpecs 中 + */ + private ThingModelDataSpecs dataSpecs; + /** + * 数据类型(dataType)为列表型(enum、bool、struct)的数据规范存储在 dataSpecsList 中 + */ + private List dataSpecsList; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelService.java new file mode 100644 index 000000000..c98acd824 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelService.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelServiceCallTypeEnum; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +import java.util.List; + +/** + * IoT 物模型中的服务 + * + * @author HUIHUI + */ +@Data +public class ThingModelService { + + /** + * 服务标识符 + */ + @NotEmpty(message = "服务标识符不能为空") + @Pattern(regexp = "^[a-zA-Z][a-zA-Z0-9_]{0,31}$", message = "服务标识符只能由字母、数字和下划线组成,必须以字母开头,长度不超过 32 个字符") + private String identifier; + /** + * 服务名称 + */ + @NotEmpty(message = "服务名称不能为空") + private String name; + /** + * 是否是标准品类的必选服务 + */ + private Boolean required; + /** + * 调用类型 + * + * 枚举 {@link IotThingModelServiceCallTypeEnum} + */ + @NotEmpty(message = "调用类型不能为空") + @InEnum(IotThingModelServiceCallTypeEnum.class) + private String callType; + /** + * 服务的输入参数 + * + * 输入参数定义服务调用时所需提供的信息,用于控制设备行为或执行特定任务 + */ + @Valid + private List inputParams; + /** + * 服务的输出参数 + * + * 输出参数定义服务调用后返回的结果或反馈信息,用于确认操作结果或提供额外的信息。 + */ + @Valid + private List outputParams; + /** + * 标识设备需要执行的具体操作 + */ + private String method; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelArrayDataSpecs.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelArrayDataSpecs.java new file mode 100644 index 000000000..50011aabf --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelArrayDataSpecs.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * IoT 物模型数据类型为数组的 DataSpec 定义 + * + * @author HUIHUI + */ +@Data +@EqualsAndHashCode(callSuper = true) +@JsonIgnoreProperties({"dataType"}) // 忽略子类中的 dataType 字段,从而避免重复 +public class ThingModelArrayDataSpecs extends ThingModelDataSpecs { + + /** + * 数组中的元素个数 + */ + private Integer size; + /** + * 数组中的元素的数据类型。可选值:struct、int、float、double 或 text + */ + private String childDataType; + /** + * 数据类型(childDataType)为列表型 struct 的数据规范存储在 dataSpecsList 中 + * 此时 struct 取值范围为:int、float、double、text、date、enum、bool + */ + private List dataSpecsList; + +} + diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelBoolOrEnumDataSpecs.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelBoolOrEnumDataSpecs.java new file mode 100644 index 000000000..925bc6719 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelBoolOrEnumDataSpecs.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * IoT 物模型数据类型为布尔型或枚举型的 DataSpec 定义 + * + * 数据类型,取值为 bool 或 enum。 + * + * @author HUIHUI + */ +@Data +@EqualsAndHashCode(callSuper = true) +@JsonIgnoreProperties({"dataType"}) // 忽略子类中的 dataType 字段,从而避免重复 +public class ThingModelBoolOrEnumDataSpecs extends ThingModelDataSpecs { + + // TODO @puhui999:要不写下参数校验?这样,注释可以简洁一点 + /** + * 枚举项的名称。 + * 可包含中文、大小写英文字母、数字、下划线(_)和短划线(-) + * 必须以中文、英文字母或数字开头,长度不超过 20 个字符 + */ + private String name; + /** + * 枚举值。 + */ + private Integer value; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelDataSpecs.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelDataSpecs.java new file mode 100644 index 000000000..d9fc12dd9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelDataSpecs.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import lombok.Data; + +/** + * IoT ThingModelDataSpecs 抽象类 + * + * 用于表示物模型数据的通用类型,根据具体的 "dataType" 字段动态映射到对应的子类。 + * 提供多态支持,适用于不同类型的数据结构序列化和反序列化场景。 + * + * @author HUIHUI + */ +@Data +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "dataType", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = ThingModelNumericDataSpec.class, name = "int"), + @JsonSubTypes.Type(value = ThingModelNumericDataSpec.class, name = "float"), + @JsonSubTypes.Type(value = ThingModelNumericDataSpec.class, name = "double"), + @JsonSubTypes.Type(value = ThingModelDateOrTextDataSpecs.class, name = "text"), + @JsonSubTypes.Type(value = ThingModelDateOrTextDataSpecs.class, name = "date"), + @JsonSubTypes.Type(value = ThingModelBoolOrEnumDataSpecs.class, name = "bool"), + @JsonSubTypes.Type(value = ThingModelBoolOrEnumDataSpecs.class, name = "enum"), + @JsonSubTypes.Type(value = ThingModelArrayDataSpecs.class, name = "array"), + @JsonSubTypes.Type(value = ThingModelStructDataSpecs.class, name = "struct") +}) +public abstract class ThingModelDataSpecs { + + /** + * 数据类型 + */ + private String dataType; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelDateOrTextDataSpecs.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelDateOrTextDataSpecs.java new file mode 100644 index 000000000..62500bc56 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelDateOrTextDataSpecs.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * IoT 物模型数据类型为时间型或文本型的 DataSpec 定义 + * + * 数据类型,取值为 date 或 text。 + * + * @author HUIHUI + */ +@Data +@EqualsAndHashCode(callSuper = true) +@JsonIgnoreProperties({"dataType"}) // 忽略子类中的 dataType 字段,从而避免重复 +public class ThingModelDateOrTextDataSpecs extends ThingModelDataSpecs { + + /** + * 数据长度,单位为字节。取值不能超过 2048。 + * 当 dataType 为 text 时,需传入该参数。 + */ + private Integer length; + /** + * 默认值,可选参数,用于存储默认值。 + */ + private String defaultValue; + +} + diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelNumericDataSpec.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelNumericDataSpec.java new file mode 100644 index 000000000..8d0827c01 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelNumericDataSpec.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * IoT 物模型数据类型为数值的 DataSpec 定义 + * + * 数据类型,取值为 int、float 或 double。 + * + * @author HUIHUI + */ +@Data +@EqualsAndHashCode(callSuper = true) +@JsonIgnoreProperties({"dataType"}) // 忽略子类中的 dataType 字段,从而避免重复 +public class ThingModelNumericDataSpec extends ThingModelDataSpecs { + + /** + * 最大值,需转为字符串类型。值必须与 dataType 类型一致。 + * 例如,当 dataType 为 int 时,取值为 "200",而不是 200。 + */ + private String max; + /** + * 最小值,需转为字符串类型。值必须与 dataType 类型一致。 + * 例如,当 dataType 为 int 时,取值为 "0",而不是 0。 + */ + private String min; + /** + * 步长,需转为字符串类型。值必须与 dataType 类型一致。 + * 例如,当 dataType 为 int 时,取值为 "10",而不是 10。 + */ + private String step; + /** + * 精度。当 dataType 为 float 或 double 时可选传入。 + */ + private String precise; + /** + * 默认值,可传入用于存储的默认值。 + */ + private String defaultValue; + /** + * 单位的符号。 + */ + private String unit; + /** + * 单位的名称。 + */ + private String unitName; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelStructDataSpecs.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelStructDataSpecs.java new file mode 100644 index 000000000..6d483eeaa --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelStructDataSpecs.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType; + +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelAccessModeEnum; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * IoT 物模型数据类型为 struct 的 DataSpec 定义 + * + * @author HUIHUI + */ +@Data +@EqualsAndHashCode(callSuper = true) +@JsonIgnoreProperties({"dataType"}) // 忽略子类中的 dataType 字段,从而避免重复 +public class ThingModelStructDataSpecs extends ThingModelDataSpecs { + + /** + * 属性标识符 + */ + private String identifier; + /** + * 属性名称 + */ + private String name; + /** + * 云端可以对该属性进行的操作类型 + * + * 枚举 {@link IotThingModelAccessModeEnum} + */ + private String accessMode; + /** + * 是否是标准品类的必选服务 + */ + private Boolean required; + /** + * struct 数据的数据类型 + */ + private String childDataType; + /** + * 数据类型(dataType)为非列表型(int、float、double、text、date、array)的数据规范存储在 dataSpecs 中 + */ + private ThingModelDataSpecs dataSpecs; + /** + * 数据类型(dataType)为列表型(enum、bool、struct)的数据规范存储在 dataSpecsList 中 + */ + private List dataSpecsList; + +} + diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelListReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelListReqVO.java new file mode 100644 index 000000000..5b92256bb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelListReqVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 产品物模型 List Request VO") +@Data +public class IotThingModelListReqVO { + + @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "产品编号不能为空") + private Long productId; + + @Schema(description = "功能标识", example = "temperature") + private String identifier; + + @Schema(description = "功能名称", example = "温度") + private String name; + + @Schema(description = "功能类型", example = "1") + @InEnum(IotThingModelTypeEnum.class) + private Integer type; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelPageReqVO.java new file mode 100644 index 000000000..8064b10e5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelPageReqVO.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 产品物模型分页 Request VO") +@Data +public class IotThingModelPageReqVO extends PageParam { + + @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "产品编号不能为空") + private Long productId; + + @Schema(description = "功能标识", example = "temperature") + private String identifier; + + @Schema(description = "功能名称", example = "温度") + private String name; + + @Schema(description = "功能类型", example = "1") + @InEnum(IotThingModelTypeEnum.class) + private Integer type; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/vo/IotThinkModelFunctionRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelRespVO.java similarity index 66% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/vo/IotThinkModelFunctionRespVO.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelRespVO.java index 9ef3f58d8..15a5b9f95 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/vo/IotThinkModelFunctionRespVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelRespVO.java @@ -1,42 +1,41 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo; +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelEvent; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelProperty; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelService; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelEvent; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelProperty; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelService; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; -import java.util.List; @Schema(description = "管理后台 - IoT 产品物模型 Response VO") @Data @ExcelIgnoreUnannotated -public class IotThinkModelFunctionRespVO { +public class IotThingModelRespVO { - @Schema(description = "产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "21816") + @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "21816") @ExcelProperty("产品ID") private Long id; - @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long productId; - @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "temperature_sensor") @ExcelProperty("产品标识") private String productKey; - @Schema(description = "功能标识", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "功能标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "temperature") private String identifier; - @Schema(description = "功能名称", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "功能名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "温度") private String name; - @Schema(description = "功能描述", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "功能描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "测量当前环境温度") private String description; - @Schema(description = "功能类型", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "功能类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer type; @Schema(description = "属性", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/vo/IotThinkModelFunctionSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelSaveReqVO.java similarity index 66% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/vo/IotThinkModelFunctionSaveReqVO.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelSaveReqVO.java index 7d51ce504..1e8564df4 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/vo/IotThinkModelFunctionSaveReqVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelSaveReqVO.java @@ -1,18 +1,19 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo; +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo; import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelEvent; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelProperty; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelService; -import cn.iocoder.yudao.module.iot.enums.product.IotProductFunctionTypeEnum; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelEvent; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelProperty; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelService; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; @Schema(description = "管理后台 - IoT 产品物模型新增/修改 Request VO") @Data -public class IotThinkModelFunctionSaveReqVO { +public class IotThingModelSaveReqVO { @Schema(description = "编号", example = "1") private Long id; @@ -21,33 +22,36 @@ public class IotThinkModelFunctionSaveReqVO { @NotNull(message = "产品ID不能为空") private Long productId; - @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "temperature_001") @NotEmpty(message = "产品标识不能为空") private String productKey; - @Schema(description = "功能标识", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "功能标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "temp_monitor") @NotEmpty(message = "功能标识不能为空") private String identifier; - @Schema(description = "功能名称", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "功能名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "温度监测器") @NotEmpty(message = "功能名称不能为空") private String name; - @Schema(description = "功能描述", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "功能描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "用于监测环境温度的传感器") private String description; - @Schema(description = "功能类型", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "功能类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "功能类型不能为空") - @InEnum(IotProductFunctionTypeEnum.class) + @InEnum(IotThingModelTypeEnum.class) private Integer type; @Schema(description = "属性", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid private ThingModelProperty property; @Schema(description = "服务", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid private ThingModelService service; @Schema(description = "事件", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid private ThingModelEvent event; } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/IotThinkModelFunctionController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/IotThinkModelFunctionController.java deleted file mode 100644 index 4f48f3628..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/IotThinkModelFunctionController.java +++ /dev/null @@ -1,84 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -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.thinkmodelfunction.vo.IotThinkModelFunctionPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo.IotThinkModelFunctionRespVO; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo.IotThinkModelFunctionSaveReqVO; -import cn.iocoder.yudao.module.iot.convert.thinkmodelfunction.IotThinkModelFunctionConvert; -import cn.iocoder.yudao.module.iot.dal.dataobject.thinkmodelfunction.IotThinkModelFunctionDO; -import cn.iocoder.yudao.module.iot.service.thinkmodelfunction.IotThinkModelFunctionService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.validation.Valid; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; - -@Tag(name = "管理后台 - IoT 产品物模型") -@RestController -@RequestMapping("/iot/think-model-function") -@Validated -public class IotThinkModelFunctionController { - - @Resource - private IotThinkModelFunctionService thinkModelFunctionService; - - @PostMapping("/create") - @Operation(summary = "创建产品物模型") - @PreAuthorize("@ss.hasPermission('iot:think-model-function:create')") - public CommonResult createThinkModelFunction(@Valid @RequestBody IotThinkModelFunctionSaveReqVO createReqVO) { - return success(thinkModelFunctionService.createThinkModelFunction(createReqVO)); - } - - @PutMapping("/update") - @Operation(summary = "更新产品物模型") - @PreAuthorize("@ss.hasPermission('iot:think-model-function:update')") - public CommonResult updateThinkModelFunction(@Valid @RequestBody IotThinkModelFunctionSaveReqVO updateReqVO) { - thinkModelFunctionService.updateThinkModelFunction(updateReqVO); - return success(true); - } - - @DeleteMapping("/delete") - @Operation(summary = "删除产品物模型") - @Parameter(name = "id", description = "编号", required = true) - @PreAuthorize("@ss.hasPermission('iot:think-model-function:delete')") - public CommonResult deleteThinkModelFunction(@RequestParam("id") Long id) { - thinkModelFunctionService.deleteThinkModelFunction(id); - return success(true); - } - - @GetMapping("/get") - @Operation(summary = "获得产品物模型") - @Parameter(name = "id", description = "编号", required = true) - @PreAuthorize("@ss.hasPermission('iot:think-model-function:query')") - public CommonResult getThinkModelFunction(@RequestParam("id") Long id) { - IotThinkModelFunctionDO function = thinkModelFunctionService.getThinkModelFunction(id); - return success(IotThinkModelFunctionConvert.INSTANCE.convert(function)); - } - - @GetMapping("/list-by-product-id") - @Operation(summary = "获得产品物模型") - @Parameter(name = "productId", description = "产品ID", required = true, example = "1024") - @PreAuthorize("@ss.hasPermission('iot:think-model-function:query')") - public CommonResult> getThinkModelFunctionListByProductId(@RequestParam("productId") Long productId) { - List list = thinkModelFunctionService.getThinkModelFunctionListByProductId(productId); - return success(IotThinkModelFunctionConvert.INSTANCE.convertList(list)); - } - - @GetMapping("/page") - @Operation(summary = "获得产品物模型分页") - @PreAuthorize("@ss.hasPermission('iot:think-model-function:query')") - public CommonResult> getThinkModelFunctionPage(@Valid IotThinkModelFunctionPageReqVO pageReqVO) { - PageResult pageResult = thinkModelFunctionService.getThinkModelFunctionPage(pageReqVO); - return success(BeanUtils.toBean(pageResult, IotThinkModelFunctionRespVO.class)); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/ThingModelEvent.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/ThingModelEvent.java deleted file mode 100644 index d7fa68758..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/ThingModelEvent.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel; - -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType.ThingModelArgument; -import lombok.Data; -import java.util.List; - -@Data -public class ThingModelEvent { - - /** - * 事件标识符 - */ - private String identifier; - /** - * 事件名称 - */ - private String name; - /** - * 事件描述 - */ - private String description; - - /** - * 事件类型 - * - * "info"、"alert"、"error" - */ - private String type; - private List outputData; - private String method; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/ThingModelProperty.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/ThingModelProperty.java deleted file mode 100644 index 025d37e76..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/ThingModelProperty.java +++ /dev/null @@ -1,27 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel; - -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType.ThingModelDataType; -import lombok.Data; - -@Data -public class ThingModelProperty { - - /** - * 属性标识符 - */ - private String identifier; - /** - * 属性名称 - */ - private String name; - /** - * 属性描述 - */ - private String description; - - private String accessMode; // "rw"、"r"、"w" - private Boolean required; - // TODO @haohao:这个是不是 dataSpecs 和 dataSpecsList?https://help.aliyun.com/zh/iot/developer-reference/api-a99t11 - private ThingModelDataType dataType; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/ThingModelService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/ThingModelService.java deleted file mode 100644 index d97e05e9c..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/ThingModelService.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel; - -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType.ThingModelArgument; -import lombok.Data; -import java.util.List; - -@Data -public class ThingModelService { - - /** - * 服务标识符 - */ - private String identifier; - /** - * 服务名称 - */ - private String name; - /** - * 服务描述 - */ - private String description; - - /** - * 调用类型 - * - * "sync"、"async" - */ - private String callType; - private List inputData; - private List outputData; - private String method; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelArgument.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelArgument.java deleted file mode 100644 index 2be24004e..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelArgument.java +++ /dev/null @@ -1,17 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; - -@Data -public class ThingModelArgument { - - private String identifier; - private String name; - private ThingModelDataType dataType; - /** - * 用于区分输入或输出参数,"input" 或 "output" - */ - private String direction; - private String description; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelArraySpecs.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelArraySpecs.java deleted file mode 100644 index c3faf6161..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelArraySpecs.java +++ /dev/null @@ -1,17 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; - -@Data -public class ThingModelArraySpecs { - - /** - * 数组长度 - */ - private int size; - /** - * 数组元素的类型 - */ - private ThingModelDataType item; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelArrayType.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelArrayType.java deleted file mode 100644 index bab87be0a..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelArrayType.java +++ /dev/null @@ -1,12 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; - -// TODO @haohao:这个是不是和别的类,不太统一哈 -@Data -public class ThingModelArrayType extends ThingModelDataType { - - private ThingModelArraySpecs specs; - -} - diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelBoolType.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelBoolType.java deleted file mode 100644 index b8ca64195..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelBoolType.java +++ /dev/null @@ -1,12 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; -import lombok.EqualsAndHashCode; - -@Data -@EqualsAndHashCode(callSuper = true) -public class ThingModelBoolType extends ThingModelDataType { - - // Bool 类型一般不需要额外的 specs - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelDataType.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelDataType.java deleted file mode 100644 index ec5f04bbb..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelDataType.java +++ /dev/null @@ -1,24 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import lombok.Data; - -@Data -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true) -@JsonSubTypes({ - @JsonSubTypes.Type(value = ThingModelIntType.class, name = "int"), - @JsonSubTypes.Type(value = ThingModelFloatType.class, name = "float"), - @JsonSubTypes.Type(value = ThingModelDoubleType.class, name = "double"), - @JsonSubTypes.Type(value = ThingModelTextType.class, name = "text"), - @JsonSubTypes.Type(value = ThingModelDateType.class, name = "date"), - @JsonSubTypes.Type(value = ThingModelBoolType.class, name = "bool"), - @JsonSubTypes.Type(value = ThingModelEnumType.class, name = "enum"), - @JsonSubTypes.Type(value = ThingModelStructType.class, name = "struct"), - @JsonSubTypes.Type(value = ThingModelArrayType.class, name = "array") -}) -public abstract class ThingModelDataType { - - private String type; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelDateType.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelDateType.java deleted file mode 100644 index 854229339..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelDateType.java +++ /dev/null @@ -1,10 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; - -@Data -public class ThingModelDateType extends ThingModelDataType { - - // Date 类型一般不需要额外的 specs - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelDoubleType.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelDoubleType.java deleted file mode 100644 index e5f3ad268..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelDoubleType.java +++ /dev/null @@ -1,18 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; - -@Data -public class ThingModelDoubleType extends ThingModelDataType { - private ThingModelDoubleSpecs specs; -} - -@Data -class ThingModelDoubleSpecs { - - private Double min; - private Double max; - private Double step; - private String unit; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelEnumType.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelEnumType.java deleted file mode 100644 index 3dcb068e9..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelEnumType.java +++ /dev/null @@ -1,15 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; - -import java.util.Map; - -@Data -public class ThingModelEnumType extends ThingModelDataType { - - /** - * 枚举值和描述的键值对 - */ - private Map specs; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelFloatType.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelFloatType.java deleted file mode 100644 index 27926fa49..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelFloatType.java +++ /dev/null @@ -1,20 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; -import lombok.EqualsAndHashCode; - -@Data -@EqualsAndHashCode(callSuper = true) -public class ThingModelFloatType extends ThingModelDataType { - private ThingModelFloatSpecs specs; -} - -@Data -class ThingModelFloatSpecs { - - private Float min; - private Float max; - private Float step; - private String unit; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelIntType.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelIntType.java deleted file mode 100644 index a126eb749..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelIntType.java +++ /dev/null @@ -1,18 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; - -@Data -public class ThingModelIntType extends ThingModelDataType { - private ThingModelIntSpecs specs; -} - -@Data -class ThingModelIntSpecs { - - private Integer min; - private Integer max; - private Integer step; - private String unit; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelStructField.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelStructField.java deleted file mode 100644 index 5e079f22b..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelStructField.java +++ /dev/null @@ -1,13 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; - -@Data -public class ThingModelStructField { - - private String identifier; - private String name; - private ThingModelDataType dataType; - private String description; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelStructType.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelStructType.java deleted file mode 100644 index f0996513c..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelStructType.java +++ /dev/null @@ -1,14 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; - -import java.util.List; - -@Data -public class ThingModelStructType extends ThingModelDataType { - - private List specs; - -} - - diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelTextType.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelTextType.java deleted file mode 100644 index 16d1e402e..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelTextType.java +++ /dev/null @@ -1,20 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; - -@Data -public class ThingModelTextType extends ThingModelDataType { - - private ThingModelTextSpecs specs; - -} - -@Data -class ThingModelTextSpecs { - - /** - * 最大长度 - */ - private Integer length; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/vo/IotThinkModelFunctionPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/vo/IotThinkModelFunctionPageReqVO.java deleted file mode 100644 index 8a590d429..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/vo/IotThinkModelFunctionPageReqVO.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo; - -import cn.iocoder.yudao.framework.common.pojo.PageParam; -import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.enums.product.IotProductFunctionTypeEnum; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.ToString; - -@Schema(description = "管理后台 - IoT 产品物模型分页 Request VO") -@Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) -public class IotThinkModelFunctionPageReqVO extends PageParam { - - @Schema(description = "功能标识") - private String identifier; - - @Schema(description = "功能名称", example = "张三") - private String name; - - @Schema(description = "功能类型", example = "1") - @InEnum(IotProductFunctionTypeEnum.class) - private Integer type; - - @Schema(description = "产品ID", requiredMode = Schema.RequiredMode.REQUIRED) - @NotNull(message = "产品ID不能为空") - private Long productId; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/package-info.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/package-info.java index c196c25c3..18d7ad21d 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/package-info.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/package-info.java @@ -1 +1,6 @@ +/** + * 提供 POJO 类的实体转换 + * + * 目前使用 MapStruct 框架 + */ package cn.iocoder.yudao.module.iot.convert; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/thingmodel/IotThingModelConvert.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/thingmodel/IotThingModelConvert.java new file mode 100644 index 000000000..9577b18f7 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/thingmodel/IotThingModelConvert.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.iot.convert.thingmodel; + +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelEvent; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelProperty; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelService; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +import java.util.Objects; + +@Mapper +public interface IotThingModelConvert { + + IotThingModelConvert INSTANCE = Mappers.getMapper(IotThingModelConvert.class); + + @Mapping(target = "property", expression = "java(convertToProperty(bean))") + @Mapping(target = "event", expression = "java(convertToEvent(bean))") + @Mapping(target = "service", expression = "java(convertToService(bean))") + IotThingModelDO convert(IotThingModelSaveReqVO bean); + + @Named("convertToProperty") + default ThingModelProperty convertToProperty(IotThingModelSaveReqVO bean) { + if (Objects.equals(bean.getType(), IotThingModelTypeEnum.PROPERTY.getType())) { + return bean.getProperty(); + } + return null; + } + + @Named("convertToEvent") + default ThingModelEvent convertToEvent(IotThingModelSaveReqVO bean) { + if (Objects.equals(bean.getType(), IotThingModelTypeEnum.EVENT.getType())) { + return bean.getEvent(); + } + return null; + } + + @Named("convertToService") + default ThingModelService convertToService(IotThingModelSaveReqVO bean) { + if (Objects.equals(bean.getType(), IotThingModelTypeEnum.SERVICE.getType())) { + return bean.getService(); + } + return null; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/thinkmodelfunction/IotThinkModelFunctionConvert.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/thinkmodelfunction/IotThinkModelFunctionConvert.java deleted file mode 100644 index 764d4c030..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/thinkmodelfunction/IotThinkModelFunctionConvert.java +++ /dev/null @@ -1,57 +0,0 @@ -package cn.iocoder.yudao.module.iot.convert.thinkmodelfunction; - -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelEvent; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelProperty; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelService; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo.IotThinkModelFunctionRespVO; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo.IotThinkModelFunctionSaveReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.thinkmodelfunction.IotThinkModelFunctionDO; -import cn.iocoder.yudao.module.iot.enums.product.IotProductFunctionTypeEnum; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; -import org.mapstruct.factory.Mappers; - -import java.util.List; -import java.util.Objects; - -@Mapper -public interface IotThinkModelFunctionConvert { - - IotThinkModelFunctionConvert INSTANCE = Mappers.getMapper(IotThinkModelFunctionConvert.class); - - // 将 SaveReqVO 转换为 DO - @Mapping(target = "property", expression = "java(convertToProperty(bean))") - @Mapping(target = "event", expression = "java(convertToEvent(bean))") - @Mapping(target = "service", expression = "java(convertToService(bean))") - IotThinkModelFunctionDO convert(IotThinkModelFunctionSaveReqVO bean); - - default ThingModelProperty convertToProperty(IotThinkModelFunctionSaveReqVO bean) { - if (Objects.equals(bean.getType(), IotProductFunctionTypeEnum.PROPERTY.getType())) { - return bean.getProperty(); - } - return null; - } - - default ThingModelEvent convertToEvent(IotThinkModelFunctionSaveReqVO bean) { - if (Objects.equals(bean.getType(), IotProductFunctionTypeEnum.EVENT.getType())) { - return bean.getEvent(); - } - return null; - } - - default ThingModelService convertToService(IotThinkModelFunctionSaveReqVO bean) { - if (Objects.equals(bean.getType(), IotProductFunctionTypeEnum.SERVICE.getType())) { - return bean.getService(); - } - return null; - } - - // 将 DO 转换为 RespVO - @Mapping(target = "property", source = "property") - @Mapping(target = "event", source = "event") - @Mapping(target = "service", source = "service") - IotThinkModelFunctionRespVO convert(IotThinkModelFunctionDO bean); - - // 批量转换 - List convertList(List list); -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java index d3f6547a1..9633d2feb 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java @@ -1,30 +1,32 @@ package cn.iocoder.yudao.module.iot.dal.dataobject.device; -import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.LongSetTypeHandler; +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStatusEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; import java.math.BigDecimal; import java.time.LocalDateTime; +import java.util.Set; /** * IoT 设备 DO * * @author haohao */ -@TableName("iot_device") +@TableName(value = "iot_device", autoResultMap = true) @KeySequence("iot_device_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor -public class IotDeviceDO extends BaseDO { +public class IotDeviceDO extends TenantBaseDO { /** * 设备 ID,主键,自增 @@ -33,6 +35,8 @@ public class IotDeviceDO extends BaseDO { private Long id; /** * 设备唯一标识符,全局唯一,用于识别设备 + * + * 类似阿里云 QueryDeviceInfo 的 IotInstanceId */ private String deviceKey; /** @@ -47,6 +51,17 @@ public class IotDeviceDO extends BaseDO { * 设备序列号 */ private String serialNumber; + /** + * 设备图片 + */ + private String picUrl; + /** + * 设备分组编号集合 + * + * 关联 {@link IotDeviceGroupDO#getId()} + */ + @TableField(typeHandler = LongSetTypeHandler.class) + private Set groupIds; /** * 产品编号 @@ -66,13 +81,6 @@ public class IotDeviceDO extends BaseDO { * 冗余 {@link IotProductDO#getDeviceType()} */ private Integer deviceType; - - /** - * 设备状态 - *

- * 枚举 {@link IotDeviceStatusEnum} - */ - private Integer status; /** * 网关设备编号 *

@@ -83,17 +91,19 @@ public class IotDeviceDO extends BaseDO { private Long gatewayId; /** - * 设备状态最后更新时间 + * 设备状态 + *

+ * 枚举 {@link IotDeviceStateEnum} */ - private LocalDateTime statusLastUpdateTime; + private Integer state; /** * 最后上线时间 */ - private LocalDateTime lastOnlineTime; + private LocalDateTime onlineTime; /** * 最后离线时间 */ - private LocalDateTime lastOfflineTime; + private LocalDateTime offlineTime; /** * 设备激活时间 */ @@ -104,10 +114,13 @@ public class IotDeviceDO extends BaseDO { */ private String ip; /** - * 设备的固件版本 + * 固件编号 + * + * 关联 {@link IotOtaFirmwareDO#getId()} */ - private String firmwareVersion; + private String firmwareId; + // TODO @芋艿:【待定 003】:要不要增加 username?目前 tl 有,阿里云之类的没有 /** * 设备密钥,用于设备认证,需安全存储 */ @@ -130,6 +143,7 @@ public class IotDeviceDO extends BaseDO { // TODO @haohao:是不是要枚举哈 private String authType; + // TODO @芋艿:【待定 002】:1)设备维护的时候,设置位置?类似 tl?;2)设备上传的时候,设置位置,类似 it? /** * 设备位置的纬度 */ @@ -149,4 +163,11 @@ public class IotDeviceDO extends BaseDO { */ private String address; + /** + * 设备配置 + * + * JSON 格式,可下发给 device 进行自定义配置 + */ + private String config; + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceGroupDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceGroupDO.java new file mode 100644 index 000000000..7865a4424 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceGroupDO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.device; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * IoT 设备分组 DO + * + * @author 芋道源码 + */ +@TableName("iot_device_group") +@KeySequence("iot_device_group_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceGroupDO extends BaseDO { + + /** + * 分组 ID + */ + @TableId + private Long id; + /** + * 分组名字 + */ + private String name; + /** + * 分组状态 + * + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + /** + * 分组描述 + */ + private String description; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceLogDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceLogDO.java new file mode 100644 index 000000000..55cfb19d4 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceLogDO.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.device; + +import cn.hutool.core.util.IdUtil; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * IoT 设备日志数据 DO + * + * 目前使用 TDengine 存储 + * + * @author alwayssuper + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceLogDO { + + /** + * 日志编号 + * + * 通过 {@link IdUtil#fastSimpleUUID()} 生成 + */ + private String id; + + /** + * 请求编号 + * + * 对应 {@link IotDeviceMessage#getRequestId()} 字段 + */ + private String requestId; + + /** + * 产品标识 + *

+ * 关联 {@link IotProductDO#getProductKey()} + */ + private String productKey; + /** + * 设备名称 + * + * 关联 {@link IotDeviceDO#getDeviceName()} + */ + private String deviceName; + /** + * 设备标识 + *

+ * 关联 {@link IotDeviceDO#getDeviceKey()}} + */ + private String deviceKey; // 非存储字段,用于 TDengine 的 TAG + + /** + * 日志类型 + * + * 枚举 {@link IotDeviceMessageTypeEnum} + */ + private String type; + /** + * 标识符 + * + * 枚举 {@link IotDeviceMessageIdentifierEnum} + */ + private String identifier; + + /** + * 数据内容 + * + * 存储具体的消息数据内容,通常是 JSON 格式 + */ + private String content; + /** + * 响应码 + * + * 目前只有 server 下行消息给 device 设备时,才会有响应码 + */ + private Integer code; + + /** + * 上报时间戳 + */ + private Long reportTime; + + /** + * 时序时间 + */ + private Long ts; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDevicePropertyDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDevicePropertyDO.java new file mode 100644 index 000000000..afb328894 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDevicePropertyDO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.device; + +import cn.iocoder.yudao.module.iot.dal.redis.device.DevicePropertyRedisDAO; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * IoT 设备属性项 Redis DO + * + * @see cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants#DEVICE_PROPERTY + * @see DevicePropertyRedisDAO + * + * @author haohao + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotDevicePropertyDO { + + /** + * 属性值(最新) + */ + private Object value; + + /** + * 更新时间 + */ + private LocalDateTime updateTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaFirmwareDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaFirmwareDO.java new file mode 100644 index 000000000..fa56f6938 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaFirmwareDO.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.ota; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * IoT OTA 固件 DO + * + * @see https://help.aliyun.com/zh/iot/user-guide/ota-upgrade-overview + * + * @author 芋道源码 + */ +@TableName(value = "iot_ota_firmware", autoResultMap = true) +@KeySequence("iot_ota_firmware_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotOtaFirmwareDO extends BaseDO { + + /** + * 固件编号 + */ + @TableField + private Long id; + /** + * 固件名称 + */ + private String name; + /** + * 固件版本 + */ + private String description; + /** + * 版本号 + */ + private String version; + + /** + * 产品编号 + * + * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()} + */ + // TODO @li:帮我改成 Long 哈,写错了 + private String productId; + /** + * 产品标识 + * + * 冗余 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getProductKey()} + */ + private String productKey; + + /** + * 签名方式 + * + * 例如说:MD5、SHA256 + */ + private String signMethod; + /** + * 固件文件签名 + */ + private String fileSign; + /** + * 固件文件大小 + */ + private Long fileSize; + /** + * 固件文件 URL + */ + private String fileUrl; + + /** + * 自定义信息,建议使用 JSON 格式 + */ + private String information; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaUpgradeRecordDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaUpgradeRecordDO.java new file mode 100644 index 000000000..ff4f0e7a0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaUpgradeRecordDO.java @@ -0,0 +1,92 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.ota; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * IoT OTA 升级记录 DO + * + * @author 芋道源码 + */ +@TableName(value = "iot_ota_upgrade_record", autoResultMap = true) +@KeySequence("iot_ota_upgrade_record_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotOtaUpgradeRecordDO extends BaseDO { + + @TableId + private Long id; + + /** + * 固件编号 + * + * 关联 {@link IotOtaFirmwareDO#getId()} + */ + private Long firmwareId; + /** + * 任务编号 + * + * 关联 {@link IotOtaUpgradeTaskDO#getId()} + */ + private Long taskId; + + /** + * 产品标识 + * + * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()} + */ + private String productKey; + /** + * 设备名称 + * + * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()} + */ + private String deviceName; + /** + * 设备编号 + * + * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()} + */ + private String deviceId; + /** + * 来源的固件编号 + * + * 关联 {@link IotDeviceDO#getFirmwareId()} + */ + private Long fromFirmwareId; + + /** + * 升级状态 + * + * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum} + */ + private Integer status; + /** + * 升级进度,百分比 + */ + private Integer progress; + /** + * 升级进度描述 + * + * 注意,只记录设备最后一次的升级进度描述 + * 如果想看历史记录,可以查看 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO} 设备日志 + */ + private String description; + /** + * 升级开始时间 + */ + private LocalDateTime startTime; + /** + * 升级结束时间 + */ + private LocalDateTime endTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaUpgradeTaskDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaUpgradeTaskDO.java new file mode 100644 index 000000000..221bdc56c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaUpgradeTaskDO.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.ota; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.util.List; + +/** + * IoT OTA 升级任务 DO + * + * @author 芋道源码 + */ +@TableName(value = "iot_ota_upgrade_task", autoResultMap = true) +@KeySequence("iot_ota_upgrade_task_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotOtaUpgradeTaskDO extends BaseDO { + + /** + * 任务编号 + */ + @TableField + private Long id; + /** + * 任务名称 + */ + private String name; + /** + * 任务描述 + */ + private String description; + + /** + * 固件编号 + *

+ * 关联 {@link IotOtaFirmwareDO#getId()} + */ + private Long firmwareId; + + /** + * 任务状态 + *

+ * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskStatusEnum} + */ + private Integer status; + + /** + * 升级范围 + *

+ * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum} + */ + private Integer scope; + /** + * 设备数量 + */ + private Long deviceCount; + /** + * 选中的设备编号数组 + *

+ * 关联 {@link IotDeviceDO#getId()} + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List deviceIds; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugin/IotPluginConfigDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugin/IotPluginConfigDO.java new file mode 100644 index 000000000..cb247fc30 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugin/IotPluginConfigDO.java @@ -0,0 +1,93 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.plugin; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginDeployTypeEnum; +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * IoT 插件配置 DO + * + * @author 芋道源码 + */ +@TableName("iot_plugin_config") +@KeySequence("iot_plugin_config_seq") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotPluginConfigDO extends TenantBaseDO { + + /** + * 主键 ID + */ + @TableId + private Long id; + /** + * 插件包标识符 + */ + private String pluginKey; + /** + * 插件名称 + */ + private String name; + /** + * 插件描述 + */ + private String description; + /** + * 部署方式 + *

+ * 枚举 {@link IotPluginDeployTypeEnum} + */ + private Integer deployType; + // TODO @芋艿:如果是外置的插件,fileName 和 version 的选择~ + /** + * 插件包文件名 + */ + private String fileName; + /** + * 插件版本 + */ + private String version; + // TODO @芋艿:type 字典的定义 + /** + * 插件类型 + *

+ * 枚举 {@link IotPluginTypeEnum} + */ + private Integer type; + /** + * 设备插件协议类型 + */ + // TODO @芋艿:枚举字段 + private String protocol; + // TODO @haohao:这个字段,是不是直接用 CommonStatus,开启、禁用;然后插件实例那,online 是否在线 + /** + * 状态 + *

+ * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + + // TODO @芋艿:configSchema、config 示例字段 + /** + * 插件配置项描述信息 + */ + private String configSchema; + /** + * 插件配置信息 + */ + private String config; + + // TODO @芋艿:script 后续的使用 + /** + * 插件脚本 + */ + private String script; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugin/IotPluginInstanceDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugin/IotPluginInstanceDO.java new file mode 100644 index 000000000..34abe893e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugin/IotPluginInstanceDO.java @@ -0,0 +1,70 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.plugin; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * IoT 插件实例 DO + * + * @author 芋道源码 + */ +@TableName("iot_plugin_instance") +@KeySequence("iot_plugin_instance_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotPluginInstanceDO extends TenantBaseDO { + + /** + * 主键 + */ + @TableId + private Long id; + /** + * 插件编号 + *

+ * 关联 {@link IotPluginConfigDO#getId()} + */ + private Long pluginId; + /** + * 插件进程编号 + * + * 一般格式是:hostIp@processId@${uuid} + */ + private String processId; + + /** + * 插件实例所在 IP + */ + private String hostIp; + /** + * 设备下行端口 + */ + private Integer downstreamPort; + + /** + * 是否在线 + */ + private Boolean online; + /** + * 在线时间 + */ + private LocalDateTime onlineTime; + /** + * 离线时间 + */ + private LocalDateTime offlineTime; + /** + * 心跳时间 + * + * 目的:心路时间超过一定时间后,会被进行下线处理 + */ + private LocalDateTime heartbeatTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductCategoryDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductCategoryDO.java new file mode 100644 index 000000000..174342afb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductCategoryDO.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.product; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * IoT 产品分类 DO + * + * @author 芋道源码 + */ +@TableName("iot_product_category") +@KeySequence("iot_product_category_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotProductCategoryDO extends BaseDO { + + /** + * 分类 ID + */ + @TableId + private Long id; + /** + * 分类名字 + */ + private String name; + /** + * 分类排序 + */ + private Integer sort; + /** + * 分类状态 + * + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + /** + * 分类描述 + */ + private String description; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java index eef466eda..3caebbccb 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.iot.dal.dataobject.product; -import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; @@ -14,15 +14,13 @@ import lombok.*; @TableName("iot_product") @KeySequence("iot_product_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor -public class IotProductDO extends BaseDO { +public class IotProductDO extends TenantBaseDO { /** - * 产品ID + * 产品 ID */ @TableId private Long id; @@ -30,17 +28,24 @@ public class IotProductDO extends BaseDO { * 产品名称 */ private String name; - // TODO @haohao:这个字段,要不改成 identifier,和阿里云更统一些 /** * 产品标识 */ private String productKey; /** - * 产品所属品类编号 + * 产品分类编号 *

- * TODO 外键:后续加 + * 关联 {@link IotProductCategoryDO#getId()} */ private Long categoryId; + /** + * 产品图标 + */ + private String icon; + /** + * 产品图片 + */ + private String picUrl; /** * 产品描述 */ diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotAlertConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotAlertConfig.java new file mode 100644 index 000000000..c6a2390ac --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotAlertConfig.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.rule; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotAlertConfigReceiveTypeEnum; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.util.List; + +/** + * IoT 告警配置 DO + * + * @author 芋道源码 + */ +@TableName("iot_alert_config") +@KeySequence("iot_alert_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotAlertConfig extends BaseDO { + + /** + * 配置编号 + */ + @TableId + private Long id; + /** + * 配置名称 + */ + private String name; + /** + * 配置描述 + */ + private String description; + /** + * 配置状态 + * + * TODO 数据字典 + */ + private Integer level; + /** + * 配置状态 + * + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + + /** + * 关联的规则场景编号数组 + * + * 关联 {@link IotRuleSceneDO#getId()} + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List ruleSceneIds; + + /** + * 接收的用户编号数组 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List receiveUserIds; + /** + * 接收的类型数组 + * + * 枚举 {@link IotAlertConfigReceiveTypeEnum} + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List receiveTypes; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotAlertRecordDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotAlertRecordDO.java new file mode 100644 index 000000000..840111078 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotAlertRecordDO.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.rule; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +/** + * IoT 告警记录 DO + * + * @author 芋道源码 + */ +@TableName("iot_alert_record") +@KeySequence("iot_alert_record_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotAlertRecordDO extends BaseDO { + + /** + * 记录编号 + */ + @TableField + private Long id; + /** + * 告警名称 + * + * 冗余 {@link IotAlertConfig#getName()} + */ + private Long configId; + /** + * 告警名称 + * + * 冗余 {@link IotAlertConfig#getName()} + */ + private String name; + + /** + * 产品标识 + * + * 关联 {@link IotProductDO#getProductKey()} ()} + */ + private String productKey; + /** + * 设备名称 + * + * 冗余 {@link IotDeviceDO#getDeviceName()} + */ + private String deviceName; + + // TODO @芋艿:有没更好的方式 + /** + * 触发的设备消息 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private IotDeviceMessage deviceMessage; + + // TODO @芋艿:换成枚举,枚举对应 ApiErrorLogProcessStatusEnum + /** + * 处理状态 + * + * true - 已处理 + * false - 未处理 + */ + private Boolean processStatus; + /** + * 处理结果(备注) + */ + private String processRemark; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotDataBridgeDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotDataBridgeDO.java new file mode 100644 index 000000000..fed429872 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotDataBridgeDO.java @@ -0,0 +1,67 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.rule; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeAbstractConfig; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeDirectionEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +/** + * IoT 数据桥梁 DO + * + * @author 芋道源码 + */ +@TableName(value = "iot_data_bridge", autoResultMap = true) +@KeySequence("iot_data_bridge_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotDataBridgeDO extends BaseDO { + + /** + * 桥梁编号 + */ + @TableId + private Long id; + /** + * 桥梁名称 + */ + private String name; + /** + * 桥梁描述 + */ + private String description; + /** + * 桥梁状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 桥梁方向 + * + * 枚举 {@link IotDataBridgeDirectionEnum} + */ + private Integer direction; + + /** + * 桥梁类型 + * + * 枚举 {@link IotDataBridgeTypeEnum} + */ + private Integer type; + + /** + * 桥梁配置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private IotDataBridgeAbstractConfig config; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotRuleSceneDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotRuleSceneDO.java new file mode 100644 index 000000000..f50101a4e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotRuleSceneDO.java @@ -0,0 +1,243 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.rule; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneActionTypeEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneTriggerConditionParameterOperatorEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneTriggerTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.util.List; +import java.util.Map; + +/** + * IoT 规则场景(场景联动) DO + * + * @author 芋道源码 + */ +@TableName("iot_rule_scene") +@KeySequence("iot_rule_scene_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotRuleSceneDO extends TenantBaseDO { + + /** + * 场景编号 + */ + @TableId + private Long id; + /** + * 场景名称 + */ + private String name; + /** + * 场景描述 + */ + private String description; + /** + * 场景状态 + * + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + + /** + * 触发器数组 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List triggers; + + /** + * 执行器数组 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List actions; + + /** + * 触发器配置 + */ + @Data + public static class TriggerConfig { + + /** + * 触发类型 + * + * 枚举 {@link IotRuleSceneTriggerTypeEnum} + */ + private Integer type; + + /** + * 产品标识 + * + * 关联 {@link IotProductDO#getProductKey()} + */ + private String productKey; + /** + * 设备名称数组 + * + * 关联 {@link IotDeviceDO#getDeviceName()} + */ + private List deviceNames; + + /** + * 触发条件数组 + * + * 必填:当 {@link #type} 为 {@link IotRuleSceneTriggerTypeEnum#DEVICE} 时 + * 条件与条件之间,是“或”的关系 + */ + private List conditions; + + /** + * CRON 表达式 + * + * 必填:当 {@link #type} 为 {@link IotRuleSceneTriggerTypeEnum#TIMER} 时 + */ + private String cronExpression; + + } + + /** + * 触发条件 + */ + @Data + public static class TriggerCondition { + + /** + * 消息类型 + * + * 枚举 {@link IotDeviceMessageTypeEnum} + */ + private String type; + /** + * 消息标识符 + * + * 枚举 {@link IotDeviceMessageIdentifierEnum} + */ + private String identifier; + + /** + * 参数数组 + * + * 参数与参数之间,是“或”的关系 + */ + private List parameters; + + } + + /** + * 触发条件参数 + */ + @Data + public static class TriggerConditionParameter { + + /** + * 标识符(属性、事件、服务) + * + * 关联 {@link IotThingModelDO#getIdentifier()} + */ + private String identifier; + + /** + * 操作符 + * + * 枚举 {@link IotRuleSceneTriggerConditionParameterOperatorEnum} + */ + private String operator; + + /** + * 比较值 + * + * 如果有多个值,则使用 "," 分隔,类似 "1,2,3"。 + * 例如说,{@link IotRuleSceneTriggerConditionParameterOperatorEnum#IN}、{@link IotRuleSceneTriggerConditionParameterOperatorEnum#BETWEEN} + */ + private String value; + + } + + /** + * 执行器配置 + */ + @Data + public static class ActionConfig { + + /** + * 执行类型 + * + * 枚举 {@link IotRuleSceneActionTypeEnum} + */ + private Integer type; + + /** + * 设备控制 + * + * 必填:当 {@link #type} 为 {@link IotRuleSceneActionTypeEnum#DEVICE_CONTROL} 时 + */ + private ActionDeviceControl deviceControl; + + /** + * 数据桥接编号 + * + * 必填:当 {@link #type} 为 {@link IotRuleSceneActionTypeEnum#DATA_BRIDGE} 时 + * 关联:{@link IotDataBridgeDO#getId()} + */ + private Long dataBridgeId; + + } + + /** + * 执行设备控制 + */ + @Data + public static class ActionDeviceControl { + + /** + * 产品标识 + * + * 关联 {@link IotProductDO#getProductKey()} + */ + private String productKey; + /** + * 设备名称数组 + * + * 关联 {@link IotDeviceDO#getDeviceName()} + */ + private List deviceNames; + + /** + * 消息类型 + * + * 枚举 {@link IotDeviceMessageTypeEnum#PROPERTY}、{@link IotDeviceMessageTypeEnum#SERVICE} + */ + private String type; + /** + * 消息标识符 + * + * 枚举 {@link IotDeviceMessageIdentifierEnum} + * + * 1. 属性设置:对应 {@link IotDeviceMessageIdentifierEnum#PROPERTY_SET} + * 2. 服务调用:对应 {@link IotDeviceMessageIdentifierEnum#SERVICE_INVOKE} + */ + private String identifier; + + /** + * 具体数据 + * + * 1. 属性设置:在 {@link #type} 是 {@link IotDeviceMessageTypeEnum#PROPERTY} 时,对应 properties + * 2. 服务调用:在 {@link #type} 是 {@link IotDeviceMessageTypeEnum#SERVICE} 时,对应 params + */ + private Map data; + + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thinkmodelfunction/IotThinkModelFunctionDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/IotThingModelDO.java similarity index 63% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thinkmodelfunction/IotThinkModelFunctionDO.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/IotThingModelDO.java index 02b2f9707..e3b4a6d9a 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thinkmodelfunction/IotThinkModelFunctionDO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/IotThingModelDO.java @@ -1,11 +1,11 @@ -package cn.iocoder.yudao.module.iot.dal.dataobject.thinkmodelfunction; +package cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelEvent; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelProperty; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelService; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelEvent; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelProperty; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelService; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; -import cn.iocoder.yudao.module.iot.enums.product.IotProductFunctionTypeEnum; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; @@ -19,17 +19,17 @@ import lombok.NoArgsConstructor; /** * IoT 产品物模型功能 DO *

- * 每个 {@link IotProductDO} 和 {@link IotThinkModelFunctionDO} 是“一对多”的关系,它的每个属性、事件、服务都对应一条记录 + * 每个 {@link IotProductDO} 和 {@link IotThingModelDO} 是“一对多”的关系,它的每个属性、事件、服务都对应一条记录 * * @author 芋道源码 */ -@TableName(value = "iot_think_model_function", autoResultMap = true) -@KeySequence("iot_think_model_function_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@TableName(value = "iot_thing_model", autoResultMap = true) +@KeySequence("iot_thing_model_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @Builder @NoArgsConstructor @AllArgsConstructor -public class IotThinkModelFunctionDO extends BaseDO { +public class IotThingModelDO extends BaseDO { /** * 物模型功能编号 @@ -66,7 +66,7 @@ public class IotThinkModelFunctionDO extends BaseDO { /** * 功能类型 *

- * 枚举 {@link IotProductFunctionTypeEnum} + * 枚举 {@link IotThingModelTypeEnum} */ private Integer type; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceGroupMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceGroupMapper.java new file mode 100644 index 000000000..1f80ae455 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceGroupMapper.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.device; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.group.IotDeviceGroupPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * IoT 设备分组 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface IotDeviceGroupMapper extends BaseMapperX { + + default PageResult selectPage(IotDeviceGroupPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(IotDeviceGroupDO::getName, reqVO.getName()) + .betweenIfPresent(IotDeviceGroupDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(IotDeviceGroupDO::getId)); + } + + default List selectListByStatus(Integer status) { + return selectList(IotDeviceGroupDO::getStatus, status); + } + + default IotDeviceGroupDO selectByName(String name) { + return selectOne(IotDeviceGroupDO::getName, name); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java index 0e5552f83..babbf29e7 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java @@ -1,12 +1,19 @@ package cn.iocoder.yudao.module.iot.dal.mysql.device; +import cn.hutool.core.util.ObjectUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDevicePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDevicePageReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.apache.ibatis.annotations.Mapper; +import javax.annotation.Nullable; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + /** * IoT 设备 Mapper * @@ -17,27 +24,19 @@ public interface IotDeviceMapper extends BaseMapperX { default PageResult selectPage(IotDevicePageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() - .eqIfPresent(IotDeviceDO::getDeviceKey, reqVO.getDeviceKey()) .likeIfPresent(IotDeviceDO::getDeviceName, reqVO.getDeviceName()) .eqIfPresent(IotDeviceDO::getProductId, reqVO.getProductId()) - .eqIfPresent(IotDeviceDO::getProductKey, reqVO.getProductKey()) .eqIfPresent(IotDeviceDO::getDeviceType, reqVO.getDeviceType()) .likeIfPresent(IotDeviceDO::getNickname, reqVO.getNickname()) - .eqIfPresent(IotDeviceDO::getGatewayId, reqVO.getGatewayId()) - .eqIfPresent(IotDeviceDO::getStatus, reqVO.getStatus()) - .betweenIfPresent(IotDeviceDO::getStatusLastUpdateTime, reqVO.getStatusLastUpdateTime()) - .betweenIfPresent(IotDeviceDO::getLastOnlineTime, reqVO.getLastOnlineTime()) - .betweenIfPresent(IotDeviceDO::getLastOfflineTime, reqVO.getLastOfflineTime()) - .betweenIfPresent(IotDeviceDO::getActiveTime, reqVO.getActiveTime()) - .eqIfPresent(IotDeviceDO::getDeviceSecret, reqVO.getDeviceSecret()) - .eqIfPresent(IotDeviceDO::getMqttClientId, reqVO.getMqttClientId()) - .likeIfPresent(IotDeviceDO::getMqttUsername, reqVO.getMqttUsername()) - .eqIfPresent(IotDeviceDO::getMqttPassword, reqVO.getMqttPassword()) - .eqIfPresent(IotDeviceDO::getAuthType, reqVO.getAuthType()) - .betweenIfPresent(IotDeviceDO::getCreateTime, reqVO.getCreateTime()) + .eqIfPresent(IotDeviceDO::getState, reqVO.getStatus()) + .apply(ObjectUtil.isNotNull(reqVO.getGroupId()), "FIND_IN_SET(" + reqVO.getGroupId() + ",group_ids) > 0") .orderByDesc(IotDeviceDO::getId)); } + default IotDeviceDO selectByDeviceName(String deviceName) { + return selectOne(IotDeviceDO::getDeviceName, deviceName); + } + default IotDeviceDO selectByProductKeyAndDeviceName(String productKey, String deviceName) { return selectOne(IotDeviceDO::getProductKey, productKey, IotDeviceDO::getDeviceName, deviceName); @@ -50,4 +49,48 @@ public interface IotDeviceMapper extends BaseMapperX { default Long selectCountByProductId(Long productId) { return selectCount(IotDeviceDO::getProductId, productId); } + + default IotDeviceDO selectByDeviceKey(String deviceKey) { + return selectOne(new LambdaQueryWrapper() + .apply("LOWER(device_key) = {0}", deviceKey.toLowerCase())); + } + + default List selectListByDeviceType(Integer deviceType) { + return selectList(IotDeviceDO::getDeviceType, deviceType); + } + + default List selectListByState(Integer state) { + return selectList(IotDeviceDO::getState, state); + } + + default List selectListByProductId(Long productId) { + return selectList(IotDeviceDO::getProductId, productId); + } + + default Long selectCountByGroupId(Long groupId) { + return selectCount(new LambdaQueryWrapperX() + .apply("FIND_IN_SET(" + groupId + ",group_ids) > 0")); + } + + default Long selectCountByCreateTime(@Nullable LocalDateTime createTime) { + return selectCount(new LambdaQueryWrapperX() + .geIfPresent(IotDeviceDO::getCreateTime, createTime)); + } + + /** + * 查询指定产品下各状态的设备数量 + * + * @return 设备数量统计列表 + */ + // TODO @super:通过 mybatis-plus 来写哈,然后返回 Map 貌似就行了?! + List> selectDeviceCountMapByProductId(); + + // TODO @super:通过 mybatis-plus 来写哈,然后返回 Map 貌似就行了?! + /** + * 查询各个状态下的设备数量 + * + * @return 设备数量统计列表 + */ + List> selectDeviceCountGroupByState(); + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaFirmwareMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaFirmwareMapper.java new file mode 100644 index 000000000..7adf79349 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaFirmwareMapper.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.ota; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwarePageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +// TODO @li:参考 IotOtaUpgradeRecordMapper 的写法 +@Mapper +public interface IotOtaFirmwareMapper extends BaseMapperX { + + /** + * 根据产品ID和固件版本号查询固件信息列表。 + * + * @param productId 产品ID,用于筛选固件信息。 + * @param version 固件版本号,用于筛选固件信息。 + * @return 返回符合条件的固件信息列表。 + */ + default List selectByProductIdAndVersion(String productId, String version) { + return selectList(IotOtaFirmwareDO::getProductId, productId, + IotOtaFirmwareDO::getVersion, version); + } + + /** + * 分页查询固件信息,支持根据名称和产品ID进行筛选,并按创建时间降序排列。 + * + * @param pageReqVO 分页查询请求对象,包含分页参数和筛选条件。 + * @return 返回分页查询结果,包含符合条件的固件信息列表。 + */ + default PageResult selectPage(IotOtaFirmwarePageReqVO pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .likeIfPresent(IotOtaFirmwareDO::getName, pageReqVO.getName()) + .eqIfPresent(IotOtaFirmwareDO::getProductId, pageReqVO.getProductId()) + .orderByDesc(IotOtaFirmwareDO::getCreateTime)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaUpgradeRecordMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaUpgradeRecordMapper.java new file mode 100644 index 000000000..5e5d8200f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaUpgradeRecordMapper.java @@ -0,0 +1,159 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.ota; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record.IotOtaUpgradeRecordPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeRecordDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface IotOtaUpgradeRecordMapper extends BaseMapperX { + + // TODO @li:selectByFirmwareIdAndTaskIdAndDeviceId;让方法自解释 + /** + * 根据条件查询单个OTA升级记录 + * + * @param firmwareId 固件ID,可选参数,用于筛选固件ID匹配的记录 + * @param taskId 任务ID,可选参数,用于筛选任务ID匹配的记录 + * @param deviceId 设备ID,可选参数,用于筛选设备ID匹配的记录 + * @return 返回符合条件的单个OTA升级记录,如果不存在则返回null + */ + default IotOtaUpgradeRecordDO selectByConditions(Long firmwareId, Long taskId, String deviceId) { + // 使用LambdaQueryWrapperX构建查询条件,根据传入的参数动态添加查询条件 + return selectOne(new LambdaQueryWrapperX() + .eqIfPresent(IotOtaUpgradeRecordDO::getFirmwareId, firmwareId) + .eqIfPresent(IotOtaUpgradeRecordDO::getTaskId, taskId) + .eqIfPresent(IotOtaUpgradeRecordDO::getDeviceId, deviceId)); + } + + // TODO @li:这个是不是 groupby status 就 ok 拉? + /** + * 根据任务ID和设备名称查询OTA升级记录的状态统计信息。 + * 该函数通过SQL查询统计不同状态(0到5)的记录数量,并返回一个包含统计结果的Map列表。 + * + * @param taskId 任务ID,用于筛选特定任务的OTA升级记录。 + * @param deviceName 设备名称,支持模糊查询,用于筛选特定设备的OTA升级记录。 + * @return 返回一个Map列表,每个Map包含不同状态(0到5)的记录数量。 + */ + @Select("select count(case when status = 0 then 1 else 0) as `0` " + + "count(case when status = 1 then 1 else 0) as `1` " + + "count(case when status = 2 then 1 else 0) as `2` " + + "count(case when status = 3 then 1 else 0) as `3` " + + "count(case when status = 4 then 1 else 0) as `4` " + + "count(case when status = 5 then 1 else 0) as `5` " + + "from iot_ota_upgrade_record " + + "where task_id = #{taskId} " + + "and device_name like concat('%', #{deviceName}, '%') " + + "and status = #{status}") + List> selectOtaUpgradeRecordCount(@Param("taskId") Long taskId, + @Param("deviceName") String deviceName); + + /** + * 根据固件ID查询OTA升级记录的状态统计信息。 + * 该函数通过SQL查询统计不同状态(0到5)的记录数量,并返回一个包含统计结果的Map列表。 + * + * @param firmwareId 固件ID,用于筛选特定固件的OTA升级记录。 + * @return 返回一个Map列表,每个Map包含不同状态(0到5)的记录数量。 + */ + @Select("select count(case when status = 0 then 1 else 0) as `0` " + + "count(case when status = 1 then 1 else 0) as `1` " + + "count(case when status = 2 then 1 else 0) as `2` " + + "count(case when status = 3 then 1 else 0) as `3` " + + "count(case when status = 4 then 1 else 0) as `4` " + + "count(case when status = 5 then 1 else 0) as `5` " + + "from iot_ota_upgrade_record " + + "where firmware_id = #{firmwareId}") + List> selectOtaUpgradeRecordStatistics(Long firmwareId); + + // TODO @li:这里的注释,可以去掉哈 + /** + * 根据分页查询条件获取 OTA升级记录的分页结果 + * + * @param pageReqVO 分页查询请求参数,包含设备名称、任务ID等查询条件 + * @return 返回分页查询结果,包含符合条件的 OTA升级记录列表 + */ + // TODO @li:selectPage 就 ok 拉。 + default PageResult selectUpgradeRecordPage(IotOtaUpgradeRecordPageReqVO pageReqVO) { + // TODO @li:这里的注释,可以去掉哈;然后下面的“如果”。。。也没必要注释 + // 使用LambdaQueryWrapperX构建查询条件,并根据请求参数动态添加查询条件 + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .likeIfPresent(IotOtaUpgradeRecordDO::getDeviceName, pageReqVO.getDeviceName()) // 如果设备名称存在,则添加模糊查询条件 + .eqIfPresent(IotOtaUpgradeRecordDO::getTaskId, pageReqVO.getTaskId())); // 如果任务ID存在,则添加等值查询条件 + } + + // TODO @li:这里的注释,可以去掉哈 + /** + * 根据任务ID和状态更新升级记录的状态 + *

+ * 该函数用于将符合指定任务ID和状态的升级记录的状态更新为新的状态。 + * + * @param setStatus 要设置的新状态值,类型为Integer + * @param taskId 要更新的升级记录对应的任务ID,类型为Long + * @param whereStatus 用于筛选升级记录的当前状态值,类型为Integer + */ + // TODO @li:改成 updateByTaskIdAndStatus(taskId, status, IotOtaUpgradeRecordDO) 更通用一些。 + default void updateUpgradeRecordStatusByTaskIdAndStatus(Integer setStatus, Long taskId, Integer whereStatus) { + // 使用LambdaUpdateWrapper构建更新条件,将指定状态的记录更新为指定状态 + update(new LambdaUpdateWrapper() + .set(IotOtaUpgradeRecordDO::getStatus, setStatus) + .eq(IotOtaUpgradeRecordDO::getTaskId, taskId) + .eq(IotOtaUpgradeRecordDO::getStatus, whereStatus) + ); + } + + // TODO @li:参考上面的建议,调整下这个方法 + /** + * 根据状态查询符合条件的升级记录列表 + *

+ * 该函数使用LambdaQueryWrapperX构建查询条件,查询指定状态的升级记录。 + * + * @param state 升级记录的状态,用于筛选符合条件的记录 + * @return 返回符合指定状态的升级记录列表,类型为List + */ + default List selectUpgradeRecordListByState(Integer state) { + // 使用LambdaQueryWrapperX构建查询条件,根据状态查询符合条件的升级记录 + return selectList(new LambdaQueryWrapperX() + .eq(IotOtaUpgradeRecordDO::getStatus, state)); + } + + // TODO @li:参考上面的建议,调整下这个方法 + /** + * 更新升级记录状态 + *

+ * 该函数用于批量更新指定ID列表中的升级记录状态。通过传入的ID列表和状态值,使用LambdaUpdateWrapper构建更新条件, + * 并执行更新操作。 + * + * @param ids 需要更新的升级记录ID列表,类型为List。传入的ID列表中的记录将被更新。 + * @param status 要更新的状态值,类型为Integer。该值将被设置到符合条件的升级记录中。 + */ + default void updateUpgradeRecordStatus(List ids, Integer status) { + // 使用LambdaUpdateWrapper构建更新条件,设置状态字段,并根据ID列表进行筛选 + update(new LambdaUpdateWrapper() + .set(IotOtaUpgradeRecordDO::getStatus, status) + .in(IotOtaUpgradeRecordDO::getId, ids) + ); + } + + // TODO @li:参考上面的建议,调整下这个方法 + /** + * 根据任务ID查询升级记录列表 + *

+ * 该函数通过任务ID查询符合条件的升级记录,并返回查询结果列表。 + * + * @param taskId 任务ID,用于筛选升级记录 + * @return 返回符合条件的升级记录列表,若未找到则返回空列表 + */ + default List selectUpgradeRecordListByTaskId(Long taskId) { + // 使用LambdaQueryWrapperX构建查询条件,根据任务ID查询符合条件的升级记录 + return selectList(new LambdaQueryWrapperX() + .eq(IotOtaUpgradeRecordDO::getTaskId, taskId)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaUpgradeTaskMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaUpgradeTaskMapper.java new file mode 100644 index 000000000..d955b1361 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaUpgradeTaskMapper.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.ota; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * OTA 升级任务Mapper + * + * @author Shelly + */ +@Mapper +public interface IotOtaUpgradeTaskMapper extends BaseMapperX { + + /** + * 根据固件ID和任务名称查询升级任务列表。 + * + * @param firmwareId 固件ID,用于筛选升级任务 + * @param name 任务名称,用于筛选升级任务 + * @return 符合条件的升级任务列表 + */ + default List selectByFirmwareIdAndName(Long firmwareId, String name) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(IotOtaUpgradeTaskDO::getFirmwareId, firmwareId) + .eqIfPresent(IotOtaUpgradeTaskDO::getName, name)); + } + + /** + * 分页查询升级任务列表,支持根据固件ID和任务名称进行筛选。 + * + * @param pageReqVO 分页查询请求对象,包含分页参数和筛选条件 + * @return 分页结果,包含符合条件的升级任务列表 + */ + default PageResult selectUpgradeTaskPage(IotOtaUpgradeTaskPageReqVO pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .eqIfPresent(IotOtaUpgradeTaskDO::getFirmwareId, pageReqVO.getFirmwareId()) + .likeIfPresent(IotOtaUpgradeTaskDO::getName, pageReqVO.getName())); + } + + /** + * 根据任务状态查询升级任务列表 + *

+ * 该函数通过传入的任务状态,查询数据库中符合条件的升级任务列表。 + * + * @param status 任务状态,用于筛选升级任务的状态值 + * @return 返回符合条件的升级任务列表,列表中的每个元素为 IotOtaUpgradeTaskDO 对象 + */ + default List selectUpgradeTaskByState(Integer status) { + return selectList(IotOtaUpgradeTaskDO::getStatus, status); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginConfigMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginConfigMapper.java new file mode 100644 index 000000000..0e2163a3f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginConfigMapper.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.plugin; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface IotPluginConfigMapper extends BaseMapperX { + + default PageResult selectPage(PluginConfigPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(IotPluginConfigDO::getName, reqVO.getName()) + .eqIfPresent(IotPluginConfigDO::getStatus, reqVO.getStatus()) + .orderByDesc(IotPluginConfigDO::getId)); + } + + default List selectListByStatusAndDeployType(Integer status, Integer deployType) { + return selectList(new LambdaQueryWrapperX() + .eq(IotPluginConfigDO::getStatus, status) + .eq(IotPluginConfigDO::getDeployType, deployType) + .orderByAsc(IotPluginConfigDO::getId)); + } + + default IotPluginConfigDO selectByPluginKey(String pluginKey) { + return selectOne(IotPluginConfigDO::getPluginKey, pluginKey); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginInstanceMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginInstanceMapper.java new file mode 100644 index 000000000..93ffe8728 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginInstanceMapper.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.plugin; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.List; + +// TODO @li:参考 IotOtaUpgradeRecordMapper 的写法 +@Mapper +public interface IotPluginInstanceMapper extends BaseMapperX { + + default IotPluginInstanceDO selectByProcessId(String processId) { + return selectOne(IotPluginInstanceDO::getProcessId, processId); + } + + default List selectListByHeartbeatTimeLt(LocalDateTime heartbeatTime) { + return selectList(new LambdaQueryWrapper() + .lt(IotPluginInstanceDO::getHeartbeatTime, heartbeatTime)); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductCategoryMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductCategoryMapper.java new file mode 100644 index 000000000..dc9367bbd --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductCategoryMapper.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.product; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.category.IotProductCategoryPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductCategoryDO; +import org.apache.ibatis.annotations.Mapper; + +import javax.annotation.Nullable; +import java.time.LocalDateTime; +import java.util.List; + +/** + * IoT 产品分类 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface IotProductCategoryMapper extends BaseMapperX { + + default PageResult selectPage(IotProductCategoryPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(IotProductCategoryDO::getName, reqVO.getName()) + .betweenIfPresent(IotProductCategoryDO::getCreateTime, reqVO.getCreateTime()) + .orderByAsc(IotProductCategoryDO::getSort)); + } + + default List selectListByStatus(Integer status) { + return selectList(IotProductCategoryDO::getStatus, status); + } + + default Long selectCountByCreateTime(@Nullable LocalDateTime createTime) { + return selectCount(new LambdaQueryWrapperX() + .geIfPresent(IotProductCategoryDO::getCreateTime, createTime)); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java index 0341e2492..5ba4a8177 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java @@ -3,10 +3,15 @@ package cn.iocoder.yudao.module.iot.dal.mysql.product; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; -import cn.iocoder.yudao.module.iot.controller.admin.product.vo.IotProductPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductPageReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.apache.ibatis.annotations.Mapper; +import javax.annotation.Nullable; +import java.time.LocalDateTime; +import java.util.List; + /** * IoT 产品 Mapper * @@ -23,7 +28,14 @@ public interface IotProductMapper extends BaseMapperX { } default IotProductDO selectByProductKey(String productKey) { - return selectOne(new LambdaQueryWrapperX().eq(IotProductDO::getProductKey, productKey)); + return selectOne(new LambdaQueryWrapper() + .apply("LOWER(product_key) = {0}", productKey.toLowerCase())); } + default Long selectCountByCreateTime(@Nullable LocalDateTime createTime) { + return selectCount(new LambdaQueryWrapperX() + .geIfPresent(IotProductDO::getCreateTime, createTime)); + } + + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotDataBridgeMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotDataBridgeMapper.java new file mode 100644 index 000000000..303579116 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotDataBridgeMapper.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.rule; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.IotDataBridgePageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * IoT 数据桥梁 Mapper + * + * @author HUIHUI + */ +@Mapper +public interface IotDataBridgeMapper extends BaseMapperX { + + default PageResult selectPage(IotDataBridgePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(IotDataBridgeDO::getName, reqVO.getName()) + .eqIfPresent(IotDataBridgeDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(IotDataBridgeDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(IotDataBridgeDO::getId)); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotRuleSceneMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotRuleSceneMapper.java new file mode 100644 index 000000000..e5e069a0c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotRuleSceneMapper.java @@ -0,0 +1,10 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.rule; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface IotRuleSceneMapper extends BaseMapperX { + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/thingmodel/IotThingModelMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/thingmodel/IotThingModelMapper.java new file mode 100644 index 000000000..082386b4e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/thingmodel/IotThingModelMapper.java @@ -0,0 +1,88 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.thingmodel; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelListReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * IoT 产品物模型 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface IotThingModelMapper extends BaseMapperX { + + default PageResult selectPage(IotThingModelPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(IotThingModelDO::getIdentifier, reqVO.getIdentifier()) + .likeIfPresent(IotThingModelDO::getName, reqVO.getName()) + .eqIfPresent(IotThingModelDO::getType, reqVO.getType()) + .eqIfPresent(IotThingModelDO::getProductId, reqVO.getProductId()) + // TODO @芋艿:看看要不要加枚举 + .notIn(IotThingModelDO::getIdentifier, "get", "set", "post") + .orderByDesc(IotThingModelDO::getId)); + } + + default List selectList(IotThingModelListReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(IotThingModelDO::getIdentifier, reqVO.getIdentifier()) + .likeIfPresent(IotThingModelDO::getName, reqVO.getName()) + .eqIfPresent(IotThingModelDO::getType, reqVO.getType()) + .eqIfPresent(IotThingModelDO::getProductId, reqVO.getProductId()) + // TODO @芋艿:看看要不要加枚举 + .notIn(IotThingModelDO::getIdentifier, "get", "set", "post") + .orderByDesc(IotThingModelDO::getId)); + } + + default IotThingModelDO selectByProductIdAndIdentifier(Long productId, String identifier) { + return selectOne(IotThingModelDO::getProductId, productId, + IotThingModelDO::getIdentifier, identifier); + } + + default List selectListByProductId(Long productId) { + return selectList(IotThingModelDO::getProductId, productId); + } + + default List selectListByProductKey(String productKey) { + return selectList(IotThingModelDO::getProductKey, productKey); + } + + default List selectListByProductIdAndType(Long productId, Integer type) { + return selectList(IotThingModelDO::getProductId, productId, + IotThingModelDO::getType, type); + } + + default List selectListByProductIdAndIdentifiersAndTypes(Long productId, + List identifiers, + List types) { + return selectList(new LambdaQueryWrapperX() + .eq(IotThingModelDO::getProductId, productId) + .in(IotThingModelDO::getIdentifier, identifiers) + .in(IotThingModelDO::getType, types)); + } + + default IotThingModelDO selectByProductIdAndName(Long productId, String name) { + return selectOne(IotThingModelDO::getProductId, productId, + IotThingModelDO::getName, name); + } + + // TODO @super:用不到,删除下; + /** + * 统计物模型数量 + * + * @param createTime 创建时间,如果为空,则统计所有物模型数量 + * @return 物模型数量 + */ + default Long selectCountByCreateTime(LocalDateTime createTime) { + return selectCount(new LambdaQueryWrapperX() + .geIfPresent(IotThingModelDO::getCreateTime, createTime)); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/thinkmodelfunction/IotThinkModelFunctionMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/thinkmodelfunction/IotThinkModelFunctionMapper.java deleted file mode 100644 index e8b96e022..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/thinkmodelfunction/IotThinkModelFunctionMapper.java +++ /dev/null @@ -1,58 +0,0 @@ -package cn.iocoder.yudao.module.iot.dal.mysql.thinkmodelfunction; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; -import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo.IotThinkModelFunctionPageReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.thinkmodelfunction.IotThinkModelFunctionDO; -import org.apache.ibatis.annotations.Mapper; - -import java.util.List; - -/** - * IoT 产品物模型 Mapper - * - * @author 芋道源码 - */ -@Mapper -public interface IotThinkModelFunctionMapper extends BaseMapperX { - - default PageResult selectPage(IotThinkModelFunctionPageReqVO reqVO) { - return selectPage(reqVO, new LambdaQueryWrapperX() - .eqIfPresent(IotThinkModelFunctionDO::getIdentifier, reqVO.getIdentifier()) - .likeIfPresent(IotThinkModelFunctionDO::getName, reqVO.getName()) - .eqIfPresent(IotThinkModelFunctionDO::getType, reqVO.getType()) - .eqIfPresent(IotThinkModelFunctionDO::getProductId, reqVO.getProductId()) - .notIn(IotThinkModelFunctionDO::getIdentifier, "get", "set", "post") - .orderByDesc(IotThinkModelFunctionDO::getId)); - } - - default IotThinkModelFunctionDO selectByProductIdAndIdentifier(Long productId, String identifier) { - return selectOne(IotThinkModelFunctionDO::getProductId, productId, - IotThinkModelFunctionDO::getIdentifier, identifier); - } - - default List selectListByProductId(Long productId) { - return selectList(IotThinkModelFunctionDO::getProductId, productId); - } - - default List selectListByProductIdAndType(Long productId, Integer type) { - return selectList(IotThinkModelFunctionDO::getProductId, productId, - IotThinkModelFunctionDO::getType, type); - } - - default List selectListByProductIdAndIdentifiersAndTypes(Long productId, - List identifiers, - List types){ - return selectList(new LambdaQueryWrapperX() - .eq(IotThinkModelFunctionDO::getProductId, productId) - .in(IotThinkModelFunctionDO::getIdentifier, identifiers) - .in(IotThinkModelFunctionDO::getType, types)); - } - - default IotThinkModelFunctionDO selectByProductIdAndName(Long productId, String name) { - return selectOne(IotThinkModelFunctionDO::getProductId, productId, - IotThinkModelFunctionDO::getName, name); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java new file mode 100644 index 000000000..d09dac72d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.iot.dal.redis; + +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO; + +/** + * IoT Redis Key 枚举类 + * + * @author 芋道源码 + */ +public interface RedisKeyConstants { + + /** + * 设备属性的数据缓存,采用 HASH 结构 + *

+ * KEY 格式:device_property:{deviceKey} + * HASH KEY:identifier 属性标识 + * VALUE 数据类型:String(JSON) {@link IotDevicePropertyDO} + */ + String DEVICE_PROPERTY = "iot:device_property:%s"; + + /** + * 设备的最后上报时间,采用 ZSET 结构 + * + * KEY 格式:{deviceKey} + * SCORE:上报时间 + */ + String DEVICE_REPORT_TIMES = "iot:device_report_times"; + + /** + * 设备信息的数据缓存,使用 Spring Cache 操作(忽略租户) + * + * KEY 格式:device_${productKey}_${deviceKey} + * VALUE 数据类型:String(JSON) + */ + String DEVICE = "iot:device"; + + /** + * 物模型的数据缓存,使用 Spring Cache 操作(忽略租户) + * + * KEY 格式:thing_model_${productKey} + * VALUE 数据类型:String 数组(JSON),即 {@link cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO} 列表 + */ + String THING_MODEL_LIST = "iot:thing_model_list"; + + /** + * 设备插件的插件进程编号的映射,采用 HASH 结构 + * + * KEY 格式:device_plugin_instance_process_ids + * HASH KEY:${deviceKey} + * VALUE:插件进程编号,对应 {@link IotPluginInstanceDO#getProcessId()} 字段 + */ + String DEVICE_PLUGIN_INSTANCE_PROCESS_IDS = "iot:device_plugin_instance_process_ids"; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DevicePropertyRedisDAO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DevicePropertyRedisDAO.java new file mode 100644 index 000000000..0f1196ab6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DevicePropertyRedisDAO.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.iot.dal.redis.device; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; +import jakarta.annotation.Resource; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import java.util.Collections; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants.DEVICE_PROPERTY; + +/** + * {@link IotDevicePropertyDO} 的 Redis DAO + */ +@Repository +public class DevicePropertyRedisDAO { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + public Map get(String deviceKey) { + String redisKey = formatKey(deviceKey); + Map entries = stringRedisTemplate.opsForHash().entries(redisKey); + if (CollUtil.isEmpty(entries)) { + return Collections.emptyMap(); + } + return convertMap(entries.entrySet(), + entry -> (String) entry.getKey(), + entry -> JsonUtils.parseObject((String) entry.getValue(), IotDevicePropertyDO.class)); + } + + public void putAll(String deviceKey, Map properties) { + if (CollUtil.isEmpty(properties)) { + return; + } + String redisKey = formatKey(deviceKey); + stringRedisTemplate.opsForHash().putAll(redisKey, convertMap(properties.entrySet(), + Map.Entry::getKey, + entry -> JsonUtils.toJsonString(entry.getValue()))); + } + + private static String formatKey(String deviceKey) { + return String.format(DEVICE_PROPERTY, deviceKey); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DeviceReportTimeRedisDAO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DeviceReportTimeRedisDAO.java new file mode 100644 index 000000000..d84af7543 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DeviceReportTimeRedisDAO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.iot.dal.redis.device; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; +import jakarta.annotation.Resource; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.Set; + +/** + * 设备的最后上报时间的 Redis DAO + * + * @author 芋道源码 + */ +@Repository +public class DeviceReportTimeRedisDAO { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + public void update(String deviceKey, LocalDateTime reportTime) { + stringRedisTemplate.opsForZSet().add(RedisKeyConstants.DEVICE_REPORT_TIMES, deviceKey, + LocalDateTimeUtil.toEpochMilli(reportTime)); + } + + public Set range(LocalDateTime maxReportTime) { + return stringRedisTemplate.opsForZSet().rangeByScore(RedisKeyConstants.DEVICE_REPORT_TIMES, 0, + LocalDateTimeUtil.toEpochMilli(maxReportTime)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/plugin/DevicePluginProcessIdRedisDAO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/plugin/DevicePluginProcessIdRedisDAO.java new file mode 100644 index 000000000..32559d703 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/plugin/DevicePluginProcessIdRedisDAO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.iot.dal.redis.plugin; + +import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; +import jakarta.annotation.Resource; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +/** + * 设备插件的插件进程编号的缓存的 Redis DAO + */ +@Repository +public class DevicePluginProcessIdRedisDAO { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + public void put(String deviceKey, String processId) { + stringRedisTemplate.opsForHash().put(RedisKeyConstants.DEVICE_PLUGIN_INSTANCE_PROCESS_IDS, deviceKey, processId); + } + + public String get(String deviceKey) { + return (String) stringRedisTemplate.opsForHash().get(RedisKeyConstants.DEVICE_PLUGIN_INSTANCE_PROCESS_IDS, deviceKey); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDeviceLogMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDeviceLogMapper.java new file mode 100644 index 000000000..96741e609 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDeviceLogMapper.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.module.iot.dal.tdengine; + +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDeviceLogPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO; +import cn.iocoder.yudao.module.iot.framework.tdengine.core.annotation.TDengineDS; +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * 设备日志 {@link IotDeviceLogDO} Mapper 接口 + */ +@Mapper +@TDengineDS +@InterceptorIgnore(tenantLine = "true") // 避免 SQL 解析,因为 JSqlParser 对 TDengine 的 SQL 解析会报错 +public interface IotDeviceLogMapper { + + /** + * 创建设备日志超级表 + */ + void createDeviceLogSTable(); + + /** + * 查询设备日志表是否存在 + * + * @return 存在则返回表名;不存在则返回 null + */ + String showDeviceLogSTable(); + + /** + * 插入设备日志数据 + * + * 如果子表不存在,会自动创建子表 + * + * @param log 设备日志数据 + */ + void insert(IotDeviceLogDO log); + + /** + * 获得设备日志分页 + * + * @param reqVO 分页查询条件 + * @return 设备日志列表 + */ + IPage selectPage(IPage page, + @Param("reqVO") IotDeviceLogPageReqVO reqVO); + + /** + * 统计设备日志数量 + * + * @param createTime 创建时间,如果为空,则统计所有日志数量 + * @return 日志数量 + */ + Long selectCountByCreateTime(@Param("createTime") Long createTime); + + // TODO @super:1)上行、下行,不写在 mapper 里,而是通过参数传递,这样,selectDeviceLogUpCountByHour、selectDeviceLogDownCountByHour 可以合并; + // TODO @super:2)不能只基于 identifier 来计算,而是要 type + identifier 成对 + /** + * 查询每个小时设备上行消息数量 + */ + List> selectDeviceLogUpCountByHour(@Param("deviceKey") String deviceKey, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime); + + /** + * 查询每个小时设备下行消息数量 + */ + List> selectDeviceLogDownCountByHour(@Param("deviceKey") String deviceKey, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDevicePropertyMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDevicePropertyMapper.java new file mode 100644 index 000000000..37a72e4b0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDevicePropertyMapper.java @@ -0,0 +1,90 @@ +package cn.iocoder.yudao.module.iot.dal.tdengine; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyHistoryPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyRespVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField; +import cn.iocoder.yudao.module.iot.framework.tdengine.core.annotation.TDengineDS; +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Mapper +@TDengineDS +@InterceptorIgnore(tenantLine = "true") // 避免 SQL 解析,因为 JSqlParser 对 TDengine 的 SQL 解析会报错 +public interface IotDevicePropertyMapper { + + List getProductPropertySTableFieldList(@Param("productKey") String productKey); + + void createProductPropertySTable(@Param("productKey") String productKey, + @Param("fields") List fields); + + @SuppressWarnings("SimplifyStreamApiCallChains") // 保持 JDK8 兼容性 + default void alterProductPropertySTable(String productKey, + List oldFields, + List newFields) { + oldFields.removeIf(field -> StrUtil.equalsAny(field.getField(), + TDengineTableField.FIELD_TS, "report_time", "device_key")); + List addFields = newFields.stream().filter( // 新增的字段 + newField -> oldFields.stream().noneMatch(oldField -> oldField.getField().equals(newField.getField()))) + .collect(Collectors.toList()); + List dropFields = oldFields.stream().filter( // 删除的字段 + oldField -> newFields.stream().noneMatch(n -> n.getField().equals(oldField.getField()))) + .collect(Collectors.toList()); + List modifyTypeFields = new ArrayList<>(); // 变更类型的字段 + List modifyLengthFields = new ArrayList<>(); // 变更长度的字段 + newFields.forEach(newField -> { + TDengineTableField oldField = CollUtil.findOne(oldFields, field -> field.getField().equals(newField.getField())); + if (oldField == null) { + return; + } + if (ObjectUtil.notEqual(oldField.getType(), newField.getType())) { + modifyTypeFields.add(newField); + return; + } + if (newField.getLength() != null) { + if (newField.getLength() > oldField.getLength()) { + modifyLengthFields.add(newField); + } else if (newField.getLength() < oldField.getLength()) { + // 特殊:TDengine 长度修改时,只允许变长,所以此时认为是修改类型 + modifyTypeFields.add(newField); + } + } + }); + + // 执行 + addFields.forEach(field -> alterProductPropertySTableAddField(productKey, field)); + dropFields.forEach(field -> alterProductPropertySTableDropField(productKey, field)); + modifyLengthFields.forEach(field -> alterProductPropertySTableModifyField(productKey, field)); + modifyTypeFields.forEach(field -> { + alterProductPropertySTableDropField(productKey, field); + alterProductPropertySTableAddField(productKey, field); + }); + } + + void alterProductPropertySTableAddField(@Param("productKey") String productKey, + @Param("field") TDengineTableField field); + + void alterProductPropertySTableModifyField(@Param("productKey") String productKey, + @Param("field") TDengineTableField field); + + void alterProductPropertySTableDropField(@Param("productKey") String productKey, + @Param("field") TDengineTableField field); + + void insert(@Param("device") IotDeviceDO device, + @Param("properties") Map properties, + @Param("reportTime") Long reportTime); + + IPage selectPageByHistory(IPage page, + @Param("reqVO") IotDevicePropertyHistoryPageReqVO reqVO); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/callback/EmqxCallback.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/callback/EmqxCallback.java deleted file mode 100644 index b466113f7..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/callback/EmqxCallback.java +++ /dev/null @@ -1,54 +0,0 @@ -package cn.iocoder.yudao.module.iot.emq.callback; - -import cn.iocoder.yudao.module.iot.emq.client.EmqxClient; -import cn.iocoder.yudao.module.iot.emq.service.EmqxService; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; -import org.eclipse.paho.client.mqttv3.MqttCallbackExtended; -import org.eclipse.paho.client.mqttv3.MqttMessage; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Component; - -// TODO @芋艿:详细再瞅瞅 -/** - * 用于处理MQTT连接的回调,如连接断开、消息到达、消息发布完成、连接完成等事件。 - * - * @author ahh - */ -@Slf4j -@Component -public class EmqxCallback implements MqttCallbackExtended { - - @Lazy - @Resource - private EmqxService emqxService; - - @Lazy - @Resource - private EmqxClient emqxClient; - - @Override - public void connectionLost(Throwable throwable) { - log.info("MQTT 连接断开", throwable); - } - - @Override - public void messageArrived(String topic, MqttMessage mqttMessage) { - emqxService.subscribeCallback(topic, mqttMessage); - } - - @Override - public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { - log.info("消息发送成功: {}", iMqttDeliveryToken.getMessageId()); - } - - @Override - public void connectComplete(boolean reconnect, String serverURI) { - log.info("MQTT 已连接到服务器: {}", serverURI); - emqxService.subscribe(emqxClient.getMqttClient()); - } -} - - - diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/client/EmqxClient.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/client/EmqxClient.java deleted file mode 100644 index de24585b0..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/client/EmqxClient.java +++ /dev/null @@ -1,86 +0,0 @@ -package cn.iocoder.yudao.module.iot.emq.client; - -import cn.iocoder.yudao.module.iot.emq.callback.EmqxCallback; -import cn.iocoder.yudao.module.iot.emq.config.MqttConfig; -import jakarta.annotation.Resource; -import lombok.Data; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.MqttClient; -import org.eclipse.paho.client.mqttv3.MqttConnectOptions; -import org.eclipse.paho.client.mqttv3.MqttException; -import org.eclipse.paho.client.mqttv3.MqttMessage; -import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; -import org.springframework.stereotype.Component; - -/** - * MQTT客户端类,负责建立与MQTT服务器的连接,提供发布消息和订阅主题的功能 - * - * @author ahh - */ -@Slf4j -@Data -@Component -public class EmqxClient { - - @Resource - private EmqxCallback emqxCallback; - @Resource - private MqttConfig mqttConfig; - - private MqttClient mqttClient; - - public void connect() { - if (mqttClient == null) { - createMqttClient(); - } - try { - mqttClient.connect(createMqttOptions()); - log.info("MQTT客户端连接成功"); - } catch (MqttException e) { - log.error("MQTT客户端连接失败", e); - } - } - - private void createMqttClient() { - try { - mqttClient = new MqttClient(mqttConfig.getHostUrl(), "yudao" + mqttConfig.getClientId(), new MemoryPersistence()); - mqttClient.setCallback(emqxCallback); - } catch (MqttException e) { - log.error("创建MQTT客户端失败", e); - } - } - - private MqttConnectOptions createMqttOptions() { - MqttConnectOptions options = new MqttConnectOptions(); - options.setUserName(mqttConfig.getUsername()); - options.setPassword(mqttConfig.getPassword().toCharArray()); - options.setConnectionTimeout(mqttConfig.getTimeout()); - options.setKeepAliveInterval(mqttConfig.getKeepalive()); - options.setCleanSession(mqttConfig.isClearSession()); - return options; - } - - public void publish(String topic, String message) { - try { - if (mqttClient == null || !mqttClient.isConnected()) { - connect(); - } - mqttClient.publish(topic, new MqttMessage(message.getBytes())); - log.info("消息已发布到主题: {}", topic); - } catch (MqttException e) { - log.error("消息发布失败", e); - } - } - - public void subscribe(String topic) { - try { - if (mqttClient == null || !mqttClient.isConnected()) { - connect(); - } - mqttClient.subscribe(topic); - log.info("订阅了主题: {}", topic); - } catch (MqttException e) { - log.error("订阅主题失败", e); - } - } -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/config/MqttConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/config/MqttConfig.java deleted file mode 100644 index 9d128903c..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/config/MqttConfig.java +++ /dev/null @@ -1,68 +0,0 @@ -package cn.iocoder.yudao.module.iot.emq.config; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -// TODO @芋艿:详细再瞅瞅 - -/** - * 配置类,用于读取MQTT连接的配置信息,如用户名、密码、连接地址等 - * - * @author ahh - */ -@Data -@Component -@ConfigurationProperties("iot.emq") -public class MqttConfig { - - /** - * 用户名 - */ - private String username; - /** - * 密码 - */ - private String password; - - /** - * 连接地址 - */ - private String hostUrl; - - /** - * 客户Id - */ - private String clientId; - - /** - * 默认连接话题 - */ - private String defaultTopic; - - /** - * 超时时间 - */ - private int timeout; - - /** - * 保持连接数 - */ - private int keepalive; - - /** - * 是否清除session - */ - private boolean clearSession; - - /** - * 是否共享订阅 - */ - private boolean isShared; - - /** - * 分组共享订阅 - */ - private boolean isSharedGroup; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/service/EmqxService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/service/EmqxService.java deleted file mode 100644 index 0d564c39f..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/service/EmqxService.java +++ /dev/null @@ -1,28 +0,0 @@ -package cn.iocoder.yudao.module.iot.emq.service; - -import org.eclipse.paho.client.mqttv3.MqttClient; -import org.eclipse.paho.client.mqttv3.MqttMessage; - -// TODO @芋艿:在瞅瞅 -/** - * 用于处理MQTT消息的具体业务逻辑,如订阅回调 - * - * @author ahh - */ -public interface EmqxService { - - /** - * 订阅回调 - * - * @param topic 主题 - * @param mqttMessage 消息 - */ - void subscribeCallback(String topic, MqttMessage mqttMessage); - - /** - * 订阅主题 - * - * @param client MQTT 客户端 - */ - void subscribe(MqttClient client); -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/service/EmqxServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/service/EmqxServiceImpl.java deleted file mode 100644 index 0c1a87f7f..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/service/EmqxServiceImpl.java +++ /dev/null @@ -1,39 +0,0 @@ -package cn.iocoder.yudao.module.iot.emq.service; - -import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.MqttClient; -import org.eclipse.paho.client.mqttv3.MqttMessage; -import org.springframework.stereotype.Service; - -// TODO @芋艿:在瞅瞅 - -/** - * 用于处理MQTT消息的具体业务逻辑,如订阅回调 - * - * @author ahh - */ -@Slf4j -@Service -public class EmqxServiceImpl implements EmqxService { - - // TODO 多线程处理消息 - @Override - public void subscribeCallback(String topic, MqttMessage mqttMessage) { - log.info("收到消息,主题: {}, 内容: {}", topic, new String(mqttMessage.getPayload())); - // 根据不同的主题,处理不同的业务逻辑 - if (topic.contains("/property/post")) { - // 设备上报数据 - } - } - - @Override - public void subscribe(MqttClient client) { - try { - // 订阅默认主题,可以根据需要修改 -// client.subscribe("$share/yudao/+/+/#", 1); - log.info("订阅默认主题成功"); - } catch (Exception e) { - log.error("订阅默认主题失败", e); - } - } -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/start/EmqxStart.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/start/EmqxStart.java deleted file mode 100644 index 0c316b66c..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/start/EmqxStart.java +++ /dev/null @@ -1,26 +0,0 @@ -package cn.iocoder.yudao.module.iot.emq.start; - -import cn.iocoder.yudao.module.iot.emq.client.EmqxClient; -import jakarta.annotation.Resource; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.stereotype.Component; - -// TODO @芋艿:在瞅瞅 - -/** - * 用于在应用启动时自动连接MQTT服务器 - * - * @author ahh - */ -@Component -public class EmqxStart implements ApplicationRunner { - - @Resource - private EmqxClient emqxClient; - - @Override - public void run(ApplicationArguments applicationArguments) { - emqxClient.connect(); - } -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/job/config/IotJobConfiguration.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/job/config/IotJobConfiguration.java new file mode 100644 index 000000000..7cd6f0961 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/job/config/IotJobConfiguration.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.iot.framework.job.config; + +import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.sql.DataSource; + +/** + * IoT 模块的 Job 自动配置类 + * + * @author 芋道源码 + */ +@Configuration +public class IotJobConfiguration { + + @Bean(initMethod = "start", destroyMethod = "stop") + public IotSchedulerManager iotSchedulerManager(DataSource dataSource, + ApplicationContext applicationContext) { + return new IotSchedulerManager(dataSource, applicationContext); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/job/core/IotSchedulerManager.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/job/core/IotSchedulerManager.java new file mode 100644 index 000000000..015b9ec3f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/job/core/IotSchedulerManager.java @@ -0,0 +1,186 @@ +package cn.iocoder.yudao.module.iot.framework.job.core; + +import lombok.extern.slf4j.Slf4j; +import org.quartz.*; +import org.springframework.context.ApplicationContext; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; +import org.springframework.scheduling.quartz.SpringBeanJobFactory; + +import javax.sql.DataSource; +import java.util.Map; +import java.util.Properties; + +/** + * IoT 模块的 Scheduler 管理类,基于 Quartz 实现 + * + * 疑问:为什么 IoT 模块不复用全局的 SchedulerManager 呢? + * 回复:yudao-cloud 项目,使用的是 XXL-Job 作为调度中心,无法动态添加任务。 + * + * @author 芋道源码 + */ +@Slf4j +public class IotSchedulerManager { + + private static final String SCHEDULER_NAME = "iotScheduler"; + + private final SchedulerFactoryBean schedulerFactoryBean; + + private Scheduler scheduler; + + public IotSchedulerManager(DataSource dataSource, + ApplicationContext applicationContext) { + // 1. 参考 SchedulerFactoryBean 类 + SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean(); + SpringBeanJobFactory jobFactory = new SpringBeanJobFactory(); + jobFactory.setApplicationContext(applicationContext); + schedulerFactoryBean.setJobFactory(jobFactory); + schedulerFactoryBean.setAutoStartup(true); + schedulerFactoryBean.setSchedulerName(SCHEDULER_NAME); + schedulerFactoryBean.setDataSource(dataSource); + schedulerFactoryBean.setWaitForJobsToCompleteOnShutdown(true); + Properties properties = new Properties(); + schedulerFactoryBean.setQuartzProperties(properties); + // 2. 参考 application-local.yaml 配置文件 + // 2.1 Scheduler 相关配置 + properties.put("org.quartz.scheduler.instanceName", SCHEDULER_NAME); + properties.put("org.quartz.scheduler.instanceId", "AUTO"); + // 2.2 JobStore 相关配置 + properties.put("org.quartz.jobStore.class", "org.springframework.scheduling.quartz.LocalDataSourceJobStore"); + properties.put("org.quartz.jobStore.isClustered", "true"); + properties.put("org.quartz.jobStore.clusterCheckinInterval", "15000"); + properties.put("org.quartz.jobStore.misfireThreshold", "60000"); + // 2.3 线程池相关配置 + properties.put("org.quartz.threadPool.threadCount", "25"); + properties.put("org.quartz.threadPool.threadPriority", "5"); + properties.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool"); + this.schedulerFactoryBean = schedulerFactoryBean; + } + + public void start() throws Exception { + log.info("[start][Scheduler 初始化开始]"); + // 初始化 + schedulerFactoryBean.afterPropertiesSet(); + schedulerFactoryBean.start(); + // 获得 Scheduler 对象 + this.scheduler = schedulerFactoryBean.getScheduler(); + log.info("[start][Scheduler 初始化完成]"); + } + + public void stop() { + log.info("[stop][Scheduler 关闭开始]"); + schedulerFactoryBean.stop(); + this.scheduler = null; + log.info("[stop][Scheduler 关闭完成]"); + } + + // ========== 参考 SchedulerManager 实现 ========== + + /** + * 添加或更新 Job 到 Quartz 中 + * + * @param jobClass 任务处理器的类 + * @param jobName 任务名 + * @param cronExpression CRON 表达式 + * @param jobDataMap 任务数据 + * @throws SchedulerException 添加异常 + */ + public void addOrUpdateJob(Class jobClass, String jobName, + String cronExpression, Map jobDataMap) + throws SchedulerException { + if (scheduler.checkExists(new JobKey(jobName))) { + this.updateJob(jobName, cronExpression); + } else { + this.addJob(jobClass, jobName, cronExpression, jobDataMap); + } + } + + /** + * 添加 Job 到 Quartz 中 + * + * @param jobClass 任务处理器的类 + * @param jobName 任务名 + * @param cronExpression CRON 表达式 + * @param jobDataMap 任务数据 + * @throws SchedulerException 添加异常 + */ + public void addJob(Class jobClass, String jobName, + String cronExpression, Map jobDataMap) + throws SchedulerException { + // 创建 JobDetail 对象 + JobDetail jobDetail = JobBuilder.newJob(jobClass) + .usingJobData(new JobDataMap(jobDataMap)) + .withIdentity(jobName).build(); + // 创建 Trigger 对象 + Trigger trigger = this.buildTrigger(jobName, cronExpression); + // 新增 Job 调度 + scheduler.scheduleJob(jobDetail, trigger); + } + + /** + * 更新 Job 到 Quartz + * + * @param jobName 任务名 + * @param cronExpression CRON 表达式 + * @throws SchedulerException 更新异常 + */ + public void updateJob(String jobName, String cronExpression) + throws SchedulerException { + // 创建新 Trigger 对象 + Trigger newTrigger = this.buildTrigger(jobName, cronExpression); + // 修改调度 + scheduler.rescheduleJob(new TriggerKey(jobName), newTrigger); + } + + /** + * 删除 Quartz 中的 Job + * + * @param jobName 任务名 + * @throws SchedulerException 删除异常 + */ + public void deleteJob(String jobName) throws SchedulerException { + // 暂停 Trigger 对象 + scheduler.pauseTrigger(new TriggerKey(jobName)); + // 取消并删除 Job 调度 + scheduler.unscheduleJob(new TriggerKey(jobName)); + scheduler.deleteJob(new JobKey(jobName)); + } + + /** + * 暂停 Quartz 中的 Job + * + * @param jobName 任务名 + * @throws SchedulerException 暂停异常 + */ + public void pauseJob(String jobName) throws SchedulerException { + scheduler.pauseJob(new JobKey(jobName)); + } + + /** + * 启动 Quartz 中的 Job + * + * @param jobName 任务名 + * @throws SchedulerException 启动异常 + */ + public void resumeJob(String jobName) throws SchedulerException { + scheduler.resumeJob(new JobKey(jobName)); + scheduler.resumeTrigger(new TriggerKey(jobName)); + } + + /** + * 立即触发一次 Quartz 中的 Job + * + * @param jobName 任务名 + * @throws SchedulerException 触发异常 + */ + public void triggerJob(String jobName) throws SchedulerException { + scheduler.triggerJob(new JobKey(jobName)); + } + + private Trigger buildTrigger(String jobName, String cronExpression) { + return TriggerBuilder.newTrigger() + .withIdentity(jobName) + .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)) + .build(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/config/IotPluginConfiguration.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/config/IotPluginConfiguration.java new file mode 100644 index 000000000..0a2812ac8 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/config/IotPluginConfiguration.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.iot.framework.plugin.config; + +import cn.iocoder.yudao.module.iot.framework.plugin.core.IotPluginStartRunner; +import cn.iocoder.yudao.module.iot.framework.plugin.core.IotPluginStateListener; +import cn.iocoder.yudao.module.iot.service.plugin.IotPluginConfigService; +import lombok.extern.slf4j.Slf4j; +import org.pf4j.spring.SpringPluginManager; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.nio.file.Paths; + +/** + * IoT 插件配置类 + * + * @author haohao + */ +@Configuration +@Slf4j +public class IotPluginConfiguration { + + @Bean + public IotPluginStartRunner pluginStartRunner(SpringPluginManager pluginManager, + IotPluginConfigService pluginConfigService) { + return new IotPluginStartRunner(pluginManager, pluginConfigService); + } + + // TODO @芋艿:需要 review 下 + @Bean + public SpringPluginManager pluginManager(@Value("${pf4j.pluginsDir:pluginsDir}") String pluginsDir) { + log.info("[init][实例化 SpringPluginManager]"); + SpringPluginManager springPluginManager = new SpringPluginManager(Paths.get(pluginsDir)) { + + @Override + public void startPlugins() { + // 禁用插件启动,避免插件启动时,启动所有插件 + log.info("[init][禁用默认启动所有插件]"); + } + + }; + springPluginManager.addPluginStateListener(new IotPluginStateListener()); + return springPluginManager; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/core/IotPluginStartRunner.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/core/IotPluginStartRunner.java new file mode 100644 index 000000000..64d258514 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/core/IotPluginStartRunner.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.iot.framework.plugin.core; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO; +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginDeployTypeEnum; +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; +import cn.iocoder.yudao.module.iot.service.plugin.IotPluginConfigService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.pf4j.spring.SpringPluginManager; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; + +import java.util.List; + +/** + * IoT 插件启动 Runner + * + * 用于 Spring Boot 启动时,启动 {@link IotPluginDeployTypeEnum#JAR} 部署类型的插件 + */ +@RequiredArgsConstructor +@Slf4j +public class IotPluginStartRunner implements ApplicationRunner { + + private final SpringPluginManager springPluginManager; + + private final IotPluginConfigService pluginConfigService; + + @Override + public void run(ApplicationArguments args) { + List pluginConfigList = TenantUtils.executeIgnore( + () -> pluginConfigService.getPluginConfigListByStatusAndDeployType( + IotPluginStatusEnum.RUNNING.getStatus(), IotPluginDeployTypeEnum.JAR.getDeployType())); + if (CollUtil.isEmpty(pluginConfigList)) { + log.info("[run][没有需要启动的插件]"); + return; + } + + // 遍历插件列表,逐个启动 + pluginConfigList.forEach(pluginConfig -> { + try { + log.info("[run][插件({}) 启动开始]", pluginConfig.getPluginKey()); + springPluginManager.startPlugin(pluginConfig.getPluginKey()); + log.info("[run][插件({}) 启动完成]", pluginConfig.getPluginKey()); + } catch (Exception e) { + log.error("[run][插件({}) 启动异常]", pluginConfig.getPluginKey(), e); + } + }); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/core/IotPluginStateListener.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/core/IotPluginStateListener.java new file mode 100644 index 000000000..bbc73c619 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/core/IotPluginStateListener.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.iot.framework.plugin.core; + +import lombok.extern.slf4j.Slf4j; +import org.pf4j.PluginStateEvent; +import org.pf4j.PluginStateListener; + +/** + * IoT 插件状态监听器,用于 log 插件的状态变化 + * + * @author haohao + */ +@Slf4j +public class IotPluginStateListener implements PluginStateListener { + + @Override + public void pluginStateChanged(PluginStateEvent event) { + log.info("[pluginStateChanged][插件({}) 状态变化,从 {} 变为 {}]", event.getPlugin().getPluginId(), + event.getOldState().toString(), event.getPluginState().toString()); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/config/SecurityConfiguration.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/config/SecurityConfiguration.java new file mode 100644 index 000000000..9cf00cc10 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/config/SecurityConfiguration.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.iot.framework.security.config; + +import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer; +import cn.iocoder.yudao.module.iot.enums.ApiConstants; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; + +/** + * IoT 模块的 Security 配置 + */ +@Configuration(proxyBeanMethods = false, value = "iotSecurityConfiguration") +public class SecurityConfiguration { + + @Bean("iotAuthorizeRequestsCustomizer") + public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { + return new AuthorizeRequestsCustomizer() { + + @Override + public void customize(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { + // RPC 服务的安全配置 + registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll(); + } + + }; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/core/package-info.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/core/package-info.java new file mode 100644 index 000000000..c714d1027 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.iocoder.yudao.module.iot.framework.security.core; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/config/TDengineTableInitRunner.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/config/TDengineTableInitRunner.java new file mode 100644 index 000000000..3517e1e58 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/config/TDengineTableInitRunner.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.iot.framework.tdengine.config; + +import cn.iocoder.yudao.module.iot.service.device.data.IotDeviceLogService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; + +/** + * TDengine 表初始化的 Configuration + * + * @author alwayssuper + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class TDengineTableInitRunner implements ApplicationRunner { + + private final IotDeviceLogService deviceLogService; + + @Override + public void run(ApplicationArguments args) { + try { + // 初始化设备日志表 + deviceLogService.defineDeviceLog(); + } catch (Exception ex) { + // 初始化失败时打印错误日志并退出系统 + log.error("[run][TDengine初始化设备日志表结构失败,系统无法正常运行,即将退出]", ex); + System.exit(1); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/TDengineTableField.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/TDengineTableField.java new file mode 100644 index 000000000..e3bbdd204 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/TDengineTableField.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.iot.framework.tdengine.core; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * TDEngine 表字段 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TDengineTableField { + + /** + * 字段名 - TDengine 默认 ts 字段,默认会被 TDengine 创建 + */ + public static final String FIELD_TS = "ts"; + + public static final String TYPE_TINYINT = "TINYINT"; + public static final String TYPE_INT = "INT"; + public static final String TYPE_FLOAT = "FLOAT"; + public static final String TYPE_DOUBLE = "DOUBLE"; + public static final String TYPE_BOOL = "BOOL"; + public static final String TYPE_NCHAR = "NCHAR"; + public static final String TYPE_TIMESTAMP = "TIMESTAMP"; + + /** + * 注释 - TAG 字段 + */ + public static final String NOTE_TAG = "TAG"; + + /** + * 字段名 + */ + private String field; + + /** + * 字段类型 + */ + private String type; + + /** + * 字段长度 + */ + private Integer length; + + /** + * 注释 + */ + private String note; + + public TDengineTableField(String field, String type) { + this.field = field; + this.type = type; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/annotation/TDengineDS.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/annotation/TDengineDS.java new file mode 100644 index 000000000..e3960d026 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/annotation/TDengineDS.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.module.iot.framework.tdengine.core.annotation; + +import com.baomidou.dynamic.datasource.annotation.DS; + +import java.lang.annotation.*; + +/** + * TDEngine 数据源 + * + * @author 芋道源码 + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@DS("tdengine") +public @interface TDengineDS { +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/package-info.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/package-info.java new file mode 100644 index 000000000..f92428f7b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/package-info.java @@ -0,0 +1,4 @@ +/** + * iot 模块的 tdengine 拓展封装 + */ +package cn.iocoder.yudao.module.iot.framework.tdengine; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/device/IotDeviceOfflineCheckJob.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/device/IotDeviceOfflineCheckJob.java new file mode 100644 index 000000000..4e9e9ecff --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/device/IotDeviceOfflineCheckJob.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.iot.job.device; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.IdUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.data.IotDevicePropertyService; +import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceUpstreamService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * IoT 设备离线检查 Job + * + * 检测逻辑:设备最后一条 {@link cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage} 消息超过一定时间,则认为设备离线 + * + * @author 芋道源码 + */ +@Component +public class IotDeviceOfflineCheckJob implements JobHandler { + + /** + * 设备离线超时时间 + * + * TODO 芋艿:暂定 10 分钟,后续看看要不要基于设备或者全局有配置文件 + */ + public static final Duration OFFLINE_TIMEOUT = Duration.ofMinutes(10); + + @Resource + private IotDeviceService deviceService; + @Resource + private IotDevicePropertyService devicePropertyService; + @Resource + private IotDeviceUpstreamService deviceUpstreamService; + + @Override + @TenantJob + public String execute(String param) { + // 1.1 获得在线设备列表 + List devices = deviceService.getDeviceListByState(IotDeviceStateEnum.ONLINE.getState()); + if (CollUtil.isEmpty(devices)) { + return JsonUtils.toJsonString(Collections.emptyList()); + } + // 1.2 获取超时的 deviceKey 集合 + Set timeoutDeviceKeys = devicePropertyService.getDeviceKeysByReportTime( + LocalDateTime.now().minus(OFFLINE_TIMEOUT)); + + // 2. 下线设备 + List offlineDeviceKeys = CollUtil.newArrayList(); + for (IotDeviceDO device : devices) { + if (!timeoutDeviceKeys.contains(device.getDeviceKey())) { + continue; + } + offlineDeviceKeys.add(device.getDeviceKey()); + // 为什么不直接更新状态呢?因为通过 IotDeviceMessage 可以经过一系列的处理,例如说记录日志等等 + deviceUpstreamService.updateDeviceState(((IotDeviceStateUpdateReqDTO) + new IotDeviceStateUpdateReqDTO().setRequestId(IdUtil.fastSimpleUUID()).setReportTime(LocalDateTime.now()) + .setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName())) + .setState((IotDeviceStateEnum.OFFLINE.getState()))); + } + return JsonUtils.toJsonString(offlineDeviceKeys); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/plugin/IotPluginInstancesJob.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/plugin/IotPluginInstancesJob.java new file mode 100644 index 000000000..ff93dc8db --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/plugin/IotPluginInstancesJob.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.iot.job.plugin; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; +import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import java.time.Duration; +import java.time.LocalDateTime; + +/** + * IoT 插件实例离线检查 Job + * + * @author 芋道源码 + */ +@Component +public class IotPluginInstancesJob implements JobHandler { + + /** + * 插件离线超时时间 + * + * TODO 芋艿:暂定 10 分钟,后续看要不要做配置 + */ + public static final Duration OFFLINE_TIMEOUT = Duration.ofMinutes(10); + + @Resource + private IotPluginInstanceService pluginInstanceService; + + @Override + @TenantJob + public String execute(String param) { + int count = pluginInstanceService.offlineTimeoutPluginInstance( + LocalDateTime.now().minus(OFFLINE_TIMEOUT)); + return StrUtil.format("离线超时插件实例数量为: {}", count); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/rule/IotRuleSceneJob.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/rule/IotRuleSceneJob.java new file mode 100644 index 000000000..594f9ef0b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/rule/IotRuleSceneJob.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.iot.job.rule; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.IotRuleSceneService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.quartz.JobExecutionContext; +import org.springframework.scheduling.quartz.QuartzJobBean; + +import java.util.Map; + +/** + * IoT 规则场景 Job,用于执行 {@link IotRuleSceneTriggerTypeEnum#TIMER} 类型的规则场景 + * + * @author 芋道源码 + */ +@Slf4j +public class IotRuleSceneJob extends QuartzJobBean { + + /** + * JobData Key - 规则场景编号 + */ + public static final String JOB_DATA_KEY_RULE_SCENE_ID = "ruleSceneId"; + + @Resource + private IotRuleSceneService ruleSceneService; + + @Override + protected void executeInternal(JobExecutionContext context) { + // 获得规则场景编号 + Long ruleSceneId = context.getMergedJobDataMap().getLong(JOB_DATA_KEY_RULE_SCENE_ID); + + // 执行规则场景 + ruleSceneService.executeRuleSceneByTimer(ruleSceneId); + } + + /** + * 创建 JobData Map + * + * @param ruleSceneId 规则场景编号 + * @return JobData Map + */ + public static Map buildJobDataMap(Long ruleSceneId) { + return MapUtil.of(JOB_DATA_KEY_RULE_SCENE_ID, ruleSceneId); + } + + /** + * 创建 Job 名字 + * + * @param ruleSceneId 规则场景编号 + * @return Job 名字 + */ + public static String buildJobName(Long ruleSceneId) { + return String.format("%s_%d", IotRuleSceneJob.class.getSimpleName(), ruleSceneId); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceLogMessageConsumer.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceLogMessageConsumer.java new file mode 100644 index 000000000..297267791 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceLogMessageConsumer.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.mq.consumer.device; + +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.service.device.data.IotDeviceLogService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +/** + * 针对 {@link IotDeviceMessage} 的消费者,记录设备日志 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class IotDeviceLogMessageConsumer { + + @Resource + private IotDeviceLogService deviceLogService; + + @EventListener + @Async + public void onMessage(IotDeviceMessage message) { + log.info("[onMessage][消息内容({})]", message); + deviceLogService.createDeviceLog(message); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceOnlineMessageConsumer.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceOnlineMessageConsumer.java new file mode 100644 index 000000000..f0e49bd47 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceOnlineMessageConsumer.java @@ -0,0 +1,85 @@ +package cn.iocoder.yudao.module.iot.mq.consumer.device; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceUpstreamService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * 针对 {@link IotDeviceMessage} 的消费者,将离线的设备,自动标记为上线 + * + * 注意:只有设备上行消息,才会触发该逻辑 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class IotDeviceOnlineMessageConsumer { + + @Resource + private IotDeviceService deviceService; + + @Resource + private IotDeviceUpstreamService deviceUpstreamService; + + @EventListener + @Async + public void onMessage(IotDeviceMessage message) { + // 1.1 只处理上行消息。因为,只有设备上行的消息,才会触发设备上线的逻辑 + if (!isUpstreamMessage(message)) { + return; + } + // 1.2 如果设备已在线,则不做处理 + log.info("[onMessage][消息内容({})]", message); + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache( + message.getProductKey(), message.getDeviceName()); + if (device == null) { + log.error("[onMessage][消息({}) 对应的设备部存在]", message); + return; + } + if (IotDeviceStateEnum.isOnline(device.getState())) { + return; + } + + // 2. 标记设备为在线 + // 为什么不直接更新状态呢?因为通过 IotDeviceMessage 可以经过一系列的处理,例如说记录日志等等 + deviceUpstreamService.updateDeviceState(((IotDeviceStateUpdateReqDTO) + new IotDeviceStateUpdateReqDTO().setRequestId(IdUtil.fastSimpleUUID()).setReportTime(LocalDateTime.now()) + .setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName())) + .setState((IotDeviceStateEnum.ONLINE.getState()))); + } + + private boolean isUpstreamMessage(IotDeviceMessage message) { + // 设备属性 + if (Objects.equals(message.getType(), IotDeviceMessageTypeEnum.PROPERTY.getType()) + && Objects.equals(message.getIdentifier(), IotDeviceMessageIdentifierEnum.PROPERTY_REPORT.getIdentifier())) { + return true; + } + // 设备事件 + if (Objects.equals(message.getType(), IotDeviceMessageTypeEnum.EVENT.getType())) { + return true; + } + // 设备服务 + // noinspection RedundantIfStatement + if (Objects.equals(message.getType(), IotDeviceMessageTypeEnum.SERVICE.getType()) + && !StrUtil.endWith(message.getIdentifier(), IotDeviceMessageIdentifierEnum.SERVICE_REPLY_SUFFIX.getIdentifier())) { + return true; + } + return false; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDevicePropertyMessageConsumer.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDevicePropertyMessageConsumer.java new file mode 100644 index 000000000..bf9cc5332 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDevicePropertyMessageConsumer.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.iot.mq.consumer.device; + +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.service.device.data.IotDevicePropertyService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; + +/** + * 针对 {@link IotDeviceMessage} 的消费者,记录设备属性 + * + * @author alwayssuper + */ +@Component +@Slf4j +public class IotDevicePropertyMessageConsumer { + + @Resource + private IotDevicePropertyService deviceDataService; + + @EventListener + @Async + public void onMessage(IotDeviceMessage message) { + if (ObjectUtil.notEqual(message.getType(), IotDeviceMessageTypeEnum.PROPERTY.getType()) + || ObjectUtil.notEqual(message.getIdentifier(), IotDeviceMessageIdentifierEnum.PROPERTY_REPORT.getIdentifier())) { + return; + } + log.info("[onMessage][消息内容({})]", message); + + // 保存设备属性 + deviceDataService.saveDeviceProperty(message); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotRuleSceneMessageHandler.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotRuleSceneMessageHandler.java new file mode 100644 index 000000000..e6ea3e22d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotRuleSceneMessageHandler.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.mq.consumer.rule; + +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.service.rule.IotRuleSceneService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +/** + * 针对 {@link IotDeviceMessage} 的消费者,处理规则场景 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class IotRuleSceneMessageHandler { + + @Resource + private IotRuleSceneService ruleSceneService; + + @EventListener + @Async + public void onMessage(IotDeviceMessage message) { + log.info("[onMessage][消息内容({})]", message); + ruleSceneService.executeRuleSceneByDevice(message); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/message/IotDeviceMessage.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/message/IotDeviceMessage.java new file mode 100644 index 000000000..0e8309a82 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/message/IotDeviceMessage.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.module.iot.mq.message; + +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +// TODO @芋艿:参考阿里云的物模型,优化 IoT 上下行消息的设计,尽量保持一致(渐进式,不要一口气)! +/** + * IoT 设备消息 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class IotDeviceMessage { + + /** + * 请求编号 + */ + private String requestId; + + /** + * 设备信息 + */ + private String productKey; + /** + * 设备名称 + */ + private String deviceName; + /** + * 设备标识 + */ + private String deviceKey; + + /** + * 消息类型 + * + * 枚举 {@link IotDeviceMessageTypeEnum} + */ + private String type; + /** + * 标识符 + * + * 枚举 {@link IotDeviceMessageIdentifierEnum} + */ + private String identifier; + + /** + * 请求参数 + * + * 例如说:属性上报的 properties、事件上报的 params + */ + private Object data; + /** + * 响应码 + * + * 目前只有 server 下行消息给 device 设备时,才会有响应码 + */ + private Integer code; + + /** + * 上报时间 + */ + private LocalDateTime reportTime; + + /** + * 租户编号 + */ + private Long tenantId; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/producer/device/IotDeviceProducer.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/producer/device/IotDeviceProducer.java new file mode 100644 index 000000000..11d5d96be --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/producer/device/IotDeviceProducer.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.mq.producer.device; + +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * IoT 设备相关消息的 Producer + * + * @author alwayssuper + * @since 2024/12/17 16:35 + */ +@Slf4j +@Component +public class IotDeviceProducer { + + @Resource + private ApplicationContext applicationContext; + + /** + * 发送 {@link IotDeviceMessage} 消息 + * + * @param thingModelMessage 物模型消息 + */ + public void sendDeviceMessage(IotDeviceMessage thingModelMessage) { + applicationContext.publishEvent(thingModelMessage); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/producer/package-info.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/producer/package-info.java new file mode 100644 index 000000000..37d0ba016 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/producer/package-info.java @@ -0,0 +1,4 @@ +/** + * TODO 芋艿:临时占位 + */ +package cn.iocoder.yudao.module.iot.mq.producer; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceServiceImpl.java deleted file mode 100644 index 2ae08bb94..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceServiceImpl.java +++ /dev/null @@ -1,227 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.device; - -import cn.hutool.core.util.IdUtil; -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.device.vo.IotDevicePageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDeviceSaveReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDeviceStatusUpdateReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; -import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceMapper; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStatusEnum; -import cn.iocoder.yudao.module.iot.service.product.IotProductService; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.validation.annotation.Validated; - -import java.time.LocalDateTime; -import java.util.UUID; - -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; - -/** - * IoT 设备 Service 实现类 - * - * @author 芋道源码 - */ -@Service -@Validated -@Slf4j -public class DeviceServiceImpl implements IotDeviceService { - - @Resource - private IotDeviceMapper deviceMapper; - @Resource - private IotProductService productService; - - /** - * 创建 IoT 设备 - * - * @param createReqVO 创建请求 VO - * @return 设备 ID - */ - @Override - @Transactional(rollbackFor = Exception.class) - public Long createDevice(IotDeviceSaveReqVO createReqVO) { - // 1.1 校验产品是否存在 - IotProductDO product = productService.getProduct(createReqVO.getProductId()); - if (product == null) { - throw exception(PRODUCT_NOT_EXISTS); - } - // 1.2 校验设备名称在同一产品下是否唯一 - if (StrUtil.isBlank(createReqVO.getDeviceName())) { - createReqVO.setDeviceName(generateUniqueDeviceName(product.getProductKey())); - } else { - validateDeviceNameUnique(product.getProductKey(), createReqVO.getDeviceName()); - } - - // 2.1 转换 VO 为 DO - IotDeviceDO device = BeanUtils.toBean(createReqVO, IotDeviceDO.class) - .setProductKey(product.getProductKey()) - .setDeviceType(product.getDeviceType()); - // 2.2 生成并设置必要的字段 - device.setDeviceKey(generateUniqueDeviceKey()); - device.setDeviceSecret(generateDeviceSecret()); - device.setMqttClientId(generateMqttClientId()); - device.setMqttUsername(generateMqttUsername(device.getDeviceName(), device.getProductKey())); - device.setMqttPassword(generateMqttPassword()); - // 2.3 设置设备状态为未激活 - device.setStatus(IotDeviceStatusEnum.INACTIVE.getStatus()); - device.setStatusLastUpdateTime(LocalDateTime.now()); - // 2.4 插入到数据库 - deviceMapper.insert(device); - return device.getId(); - } - - /** - * 校验设备名称在同一产品下是否唯一 - * - * @param productKey 产品 Key - * @param deviceName 设备名称 - */ - private void validateDeviceNameUnique(String productKey, String deviceName) { - IotDeviceDO existingDevice = deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName); - if (existingDevice != null) { - throw exception(DEVICE_NAME_EXISTS); - } - } - - /** - * 生成唯一的 deviceKey - * - * @return 生成的 deviceKey - */ - private String generateUniqueDeviceKey() { - return UUID.randomUUID().toString(); - } - - /** - * 生成 deviceSecret - * - * @return 生成的 deviceSecret - */ - private String generateDeviceSecret() { - return IdUtil.fastSimpleUUID(); - } - - /** - * 生成 MQTT Client ID - * - * @return 生成的 MQTT Client ID - */ - private String generateMqttClientId() { - return UUID.randomUUID().toString(); - } - - /** - * 生成 MQTT Username - * - * @param deviceName 设备名称 - * @param productKey 产品 Key - * @return 生成的 MQTT Username - */ - private String generateMqttUsername(String deviceName, String productKey) { - return deviceName + "&" + productKey; - } - - /** - * 生成 MQTT Password - * - * @return 生成的 MQTT Password - */ - private String generateMqttPassword() { - // TODO @haohao:【后续优化】在实际应用中,建议使用更安全的方法生成 MQTT Password,如加密或哈希 - return UUID.randomUUID().toString(); - } - - /** - * 生成唯一的 DeviceName - * - * @param productKey 产品标识 - * @return 生成的唯一 DeviceName - */ - private String generateUniqueDeviceName(String productKey) { - for (int i = 0; i < Short.MAX_VALUE; i++) { - String deviceName = IdUtil.fastSimpleUUID().substring(0, 20); - if (deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName) != null) { - return deviceName; - } - } - throw new IllegalArgumentException("生成 DeviceName 失败"); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void updateDevice(IotDeviceSaveReqVO updateReqVO) { - // 1. 校验存在 - validateDeviceExists(updateReqVO.getId()); - - // 2. 更新到数据库 - IotDeviceDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceDO.class) - .setDeviceName(null).setProductId(null); // 设备名称 和 产品 ID 不能修改 - deviceMapper.updateById(updateObj); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void deleteDevice(Long id) { - // 1.1 校验存在 - IotDeviceDO device = validateDeviceExists(id); - // 1.2 如果是网关设备,检查是否有子设备 - if (device.getGatewayId() != null && deviceMapper.selectCountByGatewayId(id) > 0) { - throw exception(DEVICE_HAS_CHILDREN); - } - - // 2. 删除设备 - deviceMapper.deleteById(id); - } - - /** - * 校验设备是否存在 - * - * @param id 设备 ID - * @return 设备对象 - */ - private IotDeviceDO validateDeviceExists(Long id) { - IotDeviceDO device = deviceMapper.selectById(id); - if (device == null) { - throw exception(DEVICE_NOT_EXISTS); - } - return device; - } - - @Override - public IotDeviceDO getDevice(Long id) { - IotDeviceDO device = deviceMapper.selectById(id); - if (device == null) { - throw exception(DEVICE_NOT_EXISTS); - } - return device; - } - - @Override - public PageResult getDevicePage(IotDevicePageReqVO pageReqVO) { - return deviceMapper.selectPage(pageReqVO); - } - - @Override - public void updateDeviceStatus(IotDeviceStatusUpdateReqVO updateReqVO) { - // 校验存在 - validateDeviceExists(updateReqVO.getId()); - - // 更新状态和更新时间 - IotDeviceDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceDO.class); - deviceMapper.updateById(updateObj); - } - - @Override - public Long getDeviceCountByProductId(Long productId) { - return deviceMapper.selectCountByProductId(productId); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupService.java new file mode 100644 index 000000000..5d074adb5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupService.java @@ -0,0 +1,97 @@ +package cn.iocoder.yudao.module.iot.service.device; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.group.IotDeviceGroupPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.group.IotDeviceGroupSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; +import jakarta.validation.Valid; + +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +/** + * IoT 设备分组 Service 接口 + * + * @author 芋道源码 + */ +public interface IotDeviceGroupService { + + /** + * 创建设备分组 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDeviceGroup(@Valid IotDeviceGroupSaveReqVO createReqVO); + + /** + * 更新设备分组 + * + * @param updateReqVO 更新信息 + */ + void updateDeviceGroup(@Valid IotDeviceGroupSaveReqVO updateReqVO); + + /** + * 删除设备分组 + * + * @param id 编号 + */ + void deleteDeviceGroup(Long id); + + /** + * 校验设备分组是否存在 + * + * @param ids 设备分组 ID 数组 + */ + default List validateDeviceGroupExists(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return ListUtil.empty(); + } + return convertList(ids, this::validateDeviceGroupExists); + } + + /** + * 校验设备分组是否存在 + * + * @param id 设备分组 ID + * @return 设备分组 + */ + IotDeviceGroupDO validateDeviceGroupExists(Long id); + + /** + * 获得设备分组 + * + * @param id 编号 + * @return 设备分组 + */ + IotDeviceGroupDO getDeviceGroup(Long id); + + /** + * 获得设备分组 + * + * @param name 名称 + * @return 设备分组 + */ + IotDeviceGroupDO getDeviceGroupByName(String name); + + /** + * 获得设备分组分页 + * + * @param pageReqVO 分页查询 + * @return 设备分组分页 + */ + PageResult getDeviceGroupPage(IotDeviceGroupPageReqVO pageReqVO); + + /** + * 获得设备分组列表 + * + * @param status 状态 + * @return 设备分组列表 + */ + List getDeviceGroupListByStatus(Integer status); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupServiceImpl.java new file mode 100644 index 000000000..06e5cb11d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupServiceImpl.java @@ -0,0 +1,94 @@ +package cn.iocoder.yudao.module.iot.service.device; + +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.device.vo.group.IotDeviceGroupPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.group.IotDeviceGroupSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; +import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceGroupMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +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.DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_GROUP_NOT_EXISTS; + +/** + * IoT 设备分组 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class IotDeviceGroupServiceImpl implements IotDeviceGroupService { + + @Resource + private IotDeviceGroupMapper deviceGroupMapper; + + @Resource + private IotDeviceService deviceService; + + @Override + public Long createDeviceGroup(IotDeviceGroupSaveReqVO createReqVO) { + // 插入 + IotDeviceGroupDO deviceGroup = BeanUtils.toBean(createReqVO, IotDeviceGroupDO.class); + deviceGroupMapper.insert(deviceGroup); + // 返回 + return deviceGroup.getId(); + } + + @Override + public void updateDeviceGroup(IotDeviceGroupSaveReqVO updateReqVO) { + // 校验存在 + validateDeviceGroupExists(updateReqVO.getId()); + // 更新 + IotDeviceGroupDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceGroupDO.class); + deviceGroupMapper.updateById(updateObj); + } + + @Override + public void deleteDeviceGroup(Long id) { + // 1.1 校验存在 + validateDeviceGroupExists(id); + // 1.2 校验是否存在设备 + if (deviceService.getDeviceCountByGroupId(id) > 0) { + throw exception(DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS); + } + + // 删除 + deviceGroupMapper.deleteById(id); + } + + @Override + public IotDeviceGroupDO validateDeviceGroupExists(Long id) { + IotDeviceGroupDO group = deviceGroupMapper.selectById(id); + if (group == null) { + throw exception(DEVICE_GROUP_NOT_EXISTS); + } + return group; + } + + @Override + public IotDeviceGroupDO getDeviceGroup(Long id) { + return deviceGroupMapper.selectById(id); + } + + @Override + public IotDeviceGroupDO getDeviceGroupByName(String name) { + return deviceGroupMapper.selectByName(name); + } + + @Override + public PageResult getDeviceGroupPage(IotDeviceGroupPageReqVO pageReqVO) { + return deviceGroupMapper.selectPage(pageReqVO); + } + + @Override + public List getDeviceGroupListByStatus(Integer status) { + return deviceGroupMapper.selectListByStatus(status); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java index 032a7478e..1dda3f333 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java @@ -1,9 +1,17 @@ package cn.iocoder.yudao.module.iot.service.device; -import jakarta.validation.*; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.*; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; + +import javax.annotation.Nullable; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Map; /** * IoT 设备 Service 接口 @@ -13,13 +21,25 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; public interface IotDeviceService { /** - * 创建设备 + * 【管理员】创建设备 * * @param createReqVO 创建信息 * @return 编号 */ Long createDevice(@Valid IotDeviceSaveReqVO createReqVO); + /** + * 【设备注册】创建设备 + * + * @param productKey 产品标识 + * @param deviceName 设备名称 + * @param gatewayId 网关设备 ID + * @return 设备 + */ + IotDeviceDO createDevice(@NotEmpty(message = "产品标识不能为空") String productKey, + @NotEmpty(message = "设备名称不能为空") String deviceName, + Long gatewayId); + /** * 更新设备 * @@ -27,13 +47,55 @@ public interface IotDeviceService { */ void updateDevice(@Valid IotDeviceSaveReqVO updateReqVO); + // TODO @芋艿:先这么实现。未来看情况,要不要自己实现 + /** - * 删除设备 + * 更新设备的所属网关 + * + * @param id 编号 + * @param gatewayId 网关设备 ID + */ + default void updateDeviceGateway(Long id, Long gatewayId) { + updateDevice(new IotDeviceSaveReqVO().setId(id).setGatewayId(gatewayId)); + } + + /** + * 更新设备状态 + * + * @param id 编号 + * @param state 状态 + */ + void updateDeviceState(Long id, Integer state); + + /** + * 更新设备分组 + * + * @param updateReqVO 更新信息 + */ + void updateDeviceGroup(@Valid IotDeviceUpdateGroupReqVO updateReqVO); + + /** + * 删除单个设备 * * @param id 编号 */ void deleteDevice(Long id); + /** + * 删除多个设备 + * + * @param ids 编号数组 + */ + void deleteDeviceList(Collection ids); + + /** + * 校验设备是否存在 + * + * @param id 设备 ID + * @return 设备对象 + */ + IotDeviceDO validateDeviceExists(Long id); + /** * 获得设备 * @@ -42,6 +104,14 @@ public interface IotDeviceService { */ IotDeviceDO getDevice(Long id); + /** + * 根据设备 key 获得设备 + * + * @param deviceKey 编号 + * @return IoT 设备 + */ + IotDeviceDO getDeviceByDeviceKey(String deviceKey); + /** * 获得设备分页 * @@ -51,17 +121,102 @@ public interface IotDeviceService { PageResult getDevicePage(IotDevicePageReqVO pageReqVO); /** - * 更新设备状态 + * 基于设备类型,获得设备列表 * - * @param updateReqVO 更新信息 + * @param deviceType 设备类型 + * @return 设备列表 */ - void updateDeviceStatus(IotDeviceStatusUpdateReqVO updateReqVO); + List getDeviceListByDeviceType(@Nullable Integer deviceType); /** - * 获得设备数量 + * 获得状态,获得设备列表 + * + * @param state 状态 + * @return 设备列表 + */ + List getDeviceListByState(Integer state); + + /** + * 根据产品ID获取设备列表 + * + * @param productId 产品ID,用于查询特定产品的设备列表 + * @return 返回与指定产品ID关联的设备列表,列表中的每个元素为IotDeviceDO对象 + */ + List getDeviceListByProductId(Long productId); + + /** + * 根据设备ID列表获取设备信息列表 + * + * @param deviceIdList 设备ID列表,包含需要查询的设备ID + * @return 返回与设备ID列表对应的设备信息列表,列表中的每个元素为IotDeviceDO对象 + */ + List getDeviceListByIdList(List deviceIdList); + + /** + * 基于产品编号,获得设备数量 * * @param productId 产品编号 * @return 设备数量 */ Long getDeviceCountByProductId(Long productId); + + /** + * 获得设备数量 + * + * @param groupId 分组编号 + * @return 设备数量 + */ + Long getDeviceCountByGroupId(Long groupId); + + /** + * 【缓存】根据产品 key 和设备名称,获得设备信息 + *

+ * 注意:该方法会忽略租户信息,所以调用时,需要确认会不会有跨租户访问的风险!!! + * + * @param productKey 产品 key + * @param deviceName 设备名称 + * @return 设备信息 + */ + IotDeviceDO getDeviceByProductKeyAndDeviceNameFromCache(String productKey, String deviceName); + + /** + * 导入设备 + * + * @param importDevices 导入设备列表 + * @param updateSupport 是否支持更新 + * @return 导入结果 + */ + IotDeviceImportRespVO importDevice(List importDevices, boolean updateSupport); + + /** + * 获得设备数量 + * + * @param createTime 创建时间,如果为空,则统计所有设备数量 + * @return 设备数量 + */ + Long getDeviceCount(@Nullable LocalDateTime createTime); + + /** + * 获取 MQTT 连接参数 + * + * @param deviceId 设备 ID + * @return MQTT 连接参数 + */ + IotDeviceMqttConnectionParamsRespVO getMqttConnectionParams(Long deviceId); + + /** + * 获得各个产品下的设备数量 Map + * + * @return key: 产品编号, value: 设备数量 + */ + Map getDeviceCountMapByProductId(); + + /** + * 获得各个状态下的设备数量 Map + * + * @return key: 设备状态枚举 {@link IotDeviceStateEnum} + * value: 设备数量 + */ + Map getDeviceCountMapByState(); + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java new file mode 100644 index 000000000..989f10a09 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java @@ -0,0 +1,454 @@ +package cn.iocoder.yudao.module.iot.service.device; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceMapper; +import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; +import cn.iocoder.yudao.module.iot.service.product.IotProductService; +import cn.iocoder.yudao.module.iot.util.MqttSignUtils; +import cn.iocoder.yudao.module.iot.util.MqttSignUtils.MqttSignResult; +import jakarta.annotation.Resource; +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Nullable; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; + +/** + * IoT 设备 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class IotDeviceServiceImpl implements IotDeviceService { + + @Resource + private IotDeviceMapper deviceMapper; + + @Resource + private IotProductService productService; + @Resource + @Lazy // 延迟加载,解决循环依赖 + private IotDeviceGroupService deviceGroupService; + + @Override + public Long createDevice(IotDeviceSaveReqVO createReqVO) { + // 1.1 校验产品是否存在 + IotProductDO product = productService.getProduct(createReqVO.getProductId()); + if (product == null) { + throw exception(PRODUCT_NOT_EXISTS); + } + // 1.2 统一校验 + validateCreateDeviceParam(product.getProductKey(), createReqVO.getDeviceName(), createReqVO.getDeviceKey(), + createReqVO.getGatewayId(), product); + // 1.3 校验分组存在 + deviceGroupService.validateDeviceGroupExists(createReqVO.getGroupIds()); + + // 2. 插入到数据库 + IotDeviceDO device = BeanUtils.toBean(createReqVO, IotDeviceDO.class); + initDevice(device, product); + deviceMapper.insert(device); + return device.getId(); + } + + @Override + public IotDeviceDO createDevice(String productKey, String deviceName, Long gatewayId) { + String deviceKey = generateDeviceKey(); + // 1.1 校验产品是否存在 + IotProductDO product = TenantUtils.executeIgnore(() -> productService.getProductByProductKey(productKey)); + if (product == null) { + throw exception(PRODUCT_NOT_EXISTS); + } + return TenantUtils.execute(product.getTenantId(), () -> { + // 1.2 校验设备名称在同一产品下是否唯一 + validateCreateDeviceParam(productKey, deviceName, deviceKey, gatewayId, product); + + // 2. 插入到数据库 + IotDeviceDO device = new IotDeviceDO().setDeviceName(deviceName).setDeviceKey(deviceKey) + .setGatewayId(gatewayId); + initDevice(device, product); + deviceMapper.insert(device); + return device; + }); + } + + private void validateCreateDeviceParam(String productKey, String deviceName, String deviceKey, + Long gatewayId, IotProductDO product) { + TenantUtils.executeIgnore(() -> { + // 校验设备名称在同一产品下是否唯一 + if (deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName) != null) { + throw exception(DEVICE_NAME_EXISTS); + } + // 校验设备标识是否唯一 + if (deviceMapper.selectByDeviceKey(deviceKey) != null) { + throw exception(DEVICE_KEY_EXISTS); + } + }); + + // 校验父设备是否为合法网关 + if (IotProductDeviceTypeEnum.isGatewaySub(product.getDeviceType()) + && gatewayId != null) { + validateGatewayDeviceExists(gatewayId); + } + } + + private void initDevice(IotDeviceDO device, IotProductDO product) { + device.setProductId(product.getId()).setProductKey(product.getProductKey()) + .setDeviceType(product.getDeviceType()); + // 生成密钥 + device.setDeviceSecret(generateDeviceSecret()); + // 设置设备状态为未激活 + device.setState(IotDeviceStateEnum.INACTIVE.getState()); + } + + @Override + public void updateDevice(IotDeviceSaveReqVO updateReqVO) { + updateReqVO.setDeviceKey(null).setDeviceName(null).setProductId(null); // 不允许更新 + // 1.1 校验存在 + IotDeviceDO device = validateDeviceExists(updateReqVO.getId()); + // 1.2 校验父设备是否为合法网关 + if (IotProductDeviceTypeEnum.isGatewaySub(device.getDeviceType()) + && updateReqVO.getGatewayId() != null) { + validateGatewayDeviceExists(updateReqVO.getGatewayId()); + } + // 1.3 校验分组存在 + deviceGroupService.validateDeviceGroupExists(updateReqVO.getGroupIds()); + + // 2. 更新到数据库 + IotDeviceDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceDO.class); + deviceMapper.updateById(updateObj); + + // 3. 清空对应缓存 + deleteDeviceCache(device); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateDeviceGroup(IotDeviceUpdateGroupReqVO updateReqVO) { + // 1.1 校验设备存在 + List devices = deviceMapper.selectBatchIds(updateReqVO.getIds()); + if (CollUtil.isEmpty(devices)) { + return; + } + // 1.2 校验分组存在 + deviceGroupService.validateDeviceGroupExists(updateReqVO.getGroupIds()); + + // 3. 更新设备分组 + deviceMapper.updateBatch(convertList(devices, device -> new IotDeviceDO() + .setId(device.getId()).setGroupIds(updateReqVO.getGroupIds()))); + + // 4. 清空对应缓存 + deleteDeviceCache(devices); + } + + @Override + public void deleteDevice(Long id) { + // 1.1 校验存在 + IotDeviceDO device = validateDeviceExists(id); + // 1.2 如果是网关设备,检查是否有子设备 + if (device.getGatewayId() != null && deviceMapper.selectCountByGatewayId(id) > 0) { + throw exception(DEVICE_HAS_CHILDREN); + } + + // 2. 删除设备 + deviceMapper.deleteById(id); + + // 3. 清空对应缓存 + deleteDeviceCache(device); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteDeviceList(Collection ids) { + // 1.1 校验存在 + if (CollUtil.isEmpty(ids)) { + return; + } + List devices = deviceMapper.selectBatchIds(ids); + if (CollUtil.isEmpty(devices)) { + return; + } + // 1.2 校验网关设备是否存在 + for (IotDeviceDO device : devices) { + if (device.getGatewayId() != null && deviceMapper.selectCountByGatewayId(device.getId()) > 0) { + throw exception(DEVICE_HAS_CHILDREN); + } + } + + // 2. 删除设备 + deviceMapper.deleteByIds(ids); + + // 3. 清空对应缓存 + deleteDeviceCache(devices); + } + + @Override + public IotDeviceDO validateDeviceExists(Long id) { + IotDeviceDO device = deviceMapper.selectById(id); + if (device == null) { + throw exception(DEVICE_NOT_EXISTS); + } + return device; + } + + /** + * 校验网关设备是否存在 + * + * @param id 设备 ID + */ + private void validateGatewayDeviceExists(Long id) { + IotDeviceDO device = deviceMapper.selectById(id); + if (device == null) { + throw exception(DEVICE_GATEWAY_NOT_EXISTS); + } + if (!IotProductDeviceTypeEnum.isGateway(device.getDeviceType())) { + throw exception(DEVICE_NOT_GATEWAY); + } + } + + @Override + public IotDeviceDO getDevice(Long id) { + return deviceMapper.selectById(id); + } + + @Override + public IotDeviceDO getDeviceByDeviceKey(String deviceKey) { + return deviceMapper.selectByDeviceKey(deviceKey); + } + + @Override + public PageResult getDevicePage(IotDevicePageReqVO pageReqVO) { + return deviceMapper.selectPage(pageReqVO); + } + + @Override + public List getDeviceListByDeviceType(@Nullable Integer deviceType) { + return deviceMapper.selectListByDeviceType(deviceType); + } + + @Override + public List getDeviceListByState(Integer state) { + return deviceMapper.selectListByState(state); + } + + @Override + public List getDeviceListByProductId(Long productId) { + return deviceMapper.selectListByProductId(productId); + } + + @Override + public List getDeviceListByIdList(List deviceIdList) { + return deviceMapper.selectByIds(deviceIdList); + } + + @Override + public void updateDeviceState(Long id, Integer state) { + // 1. 校验存在 + IotDeviceDO device = validateDeviceExists(id); + + // 2. 更新状态和时间 + IotDeviceDO updateObj = new IotDeviceDO().setId(id).setState(state); + if (device.getOnlineTime() == null + && Objects.equals(state, IotDeviceStateEnum.ONLINE.getState())) { + updateObj.setActiveTime(LocalDateTime.now()); + } + if (Objects.equals(state, IotDeviceStateEnum.ONLINE.getState())) { + updateObj.setOnlineTime(LocalDateTime.now()); + } else if (Objects.equals(state, IotDeviceStateEnum.OFFLINE.getState())) { + updateObj.setOfflineTime(LocalDateTime.now()); + } + deviceMapper.updateById(updateObj); + + // 3. 清空对应缓存 + deleteDeviceCache(device); + } + + @Override + public Long getDeviceCountByProductId(Long productId) { + return deviceMapper.selectCountByProductId(productId); + } + + @Override + public Long getDeviceCountByGroupId(Long groupId) { + return deviceMapper.selectCountByGroupId(groupId); + } + + @Override + @Cacheable(value = RedisKeyConstants.DEVICE, key = "#productKey + '_' + #deviceName", unless = "#result == null") + @TenantIgnore // 忽略租户信息,跨租户 productKey + deviceName 是唯一的 + public IotDeviceDO getDeviceByProductKeyAndDeviceNameFromCache(String productKey, String deviceName) { + return deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName); + } + + /** + * 生成 deviceKey + * + * @return 生成的 deviceKey + */ + private String generateDeviceKey() { + return RandomUtil.randomString(16); + } + + /** + * 生成 deviceSecret + * + * @return 生成的 deviceSecret + */ + private String generateDeviceSecret() { + return IdUtil.fastSimpleUUID(); + } + + @Override + @Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入 + public IotDeviceImportRespVO importDevice(List importDevices, boolean updateSupport) { + // 1. 参数校验 + if (CollUtil.isEmpty(importDevices)) { + throw exception(DEVICE_IMPORT_LIST_IS_EMPTY); + } + + // 2. 遍历,逐个创建 or 更新 + IotDeviceImportRespVO respVO = IotDeviceImportRespVO.builder().createDeviceNames(new ArrayList<>()) + .updateDeviceNames(new ArrayList<>()).failureDeviceNames(new LinkedHashMap<>()).build(); + importDevices.forEach(importDevice -> { + try { + // 2.1.1 校验字段是否符合要求 + try { + ValidationUtils.validate(importDevice); + } catch (ConstraintViolationException ex) { + respVO.getFailureDeviceNames().put(importDevice.getDeviceName(), ex.getMessage()); + return; + } + // 2.1.2 校验产品是否存在 + IotProductDO product = productService.validateProductExists(importDevice.getProductKey()); + // 2.1.3 校验父设备是否存在 + Long gatewayId = null; + if (StrUtil.isNotEmpty(importDevice.getParentDeviceName())) { + IotDeviceDO gatewayDevice = deviceMapper.selectByDeviceName(importDevice.getParentDeviceName()); + if (gatewayDevice == null) { + throw exception(DEVICE_GATEWAY_NOT_EXISTS); + } + if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) { + throw exception(DEVICE_NOT_GATEWAY); + } + gatewayId = gatewayDevice.getId(); + } + // 2.1.4 校验设备分组是否存在 + Set groupIds = new HashSet<>(); + if (StrUtil.isNotEmpty(importDevice.getGroupNames())) { + String[] groupNames = importDevice.getGroupNames().split(","); + for (String groupName : groupNames) { + IotDeviceGroupDO group = deviceGroupService.getDeviceGroupByName(groupName); + if (group == null) { + throw exception(DEVICE_GROUP_NOT_EXISTS); + } + groupIds.add(group.getId()); + } + } + + // 2.2.1 判断如果不存在,在进行插入 + IotDeviceDO existDevice = deviceMapper.selectByDeviceName(importDevice.getDeviceName()); + if (existDevice == null) { + createDevice(new IotDeviceSaveReqVO() + .setDeviceName(importDevice.getDeviceName()).setDeviceKey(generateDeviceKey()) + .setProductId(product.getId()).setGatewayId(gatewayId).setGroupIds(groupIds)); + respVO.getCreateDeviceNames().add(importDevice.getDeviceName()); + return; + } + // 2.2.2 如果存在,判断是否允许更新 + if (updateSupport) { + throw exception(DEVICE_KEY_EXISTS); + } + updateDevice(new IotDeviceSaveReqVO().setId(existDevice.getId()) + .setGatewayId(gatewayId).setGroupIds(groupIds)); + respVO.getUpdateDeviceNames().add(importDevice.getDeviceName()); + } catch (ServiceException ex) { + respVO.getFailureDeviceNames().put(importDevice.getDeviceName(), ex.getMessage()); + } + }); + return respVO; + } + + @Override + public IotDeviceMqttConnectionParamsRespVO getMqttConnectionParams(Long deviceId) { + IotDeviceDO device = validateDeviceExists(deviceId); + MqttSignResult mqttSignResult = MqttSignUtils.calculate(device.getProductKey(), device.getDeviceName(), + device.getDeviceSecret()); + return new IotDeviceMqttConnectionParamsRespVO() + .setMqttClientId(mqttSignResult.getClientId()) + .setMqttUsername(mqttSignResult.getUsername()) + .setMqttPassword(mqttSignResult.getPassword()); + } + + private void deleteDeviceCache(IotDeviceDO device) { + // 保证 Spring AOP 触发 + getSelf().deleteDeviceCache0(device); + } + + private void deleteDeviceCache(List devices) { + devices.forEach(this::deleteDeviceCache); + } + + @CacheEvict(value = RedisKeyConstants.DEVICE, key = "#device.productKey + '_' + #device.deviceName") + public void deleteDeviceCache0(IotDeviceDO device) { + } + + private IotDeviceServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + + @Override + public Long getDeviceCount(LocalDateTime createTime) { + return deviceMapper.selectCountByCreateTime(createTime); + } + + // TODO @super:简化 + @Override + public Map getDeviceCountMapByProductId() { + // 查询结果转换成Map + List> list = deviceMapper.selectDeviceCountMapByProductId(); + return list.stream().collect(Collectors.toMap( + map -> Long.valueOf(map.get("key").toString()), + map -> Integer.valueOf(map.get("value").toString()) + )); + } + + @Override + public Map getDeviceCountMapByState() { + // 查询结果转换成Map + List> list = deviceMapper.selectDeviceCountGroupByState(); + return list.stream().collect(Collectors.toMap( + map -> Integer.valueOf(map.get("key").toString()), + map -> Long.valueOf(map.get("value").toString()) + )); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceDownstreamService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceDownstreamService.java new file mode 100644 index 000000000..f09604dea --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceDownstreamService.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.iot.service.device.control; + +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceDownstreamReqVO; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import jakarta.validation.Valid; + +/** + * IoT 设备下行 Service 接口 + * + * 目的:服务端 -> 插件 -> 设备 + * + * @author 芋道源码 + */ +public interface IotDeviceDownstreamService { + + /** + * 设备下行,可用于设备模拟 + * + * @param downstreamReqVO 设备下行请求 VO + * @return 下发消息 + */ + IotDeviceMessage downstreamDevice(@Valid IotDeviceDownstreamReqVO downstreamReqVO); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceDownstreamServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceDownstreamServiceImpl.java new file mode 100644 index 000000000..3aab53de9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceDownstreamServiceImpl.java @@ -0,0 +1,354 @@ +package cn.iocoder.yudao.module.iot.service.device.control; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.IdUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceDownstreamReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.mq.producer.device.IotDeviceProducer; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_DOWNSTREAM_FAILED; + +/** + * IoT 设备下行 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class IotDeviceDownstreamServiceImpl implements IotDeviceDownstreamService { + + @Resource + private IotDeviceService deviceService; + @Resource + private IotPluginInstanceService pluginInstanceService; + + @Resource + private RestTemplate restTemplate; + + @Resource + private IotDeviceProducer deviceProducer; + + @Override + public IotDeviceMessage downstreamDevice(IotDeviceDownstreamReqVO downstreamReqVO) { + // 校验设备是否存在 + IotDeviceDO device = deviceService.validateDeviceExists(downstreamReqVO.getId()); + // TODO @芋艿:离线设备,不允许推送 + // TODO 芋艿:父设备的处理 + IotDeviceDO parentDevice = null; + + // 服务调用 + if (Objects.equals(downstreamReqVO.getType(), IotDeviceMessageTypeEnum.SERVICE.getType())) { + return invokeDeviceService(downstreamReqVO, device, parentDevice); + } + // 属性相关 + if (Objects.equals(downstreamReqVO.getType(), IotDeviceMessageTypeEnum.PROPERTY.getType())) { + // 属性设置 + if (Objects.equals(downstreamReqVO.getIdentifier(), + IotDeviceMessageIdentifierEnum.PROPERTY_SET.getIdentifier())) { + return setDeviceProperty(downstreamReqVO, device, parentDevice); + } + // 属性设置 + if (Objects.equals(downstreamReqVO.getIdentifier(), + IotDeviceMessageIdentifierEnum.PROPERTY_GET.getIdentifier())) { + return getDeviceProperty(downstreamReqVO, device, parentDevice); + } + } + // 配置下发 + if (Objects.equals(downstreamReqVO.getType(), IotDeviceMessageTypeEnum.CONFIG.getType()) + && Objects.equals(downstreamReqVO.getIdentifier(), + IotDeviceMessageIdentifierEnum.CONFIG_SET.getIdentifier())) { + return setDeviceConfig(downstreamReqVO, device, parentDevice); + } + // OTA 升级 + if (Objects.equals(downstreamReqVO.getType(), IotDeviceMessageTypeEnum.OTA.getType())) { + return otaUpgrade(downstreamReqVO, device, parentDevice); + } + // TODO @芋艿:取消设备的网关的时,要不要下发 REGISTER_UNREGISTER_SUB ? + throw new IllegalArgumentException("不支持的下行消息类型:" + downstreamReqVO); + } + + /** + * 调用设备服务 + * + * @param downstreamReqVO 下行请求 + * @param device 设备 + * @param parentDevice 父设备 + * @return 下发消息 + */ + @SuppressWarnings("unchecked") + private IotDeviceMessage invokeDeviceService(IotDeviceDownstreamReqVO downstreamReqVO, + IotDeviceDO device, IotDeviceDO parentDevice) { + // 1. 参数校验 + if (!(downstreamReqVO.getData() instanceof Map)) { + throw new ServiceException(BAD_REQUEST.getCode(), "data 不是 Map 类型"); + } + // TODO @super:【可优化】过滤掉不合法的服务 + + // 2. 发送请求 + String url = String.format("sys/%s/%s/thing/service/%s", + getProductKey(device, parentDevice), getDeviceName(device, parentDevice), + downstreamReqVO.getIdentifier()); + IotDeviceServiceInvokeReqDTO reqDTO = new IotDeviceServiceInvokeReqDTO() + .setParams((Map) downstreamReqVO.getData()); + CommonResult result = requestPlugin(url, reqDTO, device); + + // 3. 发送设备消息 + IotDeviceMessage message = new IotDeviceMessage().setRequestId(reqDTO.getRequestId()) + .setType(IotDeviceMessageTypeEnum.SERVICE.getType()).setIdentifier(reqDTO.getIdentifier()) + .setData(reqDTO.getParams()); + sendDeviceMessage(message, device, result.getCode()); + + // 4. 如果不成功,抛出异常,提示用户 + if (result.isError()) { + log.error("[invokeDeviceService][设备({})服务调用失败,请求参数:({}),响应结果:({})]", + device.getDeviceKey(), reqDTO, result); + throw exception(DEVICE_DOWNSTREAM_FAILED, result.getMsg()); + } + return message; + } + + /** + * 设置设备属性 + * + * @param downstreamReqVO 下行请求 + * @param device 设备 + * @param parentDevice 父设备 + * @return 下发消息 + */ + @SuppressWarnings("unchecked") + private IotDeviceMessage setDeviceProperty(IotDeviceDownstreamReqVO downstreamReqVO, + IotDeviceDO device, IotDeviceDO parentDevice) { + // 1. 参数校验 + if (!(downstreamReqVO.getData() instanceof Map)) { + throw new ServiceException(BAD_REQUEST.getCode(), "data 不是 Map 类型"); + } + // TODO @super:【可优化】过滤掉不合法的属性 + + // 2. 发送请求 + String url = String.format("sys/%s/%s/thing/service/property/set", + getProductKey(device, parentDevice), getDeviceName(device, parentDevice)); + IotDevicePropertySetReqDTO reqDTO = new IotDevicePropertySetReqDTO() + .setProperties((Map) downstreamReqVO.getData()); + CommonResult result = requestPlugin(url, reqDTO, device); + + // 3. 发送设备消息 + IotDeviceMessage message = new IotDeviceMessage().setRequestId(reqDTO.getRequestId()) + .setType(IotDeviceMessageTypeEnum.PROPERTY.getType()) + .setIdentifier(IotDeviceMessageIdentifierEnum.PROPERTY_SET.getIdentifier()) + .setData(reqDTO.getProperties()); + sendDeviceMessage(message, device, result.getCode()); + + // 4. 如果不成功,抛出异常,提示用户 + if (result.isError()) { + log.error("[setDeviceProperty][设备({})属性设置失败,请求参数:({}),响应结果:({})]", + device.getDeviceKey(), reqDTO, result); + throw exception(DEVICE_DOWNSTREAM_FAILED, result.getMsg()); + } + return message; + } + + /** + * 获取设备属性 + * + * @param downstreamReqVO 下行请求 + * @param device 设备 + * @param parentDevice 父设备 + * @return 下发消息 + */ + @SuppressWarnings("unchecked") + private IotDeviceMessage getDeviceProperty(IotDeviceDownstreamReqVO downstreamReqVO, + IotDeviceDO device, IotDeviceDO parentDevice) { + // 1. 参数校验 + if (!(downstreamReqVO.getData() instanceof List)) { + throw new ServiceException(BAD_REQUEST.getCode(), "data 不是 List 类型"); + } + // TODO @super:【可优化】过滤掉不合法的属性 + + // 2. 发送请求 + String url = String.format("sys/%s/%s/thing/service/property/get", + getProductKey(device, parentDevice), getDeviceName(device, parentDevice)); + IotDevicePropertyGetReqDTO reqDTO = new IotDevicePropertyGetReqDTO() + .setIdentifiers((List) downstreamReqVO.getData()); + CommonResult result = requestPlugin(url, reqDTO, device); + + // 3. 发送设备消息 + IotDeviceMessage message = new IotDeviceMessage().setRequestId(reqDTO.getRequestId()) + .setType(IotDeviceMessageTypeEnum.PROPERTY.getType()) + .setIdentifier(IotDeviceMessageIdentifierEnum.PROPERTY_SET.getIdentifier()) + .setData(reqDTO.getIdentifiers()); + sendDeviceMessage(message, device, result.getCode()); + + // 4. 如果不成功,抛出异常,提示用户 + if (result.isError()) { + log.error("[getDeviceProperty][设备({})属性获取失败,请求参数:({}),响应结果:({})]", + device.getDeviceKey(), reqDTO, result); + throw exception(DEVICE_DOWNSTREAM_FAILED, result.getMsg()); + } + return message; + } + + /** + * 设置设备配置 + * + * @param downstreamReqVO 下行请求 + * @param device 设备 + * @param parentDevice 父设备 + * @return 下发消息 + */ + @SuppressWarnings({ "unchecked", "unused" }) + private IotDeviceMessage setDeviceConfig(IotDeviceDownstreamReqVO downstreamReqVO, + IotDeviceDO device, IotDeviceDO parentDevice) { + // 1. 参数转换,无需校验 + Map config = JsonUtils.parseObject(device.getConfig(), Map.class); + + // 2. 发送请求 + String url = String.format("sys/%s/%s/thing/service/config/set", + getProductKey(device, parentDevice), getDeviceName(device, parentDevice)); + IotDeviceConfigSetReqDTO reqDTO = new IotDeviceConfigSetReqDTO() + .setConfig(config); + CommonResult result = requestPlugin(url, reqDTO, device); + + // 3. 发送设备消息 + IotDeviceMessage message = new IotDeviceMessage().setRequestId(reqDTO.getRequestId()) + .setType(IotDeviceMessageTypeEnum.CONFIG.getType()) + .setIdentifier(IotDeviceMessageIdentifierEnum.CONFIG_SET.getIdentifier()) + .setData(reqDTO.getConfig()); + sendDeviceMessage(message, device, result.getCode()); + + // 4. 如果不成功,抛出异常,提示用户 + if (result.isError()) { + log.error("[setDeviceConfig][设备({})配置下发失败,请求参数:({}),响应结果:({})]", + device.getDeviceKey(), reqDTO, result); + throw exception(DEVICE_DOWNSTREAM_FAILED, result.getMsg()); + } + return message; + } + + /** + * 设备 OTA 升级 + * + * @param downstreamReqVO 下行请求 + * @param device 设备 + * @param parentDevice 父设备 + * @return 下发消息 + */ + private IotDeviceMessage otaUpgrade(IotDeviceDownstreamReqVO downstreamReqVO, + IotDeviceDO device, IotDeviceDO parentDevice) { + // 1. 参数校验 + if (!(downstreamReqVO.getData() instanceof Map data)) { + throw new ServiceException(BAD_REQUEST.getCode(), "data 不是 Map 类型"); + } + + // 2. 发送请求 + String url = String.format("ota/%s/%s/upgrade", + getProductKey(device, parentDevice), getDeviceName(device, parentDevice)); + IotDeviceOtaUpgradeReqDTO reqDTO = IotDeviceOtaUpgradeReqDTO.build(data); + CommonResult result = requestPlugin(url, reqDTO, device); + + // 3. 发送设备消息 + IotDeviceMessage message = new IotDeviceMessage().setRequestId(reqDTO.getRequestId()) + .setType(IotDeviceMessageTypeEnum.OTA.getType()) + .setIdentifier(IotDeviceMessageIdentifierEnum.OTA_UPGRADE.getIdentifier()) + .setData(downstreamReqVO.getData()); + sendDeviceMessage(message, device, result.getCode()); + + // 4. 如果不成功,抛出异常,提示用户 + if (result.isError()) { + log.error("[otaUpgrade][设备({}) OTA 升级失败,请求参数:({}),响应结果:({})]", + device.getDeviceKey(), reqDTO, result); + throw exception(DEVICE_DOWNSTREAM_FAILED, result.getMsg()); + } + return message; + } + + /** + * 请求插件 + * + * @param url URL + * @param reqDTO 请求参数,只需要设置子类的参数! + * @param device 设备 + * @return 响应结果 + */ + @SuppressWarnings({ "unchecked", "HttpUrlsUsage" }) + private CommonResult requestPlugin(String url, IotDeviceDownstreamAbstractReqDTO reqDTO, + IotDeviceDO device) { + // 获得设备对应的插件实例 + IotPluginInstanceDO pluginInstance = pluginInstanceService.getPluginInstanceByDeviceKey(device.getDeviceKey()); + if (pluginInstance == null) { + throw exception(DEVICE_DOWNSTREAM_FAILED, "设备找不到对应的插件实例"); + } + + // 补充通用参数 + reqDTO.setRequestId(IdUtil.fastSimpleUUID()); + + // 执行请求 + ResponseEntity> responseEntity; + try { + responseEntity = restTemplate.postForEntity( + String.format("http://%s:%d/%s", pluginInstance.getHostIp(), pluginInstance.getDownstreamPort(), + url), + reqDTO, (Class>) (Class) CommonResult.class); + Assert.isTrue(responseEntity.getStatusCode().is2xxSuccessful(), + "HTTP 状态码不是 2xx,而是" + responseEntity.getStatusCode()); + Assert.notNull(responseEntity.getBody(), "响应结果不能为空"); + } catch (Exception ex) { + log.error("[requestPlugin][设备({}) url({}) 下行消息失败,请求参数({})]", device.getDeviceKey(), url, reqDTO, ex); + throw exception(DEVICE_DOWNSTREAM_FAILED, ExceptionUtil.getMessage(ex)); + } + return responseEntity.getBody(); + } + + private void sendDeviceMessage(IotDeviceMessage message, IotDeviceDO device, Integer code) { + // 1. 完善消息 + message.setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName()) + .setDeviceKey(device.getDeviceKey()) + .setTenantId(device.getTenantId()); + Assert.notNull(message.getRequestId(), "requestId 不能为空"); + if (message.getReportTime() == null) { + message.setReportTime(LocalDateTime.now()); + } + message.setCode(code); + + // 2. 发送消息 + try { + deviceProducer.sendDeviceMessage(message); + log.info("[sendDeviceMessage][message({}) 发送消息成功]", message); + } catch (Exception e) { + log.error("[sendDeviceMessage][message({}) 发送消息失败]", message, e); + } + } + + private String getDeviceName(IotDeviceDO device, IotDeviceDO parentDevice) { + return parentDevice != null ? parentDevice.getDeviceName() : device.getDeviceName(); + } + + private String getProductKey(IotDeviceDO device, IotDeviceDO parentDevice) { + return parentDevice != null ? parentDevice.getProductKey() : device.getProductKey(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamService.java new file mode 100644 index 000000000..dba529df2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamService.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.iot.service.device.control; + +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceUpstreamReqVO; +import jakarta.validation.Valid; + +/** + * IoT 设备上行 Service 接口 + * + * 目的:设备 -> 插件 -> 服务端 + * + * @author 芋道源码 + */ +public interface IotDeviceUpstreamService { + + /** + * 设备上行,可用于设备模拟 + * + * @param simulatorReqVO 设备上行请求 VO + */ + void upstreamDevice(@Valid IotDeviceUpstreamReqVO simulatorReqVO); + + /** + * 更新设备状态 + * + * @param updateReqDTO 更新设备状态 DTO + */ + void updateDeviceState(IotDeviceStateUpdateReqDTO updateReqDTO); + + /** + * 上报设备属性数据 + * + * @param reportReqDTO 上报设备属性数据 DTO + */ + void reportDeviceProperty(IotDevicePropertyReportReqDTO reportReqDTO); + + /** + * 上报设备事件数据 + * + * @param reportReqDTO 设备事件 + */ + void reportDeviceEvent(IotDeviceEventReportReqDTO reportReqDTO); + + /** + * 注册设备 + * + * @param registerReqDTO 注册设备 DTO + */ + void registerDevice(IotDeviceRegisterReqDTO registerReqDTO); + + /** + * 注册子设备 + * + * @param registerReqDTO 注册子设备 DTO + */ + void registerSubDevice(IotDeviceRegisterSubReqDTO registerReqDTO); + + /** + * 添加设备拓扑 + * + * @param addReqDTO 添加设备拓扑 DTO + */ + void addDeviceTopology(IotDeviceTopologyAddReqDTO addReqDTO); + + /** + * Emqx 连接认证 + * + * @param authReqDTO Emqx 连接认证 DTO + */ + boolean authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamServiceImpl.java new file mode 100644 index 000000000..6c80e75ac --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamServiceImpl.java @@ -0,0 +1,344 @@ +package cn.iocoder.yudao.module.iot.service.device.control; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceUpstreamReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.mq.producer.device.IotDeviceProducer; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.data.IotDevicePropertyService; +import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService; +import cn.iocoder.yudao.module.iot.util.MqttSignUtils; +import cn.iocoder.yudao.module.iot.util.MqttSignUtils.MqttSignResult; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Objects; + +/** + * IoT 设备上行 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class IotDeviceUpstreamServiceImpl implements IotDeviceUpstreamService { + + @Resource + private IotDeviceService deviceService; + @Resource + private IotDevicePropertyService devicePropertyService; + @Resource + private IotPluginInstanceService pluginInstanceService; + + @Resource + private IotDeviceProducer deviceProducer; + + @Override + @SuppressWarnings("unchecked") + public void upstreamDevice(IotDeviceUpstreamReqVO simulatorReqVO) { + // 1. 校验存在 + IotDeviceDO device = deviceService.validateDeviceExists(simulatorReqVO.getId()); + + // 2.1 情况一:属性上报 + String requestId = IdUtil.fastSimpleUUID(); + if (Objects.equals(simulatorReqVO.getType(), IotDeviceMessageTypeEnum.PROPERTY.getType())) { + reportDeviceProperty(((IotDevicePropertyReportReqDTO) new IotDevicePropertyReportReqDTO() + .setRequestId(requestId).setReportTime(LocalDateTime.now()) + .setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName())) + .setProperties((Map) simulatorReqVO.getData())); + return; + } + // 2.2 情况二:事件上报 + if (Objects.equals(simulatorReqVO.getType(), IotDeviceMessageTypeEnum.EVENT.getType())) { + reportDeviceEvent(((IotDeviceEventReportReqDTO) new IotDeviceEventReportReqDTO().setRequestId(requestId) + .setReportTime(LocalDateTime.now()) + .setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName())) + .setIdentifier(simulatorReqVO.getIdentifier()) + .setParams((Map) simulatorReqVO.getData())); + return; + } + // 2.3 情况三:状态变更 + if (Objects.equals(simulatorReqVO.getType(), IotDeviceMessageTypeEnum.STATE.getType())) { + updateDeviceState(((IotDeviceStateUpdateReqDTO) new IotDeviceStateUpdateReqDTO() + .setRequestId(IdUtil.fastSimpleUUID()).setReportTime(LocalDateTime.now()) + .setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName())) + .setState((Integer) simulatorReqVO.getData())); + return; + } + throw new IllegalArgumentException("未知的类型:" + simulatorReqVO.getType()); + } + + @Override + public void updateDeviceState(IotDeviceStateUpdateReqDTO updateReqDTO) { + Assert.isTrue(ObjectUtils.equalsAny(updateReqDTO.getState(), + IotDeviceStateEnum.ONLINE.getState(), IotDeviceStateEnum.OFFLINE.getState()), + "状态不合法"); + // 1.1 获得设备 + log.info("[updateDeviceState][更新设备状态: {}]", updateReqDTO); + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache( + updateReqDTO.getProductKey(), updateReqDTO.getDeviceName()); + if (device == null) { + log.error("[updateDeviceState][设备({}/{}) 不存在]", + updateReqDTO.getProductKey(), updateReqDTO.getDeviceName()); + return; + } + TenantUtils.execute(device.getTenantId(), () -> { + // 1.2 记录设备的最后时间 + updateDeviceLastTime(device, updateReqDTO); + // 1.3 当前状态一致,不处理 + if (Objects.equals(device.getState(), updateReqDTO.getState())) { + return; + } + + // 2. 更新设备状态 + deviceService.updateDeviceState(device.getId(), updateReqDTO.getState()); + + // 3. TODO 芋艿:子设备的关联 + + // 4. 发送设备消息 + IotDeviceMessage message = BeanUtils.toBean(updateReqDTO, IotDeviceMessage.class) + .setType(IotDeviceMessageTypeEnum.STATE.getType()) + .setIdentifier(ObjUtil.equals(updateReqDTO.getState(), IotDeviceStateEnum.ONLINE.getState()) + ? IotDeviceMessageIdentifierEnum.STATE_ONLINE.getIdentifier() + : IotDeviceMessageIdentifierEnum.STATE_OFFLINE.getIdentifier()); + sendDeviceMessage(message, device); + }); + } + + @Override + public void reportDeviceProperty(IotDevicePropertyReportReqDTO reportReqDTO) { + // 1.1 获得设备 + log.info("[reportDeviceProperty][上报设备属性: {}]", reportReqDTO); + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache( + reportReqDTO.getProductKey(), reportReqDTO.getDeviceName()); + if (device == null) { + log.error("[reportDeviceProperty][设备({}/{})不存在]", + reportReqDTO.getProductKey(), reportReqDTO.getDeviceName()); + return; + } + // 1.2 记录设备的最后时间 + updateDeviceLastTime(device, reportReqDTO); + + // 2. 发送设备消息 + IotDeviceMessage message = BeanUtils.toBean(reportReqDTO, IotDeviceMessage.class) + .setType(IotDeviceMessageTypeEnum.PROPERTY.getType()) + .setIdentifier(IotDeviceMessageIdentifierEnum.PROPERTY_REPORT.getIdentifier()) + .setData(reportReqDTO.getProperties()); + sendDeviceMessage(message, device); + } + + @Override + public void reportDeviceEvent(IotDeviceEventReportReqDTO reportReqDTO) { + // 1.1 获得设备 + log.info("[reportDeviceEvent][上报设备事件: {}]", reportReqDTO); + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache( + reportReqDTO.getProductKey(), reportReqDTO.getDeviceName()); + if (device == null) { + log.error("[reportDeviceEvent][设备({}/{})不存在]", + reportReqDTO.getProductKey(), reportReqDTO.getDeviceName()); + return; + } + // 1.2 记录设备的最后时间 + updateDeviceLastTime(device, reportReqDTO); + + // 2. 发送设备消息 + IotDeviceMessage message = BeanUtils.toBean(reportReqDTO, IotDeviceMessage.class) + .setType(IotDeviceMessageTypeEnum.EVENT.getType()) + .setIdentifier(reportReqDTO.getIdentifier()) + .setData(reportReqDTO.getParams()); + sendDeviceMessage(message, device); + } + + @Override + public void registerDevice(IotDeviceRegisterReqDTO registerReqDTO) { + log.info("[registerDevice][注册设备: {}]", registerReqDTO); + registerDevice0(registerReqDTO.getProductKey(), registerReqDTO.getDeviceName(), null, registerReqDTO); + } + + private void registerDevice0(String productKey, String deviceName, Long gatewayId, + IotDeviceUpstreamAbstractReqDTO registerReqDTO) { + // 1.1 注册设备 + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache(productKey, deviceName); + boolean registerNew = device == null; + if (device == null) { + device = deviceService.createDevice(productKey, deviceName, gatewayId); + log.info("[registerDevice0][消息({}) 设备({}/{}) 成功注册]", registerReqDTO, productKey, device); + } else if (gatewayId != null && ObjUtil.notEqual(device.getGatewayId(), gatewayId)) { + Long deviceId = device.getId(); + TenantUtils.execute(device.getTenantId(), + () -> deviceService.updateDeviceGateway(deviceId, gatewayId)); + log.info("[registerDevice0][消息({}) 设备({}/{}) 更新网关设备编号({})]", + registerReqDTO, productKey, device, gatewayId); + } + // 1.2 记录设备的最后时间 + updateDeviceLastTime(device, registerReqDTO); + + // 2. 发送设备消息 + if (registerNew) { + IotDeviceMessage message = BeanUtils.toBean(registerReqDTO, IotDeviceMessage.class) + .setType(IotDeviceMessageTypeEnum.REGISTER.getType()) + .setIdentifier(IotDeviceMessageIdentifierEnum.REGISTER_REGISTER.getIdentifier()); + sendDeviceMessage(message, device); + } + } + + @Override + public void registerSubDevice(IotDeviceRegisterSubReqDTO registerReqDTO) { + // 1.1 注册子设备 + log.info("[registerSubDevice][注册子设备: {}]", registerReqDTO); + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache( + registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); + if (device == null) { + log.error("[registerSubDevice][设备({}/{}) 不存在]", + registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); + return; + } + if (!IotProductDeviceTypeEnum.isGateway(device.getDeviceType())) { + log.error("[registerSubDevice][设备({}/{}) 不是网关设备({}),无法进行注册]", + registerReqDTO.getProductKey(), registerReqDTO.getDeviceName(), device); + return; + } + // 1.2 记录设备的最后时间 + updateDeviceLastTime(device, registerReqDTO); + + // 2. 处理子设备 + if (CollUtil.isNotEmpty(registerReqDTO.getParams())) { + registerReqDTO.getParams().forEach(subDevice -> registerDevice0( + subDevice.getProductKey(), subDevice.getDeviceName(), device.getId(), registerReqDTO)); + // TODO @芋艿:后续要处理,每个设备是否成功 + } + + // 3. 发送设备消息 + IotDeviceMessage message = BeanUtils.toBean(registerReqDTO, IotDeviceMessage.class) + .setType(IotDeviceMessageTypeEnum.REGISTER.getType()) + .setIdentifier(IotDeviceMessageIdentifierEnum.REGISTER_REGISTER_SUB.getIdentifier()) + .setData(registerReqDTO.getParams()); + sendDeviceMessage(message, device); + } + + @Override + public void addDeviceTopology(IotDeviceTopologyAddReqDTO addReqDTO) { + // 1.1 获得设备 + log.info("[addDeviceTopology][添加设备拓扑: {}]", addReqDTO); + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache( + addReqDTO.getProductKey(), addReqDTO.getDeviceName()); + if (device == null) { + log.error("[addDeviceTopology][设备({}/{}) 不存在]", + addReqDTO.getProductKey(), addReqDTO.getDeviceName()); + return; + } + if (!IotProductDeviceTypeEnum.isGateway(device.getDeviceType())) { + log.error("[addDeviceTopology][设备({}/{}) 不是网关设备({}),无法进行拓扑添加]", + addReqDTO.getProductKey(), addReqDTO.getDeviceName(), device); + return; + } + // 1.2 记录设备的最后时间 + updateDeviceLastTime(device, addReqDTO); + + // 2. 处理拓扑 + if (CollUtil.isNotEmpty(addReqDTO.getParams())) { + TenantUtils.execute(device.getTenantId(), () -> { + addReqDTO.getParams().forEach(subDevice -> { + IotDeviceDO subDeviceDO = deviceService.getDeviceByProductKeyAndDeviceNameFromCache( + subDevice.getProductKey(), subDevice.getDeviceName()); + // TODO @芋艿:后续要处理,每个设备是否成功 + if (subDeviceDO == null) { + log.error("[addDeviceTopology][子设备({}/{}) 不存在]", + subDevice.getProductKey(), subDevice.getDeviceName()); + return; + } + deviceService.updateDeviceGateway(subDeviceDO.getId(), device.getId()); + log.info("[addDeviceTopology][子设备({}/{}) 添加到网关设备({}) 成功]", + subDevice.getProductKey(), subDevice.getDeviceName(), device); + }); + }); + } + + // 3. 发送设备消息 + IotDeviceMessage message = BeanUtils.toBean(addReqDTO, IotDeviceMessage.class) + .setType(IotDeviceMessageTypeEnum.TOPOLOGY.getType()) + .setIdentifier(IotDeviceMessageIdentifierEnum.TOPOLOGY_ADD.getIdentifier()) + .setData(addReqDTO.getParams()); + sendDeviceMessage(message, device); + } + + // TODO @芋艿:后续需要考虑,http 的认证 + @Override + public boolean authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) { + log.info("[authenticateEmqxConnection][认证 Emqx 连接: {}]", authReqDTO); + // 1.1 校验设备是否存在。username 格式:${DeviceName}&${ProductKey} + String[] usernameParts = authReqDTO.getUsername().split("&"); + if (usernameParts.length != 2) { + log.error("[authenticateEmqxConnection][认证失败,username 格式不正确]"); + return false; + } + String deviceName = usernameParts[0]; + String productKey = usernameParts[1]; + // 1.2 获得设备 + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache(productKey, deviceName); + if (device == null) { + log.error("[authenticateEmqxConnection][设备({}/{}) 不存在]", productKey, deviceName); + return false; + } + // TODO @haohao:需要记录,记录设备的最后时间 + + // 2. 校验密码 + String deviceSecret = device.getDeviceSecret(); + String clientId = authReqDTO.getClientId(); + MqttSignResult sign = MqttSignUtils.calculate(productKey, deviceName, deviceSecret, clientId); + // TODO 建议,先失败,return false; + if (StrUtil.equals(sign.getPassword(), authReqDTO.getPassword())) { + log.info("[authenticateEmqxConnection][认证成功]"); + return true; + } + log.error("[authenticateEmqxConnection][认证失败,密码不正确]"); + return false; + } + + private void updateDeviceLastTime(IotDeviceDO device, IotDeviceUpstreamAbstractReqDTO reqDTO) { + // 1. 【异步】记录设备与插件实例的映射 + pluginInstanceService.updateDevicePluginInstanceProcessIdAsync(device.getDeviceKey(), reqDTO.getProcessId()); + + // 2. 【异步】更新设备的最后时间 + devicePropertyService.updateDeviceReportTimeAsync(device.getDeviceKey(), LocalDateTime.now()); + } + + private void sendDeviceMessage(IotDeviceMessage message, IotDeviceDO device) { + // 1. 完善消息 + message.setDeviceKey(device.getDeviceKey()) + .setTenantId(device.getTenantId()); + if (StrUtil.isEmpty(message.getRequestId())) { + message.setRequestId(IdUtil.fastSimpleUUID()); + } + if (message.getReportTime() == null) { + message.setReportTime(LocalDateTime.now()); + } + + // 2. 发送消息 + try { + deviceProducer.sendDeviceMessage(message); + log.info("[sendDeviceMessage][message({}) 发送消息成功]", message); + } catch (Exception e) { + log.error("[sendDeviceMessage][message({}) 发送消息失败]", message, e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogService.java new file mode 100644 index 000000000..b79732911 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogService.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.iot.service.device.data; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDeviceLogPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; + +import javax.annotation.Nullable; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * IoT 设备日志数据 Service 接口 + * + * @author alwayssuper + */ +public interface IotDeviceLogService { + + /** + * 初始化 TDengine 超级表 + * + * 系统启动时,会自动初始化一次 + */ + void defineDeviceLog(); + + /** + * 插入设备日志 + * + * @param message 设备数据 + */ + void createDeviceLog(IotDeviceMessage message); + + /** + * 获得设备日志分页 + * + * @param pageReqVO 分页查询 + * @return 设备日志分页 + */ + PageResult getDeviceLogPage(IotDeviceLogPageReqVO pageReqVO); + + /** + * 获得设备日志数量 + * + * @param createTime 创建时间,如果为空,则统计所有日志数量 + * @return 日志数量 + */ + Long getDeviceLogCount(@Nullable LocalDateTime createTime); + + // TODO @super:deviceKey 是不是用不上哈? + /** + * 获得每个小时设备上行消息数量统计 + * + * @param deviceKey 设备标识 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return key: 时间戳, value: 消息数量 + */ + List> getDeviceLogUpCountByHour(@Nullable String deviceKey, + @Nullable Long startTime, + @Nullable Long endTime); + + /** + * 获得每个小时设备下行消息数量统计 + * + * @param deviceKey 设备标识 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return key: 时间戳, value: 消息数量 + */ + List> getDeviceLogDownCountByHour(@Nullable String deviceKey, + @Nullable Long startTime, + @Nullable Long endTime); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogServiceImpl.java new file mode 100644 index 000000000..2ed2312bb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogServiceImpl.java @@ -0,0 +1,112 @@ +package cn.iocoder.yudao.module.iot.service.device.data; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDeviceLogPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO; +import cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceLogMapper; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * IoT 设备日志数据 Service 实现类 + * + * @author alwayssuper + */ +@Service +@Slf4j +@Validated +public class IotDeviceLogServiceImpl implements IotDeviceLogService { + + @Resource + private IotDeviceLogMapper deviceLogMapper; + + @Override + public void defineDeviceLog() { + if (StrUtil.isNotEmpty(deviceLogMapper.showDeviceLogSTable())) { + log.info("[defineDeviceLog][设备日志超级表已存在,创建跳过]"); + return; + } + + log.info("[defineDeviceLog][设备日志超级表不存在,创建开始...]"); + deviceLogMapper.createDeviceLogSTable(); + log.info("[defineDeviceLog][设备日志超级表不存在,创建成功]"); + } + + @Override + public void createDeviceLog(IotDeviceMessage message) { + IotDeviceLogDO log = BeanUtils.toBean(message, IotDeviceLogDO.class) + .setId(IdUtil.fastSimpleUUID()) + .setContent(JsonUtils.toJsonString(message.getData())); + deviceLogMapper.insert(log); + } + + @Override + public PageResult getDeviceLogPage(IotDeviceLogPageReqVO pageReqVO) { + try { + IPage page = deviceLogMapper.selectPage( + new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize()), pageReqVO); + return new PageResult<>(page.getRecords(), page.getTotal()); + } catch (Exception exception) { + if (exception.getMessage().contains("Table does not exist")) { + return PageResult.empty(); + } + throw exception; + } + } + + @Override + public Long getDeviceLogCount(LocalDateTime createTime) { + return deviceLogMapper.selectCountByCreateTime(createTime != null ? LocalDateTimeUtil.toEpochMilli(createTime) : null); + } + + // TODO @super:加一个参数,Boolean upstream:true 上行,false 下行,null 不过滤 + @Override + public List> getDeviceLogUpCountByHour(String deviceKey, Long startTime, Long endTime) { + // TODO @super:不能只基于数据库统计。因为有一些小时,可能出现没数据的情况,导致前端展示的图是不全的。可以参考 CrmStatisticsCustomerService 来实现 + List> list = deviceLogMapper.selectDeviceLogUpCountByHour(deviceKey, startTime, endTime); + return list.stream() + .map(map -> { + // 从Timestamp获取时间戳 + Timestamp timestamp = (Timestamp) map.get("time"); + Long timeMillis = timestamp.getTime(); + // 消息数量转换 + Integer count = ((Number) map.get("data")).intValue(); + return MapUtil.of(timeMillis, count); + }) + .collect(Collectors.toList()); + } + + // TODO @super:getDeviceLogDownCountByHour 融合到 getDeviceLogUpCountByHour + @Override + public List> getDeviceLogDownCountByHour(String deviceKey, Long startTime, Long endTime) { + List> list = deviceLogMapper.selectDeviceLogDownCountByHour(deviceKey, startTime, endTime); + return list.stream() + .map(map -> { + // 从Timestamp获取时间戳 + Timestamp timestamp = (Timestamp) map.get("time"); + Long timeMillis = timestamp.getTime(); + // 消息数量转换 + Integer count = ((Number) map.get("data")).intValue(); + return MapUtil.of(timeMillis, count); + }) + .collect(Collectors.toList()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDevicePropertyService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDevicePropertyService.java new file mode 100644 index 000000000..2f0626865 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDevicePropertyService.java @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.iot.service.device.data; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyHistoryPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyRespVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import jakarta.validation.Valid; + +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Set; + +/** + * IoT 设备【属性】数据 Service 接口 + * + * @author 芋道源码 + */ +public interface IotDevicePropertyService { + + // ========== 设备属性相关操作 ========== + + /** + * 定义设备属性数据的结构 + * + * @param productId 产品编号 + */ + void defineDevicePropertyData(Long productId); + + /** + * 保存设备数据 + * + * @param message 设备消息 + */ + void saveDeviceProperty(IotDeviceMessage message); + + /** + * 获得设备属性最新数据 + * + * @param deviceId 设备编号 + * @return 设备属性最新数据 + */ + Map getLatestDeviceProperties(Long deviceId); + + /** + * 获得设备属性历史数据 + * + * @param pageReqVO 分页请求 + * @return 设备属性历史数据 + */ + PageResult getHistoryDevicePropertyPage(@Valid IotDevicePropertyHistoryPageReqVO pageReqVO); + + // ========== 设备时间相关操作 ========== + + /** + * 获得最后上报时间小于指定时间的设备标识 + * + * @param maxReportTime 最大上报时间 + * @return 设备标识列表 + */ + Set getDeviceKeysByReportTime(LocalDateTime maxReportTime); + + /** + * 异步更新设备上报时间 + * + * @param deviceKey 设备标识 + * @param reportTime 上报时间 + */ + void updateDeviceReportTimeAsync(String deviceKey, LocalDateTime reportTime); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDevicePropertyServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDevicePropertyServiceImpl.java new file mode 100644 index 000000000..77dde64a6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDevicePropertyServiceImpl.java @@ -0,0 +1,200 @@ +package cn.iocoder.yudao.module.iot.service.device.data; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyHistoryPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType.ThingModelDateOrTextDataSpecs; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; +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.redis.device.DevicePropertyRedisDAO; +import cn.iocoder.yudao.module.iot.dal.redis.device.DeviceReportTimeRedisDAO; +import cn.iocoder.yudao.module.iot.dal.tdengine.IotDevicePropertyMapper; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum; +import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.product.IotProductService; +import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; + +/** + * IoT 设备【属性】数据 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { + + /** + * 物模型的数据类型,与 TDengine 数据类型的映射关系 + */ + private static final Map TYPE_MAPPING = MapUtil.builder() + .put(IotDataSpecsDataTypeEnum.INT.getDataType(), TDengineTableField.TYPE_INT) + .put(IotDataSpecsDataTypeEnum.FLOAT.getDataType(), TDengineTableField.TYPE_FLOAT) + .put(IotDataSpecsDataTypeEnum.DOUBLE.getDataType(), TDengineTableField.TYPE_DOUBLE) + .put(IotDataSpecsDataTypeEnum.ENUM.getDataType(), TDengineTableField.TYPE_TINYINT) // TODO 芋艿:为什么要映射为 TINYINT 的说明? + .put(IotDataSpecsDataTypeEnum.BOOL.getDataType(), TDengineTableField.TYPE_TINYINT) // TODO 芋艿:为什么要映射为 TINYINT 的说明? + .put(IotDataSpecsDataTypeEnum.TEXT.getDataType(), TDengineTableField.TYPE_NCHAR) + .put(IotDataSpecsDataTypeEnum.DATE.getDataType(), TDengineTableField.TYPE_TIMESTAMP) + .put(IotDataSpecsDataTypeEnum.STRUCT.getDataType(), TDengineTableField.TYPE_NCHAR) // TODO 芋艿:怎么映射!!!! + .put(IotDataSpecsDataTypeEnum.ARRAY.getDataType(), TDengineTableField.TYPE_NCHAR) // TODO 芋艿:怎么映射!!!! + .build(); + + @Resource + private IotDeviceService deviceService; + @Resource + private IotThingModelService thingModelService; + @Resource + private IotProductService productService; + + @Resource + private DevicePropertyRedisDAO deviceDataRedisDAO; + @Resource + private DeviceReportTimeRedisDAO deviceReportTimeRedisDAO; + + @Resource + private IotDevicePropertyMapper devicePropertyMapper; + + // ========== 设备属性相关操作 ========== + + @Override + public void defineDevicePropertyData(Long productId) { + // 1.1 查询产品和物模型 + IotProductDO product = productService.validateProductExists(productId); + List thingModels = filterList(thingModelService.getThingModelListByProductId(productId), + thingModel -> IotThingModelTypeEnum.PROPERTY.getType().equals(thingModel.getType())); + // 1.2 解析 DB 里的字段 + List oldFields = new ArrayList<>(); + try { + oldFields.addAll(devicePropertyMapper.getProductPropertySTableFieldList(product.getProductKey())); + } catch (Exception e) { + if (!e.getMessage().contains("Table does not exist")) { + throw e; + } + } + + // 2.1 情况一:如果是新增的时候,需要创建表 + List newFields = buildTableFieldList(thingModels); + if (CollUtil.isEmpty(oldFields)) { + if (CollUtil.isEmpty(newFields)) { + log.info("[defineDevicePropertyData][productId({}) 没有需要定义的属性]", productId); + return; + } + devicePropertyMapper.createProductPropertySTable(product.getProductKey(), newFields); + return; + } + // 2.2 情况二:如果是修改的时候,需要更新表 + devicePropertyMapper.alterProductPropertySTable(product.getProductKey(), oldFields, newFields); + } + + private List buildTableFieldList(List thingModels) { + return convertList(thingModels, thingModel -> { + TDengineTableField field = new TDengineTableField( + StrUtil.toUnderlineCase(thingModel.getIdentifier()), // TDengine 字段默认都是小写 + TYPE_MAPPING.get(thingModel.getProperty().getDataType())); + if (thingModel.getProperty().getDataType().equals(IotDataSpecsDataTypeEnum.TEXT.getDataType())) { + field.setLength(((ThingModelDateOrTextDataSpecs) thingModel.getProperty().getDataSpecs()).getLength()); + } + return field; + }); + } + + @Override + @TenantIgnore + public void saveDeviceProperty(IotDeviceMessage message) { + if (!(message.getData() instanceof Map)) { + log.error("[saveDeviceProperty][消息内容({}) 的 data 类型不正确]", message); + return; + } + // 1. 获得设备信息 + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache(message.getProductKey(), message.getDeviceName()); + if (device == null) { + log.error("[saveDeviceProperty][消息({}) 对应的设备不存在]", message); + return; + } + + // 2. 根据物模型,拼接合法的属性 + // TODO @芋艿:【待定 004】赋能后,属性到底以 thingModel 为准(ik),还是 db 的表结构为准(tl)? + List thingModels = thingModelService.getThingModelListByProductKeyFromCache(device.getProductKey()); + Map properties = new HashMap<>(); + ((Map) message.getData()).forEach((key, value) -> { + if (CollUtil.findOne(thingModels, thingModel -> thingModel.getIdentifier().equals(key)) == null) { + log.error("[saveDeviceProperty][消息({}) 的属性({}) 不存在]", message, key); + return; + } + properties.put((String) key, value); + }); + if (CollUtil.isEmpty(properties)) { + log.error("[saveDeviceProperty][消息({}) 没有合法的属性]", message); + return; + } + + // 3.1 保存设备属性【数据】 + devicePropertyMapper.insert(device, properties, + LocalDateTimeUtil.toEpochMilli(message.getReportTime())); + + // 3.2 保存设备属性【日志】 + deviceDataRedisDAO.putAll(message.getDeviceKey(), convertMap(properties.entrySet(), Map.Entry::getKey, + entry -> IotDevicePropertyDO.builder().value(entry.getValue()).updateTime(message.getReportTime()).build())); + } + + @Override + public Map getLatestDeviceProperties(Long deviceId) { + // 获取设备信息 + IotDeviceDO device = deviceService.validateDeviceExists(deviceId); + + // 获得设备属性 + return deviceDataRedisDAO.get(device.getDeviceKey()); + } + + @Override + public PageResult getHistoryDevicePropertyPage(IotDevicePropertyHistoryPageReqVO pageReqVO) { + // 获取设备信息 + IotDeviceDO device = deviceService.validateDeviceExists(pageReqVO.getDeviceId()); + pageReqVO.setDeviceKey(device.getDeviceKey()); + + try { + IPage page = devicePropertyMapper.selectPageByHistory( + new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize()), pageReqVO); + return new PageResult<>(page.getRecords(), page.getTotal()); + } catch (Exception exception) { + if (exception.getMessage().contains("Table does not exist")) { + return PageResult.empty(); + } + throw exception; + } + } + + // ========== 设备时间相关操作 ========== + + @Override + public Set getDeviceKeysByReportTime(LocalDateTime maxReportTime) { + return deviceReportTimeRedisDAO.range(maxReportTime); + } + + @Override + @Async + public void updateDeviceReportTimeAsync(String deviceKey, LocalDateTime reportTime) { + deviceReportTimeRedisDAO.update(deviceKey, reportTime); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareService.java new file mode 100644 index 000000000..99e3b382a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareService.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.iot.service.ota; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareCreateReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwarePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareUpdateReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import jakarta.validation.Valid; + +// TODO @li:注释写的有点冗余,可以看看别的模块哈。= = AI 生成的注释,有的时候太啰嗦了,需要处理下的哈 +/** + * OTA 固件管理 Service + * + * @author Shelly Chan + */ +public interface IotOtaFirmwareService { + + /** + * 创建 OTA 固件 + * + * @param saveReqVO OTA固件保存请求对象,包含固件的相关信息 + * @return 返回新创建的固件的ID + */ + Long createOtaFirmware(@Valid IotOtaFirmwareCreateReqVO saveReqVO); + + /** + * 更新 OTA 固件信息 + * + * @param updateReqVO OTA固件保存请求对象,包含需要更新的固件信息 + */ + void updateOtaFirmware(@Valid IotOtaFirmwareUpdateReqVO updateReqVO); + + /** + * 根据 ID 获取 OTA 固件信息 + * + * @param id OTA固件的唯一标识符 + * @return 返回OTA固件的详细信息对象 + */ + IotOtaFirmwareDO getOtaFirmware(Long id); + + /** + * 分页查询 OTA 固件信息 + * + * @param pageReqVO 包含分页查询条件的请求对象 + * @return 返回分页查询结果,包含固件信息列表和分页信息 + */ + PageResult getOtaFirmwarePage(@Valid IotOtaFirmwarePageReqVO pageReqVO); + + /** + * 验证物联网 OTA 固件是否存在 + * + * @param id 固件的唯一标识符 + * 该方法用于检查系统中是否存在与给定ID关联的物联网OTA固件信息 + * 主要目的是在进行固件更新操作前,确保目标固件已经存在并可以被访问 + * 如果固件不存在,该方法可能抛出异常或返回错误信息,具体行为未定义 + */ + IotOtaFirmwareDO validateFirmwareExists(Long id); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareServiceImpl.java new file mode 100644 index 000000000..7c0ddba7c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareServiceImpl.java @@ -0,0 +1,104 @@ +package cn.iocoder.yudao.module.iot.service.ota; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +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.ota.vo.firmware.IotOtaFirmwareCreateReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwarePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareUpdateReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.dal.mysql.ota.IotOtaFirmwareMapper; +import cn.iocoder.yudao.module.iot.service.product.IotProductService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.OTA_FIRMWARE_NOT_EXISTS; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.OTA_FIRMWARE_PRODUCT_VERSION_DUPLICATE; + +@Slf4j +@Service +@Validated +public class IotOtaFirmwareServiceImpl implements IotOtaFirmwareService { + + @Resource + private IotOtaFirmwareMapper otaFirmwareMapper; + @Lazy + @Resource + private IotProductService productService; + + @Override + public Long createOtaFirmware(IotOtaFirmwareCreateReqVO saveReqVO) { + // 1. 校验固件产品 + 版本号不能重复 + validateProductAndVersionDuplicate(saveReqVO.getProductId(), saveReqVO.getVersion()); + + // 2.1.转化数据格式,准备存储到数据库中 + IotOtaFirmwareDO firmware = BeanUtils.toBean(saveReqVO, IotOtaFirmwareDO.class); + // 2.2.查询ProductKey + // TODO @li:productService.getProduct(Convert.toLong(firmware.getProductId())) 放到 1. 后面,先做参考校验。逻辑两段:1)先参数校验;2)构建对象 + 存储 + IotProductDO product = productService.getProduct(Convert.toLong(firmware.getProductId())); + firmware.setProductKey(Objects.requireNonNull(product).getProductKey()); + // TODO @芋艿: 附件、附件签名等属性的计算 + otaFirmwareMapper.insert(firmware); + return firmware.getId(); + } + + @Override + public void updateOtaFirmware(IotOtaFirmwareUpdateReqVO updateReqVO) { + // 1. 校验存在 + validateFirmwareExists(updateReqVO.getId()); + + // 2. 更新数据 + IotOtaFirmwareDO updateObj = BeanUtils.toBean(updateReqVO, IotOtaFirmwareDO.class); + otaFirmwareMapper.updateById(updateObj); + } + + @Override + public IotOtaFirmwareDO getOtaFirmware(Long id) { + return otaFirmwareMapper.selectById(id); + } + + @Override + public PageResult getOtaFirmwarePage(IotOtaFirmwarePageReqVO pageReqVO) { + return otaFirmwareMapper.selectPage(pageReqVO); + } + + @Override + public IotOtaFirmwareDO validateFirmwareExists(Long id) { + IotOtaFirmwareDO firmware = otaFirmwareMapper.selectById(id); + if (firmware == null) { + throw exception(OTA_FIRMWARE_NOT_EXISTS); + } + return firmware; + } + + // TODO @li:注释有点冗余 + /** + * 验证产品和版本号是否重复 + *

+ * 该方法用于确保在系统中不存在具有相同产品ID和版本号的固件条目 + * 它通过调用otaFirmwareMapper的selectByProductIdAndVersion方法来查询数据库中是否存在匹配的产品ID和版本号的固件信息 + * 如果查询结果非空且不为null,则抛出异常,提示固件信息已存在,从而避免数据重复 + * + * @param productId 产品ID,用于数据库查询 + * @param version 版本号,用于数据库查询 + * @throws cn.iocoder.yudao.framework.common.exception.ServiceException,则抛出异常,提示固件信息已存在 + */ + private void validateProductAndVersionDuplicate(String productId, String version) { + // 查询数据库中是否存在具有相同产品ID和版本号的固件信息 + List list = otaFirmwareMapper.selectByProductIdAndVersion(productId, version); + // 如果查询结果非空且不为null,则抛出异常,提示固件信息已存在 + if (CollUtil.isNotEmpty(list)) { + throw exception(OTA_FIRMWARE_PRODUCT_VERSION_DUPLICATE); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeRecordService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeRecordService.java new file mode 100644 index 000000000..cbf900ac0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeRecordService.java @@ -0,0 +1,104 @@ +package cn.iocoder.yudao.module.iot.service.ota; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record.IotOtaUpgradeRecordPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeRecordDO; +import jakarta.validation.Valid; + +import java.util.List; +import java.util.Map; + +// TODO @li:注释写的有点冗余,可以看看别的模块哈。= = AI 生成的注释,有的时候太啰嗦了,需要处理下的哈 +/** + * IotOtaUpgradeRecordService 接口定义了与物联网设备OTA升级记录相关的操作。 + * 该接口提供了创建、更新、查询、统计和重试升级记录的功能。 + */ +public interface IotOtaUpgradeRecordService { + + /** + * 批量创建 OTA 升级记录 + * 该函数用于为指定的设备列表、固件ID和升级任务ID创建OTA升级记录。 + * + * @param deviceIds 设备ID列表,表示需要升级的设备集合。 + * @param firmwareId 固件ID,表示要升级到的固件版本。 + * @param upgradeTaskId 升级任务ID,表示此次升级任务的唯一标识。 + */ + void createOtaUpgradeRecordBatch(List deviceIds, Long firmwareId, Long upgradeTaskId); + + /** + * 获取 OTA 升级记录的数量统计 + * + * @return 返回一个 Map,其中键为状态码,值为对应状态的升级记录数量 + */ + Map getOtaUpgradeRecordCount(@Valid IotOtaUpgradeRecordPageReqVO pageReqVO); + + /** + * 获取 OTA 升级记录的统计信息。 + * + * @return 返回一个 Map,其中键为状态码,值为对应状态的升级记录统计信息 + */ + Map getOtaUpgradeRecordStatistics(Long firmwareId); + + /** + * 重试指定的 OTA 升级记录 + * + * @param id 需要重试的升级记录的ID。 + */ + void retryUpgradeRecord(Long id); + + /** + * 获取指定 ID 的 OTA 升级记录的详细信息。 + * + * @param id 需要查询的升级记录的ID。 + * @return 返回包含升级记录详细信息的响应对象。 + */ + IotOtaUpgradeRecordDO getUpgradeRecord(Long id); + + /** + * 分页查询 OTA 升级记录。 + * + * @param pageReqVO 包含分页查询条件的请求对象,必须经过验证。 + * @return 返回包含分页查询结果的响应对象。 + */ + PageResult getUpgradeRecordPage(@Valid IotOtaUpgradeRecordPageReqVO pageReqVO); + + /** + * 根据任务 ID 取消升级记录 + *

+ * 该函数用于根据给定的任务ID,取消与该任务相关的升级记录。通常用于在任务执行失败或用户手动取消时, + * 清理或标记相关的升级记录为取消状态。 + * + * @param taskId 要取消升级记录的任务ID。该ID唯一标识一个任务,通常由任务管理系统生成。 + */ + void cancelUpgradeRecordByTaskId(Long taskId); + + // TODO @li:不要的方法,可以删除下哈。 + /** + * 根据升级状态获取升级记录列表 + * + * @param state 升级状态,用于筛选符合条件的升级记录 + * @return 返回符合指定状态的升级记录列表,列表中的元素为 {@link IotOtaUpgradeRecordDO} 对象 + */ + List getUpgradeRecordListByState(Integer state); + + /** + * 更新升级记录的状态。 + *

+ * 该函数用于批量更新指定升级记录的状态。通过传入的ID列表和状态值,将对应的升级记录的状态更新为指定的值。 + * + * @param ids 需要更新状态的升级记录的ID列表。列表中的每个元素代表一个升级记录的ID。 + * @param status 要更新的状态值。该值应为有效的状态标识符,通常为整数类型。 + */ + void updateUpgradeRecordStatus(List ids, Integer status); + + /** + * 根据任务ID获取升级记录列表 + *

+ * 该函数通过给定的任务ID,查询并返回与该任务相关的所有升级记录。 + * + * @param taskId 任务ID,用于指定需要查询的任务 + * @return 返回一个包含升级记录的列表,列表中的每个元素为IotOtaUpgradeRecordDO对象 + */ + List getUpgradeRecordListByTaskId(Long taskId); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeRecordServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeRecordServiceImpl.java new file mode 100644 index 000000000..02ef39cdf --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeRecordServiceImpl.java @@ -0,0 +1,229 @@ +package cn.iocoder.yudao.module.iot.service.ota; + +import cn.hutool.core.convert.Convert; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record.IotOtaUpgradeRecordPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeRecordDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO; +import cn.iocoder.yudao.module.iot.dal.mysql.ota.IotOtaUpgradeRecordMapper; +import cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; + +// TODO @li:@Service、@Validated、@Slf4j,先用关键注解;2)类注释,简单写 +@Slf4j +@Service +@Validated +public class IotOtaUpgradeRecordServiceImpl implements IotOtaUpgradeRecordService { + + @Resource + private IotOtaUpgradeRecordMapper upgradeRecordMapper; + // TODO @li:1)@Resource 写在 @Lazy 之前,先用关键注解;2)有必要的情况下,在写 @Lazy 注解。 + @Lazy + @Resource + private IotDeviceService deviceService; + @Lazy + @Resource + private IotOtaFirmwareService firmwareService; + @Lazy + @Resource + private IotOtaUpgradeTaskService upgradeTaskService; + + @Override + public void createOtaUpgradeRecordBatch(List deviceIds, Long firmwareId, Long upgradeTaskId) { + // 1. 校验升级记录信息是否存在,并且已经取消的任务可以重新开始 + // TODO @li:批量查询。。 + deviceIds.forEach(deviceId -> validateUpgradeRecordDuplicate(firmwareId, upgradeTaskId, String.valueOf(deviceId))); + + // 2.初始化OTA升级记录列表信息 + IotOtaUpgradeTaskDO upgradeTask = upgradeTaskService.getUpgradeTask(upgradeTaskId); + IotOtaFirmwareDO firmware = firmwareService.getOtaFirmware(firmwareId); + List deviceList = deviceService.getDeviceListByIdList(deviceIds); + List upgradeRecordList = deviceList.stream().map(device -> { + IotOtaUpgradeRecordDO upgradeRecord = new IotOtaUpgradeRecordDO(); + upgradeRecord.setFirmwareId(firmware.getId()); + upgradeRecord.setTaskId(upgradeTask.getId()); + upgradeRecord.setProductKey(device.getProductKey()); + upgradeRecord.setDeviceName(device.getDeviceName()); + upgradeRecord.setDeviceId(Convert.toStr(device.getId())); + upgradeRecord.setFromFirmwareId(Convert.toLong(device.getFirmwareId())); + upgradeRecord.setStatus(IotOtaUpgradeRecordStatusEnum.PENDING.getStatus()); + upgradeRecord.setProgress(0); + return upgradeRecord; + }).toList(); + // 3.保存数据 + upgradeRecordMapper.insertBatch(upgradeRecordList); + // TODO @芋艿:在这里需要处理推送升级任务的逻辑 + + } + + // TODO @li:1)方法注释,简单写;2)父类写了注释,子类就不用写了。。。 + /** + * 获取OTA升级记录的数量统计。 + * 该方法根据传入的查询条件,统计不同状态的OTA升级记录数量,并返回一个包含各状态数量的映射。 + * + * @param pageReqVO 包含查询条件的请求对象,主要包括任务ID和设备名称等信息。 + * @return 返回一个Map,其中键为状态常量,值为对应状态的记录数量。 + */ + @Override + @Transactional + public Map getOtaUpgradeRecordCount(IotOtaUpgradeRecordPageReqVO pageReqVO) { + // 分别查询不同状态的OTA升级记录数量 + List> upgradeRecordCountList = upgradeRecordMapper.selectOtaUpgradeRecordCount( + pageReqVO.getTaskId(), pageReqVO.getDeviceName()); + Map upgradeRecordCountMap = ObjectUtils.defaultIfNull(upgradeRecordCountList.get(0)); + Objects.requireNonNull(upgradeRecordCountMap); + return upgradeRecordCountMap.entrySet().stream().collect(Collectors.toMap( + entry -> Convert.toInt(entry.getKey()), + entry -> Convert.toLong(entry.getValue()))); + } + + // TODO @li:1)方法注释,简单写;2)父类写了注释,子类就不用写了。。。 + /** + * 获取指定固件ID的OTA升级记录统计信息。 + * 该方法通过查询数据库,统计不同状态的OTA升级记录数量,并返回一个包含各状态数量的映射。 + * + * @param firmwareId 固件ID,用于指定需要统计的固件升级记录。 + * @return 返回一个Map,其中键为升级记录状态(如PENDING、PUSHED等),值为对应状态的记录数量。 + */ + @Override + @Transactional + public Map getOtaUpgradeRecordStatistics(Long firmwareId) { + // 查询并统计不同状态的OTA升级记录数量 + List> upgradeRecordStatisticsList = upgradeRecordMapper.selectOtaUpgradeRecordStatistics(firmwareId); + Map upgradeRecordStatisticsMap = ObjectUtils.defaultIfNull(upgradeRecordStatisticsList.get(0)); + Objects.requireNonNull(upgradeRecordStatisticsMap); + return upgradeRecordStatisticsMap.entrySet().stream().collect(Collectors.toMap( + entry -> Convert.toInt(entry.getKey()), + entry -> Convert.toLong(entry.getValue()))); + } + + @Override + public void retryUpgradeRecord(Long id) { + // 1.1.校验升级记录信息是否存在 + IotOtaUpgradeRecordDO upgradeRecord = validateUpgradeRecordExists(id); + // 1.2.校验升级记录是否可以重新升级 + validateUpgradeRecordCanRetry(upgradeRecord); + + // 2. 将一些数据重置,这样定时任务轮询就可以重启任务 + // TODO @li:更新的时候,wherestatus; + upgradeRecordMapper.updateById(new IotOtaUpgradeRecordDO() + .setId(upgradeRecord.getId()).setProgress(0) + .setStatus(IotOtaUpgradeRecordStatusEnum.PENDING.getStatus())); + } + + @Override + public IotOtaUpgradeRecordDO getUpgradeRecord(Long id) { + return upgradeRecordMapper.selectById(id); + } + + @Override + public PageResult getUpgradeRecordPage(IotOtaUpgradeRecordPageReqVO pageReqVO) { + return upgradeRecordMapper.selectUpgradeRecordPage(pageReqVO); + } + + @Override + public void cancelUpgradeRecordByTaskId(Long taskId) { + // 暂定只有待推送的升级记录可以取消 TODO @芋艿:可以看看阿里云,哪些可以取消 + upgradeRecordMapper.updateUpgradeRecordStatusByTaskIdAndStatus( + IotOtaUpgradeRecordStatusEnum.CANCELED.getStatus(), taskId, + IotOtaUpgradeRecordStatusEnum.PENDING.getStatus()); + } + + @Override + public List getUpgradeRecordListByState(Integer state) { + return upgradeRecordMapper.selectUpgradeRecordListByState(state); + } + + @Override + public void updateUpgradeRecordStatus(List ids, Integer status) { + upgradeRecordMapper.updateUpgradeRecordStatus(ids, status); + } + + @Override + public List getUpgradeRecordListByTaskId(Long taskId) { + return upgradeRecordMapper.selectUpgradeRecordListByTaskId(taskId); + } + + /** + * 验证指定的升级记录是否存在。 + *

+ * 该函数通过给定的ID查询升级记录,如果查询结果为空,则抛出异常,表示升级记录不存在。 + * + * @param id 升级记录的唯一标识符,类型为Long。 + * @throws cn.iocoder.yudao.framework.common.exception.ServiceException,则抛出异常,异常类型为OTA_UPGRADE_RECORD_NOT_EXISTS。 + */ + private IotOtaUpgradeRecordDO validateUpgradeRecordExists(Long id) { + // 根据ID查询升级记录 + IotOtaUpgradeRecordDO upgradeRecord = upgradeRecordMapper.selectById(id); + // 如果查询结果为空,抛出异常 + if (upgradeRecord == null) { + throw exception(OTA_UPGRADE_RECORD_NOT_EXISTS); + } + return upgradeRecord; + } + + // TODO @li:注释有点冗余 + /** + * 校验固件升级记录是否重复。 + *

+ * 该函数用于检查给定的固件ID、任务ID和设备ID是否已经存在未取消的升级记录。 + * 如果存在未取消的记录,则抛出异常,提示升级记录重复。 + * + * @param firmwareId 固件ID,用于标识特定的固件版本 + * @param taskId 任务ID,用于标识特定的升级任务 + * @param deviceId 设备ID,用于标识特定的设备 + */ + private void validateUpgradeRecordDuplicate(Long firmwareId, Long taskId, String deviceId) { + // 根据条件查询升级记录 + IotOtaUpgradeRecordDO upgradeRecord = upgradeRecordMapper.selectByConditions(firmwareId, taskId, deviceId); + // 如果查询到升级记录且状态不是已取消,则抛出异常 + // TODO @li:if return,减少括号层级; + // TODO @li:ObjUtil.notEquals,尽量不用 !取否逻辑; + if (upgradeRecord != null) { + if (!IotOtaUpgradeRecordStatusEnum.CANCELED.getStatus().equals(upgradeRecord.getStatus())) { + // TODO @li:提示的时候,需要把 deviceName 给提示出来,不然用户不知道哪个重复啦。 + throw exception(OTA_UPGRADE_RECORD_DUPLICATE); + } + } + } + + // TODO @li:注释有点冗余 + /** + * 验证升级记录是否可以重试。 + *

+ * 该方法用于检查给定的升级记录是否处于允许重试的状态。如果升级记录的状态为 + * PENDING、PUSHED 或 UPGRADING,则抛出异常,表示不允许重试。 + * + * @param upgradeRecord 需要验证的升级记录对象,类型为 IotOtaUpgradeRecordDO + * @throws cn.iocoder.yudao.framework.common.exception.ServiceException,则抛出 OTA_UPGRADE_RECORD_CANNOT_RETRY 异常 + */ + // TODO @li:这种一次性的方法(不复用的),其实一步一定要抽成小方法; + private void validateUpgradeRecordCanRetry(IotOtaUpgradeRecordDO upgradeRecord) { + // 检查升级记录的状态是否为 PENDING、PUSHED 或 UPGRADING + if (ObjectUtils.equalsAny(upgradeRecord.getStatus(), + IotOtaUpgradeRecordStatusEnum.PENDING.getStatus(), + IotOtaUpgradeRecordStatusEnum.PUSHED.getStatus(), + IotOtaUpgradeRecordStatusEnum.UPGRADING.getStatus())) { + // 如果升级记录处于上述状态之一,则抛出异常,表示不允许重试 + throw exception(OTA_UPGRADE_RECORD_CANNOT_RETRY); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeTaskService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeTaskService.java new file mode 100644 index 000000000..a2a810bf0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeTaskService.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.iot.service.ota; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * IoT OTA升级任务 Service 接口 + * + * @author Shelly Chan + */ +public interface IotOtaUpgradeTaskService { + + /** + * 创建OTA升级任务 + * + * @param createReqVO OTA升级任务的创建请求对象,包含创建任务所需的信息 + * @return 创建成功的OTA升级任务的ID + */ + Long createUpgradeTask(@Valid IotOtaUpgradeTaskSaveReqVO createReqVO); + + /** + * 取消OTA升级任务 + * + * @param id 要取消的OTA升级任务的ID + */ + void cancelUpgradeTask(Long id); + + /** + * 根据ID获取OTA升级任务的详细信息 + * + * @param id OTA升级任务的ID + * @return OTA升级任务的详细信息对象 + */ + IotOtaUpgradeTaskDO getUpgradeTask(Long id); + + /** + * 分页查询OTA升级任务 + * + * @param pageReqVO OTA升级任务的分页查询请求对象,包含查询条件和分页信息 + * @return 分页查询结果,包含OTA升级任务列表和总记录数 + */ + PageResult getUpgradeTaskPage(@Valid IotOtaUpgradeTaskPageReqVO pageReqVO); + + /** + * 根据任务状态获取升级任务列表 + * + * @param state 任务状态,用于筛选符合条件的升级任务 + * @return 返回符合指定状态的升级任务列表,列表中的元素为 IotOtaUpgradeTaskDO 对象 + */ + List getUpgradeTaskByState(Integer state); + + /** + * 更新升级任务的状态。 + *

+ * 该函数用于根据任务ID更新指定升级任务的状态。通常用于在任务执行过程中 + * 更新任务的状态,例如从“进行中”变为“已完成”或“失败”。 + * + * @param id 升级任务的唯一标识符,类型为Long。不能为null。 + * @param status 要更新的任务状态,类型为Integer。通常表示任务的状态码,如0表示未开始,1表示进行中,2表示已完成等。 + */ + void updateUpgradeTaskStatus(Long id, Integer status); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeTaskServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeTaskServiceImpl.java new file mode 100644 index 000000000..cee3ba516 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeTaskServiceImpl.java @@ -0,0 +1,207 @@ +package cn.iocoder.yudao.module.iot.service.ota; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +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.ota.vo.upgrade.task.IotOtaUpgradeTaskPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO; +import cn.iocoder.yudao.module.iot.dal.mysql.ota.IotOtaUpgradeTaskMapper; +import cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum; +import cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskStatusEnum; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; + +// TODO @li:完善注释、注解顺序 +@Slf4j +@Service +@Validated +public class IotOtaUpgradeTaskServiceImpl implements IotOtaUpgradeTaskService { + + @Resource + private IotOtaUpgradeTaskMapper upgradeTaskMapper; + + @Resource + @Lazy + private IotDeviceService deviceService; + @Resource + @Lazy + private IotOtaFirmwareService firmwareService; + @Resource + @Lazy + private IotOtaUpgradeRecordService upgradeRecordService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createUpgradeTask(IotOtaUpgradeTaskSaveReqVO createReqVO) { + // 1.1 校验同一固件的升级任务名称不重复 + validateFirmwareTaskDuplicate(createReqVO.getFirmwareId(), createReqVO.getName()); + // 1.2 校验固件信息是否存在 + IotOtaFirmwareDO firmware = firmwareService.validateFirmwareExists(createReqVO.getFirmwareId()); + // 1.3 补全设备范围信息,并且校验是否又设备可以升级,如果没有设备可以升级,则报错 + validateScopeAndDevice(createReqVO.getScope(), createReqVO.getDeviceIds(), firmware.getProductId()); + + // 2. 保存 OTA 升级任务信息到数据库 + IotOtaUpgradeTaskDO upgradeTask = initOtaUpgradeTask(createReqVO, firmware.getProductId()); + upgradeTaskMapper.insert(upgradeTask); + + // 3. 生成设备升级记录信息并存储,等待定时任务轮询 + upgradeRecordService.createOtaUpgradeRecordBatch(upgradeTask.getDeviceIds(), firmware.getId(), upgradeTask.getId()); + return upgradeTask.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void cancelUpgradeTask(Long id) { + // 1.1 校验升级任务是否存在 + IotOtaUpgradeTaskDO upgradeTask = validateUpgradeTaskExists(id); + // 1.2 校验升级任务是否可以取消 + // TODO @li:ObjUtil notequals + if (!Objects.equals(upgradeTask.getStatus(), IotOtaUpgradeTaskStatusEnum.IN_PROGRESS.getStatus())) { + throw exception(OTA_UPGRADE_TASK_CANNOT_CANCEL); + } + + // 2. 更新 OTA 升级任务状态为已取消 + upgradeTaskMapper.updateById(IotOtaUpgradeTaskDO.builder() + .id(id).status(IotOtaUpgradeTaskStatusEnum.CANCELED.getStatus()) + .build()); + + // 3. 更新 OTA 升级记录状态为已取消 + upgradeRecordService.cancelUpgradeRecordByTaskId(id); + } + + @Override + public IotOtaUpgradeTaskDO getUpgradeTask(Long id) { + return upgradeTaskMapper.selectById(id); + } + + @Override + public PageResult getUpgradeTaskPage(IotOtaUpgradeTaskPageReqVO pageReqVO) { + return upgradeTaskMapper.selectUpgradeTaskPage(pageReqVO); + } + + @Override + public List getUpgradeTaskByState(Integer state) { + return upgradeTaskMapper.selectUpgradeTaskByState(state); + } + + @Override + public void updateUpgradeTaskStatus(Long id, Integer status) { + upgradeTaskMapper.updateById(IotOtaUpgradeTaskDO.builder().id(id).status(status).build()); + } + + // TODO @li:注释有点冗余 + /** + * 校验固件升级任务是否重复 + *

+ * 该方法用于检查给定固件ID和任务名称组合是否已存在于数据库中,如果存在则抛出异常, + * 表示任务名称对于该固件而言是重复的此检查确保用户不能创建具有相同名称的任务, + * 从而避免数据重复和混淆 + * + * @param firmwareId 固件的唯一标识符,用于区分不同的固件 + * @param taskName 升级任务的名称,用于与固件ID一起检查重复性 + * @throws cn.iocoder.yudao.framework.common.exception.ServerException 则抛出预定义的异常 + */ + private void validateFirmwareTaskDuplicate(Long firmwareId, String taskName) { + // 查询数据库中是否有相同固件ID和任务名称的升级任务存在 + List upgradeTaskList = upgradeTaskMapper.selectByFirmwareIdAndName(firmwareId, taskName); + // 如果查询结果不为空,说明存在重复的任务名称,抛出异常 + if (CollUtil.isNotEmpty(upgradeTaskList)) { + throw exception(OTA_UPGRADE_TASK_NAME_DUPLICATE); + } + } + + // TODO @li:注释有点冗余 + /** + * 验证升级任务的范围和设备列表的有效性。 + *

+ * 根据升级任务的范围(scope),验证设备列表(deviceIds)或产品ID(productId)是否有效。 + * 如果范围是“选择设备”(SELECT),则必须提供设备列表;如果范围是“所有设备”(ALL),则必须根据产品ID获取设备列表,并确保列表不为空。 + * + * @param scope 升级任务的范围,参考 IotOtaUpgradeTaskScopeEnum 枚举值 + * @param deviceIds 设备ID列表,当范围为“选择设备”时,该列表不能为空 + * @param productId 产品ID,当范围为“所有设备”时,用于获取设备列表 + * @throws cn.iocoder.yudao.framework.common.exception.ServiceException,抛出相应的异常 + */ + private void validateScopeAndDevice(Integer scope, List deviceIds, String productId) { + // TODO @li:if return + // 验证范围为“选择设备”时,设备列表不能为空 + if (Objects.equals(scope, IotOtaUpgradeTaskScopeEnum.SELECT.getScope())) { + if (CollUtil.isEmpty(deviceIds)) { + throw exception(OTA_UPGRADE_TASK_DEVICE_IDS_EMPTY); + } + } else if (Objects.equals(scope, IotOtaUpgradeTaskScopeEnum.ALL.getScope())) { + // 验证范围为“所有设备”时,根据产品ID获取的设备列表不能为空 + List deviceList = deviceService.getDeviceListByProductId(Convert.toLong(productId)); + if (CollUtil.isEmpty(deviceList)) { + throw exception(OTA_UPGRADE_TASK_DEVICE_LIST_EMPTY); + } + } + } + + // TODO @li:注释有点冗余 + /** + * 验证升级任务是否存在 + *

+ * 通过查询数据库来验证给定ID的升级任务是否存在此方法主要用于确保后续操作所针对的升级任务是有效的 + * + * @param id 升级任务的唯一标识符如果为null或数据库中不存在对应的记录,则认为任务不存在 + * @throws cn.iocoder.yudao.framework.common.exception.ServiceException 如果升级任务不存在,则抛出异常提示任务不存在 + */ + private IotOtaUpgradeTaskDO validateUpgradeTaskExists(Long id) { + // 查询数据库中是否有相同固件ID和任务名称的升级任务存在 + IotOtaUpgradeTaskDO upgradeTask = upgradeTaskMapper.selectById(id); + // 如果查询结果不为空,说明存在重复的任务名称,抛出异常 + if (Objects.isNull(upgradeTask)) { + throw exception(OTA_UPGRADE_TASK_NOT_EXISTS); + } + return upgradeTask; + } + + // TODO @li:注释有点冗余 + /** + * 初始化升级任务 + *

+ * 根据请求参数创建升级任务对象,并根据选择的范围初始化设备数量 + * 如果选择特定设备进行升级,则设备数量为所选设备的总数 + * 如果选择全部设备进行升级,则设备数量为该固件对应产品下的所有设备总数 + * + * @param createReqVO 升级任务保存请求对象,包含创建升级任务所需的信息 + * @return 返回初始化后的升级任务对象 + */ + // TODO @li:一次性的方法,不用特别抽小方法 + private IotOtaUpgradeTaskDO initOtaUpgradeTask(IotOtaUpgradeTaskSaveReqVO createReqVO, String productId) { + // 将请求参数转换为升级任务对象 + IotOtaUpgradeTaskDO upgradeTask = BeanUtils.toBean(createReqVO, IotOtaUpgradeTaskDO.class); + // 初始化的时候,设置设备数量和状态 + upgradeTask.setDeviceCount(Convert.toLong(CollUtil.size(createReqVO.getDeviceIds()))) + .setStatus(IotOtaUpgradeTaskStatusEnum.IN_PROGRESS.getStatus()); + // 如果选择全选,则需要查询设备数量 + if (Objects.equals(createReqVO.getScope(), IotOtaUpgradeTaskScopeEnum.ALL.getScope())) { + // 根据产品ID查询设备数量 + List deviceList = deviceService.getDeviceListByProductId(Convert.toLong(productId)); + // 设置升级任务的设备数量 + upgradeTask.setDeviceCount((long) deviceList.size()); + upgradeTask.setDeviceIds( + deviceList.stream().map(IotDeviceDO::getId).collect(Collectors.toList())); + } + // 返回初始化后的升级任务对象 + return upgradeTask; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginConfigService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginConfigService.java new file mode 100644 index 000000000..8b6610f15 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginConfigService.java @@ -0,0 +1,100 @@ +package cn.iocoder.yudao.module.iot.service.plugin; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO; +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginDeployTypeEnum; +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * IoT 插件配置 Service 接口 + * + * @author haohao + */ +public interface IotPluginConfigService { + + /** + * 创建插件配置 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createPluginConfig(@Valid PluginConfigSaveReqVO createReqVO); + + /** + * 更新插件配置 + * + * @param updateReqVO 更新信息 + */ + void updatePluginConfig(@Valid PluginConfigSaveReqVO updateReqVO); + + /** + * 删除插件配置 + * + * @param id 编号 + */ + void deletePluginConfig(Long id); + + /** + * 获得插件配置 + * + * @param id 编号 + * @return 插件配置 + */ + IotPluginConfigDO getPluginConfig(Long id); + + /** + * 获得插件配置分页 + * + * @param pageReqVO 分页查询 + * @return 插件配置分页 + */ + PageResult getPluginConfigPage(PluginConfigPageReqVO pageReqVO); + + /** + * 上传插件的 JAR 包 + * + * @param id 插件id + * @param file 文件 + */ + void uploadFile(Long id, MultipartFile file); + + /** + * 更新插件的状态 + * + * @param id 插件id + * @param status 状态 {@link IotPluginStatusEnum} + */ + void updatePluginStatus(Long id, Integer status); + + /** + * 获得插件配置列表 + * + * @return 插件配置列表 + */ + List getPluginConfigList(); + + /** + * 根据状态和部署类型获得插件配置列表 + * + * @param status 状态 {@link IotPluginStatusEnum} + * @param deployType 部署类型 {@link IotPluginDeployTypeEnum} + * @return 插件配置列表 + */ + List getPluginConfigListByStatusAndDeployType(Integer status, Integer deployType); + + /** + * 根据插件包标识符获取插件配置 + * + * @param pluginKey 插件包标识符 + * @return 插件配置 + */ + IotPluginConfigDO getPluginConfigByPluginKey(@NotEmpty(message = "插件包标识符不能为空") String pluginKey); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginConfigServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginConfigServiceImpl.java new file mode 100644 index 000000000..18376bc57 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginConfigServiceImpl.java @@ -0,0 +1,188 @@ +package cn.iocoder.yudao.module.iot.service.plugin; + +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.plugin.vo.config.PluginConfigPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO; +import cn.iocoder.yudao.module.iot.dal.mysql.plugin.IotPluginConfigMapper; +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.pf4j.PluginWrapper; +import org.pf4j.spring.SpringPluginManager; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; +/** + * IoT 插件配置 Service 实现类 + * + * @author haohao + */ +@Service +@Validated +@Slf4j +public class IotPluginConfigServiceImpl implements IotPluginConfigService { + + @Resource + private IotPluginConfigMapper pluginConfigMapper; + + @Resource + private IotPluginInstanceService pluginInstanceService; + + @Resource + private SpringPluginManager springPluginManager; + + @Override + public Long createPluginConfig(PluginConfigSaveReqVO createReqVO) { + // 1. 校验插件标识唯一性:确保没有其他配置使用相同的 pluginKey(新建时 id 为 null) + validatePluginKeyUnique(null, createReqVO.getPluginKey()); + IotPluginConfigDO pluginConfig = BeanUtils.toBean(createReqVO, IotPluginConfigDO.class); + // 2. 插入插件配置到数据库 + pluginConfigMapper.insert(pluginConfig); + return pluginConfig.getId(); + } + + @Override + public void updatePluginConfig(PluginConfigSaveReqVO updateReqVO) { + // 1. 校验插件配置是否存在:根据传入 ID 判断记录是否存在 + validatePluginConfigExists(updateReqVO.getId()); + // 2. 校验插件标识唯一性:确保更新后的 pluginKey 没有被其他记录占用 + validatePluginKeyUnique(updateReqVO.getId(), updateReqVO.getPluginKey()); + // 3. 将更新请求对象转换为插件配置数据对象 + IotPluginConfigDO updateObj = BeanUtils.toBean(updateReqVO, IotPluginConfigDO.class); + pluginConfigMapper.updateById(updateObj); + } + + /** + * 校验插件标识唯一性 + * + * @param id 当前插件配置的 ID(如果为 null 则说明为新建操作) + * @param pluginKey 待校验的插件标识 + */ + private void validatePluginKeyUnique(Long id, String pluginKey) { + // 1. 根据 pluginKey 从数据库中查询已有的插件配置 + IotPluginConfigDO pluginConfig = pluginConfigMapper.selectByPluginKey(pluginKey); + // 2. 如果查询到记录且记录的 ID 与当前 ID 不相同,则认为存在重复,抛出异常 + if (pluginConfig != null && !pluginConfig.getId().equals(id)) { + throw exception(PLUGIN_CONFIG_KEY_DUPLICATE); + } + } + + @Override + public void deletePluginConfig(Long id) { + // 1. 校验存在 + IotPluginConfigDO pluginConfigDO = validatePluginConfigExists(id); + // 2. 未开启状态,才允许删除 + if (IotPluginStatusEnum.RUNNING.getStatus().equals(pluginConfigDO.getStatus())) { + throw exception(PLUGIN_CONFIG_DELETE_FAILED_RUNNING); + } + + // 3. 卸载插件 + pluginInstanceService.stopAndUnloadPlugin(pluginConfigDO.getPluginKey()); + // 4. 删除插件文件 + pluginInstanceService.deletePluginFile(pluginConfigDO); + + // 5. 删除插件配置 + pluginConfigMapper.deleteById(id); + } + + /** + * 校验插件配置是否存在 + * + * @param id 插件配置编号 + * @return 插件配置 + */ + private IotPluginConfigDO validatePluginConfigExists(Long id) { + IotPluginConfigDO pluginConfig = pluginConfigMapper.selectById(id); + if (pluginConfig == null) { + throw exception(PLUGIN_CONFIG_NOT_EXISTS); + } + return pluginConfig; + } + + @Override + public IotPluginConfigDO getPluginConfig(Long id) { + return pluginConfigMapper.selectById(id); + } + + @Override + public PageResult getPluginConfigPage(PluginConfigPageReqVO pageReqVO) { + return pluginConfigMapper.selectPage(pageReqVO); + } + + @Override + public void uploadFile(Long id, MultipartFile file) { + // 1. 校验插件配置是否存在 + IotPluginConfigDO pluginConfigDO = validatePluginConfigExists(id); + + // 2.1 停止并卸载旧的插件 + pluginInstanceService.stopAndUnloadPlugin(pluginConfigDO.getPluginKey()); + // 2.2 上传新的插件文件,更新插件启用状态文件 + String pluginKeyNew = pluginInstanceService.uploadAndLoadNewPlugin(file); + + // 3. 校验 file 相关参数,是否完整 + validatePluginConfigFile(pluginKeyNew); + + // 4. 更新插件配置 + IotPluginConfigDO updatedPluginConfig = new IotPluginConfigDO() + .setId(pluginConfigDO.getId()) + .setPluginKey(pluginKeyNew) + .setStatus(IotPluginStatusEnum.STOPPED.getStatus()) // TODO @haohao:这个状态,是不是非 stop 哈? + .setFileName(file.getOriginalFilename()) + .setScript("") // TODO @haohao:这个设置为 "" 会不会覆盖数据里的哈?应该从插件里读取?未来? + .setConfigSchema(springPluginManager.getPlugin(pluginKeyNew).getDescriptor().getPluginDescription()) + .setVersion(springPluginManager.getPlugin(pluginKeyNew).getDescriptor().getVersion()) + .setDescription(springPluginManager.getPlugin(pluginKeyNew).getDescriptor().getPluginDescription()); + pluginConfigMapper.updateById(updatedPluginConfig); + } + + /** + * 校验 file 相关参数 + * + * @param pluginKeyNew 插件标识符 + */ + private void validatePluginConfigFile(String pluginKeyNew) { + // TODO @haohao:校验 file 相关参数,是否完整,类似:version 之类是不是可以解析到 + PluginWrapper plugin = springPluginManager.getPlugin(pluginKeyNew); + if (plugin == null) { + throw exception(PLUGIN_INSTALL_FAILED); + } + if (plugin.getDescriptor().getVersion() == null) { + throw exception(PLUGIN_INSTALL_FAILED); + } + } + + @Override + public void updatePluginStatus(Long id, Integer status) { + // 1. 校验插件配置是否存在 + IotPluginConfigDO pluginConfigDo = validatePluginConfigExists(id); + + // 2. 更新插件状态 + pluginInstanceService.updatePluginStatus(pluginConfigDo, status); + + // 3. 更新数据库中的插件状态 + pluginConfigMapper.updateById(new IotPluginConfigDO().setId(id).setStatus(status)); + } + + @Override + public List getPluginConfigList() { + return pluginConfigMapper.selectList(); + } + + @Override + public List getPluginConfigListByStatusAndDeployType(Integer status, Integer deployType) { + return pluginConfigMapper.selectListByStatusAndDeployType(status, deployType); + } + + @Override + public IotPluginConfigDO getPluginConfigByPluginKey(String pluginKey) { + return pluginConfigMapper.selectByPluginKey(pluginKey); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceService.java new file mode 100644 index 000000000..56e1bf0f0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceService.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.module.iot.service.plugin; + +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDateTime; + +/** + * IoT 插件实例 Service 接口 + * + * @author 芋道源码 + */ +public interface IotPluginInstanceService { + + /** + * 心跳插件实例 + * + * @param heartbeatReqDTO 心跳插件实例 DTO + */ + void heartbeatPluginInstance(IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO); + + /** + * 离线超时插件实例 + * + * @param maxHeartbeatTime 最大心跳时间 + */ + int offlineTimeoutPluginInstance(LocalDateTime maxHeartbeatTime); + + /** + * 停止并卸载插件 + * + * @param pluginKey 插件标识符 + */ + void stopAndUnloadPlugin(String pluginKey); + + /** + * 删除插件文件 + * + * @param pluginConfigDO 插件配置 + */ + void deletePluginFile(IotPluginConfigDO pluginConfigDO); + + /** + * 上传并加载新的插件文件 + * + * @param file 插件文件 + * @return 插件标识符 + */ + String uploadAndLoadNewPlugin(MultipartFile file); + + /** + * 更新插件状态 + * + * @param pluginConfigDO 插件配置 + * @param status 新状态 + */ + void updatePluginStatus(IotPluginConfigDO pluginConfigDO, Integer status); + + // ========== 设备与插件的映射操作 ========== + + /** + * 更新设备对应的插件实例的进程编号 + * + * @param deviceKey 设备 Key + * @param processId 进程编号 + */ + void updateDevicePluginInstanceProcessIdAsync(String deviceKey, String processId); + + /** + * 获得设备对应的插件实例 + * + * @param deviceKey 设备 Key + * @return 插件实例 + */ + IotPluginInstanceDO getPluginInstanceByDeviceKey(String deviceKey); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceServiceImpl.java new file mode 100644 index 000000000..3c15ff774 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceServiceImpl.java @@ -0,0 +1,231 @@ +package cn.iocoder.yudao.module.iot.service.plugin; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO; +import cn.iocoder.yudao.module.iot.dal.mysql.plugin.IotPluginInstanceMapper; +import cn.iocoder.yudao.module.iot.dal.redis.plugin.DevicePluginProcessIdRedisDAO; +import cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants; +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.pf4j.PluginState; +import org.pf4j.PluginWrapper; +import org.pf4j.spring.SpringPluginManager; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.time.LocalDateTime; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; + +/** + * IoT 插件实例 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class IotPluginInstanceServiceImpl implements IotPluginInstanceService { + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private IotPluginConfigService pluginConfigService; + + @Resource + private IotPluginInstanceMapper pluginInstanceMapper; + + @Resource + private DevicePluginProcessIdRedisDAO devicePluginProcessIdRedisDAO; + + @Resource + private SpringPluginManager pluginManager; + + @Value("${pf4j.pluginsDir}") + private String pluginsDir; + + @Override + public void heartbeatPluginInstance(IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO) { + // 情况一:已存在,则进行更新 + IotPluginInstanceDO instance = TenantUtils.executeIgnore( + () -> pluginInstanceMapper.selectByProcessId(heartbeatReqDTO.getProcessId())); + if (instance != null) { + IotPluginInstanceDO.IotPluginInstanceDOBuilder updateObj = IotPluginInstanceDO.builder().id(instance.getId()) + .hostIp(heartbeatReqDTO.getHostIp()).downstreamPort(heartbeatReqDTO.getDownstreamPort()) + .online(heartbeatReqDTO.getOnline()).heartbeatTime(LocalDateTime.now()); + if (Boolean.TRUE.equals(heartbeatReqDTO.getOnline())) { + if (Boolean.FALSE.equals(instance.getOnline())) { // 当前处于离线时,才需要更新上线时间 + updateObj.onlineTime(LocalDateTime.now()); + } + } else { + updateObj.offlineTime(LocalDateTime.now()); + } + TenantUtils.execute(instance.getTenantId(), + () -> pluginInstanceMapper.updateById(updateObj.build())); + return; + } + + // 情况二:不存在,则创建 + IotPluginConfigDO info = TenantUtils.executeIgnore( + () -> pluginConfigService.getPluginConfigByPluginKey(heartbeatReqDTO.getPluginKey())); + if (info == null) { + log.error("[heartbeatPluginInstance][心跳({}) 对应的插件不存在]", heartbeatReqDTO); + return; + } + IotPluginInstanceDO.IotPluginInstanceDOBuilder insertObj = IotPluginInstanceDO.builder() + .pluginId(info.getId()).processId(heartbeatReqDTO.getProcessId()) + .hostIp(heartbeatReqDTO.getHostIp()).downstreamPort(heartbeatReqDTO.getDownstreamPort()) + .online(heartbeatReqDTO.getOnline()).heartbeatTime(LocalDateTime.now()); + if (Boolean.TRUE.equals(heartbeatReqDTO.getOnline())) { + insertObj.onlineTime(LocalDateTime.now()); + } else { + insertObj.offlineTime(LocalDateTime.now()); + } + TenantUtils.execute(info.getTenantId(), + () -> pluginInstanceMapper.insert(insertObj.build())); + } + + @Override + public int offlineTimeoutPluginInstance(LocalDateTime maxHeartbeatTime) { + List list = pluginInstanceMapper.selectListByHeartbeatTimeLt(maxHeartbeatTime); + if (CollUtil.isEmpty(list)) { + return 0; + } + + // 更新插件实例为离线 + int count = 0; + for (IotPluginInstanceDO instance : list) { + pluginInstanceMapper.updateById(IotPluginInstanceDO.builder().id(instance.getId()) + .online(false).offlineTime(LocalDateTime.now()).build()); + count++; + } + return count; + } + + @Override + public void stopAndUnloadPlugin(String pluginKey) { + PluginWrapper plugin = pluginManager.getPlugin(pluginKey); + if (plugin == null) { + log.warn("插件不存在或已卸载: {}", pluginKey); + return; + } + if (plugin.getPluginState().equals(PluginState.STARTED)) { + pluginManager.stopPlugin(pluginKey); // 停止插件 + log.info("已停止插件: {}", pluginKey); + } + pluginManager.unloadPlugin(pluginKey); // 卸载插件 + log.info("已卸载插件: {}", pluginKey); + } + + @Override + public void deletePluginFile(IotPluginConfigDO pluginConfigDO) { + File file = new File(pluginsDir, pluginConfigDO.getFileName()); + if (!file.exists()) { + return; + } + try { + TimeUnit.SECONDS.sleep(1); // 等待 1 秒,避免插件未卸载完毕 + if (!file.delete()) { + log.error("[deletePluginFile][删除插件文件({}) 失败]", pluginConfigDO.getFileName()); + } + } catch (InterruptedException e) { + log.error("[deletePluginFile][删除插件文件({}) 失败]", pluginConfigDO.getFileName(), e); + } + } + + @Override + public String uploadAndLoadNewPlugin(MultipartFile file) { + String pluginKeyNew; + // TODO @haohao:多节点,是不是要上传 s3 之类的存储器;然后定时去加载 + Path pluginsPath = Paths.get(pluginsDir); + try { + FileUtil.mkdir(pluginsPath.toFile()); // 创建插件目录 + String filename = file.getOriginalFilename(); + if (filename != null) { + Path jarPath = pluginsPath.resolve(filename); + Files.copy(file.getInputStream(), jarPath, StandardCopyOption.REPLACE_EXISTING); // 保存上传的 JAR 文件 + pluginKeyNew = pluginManager.loadPlugin(jarPath.toAbsolutePath()); // 加载插件 + log.info("已加载插件: {}", pluginKeyNew); + } else { + throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED); + } + } catch (IOException e) { + log.error("[uploadAndLoadNewPlugin][上传插件文件失败]", e); + throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED, e); + } catch (Exception e) { + log.error("[uploadAndLoadNewPlugin][加载插件失败]", e); + throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED, e); + } + return pluginKeyNew; + } + + @Override + public void updatePluginStatus(IotPluginConfigDO pluginConfigDO, Integer status) { + String pluginKey = pluginConfigDO.getPluginKey(); + PluginWrapper plugin = pluginManager.getPlugin(pluginKey); + + if (plugin == null) { + // 插件不存在且状态为停止,抛出异常 + if (IotPluginStatusEnum.STOPPED.getStatus().equals(pluginConfigDO.getStatus())) { + throw exception(ErrorCodeConstants.PLUGIN_STATUS_INVALID); + } + return; + } + + // 启动插件 + if (status.equals(IotPluginStatusEnum.RUNNING.getStatus()) + && plugin.getPluginState() != PluginState.STARTED) { + try { + pluginManager.startPlugin(pluginKey); + } catch (Exception e) { + log.error("[updatePluginStatus][启动插件({}) 失败]", pluginKey, e); + throw exception(ErrorCodeConstants.PLUGIN_START_FAILED, e); + } + log.info("已启动插件: {}", pluginKey); + } + // 停止插件 + else if (status.equals(IotPluginStatusEnum.STOPPED.getStatus()) + && plugin.getPluginState() == PluginState.STARTED) { + try { + pluginManager.stopPlugin(pluginKey); + } catch (Exception e) { + log.error("[updatePluginStatus][停止插件({}) 失败]", pluginKey, e); + throw exception(ErrorCodeConstants.PLUGIN_STOP_FAILED, e); + } + log.info("已停止插件: {}", pluginKey); + } + } + + // ========== 设备与插件的映射操作 ========== + + @Override + public void updateDevicePluginInstanceProcessIdAsync(String deviceKey, String processId) { + devicePluginProcessIdRedisDAO.put(deviceKey, processId); + } + + @Override + public IotPluginInstanceDO getPluginInstanceByDeviceKey(String deviceKey) { + String processId = devicePluginProcessIdRedisDAO.get(deviceKey); + if (StrUtil.isEmpty(processId)) { + return null; + } + return pluginInstanceMapper.selectByProcessId(processId); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryService.java new file mode 100644 index 000000000..8cc640556 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryService.java @@ -0,0 +1,103 @@ +package cn.iocoder.yudao.module.iot.service.product; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.category.IotProductCategoryPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.category.IotProductCategorySaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductCategoryDO; +import jakarta.validation.Valid; + +import javax.annotation.Nullable; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * IoT 产品分类 Service 接口 + * + * @author 芋道源码 + */ +public interface IotProductCategoryService { + + /** + * 创建产品分类 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createProductCategory(@Valid IotProductCategorySaveReqVO createReqVO); + + /** + * 更新产品分类 + * + * @param updateReqVO 更新信息 + */ + void updateProductCategory(@Valid IotProductCategorySaveReqVO updateReqVO); + + /** + * 删除产品分类 + * + * @param id 编号 + */ + void deleteProductCategory(Long id); + + /** + * 获得产品分类 + * + * @param id 编号 + * @return 产品分类 + */ + IotProductCategoryDO getProductCategory(Long id); + + /** + * 获得产品分类列表 + * + * @param ids 编号 + * @return 产品分类列表 + */ + List getProductCategoryList(Collection ids); + + /** + * 获得产品分类 Map + * + * @param ids 编号 + * @return 产品分类 Map + */ + default Map getProductCategoryMap(Collection ids) { + return convertMap(getProductCategoryList(ids), IotProductCategoryDO::getId); + } + + /** + * 获得产品分类分页 + * + * @param pageReqVO 分页查询 + * @return 产品分类分页 + */ + PageResult getProductCategoryPage(IotProductCategoryPageReqVO pageReqVO); + + /** + * 获得产品分类列表,根据状态 + * + * @param status 状态 + * @return 产品分类列表 + */ + List getProductCategoryListByStatus(Integer status); + + /** + * 获得产品分类数量 + * + * @param createTime 创建时间,如果为空,则统计所有分类数量 + * @return 产品分类数量 + */ + Long getProductCategoryCount(@Nullable LocalDateTime createTime); + + /** + * 获得各个品类下设备数量统计,其中 key 是产品分类名 + * + * @return 品类设备统计列表 + */ + Map getProductCategoryDeviceCountMap(); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryServiceImpl.java new file mode 100644 index 000000000..13a8d488e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryServiceImpl.java @@ -0,0 +1,123 @@ +package cn.iocoder.yudao.module.iot.service.product; + +import cn.hutool.core.collection.CollUtil; +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.product.vo.category.IotProductCategoryPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.category.IotProductCategorySaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductCategoryDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.dal.mysql.product.IotProductCategoryMapper; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDateTime; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.PRODUCT_CATEGORY_NOT_EXISTS; + +/** + * IoT 产品分类 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class IotProductCategoryServiceImpl implements IotProductCategoryService { + + @Resource + private IotProductCategoryMapper iotProductCategoryMapper; + + @Resource + private IotProductService productService; + + @Resource + private IotDeviceService deviceService; + + public Long createProductCategory(IotProductCategorySaveReqVO createReqVO) { + // 插入 + IotProductCategoryDO productCategory = BeanUtils.toBean(createReqVO, IotProductCategoryDO.class); + iotProductCategoryMapper.insert(productCategory); + // 返回 + return productCategory.getId(); + } + + @Override + public void updateProductCategory(IotProductCategorySaveReqVO updateReqVO) { + // 校验存在 + validateProductCategoryExists(updateReqVO.getId()); + // 更新 + IotProductCategoryDO updateObj = BeanUtils.toBean(updateReqVO, IotProductCategoryDO.class); + iotProductCategoryMapper.updateById(updateObj); + } + + @Override + public void deleteProductCategory(Long id) { + // 校验存在 + validateProductCategoryExists(id); + // 删除 + iotProductCategoryMapper.deleteById(id); + } + + private void validateProductCategoryExists(Long id) { + if (iotProductCategoryMapper.selectById(id) == null) { + throw exception(PRODUCT_CATEGORY_NOT_EXISTS); + } + } + + @Override + public IotProductCategoryDO getProductCategory(Long id) { + return iotProductCategoryMapper.selectById(id); + } + + @Override + public List getProductCategoryList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return CollUtil.newArrayList(); + } + return iotProductCategoryMapper.selectBatchIds(ids); + } + + @Override + public PageResult getProductCategoryPage(IotProductCategoryPageReqVO pageReqVO) { + return iotProductCategoryMapper.selectPage(pageReqVO); + } + + @Override + public List getProductCategoryListByStatus(Integer status) { + return iotProductCategoryMapper.selectListByStatus(status); + } + + @Override + public Long getProductCategoryCount(LocalDateTime createTime) { + return iotProductCategoryMapper.selectCountByCreateTime(createTime); + } + + @Override + public Map getProductCategoryDeviceCountMap() { + // 1. 获取所有数据 + List categoryList = iotProductCategoryMapper.selectList(); + List productList = productService.getProductList(); + // TODO @super:不要 list 查询,返回内存,而是查询一个 Map + Map deviceCountMapByProductId = deviceService.getDeviceCountMapByProductId(); + + // 2. 统计每个分类下的设备数量 + Map categoryDeviceCountMap = new HashMap<>(); + for (IotProductCategoryDO category : categoryList) { + categoryDeviceCountMap.put(category.getName(), 0); + // TODO @super:CollectionUtils.getSumValue(),看看能不能简化下 + // 2.2 找到该分类下的所有产品,累加设备数量 + for (IotProductDO product : productList) { + if (Objects.equals(product.getCategoryId(), category.getId())) { + Integer deviceCount = deviceCountMapByProductId.getOrDefault(product.getId(), 0); + categoryDeviceCountMap.merge(category.getName(), deviceCount, Integer::sum); + } + } + } + return categoryDeviceCountMap; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java index 3fff94fd9..8497d73aa 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java @@ -1,11 +1,13 @@ package cn.iocoder.yudao.module.iot.service.product; import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.iot.controller.admin.product.vo.IotProductPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.product.vo.IotProductSaveReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductSaveReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; import jakarta.validation.Valid; +import javax.annotation.Nullable; +import java.time.LocalDateTime; import java.util.List; /** @@ -45,6 +47,30 @@ public interface IotProductService { */ IotProductDO getProduct(Long id); + /** + * 根据产品 key 获得产品 + * + * @param productKey 产品 key + * @return 产品 + */ + IotProductDO getProductByProductKey(String productKey); + + /** + * 校验产品存在 + * + * @param id 编号 + * @return 产品 + */ + IotProductDO validateProductExists(Long id); + + /** + * 校验产品存在 + * + * @param productKey 产品 key + * @return 产品 + */ + IotProductDO validateProductExists(String productKey); + /** * 获得产品分页 * @@ -68,4 +94,13 @@ public interface IotProductService { */ List getProductList(); + /** + * 获得产品数量 + * + * @param createTime 创建时间,如果为空,则统计所有产品数量 + * @return 产品数量 + */ + Long getProductCount(@Nullable LocalDateTime createTime); + + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java index 15391f70b..4a7263c27 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java @@ -1,20 +1,23 @@ package cn.iocoder.yudao.module.iot.service.product; -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.product.vo.IotProductPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.product.vo.IotProductSaveReqVO; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductSaveReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; import cn.iocoder.yudao.module.iot.dal.mysql.product.IotProductMapper; import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum; +import cn.iocoder.yudao.module.iot.service.device.data.IotDevicePropertyService; +import com.baomidou.dynamic.datasource.annotation.DSTransactional; import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; +import java.time.LocalDateTime; import java.util.List; import java.util.Objects; -import java.util.UUID; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; @@ -31,34 +34,27 @@ public class IotProductServiceImpl implements IotProductService { @Resource private IotProductMapper productMapper; + @Resource + @Lazy // 延迟加载,解决循环依赖 + private IotDevicePropertyService devicePropertyDataService; + @Override public Long createProduct(IotProductSaveReqVO createReqVO) { - // 1. 生成 ProductKey - createProductKey(createReqVO); + // 1. 校验 ProductKey + TenantUtils.executeIgnore(() -> { + // 为什么忽略租户?避免多个租户之间,productKey 重复,导致 TDengine 设备属性表重复 + if (productMapper.selectByProductKey(createReqVO.getProductKey()) != null) { + throw exception(PRODUCT_KEY_EXISTS); + } + }); + // 2. 插入 - IotProductDO product = BeanUtils.toBean(createReqVO, IotProductDO.class); + IotProductDO product = BeanUtils.toBean(createReqVO, IotProductDO.class) + .setStatus(IotProductStatusEnum.UNPUBLISHED.getStatus()); productMapper.insert(product); return product.getId(); } - /** - * 创建 ProductKey - * - * @param createReqVO 创建信息 - */ - private void createProductKey(IotProductSaveReqVO createReqVO) { - String productKey = createReqVO.getProductKey(); - // 1. productKey为空,生成随机的 11 位字符串 - if (StrUtil.isEmpty(productKey)) { - productKey = UUID.randomUUID().toString().replace("-", "").substring(0, 11); - } - // 2. 校验唯一性 - if (productMapper.selectByProductKey(productKey) != null) { - throw exception(PRODUCT_IDENTIFICATION_EXISTS); - } - createReqVO.setProductKey(productKey); - } - @Override public void updateProduct(IotProductSaveReqVO updateReqVO) { updateReqVO.setProductKey(null); // 不更新产品标识 @@ -81,16 +77,26 @@ public class IotProductServiceImpl implements IotProductService { productMapper.deleteById(id); } - private IotProductDO validateProductExists(Long id) { - IotProductDO iotProductDO = productMapper.selectById(id); - if (iotProductDO == null) { + @Override + public IotProductDO validateProductExists(Long id) { + IotProductDO product = productMapper.selectById(id); + if (product == null) { throw exception(PRODUCT_NOT_EXISTS); } - return iotProductDO; + return product; } - private void validateProductStatus(IotProductDO iotProductDO) { - if (Objects.equals(iotProductDO.getStatus(), IotProductStatusEnum.PUBLISHED.getType())) { + @Override + public IotProductDO validateProductExists(String productKey) { + IotProductDO product = productMapper.selectByProductKey(productKey); + if (product == null) { + throw exception(PRODUCT_NOT_EXISTS); + } + return product; + } + + private void validateProductStatus(IotProductDO product) { + if (Objects.equals(product.getStatus(), IotProductStatusEnum.PUBLISHED.getStatus())) { throw exception(PRODUCT_STATUS_NOT_DELETE); } } @@ -100,16 +106,29 @@ public class IotProductServiceImpl implements IotProductService { return productMapper.selectById(id); } + @Override + public IotProductDO getProductByProductKey(String productKey) { + return productMapper.selectByProductKey(productKey); + } + @Override public PageResult getProductPage(IotProductPageReqVO pageReqVO) { return productMapper.selectPage(pageReqVO); } @Override + @DSTransactional(rollbackFor = Exception.class) public void updateProductStatus(Long id, Integer status) { // 1. 校验存在 validateProductExists(id); - // 2. 更新 + + // 2. 更新为发布状态,需要创建产品超级表数据模型 + // TODO @芋艿:【待定 001】1)是否需要操作后,在 redis 进行缓存,实现一个“快照”的情况,类似 tl; + if (Objects.equals(status, IotProductStatusEnum.PUBLISHED.getStatus())) { + devicePropertyDataService.defineDevicePropertyData(id); + } + + // 3. 更新 IotProductDO updateObj = IotProductDO.builder().id(id).status(status).build(); productMapper.updateById(updateObj); } @@ -119,4 +138,10 @@ public class IotProductServiceImpl implements IotProductService { return productMapper.selectList(); } + @Override + public Long getProductCount(LocalDateTime createTime) { + return productMapper.selectCountByCreateTime(createTime); + } + + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotDataBridgeService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotDataBridgeService.java new file mode 100644 index 000000000..18069376b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotDataBridgeService.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.iot.service.rule; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.IotDataBridgePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.IotDataBridgeSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; +import jakarta.validation.Valid; + +/** + * IoT 数据桥梁 Service 接口 + * + * @author HUIHUI + */ +public interface IotDataBridgeService { + + /** + * 创建数据桥梁 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDataBridge(@Valid IotDataBridgeSaveReqVO createReqVO); + + /** + * 更新数据桥梁 + * + * @param updateReqVO 更新信息 + */ + void updateDataBridge(@Valid IotDataBridgeSaveReqVO updateReqVO); + + /** + * 删除数据桥梁 + * + * @param id 编号 + */ + void deleteDataBridge(Long id); + + /** + * 获得数据桥梁 + * + * @param id 编号 + * @return 数据桥梁 + */ + IotDataBridgeDO getDataBridge(Long id); + + /** + * 获得数据桥梁分页 + * + * @param pageReqVO 分页查询 + * @return 数据桥梁分页 + */ + PageResult getDataBridgePage(IotDataBridgePageReqVO pageReqVO); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotDataBridgeServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotDataBridgeServiceImpl.java new file mode 100644 index 000000000..9e439fc99 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotDataBridgeServiceImpl.java @@ -0,0 +1,70 @@ +package cn.iocoder.yudao.module.iot.service.rule; + +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.rule.vo.databridge.IotDataBridgePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.IotDataBridgeSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; +import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotDataBridgeMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DATA_BRIDGE_NOT_EXISTS; + +/** + * IoT 数据桥梁 Service 实现类 + * + * @author HUIHUI + */ +@Service +@Validated +public class IotDataBridgeServiceImpl implements IotDataBridgeService { + + @Resource + private IotDataBridgeMapper dataBridgeMapper; + + @Override + public Long createDataBridge(IotDataBridgeSaveReqVO createReqVO) { + // 插入 + IotDataBridgeDO dataBridge = BeanUtils.toBean(createReqVO, IotDataBridgeDO.class); + dataBridgeMapper.insert(dataBridge); + // 返回 + return dataBridge.getId(); + } + + @Override + public void updateDataBridge(IotDataBridgeSaveReqVO updateReqVO) { + // 校验存在 + validateDataBridgeExists(updateReqVO.getId()); + // 更新 + IotDataBridgeDO updateObj = BeanUtils.toBean(updateReqVO, IotDataBridgeDO.class); + dataBridgeMapper.updateById(updateObj); + } + + @Override + public void deleteDataBridge(Long id) { + // 校验存在 + validateDataBridgeExists(id); + // 删除 + dataBridgeMapper.deleteById(id); + } + + private void validateDataBridgeExists(Long id) { + if (dataBridgeMapper.selectById(id) == null) { + throw exception(DATA_BRIDGE_NOT_EXISTS); + } + } + + @Override + public IotDataBridgeDO getDataBridge(Long id) { + return dataBridgeMapper.selectById(id); + } + + @Override + public PageResult getDataBridgePage(IotDataBridgePageReqVO pageReqVO) { + return dataBridgeMapper.selectPage(pageReqVO); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneService.java new file mode 100644 index 000000000..6927b1172 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneService.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.iot.service.rule; + +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; + +import java.util.List; + +/** + * IoT 规则场景 Service 接口 + * + * @author 芋道源码 + */ +public interface IotRuleSceneService { + + /** + * 【缓存】获得指定设备的场景列表 + * + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @return 场景列表 + */ + List getRuleSceneListByProductKeyAndDeviceNameFromCache(String productKey, String deviceName); + + /** + * 基于 {@link IotRuleSceneTriggerTypeEnum#DEVICE} 场景,执行规则场景 + * + * @param message 消息 + */ + void executeRuleSceneByDevice(IotDeviceMessage message); + + /** + * 基于 {@link IotRuleSceneTriggerTypeEnum#TIMER} 场景,执行规则场景 + * + * @param id 场景编号 + */ + void executeRuleSceneByTimer(Long id); + + /** + * TODO 芋艿:测试方法,需要删除 + */ + void test(); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneServiceImpl.java new file mode 100644 index 000000000..2219d4bad --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneServiceImpl.java @@ -0,0 +1,438 @@ +package cn.iocoder.yudao.module.iot.service.rule; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.CharPool; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +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.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO; +import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotRuleSceneMapper; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneActionTypeEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneTriggerConditionParameterOperatorEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager; +import cn.iocoder.yudao.module.iot.job.rule.IotRuleSceneJob; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.service.rule.action.IotRuleSceneAction; +import jakarta.annotation.Resource; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.TriggerKey; +import org.quartz.impl.StdSchedulerFactory; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; + +/** + * IoT 规则场景 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class IotRuleSceneServiceImpl implements IotRuleSceneService { + + @Resource + private IotRuleSceneMapper ruleSceneMapper; + + @Resource + private List ruleSceneActions; + + @Resource(name = "iotSchedulerManager") + private IotSchedulerManager schedulerManager; + + // TODO 芋艿,缓存待实现 + @Override + @TenantIgnore // 忽略租户隔离:因为 IotRuleSceneMessageHandler 调用时,一般未传递租户,所以需要忽略 + public List getRuleSceneListByProductKeyAndDeviceNameFromCache(String productKey, String deviceName) { + if (true) { + IotRuleSceneDO ruleScene01 = new IotRuleSceneDO(); + ruleScene01.setTriggers(CollUtil.newArrayList()); + IotRuleSceneDO.TriggerConfig trigger01 = new IotRuleSceneDO.TriggerConfig(); + trigger01.setType(IotRuleSceneTriggerTypeEnum.DEVICE.getType()); + trigger01.setConditions(CollUtil.newArrayList()); + // 属性 + IotRuleSceneDO.TriggerCondition condition01 = new IotRuleSceneDO.TriggerCondition(); + condition01.setType(IotDeviceMessageTypeEnum.PROPERTY.getType()); + condition01.setIdentifier(IotDeviceMessageIdentifierEnum.PROPERTY_REPORT.getIdentifier()); + condition01.setParameters(CollUtil.newArrayList()); +// IotRuleSceneDO.TriggerConditionParameter parameter010 = new IotRuleSceneDO.TriggerConditionParameter(); +// parameter010.setIdentifier("width"); +// parameter010.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.getOperator()); +// parameter010.setValue("abc"); +// condition01.getParameters().add(parameter010); + IotRuleSceneDO.TriggerConditionParameter parameter011 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter011.setIdentifier("width"); + parameter011.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.getOperator()); + parameter011.setValue("1"); + condition01.getParameters().add(parameter011); + IotRuleSceneDO.TriggerConditionParameter parameter012 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter012.setIdentifier("width"); + parameter012.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS.getOperator()); + parameter012.setValue("2"); + condition01.getParameters().add(parameter012); + IotRuleSceneDO.TriggerConditionParameter parameter013 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter013.setIdentifier("width"); + parameter013.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN.getOperator()); + parameter013.setValue("0"); + condition01.getParameters().add(parameter013); + IotRuleSceneDO.TriggerConditionParameter parameter014 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter014.setIdentifier("width"); + parameter014.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS.getOperator()); + parameter014.setValue("0"); + condition01.getParameters().add(parameter014); + IotRuleSceneDO.TriggerConditionParameter parameter015 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter015.setIdentifier("width"); + parameter015.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN.getOperator()); + parameter015.setValue("2"); + condition01.getParameters().add(parameter015); + IotRuleSceneDO.TriggerConditionParameter parameter016 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter016.setIdentifier("width"); + parameter016.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS.getOperator()); + parameter016.setValue("2"); + condition01.getParameters().add(parameter016); + IotRuleSceneDO.TriggerConditionParameter parameter017 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter017.setIdentifier("width"); + parameter017.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.IN.getOperator()); + parameter017.setValue("1,2,3"); + condition01.getParameters().add(parameter017); + IotRuleSceneDO.TriggerConditionParameter parameter018 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter018.setIdentifier("width"); + parameter018.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_IN.getOperator()); + parameter018.setValue("0,2,3"); + condition01.getParameters().add(parameter018); + IotRuleSceneDO.TriggerConditionParameter parameter019 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter019.setIdentifier("width"); + parameter019.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN.getOperator()); + parameter019.setValue("1,3"); + condition01.getParameters().add(parameter019); + IotRuleSceneDO.TriggerConditionParameter parameter020 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter020.setIdentifier("width"); + parameter020.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN.getOperator()); + parameter020.setValue("2,3"); + condition01.getParameters().add(parameter020); + trigger01.getConditions().add(condition01); + // 状态 + IotRuleSceneDO.TriggerCondition condition02 = new IotRuleSceneDO.TriggerCondition(); + condition02.setType(IotDeviceMessageTypeEnum.STATE.getType()); + condition02.setIdentifier(IotDeviceMessageIdentifierEnum.STATE_ONLINE.getIdentifier()); + condition02.setParameters(CollUtil.newArrayList()); + trigger01.getConditions().add(condition02); + // 事件 + IotRuleSceneDO.TriggerCondition condition03 = new IotRuleSceneDO.TriggerCondition(); + condition03.setType(IotDeviceMessageTypeEnum.EVENT.getType()); + condition03.setIdentifier("xxx"); + condition03.setParameters(CollUtil.newArrayList()); + IotRuleSceneDO.TriggerConditionParameter parameter030 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter030.setIdentifier("width"); + parameter030.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.getOperator()); + parameter030.setValue("1"); + trigger01.getConditions().add(condition03); + ruleScene01.getTriggers().add(trigger01); + // 动作 + ruleScene01.setActions(CollUtil.newArrayList()); + // 设备控制 + IotRuleSceneDO.ActionConfig action01 = new IotRuleSceneDO.ActionConfig(); + action01.setType(IotRuleSceneActionTypeEnum.DEVICE_CONTROL.getType()); + IotRuleSceneDO.ActionDeviceControl actionDeviceControl01 = new IotRuleSceneDO.ActionDeviceControl(); + actionDeviceControl01.setProductKey("4aymZgOTOOCrDKRT"); + actionDeviceControl01.setDeviceNames(ListUtil.of("small")); + actionDeviceControl01.setType(IotDeviceMessageTypeEnum.PROPERTY.getType()); + actionDeviceControl01.setIdentifier(IotDeviceMessageIdentifierEnum.PROPERTY_SET.getIdentifier()); + actionDeviceControl01.setData(MapUtil.builder() + .put("power", 1) + .put("color", "red") + .build()); + action01.setDeviceControl(actionDeviceControl01); +// ruleScene01.getActions().add(action01); // TODO 芋艿:先不测试了 + // 数据桥接(http) + IotRuleSceneDO.ActionConfig action02 = new IotRuleSceneDO.ActionConfig(); + action02.setType(IotRuleSceneActionTypeEnum.DATA_BRIDGE.getType()); + action02.setDataBridgeId(1L); + ruleScene01.getActions().add(action02); + return ListUtil.toList(ruleScene01); + } + + List list = ruleSceneMapper.selectList(); + // TODO @芋艿:需要考虑开启状态 + return filterList(list, ruleScene -> { + for (IotRuleSceneDO.TriggerConfig trigger : ruleScene.getTriggers()) { + if (ObjUtil.notEqual(trigger.getProductKey(), productKey)) { + continue; + } + if (CollUtil.isEmpty(trigger.getDeviceNames()) // 无设备名称限制 + || trigger.getDeviceNames().contains(deviceName)) { // 包含设备名称 + return true; + } + } + return false; + }); + } + + @Override + public void executeRuleSceneByDevice(IotDeviceMessage message) { + TenantUtils.execute(message.getTenantId(), () -> { + // 1. 获得设备匹配的规则场景 + List ruleScenes = getMatchedRuleSceneListByMessage(message); + if (CollUtil.isEmpty(ruleScenes)) { + return; + } + + // 2. 执行规则场景 + executeRuleSceneAction(message, ruleScenes); + }); + } + + @Override + public void executeRuleSceneByTimer(Long id) { + // 1.1 获得规则场景 +// IotRuleSceneDO scene = TenantUtils.executeIgnore(() -> ruleSceneMapper.selectById(id)); + // TODO @芋艿:这里,临时测试,后续删除。 + IotRuleSceneDO scene = new IotRuleSceneDO().setStatus(CommonStatusEnum.ENABLE.getStatus()); + if (true) { + scene.setTenantId(1L); + IotRuleSceneDO.TriggerConfig triggerConfig = new IotRuleSceneDO.TriggerConfig(); + triggerConfig.setType(IotRuleSceneTriggerTypeEnum.TIMER.getType()); + scene.setTriggers(ListUtil.toList(triggerConfig)); + // 动作 + IotRuleSceneDO.ActionConfig action01 = new IotRuleSceneDO.ActionConfig(); + action01.setType(IotRuleSceneActionTypeEnum.DEVICE_CONTROL.getType()); + IotRuleSceneDO.ActionDeviceControl actionDeviceControl01 = new IotRuleSceneDO.ActionDeviceControl(); + actionDeviceControl01.setProductKey("4aymZgOTOOCrDKRT"); + actionDeviceControl01.setDeviceNames(ListUtil.of("small")); + actionDeviceControl01.setType(IotDeviceMessageTypeEnum.PROPERTY.getType()); + actionDeviceControl01.setIdentifier(IotDeviceMessageIdentifierEnum.PROPERTY_SET.getIdentifier()); + actionDeviceControl01.setData(MapUtil.builder() + .put("power", 1) + .put("color", "red") + .build()); + action01.setDeviceControl(actionDeviceControl01); + scene.setActions(ListUtil.toList(action01)); + } + if (scene == null) { + log.error("[executeRuleSceneByTimer][规则场景({}) 不存在]", id); + return; + } + if (CommonStatusEnum.isDisable(scene.getStatus())) { + log.info("[executeRuleSceneByTimer][规则场景({}) 已被禁用]", id); + return; + } + // 1.2 判断是否有定时触发器,避免脏数据 + IotRuleSceneDO.TriggerConfig config = CollUtil.findOne(scene.getTriggers(), + trigger -> ObjUtil.equals(trigger.getType(), IotRuleSceneTriggerTypeEnum.TIMER.getType())); + if (config == null) { + log.error("[executeRuleSceneByTimer][规则场景({}) 不存在定时触发器]", scene); + return; + } + + // 2. 执行规则场景 + TenantUtils.execute(scene.getTenantId(), + () -> executeRuleSceneAction(null, ListUtil.toList(scene))); + } + + /** + * 基于消息,获得匹配的规则场景列表 + * + * @param message 设备消息 + * @return 规则场景列表 + */ + private List getMatchedRuleSceneListByMessage(IotDeviceMessage message) { + // 1. 匹配设备 + // TODO @芋艿:可能需要 getSelf(); 缓存 + List ruleScenes = getRuleSceneListByProductKeyAndDeviceNameFromCache( + message.getProductKey(), message.getDeviceName()); + if (CollUtil.isEmpty(ruleScenes)) { + return ruleScenes; + } + + // 2. 匹配 trigger 触发器的条件 + return filterList(ruleScenes, ruleScene -> { + for (IotRuleSceneDO.TriggerConfig trigger : ruleScene.getTriggers()) { + // 2.1 非设备触发,不匹配 + if (ObjUtil.notEqual(trigger.getType(), IotRuleSceneTriggerTypeEnum.DEVICE.getType())) { + return false; + } + // TODO 芋艿:产品、设备的匹配,要不要这里在做一次???貌似和 1. 部分重复了 + // 2.2 条件为空,说明没有匹配的条件,因此不匹配 + if (CollUtil.isEmpty(trigger.getConditions())) { + return false; + } + // 2.3 多个条件,只需要满足一个即可 + IotRuleSceneDO.TriggerCondition matchedCondition = CollUtil.findOne(trigger.getConditions(), condition -> { + if (ObjUtil.notEqual(message.getType(), condition.getType()) + || ObjUtil.notEqual(message.getIdentifier(), condition.getIdentifier())) { + return false; + } + // 多个条件参数,必须全部满足。所以,下面的逻辑就是找到一个不满足的条件参数 + IotRuleSceneDO.TriggerConditionParameter notMatchedParameter = CollUtil.findOne(condition.getParameters(), + parameter -> !isTriggerConditionParameterMatched(message, parameter, ruleScene, trigger)); + return notMatchedParameter == null; + }); + if (matchedCondition == null) { + return false; + } + log.info("[getMatchedRuleSceneList][消息({}) 匹配到规则场景编号({}) 的触发器({})]", message, ruleScene.getId(), trigger); + return true; + } + return false; + }); + } + + // TODO @芋艿:【可优化】可以考虑增加下单测,边界太多了。 + /** + * 判断触发器的条件参数是否匹配 + * + * @param message 设备消息 + * @param parameter 触发器条件参数 + * @param ruleScene 规则场景(用于日志,无其它作用) + * @param trigger 触发器(用于日志,无其它作用) + * @return 是否匹配 + */ + @SuppressWarnings({"unchecked", "DataFlowIssue"}) + private boolean isTriggerConditionParameterMatched(IotDeviceMessage message, IotRuleSceneDO.TriggerConditionParameter parameter, + IotRuleSceneDO ruleScene, IotRuleSceneDO.TriggerConfig trigger) { + // 1.1 校验操作符是否合法 + IotRuleSceneTriggerConditionParameterOperatorEnum operator = + IotRuleSceneTriggerConditionParameterOperatorEnum.operatorOf(parameter.getOperator()); + if (operator == null) { + log.error("[isTriggerConditionParameterMatched][规则场景编号({}) 的触发器({}) 存在错误的操作符({})]", + ruleScene.getId(), trigger, parameter.getOperator()); + return false; + } + // 1.2 校验消息是否包含对应的值 + String messageValue = MapUtil.getStr((Map) message.getData(), parameter.getIdentifier()); + if (messageValue == null) { + return false; + } + + // 2.1 构建 Spring 表达式的变量 + Map springExpressionVariables = new HashMap<>(); + try { + springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_SOURCE, messageValue); + springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_VALUE, parameter.getValue()); + List parameterValues = StrUtil.splitTrim(parameter.getValue(), CharPool.COMMA); + springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_VALUE_List, parameterValues); + // 特殊:解决数字的比较。因为 Spring 是基于它的 compareTo 方法,对数字的比较存在问题! + if (ObjectUtils.equalsAny(operator, IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN, + IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN, + IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN, + IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS, + IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN, + IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS) + && NumberUtil.isNumber(messageValue) + && NumberUtils.isAllNumber(parameterValues)) { + springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_SOURCE, + NumberUtil.parseDouble(messageValue)); + springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_VALUE, + NumberUtil.parseDouble(parameter.getValue())); + springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_VALUE_List, + convertList(parameterValues, NumberUtil::parseDouble)); + } + // 2.2 计算 Spring 表达式 + return (Boolean) SpringExpressionUtils.parseExpression(operator.getSpringExpression(), springExpressionVariables); + } catch (Exception e) { + log.error("[isTriggerConditionParameterMatched][消息({}) 规则场景编号({}) 的触发器({}) 的匹配表达式({}/{}) 计算异常]", + message, ruleScene.getId(), trigger, operator, springExpressionVariables, e); + return false; + } + } + + /** + * 执行规则场景的动作 + * + * @param message 设备消息 + * @param ruleScenes 规则场景列表 + */ + private void executeRuleSceneAction(IotDeviceMessage message, List ruleScenes) { + // 1. 遍历规则场景 + ruleScenes.forEach(ruleScene -> { + // 2. 遍历规则场景的动作 + ruleScene.getActions().forEach(actionConfig -> { + // 3.1 获取对应的动作 Action 数组 + List actions = filterList(ruleSceneActions, + action -> action.getType().getType().equals(actionConfig.getType())); + if (CollUtil.isEmpty(actions)) { + return; + } + // 3.2 执行动作 + actions.forEach(action -> { + try { + action.execute(message, actionConfig); + log.info("[executeRuleSceneAction][消息({}) 规则场景编号({}) 的执行动作({}) 成功]", + message, ruleScene.getId(), actionConfig); + } catch (Exception e) { + log.error("[executeRuleSceneAction][消息({}) 规则场景编号({}) 的执行动作({}) 执行异常]", + message, ruleScene.getId(), actionConfig, e); + } + }); + }); + }); + } + + @Override + @SneakyThrows + public void test() { + // TODO @芋艿:测试思路代码,记得删除!!! + // 1. Job 类:IotRuleSceneJob DONE + // 2. 参数:id DONE + // 3. jobHandlerName:IotRuleSceneJob + id DONE + + // 新增:addJob + // 修改:不存在 addJob、存在 updateJob + // 有 + 禁用:1)存在、停止;2)不存在:不处理;TODO 测试:直接暂停,是否可以???(结论:可以)pauseJob + // 有 + 开启:1)存在,更新;2)不存在,新增;结论:使用 save(addOrUpdateJob) + // 无 + 禁用、开启:1)存在,删除;TODO 测试:直接删除???(结论:可以)deleteJob + + // + if (false) { + Long id = 1L; + Map jobDataMap = IotRuleSceneJob.buildJobDataMap(id); + schedulerManager.addOrUpdateJob(IotRuleSceneJob.class, + IotRuleSceneJob.buildJobName(id), + "0/10 * * * * ?", + jobDataMap); + } + if (false) { + Long id = 1L; + schedulerManager.pauseJob(IotRuleSceneJob.buildJobName(id)); + } + if (true) { + Long id = 1L; + schedulerManager.deleteJob(IotRuleSceneJob.buildJobName(id)); + } + } + + public static void main2(String[] args) throws SchedulerException { +// System.out.println(QuartzJobBean.class); + Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); + scheduler.start(); + + String jobHandlerName = "123"; + // 暂停 Trigger 对象 + scheduler.pauseTrigger(new TriggerKey(jobHandlerName)); + // 取消并删除 Job 调度 + scheduler.unscheduleJob(new TriggerKey(jobHandlerName)); + scheduler.deleteJob(new JobKey(jobHandlerName)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneAction.java new file mode 100644 index 000000000..c7b921c04 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneAction.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.iot.service.rule.action; + +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneActionTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; + +import javax.annotation.Nullable; + +/** + * IoT 规则场景的场景执行器接口 + * + * @author 芋道源码 + */ +public interface IotRuleSceneAction { + + // TODO @芋艿:groovy 或者 javascript 实现数据的转换;可以考虑基于 hutool 的 ScriptUtil 做 + /** + * 执行场景 + * + * @param message 消息,允许空 + * 1. 空的情况:定时触发 + * 2. 非空的情况:设备触发 + * @param config 配置 + */ + void execute(@Nullable IotDeviceMessage message, IotRuleSceneDO.ActionConfig config) throws Exception; + + /** + * 获得类型 + * + * @return 类型 + */ + IotRuleSceneActionTypeEnum getType(); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneAlertAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneAlertAction.java new file mode 100644 index 000000000..eadc17378 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneAlertAction.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.iot.service.rule.action; + +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneActionTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import org.springframework.stereotype.Component; + +import javax.annotation.Nullable; + +/** + * IoT 告警的 {@link IotRuleSceneAction} 实现类 + * + * @author 芋道源码 + */ +@Component +public class IotRuleSceneAlertAction implements IotRuleSceneAction { + + @Override + public void execute(@Nullable IotDeviceMessage message, IotRuleSceneDO.ActionConfig config) { + // TODO @芋艿:待实现 + } + + @Override + public IotRuleSceneActionTypeEnum getType() { + return IotRuleSceneActionTypeEnum.ALERT; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneDataBridgeAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneDataBridgeAction.java new file mode 100644 index 000000000..b38e181f9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneDataBridgeAction.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.iot.service.rule.action; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneActionTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.service.rule.IotDataBridgeService; +import cn.iocoder.yudao.module.iot.service.rule.action.databridge.IotDataBridgeExecute; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * IoT 数据桥梁的 {@link IotRuleSceneAction} 实现类 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class IotRuleSceneDataBridgeAction implements IotRuleSceneAction { + + @Resource + private IotDataBridgeService dataBridgeService; + @Resource + private List> dataBridgeExecutes; + + @Override + public void execute(IotDeviceMessage message, IotRuleSceneDO.ActionConfig config) throws Exception { + // 1.1 如果消息为空,直接返回 + if (message == null) { + return; + } + // 1.2 获得数据桥梁 + Assert.notNull(config.getDataBridgeId(), "数据桥梁编号不能为空"); + IotDataBridgeDO dataBridge = dataBridgeService.getDataBridge(config.getDataBridgeId()); + if (dataBridge == null || dataBridge.getConfig() == null) { + log.error("[execute][message({}) config({}) 对应的数据桥梁不存在]", message, config); + return; + } + if (CommonStatusEnum.isDisable(dataBridge.getStatus())) { + log.info("[execute][message({}) config({}) 对应的数据桥梁({}) 状态为禁用]", message, config, dataBridge); + return; + } + + // 2. 执行数据桥接操作 + for (IotDataBridgeExecute execute : dataBridgeExecutes) { + execute.execute(message, dataBridge); + } + } + + @Override + public IotRuleSceneActionTypeEnum getType() { + return IotRuleSceneActionTypeEnum.DATA_BRIDGE; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneDeviceControlAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneDeviceControlAction.java new file mode 100644 index 000000000..d8fd76b5e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneDeviceControlAction.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.iot.service.rule.action; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceDownstreamReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneActionTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceDownstreamService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * IoT 设备控制的 {@link IotRuleSceneAction} 实现类 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class IotRuleSceneDeviceControlAction implements IotRuleSceneAction { + + @Resource + private IotDeviceDownstreamService deviceDownstreamService; + @Resource + private IotDeviceService deviceService; + + @Override + public void execute(IotDeviceMessage message, IotRuleSceneDO.ActionConfig config) { + IotRuleSceneDO.ActionDeviceControl control = config.getDeviceControl(); + Assert.notNull(control, "设备控制配置不能为空"); + // 遍历每个设备,下发消息 + control.getDeviceNames().forEach(deviceName -> { + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache(control.getProductKey(), deviceName); + if (device == null) { + log.error("[execute][message({}) config({}) 对应的设备不存在]", message, config); + return; + } + try { + IotDeviceMessage downstreamMessage = deviceDownstreamService.downstreamDevice(new IotDeviceDownstreamReqVO() + .setId(device.getId()).setType(control.getType()).setIdentifier(control.getIdentifier()) + .setData(control.getData())); + log.info("[execute][message({}) config({}) 下发消息({})成功]", message, config, downstreamMessage); + } catch (Exception e) { + log.error("[execute][message({}) config({}) 下发消息失败]", message, config, e); + } + }); + } + + @Override + public IotRuleSceneActionTypeEnum getType() { + return IotRuleSceneActionTypeEnum.DEVICE_CONTROL; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/AbstractCacheableDataBridgeExecute.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/AbstractCacheableDataBridgeExecute.java new file mode 100644 index 000000000..e7f84dd6c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/AbstractCacheableDataBridgeExecute.java @@ -0,0 +1,114 @@ +package cn.iocoder.yudao.module.iot.service.rule.action.databridge; + +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.cache.RemovalListener; +import lombok.extern.slf4j.Slf4j; + +import java.time.Duration; + +// TODO @芋艿:数据库 +// TODO @芋艿:mqtt +// TODO @芋艿:tcp +// TODO @芋艿:websocket + +/** + * 带缓存功能的数据桥梁执行器抽象类 + * + * 该类提供了一个通用的缓存机制,用于管理各类数据桥接的生产者(Producer)实例。 + * + * 主要特点: + * - 基于Guava Cache实现高效的生产者实例缓存管理 + * - 自动处理生产者的生命周期(创建、获取、关闭) + * - 支持30分钟未访问自动过期清理机制 + * - 异常处理与日志记录,便于问题排查 + * + * 子类需要实现: + * - initProducer(Config) - 初始化特定类型的生产者实例 + * - closeProducer(Producer) - 关闭生产者实例并释放资源 + * + * @param 配置信息类型,用于初始化生产者 + * @param 生产者类型,负责将数据发送到目标系统 + * @author HUIHUI + */ +@Slf4j +public abstract class AbstractCacheableDataBridgeExecute implements IotDataBridgeExecute { + + /** + * Producer 缓存 + */ + private final LoadingCache PRODUCER_CACHE = CacheBuilder.newBuilder() + .expireAfterAccess(Duration.ofMinutes(30)) // 30 分钟未访问就提前过期 + .removalListener((RemovalListener) notification -> { + Producer producer = notification.getValue(); + if (producer == null) { + return; + } + + try { + closeProducer(producer); + log.info("[PRODUCER_CACHE][配置({}) 对应的 producer 已关闭]", notification.getKey()); + } catch (Exception e) { + log.error("[PRODUCER_CACHE][配置({}) 对应的 producer 关闭失败]", notification.getKey(), e); + } + }) + .build(new CacheLoader() { + + @Override + public Producer load(Config config) throws Exception { + try { + Producer producer = initProducer(config); + log.info("[PRODUCER_CACHE][配置({}) 对应的 producer 已创建并启动]", config); + return producer; + } catch (Exception e) { + log.error("[PRODUCER_CACHE][配置({}) 对应的 producer 创建启动失败]", config, e); + throw e; // 抛出异常,触发缓存加载失败机制 + } + } + + }); + + /** + * 获取生产者 + * + * @param config 配置信息 + * @return 生产者对象 + */ + protected Producer getProducer(Config config) throws Exception { + return PRODUCER_CACHE.get(config); + } + + /** + * 初始化生产者 + * + * @param config 配置信息 + * @return 生产者对象 + * @throws Exception 如果初始化失败 + */ + protected abstract Producer initProducer(Config config) throws Exception; + + /** + * 关闭生产者 + * + * @param producer 生产者对象 + */ + protected abstract void closeProducer(Producer producer) throws Exception; + + @Override + @SuppressWarnings({"unchecked"}) + public void execute(IotDeviceMessage message, IotDataBridgeDO dataBridge) { + if (ObjUtil.notEqual(message.getType(), getType())) { + return; + } + try { + execute0(message, (Config) dataBridge.getConfig()); + } catch (Exception e) { + log.error("[execute][桥梁配置 config({}) 对应的 message({}) 发送异常]", dataBridge.getConfig(), message, e); + } + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecute.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecute.java new file mode 100644 index 000000000..ce3d0f193 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecute.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.iot.service.rule.action.databridge; + +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; + + +/** + * IoT 数据桥梁的执行器 execute 接口 + * + * @author HUIHUI + */ +public interface IotDataBridgeExecute { + + /** + * 获取数据桥梁类型 + * + * @return 数据桥梁类型 + */ + Integer getType(); + + /** + * 执行数据桥梁操作 + * + * @param message 设备消息 + * @param dataBridge 数据桥梁 + */ + @SuppressWarnings({"unchecked"}) + default void execute(IotDeviceMessage message, IotDataBridgeDO dataBridge) throws Exception { + // 1.1 校验数据桥梁类型 + if (!getType().equals(dataBridge.getType())) { + return; + } + + // 1.2 执行对应的数据桥梁发送消息 + execute0(message, (Config) dataBridge.getConfig()); + } + + /** + * 【真正】执行数据桥梁操作 + * + * @param message 设备消息 + * @param config 桥梁配置 + */ + void execute0(IotDeviceMessage message, Config config) throws Exception; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotHttpDataBridgeExecute.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotHttpDataBridgeExecute.java new file mode 100644 index 000000000..22b72e055 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotHttpDataBridgeExecute.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.iot.service.rule.action.databridge; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeHttpConfig; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.HashMap; +import java.util.Map; + +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; + +/** + * Http 的 {@link IotDataBridgeExecute} 实现类 + * + * @author HUIHUI + */ +@Component +@Slf4j +public class IotHttpDataBridgeExecute implements IotDataBridgeExecute { + + @Resource + private RestTemplate restTemplate; + + @Override + public Integer getType() { + return IotDataBridgeTypeEnum.HTTP.getType(); + } + + @Override + @SuppressWarnings({"unchecked", "deprecation"}) + public void execute0(IotDeviceMessage message, IotDataBridgeHttpConfig config) { + String url = null; + HttpMethod method = HttpMethod.valueOf(config.getMethod().toUpperCase()); + HttpEntity requestEntity = null; + ResponseEntity responseEntity = null; + try { + // 1.1 构建 Header + HttpHeaders headers = new HttpHeaders(); + if (CollUtil.isNotEmpty(config.getHeaders())) { + config.getHeaders().putAll(config.getHeaders()); + } + headers.add(HEADER_TENANT_ID, message.getTenantId().toString()); + // 1.2 构建 URL + UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(config.getUrl()); + if (CollUtil.isNotEmpty(config.getQuery())) { + config.getQuery().forEach(uriBuilder::queryParam); + } + // 1.3 构建请求体 + if (method == HttpMethod.GET) { + uriBuilder.queryParam("message", HttpUtils.encodeUtf8(JsonUtils.toJsonString(message))); + url = uriBuilder.build().toUriString(); + requestEntity = new HttpEntity<>(headers); + } else { + url = uriBuilder.build().toUriString(); + Map requestBody = JsonUtils.parseObject(config.getBody(), Map.class); + if (requestBody == null) { + requestBody = new HashMap<>(); + } + requestBody.put("message", message); + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE); + requestEntity = new HttpEntity<>(JsonUtils.toJsonString(requestBody), headers); + } + + // 2.1 发送请求 + responseEntity = restTemplate.exchange(url, method, requestEntity, String.class); + // 2.2 记录日志 + if (responseEntity.getStatusCode().is2xxSuccessful()) { + log.info("[executeHttp][message({}) config({}) url({}) method({}) requestEntity({}) 请求成功({})]", + message, config, url, method, requestEntity, responseEntity); + } else { + log.error("[executeHttp][message({}) config({}) url({}) method({}) requestEntity({}) 请求失败({})]", + message, config, url, method, requestEntity, responseEntity); + } + } catch (Exception e) { + log.error("[executeHttp][message({}) config({}) url({}) method({}) requestEntity({}) 请求异常({})]", + message, config, url, method, requestEntity, responseEntity, e); + } + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotKafkaMQDataBridgeExecute.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotKafkaMQDataBridgeExecute.java new file mode 100644 index 000000000..5674c7d60 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotKafkaMQDataBridgeExecute.java @@ -0,0 +1,78 @@ +package cn.iocoder.yudao.module.iot.service.rule.action.databridge; + +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeKafkaMQConfig; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * Kafka 的 {@link IotDataBridgeExecute} 实现类 + * + * @author HUIHUI + */ +@ConditionalOnClass(name = "org.springframework.kafka.core.KafkaTemplate") +@Component +@Slf4j +public class IotKafkaMQDataBridgeExecute extends + AbstractCacheableDataBridgeExecute> { + + private static final Duration SEND_TIMEOUT = Duration.ofMillis(10000); // 10 秒超时时间 + + @Override + public Integer getType() { + return IotDataBridgeTypeEnum.KAFKA.getType(); + } + + @Override + public void execute0(IotDeviceMessage message, IotDataBridgeKafkaMQConfig config) throws Exception { + // 1. 获取或创建 KafkaTemplate + KafkaTemplate kafkaTemplate = getProducer(config); + + // 2. 发送消息并等待结果 + kafkaTemplate.send(config.getTopic(), message.toString()) + .get(SEND_TIMEOUT.getSeconds(), TimeUnit.SECONDS); // 添加超时等待 + log.info("[execute0][message({}) 发送成功]", message); + } + + @Override + protected KafkaTemplate initProducer(IotDataBridgeKafkaMQConfig config) { + // 1.1 构建生产者配置 + Map props = new HashMap<>(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, config.getBootstrapServers()); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + // 1.2 如果配置了认证信息 + if (config.getUsername() != null && config.getPassword() != null) { + props.put("security.protocol", "SASL_PLAINTEXT"); + props.put("sasl.mechanism", "PLAIN"); + props.put("sasl.jaas.config", + "org.apache.kafka.common.security.plain.PlainLoginModule required username=\"" + + config.getUsername() + "\" password=\"" + config.getPassword() + "\";"); + } + // 1.3 如果启用 SSL + if (Boolean.TRUE.equals(config.getSsl())) { + props.put("security.protocol", "SSL"); + } + + // 2. 创建 KafkaTemplate + DefaultKafkaProducerFactory producerFactory = new DefaultKafkaProducerFactory<>(props); + return new KafkaTemplate<>(producerFactory); + } + + @Override + protected void closeProducer(KafkaTemplate producer) { + producer.destroy(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRabbitMQDataBridgeExecute.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRabbitMQDataBridgeExecute.java new file mode 100644 index 000000000..efe08b1fc --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRabbitMQDataBridgeExecute.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.iot.service.rule.action.databridge; + +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeRabbitMQConfig; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; + +/** + * RabbitMQ 的 {@link IotDataBridgeExecute} 实现类 + * + * @author HUIHUI + */ +@ConditionalOnClass(name = "com.rabbitmq.client.Channel") +@Component +@Slf4j +public class IotRabbitMQDataBridgeExecute extends + AbstractCacheableDataBridgeExecute { + + + @Override + public Integer getType() { + return IotDataBridgeTypeEnum.RABBITMQ.getType(); + } + + @Override + public void execute0(IotDeviceMessage message, IotDataBridgeRabbitMQConfig config) throws Exception { + // 1. 获取或创建 Channel + Channel channel = getProducer(config); + + // 2.1 声明交换机、队列和绑定关系 + channel.exchangeDeclare(config.getExchange(), "direct", true); + channel.queueDeclare(config.getQueue(), true, false, false, null); + channel.queueBind(config.getQueue(), config.getExchange(), config.getRoutingKey()); + + // 2.2 发送消息 + channel.basicPublish(config.getExchange(), config.getRoutingKey(), null, + message.toString().getBytes(StandardCharsets.UTF_8)); + log.info("[executeRabbitMQ][message({}) config({}) 发送成功]", message, config); + } + + @Override + @SuppressWarnings("resource") + protected Channel initProducer(IotDataBridgeRabbitMQConfig config) throws Exception { + // 1. 创建连接工厂 + ConnectionFactory factory = new ConnectionFactory(); + factory.setHost(config.getHost()); + factory.setPort(config.getPort()); + factory.setVirtualHost(config.getVirtualHost()); + factory.setUsername(config.getUsername()); + factory.setPassword(config.getPassword()); + + // 2. 创建连接 + Connection connection = factory.newConnection(); + + // 3. 创建信道 + return connection.createChannel(); + } + + @Override + protected void closeProducer(Channel channel) throws Exception { + if (channel.isOpen()) { + channel.close(); + } + Connection connection = channel.getConnection(); + if (connection.isOpen()) { + connection.close(); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRedisStreamMQDataBridgeExecute.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRedisStreamMQDataBridgeExecute.java new file mode 100644 index 000000000..2aac76619 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRedisStreamMQDataBridgeExecute.java @@ -0,0 +1,96 @@ +package cn.iocoder.yudao.module.iot.service.rule.action.databridge; + +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeRedisStreamMQConfig; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.extern.slf4j.Slf4j; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.SingleServerConfig; +import org.redisson.spring.data.connection.RedissonConnectionFactory; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.stream.ObjectRecord; +import org.springframework.data.redis.connection.stream.StreamRecords; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.stereotype.Component; + +/** + * Redis Stream MQ 的 {@link IotDataBridgeExecute} 实现类 + * + * @author HUIHUI + */ +@Component +@Slf4j +public class IotRedisStreamMQDataBridgeExecute extends + AbstractCacheableDataBridgeExecute> { + + @Override + public Integer getType() { + return IotDataBridgeTypeEnum.REDIS_STREAM.getType(); + } + + @Override + public void execute0(IotDeviceMessage message, IotDataBridgeRedisStreamMQConfig config) throws Exception { + // 1. 获取 RedisTemplate + RedisTemplate redisTemplate = getProducer(config); + + // 2. 创建并发送 Stream 记录 + ObjectRecord record = StreamRecords.newRecord() + .ofObject(message).withStreamKey(config.getTopic()); + String recordId = String.valueOf(redisTemplate.opsForStream().add(record)); + log.info("[executeRedisStream][消息发送成功] messageId: {}, config: {}", recordId, config); + } + + @Override + protected RedisTemplate initProducer(IotDataBridgeRedisStreamMQConfig config) { + // 1.1 创建 Redisson 配置 + Config redissonConfig = new Config(); + SingleServerConfig serverConfig = redissonConfig.useSingleServer() + .setAddress("redis://" + config.getHost() + ":" + config.getPort()) + .setDatabase(config.getDatabase()); + // 1.2 设置密码(如果有) + if (StrUtil.isNotBlank(config.getPassword())) { + serverConfig.setPassword(config.getPassword()); + } + + // TODO @huihui:看看能不能简化一些。按道理说,不用这么多的哈。 + // 2.1 创建 RedissonClient + RedissonClient redisson = Redisson.create(redissonConfig); + // 2.2 创建并配置 RedisTemplate + RedisTemplate template = new RedisTemplate<>(); + // 设置 RedisConnection 工厂。😈 它就是实现多种 Java Redis 客户端接入的秘密工厂。感兴趣的胖友,可以自己去撸下。 + template.setConnectionFactory(new RedissonConnectionFactory(redisson)); + // 使用 String 序列化方式,序列化 KEY 。 + template.setKeySerializer(RedisSerializer.string()); + template.setHashKeySerializer(RedisSerializer.string()); + // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。 + template.setValueSerializer(buildRedisSerializer()); + template.setHashValueSerializer(buildRedisSerializer()); + template.afterPropertiesSet();// 初始化 + return template; + } + + @Override + protected void closeProducer(RedisTemplate producer) throws Exception { + RedisConnectionFactory factory = producer.getConnectionFactory(); + if (factory != null) { + ((RedissonConnectionFactory) factory).destroy(); + } + } + + // TODO @huihui:看看能不能简化一些。按道理说,不用这么多的哈。 + public static RedisSerializer buildRedisSerializer() { + RedisSerializer json = RedisSerializer.json(); + // 解决 LocalDateTime 的序列化 + ObjectMapper objectMapper = (ObjectMapper) ReflectUtil.getFieldValue(json, "mapper"); + objectMapper.registerModules(new JavaTimeModule()); + return json; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRocketMQDataBridgeExecute.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRocketMQDataBridgeExecute.java new file mode 100644 index 000000000..d3ac77227 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRocketMQDataBridgeExecute.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.module.iot.service.rule.action.databridge; + +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeRocketMQConfig; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.stereotype.Component; + +/** + * RocketMQ 的 {@link IotDataBridgeExecute} 实现类 + * + * @author HUIHUI + */ +@ConditionalOnClass(name = "org.apache.rocketmq.client.producer.DefaultMQProducer") +@Component +@Slf4j +public class IotRocketMQDataBridgeExecute extends + AbstractCacheableDataBridgeExecute { + + @Override + public Integer getType() { + return IotDataBridgeTypeEnum.ROCKETMQ.getType(); + } + + @Override + public void execute0(IotDeviceMessage message, IotDataBridgeRocketMQConfig config) throws Exception { + // 1. 获取或创建 Producer + DefaultMQProducer producer = getProducer(config); + + // 2.1 创建消息对象,指定Topic、Tag和消息体 + Message msg = new Message( + config.getTopic(), + config.getTags(), + message.toString().getBytes(RemotingHelper.DEFAULT_CHARSET) + ); + // 2.2 发送同步消息并处理结果 + SendResult sendResult = producer.send(msg); + // 2.3 处理发送结果 + if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) { + log.info("[executeRocketMQ][message({}) config({}) 发送成功,结果({})]", message, config, sendResult); + } else { + log.error("[executeRocketMQ][message({}) config({}) 发送失败,结果({})]", message, config, sendResult); + } + } + + @Override + protected DefaultMQProducer initProducer(IotDataBridgeRocketMQConfig config) throws Exception { + DefaultMQProducer producer = new DefaultMQProducer(config.getGroup()); + producer.setNamesrvAddr(config.getNameServer()); + producer.start(); + return producer; + } + + @Override + protected void closeProducer(DefaultMQProducer producer) { + producer.shutdown(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelService.java new file mode 100644 index 000000000..8834772d3 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelService.java @@ -0,0 +1,93 @@ +package cn.iocoder.yudao.module.iot.service.thingmodel; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelListReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import jakarta.validation.Valid; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * IoT 产品物模型 Service 接口 + * + * @author 芋道源码 + */ +public interface IotThingModelService { + + /** + * 创建产品物模型 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createThingModel(@Valid IotThingModelSaveReqVO createReqVO); + + /** + * 更新产品物模型 + * + * @param updateReqVO 更新信息 + */ + void updateThingModel(@Valid IotThingModelSaveReqVO updateReqVO); + + /** + * 删除产品物模型 + * + * @param id 编号 + */ + void deleteThingModel(Long id); + + /** + * 获得产品物模型 + * + * @param id 编号 + * @return 产品物模型 + */ + IotThingModelDO getThingModel(Long id); + + /** + * 获得产品物模型列表 + * + * @param productId 产品编号 + * @return 产品物模型列表 + */ + List getThingModelListByProductId(Long productId); + + /** + * 【缓存】获得产品物模型列表 + * + * 注意:该方法会忽略租户信息,所以调用时,需要确认会不会有跨租户访问的风险!!! + * + * @param productKey 产品标识 + * @return 产品物模型列表 + */ + List getThingModelListByProductKeyFromCache(String productKey); + + /** + * 获得产品物模型分页 + * + * @param pageReqVO 分页查询 + * @return 产品物模型分页 + */ + PageResult getProductThingModelPage(IotThingModelPageReqVO pageReqVO); + + /** + * 获得产品物模型列表 + * + * @param reqVO 列表查询 + * @return 产品物模型列表 + */ + List getThingModelList(IotThingModelListReqVO reqVO); + + // TODO @super:用不到,删除下哈。 + /** + * 获得物模型数量 + * + * @param createTime 创建时间,如果为空,则统计所有物模型数量 + * @return 物模型数量 + */ + Long getThingModelCount(LocalDateTime createTime); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java new file mode 100644 index 000000000..9487ff2de --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java @@ -0,0 +1,373 @@ +package cn.iocoder.yudao.module.iot.service.thingmodel; + +import cn.hutool.core.collection.CollUtil; +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.object.BeanUtils; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelEvent; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelParam; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelService; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelListReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelSaveReqVO; +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.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.*; +import cn.iocoder.yudao.module.iot.service.product.IotProductService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDateTime; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; + +/** + * IoT 产品物模型 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class IotThingModelServiceImpl implements IotThingModelService { + + @Resource + private IotThingModelMapper thingModelMapper; + + @Resource + private IotProductService productService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createThingModel(IotThingModelSaveReqVO createReqVO) { + // 1.1 校验功能标识符在同一产品下是否唯一 + validateIdentifierUnique(null, createReqVO.getProductId(), createReqVO.getIdentifier()); + // 1.2 功能名称在同一产品下是否唯一 + validateNameUnique(createReqVO.getProductId(), createReqVO.getName()); + // 1.3 校验产品状态,发布状态下,不允许新增功能 + validateProductStatus(createReqVO.getProductId()); + + // 2. 插入数据库 + IotThingModelDO thingModel = IotThingModelConvert.INSTANCE.convert(createReqVO); + thingModelMapper.insert(thingModel); + + // 3. 如果创建的是属性,需要更新默认的事件和服务 + if (Objects.equals(createReqVO.getType(), IotThingModelTypeEnum.PROPERTY.getType())) { + createDefaultEventsAndServices(createReqVO.getProductId(), createReqVO.getProductKey()); + } + + // 4. 删除缓存 + deleteThingModelListCache(createReqVO.getProductKey()); + return thingModel.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateThingModel(IotThingModelSaveReqVO updateReqVO) { + // 1.1 校验功能是否存在 + validateProductThingModelMapperExists(updateReqVO.getId()); + // 1.2 校验功能标识符是否唯一 + validateIdentifierUnique(updateReqVO.getId(), updateReqVO.getProductId(), updateReqVO.getIdentifier()); + // 1.3 校验产品状态,发布状态下,不允许操作功能 + validateProductStatus(updateReqVO.getProductId()); + + // 2. 更新数据库 + IotThingModelDO thingModel = IotThingModelConvert.INSTANCE.convert(updateReqVO); + thingModelMapper.updateById(thingModel); + + // 3. 如果更新的是属性,需要更新默认的事件和服务 + if (Objects.equals(updateReqVO.getType(), IotThingModelTypeEnum.PROPERTY.getType())) { + createDefaultEventsAndServices(updateReqVO.getProductId(), updateReqVO.getProductKey()); + } + + // 4. 删除缓存 + deleteThingModelListCache(updateReqVO.getProductKey()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteThingModel(Long id) { + // 1.1 校验功能是否存在 + IotThingModelDO thingModel = thingModelMapper.selectById(id); + if (thingModel == null) { + throw exception(THING_MODEL_NOT_EXISTS); + } + // 1.2 校验产品状态,发布状态下,不允许操作功能 + validateProductStatus(thingModel.getProductId()); + + // 2. 删除功能 + thingModelMapper.deleteById(id); + + // 3. 如果删除的是属性,需要更新默认的事件和服务 + if (Objects.equals(thingModel.getType(), IotThingModelTypeEnum.PROPERTY.getType())) { + createDefaultEventsAndServices(thingModel.getProductId(), thingModel.getProductKey()); + } + + // 4. 删除缓存 + deleteThingModelListCache(thingModel.getProductKey()); + } + + @Override + public IotThingModelDO getThingModel(Long id) { + return thingModelMapper.selectById(id); + } + + @Override + public List getThingModelListByProductId(Long productId) { + return thingModelMapper.selectListByProductId(productId); + } + + @Override + @Cacheable(value = RedisKeyConstants.THING_MODEL_LIST, key = "#productKey") + @TenantIgnore // 忽略租户信息,跨租户 productKey 是唯一的 + public List getThingModelListByProductKeyFromCache(String productKey) { + return thingModelMapper.selectListByProductKey(productKey); + } + + @Override + public PageResult getProductThingModelPage(IotThingModelPageReqVO pageReqVO) { + return thingModelMapper.selectPage(pageReqVO); + } + + @Override + public List getThingModelList(IotThingModelListReqVO reqVO) { + return thingModelMapper.selectList(reqVO); + } + + /** + * 校验功能是否存在 + * + * @param id 功能编号 + */ + private void validateProductThingModelMapperExists(Long id) { + if (thingModelMapper.selectById(id) == null) { + throw exception(THING_MODEL_NOT_EXISTS); + } + } + + private void validateIdentifierUnique(Long id, Long productId, String identifier) { + // 1.0 情况一:创建时校验 + if (id == null) { + // 1.1 系统保留字段,不能用于标识符定义 + if (StrUtil.equalsAny(identifier, "set", "get", "post", "property", "event", "time", "value")) { + throw exception(THING_MODEL_IDENTIFIER_INVALID); + } + + // 1.2 校验唯一 + IotThingModelDO thingModel = thingModelMapper.selectByProductIdAndIdentifier(productId, identifier); + if (thingModel != null) { + throw exception(THING_MODEL_IDENTIFIER_EXISTS); + } + return; + } + + // 2.0 情况二:更新时校验 + IotThingModelDO thingModel = thingModelMapper.selectByProductIdAndIdentifier(productId, identifier); + if (thingModel != null && ObjectUtil.notEqual(thingModel.getId(), id)) { + throw exception(THING_MODEL_IDENTIFIER_EXISTS); + } + } + + private void validateProductStatus(Long createReqVO) { + IotProductDO product = productService.validateProductExists(createReqVO); + if (Objects.equals(product.getStatus(), IotProductStatusEnum.PUBLISHED.getStatus())) { + throw exception(PRODUCT_STATUS_NOT_ALLOW_THING_MODEL); + } + } + + private void validateNameUnique(Long productId, String name) { + IotThingModelDO thingModel = thingModelMapper.selectByProductIdAndName(productId, name); + if (thingModel != null) { + throw exception(THING_MODEL_NAME_EXISTS); + } + } + + /** + * 创建默认的事件和服务 + * + * @param productId 产品编号 + * @param productKey 产品标识 + */ + public void createDefaultEventsAndServices(Long productId, String productKey) { + // 1. 获取当前属性列表 + List properties = thingModelMapper + .selectListByProductIdAndType(productId, IotThingModelTypeEnum.PROPERTY.getType()); + + // 2. 生成新的事件和服务列表 + List newThingModels = new ArrayList<>(); + // 2.1 生成属性上报事件 + ThingModelEvent propertyPostEvent = generatePropertyPostEvent(properties); + if (propertyPostEvent != null) { + newThingModels.add(buildEventThingModel(productId, productKey, propertyPostEvent, "属性上报事件")); + } + // 2.2 生成属性设置服务 + ThingModelService propertySetService = generatePropertySetService(properties); + if (propertySetService != null) { + newThingModels.add(buildServiceThingModel(productId, productKey, propertySetService, "属性设置服务")); + } + // 2.3 生成属性获取服务 + ThingModelService propertyGetService = generatePropertyGetService(properties); + if (propertyGetService != null) { + newThingModels.add(buildServiceThingModel(productId, productKey, propertyGetService, "属性获取服务")); + } + + // 3.1 获取数据库中的默认的旧事件和服务列表 + List oldThingModels = thingModelMapper.selectListByProductIdAndIdentifiersAndTypes( + productId, + Arrays.asList("post", "set", "get"), + Arrays.asList(IotThingModelTypeEnum.EVENT.getType(), IotThingModelTypeEnum.SERVICE.getType()) + ); + // 3.2 创建默认的事件和服务 + createDefaultEventsAndServices(oldThingModels, newThingModels); + } + + /** + * 创建默认的事件和服务 + */ + private void createDefaultEventsAndServices(List oldThingModels, + List newThingModels) { + // 使用 diffList 方法比较新旧列表 + List> diffResult = diffList(oldThingModels, newThingModels, + (oldVal, newVal) -> { + // 继续使用 identifier 和 type 进行比较:这样可以准确地匹配对应的功能对象。 + boolean same = Objects.equals(oldVal.getIdentifier(), newVal.getIdentifier()) + && Objects.equals(oldVal.getType(), newVal.getType()); + if (same) { + newVal.setId(oldVal.getId()); // 设置编号 + } + return same; + }); + // 批量添加、修改、删除 + if (CollUtil.isNotEmpty(diffResult.get(0))) { + thingModelMapper.insertBatch(diffResult.get(0)); + } + if (CollUtil.isNotEmpty(diffResult.get(1))) { + thingModelMapper.updateBatch(diffResult.get(1)); + } + if (CollUtil.isNotEmpty(diffResult.get(2))) { + thingModelMapper.deleteByIds(convertSet(diffResult.get(2), IotThingModelDO::getId)); + } + } + + /** + * 构建事件功能对象 + */ + private IotThingModelDO buildEventThingModel(Long productId, String productKey, + ThingModelEvent event, String description) { + return new IotThingModelDO().setProductId(productId).setProductKey(productKey) + .setIdentifier(event.getIdentifier()).setName(event.getName()).setDescription(description) + .setType(IotThingModelTypeEnum.EVENT.getType()).setEvent(event); + } + + /** + * 构建服务功能对象 + */ + private IotThingModelDO buildServiceThingModel(Long productId, String productKey, + ThingModelService service, String description) { + return new IotThingModelDO().setProductId(productId).setProductKey(productKey) + .setIdentifier(service.getIdentifier()).setName(service.getName()).setDescription(description) + .setType(IotThingModelTypeEnum.SERVICE.getType()).setService(service); + } + + // TODO @haohao:是不是不用生成这个?目前属性上报,是个批量接口 + + /** + * 生成属性上报事件 + */ + private ThingModelEvent generatePropertyPostEvent(List thingModels) { + // 没有属性则不生成 + if (CollUtil.isEmpty(thingModels)) { + return null; + } + + // 生成属性上报事件 + return new ThingModelEvent().setIdentifier("post").setName("属性上报").setMethod("thing.event.property.post") + .setType(IotThingModelServiceEventTypeEnum.INFO.getType()) + .setOutputParams(buildInputOutputParam(thingModels, IotThingModelParamDirectionEnum.OUTPUT)); + } + + // TODO @haohao:是不是不用生成这个?目前属性上报,是个批量接口 + + /** + * 生成属性设置服务 + */ + private ThingModelService generatePropertySetService(List thingModels) { + // 1.1 过滤出所有可写属性 + thingModels = filterList(thingModels, thingModel -> + IotThingModelAccessModeEnum.READ_WRITE.getMode().equals(thingModel.getProperty().getAccessMode())); + // 1.2 没有可写属性则不生成 + if (CollUtil.isEmpty(thingModels)) { + return null; + } + + // 2. 生成属性设置服务 + return new ThingModelService().setIdentifier("set").setName("属性设置").setMethod("thing.service.property.set") + .setCallType(IotThingModelServiceCallTypeEnum.ASYNC.getType()) + .setInputParams(buildInputOutputParam(thingModels, IotThingModelParamDirectionEnum.INPUT)) + .setOutputParams(Collections.emptyList()); // 属性设置服务一般不需要输出参数 + } + + /** + * 生成属性获取服务 + */ + private ThingModelService generatePropertyGetService(List thingModels) { + // 1.1 没有属性则不生成 + if (CollUtil.isEmpty(thingModels)) { + return null; + } + + // 1.2 生成属性获取服务 + return new ThingModelService().setIdentifier("get").setName("属性获取").setMethod("thing.service.property.get") + .setCallType(IotThingModelServiceCallTypeEnum.ASYNC.getType()) + .setInputParams(buildInputOutputParam(thingModels, IotThingModelParamDirectionEnum.INPUT)) + .setOutputParams(buildInputOutputParam(thingModels, IotThingModelParamDirectionEnum.OUTPUT)); + } + + /** + * 构建输入/输出参数列表 + * + * @param thingModels 属性列表 + * @return 输入/输出参数列表 + */ + private List buildInputOutputParam(List thingModels, + IotThingModelParamDirectionEnum direction) { + return convertList(thingModels, thingModel -> + BeanUtils.toBean(thingModel.getProperty(), ThingModelParam.class).setParaOrder(0) // TODO @puhui999: 先搞个默认值看看怎么个事 + .setDirection(direction.getDirection())); + } + + private void deleteThingModelListCache(String productKey) { + // 保证 Spring AOP 触发 + getSelf().deleteThingModelListCache0(productKey); + } + + @CacheEvict(value = RedisKeyConstants.THING_MODEL_LIST, key = "#productKey") + public void deleteThingModelListCache0(String productKey) { + } + + private IotThingModelServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + + // TODO @super:用不到,删除下; + @Override + public Long getThingModelCount(LocalDateTime createTime) { + return thingModelMapper.selectCountByCreateTime(createTime); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thinkmodelfunction/IotThinkModelFunctionService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thinkmodelfunction/IotThinkModelFunctionService.java deleted file mode 100644 index ce8e472f5..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thinkmodelfunction/IotThinkModelFunctionService.java +++ /dev/null @@ -1,65 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.thinkmodelfunction; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo.IotThinkModelFunctionPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo.IotThinkModelFunctionSaveReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.thinkmodelfunction.IotThinkModelFunctionDO; -import jakarta.validation.Valid; - -import java.util.List; - -/** - * IoT 产品物模型 Service 接口 - * - * @author 芋道源码 - */ -public interface IotThinkModelFunctionService { - - /** - * 创建产品物模型 - * - * @param createReqVO 创建信息 - * @return 编号 - */ - Long createThinkModelFunction(@Valid IotThinkModelFunctionSaveReqVO createReqVO); - - - /** - * 更新产品物模型 - * - * @param updateReqVO 更新信息 - */ - void updateThinkModelFunction(@Valid IotThinkModelFunctionSaveReqVO updateReqVO); - - /** - * 删除产品物模型 - * - * @param id 编号 - */ - void deleteThinkModelFunction(Long id); - - /** - * 获得产品物模型 - * - * @param id 编号 - * @return 产品物模型 - */ - IotThinkModelFunctionDO getThinkModelFunction(Long id); - - /** - * 获得产品物模型列表 - * - * @param productId 产品编号 - * @return 产品物模型列表 - */ - List getThinkModelFunctionListByProductId(Long productId); - - /** - * 获得产品物模型分页 - * - * @param pageReqVO 分页查询 - * @return 产品物模型分页 - */ - PageResult getThinkModelFunctionPage(IotThinkModelFunctionPageReqVO pageReqVO); - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thinkmodelfunction/IotThinkModelFunctionServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thinkmodelfunction/IotThinkModelFunctionServiceImpl.java deleted file mode 100644 index 793786944..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thinkmodelfunction/IotThinkModelFunctionServiceImpl.java +++ /dev/null @@ -1,404 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.thinkmodelfunction; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.ObjectUtil; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; -import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelEvent; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelProperty; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelService; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType.ThingModelArgument; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType.ThingModelArraySpecs; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType.ThingModelArrayType; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType.ThingModelTextType; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo.IotThinkModelFunctionPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo.IotThinkModelFunctionSaveReqVO; -import cn.iocoder.yudao.module.iot.convert.thinkmodelfunction.IotThinkModelFunctionConvert; -import cn.iocoder.yudao.module.iot.dal.dataobject.thinkmodelfunction.IotThinkModelFunctionDO; -import cn.iocoder.yudao.module.iot.dal.mysql.thinkmodelfunction.IotThinkModelFunctionMapper; -import cn.iocoder.yudao.module.iot.enums.product.IotAccessModeEnum; -import cn.iocoder.yudao.module.iot.enums.product.IotProductFunctionTypeEnum; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.validation.annotation.Validated; - -import java.util.*; -import java.util.stream.Collectors; - -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList; -import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; - -/** - * IoT 产品物模型 Service 实现类 - * - * @author 芋道源码 - */ -@Service -@Validated -@Slf4j -public class IotThinkModelFunctionServiceImpl implements IotThinkModelFunctionService { - - @Resource - private IotThinkModelFunctionMapper thinkModelFunctionMapper; - - @Override - @Transactional(rollbackFor = Exception.class) - public Long createThinkModelFunction(IotThinkModelFunctionSaveReqVO createReqVO) { - // 1. 校验功能标识符在同一产品下是否唯一 - validateIdentifierUnique(createReqVO.getProductId(), createReqVO.getIdentifier()); - - // 2. 功能名称在同一产品下是否唯一 - validateNameUnique(createReqVO.getProductId(), createReqVO.getName()); - - // 3. 系统保留字段,不能用于标识符定义 - validateNotDefaultEventAndService(createReqVO.getIdentifier()); - - // 3. 插入数据库 - IotThinkModelFunctionDO function = IotThinkModelFunctionConvert.INSTANCE.convert(createReqVO); - thinkModelFunctionMapper.insert(function); - - // 4. 如果创建的是属性,需要更新默认的事件和服务 - if (Objects.equals(createReqVO.getType(), IotProductFunctionTypeEnum.PROPERTY.getType())) { - createDefaultEventsAndServices(createReqVO.getProductId(), createReqVO.getProductKey()); - } - return function.getId(); - } - - private void validateNotDefaultEventAndService(String identifier) { - // set, get, post, property, event, time, value 是系统保留字段,不能用于标识符定义 - if (CollUtil.containsAny(Arrays.asList("set", "get", "post", "property", "event", "time", "value"), Collections.singletonList(identifier))) { - throw exception(THINK_MODEL_FUNCTION_IDENTIFIER_INVALID); - } -// if (CollUtil.containsAny(Arrays.asList("post", "set", "get"), identifier)) { -// throw exception(THINK_MODEL_FUNCTION_IDENTIFIER_INVALID); -// } - } - - private void validateNameUnique(Long productId, String name) { - IotThinkModelFunctionDO function = thinkModelFunctionMapper.selectByProductIdAndName(productId, name); - if (function != null) { - throw exception(THINK_MODEL_FUNCTION_NAME_EXISTS); - } - } - - private void validateIdentifierUnique(Long productId, String identifier) { - IotThinkModelFunctionDO function = thinkModelFunctionMapper.selectByProductIdAndIdentifier(productId, identifier); - if (function != null) { - throw exception(THINK_MODEL_FUNCTION_IDENTIFIER_EXISTS); - } - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void updateThinkModelFunction(IotThinkModelFunctionSaveReqVO updateReqVO) { - // 1. 校验功能是否存在 - validateThinkModelFunctionExists(updateReqVO.getId()); - - // 2. 校验功能标识符是否唯一 - validateIdentifierUniqueForUpdate(updateReqVO.getId(), updateReqVO.getProductId(), updateReqVO.getIdentifier()); - - // 3. 更新数据库 - IotThinkModelFunctionDO thinkModelFunction = IotThinkModelFunctionConvert.INSTANCE.convert(updateReqVO); - thinkModelFunctionMapper.updateById(thinkModelFunction); - - // 4. 如果更新的是属性,需要更新默认的事件和服务 - if (Objects.equals(updateReqVO.getType(), IotProductFunctionTypeEnum.PROPERTY.getType())) { - createDefaultEventsAndServices(updateReqVO.getProductId(), updateReqVO.getProductKey()); - } - } - - private void validateIdentifierUniqueForUpdate(Long id, Long productId, String identifier) { - IotThinkModelFunctionDO function = thinkModelFunctionMapper.selectByProductIdAndIdentifier(productId, identifier); - if (function != null && ObjectUtil.notEqual(function.getId(), id)) { - throw exception(THINK_MODEL_FUNCTION_IDENTIFIER_EXISTS); - } - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void deleteThinkModelFunction(Long id) { - // 1. 校验功能是否存在 - IotThinkModelFunctionDO functionDO = thinkModelFunctionMapper.selectById(id); - if (functionDO == null) { - throw exception(THINK_MODEL_FUNCTION_NOT_EXISTS); - } - - // 2. 删除功能 - thinkModelFunctionMapper.deleteById(id); - - // 3. 如果删除的是属性,需要更新默认的事件和服务 - if (Objects.equals(functionDO.getType(), IotProductFunctionTypeEnum.PROPERTY.getType())) { - createDefaultEventsAndServices(functionDO.getProductId(), functionDO.getProductKey()); - } - } - - /** - * 校验功能是否存在 - * - * @param id 功能编号 - */ - private void validateThinkModelFunctionExists(Long id) { - if (thinkModelFunctionMapper.selectById(id) == null) { - throw exception(THINK_MODEL_FUNCTION_NOT_EXISTS); - } - } - - @Override - public IotThinkModelFunctionDO getThinkModelFunction(Long id) { - return thinkModelFunctionMapper.selectById(id); - } - - @Override - public List getThinkModelFunctionListByProductId(Long productId) { - return thinkModelFunctionMapper.selectListByProductId(productId); - } - - @Override - public PageResult getThinkModelFunctionPage(IotThinkModelFunctionPageReqVO pageReqVO) { - return thinkModelFunctionMapper.selectPage(pageReqVO); - } - - /** - * 创建默认的事件和服务 - */ - public void createDefaultEventsAndServices(Long productId, String productKey) { - // 1. 获取当前属性列表 - List propertyList = thinkModelFunctionMapper - .selectListByProductIdAndType(productId, IotProductFunctionTypeEnum.PROPERTY.getType()); - - // 2. 生成新的事件和服务列表 - List newFunctionList = new ArrayList<>(); - // 生成属性上报事件 - ThingModelEvent propertyPostEvent = generatePropertyPostEvent(propertyList); - if (propertyPostEvent != null) { - IotThinkModelFunctionDO eventFunction = buildEventFunctionDO(productId, productKey, propertyPostEvent); - newFunctionList.add(eventFunction); - } - // 生成属性设置服务 - ThingModelService propertySetService = generatePropertySetService(propertyList); - if (propertySetService != null) { - IotThinkModelFunctionDO setServiceFunction = buildServiceFunctionDO(productId, productKey, propertySetService); - newFunctionList.add(setServiceFunction); - } - // 生成属性获取服务 - ThingModelService propertyGetService = generatePropertyGetService(propertyList); - if (propertyGetService != null) { - IotThinkModelFunctionDO getServiceFunction = buildServiceFunctionDO(productId, productKey, propertyGetService); - newFunctionList.add(getServiceFunction); - } - - // 3. 获取数据库中的默认的旧事件和服务列表 - List oldFunctionList = thinkModelFunctionMapper.selectListByProductIdAndIdentifiersAndTypes( - productId, - Arrays.asList("post", "set", "get"), - Arrays.asList(IotProductFunctionTypeEnum.EVENT.getType(), IotProductFunctionTypeEnum.SERVICE.getType()) - ); - - // 3.1 使用 diffList 方法比较新旧列表 - List> diffResult = diffList(oldFunctionList, newFunctionList, - // 继续使用 identifier 和 type 进行比较:这样可以准确地匹配对应的功能对象。 - (oldFunc, newFunc) -> Objects.equals(oldFunc.getIdentifier(), newFunc.getIdentifier()) - && Objects.equals(oldFunc.getType(), newFunc.getType())); - List createList = diffResult.get(0); // 需要新增的 - List updateList = diffResult.get(1); // 需要更新的 - List deleteList = diffResult.get(2); // 需要删除的 - - // 3.2 批量执行数据库操作 - // 新增数据库中的新事件和服务列表 - if (CollUtil.isNotEmpty(createList)) { - thinkModelFunctionMapper.insertBatch(createList); - } - // 更新数据库中的事件和服务列表 - if (CollUtil.isNotEmpty(updateList)) { - // 首先,为每个需要更新的对象设置其对应的 ID - updateList.forEach(updateFunc -> { - IotThinkModelFunctionDO oldFunc = findFunctionByIdentifierAndType( - oldFunctionList, updateFunc.getIdentifier(), updateFunc.getType()); - if (oldFunc != null) { - updateFunc.setId(oldFunc.getId()); - } - }); - // 过滤掉没有设置 ID 的对象 - List validUpdateList = updateList.stream() - .filter(func -> func.getId() != null) - .collect(Collectors.toList()); - // 执行批量更新 - if (CollUtil.isNotEmpty(validUpdateList)) { - thinkModelFunctionMapper.updateBatch(validUpdateList); - } - } - - // 删除数据库中的旧事件和服务列表 - if (CollUtil.isNotEmpty(deleteList)) { - Set idsToDelete = CollectionUtils.convertSet(deleteList, IotThinkModelFunctionDO::getId); - thinkModelFunctionMapper.deleteByIds(idsToDelete); - } - } - - /** - * 根据标识符和类型查找功能对象 - */ - private IotThinkModelFunctionDO findFunctionByIdentifierAndType(List functionList, - String identifier, Integer type) { - return CollUtil.findOne(functionList, func -> - Objects.equals(func.getIdentifier(), identifier) && Objects.equals(func.getType(), type)); - } - - /** - * 构建事件功能对象 - */ - private IotThinkModelFunctionDO buildEventFunctionDO(Long productId, String productKey, ThingModelEvent event) { - return new IotThinkModelFunctionDO() - .setProductId(productId) - .setProductKey(productKey) - .setIdentifier(event.getIdentifier()) - .setName(event.getName()) - .setDescription(event.getDescription()) - .setType(IotProductFunctionTypeEnum.EVENT.getType()) - .setEvent(event); - } - - /** - * 构建服务功能对象 - */ - private IotThinkModelFunctionDO buildServiceFunctionDO(Long productId, String productKey, ThingModelService service) { - return new IotThinkModelFunctionDO() - .setProductId(productId) - .setProductKey(productKey) - .setIdentifier(service.getIdentifier()) - .setName(service.getName()) - .setDescription(service.getDescription()) - .setType(IotProductFunctionTypeEnum.SERVICE.getType()) - .setService(service); - } - - /** - * 生成属性上报事件 - */ - private ThingModelEvent generatePropertyPostEvent(List propertyList) { - if (CollUtil.isEmpty(propertyList)) { - return null; - } - - ThingModelEvent event = new ThingModelEvent() - .setIdentifier("post") - .setName("属性上报") - .setType("info") - .setDescription("属性上报事件") - .setMethod("thing.event.property.post"); - - // 将属性列表转换为事件的输出参数 - List outputData = new ArrayList<>(); - for (IotThinkModelFunctionDO functionDO : propertyList) { - ThingModelProperty property = functionDO.getProperty(); - ThingModelArgument arg = new ThingModelArgument() - .setIdentifier(property.getIdentifier()) - .setName(property.getName()) - .setDataType(property.getDataType()) - .setDescription(property.getDescription()) - .setDirection("output"); // 设置为输出参数 - outputData.add(arg); - } - event.setOutputData(outputData); - return event; - } - - /** - * 生成属性设置服务 - */ - private ThingModelService generatePropertySetService(List propertyList) { - if (propertyList == null || propertyList.isEmpty()) { - return null; - } - - List inputData = new ArrayList<>(); - for (IotThinkModelFunctionDO functionDO : propertyList) { - ThingModelProperty property = functionDO.getProperty(); - if (IotAccessModeEnum.WRITE.getMode().equals(property.getAccessMode()) || IotAccessModeEnum.READ_WRITE.getMode().equals(property.getAccessMode())) { - ThingModelArgument arg = new ThingModelArgument() - .setIdentifier(property.getIdentifier()) - .setName(property.getName()) - .setDataType(property.getDataType()) - .setDescription(property.getDescription()) - .setDirection("input"); // 设置为输入参数 - inputData.add(arg); - } - } - if (inputData.isEmpty()) { - // 如果没有可写属性,不生成属性设置服务 - return null; - } - - // 属性设置服务一般不需要输出参数 - return new ThingModelService() - .setIdentifier("set") - .setName("属性设置") - .setCallType("async") - .setDescription("属性设置服务") - .setMethod("thing.service.property.set") - .setInputData(inputData) - // 属性设置服务一般不需要输出参数 - .setOutputData(new ArrayList<>()); - } - - /** - * 生成属性获取服务 - */ - private ThingModelService generatePropertyGetService(List propertyList) { - if (propertyList == null || propertyList.isEmpty()) { - return null; - } - - List outputData = new ArrayList<>(); - for (IotThinkModelFunctionDO functionDO : propertyList) { - ThingModelProperty property = functionDO.getProperty(); - if (ObjectUtils.equalsAny(property.getAccessMode(), - IotAccessModeEnum.READ.getMode(), IotAccessModeEnum.READ_WRITE.getMode())) { - ThingModelArgument arg = new ThingModelArgument() - .setIdentifier(property.getIdentifier()) - .setName(property.getName()) - .setDataType(property.getDataType()) - .setDescription(property.getDescription()) - .setDirection("output"); // 设置为输出参数 - outputData.add(arg); - } - } - if (outputData.isEmpty()) { - // 如果没有可读属性,不生成属性获取服务 - return null; - } - - ThingModelService service = new ThingModelService() - .setIdentifier("get") - .setName("属性获取") - .setCallType("async") - .setDescription("属性获取服务") - .setMethod("thing.service.property.get"); - - // 定义输入参数:属性标识符列表 - ThingModelArgument inputArg = new ThingModelArgument() - .setIdentifier("properties") - .setName("属性标识符列表") - .setDescription("需要获取的属性标识符列表") - .setDirection("input"); // 设置为输入参数 - - // 创建数组类型,元素类型为文本类型(字符串) - ThingModelArrayType arrayType = new ThingModelArrayType(); - arrayType.setType("array"); - ThingModelArraySpecs arraySpecs = new ThingModelArraySpecs(); - ThingModelTextType textType = new ThingModelTextType(); - textType.setType("text"); - arraySpecs.setItem(textType); - arrayType.setSpecs(arraySpecs); - inputArg.setDataType(arrayType); - - service.setInputData(Collections.singletonList(inputArg)); - service.setOutputData(outputData); - return service; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/util/MqttSignUtils.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/util/MqttSignUtils.java new file mode 100644 index 000000000..01a6dba93 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/util/MqttSignUtils.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.module.iot.util; + +import cn.hutool.crypto.digest.HMac; +import cn.hutool.crypto.digest.HmacAlgorithm; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.nio.charset.StandardCharsets; + +/** + * MQTT 签名工具类 + * + * 提供静态方法来计算 MQTT 连接参数 + */ +public class MqttSignUtils { + + /** + * 计算 MQTT 连接参数 + * + * @param productKey 产品密钥 + * @param deviceName 设备名称 + * @param deviceSecret 设备密钥 + * @return 包含 clientId, username, password 的结果对象 + */ + public static MqttSignResult calculate(String productKey, String deviceName, String deviceSecret) { + return calculate(productKey, deviceName, deviceSecret, productKey + "." + deviceName); + } + + /** + * 计算 MQTT 连接参数 + * + * @param productKey 产品密钥 + * @param deviceName 设备名称 + * @param deviceSecret 设备密钥 + * @param clientId 客户端 ID + * @return 包含 clientId, username, password 的结果对象 + */ + public static MqttSignResult calculate(String productKey, String deviceName, String deviceSecret, String clientId) { + String username = deviceName + "&" + productKey; + // 构建签名内容 + StringBuilder signContentBuilder = new StringBuilder() + .append("clientId").append(clientId) + .append("deviceName").append(deviceName) + .append("deviceSecret").append(deviceSecret) + .append("productKey").append(productKey); + + // 使用 HMac 计算签名 + byte[] key = deviceSecret.getBytes(StandardCharsets.UTF_8); + String signContent = signContentBuilder.toString(); + HMac mac = new HMac(HmacAlgorithm.HmacSHA256, key); + String password = mac.digestHex(signContent); + + return new MqttSignResult(clientId, username, password); + } + + /** + * MQTT 签名结果类 + */ + @Getter + @AllArgsConstructor + public static class MqttSignResult { + + private final String clientId; + private final String username; + private final String password; + + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceLogMapper.xml b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceLogMapper.xml new file mode 100644 index 000000000..932a9a862 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceLogMapper.xml @@ -0,0 +1,122 @@ + + + + + + CREATE STABLE IF NOT EXISTS device_log ( + ts TIMESTAMP, + id NCHAR(50), + product_key NCHAR(50), + device_name NCHAR(50), + type NCHAR(50), + identifier NCHAR(255), + content NCHAR(1024), + code INT, + report_time TIMESTAMP + ) TAGS ( + device_key NCHAR(50) + ) + + + + + + INSERT INTO device_log_${deviceKey} (ts, id, product_key, device_name, type, identifier, content, code, report_time) + USING device_log + TAGS ('${deviceKey}') + VALUES ( + NOW, + #{id}, + #{productKey}, + #{deviceName}, + #{type}, + #{identifier}, + #{content}, + #{code}, + #{reportTime} + ) + + + + + + + + + + + \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceMapper.xml b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceMapper.xml new file mode 100644 index 000000000..8404729cc --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceMapper.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDevicePropertyMapper.xml b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDevicePropertyMapper.xml new file mode 100644 index 000000000..bdc40e833 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDevicePropertyMapper.xml @@ -0,0 +1,78 @@ + + + + + + + + CREATE STABLE product_property_${productKey} ( + ts TIMESTAMP, + report_time TIMESTAMP, + + ${field.field} ${field.type} + + (${field.length}) + + + ) + TAGS ( + device_key NCHAR(50) + ) + + + + ALTER STABLE product_property_${productKey} + ADD COLUMN ${field.field} ${field.type} + + (${field.length}) + + + + + ALTER STABLE product_property_${productKey} + MODIFY COLUMN ${field.field} ${field.type} + + (${field.length}) + + + + + ALTER STABLE product_property_${productKey} + DROP COLUMN ${field.field} + + + + INSERT INTO device_property_${device.deviceKey} + USING product_property_${device.productKey} + TAGS ('${device.deviceKey}') + (ts, report_time, + + ${@cn.hutool.core.util.StrUtil@toUnderlineCase(key)} + + ) + VALUES + (NOW, #{reportTime}, + + #{value} + + ) + + + + + + + \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecuteTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecuteTest.java new file mode 100644 index 000000000..38586afdd --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecuteTest.java @@ -0,0 +1,154 @@ +package cn.iocoder.yudao.module.iot.service.rule.action.databridge; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.*; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDateTime; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +/** + * {@link IotDataBridgeExecute} 实现类的测试 + * + * @author HUIHUI + */ +@Disabled // 默认禁用,需要手动启用测试 +@Slf4j +public class IotDataBridgeExecuteTest extends BaseMockitoUnitTest { + + private IotDeviceMessage message; + + @Mock + private RestTemplate restTemplate; + + @InjectMocks + private IotHttpDataBridgeExecute httpDataBridgeExecute; + + @BeforeEach + public void setUp() { + // 创建共享的测试消息 + message = IotDeviceMessage.builder().requestId("TEST-001").reportTime(LocalDateTime.now()).tenantId(1L) + .productKey("testProduct").deviceName("testDevice").deviceKey("testDeviceKey") + .type("property").identifier("temperature").data("{\"value\": 60}") + .build(); + + // 配置 RestTemplate mock 返回成功响应 + // TODO @puhui999:这个应该放到 testHttpDataBridge 里 + when(restTemplate.exchange(anyString(), any(HttpMethod.class), any(), any(Class.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + } + + @Test + public void testKafkaMQDataBridge() { + // 1. 创建执行器实例 + IotKafkaMQDataBridgeExecute action = new IotKafkaMQDataBridgeExecute(); + + // 2. 创建配置 + // TODO @puhui999:可以改成链式哈。 + IotDataBridgeKafkaMQConfig config = new IotDataBridgeKafkaMQConfig(); + config.setBootstrapServers("127.0.0.1:9092"); + config.setTopic("test-topic"); + config.setSsl(false); + config.setUsername(null); + config.setPassword(null); + + // 3. 执行两次测试,验证缓存 + log.info("[testKafkaMQDataBridge][第一次执行,应该会创建新的 producer]"); + action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); + + log.info("[testKafkaMQDataBridge][第二次执行,应该会复用缓存的 producer]"); + action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); + } + + @Test + public void testRabbitMQDataBridge() { + // 1. 创建执行器实例 + IotRabbitMQDataBridgeExecute action = new IotRabbitMQDataBridgeExecute(); + + // 2. 创建配置 + IotDataBridgeRabbitMQConfig config = new IotDataBridgeRabbitMQConfig(); + config.setHost("localhost"); + config.setPort(5672); + config.setVirtualHost("/"); + config.setUsername("admin"); + config.setPassword("123456"); + config.setExchange("test-exchange"); + config.setRoutingKey("test-key"); + config.setQueue("test-queue"); + + // 3. 执行两次测试,验证缓存 + log.info("[testRabbitMQDataBridge][第一次执行,应该会创建新的 producer]"); + action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); + + log.info("[testRabbitMQDataBridge][第二次执行,应该会复用缓存的 producer]"); + action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); + } + + @Test + public void testRedisStreamMQDataBridge() { + // 1. 创建执行器实例 + IotRedisStreamMQDataBridgeExecute action = new IotRedisStreamMQDataBridgeExecute(); + + // 2. 创建配置 + IotDataBridgeRedisStreamMQConfig config = new IotDataBridgeRedisStreamMQConfig(); + config.setHost("127.0.0.1"); + config.setPort(6379); + config.setDatabase(0); + config.setPassword("123456"); + config.setTopic("test-stream"); + + // 3. 执行两次测试,验证缓存 + log.info("[testRedisStreamMQDataBridge][第一次执行,应该会创建新的 producer]"); + action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); + + log.info("[testRedisStreamMQDataBridge][第二次执行,应该会复用缓存的 producer]"); + action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); + } + + @Test + public void testRocketMQDataBridge() { + // 1. 创建执行器实例 + IotRocketMQDataBridgeExecute action = new IotRocketMQDataBridgeExecute(); + + // 2. 创建配置 + IotDataBridgeRocketMQConfig config = new IotDataBridgeRocketMQConfig(); + config.setNameServer("127.0.0.1:9876"); + config.setGroup("test-group"); + config.setTopic("test-topic"); + config.setTags("test-tag"); + + // 3. 执行两次测试,验证缓存 + log.info("[testRocketMQDataBridge][第一次执行,应该会创建新的 producer]"); + action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); + + log.info("[testRocketMQDataBridge][第二次执行,应该会复用缓存的 producer]"); + action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); + } + + @Test + public void testHttpDataBridge() throws Exception { + // 创建配置 + IotDataBridgeHttpConfig config = new IotDataBridgeHttpConfig(); + config.setUrl("https://doc.iocoder.cn/"); + config.setMethod(HttpMethod.GET.name()); + + // 执行测试 + log.info("[testHttpDataBridge][执行HTTP数据桥接测试]"); + httpDataBridgeExecute.execute(message, new IotDataBridgeDO().setType(httpDataBridgeExecute.getType()).setConfig(config)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/pom.xml b/yudao-module-iot/yudao-module-iot-plugins/pom.xml new file mode 100644 index 000000000..d33292527 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/pom.xml @@ -0,0 +1,27 @@ + + + + yudao-module-iot + cn.iocoder.boot + ${revision} + + + yudao-module-iot-plugin-common + yudao-module-iot-plugin-http + yudao-module-iot-plugin-mqtt + yudao-module-iot-plugin-emqx + + + 4.0.0 + + yudao-module-iot-plugins + pom + + ${project.artifactId} + + 物联网 插件 模块 + + + \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/pom.xml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/pom.xml new file mode 100644 index 000000000..1e5a69bfa --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/pom.xml @@ -0,0 +1,52 @@ + + + + yudao-module-iot-plugins + cn.iocoder.boot + ${revision} + + 4.0.0 + yudao-module-iot-plugin-common + jar + + ${project.artifactId} + + + 物联网 插件 模块 - 通用功能 + + + + + org.springframework.boot + spring-boot-starter + + + + cn.iocoder.boot + yudao-module-iot-api + ${revision} + + + + + org.springframework + spring-web + + + + + io.vertx + vertx-web + + + + + org.springframework.boot + spring-boot-starter-validation + true + + + + diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonAutoConfiguration.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonAutoConfiguration.java new file mode 100644 index 000000000..ba7d56fe6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonAutoConfiguration.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.iot.plugin.common.config; + +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamServer; +import cn.iocoder.yudao.module.iot.plugin.common.heartbeat.IotPluginInstanceHeartbeatJob; +import cn.iocoder.yudao.module.iot.plugin.common.upstream.IotDeviceUpstreamClient; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.web.client.RestTemplate; + +/** + * IoT 插件的通用自动配置类 + * + * @author haohao + */ +@AutoConfiguration +@EnableConfigurationProperties(IotPluginCommonProperties.class) +@EnableScheduling // 开启定时任务,因为 IotPluginInstanceHeartbeatJob 是一个定时任务 +public class IotPluginCommonAutoConfiguration { + + @Bean + public RestTemplate restTemplate(IotPluginCommonProperties properties) { + return new RestTemplateBuilder() + .connectTimeout(properties.getUpstreamConnectTimeout()) + .readTimeout(properties.getUpstreamReadTimeout()) + .build(); + } + + @Bean + public IotDeviceUpstreamApi deviceUpstreamApi(IotPluginCommonProperties properties, + RestTemplate restTemplate) { + return new IotDeviceUpstreamClient(properties, restTemplate); + } + + @Bean(initMethod = "start", destroyMethod = "stop") + public IotDeviceDownstreamServer deviceDownstreamServer(IotPluginCommonProperties properties, + IotDeviceDownstreamHandler deviceDownstreamHandler) { + return new IotDeviceDownstreamServer(properties, deviceDownstreamHandler); + } + + @Bean(initMethod = "init", destroyMethod = "stop") + public IotPluginInstanceHeartbeatJob pluginInstanceHeartbeatJob(IotDeviceUpstreamApi deviceDataApi, + IotDeviceDownstreamServer deviceDownstreamServer, + IotPluginCommonProperties commonProperties) { + return new IotPluginInstanceHeartbeatJob(deviceDataApi, deviceDownstreamServer, commonProperties); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonProperties.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonProperties.java new file mode 100644 index 000000000..03d42c288 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonProperties.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.iot.plugin.common.config; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import java.time.Duration; + +/** + * IoT 插件的通用配置类 + * + * @author haohao + */ +@ConfigurationProperties(prefix = "yudao.iot.plugin.common") +@Validated +@Data +public class IotPluginCommonProperties { + + /** + * 上行连接超时的默认值 + */ + public static final Duration UPSTREAM_CONNECT_TIMEOUT_DEFAULT = Duration.ofSeconds(30); + /** + * 上行读取超时的默认值 + */ + public static final Duration UPSTREAM_READ_TIMEOUT_DEFAULT = Duration.ofSeconds(30); + + /** + * 下行端口 - 随机 + */ + public static final Integer DOWNSTREAM_PORT_RANDOM = 0; + + /** + * 上行 URL + */ + @NotEmpty(message = "上行 URL 不能为空") + private String upstreamUrl; + /** + * 上行连接超时 + */ + private Duration upstreamConnectTimeout = UPSTREAM_CONNECT_TIMEOUT_DEFAULT; + /** + * 上行读取超时 + */ + private Duration upstreamReadTimeout = UPSTREAM_READ_TIMEOUT_DEFAULT; + + /** + * 下行端口 + */ + private Integer downstreamPort = DOWNSTREAM_PORT_RANDOM; + + /** + * 插件包标识符 + */ + @NotEmpty(message = "插件包标识符不能为空") + private String pluginKey; + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/IotDeviceDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/IotDeviceDownstreamHandler.java new file mode 100644 index 000000000..38aba3df6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/IotDeviceDownstreamHandler.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.iot.plugin.common.downstream; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*; + +/** + * IoT 设备下行处理器 + * + * 目的:每个 plugin 需要实现,用于处理 server 下行的指令(请求),从而实现从 server => plugin => device 的下行流程 + * + * @author 芋道源码 + */ +public interface IotDeviceDownstreamHandler { + + /** + * 调用设备服务 + * + * @param invokeReqDTO 调用设备服务的请求 + * @return 是否成功 + */ + CommonResult invokeDeviceService(IotDeviceServiceInvokeReqDTO invokeReqDTO); + + /** + * 获取设备属性 + * + * @param getReqDTO 获取设备属性的请求 + * @return 是否成功 + */ + CommonResult getDeviceProperty(IotDevicePropertyGetReqDTO getReqDTO); + + /** + * 设置设备属性 + * + * @param setReqDTO 设置设备属性的请求 + * @return 是否成功 + */ + CommonResult setDeviceProperty(IotDevicePropertySetReqDTO setReqDTO); + + /** + * 设置设备配置 + * + * @param setReqDTO 设置设备配置的请求 + * @return 是否成功 + */ + CommonResult setDeviceConfig(IotDeviceConfigSetReqDTO setReqDTO); + + /** + * 升级设备 OTA + * + * @param upgradeReqDTO 升级设备 OTA 的请求 + * @return 是否成功 + */ + CommonResult upgradeDeviceOta(IotDeviceOtaUpgradeReqDTO upgradeReqDTO); + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/IotDeviceDownstreamServer.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/IotDeviceDownstreamServer.java new file mode 100644 index 000000000..719fdb5c3 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/IotDeviceDownstreamServer.java @@ -0,0 +1,94 @@ +package cn.iocoder.yudao.module.iot.plugin.common.downstream; + +import cn.iocoder.yudao.module.iot.plugin.common.config.IotPluginCommonProperties; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.router.*; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.BodyHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 设备下行服务端,接收来自 server 服务器的请求,转发给 device 设备 + * + * @author 芋道源码 + */ +@Slf4j +public class IotDeviceDownstreamServer { + + private final Vertx vertx; + private final HttpServer server; + private final IotPluginCommonProperties properties; + + public IotDeviceDownstreamServer(IotPluginCommonProperties properties, + IotDeviceDownstreamHandler deviceDownstreamHandler) { + this.properties = properties; + // 创建 Vertx 实例 + this.vertx = Vertx.vertx(); + // 创建 Router 实例 + Router router = Router.router(vertx); + router.route().handler(BodyHandler.create()); // 处理 Body + router.post(IotDeviceServiceInvokeVertxHandler.PATH) + .handler(new IotDeviceServiceInvokeVertxHandler(deviceDownstreamHandler)); + router.post(IotDevicePropertySetVertxHandler.PATH) + .handler(new IotDevicePropertySetVertxHandler(deviceDownstreamHandler)); + router.post(IotDevicePropertyGetVertxHandler.PATH) + .handler(new IotDevicePropertyGetVertxHandler(deviceDownstreamHandler)); + router.post(IotDeviceConfigSetVertxHandler.PATH) + .handler(new IotDeviceConfigSetVertxHandler(deviceDownstreamHandler)); + router.post(IotDeviceOtaUpgradeVertxHandler.PATH) + .handler(new IotDeviceOtaUpgradeVertxHandler(deviceDownstreamHandler)); + // 创建 HttpServer 实例 + this.server = vertx.createHttpServer().requestHandler(router); + } + + /** + * 启动 HTTP 服务器 + */ + public void start() { + log.info("[start][开始启动]"); + server.listen(properties.getDownstreamPort()) + .toCompletionStage() + .toCompletableFuture() + .join(); + log.info("[start][启动完成,端口({})]", this.server.actualPort()); + } + + /** + * 停止所有 + */ + public void stop() { + log.info("[stop][开始关闭]"); + try { + // 关闭 HTTP 服务器 + if (server != null) { + server.close() + .toCompletionStage() + .toCompletableFuture() + .join(); + } + + // 关闭 Vertx 实例 + if (vertx != null) { + vertx.close() + .toCompletionStage() + .toCompletableFuture() + .join(); + } + log.info("[stop][关闭完成]"); + } catch (Exception e) { + log.error("[stop][关闭异常]", e); + throw new RuntimeException(e); + } + } + + /** + * 获得端口 + * + * @return 端口 + */ + public int getPort() { + return this.server.actualPort(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceConfigSetVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceConfigSetVertxHandler.java new file mode 100644 index 000000000..1693f128d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceConfigSetVertxHandler.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.iot.plugin.common.downstream.router; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDeviceConfigSetReqDTO; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; +import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; + +/** + * IoT 设备配置设置 Vertx Handler + * + * 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class IotDeviceConfigSetVertxHandler implements Handler { + + // TODO @haohao:是不是可以把 PATH、Method 所有的,抽到一个枚举类里?因为 topic、path、method 相当于不同的几个表达? + public static final String PATH = "/sys/:productKey/:deviceName/thing/service/config/set"; + public static final String METHOD = "thing.service.config.set"; + + private final IotDeviceDownstreamHandler deviceDownstreamHandler; + + @Override + @SuppressWarnings("unchecked") + public void handle(RoutingContext routingContext) { + // 1. 解析参数 + IotDeviceConfigSetReqDTO reqDTO; + try { + String productKey = routingContext.pathParam("productKey"); + String deviceName = routingContext.pathParam("deviceName"); + JsonObject body = routingContext.body().asJsonObject(); + String requestId = body.getString("requestId"); + Map config = (Map) body.getMap().get("config"); + reqDTO = ((IotDeviceConfigSetReqDTO) new IotDeviceConfigSetReqDTO() + .setRequestId(requestId).setProductKey(productKey).setDeviceName(deviceName)) + .setConfig(config); + } catch (Exception e) { + log.error("[handle][路径参数({}) 解析参数失败]", routingContext.pathParams(), e); + IotStandardResponse errorResponse = IotStandardResponse.error( + null, METHOD, BAD_REQUEST.getCode(), BAD_REQUEST.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + return; + } + + // 2. 调用处理器 + try { + CommonResult result = deviceDownstreamHandler.setDeviceConfig(reqDTO); + + // 3. 响应结果 + IotStandardResponse response = result.isSuccess() ? + IotStandardResponse.success(reqDTO.getRequestId(), METHOD, result.getData()) + : IotStandardResponse.error(reqDTO.getRequestId(), METHOD, result.getCode(), result.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, response); + } catch (Exception e) { + log.error("[handle][请求参数({}) 配置设置异常]", reqDTO, e); + IotStandardResponse errorResponse = IotStandardResponse.error( + reqDTO.getRequestId(), METHOD, INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceOtaUpgradeVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceOtaUpgradeVertxHandler.java new file mode 100644 index 000000000..b417229aa --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceOtaUpgradeVertxHandler.java @@ -0,0 +1,78 @@ +package cn.iocoder.yudao.module.iot.plugin.common.downstream.router; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDeviceOtaUpgradeReqDTO; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; +import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; + +/** + * IoT 设备 OTA 升级 Vertx Handler + *

+ * 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class IotDeviceOtaUpgradeVertxHandler implements Handler { + + public static final String PATH = "/ota/:productKey/:deviceName/upgrade"; + public static final String METHOD = "ota.device.upgrade"; + + private final IotDeviceDownstreamHandler deviceDownstreamHandler; + + @Override + public void handle(RoutingContext routingContext) { + // 1. 解析参数 + IotDeviceOtaUpgradeReqDTO reqDTO; + try { + String productKey = routingContext.pathParam("productKey"); + String deviceName = routingContext.pathParam("deviceName"); + JsonObject body = routingContext.body().asJsonObject(); + String requestId = body.getString("requestId"); + Long firmwareId = body.getLong("firmwareId"); + String version = body.getString("version"); + String signMethod = body.getString("signMethod"); + String fileSign = body.getString("fileSign"); + Long fileSize = body.getLong("fileSize"); + String fileUrl = body.getString("fileUrl"); + String information = body.getString("information"); + reqDTO = ((IotDeviceOtaUpgradeReqDTO) new IotDeviceOtaUpgradeReqDTO() + .setRequestId(requestId).setProductKey(productKey).setDeviceName(deviceName)) + .setFirmwareId(firmwareId).setVersion(version) + .setSignMethod(signMethod).setFileSign(fileSign).setFileSize(fileSize).setFileUrl(fileUrl) + .setInformation(information); + } catch (Exception e) { + log.error("[handle][路径参数({}) 解析参数失败]", routingContext.pathParams(), e); + IotStandardResponse errorResponse = IotStandardResponse.error( + null, METHOD, BAD_REQUEST.getCode(), BAD_REQUEST.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + return; + } + + // 2. 调用处理器 + try { + CommonResult result = deviceDownstreamHandler.upgradeDeviceOta(reqDTO); + + // 3. 响应结果 + // TODO @haohao:可以考虑 IotStandardResponse.of(requestId, method, CommonResult) + IotStandardResponse response = result.isSuccess() ? + IotStandardResponse.success(reqDTO.getRequestId(), METHOD, result.getData()) + :IotStandardResponse.error(reqDTO.getRequestId(), METHOD, result.getCode(), result.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, response); + } catch (Exception e) { + log.error("[handle][请求参数({}) OTA 升级异常]", reqDTO, e); + // TODO @haohao:可以考虑 IotStandardResponse.of(requestId, method, ErrorCode) + IotStandardResponse errorResponse = IotStandardResponse.error( + reqDTO.getRequestId(), METHOD, INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + } + } +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertyGetVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertyGetVertxHandler.java new file mode 100644 index 000000000..3cb4bc941 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertyGetVertxHandler.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.iot.plugin.common.downstream.router; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDevicePropertyGetReqDTO; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; +import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; + +/** + * IoT 设备服务获取 Vertx Handler + * + * 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class IotDevicePropertyGetVertxHandler implements Handler { + + public static final String PATH = "/sys/:productKey/:deviceName/thing/service/property/get"; + public static final String METHOD = "thing.service.property.get"; + + private final IotDeviceDownstreamHandler deviceDownstreamHandler; + + @Override + @SuppressWarnings("unchecked") + public void handle(RoutingContext routingContext) { + // 1. 解析参数 + IotDevicePropertyGetReqDTO reqDTO; + try { + String productKey = routingContext.pathParam("productKey"); + String deviceName = routingContext.pathParam("deviceName"); + JsonObject body = routingContext.body().asJsonObject(); + String requestId = body.getString("requestId"); + List identifiers = (List) body.getMap().get("identifiers"); + reqDTO = ((IotDevicePropertyGetReqDTO) new IotDevicePropertyGetReqDTO() + .setRequestId(requestId).setProductKey(productKey).setDeviceName(deviceName)) + .setIdentifiers(identifiers); + } catch (Exception e) { + log.error("[handle][路径参数({}) 解析参数失败]", routingContext.pathParams(), e); + IotStandardResponse errorResponse = IotStandardResponse.error( + null, METHOD, BAD_REQUEST.getCode(), BAD_REQUEST.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + return; + } + + // 2. 调用处理器 + try { + CommonResult result = deviceDownstreamHandler.getDeviceProperty(reqDTO); + + // 3. 响应结果 + IotStandardResponse response; + if (result.isSuccess()) { + response = IotStandardResponse.success(reqDTO.getRequestId(), METHOD, result.getData()); + } else { + response = IotStandardResponse.error(reqDTO.getRequestId(), METHOD, result.getCode(), result.getMsg()); + } + IotPluginCommonUtils.writeJsonResponse(routingContext, response); + } catch (Exception e) { + log.error("[handle][请求参数({}) 属性获取异常]", reqDTO, e); + IotStandardResponse errorResponse = IotStandardResponse.error( + reqDTO.getRequestId(), METHOD, INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertySetVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertySetVertxHandler.java new file mode 100644 index 000000000..251be1eb9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertySetVertxHandler.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.iot.plugin.common.downstream.router; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDevicePropertySetReqDTO; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; +import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; + +/** + * IoT 设置设备属性 Vertx Handler + * + * 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class IotDevicePropertySetVertxHandler implements Handler { + + public static final String PATH = "/sys/:productKey/:deviceName/thing/service/property/set"; + public static final String METHOD = "thing.service.property.set"; + + private final IotDeviceDownstreamHandler deviceDownstreamHandler; + + @Override + @SuppressWarnings("unchecked") + public void handle(RoutingContext routingContext) { + // 1. 解析参数 + IotDevicePropertySetReqDTO reqDTO; + try { + String productKey = routingContext.pathParam("productKey"); + String deviceName = routingContext.pathParam("deviceName"); + JsonObject body = routingContext.body().asJsonObject(); + String requestId = body.getString("requestId"); + Map properties = (Map) body.getMap().get("properties"); + reqDTO = ((IotDevicePropertySetReqDTO) new IotDevicePropertySetReqDTO() + .setRequestId(requestId).setProductKey(productKey).setDeviceName(deviceName)) + .setProperties(properties); + } catch (Exception e) { + log.error("[handle][路径参数({}) 解析参数失败]", routingContext.pathParams(), e); + IotStandardResponse errorResponse = IotStandardResponse.error( + null, METHOD, BAD_REQUEST.getCode(), BAD_REQUEST.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + return; + } + + // 2. 调用处理器 + try { + CommonResult result = deviceDownstreamHandler.setDeviceProperty(reqDTO); + + // 3. 响应结果 + IotStandardResponse response; + if (result.isSuccess()) { + response = IotStandardResponse.success(reqDTO.getRequestId(), METHOD, result.getData()); + } else { + response = IotStandardResponse.error(reqDTO.getRequestId(), METHOD, result.getCode(), result.getMsg()); + } + IotPluginCommonUtils.writeJsonResponse(routingContext, response); + } catch (Exception e) { + log.error("[handle][请求参数({}) 属性设置异常]", reqDTO, e); + IotStandardResponse errorResponse = IotStandardResponse.error( + reqDTO.getRequestId(), METHOD, INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceServiceInvokeVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceServiceInvokeVertxHandler.java new file mode 100644 index 000000000..534823f75 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceServiceInvokeVertxHandler.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.iot.plugin.common.downstream.router; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDeviceServiceInvokeReqDTO; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; +import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; + +/** + * IoT 设备服务调用 Vertx Handler + * + * 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class IotDeviceServiceInvokeVertxHandler implements Handler { + + public static final String PATH = "/sys/:productKey/:deviceName/thing/service/:identifier"; + public static final String METHOD_PREFIX = "thing.service."; + public static final String METHOD_SUFFIX = ""; + + private final IotDeviceDownstreamHandler deviceDownstreamHandler; + + @Override + @SuppressWarnings("unchecked") + public void handle(RoutingContext routingContext) { + // 1. 解析参数 + IotDeviceServiceInvokeReqDTO reqDTO; + try { + String productKey = routingContext.pathParam("productKey"); + String deviceName = routingContext.pathParam("deviceName"); + String identifier = routingContext.pathParam("identifier"); + JsonObject body = routingContext.body().asJsonObject(); + String requestId = body.getString("requestId"); + Map params = (Map) body.getMap().get("params"); + reqDTO = ((IotDeviceServiceInvokeReqDTO) new IotDeviceServiceInvokeReqDTO() + .setRequestId(requestId).setProductKey(productKey).setDeviceName(deviceName)) + .setIdentifier(identifier).setParams(params); + } catch (Exception e) { + log.error("[handle][路径参数({}) 解析参数失败]", routingContext.pathParams(), e); + String method = METHOD_PREFIX + routingContext.pathParam("identifier") + METHOD_SUFFIX; + IotStandardResponse errorResponse = IotStandardResponse.error( + null, method, BAD_REQUEST.getCode(), BAD_REQUEST.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + return; + } + + // 2. 调用处理器 + try { + CommonResult result = deviceDownstreamHandler.invokeDeviceService(reqDTO); + + // 3. 响应结果 + String method = METHOD_PREFIX + reqDTO.getIdentifier() + METHOD_SUFFIX; + IotStandardResponse response; + if (result.isSuccess()) { + response = IotStandardResponse.success(reqDTO.getRequestId(), method, result.getData()); + } else { + response = IotStandardResponse.error(reqDTO.getRequestId(), method, result.getCode(), result.getMsg()); + } + IotPluginCommonUtils.writeJsonResponse(routingContext, response); + } catch (Exception e) { + log.error("[handle][请求参数({}) 服务调用异常]", reqDTO, e); + String method = METHOD_PREFIX + reqDTO.getIdentifier() + METHOD_SUFFIX; + IotStandardResponse errorResponse = IotStandardResponse.error( + reqDTO.getRequestId(), method, INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/heartbeat/IotPluginInstanceHeartbeatJob.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/heartbeat/IotPluginInstanceHeartbeatJob.java new file mode 100644 index 000000000..f272468c5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/heartbeat/IotPluginInstanceHeartbeatJob.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.iot.plugin.common.heartbeat; + +import cn.hutool.system.SystemUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO; +import cn.iocoder.yudao.module.iot.plugin.common.config.IotPluginCommonProperties; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamServer; +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; + +import java.util.concurrent.TimeUnit; + +/** + * IoT 插件实例心跳 Job + * + * 用于定时发送心跳给服务端 + */ +@RequiredArgsConstructor +@Slf4j +public class IotPluginInstanceHeartbeatJob { + + private final IotDeviceUpstreamApi deviceUpstreamApi; + private final IotDeviceDownstreamServer deviceDownstreamServer; + private final IotPluginCommonProperties commonProperties; + + public void init() { + CommonResult result = deviceUpstreamApi.heartbeatPluginInstance(buildPluginInstanceHeartbeatReqDTO(true)); + log.info("[init][上线结果:{})]", result); + } + + public void stop() { + CommonResult result = deviceUpstreamApi.heartbeatPluginInstance(buildPluginInstanceHeartbeatReqDTO(false)); + log.info("[stop][下线结果:{})]", result); + } + + @Scheduled(initialDelay = 3, fixedRate = 3, timeUnit = TimeUnit.MINUTES) // 3 分钟执行一次 + public void execute() { + CommonResult result = deviceUpstreamApi.heartbeatPluginInstance(buildPluginInstanceHeartbeatReqDTO(true)); + log.info("[execute][心跳结果:{})]", result); + } + + private IotPluginInstanceHeartbeatReqDTO buildPluginInstanceHeartbeatReqDTO(Boolean online) { + return new IotPluginInstanceHeartbeatReqDTO() + .setPluginKey(commonProperties.getPluginKey()).setProcessId(IotPluginCommonUtils.getProcessId()) + .setHostIp(SystemUtil.getHostInfo().getAddress()).setDownstreamPort(deviceDownstreamServer.getPort()) + .setOnline(online); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/package-info.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/package-info.java new file mode 100644 index 000000000..83b5bb58a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/package-info.java @@ -0,0 +1,2 @@ +// TODO @芋艿:注释 +package cn.iocoder.yudao.module.iot.plugin.common; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/pojo/IotStandardResponse.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/pojo/IotStandardResponse.java new file mode 100644 index 000000000..131eb1b9c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/pojo/IotStandardResponse.java @@ -0,0 +1,94 @@ +package cn.iocoder.yudao.module.iot.plugin.common.pojo; + +import lombok.Data; + +// TODO @芋艿:1)后续考虑,要不要叫 IoT 网关之类的 Response;2)包名 pojo +/** + * IoT 标准协议响应实体类 + *

+ * 用于统一 MQTT 和 HTTP 的响应格式 + * + * @author haohao + */ +@Data +public class IotStandardResponse { + + /** + * 消息ID + */ + private String id; + + /** + * 状态码 + */ + private Integer code; + + /** + * 响应数据 + */ + private Object data; + + /** + * 响应消息 + */ + private String message; + + /** + * 方法名 + */ + private String method; + + /** + * 协议版本 + */ + private String version; + + /** + * 创建成功响应 + * + * @param id 消息ID + * @param method 方法名 + * @return 成功响应 + */ + public static IotStandardResponse success(String id, String method) { + return success(id, method, null); + } + + /** + * 创建成功响应 + * + * @param id 消息ID + * @param method 方法名 + * @param data 响应数据 + * @return 成功响应 + */ + public static IotStandardResponse success(String id, String method, Object data) { + return new IotStandardResponse() + .setId(id) + .setCode(200) + .setData(data) + .setMessage("success") + .setMethod(method) + .setVersion("1.0"); + } + + /** + * 创建错误响应 + * + * @param id 消息ID + * @param method 方法名 + * @param code 错误码 + * @param message 错误消息 + * @return 错误响应 + */ + public static IotStandardResponse error(String id, String method, Integer code, String message) { + return new IotStandardResponse() + .setId(id) + .setCode(code) + .setData(null) + .setMessage(message) + .setMethod(method) + .setVersion("1.0"); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/upstream/IotDeviceUpstreamClient.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/upstream/IotDeviceUpstreamClient.java new file mode 100644 index 000000000..1bf4d676c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/upstream/IotDeviceUpstreamClient.java @@ -0,0 +1,91 @@ +package cn.iocoder.yudao.module.iot.plugin.common.upstream; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; +import cn.iocoder.yudao.module.iot.plugin.common.config.IotPluginCommonProperties; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.client.RestTemplate; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; + +/** + * 设备数据 Upstream 上行客户端 + * + * 通过 HTTP 调用远程的 IotDeviceUpstreamApi 接口 + * + * @author haohao + */ +@RequiredArgsConstructor +@Slf4j +public class IotDeviceUpstreamClient implements IotDeviceUpstreamApi { + + public static final String URL_PREFIX = "/rpc-api/iot/device/upstream"; + + private final IotPluginCommonProperties properties; + + private final RestTemplate restTemplate; + + @Override + public CommonResult updateDeviceState(IotDeviceStateUpdateReqDTO updateReqDTO) { + String url = properties.getUpstreamUrl() + URL_PREFIX + "/update-state"; + return doPost(url, updateReqDTO); + } + + @Override + public CommonResult reportDeviceEvent(IotDeviceEventReportReqDTO reportReqDTO) { + String url = properties.getUpstreamUrl() + URL_PREFIX + "/report-event"; + return doPost(url, reportReqDTO); + } + + // TODO @芋艿:待实现 + @Override + public CommonResult registerDevice(IotDeviceRegisterReqDTO registerReqDTO) { + return null; + } + + // TODO @芋艿:待实现 + @Override + public CommonResult registerSubDevice(IotDeviceRegisterSubReqDTO registerReqDTO) { + return null; + } + + // TODO @芋艿:待实现 + @Override + public CommonResult addDeviceTopology(IotDeviceTopologyAddReqDTO addReqDTO) { + return null; + } + + @Override + public CommonResult authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) { + String url = properties.getUpstreamUrl() + URL_PREFIX + "/authenticate-emqx-connection"; + return doPost(url, authReqDTO); + } + + @Override + public CommonResult reportDeviceProperty(IotDevicePropertyReportReqDTO reportReqDTO) { + String url = properties.getUpstreamUrl() + URL_PREFIX + "/report-property"; + return doPost(url, reportReqDTO); + } + + @Override + public CommonResult heartbeatPluginInstance(IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO) { + String url = properties.getUpstreamUrl() + URL_PREFIX + "/heartbeat-plugin-instance"; + return doPost(url, heartbeatReqDTO); + } + + @SuppressWarnings("unchecked") + private CommonResult doPost(String url, T requestBody) { + try { + CommonResult result = restTemplate.postForObject(url, requestBody, + (Class>) (Class) CommonResult.class); + log.info("[doPost][url({}) requestBody({}) result({})]", url, requestBody, result); + return result; + } catch (Exception e) { + log.error("[doPost][url({}) requestBody({}) 发生异常]", url, requestBody, e); + return CommonResult.error(INTERNAL_SERVER_ERROR); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/util/IotPluginCommonUtils.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/util/IotPluginCommonUtils.java new file mode 100644 index 000000000..34c6c0fe2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/util/IotPluginCommonUtils.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.module.iot.plugin.common.util; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.system.SystemUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; +import io.vertx.core.http.HttpHeaders; +import io.vertx.ext.web.RoutingContext; +import org.springframework.http.MediaType; + +/** + * IoT 插件的通用工具类 + * + * @author 芋道源码 + */ +public class IotPluginCommonUtils { + + /** + * 流程实例的进程编号 + */ + private static String processId; + + public static String getProcessId() { + if (StrUtil.isEmpty(processId)) { + initProcessId(); + } + return processId; + } + + private synchronized static void initProcessId() { + processId = String.format("%s@%d@%s", // IP@PID@${uuid} + SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID(), IdUtil.fastSimpleUUID()); + } + + /** + * 将对象转换为JSON字符串后写入HTTP响应 + * + * @param routingContext 路由上下文 + * @param data 数据对象 + */ + @SuppressWarnings("deprecation") + public static void writeJsonResponse(RoutingContext routingContext, Object data) { + routingContext.response() + .setStatusCode(200) + .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE) + .end(JsonUtils.toJsonString(data)); + } + + /** + * 生成标准JSON格式的响应并写入HTTP响应(基于IotStandardResponse) + *

+ * 推荐使用此方法,统一MQTT和HTTP的响应格式。使用方式: + * + *

+     * // 成功响应
+     * IotStandardResponse response = IotStandardResponse.success(requestId, method, data);
+     * IotPluginCommonUtils.writeJsonResponse(routingContext, response);
+     *
+     * // 错误响应
+     * IotStandardResponse errorResponse = IotStandardResponse.error(requestId, method, code, message);
+     * IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse);
+     * 
+ * + * @param routingContext 路由上下文 + * @param response IotStandardResponse响应对象 + */ + @SuppressWarnings("deprecation") + public static void writeJsonResponse(RoutingContext routingContext, IotStandardResponse response) { + routingContext.response() + .setStatusCode(200) + .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE) + .end(JsonUtils.toJsonString(response)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..eae9ad882 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.iocoder.yudao.module.iot.plugin.common.config.IotPluginCommonAutoConfiguration \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/plugin.properties b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/plugin.properties new file mode 100644 index 000000000..565e81eb0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/plugin.properties @@ -0,0 +1,6 @@ +plugin.id=yudao-module-iot-plugin-emqx +plugin.class=cn.iocoder.yudao.module.iot.plugin.emqx.config.IotEmqxPlugin +plugin.version=1.0.0 +plugin.provider=yudao +plugin.dependencies= +plugin.description=yudao-module-iot-plugin-emqx-1.0.0 diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/pom.xml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/pom.xml new file mode 100644 index 000000000..8620ecaa6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/pom.xml @@ -0,0 +1,169 @@ + + + + yudao-module-iot-plugins + cn.iocoder.boot + ${revision} + + 4.0.0 + jar + + yudao-module-iot-plugin-emqx + 1.0.0 + + ${project.artifactId} + + + 物联网 插件模块 - emqx 插件 + + + + + emqx-plugin + cn.iocoder.yudao.module.iot.plugin.emqx.config.IotEmqxPlugin + ${project.version} + yudao + ${project.artifactId}-${project.version} + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.6 + + + unzip jar file + package + + + + + + + run + + + + + + + maven-assembly-plugin + 2.3 + + + + src/main/assembly/assembly.xml + + + false + + + + make-assembly + package + + attached + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + ${plugin.id} + ${plugin.class} + ${plugin.version} + ${plugin.provider} + ${plugin.description} + ${plugin.dependencies} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + + repackage + + + -standalone + + + + + + + + + + + cn.iocoder.boot + yudao-module-iot-plugin-common + ${revision} + + + + + org.springframework.boot + spring-boot-starter-web + + + + + io.vertx + vertx-web + + + io.vertx + vertx-mqtt + + + \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/assembly/assembly.xml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/assembly/assembly.xml new file mode 100644 index 000000000..daec9e431 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/assembly/assembly.xml @@ -0,0 +1,31 @@ + + plugin + + zip + + false + + + false + runtime + lib + + *:jar:* + + + + + + + target/plugin-classes + classes + + + diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/IotEmqxPluginApplication.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/IotEmqxPluginApplication.java new file mode 100644 index 000000000..178038417 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/IotEmqxPluginApplication.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.iot.plugin.emqx; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * IoT Emqx 插件的独立运行入口 + */ +@Slf4j +@SpringBootApplication +public class IotEmqxPluginApplication { + + public static void main(String[] args) { + SpringApplication application = new SpringApplication(IotEmqxPluginApplication.class); + application.setWebApplicationType(WebApplicationType.NONE); + application.run(args); + log.info("[main][独立模式启动完成]"); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotEmqxPlugin.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotEmqxPlugin.java new file mode 100644 index 000000000..275c20eb1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotEmqxPlugin.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.iot.plugin.emqx.config; + +import cn.hutool.extra.spring.SpringUtil; +import lombok.extern.slf4j.Slf4j; +import org.pf4j.PluginWrapper; +import org.pf4j.spring.SpringPlugin; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +/** + * EMQX 插件实现类 + * + * 基于 PF4J 插件框架,实现 EMQX 消息中间件的集成:负责插件的生命周期管理,包括启动、停止和应用上下文的创建 + * + * @author haohao + */ +@Slf4j +public class IotEmqxPlugin extends SpringPlugin { + + public IotEmqxPlugin(PluginWrapper wrapper) { + super(wrapper); + } + + @Override + public void start() { + log.info("[EmqxPlugin][EmqxPlugin 插件启动开始...]"); + try { + log.info("[EmqxPlugin][EmqxPlugin 插件启动成功...]"); + } catch (Exception e) { + log.error("[EmqxPlugin][EmqxPlugin 插件开启动异常...]", e); + } + } + + @Override + public void stop() { + log.info("[EmqxPlugin][EmqxPlugin 插件停止开始...]"); + try { + log.info("[EmqxPlugin][EmqxPlugin 插件停止成功...]"); + } catch (Exception e) { + log.error("[EmqxPlugin][EmqxPlugin 插件停止异常...]", e); + } + } + + @Override + protected ApplicationContext createApplicationContext() { + // 创建插件自己的 ApplicationContext + AnnotationConfigApplicationContext pluginContext = new AnnotationConfigApplicationContext(); + // 设置父容器为主应用的 ApplicationContext (确保主应用中提供的类可用) + pluginContext.setParent(SpringUtil.getApplicationContext()); + // 继续使用插件自己的 ClassLoader 以加载插件内部的类 + pluginContext.setClassLoader(getWrapper().getPluginClassLoader()); + // 扫描当前插件的自动配置包 + // TODO @芋艿:是不是要配置下包 + pluginContext.scan("cn.iocoder.yudao.module.iot.plugin.emqx.config"); + pluginContext.refresh(); + return pluginContext; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxAutoConfiguration.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxAutoConfiguration.java new file mode 100644 index 000000000..e1d11504c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxAutoConfiguration.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.iot.plugin.emqx.config; + +import cn.hutool.core.util.IdUtil; +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; +import cn.iocoder.yudao.module.iot.plugin.emqx.downstream.IotDeviceDownstreamHandlerImpl; +import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.IotDeviceUpstreamServer; +import io.vertx.core.Vertx; +import io.vertx.mqtt.MqttClient; +import io.vertx.mqtt.MqttClientOptions; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * IoT 插件 EMQX 的专用自动配置类 + * + * @author haohao + */ +@Slf4j +@Configuration +@EnableConfigurationProperties(IotPluginEmqxProperties.class) +public class IotPluginEmqxAutoConfiguration { + + @Bean + public Vertx vertx() { + return Vertx.vertx(); + } + + @Bean + public MqttClient mqttClient(Vertx vertx, IotPluginEmqxProperties emqxProperties) { + MqttClientOptions options = new MqttClientOptions() + .setClientId("yudao-iot-downstream-" + IdUtil.fastSimpleUUID()) + .setUsername(emqxProperties.getMqttUsername()) + .setPassword(emqxProperties.getMqttPassword()) + .setSsl(emqxProperties.getMqttSsl()); + return MqttClient.create(vertx, options); + } + + @Bean(initMethod = "start", destroyMethod = "stop") + public IotDeviceUpstreamServer deviceUpstreamServer(IotDeviceUpstreamApi deviceUpstreamApi, + IotPluginEmqxProperties emqxProperties, + Vertx vertx, + MqttClient mqttClient) { + return new IotDeviceUpstreamServer(emqxProperties, deviceUpstreamApi, vertx, mqttClient); + } + + @Bean + public IotDeviceDownstreamHandler deviceDownstreamHandler(MqttClient mqttClient) { + return new IotDeviceDownstreamHandlerImpl(mqttClient); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxProperties.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxProperties.java new file mode 100644 index 000000000..219fe0360 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxProperties.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.iot.plugin.emqx.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +/** + * 物联网插件 - EMQX 配置 + * + * @author 芋道源码 + */ +@ConfigurationProperties(prefix = "yudao.iot.plugin.emqx") +@Validated +@Data +public class IotPluginEmqxProperties { + + // TODO @haohao:参数校验,加下,啊哈 + + /** + * 服务主机 + */ + private String mqttHost; + /** + * 服务端口 + */ + private Integer mqttPort; + /** + * 服务用户名 + */ + private String mqttUsername; + /** + * 服务密码 + */ + private String mqttPassword; + /** + * 是否启用 SSL + */ + private Boolean mqttSsl; + + /** + * 订阅的主题列表 + */ + private String[] mqttTopics; + + /** + * 认证端口 + */ + private Integer authPort; + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/downstream/IotDeviceDownstreamHandlerImpl.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/downstream/IotDeviceDownstreamHandlerImpl.java new file mode 100644 index 000000000..f5c19224a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/downstream/IotDeviceDownstreamHandlerImpl.java @@ -0,0 +1,176 @@ +package cn.iocoder.yudao.module.iot.plugin.emqx.downstream; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.buffer.Buffer; +import io.vertx.mqtt.MqttClient; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.MQTT_TOPIC_ILLEGAL; + +/** + * EMQX 插件的 {@link IotDeviceDownstreamHandler} 实现类 + * + * @author 芋道源码 + */ +@Slf4j +public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandler { + + private static final String SYS_TOPIC_PREFIX = "/sys/"; + + // TODO @haohao:是不是可以类似 IotDeviceConfigSetVertxHandler 的建议,抽到统一的枚举类 + // TODO @haohao:讨论,感觉 mqtt 和 http,可以做个相对统一的格式哈。;回复 都使用 Alink 格式,方便后续扩展。 + // 设备服务调用 标准 JSON + // 请求Topic:/sys/${productKey}/${deviceName}/thing/service/${tsl.service.identifier} + // 响应Topic:/sys/${productKey}/${deviceName}/thing/service/${tsl.service.identifier}_reply + private static final String SERVICE_TOPIC_PREFIX = "/thing/service/"; + + // 设置设备属性 标准 JSON + // 请求Topic:/sys/${productKey}/${deviceName}/thing/service/property/set + // 响应Topic:/sys/${productKey}/${deviceName}/thing/service/property/set_reply + private static final String PROPERTY_SET_TOPIC = "/thing/service/property/set"; + + private final MqttClient mqttClient; + + /** + * 构造函数 + * + * @param mqttClient MQTT客户端 + */ + public IotDeviceDownstreamHandlerImpl(MqttClient mqttClient) { + this.mqttClient = mqttClient; + } + + @Override + public CommonResult invokeDeviceService(IotDeviceServiceInvokeReqDTO reqDTO) { + log.info("[invokeService][开始调用设备服务][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO)); + + // 验证参数 + if (reqDTO.getProductKey() == null || reqDTO.getDeviceName() == null || reqDTO.getIdentifier() == null) { + log.error("[invokeService][参数不完整][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO)); + return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg()); + } + + try { + // 构建请求主题 + String topic = buildServiceTopic(reqDTO.getProductKey(), reqDTO.getDeviceName(), reqDTO.getIdentifier()); + // 构建请求消息 + String requestId = reqDTO.getRequestId() != null ? reqDTO.getRequestId() : generateRequestId(); + JSONObject request = buildServiceRequest(requestId, reqDTO.getIdentifier(), reqDTO.getParams()); + // 发送消息 + publishMessage(topic, request); + + log.info("[invokeService][调用设备服务成功][requestId: {}][topic: {}]", requestId, topic); + return CommonResult.success(true); + } catch (Exception e) { + log.error("[invokeService][调用设备服务异常][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO), e); + return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg()); + } + } + + @Override + public CommonResult getDeviceProperty(IotDevicePropertyGetReqDTO getReqDTO) { + return CommonResult.success(true); + } + + @Override + public CommonResult setDeviceProperty(IotDevicePropertySetReqDTO reqDTO) { + // 验证参数 + log.info("[setProperty][开始设置设备属性][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO)); + if (reqDTO.getProductKey() == null || reqDTO.getDeviceName() == null) { + log.error("[setProperty][参数不完整][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO)); + return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg()); + } + + try { + // 构建请求主题 + String topic = buildPropertySetTopic(reqDTO.getProductKey(), reqDTO.getDeviceName()); + // 构建请求消息 + String requestId = reqDTO.getRequestId() != null ? reqDTO.getRequestId() : generateRequestId(); + JSONObject request = buildPropertySetRequest(requestId, reqDTO.getProperties()); + // 发送消息 + publishMessage(topic, request); + + log.info("[setProperty][设置设备属性成功][requestId: {}][topic: {}]", requestId, topic); + return CommonResult.success(true); + } catch (Exception e) { + log.error("[setProperty][设置设备属性异常][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO), e); + return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg()); + } + } + + @Override + public CommonResult setDeviceConfig(IotDeviceConfigSetReqDTO setReqDTO) { + return CommonResult.success(true); + } + + @Override + public CommonResult upgradeDeviceOta(IotDeviceOtaUpgradeReqDTO upgradeReqDTO) { + return CommonResult.success(true); + } + + /** + * 构建服务调用主题 + */ + private String buildServiceTopic(String productKey, String deviceName, String serviceIdentifier) { + return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + SERVICE_TOPIC_PREFIX + serviceIdentifier; + } + + /** + * 构建属性设置主题 + */ + private String buildPropertySetTopic(String productKey, String deviceName) { + return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + PROPERTY_SET_TOPIC; + } + + // TODO @haohao:这个,后面搞个对象,会不会好点哈? + /** + * 构建服务调用请求 + */ + private JSONObject buildServiceRequest(String requestId, String serviceIdentifier, Map params) { + return new JSONObject() + .set("id", requestId) + .set("version", "1.0") + .set("method", "thing.service." + serviceIdentifier) + .set("params", params != null ? params : new JSONObject()); + } + + /** + * 构建属性设置请求 + */ + private JSONObject buildPropertySetRequest(String requestId, Map properties) { + return new JSONObject() + .set("id", requestId) + .set("version", "1.0") + .set("method", "thing.service.property.set") + .set("params", properties); + } + + /** + * 发布 MQTT 消息 + */ + private void publishMessage(String topic, JSONObject payload) { + mqttClient.publish( + topic, + Buffer.buffer(payload.toString()), + MqttQoS.AT_LEAST_ONCE, + false, + false); + log.info("[publishMessage][发送消息成功][topic: {}][payload: {}]", topic, payload); + } + + /** + * 生成请求 ID + */ + private String generateRequestId() { + return IdUtil.fastSimpleUUID(); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/IotDeviceUpstreamServer.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/IotDeviceUpstreamServer.java new file mode 100644 index 000000000..00792ebcf --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/IotDeviceUpstreamServer.java @@ -0,0 +1,236 @@ +package cn.iocoder.yudao.module.iot.plugin.emqx.upstream; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; +import cn.iocoder.yudao.module.iot.plugin.emqx.config.IotPluginEmqxProperties; +import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router.IotDeviceAuthVertxHandler; +import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router.IotDeviceMqttMessageHandler; +import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router.IotDeviceWebhookVertxHandler; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.BodyHandler; +import io.vertx.mqtt.MqttClient; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +/** + * IoT 设备下行服务端,接收来自 device 设备的请求,转发给 server 服务器 + *

+ * 协议:HTTP、MQTT + * + * @author haohao + */ +@Slf4j +public class IotDeviceUpstreamServer { + + /** + * 重连延迟时间(毫秒) + */ + private static final int RECONNECT_DELAY_MS = 5000; + /** + * 连接超时时间(毫秒) + */ + private static final int CONNECTION_TIMEOUT_MS = 10000; + /** + * 默认 QoS 级别 + */ + private static final MqttQoS DEFAULT_QOS = MqttQoS.AT_LEAST_ONCE; + + private final Vertx vertx; + private final HttpServer server; + private final MqttClient client; + private final IotPluginEmqxProperties emqxProperties; + private final IotDeviceMqttMessageHandler mqttMessageHandler; + + /** + * 服务运行状态标志 + */ + private volatile boolean isRunning = false; + + public IotDeviceUpstreamServer(IotPluginEmqxProperties emqxProperties, + IotDeviceUpstreamApi deviceUpstreamApi, + Vertx vertx, + MqttClient client) { + this.vertx = vertx; + this.emqxProperties = emqxProperties; + this.client = client; + + // 创建 Router 实例 + Router router = Router.router(vertx); + router.route().handler(BodyHandler.create()); // 处理 Body + router.post(IotDeviceAuthVertxHandler.PATH) + // TODO @haohao:疑问,mqtt 的认证,需要通过 http 呀? + // 回复:MQTT 认证不必须通过 HTTP 进行,但 HTTP 认证是 EMQX 等 MQTT 服务器支持的一种灵活的认证方式 + .handler(new IotDeviceAuthVertxHandler(deviceUpstreamApi)); + // 添加 Webhook 处理器,用于处理设备连接和断开连接事件 + router.post(IotDeviceWebhookVertxHandler.PATH) + .handler(new IotDeviceWebhookVertxHandler(deviceUpstreamApi)); + // 创建 HttpServer 实例 + this.server = vertx.createHttpServer().requestHandler(router); + this.mqttMessageHandler = new IotDeviceMqttMessageHandler(deviceUpstreamApi, client); + } + + /** + * 启动 HTTP 服务器、MQTT 客户端 + */ + public void start() { + if (isRunning) { + log.warn("[start][服务已经在运行中,请勿重复启动]"); + return; + } + log.info("[start][开始启动服务]"); + + // TODO @haohao:建议先启动 MQTT Broker,再启动 HTTP Server。类似 jdbc 先连接了,在启动 tomcat 的味道 + // 1. 启动 HTTP 服务器 + CompletableFuture httpFuture = server.listen(emqxProperties.getAuthPort()) + .toCompletionStage() + .toCompletableFuture() + .thenAccept(v -> log.info("[start][HTTP 服务器启动完成,端口: {}]", server.actualPort())); + + // 2. 连接 MQTT Broker + CompletableFuture mqttFuture = connectMqtt() + .toCompletionStage() + .toCompletableFuture() + .thenAccept(v -> { + // 2.1 添加 MQTT 断开重连监听器 + client.closeHandler(closeEvent -> { + log.warn("[closeHandler][MQTT 连接已断开,准备重连]"); + reconnectWithDelay(); + }); + // 2.2 设置 MQTT 消息处理器 + setupMessageHandler(); + }); + + // 3. 等待所有服务启动完成 + CompletableFuture.allOf(httpFuture, mqttFuture) + .orTimeout(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS) // TODO @芋艿:JDK8 不兼容 + .whenComplete((result, error) -> { + if (error != null) { + log.error("[start][服务启动失败]", error); + } else { + isRunning = true; + log.info("[start][所有服务启动完成]"); + } + }); + } + + /** + * 设置 MQTT 消息处理器 + */ + private void setupMessageHandler() { + client.publishHandler(mqttMessageHandler::handle); + log.debug("[setupMessageHandler][MQTT 消息处理器设置完成]"); + } + + /** + * 重连 MQTT 客户端 + */ + private void reconnectWithDelay() { + if (!isRunning) { + log.info("[reconnectWithDelay][服务已停止,不再尝试重连]"); + return; + } + + vertx.setTimer(RECONNECT_DELAY_MS, id -> { + log.info("[reconnectWithDelay][开始重新连接 MQTT]"); + connectMqtt(); + }); + } + + /** + * 连接 MQTT Broker 并订阅主题 + * + * @return 连接结果的Future + */ + private Future connectMqtt() { + return client.connect(emqxProperties.getMqttPort(), emqxProperties.getMqttHost()) + .compose(connAck -> { + log.info("[connectMqtt][MQTT客户端连接成功]"); + return subscribeToTopics(); + }) + .recover(error -> { + log.error("[connectMqtt][连接MQTT Broker失败:]", error); + reconnectWithDelay(); + return Future.failedFuture(error); + }); + } + + /** + * 订阅设备上行消息主题 + * + * @return 订阅结果的 Future + */ + private Future subscribeToTopics() { + String[] topics = emqxProperties.getMqttTopics(); + if (ArrayUtil.isEmpty(topics)) { + log.warn("[subscribeToTopics][未配置MQTT主题,跳过订阅]"); + return Future.succeededFuture(); + } + log.info("[subscribeToTopics][开始订阅设备上行消息主题]"); + + Future compositeFuture = Future.succeededFuture(); + for (String topic : topics) { + String trimmedTopic = topic.trim(); + if (trimmedTopic.isEmpty()) { + continue; + } + compositeFuture = compositeFuture.compose(v -> client.subscribe(trimmedTopic, DEFAULT_QOS.value()) + .map(ack -> { + log.info("[subscribeToTopics][成功订阅主题: {}]", trimmedTopic); + return null; + }) + .recover(error -> { + log.error("[subscribeToTopics][订阅主题失败: {}]", trimmedTopic, error); + return Future.succeededFuture(); // 继续订阅其他主题 + })); + } + return compositeFuture; + } + + /** + * 停止所有服务 + */ + public void stop() { + if (!isRunning) { + log.warn("[stop][服务未运行,无需停止]"); + return; + } + log.info("[stop][开始关闭服务]"); + isRunning = false; + + try { + // 关闭 HTTP 服务器 + if (server != null) { + server.close() + .toCompletionStage() + .toCompletableFuture() + .join(); + } + + // 关闭 MQTT 客户端 + if (client != null) { + client.disconnect() + .toCompletionStage() + .toCompletableFuture() + .join(); + } + + // 关闭 Vertx 实例 + if (vertx!= null) { + vertx.close() + .toCompletionStage() + .toCompletableFuture() + .join(); + } + log.info("[stop][关闭完成]"); + } catch (Exception e) { + log.error("[stop][关闭服务异常]", e); + throw new RuntimeException("关闭 IoT 设备上行服务失败", e); + } + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceAuthVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceAuthVertxHandler.java new file mode 100644 index 000000000..e9206d5b6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceAuthVertxHandler.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEmqxAuthReqDTO; +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Collections; + +/** + * IoT EMQX 连接认证的 Vert.x Handler + * + * 参考:EMQX HTTP + * + * 注意:该处理器需要返回特定格式:{"result": "allow"} 或 {"result": "deny"}, + * 以符合 EMQX 认证插件的要求,因此不使用 IotStandardResponse 实体类 + * + * @author haohao + */ +@RequiredArgsConstructor +@Slf4j +public class IotDeviceAuthVertxHandler implements Handler { + + public static final String PATH = "/mqtt/auth"; + + private final IotDeviceUpstreamApi deviceUpstreamApi; + + @Override + public void handle(RoutingContext routingContext) { + try { + // 构建认证请求 DTO + JsonObject json = routingContext.body().asJsonObject(); + String clientId = json.getString("clientid"); + String username = json.getString("username"); + String password = json.getString("password"); + IotDeviceEmqxAuthReqDTO authReqDTO = new IotDeviceEmqxAuthReqDTO() + .setClientId(clientId) + .setUsername(username) + .setPassword(password); + + // 调用认证 API + CommonResult authResult = deviceUpstreamApi.authenticateEmqxConnection(authReqDTO); + if (authResult.getCode() != 0 || !authResult.getData()) { + // 注意:这里必须返回 {"result": "deny"} 格式,以符合 EMQX 认证插件的要求 + IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "deny")); + return; + } + + // 响应结果 + // 注意:这里必须返回 {"result": "allow"} 格式,以符合 EMQX 认证插件的要求 + IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "allow")); + } catch (Exception e) { + log.error("[handle][EMQX 认证异常]", e); + // 注意:这里必须返回 {"result": "deny"} 格式,以符合 EMQX 认证插件的要求 + IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "deny")); + } + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceMqttMessageHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceMqttMessageHandler.java new file mode 100644 index 000000000..00fa1b96d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceMqttMessageHandler.java @@ -0,0 +1,296 @@ +package cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO; +import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.buffer.Buffer; +import io.vertx.mqtt.MqttClient; +import io.vertx.mqtt.messages.MqttPublishMessage; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * IoT 设备 MQTT 消息处理器 + * + * 参考:设备属性、事件、服务 + */ +@Slf4j +public class IotDeviceMqttMessageHandler { + + // TODO @haohao:讨论,感觉 mqtt 和 http,可以做个相对统一的格式哈;回复 都使用 Alink 格式,方便后续扩展。 + // 设备上报属性 标准 JSON + // 请求 Topic:/sys/${productKey}/${deviceName}/thing/event/property/post + // 响应 Topic:/sys/${productKey}/${deviceName}/thing/event/property/post_reply + + // 设备上报事件 标准 JSON + // 请求 Topic:/sys/${productKey}/${deviceName}/thing/event/${tsl.event.identifier}/post + // 响应 Topic:/sys/${productKey}/${deviceName}/thing/event/${tsl.event.identifier}/post_reply + + private static final String SYS_TOPIC_PREFIX = "/sys/"; + private static final String PROPERTY_POST_TOPIC = "/thing/event/property/post"; + private static final String EVENT_POST_TOPIC_PREFIX = "/thing/event/"; + private static final String EVENT_POST_TOPIC_SUFFIX = "/post"; + private static final String REPLY_SUFFIX = "_reply"; + private static final String PROPERTY_METHOD = "thing.event.property.post"; + private static final String EVENT_METHOD_PREFIX = "thing.event."; + private static final String EVENT_METHOD_SUFFIX = ".post"; + + private final IotDeviceUpstreamApi deviceUpstreamApi; + private final MqttClient mqttClient; + + public IotDeviceMqttMessageHandler(IotDeviceUpstreamApi deviceUpstreamApi, MqttClient mqttClient) { + this.deviceUpstreamApi = deviceUpstreamApi; + this.mqttClient = mqttClient; + } + + /** + * 处理MQTT消息 + * + * @param message MQTT发布消息 + */ + public void handle(MqttPublishMessage message) { + String topic = message.topicName(); + String payload = message.payload().toString(); + log.info("[messageHandler][接收到消息][topic: {}][payload: {}]", topic, payload); + + try { + if (StrUtil.isEmpty(payload)) { + log.warn("[messageHandler][消息内容为空][topic: {}]", topic); + return; + } + handleMessage(topic, payload); + } catch (Exception e) { + log.error("[messageHandler][处理消息失败][topic: {}][payload: {}]", topic, payload, e); + } + } + + /** + * 根据主题类型处理消息 + * + * @param topic 主题 + * @param payload 消息内容 + */ + private void handleMessage(String topic, String payload) { + // 校验前缀 + if (!topic.startsWith(SYS_TOPIC_PREFIX)) { + log.warn("[handleMessage][未知的消息类型][topic: {}]", topic); + return; + } + + // 处理设备属性上报消息 + if (topic.endsWith(PROPERTY_POST_TOPIC)) { + log.info("[handleMessage][接收到设备属性上报][topic: {}]", topic); + handlePropertyPost(topic, payload); + return; + } + + // 处理设备事件上报消息 + if (topic.contains(EVENT_POST_TOPIC_PREFIX) && topic.endsWith(EVENT_POST_TOPIC_SUFFIX)) { + log.info("[handleMessage][接收到设备事件上报][topic: {}]", topic); + handleEventPost(topic, payload); + return; + } + + // 未知消息类型 + log.warn("[handleMessage][未知的消息类型][topic: {}]", topic); + } + + /** + * 处理设备属性上报消息 + * + * @param topic 主题 + * @param payload 消息内容 + */ + private void handlePropertyPost(String topic, String payload) { + try { + // 解析消息内容 + JSONObject jsonObject = JSONUtil.parseObj(payload); + String[] topicParts = parseTopic(topic); + if (topicParts == null) { + return; + } + + // 构建设备属性上报请求对象 + IotDevicePropertyReportReqDTO reportReqDTO = buildPropertyReportDTO(jsonObject, topicParts); + + // 调用上游 API 处理设备上报数据 + deviceUpstreamApi.reportDeviceProperty(reportReqDTO); + log.info("[handlePropertyPost][处理设备属性上报成功][topic: {}]", topic); + + // 发送响应消息 + sendResponse(topic, jsonObject, PROPERTY_METHOD, null); + } catch (Exception e) { + log.error("[handlePropertyPost][处理设备属性上报失败][topic: {}][payload: {}]", topic, payload, e); + } + } + + /** + * 处理设备事件上报消息 + * + * @param topic 主题 + * @param payload 消息内容 + */ + private void handleEventPost(String topic, String payload) { + try { + // 解析消息内容 + JSONObject jsonObject = JSONUtil.parseObj(payload); + String[] topicParts = parseTopic(topic); + if (topicParts == null) { + return; + } + + // 构建设备事件上报请求对象 + IotDeviceEventReportReqDTO reportReqDTO = buildEventReportDTO(jsonObject, topicParts); + + // 调用上游 API 处理设备上报数据 + deviceUpstreamApi.reportDeviceEvent(reportReqDTO); + log.info("[handleEventPost][处理设备事件上报成功][topic: {}]", topic); + + // 从 topic 中获取事件标识符 + String eventIdentifier = getEventIdentifier(topicParts, topic); + if (eventIdentifier == null) { + return; + } + + // 发送响应消息 + String method = EVENT_METHOD_PREFIX + eventIdentifier + EVENT_METHOD_SUFFIX; + sendResponse(topic, jsonObject, method, null); + } catch (Exception e) { + log.error("[handleEventPost][处理设备事件上报失败][topic: {}][payload: {}]", topic, payload, e); + } + } + + /** + * 解析主题,获取主题各部分 + * + * @param topic 主题 + * @return 主题各部分数组,如果解析失败返回null + */ + private String[] parseTopic(String topic) { + String[] topicParts = topic.split("/"); + if (topicParts.length < 7) { + log.warn("[parseTopic][主题格式不正确][topic: {}]", topic); + return null; + } + return topicParts; + } + + /** + * 从主题部分中获取事件标识符 + * + * @param topicParts 主题各部分 + * @param topic 原始主题,用于日志 + * @return 事件标识符,如果获取失败返回null + */ + private String getEventIdentifier(String[] topicParts, String topic) { + try { + return topicParts[6]; + } catch (ArrayIndexOutOfBoundsException e) { + log.warn("[getEventIdentifier][无法从主题中获取事件标识符][topic: {}][topicParts: {}]", + topic, Arrays.toString(topicParts)); + return null; + } + } + + /** + * 发送响应消息 + * + * @param topic 原始主题 + * @param jsonObject 原始消息JSON对象 + * @param method 响应方法 + * @param customData 自定义数据,可为 null + */ + private void sendResponse(String topic, JSONObject jsonObject, String method, Object customData) { + String replyTopic = topic + REPLY_SUFFIX; + + // 响应结果 + IotStandardResponse response = IotStandardResponse.success( + jsonObject.getStr("id"), method, customData); + try { + mqttClient.publish(replyTopic, Buffer.buffer(JsonUtils.toJsonString(response)), + MqttQoS.AT_LEAST_ONCE, false, false); + log.info("[sendResponse][发送响应消息成功][topic: {}]", replyTopic); + } catch (Exception e) { + log.error("[sendResponse][发送响应消息失败][topic: {}][response: {}]", replyTopic, response, e); + } + } + + /** + * 构建设备属性上报请求对象 + * + * @param jsonObject 消息内容 + * @param topicParts 主题部分 + * @return 设备属性上报请求对象 + */ + private IotDevicePropertyReportReqDTO buildPropertyReportDTO(JSONObject jsonObject, String[] topicParts) { + IotDevicePropertyReportReqDTO reportReqDTO = new IotDevicePropertyReportReqDTO(); + reportReqDTO.setRequestId(jsonObject.getStr("id")); + reportReqDTO.setProcessId(IotPluginCommonUtils.getProcessId()); + reportReqDTO.setReportTime(LocalDateTime.now()); + reportReqDTO.setProductKey(topicParts[2]); + reportReqDTO.setDeviceName(topicParts[3]); + + // 只使用标准JSON格式处理属性数据 + JSONObject params = jsonObject.getJSONObject("params"); + if (params == null) { + log.warn("[buildPropertyReportDTO][消息格式不正确,缺少params字段][jsonObject: {}]", jsonObject); + params = new JSONObject(); + } + + // 将标准格式的params转换为平台需要的properties格式 + Map properties = new HashMap<>(); + for (Map.Entry entry : params.entrySet()) { + String key = entry.getKey(); + Object valueObj = entry.getValue(); + + // 如果是复杂结构(包含value和time) + if (valueObj instanceof JSONObject valueJson) { + properties.put(key, valueJson.getOrDefault("value", valueObj)); + } else { + properties.put(key, valueObj); + } + } + reportReqDTO.setProperties(properties); + + return reportReqDTO; + } + + /** + * 构建设备事件上报请求对象 + * + * @param jsonObject 消息内容 + * @param topicParts 主题部分 + * @return 设备事件上报请求对象 + */ + private IotDeviceEventReportReqDTO buildEventReportDTO(JSONObject jsonObject, String[] topicParts) { + IotDeviceEventReportReqDTO reportReqDTO = new IotDeviceEventReportReqDTO(); + reportReqDTO.setRequestId(jsonObject.getStr("id")); + reportReqDTO.setProcessId(IotPluginCommonUtils.getProcessId()); + reportReqDTO.setReportTime(LocalDateTime.now()); + reportReqDTO.setProductKey(topicParts[2]); + reportReqDTO.setDeviceName(topicParts[3]); + reportReqDTO.setIdentifier(topicParts[6]); + + // 只使用标准JSON格式处理事件参数 + JSONObject params = jsonObject.getJSONObject("params"); + if (params == null) { + log.warn("[buildEventReportDTO][消息格式不正确,缺少params字段][jsonObject: {}]", jsonObject); + params = new JSONObject(); + } + reportReqDTO.setParams(params); + + return reportReqDTO; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceWebhookVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceWebhookVertxHandler.java new file mode 100644 index 000000000..21b49e097 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceWebhookVertxHandler.java @@ -0,0 +1,152 @@ +package cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.Collections; + +/** + * IoT EMQX Webhook 事件处理的 Vert.x Handler + * + * 参考:EMQX Webhook + * + * 注意:该处理器需要返回特定格式:{"result": "success"} 或 {"result": "error"}, + * 以符合 EMQX Webhook 插件的要求,因此不使用 IotStandardResponse 实体类。 + * + * @author haohao + */ +@RequiredArgsConstructor +@Slf4j +public class IotDeviceWebhookVertxHandler implements Handler { + + public static final String PATH = "/mqtt/webhook"; + + private final IotDeviceUpstreamApi deviceUpstreamApi; + + @Override + public void handle(RoutingContext routingContext) { + try { + // 解析请求体 + JsonObject json = routingContext.body().asJsonObject(); + String event = json.getString("event"); + String clientId = json.getString("clientid"); + String username = json.getString("username"); + + // 处理不同的事件类型 + switch (event) { + case "client.connected": + handleClientConnected(clientId, username); + break; + case "client.disconnected": + handleClientDisconnected(clientId, username); + break; + default: + log.info("[handle][未处理的 Webhook 事件] event={}, clientId={}, username={}", event, clientId, username); + break; + } + + // 返回成功响应 + // 注意:这里必须返回 {"result": "success"} 格式,以符合 EMQX Webhook 插件的要求 + IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "success")); + } catch (Exception e) { + log.error("[handle][处理 Webhook 事件异常]", e); + // 注意:这里必须返回 {"result": "error"} 格式,以符合 EMQX Webhook 插件的要求 + IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "error")); + } + } + + /** + * 处理客户端连接事件 + * + * @param clientId 客户端ID + * @param username 用户名 + */ + private void handleClientConnected(String clientId, String username) { + // 解析产品标识和设备名称 + if (StrUtil.isEmpty(username) || "undefined".equals(username)) { + log.warn("[handleClientConnected][客户端连接事件,但用户名为空] clientId={}", clientId); + return; + } + String[] parts = parseUsername(username); + if (parts == null) { + return; + } + + // 更新设备状态为在线 + IotDeviceStateUpdateReqDTO updateReqDTO = new IotDeviceStateUpdateReqDTO(); + updateReqDTO.setProductKey(parts[1]); + updateReqDTO.setDeviceName(parts[0]); + updateReqDTO.setState(IotDeviceStateEnum.ONLINE.getState()); + updateReqDTO.setProcessId(IotPluginCommonUtils.getProcessId()); + updateReqDTO.setReportTime(LocalDateTime.now()); + CommonResult result = deviceUpstreamApi.updateDeviceState(updateReqDTO); + if (result.getCode() != 0 || !result.getData()) { + log.error("[handleClientConnected][更新设备状态为在线失败] clientId={}, username={}, code={}, msg={}", + clientId, username, result.getCode(), result.getMsg()); + } else { + log.info("[handleClientConnected][更新设备状态为在线成功] clientId={}, username={}", clientId, username); + } + } + + /** + * 处理客户端断开连接事件 + * + * @param clientId 客户端ID + * @param username 用户名 + */ + private void handleClientDisconnected(String clientId, String username) { + // 解析产品标识和设备名称 + if (StrUtil.isEmpty(username) || "undefined".equals(username)) { + log.warn("[handleClientDisconnected][客户端断开连接事件,但用户名为空] clientId={}", clientId); + return; + } + String[] parts = parseUsername(username); + if (parts == null) { + return; + } + + // 更新设备状态为离线 + IotDeviceStateUpdateReqDTO offlineReqDTO = new IotDeviceStateUpdateReqDTO(); + offlineReqDTO.setProductKey(parts[1]); + offlineReqDTO.setDeviceName(parts[0]); + offlineReqDTO.setState(IotDeviceStateEnum.OFFLINE.getState()); + offlineReqDTO.setProcessId(IotPluginCommonUtils.getProcessId()); + offlineReqDTO.setReportTime(LocalDateTime.now()); + CommonResult offlineResult = deviceUpstreamApi.updateDeviceState(offlineReqDTO); + if (offlineResult.getCode() != 0 || !offlineResult.getData()) { + log.error("[handleClientDisconnected][更新设备状态为离线失败] clientId={}, username={}, code={}, msg={}", + clientId, username, offlineResult.getCode(), offlineResult.getMsg()); + } else { + log.info("[handleClientDisconnected][更新设备状态为离线成功] clientId={}, username={}", clientId, username); + } + } + + /** + * 解析用户名,格式为 deviceName&productKey + * + * @param username 用户名 + * @return 解析结果,[0] 为 deviceName,[1] 为 productKey,解析失败返回 null + */ + private String[] parseUsername(String username) { + if (StrUtil.isEmpty(username)) { + return null; + } + String[] parts = username.split("&"); + if (parts.length != 2) { + log.warn("[parseUsername][用户名格式({})不正确,无法解析产品标识和设备名称]", username); + return null; + } + return parts; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/resources/application.yml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/resources/application.yml new file mode 100644 index 000000000..c00621c82 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/resources/application.yml @@ -0,0 +1,20 @@ +spring: + application: + name: yudao-module-iot-plugin-emqx + +yudao: + iot: + plugin: + common: + upstream-url: http://127.0.0.1:48080 + downstream-port: 8100 + plugin-key: yudao-module-iot-plugin-emqx + emqx: + mqtt-host: 127.0.0.1 + mqtt-port: 1883 + mqtt-ssl: false + mqtt-username: yudao + mqtt-password: 123456 + mqtt-topics: + - "/sys/#" + auth-port: 8101 diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/plugin.properties b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/plugin.properties new file mode 100644 index 000000000..647d55155 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/plugin.properties @@ -0,0 +1,6 @@ +plugin.id=yudao-module-iot-plugin-http +plugin.class=cn.iocoder.yudao.module.iot.plugin.http.config.IotHttpVertxPlugin +plugin.version=1.0.0 +plugin.provider=yudao +plugin.dependencies= +plugin.description=yudao-module-iot-plugin-http-1.0.0 \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/pom.xml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/pom.xml new file mode 100644 index 000000000..88a413ca6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/pom.xml @@ -0,0 +1,165 @@ + + + + yudao-module-iot-plugins + cn.iocoder.boot + ${revision} + + 4.0.0 + jar + + yudao-module-iot-plugin-http + 1.0.0 + + ${project.artifactId} + + + 物联网 插件模块 - http 插件 + + + + + ${project.artifactId} + cn.iocoder.yudao.module.iot.plugin.http.config.IotHttpVertxPlugin + ${project.version} + yudao + ${project.artifactId}-${project.version} + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.6 + + + unzip jar file + package + + + + + + + run + + + + + + + maven-assembly-plugin + 2.3 + + + + src/main/assembly/assembly.xml + + + false + + + + make-assembly + package + + attached + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + ${plugin.id} + ${plugin.class} + ${plugin.version} + ${plugin.provider} + ${plugin.description} + ${plugin.dependencies} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + + repackage + + + -standalone + + + + + + + + + + + cn.iocoder.boot + yudao-module-iot-plugin-common + ${revision} + + + + + org.springframework.boot + spring-boot-starter-web + + + + + io.vertx + vertx-web + + + \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/assembly/assembly.xml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/assembly/assembly.xml new file mode 100644 index 000000000..9b79e6152 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/assembly/assembly.xml @@ -0,0 +1,24 @@ + + plugin + + zip + + false + + + false + runtime + lib + + *:jar:* + + + + + + + target/plugin-classes + classes + + + diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/IotHttpPluginApplication.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/IotHttpPluginApplication.java new file mode 100644 index 000000000..a88b34eb3 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/IotHttpPluginApplication.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.iot.plugin.http; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 独立运行入口 + */ +@Slf4j +@SpringBootApplication +public class IotHttpPluginApplication { + + public static void main(String[] args) { + SpringApplication application = new SpringApplication(IotHttpPluginApplication.class); + application.setWebApplicationType(WebApplicationType.NONE); + application.run(args); + log.info("[main][独立模式启动完成]"); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotHttpVertxPlugin.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotHttpVertxPlugin.java new file mode 100644 index 000000000..f704c1844 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotHttpVertxPlugin.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.iot.plugin.http.config; + +import cn.hutool.core.lang.Assert; +import cn.hutool.extra.spring.SpringUtil; +import lombok.extern.slf4j.Slf4j; +import org.pf4j.PluginWrapper; +import org.pf4j.spring.SpringPlugin; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +// TODO @芋艿:完善注释 +/** + * 负责插件的启动和停止,与 Vert.x 的生命周期管理 + */ +@Slf4j +public class IotHttpVertxPlugin extends SpringPlugin { + + public IotHttpVertxPlugin(PluginWrapper wrapper) { + super(wrapper); + } + + @Override + public void start() { + log.info("[HttpVertxPlugin][HttpVertxPlugin 插件启动开始...]"); + try { + ApplicationContext pluginContext = getApplicationContext(); + Assert.notNull(pluginContext, "pluginContext 不能为空"); + log.info("[HttpVertxPlugin][HttpVertxPlugin 插件启动成功...]"); + } catch (Exception e) { + log.error("[HttpVertxPlugin][HttpVertxPlugin 插件开启动异常...]", e); + } + } + + @Override + public void stop() { + log.info("[HttpVertxPlugin][HttpVertxPlugin 插件停止开始...]"); + try { + log.info("[HttpVertxPlugin][HttpVertxPlugin 插件停止成功...]"); + } catch (Exception e) { + log.error("[HttpVertxPlugin][HttpVertxPlugin 插件停止异常...]", e); + } + } + + // TODO @芋艿:思考下,未来要不要。。。 + @Override + protected ApplicationContext createApplicationContext() { + // 创建插件自己的 ApplicationContext + AnnotationConfigApplicationContext pluginContext = new AnnotationConfigApplicationContext(); + // 设置父容器为主应用的 ApplicationContext (确保主应用中提供的类可用) + pluginContext.setParent(SpringUtil.getApplicationContext()); + // 继续使用插件自己的 ClassLoader 以加载插件内部的类 + pluginContext.setClassLoader(getWrapper().getPluginClassLoader()); + // 扫描当前插件的自动配置包 + // TODO @芋艿:后续看看,怎么配置类包 + pluginContext.scan("cn.iocoder.yudao.module.iot.plugin.http.config"); + pluginContext.refresh(); + return pluginContext; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotPluginHttpAutoConfiguration.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotPluginHttpAutoConfiguration.java new file mode 100644 index 000000000..63e55f58f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotPluginHttpAutoConfiguration.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.plugin.http.config; + +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; +import cn.iocoder.yudao.module.iot.plugin.http.downstream.IotDeviceDownstreamHandlerImpl; +import cn.iocoder.yudao.module.iot.plugin.http.upstream.IotDeviceUpstreamServer; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * IoT 插件 HTTP 的专用自动配置类 + * + * @author haohao + */ +@Configuration +@EnableConfigurationProperties(IotPluginHttpProperties.class) +public class IotPluginHttpAutoConfiguration { + + @Bean(initMethod = "start", destroyMethod = "stop") + public IotDeviceUpstreamServer deviceUpstreamServer(IotDeviceUpstreamApi deviceUpstreamApi, + IotPluginHttpProperties properties) { + return new IotDeviceUpstreamServer(properties, deviceUpstreamApi); + } + + @Bean + public IotDeviceDownstreamHandler deviceDownstreamHandler() { + return new IotDeviceDownstreamHandlerImpl(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotPluginHttpProperties.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotPluginHttpProperties.java new file mode 100644 index 000000000..49dca8126 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotPluginHttpProperties.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.module.iot.plugin.http.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +@ConfigurationProperties(prefix = "yudao.iot.plugin.http") +@Validated +@Data +public class IotPluginHttpProperties { + + /** + * HTTP 服务端口 + */ + private Integer serverPort; + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/downstream/IotDeviceDownstreamHandlerImpl.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/downstream/IotDeviceDownstreamHandlerImpl.java new file mode 100644 index 000000000..869fe7234 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/downstream/IotDeviceDownstreamHandlerImpl.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.iot.plugin.http.downstream; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED; + +/** + * HTTP 插件的 {@link IotDeviceDownstreamHandler} 实现类 + * + * 但是:由于设备通过 HTTP 短链接接入,导致其实无法下行指导给 device 设备,所以基本都是直接返回失败!!! + * 类似 MQTT、WebSocket、TCP 插件,是可以实现下行指令的。 + * + * @author 芋道源码 + */ +public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandler { + + @Override + public CommonResult invokeDeviceService(IotDeviceServiceInvokeReqDTO invokeReqDTO) { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持调用设备服务"); + } + + @Override + public CommonResult getDeviceProperty(IotDevicePropertyGetReqDTO getReqDTO) { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持获取设备属性"); + } + + @Override + public CommonResult setDeviceProperty(IotDevicePropertySetReqDTO setReqDTO) { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持设置设备属性"); + } + + @Override + public CommonResult setDeviceConfig(IotDeviceConfigSetReqDTO setReqDTO) { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持设置设备属性"); + } + + @Override + public CommonResult upgradeDeviceOta(IotDeviceOtaUpgradeReqDTO upgradeReqDTO) { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持设置设备属性"); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/upstream/IotDeviceUpstreamServer.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/upstream/IotDeviceUpstreamServer.java new file mode 100644 index 000000000..67129a4d1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/upstream/IotDeviceUpstreamServer.java @@ -0,0 +1,83 @@ +package cn.iocoder.yudao.module.iot.plugin.http.upstream; + +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; +import cn.iocoder.yudao.module.iot.plugin.http.config.IotPluginHttpProperties; +import cn.iocoder.yudao.module.iot.plugin.http.upstream.router.IotDeviceUpstreamVertxHandler; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.BodyHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 设备下行服务端,接收来自 device 设备的请求,转发给 server 服务器 + * + * 协议:HTTP + * + * @author haohao + */ +@Slf4j +public class IotDeviceUpstreamServer { + + private final Vertx vertx; + private final HttpServer server; + private final IotPluginHttpProperties properties; + + public IotDeviceUpstreamServer(IotPluginHttpProperties properties, + IotDeviceUpstreamApi deviceUpstreamApi) { + this.properties = properties; + // 创建 Vertx 实例 + this.vertx = Vertx.vertx(); + // 创建 Router 实例 + Router router = Router.router(vertx); + router.route().handler(BodyHandler.create()); // 处理 Body + + // 使用统一的 Handler 处理所有上行请求 + IotDeviceUpstreamVertxHandler upstreamHandler = new IotDeviceUpstreamVertxHandler(deviceUpstreamApi); + router.post(IotDeviceUpstreamVertxHandler.PROPERTY_PATH).handler(upstreamHandler); + router.post(IotDeviceUpstreamVertxHandler.EVENT_PATH).handler(upstreamHandler); + + // 创建 HttpServer 实例 + this.server = vertx.createHttpServer().requestHandler(router); + } + + /** + * 启动 HTTP 服务器 + */ + public void start() { + log.info("[start][开始启动]"); + server.listen(properties.getServerPort()) + .toCompletionStage() + .toCompletableFuture() + .join(); + log.info("[start][启动完成,端口({})]", this.server.actualPort()); + } + + /** + * 停止所有 + */ + public void stop() { + log.info("[stop][开始关闭]"); + try { + // 关闭 HTTP 服务器 + if (server != null) { + server.close() + .toCompletionStage() + .toCompletableFuture() + .join(); + } + + // 关闭 Vertx 实例 + if (vertx != null) { + vertx.close() + .toCompletionStage() + .toCompletableFuture() + .join(); + } + log.info("[stop][关闭完成]"); + } catch (Exception e) { + log.error("[stop][关闭异常]", e); + throw new RuntimeException(e); + } + } +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/upstream/router/IotDeviceUpstreamVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/upstream/router/IotDeviceUpstreamVertxHandler.java new file mode 100644 index 000000000..79d465ea0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/upstream/router/IotDeviceUpstreamVertxHandler.java @@ -0,0 +1,188 @@ +package cn.iocoder.yudao.module.iot.plugin.http.upstream.router; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; + +/** + * IoT 设备上行统一处理的 Vert.x Handler + *

+ * 统一处理设备属性上报和事件上报的请求 + * + * @author haohao + */ +@RequiredArgsConstructor +@Slf4j +public class IotDeviceUpstreamVertxHandler implements Handler { + + // TODO @haohao:要不要类似 IotDeviceConfigSetVertxHandler 写的,把这些 PATH、METHOD 之类的抽走 + /** + * 属性上报路径 + */ + public static final String PROPERTY_PATH = "/sys/:productKey/:deviceName/thing/event/property/post"; + /** + * 事件上报路径 + */ + public static final String EVENT_PATH = "/sys/:productKey/:deviceName/thing/event/:identifier/post"; + + private static final String PROPERTY_METHOD = "thing.event.property.post"; + private static final String EVENT_METHOD_PREFIX = "thing.event."; + private static final String EVENT_METHOD_SUFFIX = ".post"; + + private final IotDeviceUpstreamApi deviceUpstreamApi; + + // TODO @haohao:要不要分成多个 Handler?每个只解决一个问题哈。 + @Override + public void handle(RoutingContext routingContext) { + String path = routingContext.request().path(); + String requestId = IdUtil.fastSimpleUUID(); + + try { + // 1. 解析通用参数 + String productKey = routingContext.pathParam("productKey"); + String deviceName = routingContext.pathParam("deviceName"); + JsonObject body = routingContext.body().asJsonObject(); + requestId = ObjUtil.defaultIfBlank(body.getString("id"), requestId); + + // 2. 根据路径模式处理不同类型的请求 + CommonResult result; + String method; + if (path.matches(".*/thing/event/property/post")) { + // 处理属性上报 + IotDevicePropertyReportReqDTO reportReqDTO = parsePropertyReportRequest(productKey, deviceName, requestId, body); + + // 设备上线 + updateDeviceState(reportReqDTO.getProductKey(), reportReqDTO.getDeviceName()); + + // 属性上报 + result = deviceUpstreamApi.reportDeviceProperty(reportReqDTO); + method = PROPERTY_METHOD; + } else if (path.matches(".*/thing/event/.+/post")) { + // 处理事件上报 + String identifier = routingContext.pathParam("identifier"); + IotDeviceEventReportReqDTO reportReqDTO = parseEventReportRequest(productKey, deviceName, identifier, requestId, body); + + // 设备上线 + updateDeviceState(reportReqDTO.getProductKey(), reportReqDTO.getDeviceName()); + + // 事件上报 + result = deviceUpstreamApi.reportDeviceEvent(reportReqDTO); + method = EVENT_METHOD_PREFIX + identifier + EVENT_METHOD_SUFFIX; + } else { + // 不支持的请求路径 + IotStandardResponse errorResponse = IotStandardResponse.error(requestId, "unknown", BAD_REQUEST.getCode(), "不支持的请求路径"); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + return; + } + + // 3. 返回标准响应 + IotStandardResponse response; + if (result.isSuccess()) { + response = IotStandardResponse.success(requestId, method, result.getData()); + } else { + response = IotStandardResponse.error(requestId, method, result.getCode(), result.getMsg()); + } + IotPluginCommonUtils.writeJsonResponse(routingContext, response); + } catch (Exception e) { + log.error("[handle][处理上行请求异常] path={}", path, e); + String method = path.contains("/property/") ? PROPERTY_METHOD + : EVENT_METHOD_PREFIX + (routingContext.pathParams().containsKey("identifier") + ? routingContext.pathParam("identifier") + : "unknown") + EVENT_METHOD_SUFFIX; + IotStandardResponse errorResponse = IotStandardResponse.error(requestId, method, INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + } + } + + /** + * 更新设备状态 + * + * @param productKey 产品 Key + * @param deviceName 设备名称 + */ + private void updateDeviceState(String productKey, String deviceName) { + deviceUpstreamApi.updateDeviceState(((IotDeviceStateUpdateReqDTO) new IotDeviceStateUpdateReqDTO() + .setRequestId(IdUtil.fastSimpleUUID()).setProcessId(IotPluginCommonUtils.getProcessId()).setReportTime(LocalDateTime.now()) + .setProductKey(productKey).setDeviceName(deviceName)).setState(IotDeviceStateEnum.ONLINE.getState())); + } + + /** + * 解析属性上报请求 + * + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @param requestId 请求 ID + * @param body 请求体 + * @return 属性上报请求 DTO + */ + @SuppressWarnings("unchecked") + private IotDevicePropertyReportReqDTO parsePropertyReportRequest(String productKey, String deviceName, String requestId, JsonObject body) { + // 按照标准 JSON 格式处理属性数据 + Map properties = new HashMap<>(); + Map params = body.getJsonObject("params") != null ? body.getJsonObject("params").getMap() : null; + if (params != null) { + // 将标准格式的 params 转换为平台需要的 properties 格式 + for (Map.Entry entry : params.entrySet()) { + String key = entry.getKey(); + Object valueObj = entry.getValue(); + // 如果是复杂结构(包含 value 和 time) + if (valueObj instanceof Map) { + Map valueMap = (Map) valueObj; + properties.put(key, valueMap.getOrDefault("value", valueObj)); + } else { + properties.put(key, valueObj); + } + } + } + + // 构建属性上报请求 DTO + return ((IotDevicePropertyReportReqDTO) new IotDevicePropertyReportReqDTO().setRequestId(requestId) + .setProcessId(IotPluginCommonUtils.getProcessId()).setReportTime(LocalDateTime.now()) + .setProductKey(productKey).setDeviceName(deviceName)).setProperties(properties); + } + + /** + * 解析事件上报请求 + * + * @param productKey 产品K ey + * @param deviceName 设备名称 + * @param identifier 事件标识符 + * @param requestId 请求 ID + * @param body 请求体 + * @return 事件上报请求 DTO + */ + private IotDeviceEventReportReqDTO parseEventReportRequest(String productKey, String deviceName, String identifier, String requestId, JsonObject body) { + // 按照标准 JSON 格式处理事件参数 + Map params; + if (body.containsKey("params")) { + params = body.getJsonObject("params").getMap(); + } else { + // 兼容旧格式 + params = new HashMap<>(); + } + + // 构建事件上报请求 DTO + return ((IotDeviceEventReportReqDTO) new IotDeviceEventReportReqDTO().setRequestId(requestId) + .setProcessId(IotPluginCommonUtils.getProcessId()).setReportTime(LocalDateTime.now()) + .setProductKey(productKey).setDeviceName(deviceName)).setIdentifier(identifier).setParams(params); + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/resources/application.yml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/resources/application.yml new file mode 100644 index 000000000..f195628a6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/resources/application.yml @@ -0,0 +1,13 @@ +spring: + application: + name: yudao-module-iot-plugin-http + +yudao: + iot: + plugin: + common: + upstream-url: http://127.0.0.1:48080 + downstream-port: 8093 + plugin-key: yudao-module-iot-plugin-http + http: + server-port: 8092 diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/plugin.properties b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/plugin.properties new file mode 100644 index 000000000..939e0f692 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/plugin.properties @@ -0,0 +1,7 @@ +plugin.id=mqtt-plugin +plugin.description=Vert.x MQTT plugin +plugin.class=cn.iocoder.yudao.module.iot.plugin.MqttPlugin +plugin.version=1.0.0 +plugin.requires= +plugin.provider=ahh +plugin.license=Apache-2.0 diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/pom.xml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/pom.xml new file mode 100644 index 000000000..f1fba5059 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/pom.xml @@ -0,0 +1,156 @@ + + + + yudao-module-iot-plugins + cn.iocoder.boot + ${revision} + + 4.0.0 + jar + + yudao-module-iot-plugin-mqtt + + ${project.artifactId} + + + 物联网 插件模块 - mqtt 插件 + + + + + mqtt-plugin + cn.iocoder.yudao.module.iot.plugin.MqttPlugin + 0.0.1 + ahh + mqtt-plugin-0.0.1 + + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.6 + + + unzip jar file + package + + + + + + + run + + + + + + + maven-assembly-plugin + 2.3 + + + + src/main/assembly/assembly.xml + + + false + + + + make-assembly + package + + attached + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + ${plugin.id} + ${plugin.class} + ${plugin.version} + ${plugin.provider} + ${plugin.description} + ${plugin.dependencies} + + + + + + + maven-deploy-plugin + + true + + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.pf4j + pf4j-spring + provided + + + + cn.iocoder.boot + yudao-module-iot-api + ${revision} + + + org.projectlombok + lombok + ${lombok.version} + provided + + + + io.vertx + vertx-mqtt + 4.5.11 + + + \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/assembly/assembly.xml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/assembly/assembly.xml new file mode 100644 index 000000000..daec9e431 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/assembly/assembly.xml @@ -0,0 +1,31 @@ + + plugin + + zip + + false + + + false + runtime + lib + + *:jar:* + + + + + + + target/plugin-classes + classes + + + diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttPlugin.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttPlugin.java new file mode 100644 index 000000000..7883fa8b1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttPlugin.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.plugin; + +import lombok.extern.slf4j.Slf4j; +import org.pf4j.Plugin; +import org.pf4j.PluginWrapper; + +// TODO @芋艿:暂未实现 +@Slf4j +public class MqttPlugin extends Plugin { + + private MqttServerExtension mqttServerExtension; + + public MqttPlugin(PluginWrapper wrapper) { + super(wrapper); + } + + @Override + public void start() { + log.info("MQTT Plugin started."); + mqttServerExtension = new MqttServerExtension(); + mqttServerExtension.startMqttServer(); + } + + @Override + public void stop() { + log.info("MQTT Plugin stopped."); + if (mqttServerExtension != null) { + mqttServerExtension.stopMqttServer().onComplete(ar -> { + if (ar.succeeded()) { + log.info("Stopped MQTT Server successfully"); + } else { + log.error("Failed to stop MQTT Server: {}", ar.cause().getMessage()); + } + }); + } + } +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttServerExtension.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttServerExtension.java new file mode 100644 index 000000000..dd0c5da37 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttServerExtension.java @@ -0,0 +1,232 @@ +package cn.iocoder.yudao.module.iot.plugin; + +import io.netty.handler.codec.mqtt.MqttProperties; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.mqtt.MqttEndpoint; +import io.vertx.mqtt.MqttServer; +import io.vertx.mqtt.MqttServerOptions; +import io.vertx.mqtt.MqttTopicSubscription; +import io.vertx.mqtt.messages.MqttDisconnectMessage; +import io.vertx.mqtt.messages.MqttPublishMessage; +import io.vertx.mqtt.messages.MqttSubscribeMessage; +import io.vertx.mqtt.messages.MqttUnsubscribeMessage; +import io.vertx.mqtt.messages.codes.MqttSubAckReasonCode; +import lombok.extern.slf4j.Slf4j; +import org.pf4j.Extension; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +// TODO @芋艿:暂未实现 +/** + * 根据官方示例,整合常见 MQTT 功能到 PF4J 的 Extension 类中 + */ +@Slf4j +@Extension +public class MqttServerExtension { + + private Vertx vertx; + private MqttServer mqttServer; + + /** + * 启动 MQTT 服务端 + * 可根据需要决定是否启用 SSL/TLS、WebSocket、多实例部署等 + */ + public void startMqttServer() { + // 初始化 Vert.x + vertx = Vertx.vertx(); + + // ========== 如果需要 SSL/TLS,请参考下面注释,启用注释并替换端口、证书路径等 ========== + // MqttServerOptions options = new MqttServerOptions() + // .setPort(8883) + // .setKeyCertOptions(new PemKeyCertOptions() + // .setKeyPath("./src/test/resources/tls/server-key.pem") + // .setCertPath("./src/test/resources/tls/server-cert.pem")) + // .setSsl(true); + + // ========== 如果需要 WebSocket,请设置 setUseWebSocket(true) ========== + // options.setUseWebSocket(true); + + // ========== 默认不启用 SSL 的示例 ========== + MqttServerOptions options = new MqttServerOptions() + .setPort(1883) + .setHost("0.0.0.0") + .setUseWebSocket(false); // 如果需要 WebSocket,请改为 true + + mqttServer = MqttServer.create(vertx, options); + + // 指定 endpointHandler,处理客户端连接等 + mqttServer.endpointHandler(endpoint -> { + handleClientConnect(endpoint); + handleDisconnect(endpoint); + handleSubscribe(endpoint); + handleUnsubscribe(endpoint); + handlePublish(endpoint); + handlePing(endpoint); + }); + + // 启动监听 + mqttServer.listen(ar -> { + if (ar.succeeded()) { + log.info("MQTT server is listening on port {}", mqttServer.actualPort()); + } else { + log.error("Error on starting the server", ar.cause()); + } + }); + } + + /** + * 优雅关闭 MQTT 服务端 + */ + public Future stopMqttServer() { + if (mqttServer != null) { + return mqttServer.close().onComplete(ar -> { + if (ar.succeeded()) { + log.info("MQTT server closed."); + if (vertx != null) { + vertx.close(); + log.info("Vert.x instance closed."); + } + } else { + log.error("Failed to close MQTT server: {}", ar.cause().getMessage()); + } + }); + } + return Future.succeededFuture(); + } + + // ==================== 以下为官方示例中常见事件的处理封装 ==================== + + /** + * 处理客户端连接 (CONNECT) + */ + private void handleClientConnect(MqttEndpoint endpoint) { + // 打印 CONNECT 的主要信息 + log.info("MQTT client [{}] request to connect, clean session = {}", + endpoint.clientIdentifier(), endpoint.isCleanSession()); + + if (endpoint.auth() != null) { + log.info("[username = {}, password = {}]", endpoint.auth().getUsername(), endpoint.auth().getPassword()); + } + log.info("[properties = {}]", endpoint.connectProperties()); + + if (endpoint.will() != null) { + log.info("[will topic = {}, msg = {}, QoS = {}, isRetain = {}]", + endpoint.will().getWillTopic(), + new String(endpoint.will().getWillMessageBytes()), + endpoint.will().getWillQos(), + endpoint.will().isWillRetain()); + } + + log.info("[keep alive timeout = {}]", endpoint.keepAliveTimeSeconds()); + + // 接受远程客户端的连接 + endpoint.accept(false); + } + + /** + * 处理客户端主动断开 (DISCONNECT) + */ + private void handleDisconnect(MqttEndpoint endpoint) { + endpoint.disconnectMessageHandler((MqttDisconnectMessage disconnectMessage) -> { + log.info("Received disconnect from client [{}], reason code = {}", + endpoint.clientIdentifier(), disconnectMessage.code()); + }); + } + + /** + * 处理客户端订阅 (SUBSCRIBE) + */ + private void handleSubscribe(MqttEndpoint endpoint) { + endpoint.subscribeHandler((MqttSubscribeMessage subscribe) -> { + List reasonCodes = new ArrayList<>(); + for (MqttTopicSubscription s : subscribe.topicSubscriptions()) { + log.info("Subscription for {} with QoS {}", s.topicName(), s.qualityOfService()); + // 将客户端请求的 QoS 转换为返回给客户端的 reason code(可能是错误码或实际 granted QoS) + reasonCodes.add(MqttSubAckReasonCode.qosGranted(s.qualityOfService())); + } + // 回复 SUBACK,MQTT 5.0 时可指定 reasonCodes、properties + endpoint.subscribeAcknowledge(subscribe.messageId(), reasonCodes, MqttProperties.NO_PROPERTIES); + }); + } + + /** + * 处理客户端取消订阅 (UNSUBSCRIBE) + */ + private void handleUnsubscribe(MqttEndpoint endpoint) { + endpoint.unsubscribeHandler((MqttUnsubscribeMessage unsubscribe) -> { + for (String topic : unsubscribe.topics()) { + log.info("Unsubscription for {}", topic); + } + // 回复 UNSUBACK,MQTT 5.0 时可指定 reasonCodes、properties + endpoint.unsubscribeAcknowledge(unsubscribe.messageId()); + }); + } + + /** + * 处理客户端发布的消息 (PUBLISH) + */ + private void handlePublish(MqttEndpoint endpoint) { + // 接收 PUBLISH 消息 + endpoint.publishHandler((MqttPublishMessage message) -> { + String payload = message.payload().toString(Charset.defaultCharset()); + log.info("Received message [{}] on topic [{}] with QoS [{}]", + payload, message.topicName(), message.qosLevel()); + + // 根据不同 QoS,回复客户端 + if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) { + endpoint.publishAcknowledge(message.messageId()); + } else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) { + endpoint.publishReceived(message.messageId()); + } + }); + + // 如果 QoS = 2,需要处理 PUBREL + endpoint.publishReleaseHandler(messageId -> { + endpoint.publishComplete(messageId); + }); + } + + /** + * 处理客户端 PINGREQ + */ + private void handlePing(MqttEndpoint endpoint) { + endpoint.pingHandler(v -> { + // 这里仅做日志, PINGRESP 已自动发送 + log.info("Ping received from client [{}]", endpoint.clientIdentifier()); + }); + } + + // ==================== 如果需要服务端向客户端发布消息,可用以下示例 ==================== + + /** + * 服务端主动向已连接的某个 endpoint 发布消息的示例 + * 如果使用 MQTT 5.0,可以传递更多消息属性 + */ + public void publishToClient(MqttEndpoint endpoint, String topic, String content) { + endpoint.publish(topic, + Buffer.buffer(content), + MqttQoS.AT_LEAST_ONCE, // QoS 自行选择 + false, + false); + + // 处理 QoS 1 和 QoS 2 的 ACK + endpoint.publishAcknowledgeHandler(messageId -> { + log.info("Received PUBACK from client [{}] for messageId = {}", endpoint.clientIdentifier(), messageId); + }).publishReceivedHandler(messageId -> { + endpoint.publishRelease(messageId); + }).publishCompletionHandler(messageId -> { + log.info("Received PUBCOMP from client [{}] for messageId = {}", endpoint.clientIdentifier(), messageId); + }); + } + + // ==================== 如果需要多实例部署,用于多核扩展,可参考以下思路 ==================== + // 例如,在宿主应用或插件中循环启动多个 MqttServerExtension 实例,或使用 Vert.x 的 deployVerticle: + // DeploymentOptions options = new DeploymentOptions().setInstances(10); + // vertx.deployVerticle(() -> new MyMqttVerticle(), options); + +} diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/history/ProductBrowseHistoryMapper.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/history/ProductBrowseHistoryMapper.java index 124357cac..8ea2ca510 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/history/ProductBrowseHistoryMapper.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/history/ProductBrowseHistoryMapper.java @@ -21,9 +21,8 @@ import java.util.Collection; public interface ProductBrowseHistoryMapper extends BaseMapperX { default ProductBrowseHistoryDO selectByUserIdAndSpuId(Long userId, Long spuId) { - return selectOne(new LambdaQueryWrapperX() - .eq(ProductBrowseHistoryDO::getUserId, userId) - .eq(ProductBrowseHistoryDO::getSpuId, spuId)); + return selectFirstOne(ProductBrowseHistoryDO::getUserId, userId, + ProductBrowseHistoryDO::getSpuId, spuId); } default PageResult selectPage(ProductBrowseHistoryPageReqVO reqVO) { diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java index 29b771126..3096a49f3 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java @@ -47,7 +47,7 @@ public interface CouponTemplateMapper extends BaseMapperX { } default List selectListByTakeType(Integer takeType) { - return selectList(CouponTemplateDO::getTakeType, takeType); + return selectList(CouponTemplateDO::getTakeType, takeType, CouponTemplateDO::getStatus, CommonStatusEnum.ENABLE.getStatus()); } default List selectList(List canTakeTypes, Integer productScope, Long productScopeValue, Integer count) { diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java index 060306f11..1c5db8288 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java @@ -121,6 +121,10 @@ public class CouponServiceImpl implements CouponService { @Transactional(rollbackFor = Exception.class) public Map> takeCoupon(Long templateId, Set userIds, CouponTakeTypeEnum takeType) { CouponTemplateDO template = couponTemplateService.getCouponTemplate(templateId); + return takeCoupon(template, userIds, takeType); + } + + private Map> takeCoupon(CouponTemplateDO template, Set userIds, CouponTakeTypeEnum takeType) { // 1. 过滤掉达到领取限制的用户 removeTakeLimitUser(userIds, template); // 2. 校验优惠劵是否可以领取 @@ -131,7 +135,7 @@ public class CouponServiceImpl implements CouponService { couponMapper.insertBatch(couponList); // 4. 增加优惠劵模板的领取数量 - couponTemplateService.updateCouponTemplateTakeCount(templateId, userIds.size()); + couponTemplateService.updateCouponTemplateTakeCount(template.getId(), userIds.size()); return convertMultiMap(couponList, CouponDO::getUserId, CouponDO::getId); } @@ -208,7 +212,7 @@ public class CouponServiceImpl implements CouponService { public void takeCouponByRegister(Long userId) { List templates = couponTemplateService.getCouponTemplateListByTakeType(CouponTakeTypeEnum.REGISTER); for (CouponTemplateDO template : templates) { - takeCoupon(template.getId(), CollUtil.newHashSet(userId), CouponTakeTypeEnum.REGISTER); + takeCoupon(template, CollUtil.newHashSet(userId), CouponTakeTypeEnum.REGISTER); } } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/config/TradeExpressProperties.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/config/TradeExpressProperties.java index 3d836bb17..9cd6ff793 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/config/TradeExpressProperties.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/config/TradeExpressProperties.java @@ -56,6 +56,15 @@ public class TradeExpressProperties { @NotEmpty(message = "快递鸟 Api Key 配置项不能为空") private String apiKey; + /** + * 接口指令 + * + * 1. 1002:免费版(只能查询申通、圆通快递) + * 2. 8001:付费版 + */ + @NotEmpty(message = "RequestType 配置项不能为空") + private String requestType = "1002"; + } /** diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/impl/kdniao/KdNiaoExpressClient.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/impl/kdniao/KdNiaoExpressClient.java index 24cf8e6ed..5ecd1c3f0 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/impl/kdniao/KdNiaoExpressClient.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/impl/kdniao/KdNiaoExpressClient.java @@ -39,11 +39,6 @@ public class KdNiaoExpressClient implements ExpressClient { private static final String REAL_TIME_QUERY_URL = "https://api.kdniao.com/Ebusiness/EbusinessOrderHandle.aspx"; - /** - * 快递鸟即时查询免费版 RequestType - */ - private static final String REAL_TIME_FREE_REQ_TYPE = "1002"; - private final RestTemplate restTemplate; private final TradeExpressProperties.KdNiaoConfig config; @@ -67,7 +62,7 @@ public class KdNiaoExpressClient implements ExpressClient { && StrUtil.length(reqDTO.getPhone()) >= 4) { requestDTO.setCustomerName(StrUtil.subSufByLength(reqDTO.getPhone(), 4)); } - KdNiaoExpressQueryRespDTO respDTO = httpRequest(REAL_TIME_QUERY_URL, REAL_TIME_FREE_REQ_TYPE, + KdNiaoExpressQueryRespDTO respDTO = httpRequest(REAL_TIME_QUERY_URL, config.getRequestType(), requestDTO, KdNiaoExpressQueryRespDTO.class); // 处理结果 diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDiscountActivityPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDiscountActivityPriceCalculator.java index 801c7c018..bf8651af5 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDiscountActivityPriceCalculator.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDiscountActivityPriceCalculator.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.trade.service.price.calculator; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.module.member.api.level.MemberLevelApi; import cn.iocoder.yudao.module.member.api.level.dto.MemberLevelRespDTO; import cn.iocoder.yudao.module.member.api.user.MemberUserApi; @@ -141,7 +142,9 @@ public class TradeDiscountActivityPriceCalculator implements TradePriceCalculato */ public Integer calculateVipPrice(MemberLevelRespDTO level, TradePriceCalculateRespBO.OrderItem orderItem) { - if (level == null || level.getDiscountPercent() == null) { + if (level == null + || CommonStatusEnum.isDisable(level.getStatus()) + || level.getDiscountPercent() == null) { return 0; } Integer newPrice = calculateRatePrice(orderItem.getPayPrice(), level.getDiscountPercent().doubleValue()); diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyLogDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyLogDO.java index a482605d5..384628370 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyLogDO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyLogDO.java @@ -44,7 +44,7 @@ public class PayNotifyLogDO extends BaseDO { /** * 支付通知状态 * - * 外键 {@link PayNotifyStatusEnum} + * 枚举 {@link PayNotifyStatusEnum} */ private Integer status; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java index 7bfabad3f..9f9ee7fef 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java @@ -52,7 +52,7 @@ public class PayNotifyTaskDO extends TenantBaseDO { /** * 通知类型 * - * 外键 {@link PayNotifyTypeEnum} + * 枚举 {@link PayNotifyTypeEnum} */ private Integer type; /** @@ -73,7 +73,7 @@ public class PayNotifyTaskDO extends TenantBaseDO { /** * 通知状态 * - * 外键 {@link PayNotifyStatusEnum} + * 枚举 {@link PayNotifyStatusEnum} */ private Integer status; /** diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxAppPayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxAppPayClient.java index 396694a75..ec88aa73a 100644 --- a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxAppPayClient.java +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxAppPayClient.java @@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; -import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult; +import com.github.binarywang.wxpay.bean.order.WxPayAppOrderResult; import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest; import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result; @@ -41,7 +41,7 @@ public class WxAppPayClient extends AbstractWxPayClient { // 构建 WxPayUnifiedOrderRequest 对象 WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO); // 执行请求 - WxPayMpOrderResult response = client.createOrder(request); + WxPayAppOrderResult response = client.createOrder(request); // 转换结果 return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response), diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java index a3cc1cefa..6db0e567c 100644 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java @@ -24,6 +24,7 @@ public interface ErrorCodeConstants { ErrorCode MENU_NOT_EXISTS = new ErrorCode(1_002_001_003, "菜单不存在"); ErrorCode MENU_EXISTS_CHILDREN = new ErrorCode(1_002_001_004, "存在子菜单,无法删除"); ErrorCode MENU_PARENT_NOT_DIR_OR_MENU = new ErrorCode(1_002_001_005, "父菜单的类型必须是目录或者菜单"); + ErrorCode MENU_COMPONENT_NAME_DUPLICATE = new ErrorCode(1_002_001_006, "已经存在该组件名的菜单"); // ========== 角色模块 1-002-002-000 ========== ErrorCode ROLE_NOT_EXISTS = new ErrorCode(1_002_002_000, "角色不存在"); diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApiImpl.java index 6af7bcd42..4f7b4e468 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApiImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApiImpl.java @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.system.api.user; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import cn.iocoder.yudao.framework.datapermission.core.util.DataPermissionUtils; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; @@ -12,7 +12,10 @@ import cn.iocoder.yudao.module.system.service.user.AdminUserService; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; -import java.util.*; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; @@ -56,10 +59,11 @@ public class AdminUserApiImpl implements AdminUserApi { } @Override - @DataPermission(enable = false) // 禁用数据权限。原因是,一般基于指定 id 的 API 查询,都是数据拼接为主 public List getUserList(Collection ids) { - List users = userService.getUserList(ids); - return BeanUtils.toBean(users, AdminUserRespDTO.class); + return DataPermissionUtils.executeIgnore(() -> { // 禁用数据权限。原因是,一般基于指定 id 的 API 查询,都是数据拼接为主 + List users = userService.getUserList(ids); + return BeanUtils.toBean(users, AdminUserRespDTO.class); + }); } @Override diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java index 1e7a99a32..d9269470d 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java @@ -7,14 +7,7 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.security.config.SecurityProperties; import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthLoginReqVO; -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthLoginRespVO; -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthPermissionInfoRespVO; -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthRegisterReqVO; -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthResetPasswordReqVO; -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthSmsLoginReqVO; -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthSmsSendReqVO; -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthSocialLoginReqVO; +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*; import cn.iocoder.yudao.module.system.convert.auth.AuthConvert; import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; @@ -36,12 +29,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.Collections; import java.util.List; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java index b6d067f1f..824fe4104 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java @@ -67,8 +67,8 @@ public class MenuController { } @GetMapping({"/list-all-simple", "simple-list"}) - @Operation(summary = "获取菜单精简信息列表", description = "只包含被开启的菜单,用于【角色分配菜单】功能的选项。" + - "在多租户的场景下,会只返回租户所在套餐有的菜单") + @Operation(summary = "获取菜单精简信息列表", + description = "只包含被开启的菜单,用于【角色分配菜单】功能的选项。在多租户的场景下,会只返回租户所在套餐有的菜单") public CommonResult> getSimpleMenuList() { List list = menuService.getMenuListByTenant( new MenuListReqVO().setStatus(CommonStatusEnum.ENABLE.getStatus())); diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java index 090c6a80f..f698b92d0 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.system.controller.admin.tenant; 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; @@ -9,7 +10,6 @@ import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantRespVO; import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO; -import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantSimpleRespVO; import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO; import cn.iocoder.yudao.module.system.service.tenant.TenantService; import io.swagger.v3.oas.annotations.Operation; @@ -27,6 +27,7 @@ import java.util.List; import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; @Tag(name = "管理后台 - 租户") @RestController @@ -45,13 +46,25 @@ public class TenantController { return success(tenant != null ? tenant.getId() : null); } + @GetMapping({ "simple-list" }) + @PermitAll + @Operation(summary = "获取租户精简信息列表", description = "只包含被开启的租户,用于【首页】功能的选择租户选项") + public CommonResult> getTenantSimpleList() { + List list = tenantService.getTenantListByStatus(CommonStatusEnum.ENABLE.getStatus()); + return success(convertList(list, tenantDO -> + new TenantRespVO().setId(tenantDO.getId()).setName(tenantDO.getName()))); + } + @GetMapping("/get-by-website") @PermitAll @Operation(summary = "使用域名,获得租户信息", description = "登录界面,根据用户的域名,获得租户信息") @Parameter(name = "website", description = "域名", required = true, example = "www.iocoder.cn") - public CommonResult getTenantByWebsite(@RequestParam("website") String website) { + public CommonResult getTenantByWebsite(@RequestParam("website") String website) { TenantDO tenant = tenantService.getTenantByWebsite(website); - return success(BeanUtils.toBean(tenant, TenantSimpleRespVO.class)); + if (tenant == null || CommonStatusEnum.isDisable(tenant.getStatus())) { + return success(null); + } + return success(new TenantRespVO().setId(tenant.getId()).setName(tenant.getName())); } @PostMapping("/create") @@ -99,8 +112,7 @@ public class TenantController { @Operation(summary = "导出租户 Excel") @PreAuthorize("@ss.hasPermission('system:tenant:export')") @ApiAccessLog(operateType = EXPORT) - public void exportTenantExcel(@Valid TenantPageReqVO exportReqVO, - HttpServletResponse response) throws IOException { + public void exportTenantExcel(@Valid TenantPageReqVO exportReqVO, HttpServletResponse response) throws IOException { exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List list = tenantService.getTenantPage(exportReqVO).getList(); // 导出 Excel diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantSimpleRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantSimpleRespVO.java deleted file mode 100755 index 49752278d..000000000 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantSimpleRespVO.java +++ /dev/null @@ -1,16 +0,0 @@ -package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Schema(description = "管理后台 - 租户精简 Response VO") -@Data -public class TenantSimpleRespVO { - - @Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") - private Long id; - - @Schema(description = "租户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") - private String name; - -} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/MenuMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/MenuMapper.java index 8458faa67..31eb117d2 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/MenuMapper.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/MenuMapper.java @@ -28,4 +28,9 @@ public interface MenuMapper extends BaseMapperX { default List selectListByPermission(String permission) { return selectList(MenuDO::getPermission, permission); } + + default MenuDO selectByComponentName(String componentName) { + return selectOne(MenuDO::getComponentName, componentName); + } + } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserMapper.java index af30ecee2..a90e6ac2a 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserMapper.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserMapper.java @@ -5,23 +5,20 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.system.controller.admin.socail.vo.user.SocialUserPageReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserDO; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface SocialUserMapper extends BaseMapperX { default SocialUserDO selectByTypeAndCodeAnState(Integer type, String code, String state) { - return selectOne(new LambdaQueryWrapper() - .eq(SocialUserDO::getType, type) - .eq(SocialUserDO::getCode, code) - .eq(SocialUserDO::getState, state)); + return selectOne(SocialUserDO::getType, type, + SocialUserDO::getCode, code, + SocialUserDO::getState, state); } default SocialUserDO selectByTypeAndOpenid(Integer type, String openid) { - return selectOne(new LambdaQueryWrapper() - .eq(SocialUserDO::getType, type) - .eq(SocialUserDO::getOpenid, openid)); + return selectFirstOne(SocialUserDO::getType, type, + SocialUserDO::getOpenid, openid); } default PageResult selectPage(SocialUserPageReqVO reqVO) { diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/tenant/TenantMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/tenant/TenantMapper.java index aaca0160a..8ddf06065 100755 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/tenant/TenantMapper.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/tenant/TenantMapper.java @@ -43,4 +43,8 @@ public interface TenantMapper extends BaseMapperX { return selectList(TenantDO::getPackageId, packageId); } + default List selectListByStatus(Integer status) { + return selectList(TenantDO::getStatus, status); + } + } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java index 558dbdef2..3dd12491a 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java @@ -184,7 +184,7 @@ public class AliyunSmsClient extends AbstractSmsClient { @SneakyThrows private static String percentCode(String str) { Assert.notNull(str, "str 不能为空"); - return URLEncoder.encode(str, StandardCharsets.UTF_8.name()) + return HttpUtils.encodeUtf8(str) .replace("+", "%20") // 加号 "+" 被替换为 "%20" .replace("*", "%2A") // 星号 "*" 被替换为 "%2A" .replace("%7E", "~"); // 波浪号 "%7E" 被替换为 "~" diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java index 82f55395e..622f8ac1b 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.date.format.FastDateFormat; import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.CharsetUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.SecureUtil; import cn.hutool.http.HttpUtil; @@ -19,8 +18,6 @@ import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditS import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; import lombok.extern.slf4j.Slf4j; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.LocalDateTime; @@ -156,10 +153,9 @@ public class HuaweiSmsClient extends AbstractSmsClient { .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(null); } - @SuppressWarnings("CharsetObjectCanBeUsed") - private static void appendToBody(StringBuilder body, String key, String value) throws UnsupportedEncodingException { + private static void appendToBody(StringBuilder body, String key, String value) { if (StrUtil.isNotEmpty(value)) { - body.append(key).append(URLEncoder.encode(value, CharsetUtil.CHARSET_UTF_8.name())); + body.append(key).append(HttpUtils.encodeUtf8(value)); } } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java index 98052eb65..5378108dd 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.system.service.permission; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuListReqVO; @@ -53,7 +54,8 @@ public class MenuServiceImpl implements MenuService { // 校验父菜单存在 validateParentMenu(createReqVO.getParentId(), null); // 校验菜单(自己) - validateMenu(createReqVO.getParentId(), createReqVO.getName(), null); + validateMenuName(createReqVO.getParentId(), createReqVO.getName(), null); + validateMenuComponentName(createReqVO.getComponentName(), null); // 插入数据库 MenuDO menu = BeanUtils.toBean(createReqVO, MenuDO.class); @@ -74,7 +76,8 @@ public class MenuServiceImpl implements MenuService { // 校验父菜单存在 validateParentMenu(updateReqVO.getParentId(), updateReqVO.getId()); // 校验菜单(自己) - validateMenu(updateReqVO.getParentId(), updateReqVO.getName(), updateReqVO.getId()); + validateMenuName(updateReqVO.getParentId(), updateReqVO.getName(), updateReqVO.getId()); + validateMenuComponentName(updateReqVO.getComponentName(), updateReqVO.getId()); // 更新到数据库 MenuDO updateObj = BeanUtils.toBean(updateReqVO, MenuDO.class); @@ -228,7 +231,7 @@ public class MenuServiceImpl implements MenuService { * @param id 菜单编号 */ @VisibleForTesting - void validateMenu(Long parentId, String name, Long id) { + void validateMenuName(Long parentId, String name, Long id) { MenuDO menu = menuMapper.selectByParentIdAndName(parentId, name); if (menu == null) { return; @@ -242,6 +245,30 @@ public class MenuServiceImpl implements MenuService { } } + /** + * 校验菜单组件名是否合法 + * + * @param componentName 组件名 + * @param id 菜单编号 + */ + @VisibleForTesting + void validateMenuComponentName(String componentName, Long id) { + if (StrUtil.isBlank(componentName)) { + return; + } + MenuDO menu = menuMapper.selectByComponentName(componentName); + if (menu == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的菜单 + if (id == null) { + return; + } + if (!menu.getId().equals(id)) { + throw exception(MENU_COMPONENT_NAME_DUPLICATE); + } + } + /** * 初始化菜单的通用属性。 *

diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantService.java index e0bd9d291..425d18d60 100755 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantService.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantService.java @@ -38,7 +38,7 @@ public interface TenantService { * 更新租户的角色菜单 * * @param tenantId 租户编号 - * @param menuIds 菜单编号数组 + * @param menuIds 菜单编号数组 */ void updateTenantRoleMenu(Long tenantId, Set menuIds); @@ -97,6 +97,14 @@ public interface TenantService { */ List getTenantListByPackageId(Long packageId); + /** + * 获得指定状态的租户列表 + * + * @param status 状态 + * @return 租户列表 + */ + List getTenantListByStatus(Integer status); + /** * 进行租户的信息处理逻辑 * 其中,租户编号从 {@link TenantContextHolder} 上下文中获取 diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImpl.java index b5b3c358e..c3b09ec5a 100755 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImpl.java @@ -265,6 +265,11 @@ public class TenantServiceImpl implements TenantService { return tenantMapper.selectListByPackageId(packageId); } + @Override + public List getTenantListByStatus(Integer status) { + return tenantMapper.selectListByStatus(status); + } + @Override public void handleTenantInfo(TenantInfoHandler handler) { // 如果禁用,则不执行逻辑 diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java index 093060e84..b04f426dc 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java @@ -12,6 +12,7 @@ import com.google.common.collect.Lists; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.MockedStatic; +import org.mockito.stubbing.Answer; import java.time.LocalDateTime; import java.util.List; @@ -50,6 +51,8 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest { // mock 方法 httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) .thenReturn("{\"Message\":\"OK\",\"RequestId\":\"30067CE9-3710-5984-8881-909B21D8DB28\",\"Code\":\"OK\",\"BizId\":\"800025323183427988\"}"); + httpUtilsMockedStatic.when(() -> HttpUtils.encodeUtf8(anyString())) + .then((Answer) invocationOnMock -> (String) invocationOnMock.getArguments()[0]); // 调用 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, @@ -75,6 +78,8 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest { // mock 方法 httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) .thenReturn("{\"Message\":\"手机号码格式错误\",\"RequestId\":\"B7700B8E-227E-5886-9564-26036172F01F\",\"Code\":\"isv.MOBILE_NUMBER_ILLEGAL\"}"); + httpUtilsMockedStatic.when(() -> HttpUtils.encodeUtf8(anyString())) + .then((Answer) invocationOnMock -> (String) invocationOnMock.getArguments()[0]); // 调用 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams); @@ -127,6 +132,8 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest { // mock 方法 httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) .thenReturn("{\"TemplateCode\":\"SMS_207945135\",\"RequestId\":\"6F4CC077-29C8-5BA5-AB62-5FF95068A5AC\",\"Message\":\"OK\",\"TemplateContent\":\"您的验证码${code},该验证码5分钟内有效,请勿泄漏于他人!\",\"TemplateName\":\"公告通知\",\"TemplateType\":0,\"Code\":\"OK\",\"CreateDate\":\"2020-12-23 17:34:42\",\"Reason\":\"无审批备注\",\"TemplateStatus\":1}"); + httpUtilsMockedStatic.when(() -> HttpUtils.encodeUtf8(anyString())) + .then((Answer) invocationOnMock -> (String) invocationOnMock.getArguments()[0]); // 调用 SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId); diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImplTest.java index 27f1efb36..cc393796c 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImplTest.java @@ -275,7 +275,7 @@ public class MenuServiceImplTest extends BaseDbUnitTest { } @Test - public void testValidateMenu_success() { + public void testValidateMenu_Name_success() { // mock 父子菜单 MenuDO sonMenu = createParentAndSonMenu(); // 准备参数 @@ -284,11 +284,11 @@ public class MenuServiceImplTest extends BaseDbUnitTest { String otherSonMenuName = randomString(); // 调用,无需断言 - menuService.validateMenu(parentId, otherSonMenuName, otherSonMenuId); + menuService.validateMenuName(parentId, otherSonMenuName, otherSonMenuId); } @Test - public void testValidateMenu_sonMenuNameDuplicate() { + public void testValidateMenu_sonMenuNameNameDuplicate() { // mock 父子菜单 MenuDO sonMenu = createParentAndSonMenu(); // 准备参数 @@ -297,7 +297,7 @@ public class MenuServiceImplTest extends BaseDbUnitTest { String otherSonMenuName = sonMenu.getName(); //相同名称 // 调用,并断言异常 - assertServiceException(() -> menuService.validateMenu(parentId, otherSonMenuName, otherSonMenuId), + assertServiceException(() -> menuService.validateMenuName(parentId, otherSonMenuName, otherSonMenuId), MENU_NAME_DUPLICATE); } diff --git a/yudao-server/src/main/java/cn/iocoder/yudao/server/controller/DefaultController.java b/yudao-server/src/main/java/cn/iocoder/yudao/server/controller/DefaultController.java index 2bf6e5277..a9ed8fbf4 100644 --- a/yudao-server/src/main/java/cn/iocoder/yudao/server/controller/DefaultController.java +++ b/yudao-server/src/main/java/cn/iocoder/yudao/server/controller/DefaultController.java @@ -1,6 +1,10 @@ package cn.iocoder.yudao.server.controller; import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import jakarta.annotation.security.PermitAll; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -13,6 +17,7 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC * @author 芋道源码 */ @RestController +@Slf4j public class DefaultController { @RequestMapping("/admin-api/bpm/**") @@ -27,9 +32,9 @@ public class DefaultController { "[微信公众号 yudao-module-mp - 已禁用][参考 https://doc.iocoder.cn/mp/build/ 开启]"); } - @RequestMapping(value = {"/admin-api/product/**", // 商品中心 + @RequestMapping(value = { "/admin-api/product/**", // 商品中心 "/admin-api/trade/**", // 交易中心 - "/admin-api/promotion/**"}) // 营销中心 + "/admin-api/promotion/**" }) // 营销中心 public CommonResult mall404() { return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[商城系统 yudao-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]"); @@ -47,28 +52,43 @@ public class DefaultController { "[CRM 模块 yudao-module-crm - 已禁用][参考 https://doc.iocoder.cn/crm/build/ 开启]"); } - @RequestMapping(value = {"/admin-api/report/**"}) + @RequestMapping(value = { "/admin-api/report/**"}) public CommonResult report404() { return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[报表模块 yudao-module-report - 已禁用][参考 https://doc.iocoder.cn/report/ 开启]"); } - @RequestMapping(value = {"/admin-api/pay/**"}) + @RequestMapping(value = { "/admin-api/pay/**"}) public CommonResult pay404() { return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[支付模块 yudao-module-pay - 已禁用][参考 https://doc.iocoder.cn/pay/build/ 开启]"); } - @RequestMapping(value = {"/admin-api/ai/**"}) + @RequestMapping(value = { "/admin-api/ai/**"}) public CommonResult ai404() { return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[AI 大模型 yudao-module-ai - 已禁用][参考 https://doc.iocoder.cn/ai/build/ 开启]"); } - @RequestMapping(value = {"/admin-api/iot/**"}) + @RequestMapping(value = { "/admin-api/iot/**"}) public CommonResult iot404() { return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[IOT 物联网 yudao-module-iot - 已禁用][参考 https://doc.iocoder.cn/iot/build/ 开启]"); + "[IoT 物联网 yudao-module-iot - 已禁用][参考 https://doc.iocoder.cn/iot/build/ 开启]"); + } + + /** + * 测试接口:打印 query、header、body + */ + @RequestMapping(value = { "/test" }) + @PermitAll + public CommonResult test(HttpServletRequest request) { + // 打印查询参数 + log.info("Query: {}", ServletUtils.getParamMap(request)); + // 打印请求头 + log.info("Header: {}", ServletUtils.getHeaderMap(request)); + // 打印请求体 + log.info("Body: {}", ServletUtils.getBody(request)); + return CommonResult.success(true); } } diff --git a/yudao-server/src/main/resources/application-dev.yaml b/yudao-server/src/main/resources/application-dev.yaml index 9377af95c..7ca9ec6cc 100644 --- a/yudao-server/src/main/resources/application-dev.yaml +++ b/yudao-server/src/main/resources/application-dev.yaml @@ -4,11 +4,10 @@ server: --- #################### 数据库相关配置 #################### spring: - spring: - autoconfigure: - exclude: - - org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建 - - org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建 + autoconfigure: + exclude: + - org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建 + - org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建 # 数据源配置项 datasource: druid: # Druid 【监控】相关的全局配置 @@ -218,4 +217,9 @@ iot: # 保持连接 keepalive: 60 # 清除会话(设置为false,断开连接,重连后使用原来的会话 保留订阅的主题,能接收离线期间的消息) - clearSession: true \ No newline at end of file + clearSession: true + + +# 插件配置 +pf4j: + pluginsDir: ${user.home}/plugins # 插件目录 \ No newline at end of file diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index 60564f094..77438ef2a 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -68,6 +68,13 @@ spring: url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true username: root password: 123456 +# tdengine: # IoT 数据库(需要 IoT 物联网再开启噢!) +# url: jdbc:TAOS-RS://127.0.0.1:6041/ruoyi_vue_pro +# driver-class-name: com.taosdata.jdbc.rs.RestfulDriver +# username: root +# password: taosdata +# druid: +# validation-query: SELECT SERVER_STATUS() # TDengine 数据源的有效性检查 SQL # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 data: @@ -75,7 +82,7 @@ spring: host: 127.0.0.1 # 地址 port: 6379 # 端口 database: 0 # 数据库索引 -# password: dev # 密码,建议生产环境开启 + # password: dev # 密码,建议生产环境开启 --- #################### 定时任务相关配置 #################### @@ -177,6 +184,7 @@ logging: cn.iocoder.yudao.module.crm.dal.mysql: debug cn.iocoder.yudao.module.erp.dal.mysql: debug cn.iocoder.yudao.module.iot.dal.mysql: debug + cn.iocoder.yudao.module.iot.dal.tdengine: DEBUG cn.iocoder.yudao.module.ai.dal.mysql: debug org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO 芋艿:先禁用,Spring Boot 3.X 存在部分错误的 WARN 提示 @@ -258,20 +266,7 @@ justauth: prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE:: timeout: 24h # 超时时长,目前只对 Redis 缓存生效,默认 3 分钟 ---- #################### iot相关配置 TODO 芋艿:再瞅瞅 #################### -iot: - emq: - # 账号 - username: anhaohao - # 密码 - password: ahh@123456 - # 主机地址 - hostUrl: tcp://chaojiniu.top:1883 - # 客户端Id,不能相同,采用随机数 ${random.value} - client-id: ${random.int} - # 默认主题 - default-topic: test - # 保持连接 - keepalive: 60 - # 清除会话(设置为false,断开连接,重连后使用原来的会话 保留订阅的主题,能接收离线期间的消息) - clearSession: true \ No newline at end of file +--- #################### iot相关配置 TODO 芋艿【IOT】:再瞅瞅 #################### +pf4j: +# pluginsDir: /tmp/ + pluginsDir: ../plugins \ No newline at end of file diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 8c906e389..57b3b87d0 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -48,7 +48,7 @@ springdoc: default-flat-param-object: true # 参见 https://doc.xiaominfo.com/docs/faq/v4/knife4j-parameterobject-flat-param 文档 knife4j: - enable: true + enable: false # TODO 芋艿:需要关闭增强,具体原因见:https://github.com/xiaoymin/knife4j/issues/874 setting: language: zh_cn @@ -212,6 +212,10 @@ yudao: appKey: 75b161ed2aef4719b275d6e7f2a4d4cd secretKey: YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz model: generalv3.5 + baichuan: # 百川智能 + enable: true + api-key: sk-abc + model: Baichuan4-Turbo midjourney: enable: true # base-url: https://api.holdai.top/mj-relax/mj @@ -271,6 +275,7 @@ yudao: ignore-urls: - /admin-api/system/tenant/get-id-by-name # 基于名字获取租户,不许带租户编号 - /admin-api/system/tenant/get-by-website # 基于域名获取租户,不许带租户编号 + - /admin-api/system/tenant/simple-list # 获取租户列表,不许带租户编号 - /admin-api/system/captcha/get # 获取图片验证码,和租户无关 - /admin-api/system/captcha/check # 校验图片验证码,和租户无关 - /admin-api/infra/file/*/get/** # 获取图片,和租户无关 @@ -303,6 +308,8 @@ yudao: - infra_job - infra_job_log - infra_job_log + - iot_plugin_info + - iot_plugin_instance - infra_data_source_config - jimu_dict - jimu_dict_item @@ -328,6 +335,8 @@ yudao: - mail_account - mail_template - sms_template + - iot:device + - iot:thing_model_list sms-code: # 短信验证码相关的配置项 expire-times: 10m send-frequency: 1m @@ -340,12 +349,16 @@ yudao: receive-expire-time: 14d # 收货的过期时间 comment-expire-time: 7d # 评论的过期时间 express: - client: kd_niao + client: KD_NIAO kd-niao: api-key: cb022f1e-48f1-4c4a-a723-9001ac9676b8 business-id: 1809751 + request-type: 1002 # 免费版 1002;付费版 8001 kd100: key: pLXUGAwK5305 customer: E77DF18BE109F454A5CD319E44BF5177 -debug: false \ No newline at end of file +debug: false +# 插件配置 TODO 芋艿:【IOT】需要处理下 +pf4j: + pluginsDir: /Users/anhaohao/code/gitee/ruoyi-vue-pro/plugins # 插件目录 \ No newline at end of file