diff --git a/pom.xml b/pom.xml index 924692c..6f06c07 100644 --- a/pom.xml +++ b/pom.xml @@ -306,12 +306,6 @@ ${hutool-all.version} - - org.springframework.boot - spring-boot-starter-test - ${springboottest.version} - - javax.servlet javax.servlet-api diff --git a/sql/Device.sql b/sql/Device.sql index b60d6f5..ae0fc39 100644 --- a/sql/Device.sql +++ b/sql/Device.sql @@ -1,17 +1,20 @@ -- 创建数据库 -CREATE DATABASE IF NOT EXISTS `storm-device`; +CREATE DATABASE IF NOT EXISTS `storm-device` +DEFAULT CHARACTER SET utf8mb4 +COLLATE utf8mb4_unicode_ci; + USE `storm-device`; SET NAMES utf8mb4; --- 设备表(主表) +-- 设备表修改 CREATE TABLE device ( - id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', device_id VARCHAR(255) NOT NULL COMMENT '设备ID', device_name VARCHAR(255) DEFAULT NULL COMMENT '设备名称', product_id VARCHAR(255) DEFAULT NULL COMMENT '产品ID', product_name VARCHAR(255) DEFAULT NULL COMMENT '产品名称', device_type INT DEFAULT NULL COMMENT '设备类型(1:RS-LL-X1, 2:RS-LL-X2)', - STATUS VARCHAR(50) DEFAULT 'OFFLINE' COMMENT '设备在线状态(ONLINE/OFFLINE)', + status VARCHAR(50) DEFAULT 'OFFLINE' COMMENT '设备在线状态(ONLINE/OFFLINE)', -- 位置信息 location VARCHAR(255) DEFAULT NULL COMMENT '安装位置', @@ -27,13 +30,13 @@ CREATE TABLE device ( activation_code VARCHAR(255) DEFAULT NULL COMMENT '激活码', serial_number VARCHAR(255) DEFAULT NULL COMMENT '序列号', license_status VARCHAR(50) DEFAULT NULL COMMENT '许可证状态', - license_expire_time DATETIME COMMENT '许可证过期时间', - last_massage_time DATETIME COMMENT '最后消息时间', + license_expire_time DATETIME DEFAULT NULL COMMENT '许可证过期时间', + last_massage_time DATETIME DEFAULT NULL COMMENT '最后按摩时间', -- 在线状态时间记录 - last_online_time DATETIME COMMENT '最后上线时间', - last_offline_time DATETIME COMMENT '最后下线时间', - activation_time DATETIME COMMENT '激活时间', + last_online_time DATETIME DEFAULT NULL COMMENT '最后上线时间', + last_offline_time DATETIME DEFAULT NULL COMMENT '最后下线时间', + activation_time DATETIME DEFAULT NULL COMMENT '激活时间', -- 运行时长统计(秒) total_online_duration BIGINT DEFAULT 0 COMMENT '累计在线时长(秒)', @@ -46,29 +49,28 @@ CREATE TABLE device ( remark TEXT COMMENT '备注', -- 系统字段 - is_deleted TINYINT(1) DEFAULT '0' COMMENT '是否删除(0-未删除, 1-已删除)', + is_deleted TINYINT(1) DEFAULT 0 COMMENT '是否删除(0-未删除, 1-已删除)', create_by VARCHAR(255) DEFAULT NULL COMMENT '创建人', update_by VARCHAR(255) DEFAULT NULL COMMENT '更新人', - create_time DATETIME DEFAULT NULL COMMENT '创建时间', - update_time DATETIME DEFAULT NULL COMMENT '更新时间', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (id), UNIQUE KEY uk_device_id (device_id), - KEY idx_device_id (device_id), KEY idx_product_id (product_id), KEY idx_shop_id (shop_id), - KEY idx_status (STATUS), - KEY idx_create_time (create_time), + KEY idx_status (status), KEY idx_license_status (license_status), - KEY idx_last_online_time (last_online_time) + KEY idx_last_online_time (last_online_time), + KEY idx_create_time (create_time) ) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备表'; --- 设备状态日志表 +-- 设备状态日志表修改 CREATE TABLE device_status_log ( - id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', device_id VARCHAR(255) NOT NULL COMMENT '设备ID', - STATUS VARCHAR(255) DEFAULT NULL COMMENT '状态', - event_type VARCHAR(255) DEFAULT NULL COMMENT '事件类型', + status VARCHAR(50) DEFAULT NULL COMMENT '状态', + event_type VARCHAR(100) DEFAULT NULL COMMENT '事件类型', -- 毫秒时间戳字段 massage_start_time BIGINT DEFAULT NULL COMMENT '按摩开始时间(毫秒时间戳)', @@ -76,39 +78,44 @@ CREATE TABLE device_status_log ( massage_duration BIGINT DEFAULT NULL COMMENT '按摩持续时间(毫秒)', -- 按摩相关信息 - head_type VARCHAR(255) DEFAULT NULL COMMENT '按摩头类型', - body_part VARCHAR(255) DEFAULT NULL COMMENT '身体部位', + head_type VARCHAR(100) DEFAULT NULL COMMENT '按摩头类型', + body_part VARCHAR(100) DEFAULT NULL COMMENT '身体部位', massage_plan VARCHAR(255) DEFAULT NULL COMMENT '按摩方案', -- 时间字段 event_time DATETIME DEFAULT NULL COMMENT '事件时间', last_online_time DATETIME DEFAULT NULL COMMENT '最后在线时间', - duration INT(11) DEFAULT NULL COMMENT '持续时间(秒)', + duration INT DEFAULT NULL COMMENT '持续时间(秒)', + + -- 描述和备注 + description TEXT COMMENT '描述', + remark TEXT COMMENT '备注', -- 系统字段 - is_deleted TINYINT(1) DEFAULT '0' COMMENT '是否删除(0-未删除, 1-已删除)', + is_deleted TINYINT(1) DEFAULT 0 COMMENT '是否删除(0-未删除, 1-已删除)', create_by VARCHAR(255) DEFAULT NULL COMMENT '创建人', update_by VARCHAR(255) DEFAULT NULL COMMENT '更新人', - create_time DATETIME DEFAULT NULL COMMENT '创建时间', - update_time DATETIME DEFAULT NULL COMMENT '更新时间', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (id), - UNIQUE KEY uk_device_event (device_id, event_time, STATUS), + UNIQUE KEY uk_device_event_time (device_id, event_time, status, event_type), KEY idx_device_id (device_id), KEY idx_event_time (event_time), - KEY idx_status (STATUS), + KEY idx_status (status), KEY idx_head_type (head_type), - KEY idx_event_type (event_type) + KEY idx_event_type (event_type), + KEY idx_create_time (create_time) ) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备状态日志表'; --- 按摩任务表 +-- 按摩任务表修改 CREATE TABLE massage_task ( - id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', device_id VARCHAR(255) NOT NULL COMMENT '设备ID', device_name VARCHAR(255) DEFAULT NULL COMMENT '设备名称', product_name VARCHAR(255) DEFAULT NULL COMMENT '产品名称', - head_type VARCHAR(255) DEFAULT NULL COMMENT '按摩头类型', - body_part VARCHAR(255) DEFAULT NULL COMMENT '身体部位', + head_type VARCHAR(100) DEFAULT NULL COMMENT '按摩头类型', + body_part VARCHAR(100) DEFAULT NULL COMMENT '身体部位', massage_plan VARCHAR(255) DEFAULT NULL COMMENT '按摩方案', -- 创建人 @@ -120,16 +127,20 @@ CREATE TABLE massage_task ( task_time BIGINT DEFAULT NULL COMMENT '任务时长(毫秒)', create_timestamp BIGINT DEFAULT NULL COMMENT '创建时间戳(毫秒)', + -- 描述和备注 + description TEXT COMMENT '描述', + remark TEXT COMMENT '备注', + -- 时间字段 - create_time DATETIME DEFAULT NULL COMMENT '创建时间', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 系统字段 - is_deleted TINYINT(1) DEFAULT '0' COMMENT '是否删除(0-未删除, 1-已删除)', + is_deleted TINYINT(1) DEFAULT 0 COMMENT '是否删除(0-未删除, 1-已删除)', update_by VARCHAR(255) DEFAULT NULL COMMENT '更新人', - update_time DATETIME DEFAULT NULL COMMENT '更新时间', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (id), - UNIQUE KEY uk_device_task (device_id, start_time, head_type, body_part), + UNIQUE KEY uk_device_task_time (device_id, start_time, head_type, body_part), KEY idx_device_id (device_id), KEY idx_start_time (start_time), KEY idx_head_type (head_type), @@ -137,9 +148,9 @@ CREATE TABLE massage_task ( KEY idx_create_time (create_time) ) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='按摩任务表'; --- 设备运行时长统计表 +-- 设备运行时长统计表修改 CREATE TABLE device_runtime_stats ( - id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', device_id VARCHAR(255) NOT NULL COMMENT '设备唯一标识', stat_date DATE NOT NULL COMMENT '统计日期', daily_duration BIGINT DEFAULT 0 COMMENT '日运行时长(秒)', @@ -147,21 +158,27 @@ CREATE TABLE device_runtime_stats ( monthly_duration BIGINT DEFAULT 0 COMMENT '月运行时长(秒)', online_count INT DEFAULT 0 COMMENT '上线次数', + -- 描述和备注 + description TEXT COMMENT '描述', + remark TEXT COMMENT '备注', + -- 系统字段 - is_deleted TINYINT(1) DEFAULT '0' COMMENT '是否删除(0-未删除, 1-已删除)', + is_deleted TINYINT(1) DEFAULT 0 COMMENT '是否删除(0-未删除, 1-已删除)', create_by VARCHAR(255) DEFAULT NULL COMMENT '创建人', update_by VARCHAR(255) DEFAULT NULL COMMENT '更新人', - create_time DATETIME DEFAULT NULL COMMENT '创建时间', - update_time DATETIME DEFAULT NULL COMMENT '更新时间', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - UNIQUE KEY uk_device_date (device_id, stat_date), + PRIMARY KEY (id), + UNIQUE KEY uk_device_stat_date (device_id, stat_date), KEY idx_stat_date (stat_date), - KEY idx_device_id (device_id) + KEY idx_device_id (device_id), + KEY idx_create_time (create_time) ) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备运行时长统计表'; --- 设备按头使用统计表 +-- 设备按头使用统计表修改 CREATE TABLE device_head_usage ( - id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', device_id VARCHAR(255) NOT NULL COMMENT '设备唯一标识', head_type VARCHAR(100) NOT NULL COMMENT '按头类型', usage_count INT DEFAULT 0 COMMENT '使用次数', @@ -169,23 +186,23 @@ CREATE TABLE device_head_usage ( daily_duration BIGINT DEFAULT 0 COMMENT '今日使用时长(秒)', weekly_duration BIGINT DEFAULT 0 COMMENT '本周使用时长(秒)', monthly_duration BIGINT DEFAULT 0 COMMENT '本月使用时长(秒)', - last_used_time DATETIME COMMENT '最后使用时间', + last_used_time DATETIME DEFAULT NULL COMMENT '最后使用时间', + + -- 描述和备注 + description TEXT COMMENT '描述', + remark TEXT COMMENT '备注', -- 系统字段 - is_deleted TINYINT(1) DEFAULT '0' COMMENT '是否删除(0-未删除, 1-已删除)', + is_deleted TINYINT(1) DEFAULT 0 COMMENT '是否删除(0-未删除, 1-已删除)', create_by VARCHAR(255) DEFAULT NULL COMMENT '创建人', update_by VARCHAR(255) DEFAULT NULL COMMENT '更新人', - create_time DATETIME DEFAULT NULL COMMENT '创建时间', - update_time DATETIME DEFAULT NULL COMMENT '更新时间', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - UNIQUE KEY uk_device_head (device_id, head_type), + PRIMARY KEY (id), + UNIQUE KEY uk_device_head_type (device_id, head_type), KEY idx_head_type (head_type), KEY idx_last_used_time (last_used_time), - KEY idx_device_id (device_id) -) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备按头使用统计表'; - --- 如果业务允许,可以考虑调整唯一键约束,增加时间容差 --- 比如允许同一设备在同一分钟内只有一条相同状态的记录 -ALTER TABLE device_status_log -DROP INDEX uk_device_event, -ADD UNIQUE KEY uk_device_event_minute (device_id, DATE_FORMAT(event_time, '%Y-%m-%d %H:%i'), status); \ No newline at end of file + KEY idx_device_id (device_id), + KEY idx_create_time (create_time) +) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备按头使用统计表'; \ No newline at end of file diff --git a/sql/storm-AfterSales.sql b/sql/storm-AfterSales.sql new file mode 100644 index 0000000..c6828b4 --- /dev/null +++ b/sql/storm-AfterSales.sql @@ -0,0 +1,140 @@ +CREATE DATABASE `storm-AfterSales` + +USE `storm-AfterSales` + +CREATE TABLE sys_work_order_type ( + type_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '类型ID', + type_name VARCHAR(50) NOT NULL COMMENT '类型名称(操作习惯、部件维修、设备巡厂维修、产品建议、投诉)', + type_key VARCHAR(50) NOT NULL COMMENT '类型键值', + STATUS CHAR(1) DEFAULT '0' COMMENT '状态(0正常 1停用)', + order_num INT DEFAULT 0 COMMENT '显示顺序', + create_by VARCHAR(64) DEFAULT '' COMMENT '创建者', + create_time DATETIME COMMENT '创建时间', + update_by VARCHAR(64) DEFAULT '' COMMENT '更新者', + update_time DATETIME COMMENT '更新时间', + remark VARCHAR(500) DEFAULT NULL COMMENT '备注', + UNIQUE KEY uk_type_key (type_key) +) ENGINE=INNODB COMMENT='工单类型表'; + +CREATE TABLE sys_work_order ( + order_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '工单ID', + order_no VARCHAR(50) UNIQUE NOT NULL COMMENT '工单编号', + order_title VARCHAR(200) NOT NULL COMMENT '工单标题', + order_type VARCHAR(50) COMMENT '工单类型(对应type_key)', + order_status VARCHAR(20) DEFAULT 'pending' COMMENT '工单状态(pending, processing, resolved, closed, cancelled)', + priority VARCHAR(10) DEFAULT 'medium' COMMENT '优先级(low, medium, high, urgent)', + + -- 客户信息 + customer_id BIGINT COMMENT '客户ID', + customer_name VARCHAR(100) COMMENT '客户姓名', + customer_contact VARCHAR(50) COMMENT '客户联系方式', + store_address VARCHAR(200) COMMENT '门店地址', + + -- 设备信息 + device_name VARCHAR(100) COMMENT '设备名称', + device_serial_no VARCHAR(100) COMMENT '设备序列号', + device_model VARCHAR(100) COMMENT '设备型号', + production_date DATE COMMENT '生产日期', + + -- 问题描述(多媒体支持) + problem_description TEXT COMMENT '问题描述', + attachment_urls JSON COMMENT '附件URL(图片/视频) - 支持多个', + problem_category VARCHAR(50) COMMENT '问题分类(数量问题、质量问题、软件故障等)', + + -- 处理信息 + assignee_id BIGINT COMMENT '处理人ID', + assignee_name VARCHAR(100) COMMENT '处理人姓名', + assign_time DATETIME COMMENT '分配时间', + current_handler_id BIGINT COMMENT '当前处理人ID', + current_handler_name VARCHAR(100) COMMENT '当前处理人姓名', + + -- 进度管理(结构化) + root_cause TEXT COMMENT '原因定位', + solution_plan TEXT COMMENT '处理方案', + final_solution TEXT COMMENT '最终解决方案', + + -- 物流信息(图片1明确需求) + logistics_number VARCHAR(100) COMMENT '物流单号', + logistics_progress VARCHAR(500) COMMENT '物流进度', + + -- 客户反馈 + customer_feedback TEXT COMMENT '客户反馈', + satisfaction_level INT COMMENT '满意度(1-5)', + feedback_type VARCHAR(20) COMMENT '反馈类型(praise, complaint, suggestion)', + + -- 时间管理 + plan_finish_time DATETIME COMMENT '计划完成时间', + actual_finish_time DATETIME COMMENT '实际完成时间', + response_deadline DATETIME COMMENT '响应截止时间(30分钟规则)', + + -- 来源渠道 + source_channel VARCHAR(20) DEFAULT 'feishu' COMMENT '来源渠道(feishu, phone, wechat, app, web)', + feishu_chat_id VARCHAR(100) COMMENT '飞书会话ID', + + -- 若依标准字段 + create_by VARCHAR(64) DEFAULT '' COMMENT '创建者', + create_time DATETIME COMMENT '创建时间', + update_by VARCHAR(64) DEFAULT '' COMMENT '更新者', + update_time DATETIME COMMENT '更新时间', + remark VARCHAR(500) DEFAULT NULL COMMENT '备注', + + -- 索引优化 + INDEX idx_order_no (order_no), + INDEX idx_status (order_status), + INDEX idx_assignee (assignee_id), + INDEX idx_customer (customer_id), + INDEX idx_create_time (create_time), + INDEX idx_device_serial (device_serial_no), + INDEX idx_logistics (logistics_number) +) ENGINE=INNODB COMMENT='工单主表'; + +-- 工单处理记录表(增强进度跟踪) +CREATE TABLE sys_work_order_record ( + record_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '记录ID', + order_id BIGINT NOT NULL COMMENT '工单ID', + process_type VARCHAR(50) COMMENT '处理类型(create, assign, process, feedback, close, visit等)', + process_content TEXT COMMENT '处理内容', + + -- 结构化进度信息 + progress_type VARCHAR(50) COMMENT '进度类型(logistics, repair, replacement, optimization, complaint)', + progress_value VARCHAR(200) COMMENT '进度值', + progress_percent INT COMMENT '进度百分比', + + process_user_id BIGINT COMMENT '处理人ID', + process_user_name VARCHAR(100) COMMENT '处理人姓名', + process_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '处理时间', + attachment_url VARCHAR(500) COMMENT '进度附件', + + INDEX idx_order_id (order_id), + INDEX idx_process_time (process_time), + INDEX idx_progress_type (progress_type) +) ENGINE=INNODB COMMENT='工单处理记录表'; + +-- 工单聊天关联表(支持多渠道) +CREATE TABLE sys_work_order_chat_rel ( + id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', + order_id BIGINT NOT NULL COMMENT '工单ID', + chat_session_id VARCHAR(100) COMMENT '聊天会话ID', + chat_platform VARCHAR(20) DEFAULT 'feishu' COMMENT '平台(feishu, wechat, phone)', + message_id VARCHAR(100) COMMENT '消息ID', + chat_content TEXT COMMENT '聊天内容摘要', + chat_duration INT COMMENT '通话时长(秒)', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + + INDEX idx_order_id (order_id), + INDEX idx_chat_session (chat_session_id), + INDEX idx_platform (chat_platform) +) ENGINE=INNODB COMMENT='工单聊天关联表'; + +-- 新增:客服知识库关联表 +CREATE TABLE sys_knowledge_rel ( + id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', + order_id BIGINT NOT NULL COMMENT '工单ID', + knowledge_id BIGINT COMMENT '知识库文章ID', + knowledge_title VARCHAR(200) COMMENT '知识标题', + used_count INT DEFAULT 1 COMMENT '使用次数', + create_time DATETIME COMMENT '关联时间', + + INDEX idx_order_id (order_id), + INDEX idx_knowledge (knowledge_id) +) ENGINE=INNODB COMMENT='工单知识库关联表'; \ No newline at end of file diff --git a/storm-afterSales/pom.xml b/storm-afterSales/pom.xml new file mode 100644 index 0000000..bc810a5 --- /dev/null +++ b/storm-afterSales/pom.xml @@ -0,0 +1,111 @@ + + + 4.0.0 + + com.storm + storm + 3.6.6 + + + storm-afterSales + 0.0.1-SNAPSHOT + storm-afterSales + storm-afterSales工单管理 + + + 17 + 17 + UTF-8 + + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + 2.2.6.RELEASE + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + 2.2.6.RELEASE + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + + + + com.storm + storm-common-security + + + + com.storm + storm-common-redis + + + + com.storm + storm-common-datascope + + + + com.storm + storm-api-system + + + + + com.mysql + mysql-connector-j + + + + + com.storm + storm-common-log + + + + + com.storm + storm-common-swagger + + + + org.projectlombok + lombok + + + + com.storm + storm-common-oss + 3.6.6 + + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + \ No newline at end of file diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/AfterSalesApplication.java b/storm-afterSales/src/main/java/com/storm/afterSales/AfterSalesApplication.java new file mode 100644 index 0000000..1117ee6 --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/AfterSalesApplication.java @@ -0,0 +1,13 @@ +package com.storm.afterSales; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AfterSalesApplication { + + public static void main(String[] args) { + SpringApplication.run(AfterSalesApplication.class, args); + } + +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/controller/SysKnowledgeRelController.java b/storm-afterSales/src/main/java/com/storm/afterSales/controller/SysKnowledgeRelController.java new file mode 100644 index 0000000..b04c7a3 --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/controller/SysKnowledgeRelController.java @@ -0,0 +1,117 @@ +package com.storm.afterSales.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import com.storm.afterSales.domain.SysKnowledgeRel; +import com.storm.afterSales.service.ISysKnowledgeRelService; +import com.storm.common.core.domain.R; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.utils.poi.ExcelUtil; +import com.storm.common.core.web.page.TableDataInfo; + +/** + * 工单知识库关联Controller + * + * @author storm + * @date 2025-10-02 + */ +@RestController +@RequestMapping("/afterSales") +@Tag(name = "工单知识库关联相关接口") +public class SysKnowledgeRelController extends BaseController +{ + @Autowired + private ISysKnowledgeRelService sysKnowledgeRelService; + + /** + * 查询工单知识库关联列表 + */ + @RequiresPermissions("afterSales:afterSales:list") + @GetMapping("/list") + @Operation(summary = "查询工单知识库关联列表") + public TableDataInfo> list(@Parameter(description = "工单知识库关联查询条件") SysKnowledgeRel sysKnowledgeRel) + { + startPage(); + List list = sysKnowledgeRelService.selectSysKnowledgeRelList(sysKnowledgeRel); + return getDataTable(list); + } + + /** + * 导出工单知识库关联列表 + */ + @RequiresPermissions("afterSales:afterSales:export") + @Log(title = "工单知识库关联", businessType = BusinessType.EXPORT) + @PostMapping("/export") + @Operation(summary = "导出工单知识库关联列表") + public void export(HttpServletResponse response, @Parameter(description = "工单知识库关联查询条件") SysKnowledgeRel sysKnowledgeRel) + { + List list = sysKnowledgeRelService.selectSysKnowledgeRelList(sysKnowledgeRel); + ExcelUtil util = new ExcelUtil(SysKnowledgeRel.class); + util.exportExcel(response, list, "工单知识库关联数据"); + } + + /** + * 获取工单知识库关联详细信息 + */ + @RequiresPermissions("afterSales:afterSales:query") + @GetMapping(value = "/{id}") + @Operation(summary = "获取工单知识库关联详细信息") + public R getInfo(@Parameter(description = "工单知识库关联ID", required = true) + @PathVariable("id") Long id) + { + return R.ok(sysKnowledgeRelService.selectSysKnowledgeRelById(id)); + } + + /** + * 新增工单知识库关联 + */ + @RequiresPermissions("afterSales:afterSales:add") + @Log(title = "工单知识库关联", businessType = BusinessType.INSERT) + @PostMapping + @Operation(summary = "新增工单知识库关联") + public AjaxResult add(@Parameter(description = "工单知识库关联实体", required = true) @RequestBody SysKnowledgeRel sysKnowledgeRel) + { + return toAjax(sysKnowledgeRelService.insertSysKnowledgeRel(sysKnowledgeRel)); + } + + /** + * 修改工单知识库关联 + */ + @RequiresPermissions("afterSales:afterSales:edit") + @Log(title = "工单知识库关联", businessType = BusinessType.UPDATE) + @PutMapping + @Operation(summary = "修改工单知识库关联") + public AjaxResult edit(@Parameter(description = "工单知识库关联实体", required = true) @RequestBody SysKnowledgeRel sysKnowledgeRel) + { + return toAjax(sysKnowledgeRelService.updateSysKnowledgeRel(sysKnowledgeRel)); + } + + /** + * 删除工单知识库关联 + */ + @RequiresPermissions("afterSales:afterSales:remove") + @Log(title = "工单知识库关联", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + @Operation(summary = "删除工单知识库关联") + public AjaxResult remove(@Parameter(description = "工单知识库关联ID数组", required = true) @PathVariable Long[] ids) + { + return toAjax(sysKnowledgeRelService.deleteSysKnowledgeRelByIds(ids)); + } +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/controller/SysWorkOrderChatRelController.java b/storm-afterSales/src/main/java/com/storm/afterSales/controller/SysWorkOrderChatRelController.java new file mode 100644 index 0000000..8da1ab3 --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/controller/SysWorkOrderChatRelController.java @@ -0,0 +1,117 @@ +package com.storm.afterSales.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import com.storm.common.core.domain.R; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.afterSales.domain.SysWorkOrderChatRel; +import com.storm.afterSales.service.ISysWorkOrderChatRelService; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.utils.poi.ExcelUtil; +import com.storm.common.core.web.page.TableDataInfo; + +/** + * 工单聊天关联Controller + * + * @author storm + * @date 2025-10-02 + */ +@RestController +@RequestMapping("/afterSales") +@Tag(name = "工单聊天关联相关接口") +public class SysWorkOrderChatRelController extends BaseController +{ + @Autowired + private ISysWorkOrderChatRelService sysWorkOrderChatRelService; + + /** + * 查询工单聊天关联列表 + */ + @RequiresPermissions("afterSales:afterSales:list") + @GetMapping("/list") + @Operation(summary = "查询工单聊天关联列表") + public TableDataInfo> list(@Parameter(description = "工单聊天关联查询条件") SysWorkOrderChatRel sysWorkOrderChatRel) + { + startPage(); + List list = sysWorkOrderChatRelService.selectSysWorkOrderChatRelList(sysWorkOrderChatRel); + return getDataTable(list); + } + + /** + * 导出工单聊天关联列表 + */ + @RequiresPermissions("afterSales:afterSales:export") + @Log(title = "工单聊天关联", businessType = BusinessType.EXPORT) + @PostMapping("/export") + @Operation(summary = "导出工单聊天关联列表") + public void export(HttpServletResponse response, @Parameter(description = "工单聊天关联查询条件") SysWorkOrderChatRel sysWorkOrderChatRel) + { + List list = sysWorkOrderChatRelService.selectSysWorkOrderChatRelList(sysWorkOrderChatRel); + ExcelUtil util = new ExcelUtil(SysWorkOrderChatRel.class); + util.exportExcel(response, list, "工单聊天关联数据"); + } + + /** + * 获取工单聊天关联详细信息 + */ + @RequiresPermissions("afterSales:afterSales:query") + @GetMapping(value = "/{id}") + @Operation(summary = "获取工单聊天关联详细信息") + public R getInfo(@Parameter(description = "工单聊天关联ID", required = true) + @PathVariable("id") Long id) + { + return R.ok(sysWorkOrderChatRelService.selectSysWorkOrderChatRelById(id)); + } + + /** + * 新增工单聊天关联 + */ + @RequiresPermissions("afterSales:afterSales:add") + @Log(title = "工单聊天关联", businessType = BusinessType.INSERT) + @PostMapping + @Operation(summary = "新增工单聊天关联") + public AjaxResult add(@Parameter(description = "工单聊天关联实体", required = true) @RequestBody SysWorkOrderChatRel sysWorkOrderChatRel) + { + return toAjax(sysWorkOrderChatRelService.insertSysWorkOrderChatRel(sysWorkOrderChatRel)); + } + + /** + * 修改工单聊天关联 + */ + @RequiresPermissions("afterSales:afterSales:edit") + @Log(title = "工单聊天关联", businessType = BusinessType.UPDATE) + @PutMapping + @Operation(summary = "修改工单聊天关联") + public AjaxResult edit(@Parameter(description = "工单聊天关联实体", required = true) @RequestBody SysWorkOrderChatRel sysWorkOrderChatRel) + { + return toAjax(sysWorkOrderChatRelService.updateSysWorkOrderChatRel(sysWorkOrderChatRel)); + } + + /** + * 删除工单聊天关联 + */ + @RequiresPermissions("afterSales:afterSales:remove") + @Log(title = "工单聊天关联", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + @Operation(summary = "删除工单聊天关联") + public AjaxResult remove(@Parameter(description = "工单聊天关联ID数组", required = true) @PathVariable Long[] ids) + { + return toAjax(sysWorkOrderChatRelService.deleteSysWorkOrderChatRelByIds(ids)); + } +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/controller/SysWorkOrderController.java b/storm-afterSales/src/main/java/com/storm/afterSales/controller/SysWorkOrderController.java new file mode 100644 index 0000000..f90d44f --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/controller/SysWorkOrderController.java @@ -0,0 +1,117 @@ +package com.storm.afterSales.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import com.storm.common.core.domain.R; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.afterSales.domain.SysWorkOrder; +import com.storm.afterSales.service.ISysWorkOrderService; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.utils.poi.ExcelUtil; +import com.storm.common.core.web.page.TableDataInfo; + +/** + * 工单主Controller + * + * @author storm + * @date 2025-10-02 + */ +@RestController +@RequestMapping("/afterSales") +@Tag(name = "工单主相关接口") +public class SysWorkOrderController extends BaseController +{ + @Autowired + private ISysWorkOrderService sysWorkOrderService; + + /** + * 查询工单主列表 + */ + @RequiresPermissions("afterSales:afterSales:list") + @GetMapping("/list") + @Operation(summary = "查询工单主列表") + public TableDataInfo> list(@Parameter(description = "工单主查询条件") SysWorkOrder sysWorkOrder) + { + startPage(); + List list = sysWorkOrderService.selectSysWorkOrderList(sysWorkOrder); + return getDataTable(list); + } + + /** + * 导出工单主列表 + */ + @RequiresPermissions("afterSales:afterSales:export") + @Log(title = "工单主", businessType = BusinessType.EXPORT) + @PostMapping("/export") + @Operation(summary = "导出工单主列表") + public void export(HttpServletResponse response, @Parameter(description = "工单主查询条件") SysWorkOrder sysWorkOrder) + { + List list = sysWorkOrderService.selectSysWorkOrderList(sysWorkOrder); + ExcelUtil util = new ExcelUtil(SysWorkOrder.class); + util.exportExcel(response, list, "工单主数据"); + } + + /** + * 获取工单主详细信息 + */ + @RequiresPermissions("afterSales:afterSales:query") + @GetMapping(value = "/{orderId}") + @Operation(summary = "获取工单主详细信息") + public R getInfo(@Parameter(description = "工单主ID", required = true) + @PathVariable("orderId") Long orderId) + { + return R.ok(sysWorkOrderService.selectSysWorkOrderByOrderId(orderId)); + } + + /** + * 新增工单主 + */ + @RequiresPermissions("afterSales:afterSales:add") + @Log(title = "工单主", businessType = BusinessType.INSERT) + @PostMapping + @Operation(summary = "新增工单主") + public AjaxResult add(@Parameter(description = "工单主实体", required = true) @RequestBody SysWorkOrder sysWorkOrder) + { + return toAjax(sysWorkOrderService.insertSysWorkOrder(sysWorkOrder)); + } + + /** + * 修改工单主 + */ + @RequiresPermissions("afterSales:afterSales:edit") + @Log(title = "工单主", businessType = BusinessType.UPDATE) + @PutMapping + @Operation(summary = "修改工单主") + public AjaxResult edit(@Parameter(description = "工单主实体", required = true) @RequestBody SysWorkOrder sysWorkOrder) + { + return toAjax(sysWorkOrderService.updateSysWorkOrder(sysWorkOrder)); + } + + /** + * 删除工单主 + */ + @RequiresPermissions("afterSales:afterSales:remove") + @Log(title = "工单主", businessType = BusinessType.DELETE) + @DeleteMapping("/{orderIds}") + @Operation(summary = "删除工单主") + public AjaxResult remove(@Parameter(description = "工单主ID数组", required = true) @PathVariable Long[] orderIds) + { + return toAjax(sysWorkOrderService.deleteSysWorkOrderByOrderIds(orderIds)); + } +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/controller/SysWorkOrderRecordController.java b/storm-afterSales/src/main/java/com/storm/afterSales/controller/SysWorkOrderRecordController.java new file mode 100644 index 0000000..0ebb15c --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/controller/SysWorkOrderRecordController.java @@ -0,0 +1,117 @@ +package com.storm.afterSales.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import com.storm.common.core.domain.R; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.afterSales.domain.SysWorkOrderRecord; +import com.storm.afterSales.service.ISysWorkOrderRecordService; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.utils.poi.ExcelUtil; +import com.storm.common.core.web.page.TableDataInfo; + +/** + * 工单处理记录Controller + * + * @author storm + * @date 2025-10-02 + */ +@RestController +@RequestMapping("/afterSales") +@Tag(name = "工单处理记录相关接口") +public class SysWorkOrderRecordController extends BaseController +{ + @Autowired + private ISysWorkOrderRecordService sysWorkOrderRecordService; + + /** + * 查询工单处理记录列表 + */ + @RequiresPermissions("afterSales:afterSales:list") + @GetMapping("/list") + @Operation(summary = "查询工单处理记录列表") + public TableDataInfo> list(@Parameter(description = "工单处理记录查询条件") SysWorkOrderRecord sysWorkOrderRecord) + { + startPage(); + List list = sysWorkOrderRecordService.selectSysWorkOrderRecordList(sysWorkOrderRecord); + return getDataTable(list); + } + + /** + * 导出工单处理记录列表 + */ + @RequiresPermissions("afterSales:afterSales:export") + @Log(title = "工单处理记录", businessType = BusinessType.EXPORT) + @PostMapping("/export") + @Operation(summary = "导出工单处理记录列表") + public void export(HttpServletResponse response, @Parameter(description = "工单处理记录查询条件") SysWorkOrderRecord sysWorkOrderRecord) + { + List list = sysWorkOrderRecordService.selectSysWorkOrderRecordList(sysWorkOrderRecord); + ExcelUtil util = new ExcelUtil(SysWorkOrderRecord.class); + util.exportExcel(response, list, "工单处理记录数据"); + } + + /** + * 获取工单处理记录详细信息 + */ + @RequiresPermissions("afterSales:afterSales:query") + @GetMapping(value = "/{recordId}") + @Operation(summary = "获取工单处理记录详细信息") + public R getInfo(@Parameter(description = "工单处理记录ID", required = true) + @PathVariable("recordId") Long recordId) + { + return R.ok(sysWorkOrderRecordService.selectSysWorkOrderRecordByRecordId(recordId)); + } + + /** + * 新增工单处理记录 + */ + @RequiresPermissions("afterSales:afterSales:add") + @Log(title = "工单处理记录", businessType = BusinessType.INSERT) + @PostMapping + @Operation(summary = "新增工单处理记录") + public AjaxResult add(@Parameter(description = "工单处理记录实体", required = true) @RequestBody SysWorkOrderRecord sysWorkOrderRecord) + { + return toAjax(sysWorkOrderRecordService.insertSysWorkOrderRecord(sysWorkOrderRecord)); + } + + /** + * 修改工单处理记录 + */ + @RequiresPermissions("afterSales:afterSales:edit") + @Log(title = "工单处理记录", businessType = BusinessType.UPDATE) + @PutMapping + @Operation(summary = "修改工单处理记录") + public AjaxResult edit(@Parameter(description = "工单处理记录实体", required = true) @RequestBody SysWorkOrderRecord sysWorkOrderRecord) + { + return toAjax(sysWorkOrderRecordService.updateSysWorkOrderRecord(sysWorkOrderRecord)); + } + + /** + * 删除工单处理记录 + */ + @RequiresPermissions("afterSales:afterSales:remove") + @Log(title = "工单处理记录", businessType = BusinessType.DELETE) + @DeleteMapping("/{recordIds}") + @Operation(summary = "删除工单处理记录") + public AjaxResult remove(@Parameter(description = "工单处理记录ID数组", required = true) @PathVariable Long[] recordIds) + { + return toAjax(sysWorkOrderRecordService.deleteSysWorkOrderRecordByRecordIds(recordIds)); + } +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/controller/SysWorkOrderTypeController.java b/storm-afterSales/src/main/java/com/storm/afterSales/controller/SysWorkOrderTypeController.java new file mode 100644 index 0000000..dde74bb --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/controller/SysWorkOrderTypeController.java @@ -0,0 +1,117 @@ +package com.storm.afterSales.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import com.storm.common.core.domain.R; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.afterSales.domain.SysWorkOrderType; +import com.storm.afterSales.service.ISysWorkOrderTypeService; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.utils.poi.ExcelUtil; +import com.storm.common.core.web.page.TableDataInfo; + +/** + * 工单类型Controller + * + * @author storm + * @date 2025-10-02 + */ +@RestController +@RequestMapping("/type") +@Tag(name = "工单类型相关接口") +public class SysWorkOrderTypeController extends BaseController +{ + @Autowired + private ISysWorkOrderTypeService sysWorkOrderTypeService; + + /** + * 查询工单类型列表 + */ + @RequiresPermissions("afterSales:type:list") + @GetMapping("/list") + @Operation(summary = "查询工单类型列表") + public TableDataInfo> list(@Parameter(description = "工单类型查询条件") SysWorkOrderType sysWorkOrderType) + { + startPage(); + List list = sysWorkOrderTypeService.selectSysWorkOrderTypeList(sysWorkOrderType); + return getDataTable(list); + } + + /** + * 导出工单类型列表 + */ + @RequiresPermissions("afterSales:type:export") + @Log(title = "工单类型", businessType = BusinessType.EXPORT) + @PostMapping("/export") + @Operation(summary = "导出工单类型列表") + public void export(HttpServletResponse response, @Parameter(description = "工单类型查询条件") SysWorkOrderType sysWorkOrderType) + { + List list = sysWorkOrderTypeService.selectSysWorkOrderTypeList(sysWorkOrderType); + ExcelUtil util = new ExcelUtil(SysWorkOrderType.class); + util.exportExcel(response, list, "工单类型数据"); + } + + /** + * 获取工单类型详细信息 + */ + @RequiresPermissions("afterSales:type:query") + @GetMapping(value = "/{typeId}") + @Operation(summary = "获取工单类型详细信息") + public R getInfo(@Parameter(description = "工单类型ID", required = true) + @PathVariable("typeId") Long typeId) + { + return R.ok(sysWorkOrderTypeService.selectSysWorkOrderTypeByTypeId(typeId)); + } + + /** + * 新增工单类型 + */ + @RequiresPermissions("afterSales:type:add") + @Log(title = "工单类型", businessType = BusinessType.INSERT) + @PostMapping + @Operation(summary = "新增工单类型") + public AjaxResult add(@Parameter(description = "工单类型实体", required = true) @RequestBody SysWorkOrderType sysWorkOrderType) + { + return toAjax(sysWorkOrderTypeService.insertSysWorkOrderType(sysWorkOrderType)); + } + + /** + * 修改工单类型 + */ + @RequiresPermissions("afterSales:type:edit") + @Log(title = "工单类型", businessType = BusinessType.UPDATE) + @PutMapping + @Operation(summary = "修改工单类型") + public AjaxResult edit(@Parameter(description = "工单类型实体", required = true) @RequestBody SysWorkOrderType sysWorkOrderType) + { + return toAjax(sysWorkOrderTypeService.updateSysWorkOrderType(sysWorkOrderType)); + } + + /** + * 删除工单类型 + */ + @RequiresPermissions("afterSales:type:remove") + @Log(title = "工单类型", businessType = BusinessType.DELETE) + @DeleteMapping("/{typeIds}") + @Operation(summary = "删除工单类型") + public AjaxResult remove(@Parameter(description = "工单类型ID数组", required = true) @PathVariable Long[] typeIds) + { + return toAjax(sysWorkOrderTypeService.deleteSysWorkOrderTypeByTypeIds(typeIds)); + } +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/domain/SysKnowledgeRel.java b/storm-afterSales/src/main/java/com/storm/afterSales/domain/SysKnowledgeRel.java new file mode 100644 index 0000000..6249cf1 --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/domain/SysKnowledgeRel.java @@ -0,0 +1,49 @@ +package com.storm.afterSales.domain; + +import io.swagger.v3.oas.annotations.media.Schema; +import com.storm.common.core.annotation.Excel; +import com.storm.common.core.web.domain.BaseEntity; +import lombok.Data; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +/** + * 工单知识库关联对象 sys_knowledge_rel + * + * @author storm + * @date 2025-10-02 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "工单知识库关联实体") +public class SysKnowledgeRel extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 主键 */ + @Schema(description = "主键") + private Long id; + + /** 工单ID */ + @Excel(name = "工单ID") + @Schema(description = "工单ID") + private Long orderId; + + /** 知识库文章ID */ + @Excel(name = "知识库文章ID") + @Schema(description = "知识库文章ID") + private Long knowledgeId; + + /** 知识标题 */ + @Excel(name = "知识标题") + @Schema(description = "知识标题") + private String knowledgeTitle; + + /** 使用次数 */ + @Excel(name = "使用次数") + @Schema(description = "使用次数") + private Integer usedCount; + + +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/domain/SysWorkOrder.java b/storm-afterSales/src/main/java/com/storm/afterSales/domain/SysWorkOrder.java new file mode 100644 index 0000000..2526823 --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/domain/SysWorkOrder.java @@ -0,0 +1,205 @@ +package com.storm.afterSales.domain; + +import java.time.LocalDateTime; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import com.storm.common.core.annotation.Excel; +import com.storm.common.core.web.domain.BaseEntity; +import lombok.Data; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +/** + * 工单主对象 sys_work_order + * + * @author storm + * @date 2025-10-02 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "工单主实体") +public class SysWorkOrder extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 工单ID */ + @Schema(description = "工单ID") + private Long orderId; + + /** 工单编号 */ + @Excel(name = "工单编号") + @Schema(description = "工单编号") + private String orderNo; + + /** 工单标题 */ + @Excel(name = "工单标题") + @Schema(description = "工单标题") + private String orderTitle; + + /** 工单类型(对应type_key) */ + @Excel(name = "工单类型", readConverterExp = "对=应type_key") + @Schema(description = "工单类型(对应type_key)") + private String orderType; + + /** 工单状态(pending, processing, resolved, closed, cancelled) */ + @Excel(name = "工单状态", readConverterExp = "p=ending,,p=rocessing,,r=esolved,,c=losed,,c=ancelled") + @Schema(description = "工单状态(pending, processing, resolved, closed, cancelled)") + private String orderStatus; + + /** 优先级(low, medium, high, urgent) */ + @Excel(name = "优先级", readConverterExp = "l=ow,,m=edium,,h=igh,,u=rgent") + @Schema(description = "优先级(low, medium, high, urgent)") + private String priority; + + /** 客户ID */ + @Excel(name = "客户ID") + @Schema(description = "客户ID") + private Long customerId; + + /** 客户姓名 */ + @Excel(name = "客户姓名") + @Schema(description = "客户姓名") + private String customerName; + + /** 客户联系方式 */ + @Excel(name = "客户联系方式") + @Schema(description = "客户联系方式") + private String customerContact; + + /** 门店地址 */ + @Excel(name = "门店地址") + @Schema(description = "门店地址") + private String storeAddress; + + /** 设备名称 */ + @Excel(name = "设备名称") + @Schema(description = "设备名称") + private String deviceName; + + /** 设备序列号 */ + @Excel(name = "设备序列号") + @Schema(description = "设备序列号") + private String deviceSerialNo; + + /** 设备型号 */ + @Excel(name = "设备型号") + @Schema(description = "设备型号") + private String deviceModel; + + /** 生产日期 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "生产日期", width = 30, dateFormat = "yyyy-MM-dd") + @Schema(description = "生产日期") + private LocalDateTime productionDate; + + /** 问题描述 */ + @Excel(name = "问题描述") + @Schema(description = "问题描述") + private String problemDescription; + + /** 附件URL(图片/视频) - 支持多个 */ + @Excel(name = "附件URL", readConverterExp = "图=片/视频") + @Schema(description = "附件URL(图片/视频) - 支持多个") + private String attachmentUrls; + + /** 问题分类(数量问题、质量问题、软件故障等) */ + @Excel(name = "问题分类", readConverterExp = "数=量问题、质量问题、软件故障等") + @Schema(description = "问题分类(数量问题、质量问题、软件故障等)") + private String problemCategory; + + /** 处理人ID */ + @Excel(name = "处理人ID") + @Schema(description = "处理人ID") + private Long assigneeId; + + /** 处理人姓名 */ + @Excel(name = "处理人姓名") + @Schema(description = "处理人姓名") + private String assigneeName; + + /** 分配时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "分配时间", width = 30, dateFormat = "yyyy-MM-dd") + @Schema(description = "分配时间") + private LocalDateTime assignTime; + + /** 当前处理人ID */ + @Excel(name = "当前处理人ID") + @Schema(description = "当前处理人ID") + private Long currentHandlerId; + + /** 当前处理人姓名 */ + @Excel(name = "当前处理人姓名") + @Schema(description = "当前处理人姓名") + private String currentHandlerName; + + /** 原因定位 */ + @Excel(name = "原因定位") + @Schema(description = "原因定位") + private String rootCause; + + /** 处理方案 */ + @Excel(name = "处理方案") + @Schema(description = "处理方案") + private String solutionPlan; + + /** 最终解决方案 */ + @Excel(name = "最终解决方案") + @Schema(description = "最终解决方案") + private String finalSolution; + + /** 物流单号 */ + @Excel(name = "物流单号") + @Schema(description = "物流单号") + private String logisticsNumber; + + /** 物流进度 */ + @Excel(name = "物流进度") + @Schema(description = "物流进度") + private String logisticsProgress; + + /** 客户反馈 */ + @Excel(name = "客户反馈") + @Schema(description = "客户反馈") + private String customerFeedback; + + /** 满意度(1-5) */ + @Excel(name = "满意度", readConverterExp = "1=-5") + @Schema(description = "满意度(1-5)") + private Integer satisfactionLevel; + + /** 反馈类型(praise, complaint, suggestion) */ + @Excel(name = "反馈类型", readConverterExp = "p=raise,,c=omplaint,,s=uggestion") + @Schema(description = "反馈类型(praise, complaint, suggestion)") + private String feedbackType; + + /** 计划完成时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "计划完成时间", width = 30, dateFormat = "yyyy-MM-dd") + @Schema(description = "计划完成时间") + private LocalDateTime planFinishTime; + + /** 实际完成时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "实际完成时间", width = 30, dateFormat = "yyyy-MM-dd") + @Schema(description = "实际完成时间") + private LocalDateTime actualFinishTime; + + /** 响应截止时间(30分钟规则) */ + @Excel(name = "响应截止时间", readConverterExp = "3=0分钟规则") + @Schema(description = "响应截止时间(30分钟规则)") + private LocalDateTime responseDeadline; + + /** 来源渠道(feishu, phone, wechat, app, web) */ + @Excel(name = "来源渠道", readConverterExp = "f=eishu,,p=hone,,w=echat,,a=pp,,w=eb") + @Schema(description = "来源渠道(feishu, phone, wechat, app, web)") + private String sourceChannel; + + /** 飞书会话ID */ + @Excel(name = "飞书会话ID") + @Schema(description = "飞书会话ID") + private String feishuChatId; + + +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/domain/SysWorkOrderChatRel.java b/storm-afterSales/src/main/java/com/storm/afterSales/domain/SysWorkOrderChatRel.java new file mode 100644 index 0000000..091e826 --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/domain/SysWorkOrderChatRel.java @@ -0,0 +1,59 @@ +package com.storm.afterSales.domain; + +import io.swagger.v3.oas.annotations.media.Schema; +import com.storm.common.core.annotation.Excel; +import com.storm.common.core.web.domain.BaseEntity; +import lombok.Data; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +/** + * 工单聊天关联对象 sys_work_order_chat_rel + * + * @author storm + * @date 2025-10-02 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "工单聊天关联实体") +public class SysWorkOrderChatRel extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 主键 */ + @Schema(description = "主键") + private Long id; + + /** 工单ID */ + @Excel(name = "工单ID") + @Schema(description = "工单ID") + private Long orderId; + + /** 聊天会话ID */ + @Excel(name = "聊天会话ID") + @Schema(description = "聊天会话ID") + private String chatSessionId; + + /** 平台(feishu, wechat, phone) */ + @Excel(name = "平台", readConverterExp = "f=eishu,,w=echat,,p=hone") + @Schema(description = "平台(feishu, wechat, phone)") + private String chatPlatform; + + /** 消息ID */ + @Excel(name = "消息ID") + @Schema(description = "消息ID") + private String messageId; + + /** 聊天内容摘要 */ + @Excel(name = "聊天内容摘要") + @Schema(description = "聊天内容摘要") + private String chatContent; + + /** 通话时长(秒) */ + @Excel(name = "通话时长", readConverterExp = "秒=") + @Schema(description = "通话时长(秒)") + private Integer chatDuration; + + +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/domain/SysWorkOrderRecord.java b/storm-afterSales/src/main/java/com/storm/afterSales/domain/SysWorkOrderRecord.java new file mode 100644 index 0000000..824f4de --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/domain/SysWorkOrderRecord.java @@ -0,0 +1,82 @@ +package com.storm.afterSales.domain; + +import java.time.LocalDateTime; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import com.storm.common.core.annotation.Excel; +import com.storm.common.core.web.domain.BaseEntity; +import lombok.Data; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +/** + * 工单处理记录对象 sys_work_order_record + * + * @author storm + * @date 2025-10-02 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "工单处理记录实体") +public class SysWorkOrderRecord extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 记录ID */ + @Schema(description = "记录ID") + private Long recordId; + + /** 工单ID */ + @Excel(name = "工单ID") + @Schema(description = "工单ID") + private Long orderId; + + /** 处理类型(create, assign, process, feedback, close, visit等) */ + @Excel(name = "处理类型", readConverterExp = "c=reate,,a=ssign,,p=rocess,,f=eedback,,c=lose,,v=isit等") + @Schema(description = "处理类型(create, assign, process, feedback, close, visit等)") + private String processType; + + /** 处理内容 */ + @Excel(name = "处理内容") + @Schema(description = "处理内容") + private String processContent; + + /** 进度类型(logistics, repair, replacement, optimization, complaint) */ + @Excel(name = "进度类型", readConverterExp = "l=ogistics,,r=epair,,r=eplacement,,o=ptimization,,c=omplaint") + @Schema(description = "进度类型(logistics, repair, replacement, optimization, complaint)") + private String progressType; + + /** 进度值 */ + @Excel(name = "进度值") + @Schema(description = "进度值") + private String progressValue; + + /** 进度百分比 */ + @Excel(name = "进度百分比") + @Schema(description = "进度百分比") + private Integer progressPercent; + + /** 处理人ID */ + @Excel(name = "处理人ID") + @Schema(description = "处理人ID") + private Long processUserId; + + /** 处理人姓名 */ + @Excel(name = "处理人姓名") + @Schema(description = "处理人姓名") + private String processUserName; + + /** 处理时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "处理时间", width = 30, dateFormat = "yyyy-MM-dd") + @Schema(description = "处理时间") + private LocalDateTime processTime; + + /** 进度附件 */ + @Excel(name = "进度附件") + @Schema(description = "进度附件") + private String attachmentUrl; + + +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/domain/SysWorkOrderType.java b/storm-afterSales/src/main/java/com/storm/afterSales/domain/SysWorkOrderType.java new file mode 100644 index 0000000..3c3f5fe --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/domain/SysWorkOrderType.java @@ -0,0 +1,49 @@ +package com.storm.afterSales.domain; + +import io.swagger.v3.oas.annotations.media.Schema; +import com.storm.common.core.annotation.Excel; +import com.storm.common.core.web.domain.BaseEntity; +import lombok.Data; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +/** + * 工单类型对象 sys_work_order_type + * + * @author storm + * @date 2025-10-02 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "工单类型实体") +public class SysWorkOrderType extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 类型ID */ + @Schema(description = "类型ID") + private Long typeId; + + /** 类型名称(操作习惯、部件维修、设备巡厂维修、产品建议、投诉) */ + @Excel(name = "类型名称", readConverterExp = "操=作习惯、部件维修、设备巡厂维修、产品建议、投诉") + @Schema(description = "类型名称(操作习惯、部件维修、设备巡厂维修、产品建议、投诉)") + private String typeName; + + /** 类型键值 */ + @Excel(name = "类型键值") + @Schema(description = "类型键值") + private String typeKey; + + /** 状态(0正常 1停用) */ + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + @Schema(description = "状态(0正常 1停用)") + private String STATUS; + + /** 显示顺序 */ + @Excel(name = "显示顺序") + @Schema(description = "显示顺序") + private Integer orderNum; + + +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/mapper/SysKnowledgeRelMapper.java b/storm-afterSales/src/main/java/com/storm/afterSales/mapper/SysKnowledgeRelMapper.java new file mode 100644 index 0000000..c7d58c3 --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/mapper/SysKnowledgeRelMapper.java @@ -0,0 +1,64 @@ +package com.storm.afterSales.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import java.util.List; +import com.storm.afterSales.domain.SysKnowledgeRel; + +/** + * 工单知识库关联Mapper接口 + * + * @author storm + * @date 2025-10-02 + */ +@Mapper +public interface SysKnowledgeRelMapper extends BaseMapper +{ + /** + * 查询工单知识库关联 + * + * @param id 工单知识库关联主键 + * @return 工单知识库关联 + */ + public SysKnowledgeRel selectSysKnowledgeRelById(Long id); + + /** + * 查询工单知识库关联列表 + * + * @param sysKnowledgeRel 工单知识库关联 + * @return 工单知识库关联集合 + */ + public List selectSysKnowledgeRelList(SysKnowledgeRel sysKnowledgeRel); + + /** + * 新增工单知识库关联 + * + * @param sysKnowledgeRel 工单知识库关联 + * @return 结果 + */ + public int insertSysKnowledgeRel(SysKnowledgeRel sysKnowledgeRel); + + /** + * 修改工单知识库关联 + * + * @param sysKnowledgeRel 工单知识库关联 + * @return 结果 + */ + public int updateSysKnowledgeRel(SysKnowledgeRel sysKnowledgeRel); + + /** + * 删除工单知识库关联 + * + * @param id 工单知识库关联主键 + * @return 结果 + */ + public int deleteSysKnowledgeRelById(Long id); + + /** + * 批量删除工单知识库关联 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteSysKnowledgeRelByIds(Long[] ids); +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/mapper/SysWorkOrderChatRelMapper.java b/storm-afterSales/src/main/java/com/storm/afterSales/mapper/SysWorkOrderChatRelMapper.java new file mode 100644 index 0000000..31cbe6f --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/mapper/SysWorkOrderChatRelMapper.java @@ -0,0 +1,64 @@ +package com.storm.afterSales.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import java.util.List; +import com.storm.afterSales.domain.SysWorkOrderChatRel; + +/** + * 工单聊天关联Mapper接口 + * + * @author storm + * @date 2025-10-02 + */ +@Mapper +public interface SysWorkOrderChatRelMapper extends BaseMapper +{ + /** + * 查询工单聊天关联 + * + * @param id 工单聊天关联主键 + * @return 工单聊天关联 + */ + public SysWorkOrderChatRel selectSysWorkOrderChatRelById(Long id); + + /** + * 查询工单聊天关联列表 + * + * @param sysWorkOrderChatRel 工单聊天关联 + * @return 工单聊天关联集合 + */ + public List selectSysWorkOrderChatRelList(SysWorkOrderChatRel sysWorkOrderChatRel); + + /** + * 新增工单聊天关联 + * + * @param sysWorkOrderChatRel 工单聊天关联 + * @return 结果 + */ + public int insertSysWorkOrderChatRel(SysWorkOrderChatRel sysWorkOrderChatRel); + + /** + * 修改工单聊天关联 + * + * @param sysWorkOrderChatRel 工单聊天关联 + * @return 结果 + */ + public int updateSysWorkOrderChatRel(SysWorkOrderChatRel sysWorkOrderChatRel); + + /** + * 删除工单聊天关联 + * + * @param id 工单聊天关联主键 + * @return 结果 + */ + public int deleteSysWorkOrderChatRelById(Long id); + + /** + * 批量删除工单聊天关联 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteSysWorkOrderChatRelByIds(Long[] ids); +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/mapper/SysWorkOrderMapper.java b/storm-afterSales/src/main/java/com/storm/afterSales/mapper/SysWorkOrderMapper.java new file mode 100644 index 0000000..e761638 --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/mapper/SysWorkOrderMapper.java @@ -0,0 +1,64 @@ +package com.storm.afterSales.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import java.util.List; +import com.storm.afterSales.domain.SysWorkOrder; + +/** + * 工单主Mapper接口 + * + * @author storm + * @date 2025-10-02 + */ +@Mapper +public interface SysWorkOrderMapper extends BaseMapper +{ + /** + * 查询工单主 + * + * @param orderId 工单主主键 + * @return 工单主 + */ + public SysWorkOrder selectSysWorkOrderByOrderId(Long orderId); + + /** + * 查询工单主列表 + * + * @param sysWorkOrder 工单主 + * @return 工单主集合 + */ + public List selectSysWorkOrderList(SysWorkOrder sysWorkOrder); + + /** + * 新增工单主 + * + * @param sysWorkOrder 工单主 + * @return 结果 + */ + public int insertSysWorkOrder(SysWorkOrder sysWorkOrder); + + /** + * 修改工单主 + * + * @param sysWorkOrder 工单主 + * @return 结果 + */ + public int updateSysWorkOrder(SysWorkOrder sysWorkOrder); + + /** + * 删除工单主 + * + * @param orderId 工单主主键 + * @return 结果 + */ + public int deleteSysWorkOrderByOrderId(Long orderId); + + /** + * 批量删除工单主 + * + * @param orderIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteSysWorkOrderByOrderIds(Long[] orderIds); +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/mapper/SysWorkOrderRecordMapper.java b/storm-afterSales/src/main/java/com/storm/afterSales/mapper/SysWorkOrderRecordMapper.java new file mode 100644 index 0000000..d8dc6ec --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/mapper/SysWorkOrderRecordMapper.java @@ -0,0 +1,64 @@ +package com.storm.afterSales.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import java.util.List; +import com.storm.afterSales.domain.SysWorkOrderRecord; + +/** + * 工单处理记录Mapper接口 + * + * @author storm + * @date 2025-10-02 + */ +@Mapper +public interface SysWorkOrderRecordMapper extends BaseMapper +{ + /** + * 查询工单处理记录 + * + * @param recordId 工单处理记录主键 + * @return 工单处理记录 + */ + public SysWorkOrderRecord selectSysWorkOrderRecordByRecordId(Long recordId); + + /** + * 查询工单处理记录列表 + * + * @param sysWorkOrderRecord 工单处理记录 + * @return 工单处理记录集合 + */ + public List selectSysWorkOrderRecordList(SysWorkOrderRecord sysWorkOrderRecord); + + /** + * 新增工单处理记录 + * + * @param sysWorkOrderRecord 工单处理记录 + * @return 结果 + */ + public int insertSysWorkOrderRecord(SysWorkOrderRecord sysWorkOrderRecord); + + /** + * 修改工单处理记录 + * + * @param sysWorkOrderRecord 工单处理记录 + * @return 结果 + */ + public int updateSysWorkOrderRecord(SysWorkOrderRecord sysWorkOrderRecord); + + /** + * 删除工单处理记录 + * + * @param recordId 工单处理记录主键 + * @return 结果 + */ + public int deleteSysWorkOrderRecordByRecordId(Long recordId); + + /** + * 批量删除工单处理记录 + * + * @param recordIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteSysWorkOrderRecordByRecordIds(Long[] recordIds); +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/mapper/SysWorkOrderTypeMapper.java b/storm-afterSales/src/main/java/com/storm/afterSales/mapper/SysWorkOrderTypeMapper.java new file mode 100644 index 0000000..eaa7974 --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/mapper/SysWorkOrderTypeMapper.java @@ -0,0 +1,64 @@ +package com.storm.afterSales.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import java.util.List; +import com.storm.afterSales.domain.SysWorkOrderType; + +/** + * 工单类型Mapper接口 + * + * @author storm + * @date 2025-10-02 + */ +@Mapper +public interface SysWorkOrderTypeMapper extends BaseMapper +{ + /** + * 查询工单类型 + * + * @param typeId 工单类型主键 + * @return 工单类型 + */ + public SysWorkOrderType selectSysWorkOrderTypeByTypeId(Long typeId); + + /** + * 查询工单类型列表 + * + * @param sysWorkOrderType 工单类型 + * @return 工单类型集合 + */ + public List selectSysWorkOrderTypeList(SysWorkOrderType sysWorkOrderType); + + /** + * 新增工单类型 + * + * @param sysWorkOrderType 工单类型 + * @return 结果 + */ + public int insertSysWorkOrderType(SysWorkOrderType sysWorkOrderType); + + /** + * 修改工单类型 + * + * @param sysWorkOrderType 工单类型 + * @return 结果 + */ + public int updateSysWorkOrderType(SysWorkOrderType sysWorkOrderType); + + /** + * 删除工单类型 + * + * @param typeId 工单类型主键 + * @return 结果 + */ + public int deleteSysWorkOrderTypeByTypeId(Long typeId); + + /** + * 批量删除工单类型 + * + * @param typeIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteSysWorkOrderTypeByTypeIds(Long[] typeIds); +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/service/ISysKnowledgeRelService.java b/storm-afterSales/src/main/java/com/storm/afterSales/service/ISysKnowledgeRelService.java new file mode 100644 index 0000000..d80fd8e --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/service/ISysKnowledgeRelService.java @@ -0,0 +1,63 @@ +package com.storm.afterSales.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; +import com.storm.afterSales.domain.SysKnowledgeRel; + +/** + * 工单知识库关联Service接口 + * + * @author storm + * @date 2025-10-02 + */ +public interface ISysKnowledgeRelService extends IService +{ + /** + * 查询工单知识库关联 + * + * @param id 工单知识库关联主键 + * @return 工单知识库关联 + */ + public SysKnowledgeRel selectSysKnowledgeRelById(Long id); + + /** + * 查询工单知识库关联列表 + * + * @param sysKnowledgeRel 工单知识库关联 + * @return 工单知识库关联集合 + */ + public List selectSysKnowledgeRelList(SysKnowledgeRel sysKnowledgeRel); + + /** + * 新增工单知识库关联 + * + * @param sysKnowledgeRel 工单知识库关联 + * @return 结果 + */ + public int insertSysKnowledgeRel(SysKnowledgeRel sysKnowledgeRel); + + /** + * 修改工单知识库关联 + * + * @param sysKnowledgeRel 工单知识库关联 + * @return 结果 + */ + public int updateSysKnowledgeRel(SysKnowledgeRel sysKnowledgeRel); + + /** + * 批量删除工单知识库关联 + * + * @param ids 需要删除的工单知识库关联主键集合 + * @return 结果 + */ + public int deleteSysKnowledgeRelByIds(Long[] ids); + + /** + * 删除工单知识库关联信息 + * + * @param id 工单知识库关联主键 + * @return 结果 + */ + public int deleteSysKnowledgeRelById(Long id); +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/service/ISysWorkOrderChatRelService.java b/storm-afterSales/src/main/java/com/storm/afterSales/service/ISysWorkOrderChatRelService.java new file mode 100644 index 0000000..d368b65 --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/service/ISysWorkOrderChatRelService.java @@ -0,0 +1,63 @@ +package com.storm.afterSales.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; +import com.storm.afterSales.domain.SysWorkOrderChatRel; + +/** + * 工单聊天关联Service接口 + * + * @author storm + * @date 2025-10-02 + */ +public interface ISysWorkOrderChatRelService extends IService +{ + /** + * 查询工单聊天关联 + * + * @param id 工单聊天关联主键 + * @return 工单聊天关联 + */ + public SysWorkOrderChatRel selectSysWorkOrderChatRelById(Long id); + + /** + * 查询工单聊天关联列表 + * + * @param sysWorkOrderChatRel 工单聊天关联 + * @return 工单聊天关联集合 + */ + public List selectSysWorkOrderChatRelList(SysWorkOrderChatRel sysWorkOrderChatRel); + + /** + * 新增工单聊天关联 + * + * @param sysWorkOrderChatRel 工单聊天关联 + * @return 结果 + */ + public int insertSysWorkOrderChatRel(SysWorkOrderChatRel sysWorkOrderChatRel); + + /** + * 修改工单聊天关联 + * + * @param sysWorkOrderChatRel 工单聊天关联 + * @return 结果 + */ + public int updateSysWorkOrderChatRel(SysWorkOrderChatRel sysWorkOrderChatRel); + + /** + * 批量删除工单聊天关联 + * + * @param ids 需要删除的工单聊天关联主键集合 + * @return 结果 + */ + public int deleteSysWorkOrderChatRelByIds(Long[] ids); + + /** + * 删除工单聊天关联信息 + * + * @param id 工单聊天关联主键 + * @return 结果 + */ + public int deleteSysWorkOrderChatRelById(Long id); +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/service/ISysWorkOrderRecordService.java b/storm-afterSales/src/main/java/com/storm/afterSales/service/ISysWorkOrderRecordService.java new file mode 100644 index 0000000..781a256 --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/service/ISysWorkOrderRecordService.java @@ -0,0 +1,63 @@ +package com.storm.afterSales.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; +import com.storm.afterSales.domain.SysWorkOrderRecord; + +/** + * 工单处理记录Service接口 + * + * @author storm + * @date 2025-10-02 + */ +public interface ISysWorkOrderRecordService extends IService +{ + /** + * 查询工单处理记录 + * + * @param recordId 工单处理记录主键 + * @return 工单处理记录 + */ + public SysWorkOrderRecord selectSysWorkOrderRecordByRecordId(Long recordId); + + /** + * 查询工单处理记录列表 + * + * @param sysWorkOrderRecord 工单处理记录 + * @return 工单处理记录集合 + */ + public List selectSysWorkOrderRecordList(SysWorkOrderRecord sysWorkOrderRecord); + + /** + * 新增工单处理记录 + * + * @param sysWorkOrderRecord 工单处理记录 + * @return 结果 + */ + public int insertSysWorkOrderRecord(SysWorkOrderRecord sysWorkOrderRecord); + + /** + * 修改工单处理记录 + * + * @param sysWorkOrderRecord 工单处理记录 + * @return 结果 + */ + public int updateSysWorkOrderRecord(SysWorkOrderRecord sysWorkOrderRecord); + + /** + * 批量删除工单处理记录 + * + * @param recordIds 需要删除的工单处理记录主键集合 + * @return 结果 + */ + public int deleteSysWorkOrderRecordByRecordIds(Long[] recordIds); + + /** + * 删除工单处理记录信息 + * + * @param recordId 工单处理记录主键 + * @return 结果 + */ + public int deleteSysWorkOrderRecordByRecordId(Long recordId); +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/service/ISysWorkOrderService.java b/storm-afterSales/src/main/java/com/storm/afterSales/service/ISysWorkOrderService.java new file mode 100644 index 0000000..c97be93 --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/service/ISysWorkOrderService.java @@ -0,0 +1,63 @@ +package com.storm.afterSales.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; +import com.storm.afterSales.domain.SysWorkOrder; + +/** + * 工单主Service接口 + * + * @author storm + * @date 2025-10-02 + */ +public interface ISysWorkOrderService extends IService +{ + /** + * 查询工单主 + * + * @param orderId 工单主主键 + * @return 工单主 + */ + public SysWorkOrder selectSysWorkOrderByOrderId(Long orderId); + + /** + * 查询工单主列表 + * + * @param sysWorkOrder 工单主 + * @return 工单主集合 + */ + public List selectSysWorkOrderList(SysWorkOrder sysWorkOrder); + + /** + * 新增工单主 + * + * @param sysWorkOrder 工单主 + * @return 结果 + */ + public int insertSysWorkOrder(SysWorkOrder sysWorkOrder); + + /** + * 修改工单主 + * + * @param sysWorkOrder 工单主 + * @return 结果 + */ + public int updateSysWorkOrder(SysWorkOrder sysWorkOrder); + + /** + * 批量删除工单主 + * + * @param orderIds 需要删除的工单主主键集合 + * @return 结果 + */ + public int deleteSysWorkOrderByOrderIds(Long[] orderIds); + + /** + * 删除工单主信息 + * + * @param orderId 工单主主键 + * @return 结果 + */ + public int deleteSysWorkOrderByOrderId(Long orderId); +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/service/ISysWorkOrderTypeService.java b/storm-afterSales/src/main/java/com/storm/afterSales/service/ISysWorkOrderTypeService.java new file mode 100644 index 0000000..a5bea7d --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/service/ISysWorkOrderTypeService.java @@ -0,0 +1,63 @@ +package com.storm.afterSales.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; +import com.storm.afterSales.domain.SysWorkOrderType; + +/** + * 工单类型Service接口 + * + * @author storm + * @date 2025-10-02 + */ +public interface ISysWorkOrderTypeService extends IService +{ + /** + * 查询工单类型 + * + * @param typeId 工单类型主键 + * @return 工单类型 + */ + public SysWorkOrderType selectSysWorkOrderTypeByTypeId(Long typeId); + + /** + * 查询工单类型列表 + * + * @param sysWorkOrderType 工单类型 + * @return 工单类型集合 + */ + public List selectSysWorkOrderTypeList(SysWorkOrderType sysWorkOrderType); + + /** + * 新增工单类型 + * + * @param sysWorkOrderType 工单类型 + * @return 结果 + */ + public int insertSysWorkOrderType(SysWorkOrderType sysWorkOrderType); + + /** + * 修改工单类型 + * + * @param sysWorkOrderType 工单类型 + * @return 结果 + */ + public int updateSysWorkOrderType(SysWorkOrderType sysWorkOrderType); + + /** + * 批量删除工单类型 + * + * @param typeIds 需要删除的工单类型主键集合 + * @return 结果 + */ + public int deleteSysWorkOrderTypeByTypeIds(Long[] typeIds); + + /** + * 删除工单类型信息 + * + * @param typeId 工单类型主键 + * @return 结果 + */ + public int deleteSysWorkOrderTypeByTypeId(Long typeId); +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/service/impl/SysKnowledgeRelServiceImpl.java b/storm-afterSales/src/main/java/com/storm/afterSales/service/impl/SysKnowledgeRelServiceImpl.java new file mode 100644 index 0000000..ecbf587 --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/service/impl/SysKnowledgeRelServiceImpl.java @@ -0,0 +1,96 @@ +package com.storm.afterSales.service.impl; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.storm.afterSales.mapper.SysKnowledgeRelMapper; +import com.storm.afterSales.domain.SysKnowledgeRel; +import com.storm.afterSales.service.ISysKnowledgeRelService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import java.util.Arrays; + +/** + * 工单知识库关联Service业务层处理 + * + * @author storm + * @date 2025-10-02 + */ +@Service +public class SysKnowledgeRelServiceImpl extends ServiceImpl implements ISysKnowledgeRelService +{ + @Autowired + private SysKnowledgeRelMapper sysKnowledgeRelMapper; + + /** + * 查询工单知识库关联 + * + * @param id 工单知识库关联主键 + * @return 工单知识库关联 + */ + @Override + public SysKnowledgeRel selectSysKnowledgeRelById(Long id) + { + return getById(id); + } + + /** + * 查询工单知识库关联列表 + * + * @param sysKnowledgeRel 工单知识库关联 + * @return 工单知识库关联 + */ + @Override + public List selectSysKnowledgeRelList(SysKnowledgeRel sysKnowledgeRel) + { + return sysKnowledgeRelMapper.selectSysKnowledgeRelList(sysKnowledgeRel); + } + + /** + * 新增工单知识库关联 + * + * @param sysKnowledgeRel 工单知识库关联 + * @return 结果 + */ + @Override + public int insertSysKnowledgeRel(SysKnowledgeRel sysKnowledgeRel) + { + return save(sysKnowledgeRel) ? 1 : 0; + } + + /** + * 修改工单知识库关联 + * + * @param sysKnowledgeRel 工单知识库关联 + * @return 结果 + */ + @Override + public int updateSysKnowledgeRel(SysKnowledgeRel sysKnowledgeRel) + { + return updateById(sysKnowledgeRel) ? 1 : 0; + } + + /** + * 批量删除工单知识库关联 + * + * @param ids 需要删除的工单知识库关联主键 + * @return 结果 + */ + @Override + public int deleteSysKnowledgeRelByIds(Long[] ids) + { + return removeByIds(Arrays.asList(ids)) ? 1 : 0; + } + + /** + * 删除工单知识库关联信息 + * + * @param id 工单知识库关联主键 + * @return 结果 + */ + @Override + public int deleteSysKnowledgeRelById(Long id) + { + return removeById(id) ? 1 : 0; + } +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/service/impl/SysWorkOrderChatRelServiceImpl.java b/storm-afterSales/src/main/java/com/storm/afterSales/service/impl/SysWorkOrderChatRelServiceImpl.java new file mode 100644 index 0000000..1ee9323 --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/service/impl/SysWorkOrderChatRelServiceImpl.java @@ -0,0 +1,96 @@ +package com.storm.afterSales.service.impl; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.storm.afterSales.mapper.SysWorkOrderChatRelMapper; +import com.storm.afterSales.domain.SysWorkOrderChatRel; +import com.storm.afterSales.service.ISysWorkOrderChatRelService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import java.util.Arrays; + +/** + * 工单聊天关联Service业务层处理 + * + * @author storm + * @date 2025-10-02 + */ +@Service +public class SysWorkOrderChatRelServiceImpl extends ServiceImpl implements ISysWorkOrderChatRelService +{ + @Autowired + private SysWorkOrderChatRelMapper sysWorkOrderChatRelMapper; + + /** + * 查询工单聊天关联 + * + * @param id 工单聊天关联主键 + * @return 工单聊天关联 + */ + @Override + public SysWorkOrderChatRel selectSysWorkOrderChatRelById(Long id) + { + return getById(id); + } + + /** + * 查询工单聊天关联列表 + * + * @param sysWorkOrderChatRel 工单聊天关联 + * @return 工单聊天关联 + */ + @Override + public List selectSysWorkOrderChatRelList(SysWorkOrderChatRel sysWorkOrderChatRel) + { + return sysWorkOrderChatRelMapper.selectSysWorkOrderChatRelList(sysWorkOrderChatRel); + } + + /** + * 新增工单聊天关联 + * + * @param sysWorkOrderChatRel 工单聊天关联 + * @return 结果 + */ + @Override + public int insertSysWorkOrderChatRel(SysWorkOrderChatRel sysWorkOrderChatRel) + { + return save(sysWorkOrderChatRel) ? 1 : 0; + } + + /** + * 修改工单聊天关联 + * + * @param sysWorkOrderChatRel 工单聊天关联 + * @return 结果 + */ + @Override + public int updateSysWorkOrderChatRel(SysWorkOrderChatRel sysWorkOrderChatRel) + { + return updateById(sysWorkOrderChatRel) ? 1 : 0; + } + + /** + * 批量删除工单聊天关联 + * + * @param ids 需要删除的工单聊天关联主键 + * @return 结果 + */ + @Override + public int deleteSysWorkOrderChatRelByIds(Long[] ids) + { + return removeByIds(Arrays.asList(ids)) ? 1 : 0; + } + + /** + * 删除工单聊天关联信息 + * + * @param id 工单聊天关联主键 + * @return 结果 + */ + @Override + public int deleteSysWorkOrderChatRelById(Long id) + { + return removeById(id) ? 1 : 0; + } +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/service/impl/SysWorkOrderRecordServiceImpl.java b/storm-afterSales/src/main/java/com/storm/afterSales/service/impl/SysWorkOrderRecordServiceImpl.java new file mode 100644 index 0000000..3ea8867 --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/service/impl/SysWorkOrderRecordServiceImpl.java @@ -0,0 +1,95 @@ +package com.storm.afterSales.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.storm.afterSales.mapper.SysWorkOrderRecordMapper; +import com.storm.afterSales.domain.SysWorkOrderRecord; +import com.storm.afterSales.service.ISysWorkOrderRecordService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import java.util.Arrays; + +/** + * 工单处理记录Service业务层处理 + * + * @author storm + * @date 2025-10-02 + */ +@Service +public class SysWorkOrderRecordServiceImpl extends ServiceImpl implements ISysWorkOrderRecordService +{ + @Autowired + private SysWorkOrderRecordMapper sysWorkOrderRecordMapper; + + /** + * 查询工单处理记录 + * + * @param recordId 工单处理记录主键 + * @return 工单处理记录 + */ + @Override + public SysWorkOrderRecord selectSysWorkOrderRecordByRecordId(Long recordId) + { + return getById(recordId); + } + + /** + * 查询工单处理记录列表 + * + * @param sysWorkOrderRecord 工单处理记录 + * @return 工单处理记录 + */ + @Override + public List selectSysWorkOrderRecordList(SysWorkOrderRecord sysWorkOrderRecord) + { + return sysWorkOrderRecordMapper.selectSysWorkOrderRecordList(sysWorkOrderRecord); + } + + /** + * 新增工单处理记录 + * + * @param sysWorkOrderRecord 工单处理记录 + * @return 结果 + */ + @Override + public int insertSysWorkOrderRecord(SysWorkOrderRecord sysWorkOrderRecord) + { + return save(sysWorkOrderRecord) ? 1 : 0; + } + + /** + * 修改工单处理记录 + * + * @param sysWorkOrderRecord 工单处理记录 + * @return 结果 + */ + @Override + public int updateSysWorkOrderRecord(SysWorkOrderRecord sysWorkOrderRecord) + { + return updateById(sysWorkOrderRecord) ? 1 : 0; + } + + /** + * 批量删除工单处理记录 + * + * @param recordIds 需要删除的工单处理记录主键 + * @return 结果 + */ + @Override + public int deleteSysWorkOrderRecordByRecordIds(Long[] recordIds) + { + return removeByIds(Arrays.asList(recordIds)) ? 1 : 0; + } + + /** + * 删除工单处理记录信息 + * + * @param recordId 工单处理记录主键 + * @return 结果 + */ + @Override + public int deleteSysWorkOrderRecordByRecordId(Long recordId) + { + return removeById(recordId) ? 1 : 0; + } +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/service/impl/SysWorkOrderServiceImpl.java b/storm-afterSales/src/main/java/com/storm/afterSales/service/impl/SysWorkOrderServiceImpl.java new file mode 100644 index 0000000..0b4ecca --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/service/impl/SysWorkOrderServiceImpl.java @@ -0,0 +1,96 @@ +package com.storm.afterSales.service.impl; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.storm.afterSales.mapper.SysWorkOrderMapper; +import com.storm.afterSales.domain.SysWorkOrder; +import com.storm.afterSales.service.ISysWorkOrderService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import java.util.Arrays; + +/** + * 工单主Service业务层处理 + * + * @author storm + * @date 2025-10-02 + */ +@Service +public class SysWorkOrderServiceImpl extends ServiceImpl implements ISysWorkOrderService +{ + @Autowired + private SysWorkOrderMapper sysWorkOrderMapper; + + /** + * 查询工单主 + * + * @param orderId 工单主主键 + * @return 工单主 + */ + @Override + public SysWorkOrder selectSysWorkOrderByOrderId(Long orderId) + { + return getById(orderId); + } + + /** + * 查询工单主列表 + * + * @param sysWorkOrder 工单主 + * @return 工单主 + */ + @Override + public List selectSysWorkOrderList(SysWorkOrder sysWorkOrder) + { + return sysWorkOrderMapper.selectSysWorkOrderList(sysWorkOrder); + } + + /** + * 新增工单主 + * + * @param sysWorkOrder 工单主 + * @return 结果 + */ + @Override + public int insertSysWorkOrder(SysWorkOrder sysWorkOrder) + { + return save(sysWorkOrder) ? 1 : 0; + } + + /** + * 修改工单主 + * + * @param sysWorkOrder 工单主 + * @return 结果 + */ + @Override + public int updateSysWorkOrder(SysWorkOrder sysWorkOrder) + { + return updateById(sysWorkOrder) ? 1 : 0; + } + + /** + * 批量删除工单主 + * + * @param orderIds 需要删除的工单主主键 + * @return 结果 + */ + @Override + public int deleteSysWorkOrderByOrderIds(Long[] orderIds) + { + return removeByIds(Arrays.asList(orderIds)) ? 1 : 0; + } + + /** + * 删除工单主信息 + * + * @param orderId 工单主主键 + * @return 结果 + */ + @Override + public int deleteSysWorkOrderByOrderId(Long orderId) + { + return removeById(orderId) ? 1 : 0; + } +} diff --git a/storm-afterSales/src/main/java/com/storm/afterSales/service/impl/SysWorkOrderTypeServiceImpl.java b/storm-afterSales/src/main/java/com/storm/afterSales/service/impl/SysWorkOrderTypeServiceImpl.java new file mode 100644 index 0000000..b176cc3 --- /dev/null +++ b/storm-afterSales/src/main/java/com/storm/afterSales/service/impl/SysWorkOrderTypeServiceImpl.java @@ -0,0 +1,96 @@ +package com.storm.afterSales.service.impl; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.storm.afterSales.mapper.SysWorkOrderTypeMapper; +import com.storm.afterSales.domain.SysWorkOrderType; +import com.storm.afterSales.service.ISysWorkOrderTypeService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import java.util.Arrays; + +/** + * 工单类型Service业务层处理 + * + * @author storm + * @date 2025-10-02 + */ +@Service +public class SysWorkOrderTypeServiceImpl extends ServiceImpl implements ISysWorkOrderTypeService +{ + @Autowired + private SysWorkOrderTypeMapper sysWorkOrderTypeMapper; + + /** + * 查询工单类型 + * + * @param typeId 工单类型主键 + * @return 工单类型 + */ + @Override + public SysWorkOrderType selectSysWorkOrderTypeByTypeId(Long typeId) + { + return getById(typeId); + } + + /** + * 查询工单类型列表 + * + * @param sysWorkOrderType 工单类型 + * @return 工单类型 + */ + @Override + public List selectSysWorkOrderTypeList(SysWorkOrderType sysWorkOrderType) + { + return sysWorkOrderTypeMapper.selectSysWorkOrderTypeList(sysWorkOrderType); + } + + /** + * 新增工单类型 + * + * @param sysWorkOrderType 工单类型 + * @return 结果 + */ + @Override + public int insertSysWorkOrderType(SysWorkOrderType sysWorkOrderType) + { + return save(sysWorkOrderType) ? 1 : 0; + } + + /** + * 修改工单类型 + * + * @param sysWorkOrderType 工单类型 + * @return 结果 + */ + @Override + public int updateSysWorkOrderType(SysWorkOrderType sysWorkOrderType) + { + return updateById(sysWorkOrderType) ? 1 : 0; + } + + /** + * 批量删除工单类型 + * + * @param typeIds 需要删除的工单类型主键 + * @return 结果 + */ + @Override + public int deleteSysWorkOrderTypeByTypeIds(Long[] typeIds) + { + return removeByIds(Arrays.asList(typeIds)) ? 1 : 0; + } + + /** + * 删除工单类型信息 + * + * @param typeId 工单类型主键 + * @return 结果 + */ + @Override + public int deleteSysWorkOrderTypeByTypeId(Long typeId) + { + return removeById(typeId) ? 1 : 0; + } +} diff --git a/storm-afterSales/src/main/resources/application.properties b/storm-afterSales/src/main/resources/application.properties new file mode 100644 index 0000000..c13ff87 --- /dev/null +++ b/storm-afterSales/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=storm-afterSales diff --git a/storm-afterSales/src/main/resources/mapper/SysKnowledgeRelMapper.xml b/storm-afterSales/src/main/resources/mapper/SysKnowledgeRelMapper.xml new file mode 100644 index 0000000..6394549 --- /dev/null +++ b/storm-afterSales/src/main/resources/mapper/SysKnowledgeRelMapper.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + select id, order_id, knowledge_id, knowledge_title, used_count, create_time from sys_knowledge_rel + + + + + + + + insert into sys_knowledge_rel + + order_id, + knowledge_id, + knowledge_title, + used_count, + create_time, + + + #{orderId}, + #{knowledgeId}, + #{knowledgeTitle}, + #{usedCount}, + #{createTime}, + + + + + update sys_knowledge_rel + + order_id = #{orderId}, + knowledge_id = #{knowledgeId}, + knowledge_title = #{knowledgeTitle}, + used_count = #{usedCount}, + create_time = #{createTime}, + + where id = #{id} + + + + delete from sys_knowledge_rel where id = #{id} + + + + delete from sys_knowledge_rel where id in + + #{id} + + + \ No newline at end of file diff --git a/storm-afterSales/src/main/resources/mapper/SysWorkOrderChatRelMapper.xml b/storm-afterSales/src/main/resources/mapper/SysWorkOrderChatRelMapper.xml new file mode 100644 index 0000000..c3c2dae --- /dev/null +++ b/storm-afterSales/src/main/resources/mapper/SysWorkOrderChatRelMapper.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + select id, order_id, chat_session_id, chat_platform, message_id, chat_content, chat_duration, create_time from sys_work_order_chat_rel + + + + + + + + insert into sys_work_order_chat_rel + + order_id, + chat_session_id, + chat_platform, + message_id, + chat_content, + chat_duration, + create_time, + + + #{orderId}, + #{chatSessionId}, + #{chatPlatform}, + #{messageId}, + #{chatContent}, + #{chatDuration}, + #{createTime}, + + + + + update sys_work_order_chat_rel + + order_id = #{orderId}, + chat_session_id = #{chatSessionId}, + chat_platform = #{chatPlatform}, + message_id = #{messageId}, + chat_content = #{chatContent}, + chat_duration = #{chatDuration}, + create_time = #{createTime}, + + where id = #{id} + + + + delete from sys_work_order_chat_rel where id = #{id} + + + + delete from sys_work_order_chat_rel where id in + + #{id} + + + \ No newline at end of file diff --git a/storm-afterSales/src/main/resources/mapper/SysWorkOrderMapper.xml b/storm-afterSales/src/main/resources/mapper/SysWorkOrderMapper.xml new file mode 100644 index 0000000..10ec074 --- /dev/null +++ b/storm-afterSales/src/main/resources/mapper/SysWorkOrderMapper.xml @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select order_id, order_no, order_title, order_type, order_status, priority, customer_id, customer_name, customer_contact, store_address, device_name, device_serial_no, device_model, production_date, problem_description, attachment_urls, problem_category, assignee_id, assignee_name, assign_time, current_handler_id, current_handler_name, root_cause, solution_plan, final_solution, logistics_number, logistics_progress, customer_feedback, satisfaction_level, feedback_type, plan_finish_time, actual_finish_time, response_deadline, source_channel, feishu_chat_id, create_by, create_time, update_by, update_time, remark from sys_work_order + + + + + + + + insert into sys_work_order + + order_no, + order_title, + order_type, + order_status, + priority, + customer_id, + customer_name, + customer_contact, + store_address, + device_name, + device_serial_no, + device_model, + production_date, + problem_description, + attachment_urls, + problem_category, + assignee_id, + assignee_name, + assign_time, + current_handler_id, + current_handler_name, + root_cause, + solution_plan, + final_solution, + logistics_number, + logistics_progress, + customer_feedback, + satisfaction_level, + feedback_type, + plan_finish_time, + actual_finish_time, + response_deadline, + source_channel, + feishu_chat_id, + create_by, + create_time, + update_by, + update_time, + remark, + + + #{orderNo}, + #{orderTitle}, + #{orderType}, + #{orderStatus}, + #{priority}, + #{customerId}, + #{customerName}, + #{customerContact}, + #{storeAddress}, + #{deviceName}, + #{deviceSerialNo}, + #{deviceModel}, + #{productionDate}, + #{problemDescription}, + #{attachmentUrls}, + #{problemCategory}, + #{assigneeId}, + #{assigneeName}, + #{assignTime}, + #{currentHandlerId}, + #{currentHandlerName}, + #{rootCause}, + #{solutionPlan}, + #{finalSolution}, + #{logisticsNumber}, + #{logisticsProgress}, + #{customerFeedback}, + #{satisfactionLevel}, + #{feedbackType}, + #{planFinishTime}, + #{actualFinishTime}, + #{responseDeadline}, + #{sourceChannel}, + #{feishuChatId}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + #{remark}, + + + + + update sys_work_order + + order_no = #{orderNo}, + order_title = #{orderTitle}, + order_type = #{orderType}, + order_status = #{orderStatus}, + priority = #{priority}, + customer_id = #{customerId}, + customer_name = #{customerName}, + customer_contact = #{customerContact}, + store_address = #{storeAddress}, + device_name = #{deviceName}, + device_serial_no = #{deviceSerialNo}, + device_model = #{deviceModel}, + production_date = #{productionDate}, + problem_description = #{problemDescription}, + attachment_urls = #{attachmentUrls}, + problem_category = #{problemCategory}, + assignee_id = #{assigneeId}, + assignee_name = #{assigneeName}, + assign_time = #{assignTime}, + current_handler_id = #{currentHandlerId}, + current_handler_name = #{currentHandlerName}, + root_cause = #{rootCause}, + solution_plan = #{solutionPlan}, + final_solution = #{finalSolution}, + logistics_number = #{logisticsNumber}, + logistics_progress = #{logisticsProgress}, + customer_feedback = #{customerFeedback}, + satisfaction_level = #{satisfactionLevel}, + feedback_type = #{feedbackType}, + plan_finish_time = #{planFinishTime}, + actual_finish_time = #{actualFinishTime}, + response_deadline = #{responseDeadline}, + source_channel = #{sourceChannel}, + feishu_chat_id = #{feishuChatId}, + create_by = #{createBy}, + create_time = #{createTime}, + update_by = #{updateBy}, + update_time = #{updateTime}, + remark = #{remark}, + + where order_id = #{orderId} + + + + delete from sys_work_order where order_id = #{orderId} + + + + delete from sys_work_order where order_id in + + #{orderId} + + + \ No newline at end of file diff --git a/storm-afterSales/src/main/resources/mapper/SysWorkOrderRecordMapper.xml b/storm-afterSales/src/main/resources/mapper/SysWorkOrderRecordMapper.xml new file mode 100644 index 0000000..36bb8d0 --- /dev/null +++ b/storm-afterSales/src/main/resources/mapper/SysWorkOrderRecordMapper.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + select record_id, order_id, process_type, process_content, progress_type, progress_value, progress_percent, process_user_id, process_user_name, process_time, attachment_url from sys_work_order_record + + + + + + + + insert into sys_work_order_record + + order_id, + process_type, + process_content, + progress_type, + progress_value, + progress_percent, + process_user_id, + process_user_name, + process_time, + attachment_url, + + + #{orderId}, + #{processType}, + #{processContent}, + #{progressType}, + #{progressValue}, + #{progressPercent}, + #{processUserId}, + #{processUserName}, + #{processTime}, + #{attachmentUrl}, + + + + + update sys_work_order_record + + order_id = #{orderId}, + process_type = #{processType}, + process_content = #{processContent}, + progress_type = #{progressType}, + progress_value = #{progressValue}, + progress_percent = #{progressPercent}, + process_user_id = #{processUserId}, + process_user_name = #{processUserName}, + process_time = #{processTime}, + attachment_url = #{attachmentUrl}, + + where record_id = #{recordId} + + + + delete from sys_work_order_record where record_id = #{recordId} + + + + delete from sys_work_order_record where record_id in + + #{recordId} + + + \ No newline at end of file diff --git a/storm-afterSales/src/main/resources/mapper/SysWorkOrderTypeMapper.xml b/storm-afterSales/src/main/resources/mapper/SysWorkOrderTypeMapper.xml new file mode 100644 index 0000000..e47c00a --- /dev/null +++ b/storm-afterSales/src/main/resources/mapper/SysWorkOrderTypeMapper.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + select type_id, type_name, type_key, STATUS, order_num, create_by, create_time, update_by, update_time, remark from sys_work_order_type + + + + + + + + insert into sys_work_order_type + + type_name, + type_key, + STATUS, + order_num, + create_by, + create_time, + update_by, + update_time, + remark, + + + #{typeName}, + #{typeKey}, + #{STATUS}, + #{orderNum}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + #{remark}, + + + + + update sys_work_order_type + + type_name = #{typeName}, + type_key = #{typeKey}, + STATUS = #{STATUS}, + order_num = #{orderNum}, + create_by = #{createBy}, + create_time = #{createTime}, + update_by = #{updateBy}, + update_time = #{updateTime}, + remark = #{remark}, + + where type_id = #{typeId} + + + + delete from sys_work_order_type where type_id = #{typeId} + + + + delete from sys_work_order_type where type_id in + + #{typeId} + + + \ No newline at end of file diff --git a/storm-common/storm-common-oss/pom.xml b/storm-common/storm-common-oss/pom.xml index 0161089..70ad6da 100644 --- a/storm-common/storm-common-oss/pom.xml +++ b/storm-common/storm-common-oss/pom.xml @@ -13,8 +13,8 @@ storm-common-oss - 11 - 11 + 17 + 17 UTF-8 diff --git a/storm-device/pom.xml b/storm-device/pom.xml index c0300f8..216c485 100644 --- a/storm-device/pom.xml +++ b/storm-device/pom.xml @@ -12,8 +12,8 @@ storm-device - 11 - 11 + 17 + 17 UTF-8 @@ -104,12 +104,6 @@ huaweicloud-sdk-iotda - - org.springframework.boot - spring-boot-starter-test - test - - org.apache.qpid qpid-jms-client diff --git a/storm-device/src/main/java/com/storm/device/domain/po/DeviceHeadUsage.java b/storm-device/src/main/java/com/storm/device/domain/po/DeviceHeadUsage.java index d3ef285..f3b60d6 100644 --- a/storm-device/src/main/java/com/storm/device/domain/po/DeviceHeadUsage.java +++ b/storm-device/src/main/java/com/storm/device/domain/po/DeviceHeadUsage.java @@ -1,5 +1,6 @@ package com.storm.device.domain.po; +import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import com.storm.common.core.annotation.Excel; import com.storm.common.core.web.domain.BaseEntity; @@ -70,4 +71,12 @@ public class DeviceHeadUsage extends BaseEntity @Excel(name = "最后使用时间") @Schema(description = "最后使用时间") private Date lastUsedTime; + + private String description; + private String remark; + + @TableLogic + private Integer isDeleted; + private String createBy; + private String updateBy; } \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/domain/po/DeviceRuntimeStats.java b/storm-device/src/main/java/com/storm/device/domain/po/DeviceRuntimeStats.java index 79aa411..8187cb4 100644 --- a/storm-device/src/main/java/com/storm/device/domain/po/DeviceRuntimeStats.java +++ b/storm-device/src/main/java/com/storm/device/domain/po/DeviceRuntimeStats.java @@ -68,4 +68,12 @@ public class DeviceRuntimeStats extends BaseEntity @TableField(fill = FieldFill.INSERT_UPDATE) private Date updateTime; + + private String description; + private String remark; + + @TableLogic + private Integer isDeleted; + private String createBy; + private String updateBy; } \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/domain/po/DeviceStatusLog.java b/storm-device/src/main/java/com/storm/device/domain/po/DeviceStatusLog.java index 7ded343..d3cc98b 100644 --- a/storm-device/src/main/java/com/storm/device/domain/po/DeviceStatusLog.java +++ b/storm-device/src/main/java/com/storm/device/domain/po/DeviceStatusLog.java @@ -55,6 +55,9 @@ public class DeviceStatusLog { @TableField(exist = false) private Date endTimeDate; + private String description; + private String remark; + public Date getStartTimeDate() { return massageStartTime != null ? new Date(massageStartTime) : null; } diff --git a/storm-device/src/main/java/com/storm/device/domain/po/MassageTask.java b/storm-device/src/main/java/com/storm/device/domain/po/MassageTask.java index c288cb7..0641598 100644 --- a/storm-device/src/main/java/com/storm/device/domain/po/MassageTask.java +++ b/storm-device/src/main/java/com/storm/device/domain/po/MassageTask.java @@ -41,6 +41,13 @@ public class MassageTask { @TableField(exist = false) private Date endTimeDate; + private String description; + private String remark; + + @TableLogic + private Integer isDeleted; + private String updateBy; + public Date getStartTimeDate() { return startTime != null ? new Date(startTime) : null; } diff --git a/storm-device/src/main/java/com/storm/device/processor/DeviceDataCoordinator.java b/storm-device/src/main/java/com/storm/device/processor/DeviceDataCoordinator.java index 68c79e2..2aef64c 100644 --- a/storm-device/src/main/java/com/storm/device/processor/DeviceDataCoordinator.java +++ b/storm-device/src/main/java/com/storm/device/processor/DeviceDataCoordinator.java @@ -13,11 +13,11 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; +import java.time.Duration; import java.util.Date; import java.util.Map; import java.util.Objects; +import java.util.function.Consumer; @Slf4j @Component @@ -41,15 +41,16 @@ public class DeviceDataCoordinator { @Autowired private IDeviceStatusLogService deviceStatusLogService; + private static final String DEVICE_EVENT_PREFIX = "device:event:id:"; + private static final long DEVICE_EVENT_EXPIRE_HOURS = 24; private static final String SYNC_LOCK_PREFIX = "device:sync:lock:"; @Autowired private DeviceRuntimeStatsService deviceRuntimeStatsService; /** - * 统一处理设备数据 - 调用协调器确保所有表同步 + * 统一处理设备数据 - 增强版本,确保所有表字段都能正确更新 */ - @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public void processDeviceData(IotMsgNotifyDataPro iotMsgNotifyData) { if (iotMsgNotifyData == null || iotMsgNotifyData.getHeader() == null) { log.warn("接收到的设备信息数据为空"); @@ -62,374 +63,142 @@ public class DeviceDataCoordinator { return; } + // 生成事件唯一标识(使用设备ID+事件时间) + String eventUniqueId = generateEventUniqueId(iotMsgNotifyData); + if (StrUtil.isNotBlank(eventUniqueId)) { + String idempotentKey = DEVICE_EVENT_PREFIX + eventUniqueId; + Boolean isNewEvent = redisTemplate.opsForValue().setIfAbsent( + idempotentKey, "1", Duration.ofHours(DEVICE_EVENT_EXPIRE_HOURS)); + + if (isNewEvent != null && !isNewEvent) { + log.debug("设备事件已处理,跳过重复事件: {}", eventUniqueId); + return; // 已处理过,直接返回 + } + } + try { // 使用协调器统一处理所有表数据 coordinateDeviceData(iotMsgNotifyData); - log.info("设备数据处理完成: {}", deviceId); } catch (Exception e) { log.error("处理设备数据时发生异常,设备ID: {}", deviceId, e); - throw new RuntimeException("设备数据处理失败", e); - } - } - /** - * 确保设备存在 - */ - private Device ensureDeviceExists(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { - return deviceManagerService.getOrCreateDevice(deviceId, (device) -> { - // 初始化设备信息 - if (iotMsgNotifyData.getHeader() != null) { - device.setProductId(iotMsgNotifyData.getHeader().getProductId()); - } - if (iotMsgNotifyData.getBody() != null) { - device.setStatus(iotMsgNotifyData.getBody().getStatus()); - } - device.setCreateTime(new Date()); - device.setCreateBy("系统自动创建"); - }); - } - - /** - * 处理按摩任务数据 - */ - private void processMassageTaskData(Device device, IotMsgNotifyDataPro iotMsgNotifyData) { - if (iotMsgNotifyData.getBody() == null || iotMsgNotifyData.getBody().getServices() == null) { - return; - } - - for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { - if ("StatusChange".equals(service.getServiceId()) && service.getProperties() != null) { - Map props = service.getProperties(); - - if (!isValidMassageTaskData(device.getDeviceId(), props)) { - continue; - } - - // 检查是否重复 - if (isMassageTaskExists(device.getDeviceId(), props)) { - log.debug("按摩任务已存在,跳过: {}", device.getDeviceId()); - continue; - } - - MassageTask task = createMassageTask(device, props); - if (task != null) { - massageTaskMapper.insert(task); - log.info("按摩任务记录成功: 设备{},时长{}秒", device.getDeviceId(), task.getTaskTime()); - } - break; + // 处理失败时,删除幂等键,允许重试 + if (StrUtil.isNotBlank(eventUniqueId)) { + redisTemplate.delete(DEVICE_EVENT_PREFIX + eventUniqueId); } } } /** - * 更新按头使用统计 + * 生成事件唯一标识 */ - private void updateHeadUsageStatistics(Device device, IotMsgNotifyDataPro iotMsgNotifyData) { - try { - if (iotMsgNotifyData.getBody() == null || iotMsgNotifyData.getBody().getServices() == null) { - return; - } + private String generateEventUniqueId(IotMsgNotifyDataPro iotMsgNotifyData) { + if (iotMsgNotifyData.getHeader() == null) { + return null; + } + String deviceId = iotMsgNotifyData.getHeader().getDeviceId(); + String eventTime = null; + + // 尝试从body中获取事件时间 + if (iotMsgNotifyData.getBody() != null && iotMsgNotifyData.getBody().getStatusUpdateTime() != null) { + eventTime = String.valueOf(iotMsgNotifyData.getBody().getStatusUpdateTime().getTime()); + } + + // 尝试从services中获取事件时间 + if (StrUtil.isBlank(eventTime) && iotMsgNotifyData.getBody() != null && + iotMsgNotifyData.getBody().getServices() != null) { for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { - if ("StatusChange".equals(service.getServiceId()) && service.getProperties() != null) { - Map props = service.getProperties(); - String headType = safeGetString(props, "head_type"); - - if (StrUtil.isBlank(headType) || !isValidMassageTaskData(device.getDeviceId(), props)) { - continue; - } - - Long duration = calculateMassageDuration(iotMsgNotifyData); - if (duration <= 0) { - continue; - } - - DeviceHeadUsage usage = deviceHeadUsageMapper.selectOne( - new LambdaQueryWrapper() - .eq(DeviceHeadUsage::getDeviceId, device.getDeviceId()) - .eq(DeviceHeadUsage::getHeadType, headType)); - - if (usage == null) { - usage = new DeviceHeadUsage(); - usage.setDeviceId(device.getDeviceId()); - usage.setHeadType(headType); - usage.setUsageCount(0); - usage.setTotalDuration(0L); - usage.setDailyDuration(0L); - usage.setWeeklyDuration(0L); - usage.setMonthlyDuration(0L); - usage.setCreateBy("系统自动创建"); - usage.setCreateTime(new Date()); - } - - usage.setUsageCount(usage.getUsageCount() + 1); - usage.setTotalDuration(usage.getTotalDuration() + duration); - usage.setDailyDuration(usage.getDailyDuration() + duration); - usage.setWeeklyDuration(usage.getWeeklyDuration() + duration); - usage.setMonthlyDuration(usage.getMonthlyDuration() + duration); - usage.setLastUsedTime(new Date()); - usage.setUpdateTime(new Date()); - usage.setUpdateBy("系统自动修改"); - if (usage.getId() == null) { - deviceHeadUsageMapper.insert(usage); - } else { - deviceHeadUsageMapper.updateById(usage); - } - + if (service.getEventTime() != null) { + eventTime = String.valueOf(service.getEventTime().getTime()); break; } } - } catch (Exception e) { - log.error("更新按头使用统计失败,设备: {}", device.getDeviceId(), e); - } - } - - // ========== 辅助方法 ========== - - private boolean acquireLock(String lockKey) { - try { - Boolean acquired = redisTemplate.opsForValue().setIfAbsent( - lockKey, "1", java.time.Duration.ofSeconds(10)); - return acquired != null && acquired; - } catch (Exception e) { - log.warn("获取锁失败: {}", lockKey, e); - return false; - } - } - - private void releaseLock(String lockKey) { - try { - redisTemplate.delete(lockKey); - } catch (Exception e) { - log.warn("释放锁失败: {}", lockKey, e); - } - } - - private boolean isValidMassageTaskData(String deviceId, Map props) { - return props.containsKey("massage_start_time") && - props.containsKey("massage_end_time") && - props.containsKey("head_type") && - props.containsKey("body_part"); - } - - private boolean isMassageTaskExists(String deviceId, Map props) { - Long startTime = safeParseTimestamp(props.get("massage_start_time")); - String headType = safeGetString(props, "head_type"); - - if (startTime == null || StrUtil.isBlank(headType)) { - return false; } - LambdaQueryWrapper query = new LambdaQueryWrapper<>(); - query.eq(MassageTask::getDeviceId, deviceId) - .eq(MassageTask::getStartTime, startTime) - .eq(MassageTask::getHeadType, headType); - - return massageTaskMapper.selectCount(query) > 0; - } - - private MassageTask createMassageTask(Device device, Map props) { - try { - MassageTask task = new MassageTask(); - task.setDeviceId(device.getDeviceId()); - task.setDeviceName(device.getDeviceName()); - task.setProductName(device.getProductName()); - task.setHeadType(safeGetString(props, "head_type")); - task.setBodyPart(safeGetString(props, "body_part")); - task.setMassagePlan(safeGetString(props, "massage_plan")); - task.setCreateBy("系统自动创建"); - Long startTime = safeParseTimestamp(props.get("massage_start_time")); - Long endTime = safeParseTimestamp(props.get("massage_end_time")); - task.setCreateTimestamp(new Date().getTime()); - if (startTime != null && endTime != null && startTime < endTime && startTime > 0) { - task.setStartTime(startTime); - task.setEndTime(endTime); - task.setTaskTime((endTime - startTime) / 1000); - task.setCreateTime(new Date()); - return task; - } - } catch (Exception e) { - log.error("创建按摩任务失败", e); - } - return null; - } - - private Long calculateMassageDuration(IotMsgNotifyDataPro iotMsgNotifyData) { - if (iotMsgNotifyData.getBody() == null || iotMsgNotifyData.getBody().getServices() == null) { - return 0L; + // 如果仍然没有事件时间,使用当前时间 + if (StrUtil.isBlank(eventTime)) { + eventTime = String.valueOf(System.currentTimeMillis()); } - for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { - if ("StatusChange".equals(service.getServiceId()) && service.getProperties() != null) { - Map props = service.getProperties(); - Long startTime = safeParseTimestamp(props.get("massage_start_time")); - Long endTime = safeParseTimestamp(props.get("massage_end_time")); - - if (startTime != null && endTime != null && startTime < endTime && startTime > 0) { - return (endTime - startTime) / 1000; // 转换为秒 - } - break; - } - } - return 0L; - } - - private boolean shouldCountOnline(IotMsgNotifyDataPro iotMsgNotifyData) { - return iotMsgNotifyData.getBody() != null && - "ONLINE".equalsIgnoreCase(iotMsgNotifyData.getBody().getStatus()); - } - - private String safeGetString(Map props, String key) { - if (props == null || !props.containsKey(key)) return null; - Object value = props.get(key); - return value != null ? value.toString() : null; - } - - private Long safeParseTimestamp(Object timestampObj) { - if (timestampObj == null) return null; - try { - if (timestampObj instanceof Number) { - return ((Number) timestampObj).longValue(); - } else if (timestampObj instanceof String) { - return Long.parseLong(timestampObj.toString()); - } - } catch (Exception e) { - log.warn("时间戳解析失败: {}", timestampObj); - } - return null; + return deviceId + ":" + eventTime; } /** - * 处理设备状态变更 - 最终修复版本 + * 协调设备数据 - 增强版本,确保所有表字段都能正确更新 */ - private void processDeviceStatusChange(Device device, IotMsgNotifyDataPro iotMsgNotifyData) { - if (iotMsgNotifyData.getBody() == null) { + public void coordinateDeviceData(IotMsgNotifyDataPro iotMsgNotifyData) { + if (iotMsgNotifyData == null || iotMsgNotifyData.getHeader() == null) { return; } - // 更新设备状态 - String newStatus = iotMsgNotifyData.getBody().getStatus(); - if (newStatus != null) { - String normalizedStatus = "ONLINE".equalsIgnoreCase(newStatus) ? "ONLINE" : "OFFLINE"; - device.setStatus(normalizedStatus); - - Date now = new Date(); - if ("ONLINE".equals(normalizedStatus)) { - device.setLastOnlineTime(now); - } else { - device.setLastOfflineTime(now); - } - - device.setUpdateTime(now); - deviceMapper.updateById(device); - - log.debug("设备状态更新: {} -> {}", device.getDeviceId(), normalizedStatus); - } - - // 使用专门的服务处理状态日志插入 - boolean success = deviceStatusLogService.safeInsertStatusLog(device.getDeviceId(), iotMsgNotifyData); - - if (success) { - log.debug("状态日志处理成功: {}", device.getDeviceId()); - } else { - log.warn("状态日志处理失败: {}", device.getDeviceId()); - } - } - - /** - * 更新设备位置信息 - 新增方法 - */ - private void updateDeviceLocation(Device device, IotMsgNotifyDataPro iotMsgNotifyData) { - if (iotMsgNotifyData.getBody() == null || iotMsgNotifyData.getBody().getServices() == null) { + String deviceId = iotMsgNotifyData.getHeader().getDeviceId(); + if (StringUtils.isBlank(deviceId)) { return; } - boolean locationUpdated = false; + String normalizedDeviceId = deviceManagerService.normalizeDeviceId(deviceId); + String lockKey = SYNC_LOCK_PREFIX + normalizedDeviceId; - for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { - if (service.getProperties() == null) continue; - - Map props = service.getProperties(); - - // 检查是否有新的位置信息 - boolean hasNewLongitude = props.containsKey("longitude") && props.get("longitude") != null; - boolean hasNewLatitude = props.containsKey("latitude") && props.get("latitude") != null; - - if (hasNewLongitude || hasNewLatitude) { - String newLongitude = hasNewLongitude ? props.get("longitude").toString() : null; - String newLatitude = hasNewLatitude ? props.get("latitude").toString() : null; - - // 检查位置是否发生变化 - boolean longitudeChanged = !Objects.equals(device.getLongitude(), newLongitude); - boolean latitudeChanged = !Objects.equals(device.getLatitude(), newLatitude); - - if (longitudeChanged || latitudeChanged) { - if (hasNewLongitude) { - device.setLongitude(newLongitude); - log.debug("更新设备经度: {} -> {}", device.getDeviceId(), newLongitude); - } - if (hasNewLatitude) { - device.setLatitude(newLatitude); - log.debug("更新设备纬度: {} -> {}", device.getDeviceId(), newLatitude); - } - - locationUpdated = true; - - // 如果有位置信息,尝试获取详细地址 - if (hasNewLongitude && hasNewLatitude) { - try { -// TODO 基于经纬度获取省市位置 - } catch (Exception e) { - log.warn("获取设备位置地址失败: {}", device.getDeviceId(), e); - } - } - } - } - - // 同时处理版本信息更新 - if (props.containsKey("software_version") && props.get("software_version") != null) { - String newSoftwareVersion = props.get("software_version").toString(); - if (!Objects.equals(device.getSoftwareVersion(), newSoftwareVersion)) { - device.setSoftwareVersion(newSoftwareVersion); - log.debug("更新软件版本: {} -> {}", device.getDeviceId(), newSoftwareVersion); - locationUpdated = true; - } - } - - if (props.containsKey("vtxdb_version") && props.get("vtxdb_version") != null) { - String newVtxdbVersion = props.get("vtxdb_version").toString(); - if (!Objects.equals(device.getVtxdbVersion(), newVtxdbVersion)) { - device.setVtxdbVersion(newVtxdbVersion); - log.debug("更新参数服务器版本: {} -> {}", device.getDeviceId(), newVtxdbVersion); - locationUpdated = true; - } - } - } - - if (locationUpdated) { - device.setUpdateTime(new Date()); - deviceMapper.updateById(device); - log.info("设备位置和版本信息更新完成: {}", device.getDeviceId()); - } - } - - /** - * 更新运行统计 - 最终修复版本 - */ - private void updateRuntimeStatistics(Device device, IotMsgNotifyDataPro iotMsgNotifyData) { + boolean locked = false; try { - // 使用专门的服务处理运行统计更新 - boolean success = deviceRuntimeStatsService.safeUpdateRuntimeStats(device.getDeviceId(), iotMsgNotifyData); - - if (success) { - log.debug("运行统计更新成功: {}", device.getDeviceId()); - } else { - log.warn("运行统计更新失败: {}", device.getDeviceId()); + // 获取分布式锁 + locked = acquireLock(lockKey); + if (!locked) { + log.warn("获取设备数据同步锁失败,设备: {}", normalizedDeviceId); + return; } + // 1. 确保设备基础信息存在并更新所有字段 + Device device = ensureDeviceExists(normalizedDeviceId, iotMsgNotifyData); + if (device == null) { + log.error("设备基础信息创建失败: {}", normalizedDeviceId); + return; + } + + // 2. 处理设备核心信息更新 (必需):状态、位置、版本、元数据、许可证信息等 + updateDeviceCoreInfo(device, iotMsgNotifyData); + + // 3. 处理设备状态变更日志 (必需) + deviceStatusLogService.safeInsertStatusLog(device.getDeviceId(), iotMsgNotifyData); + + // 4. 处理按摩任务数据 + try { + processMassageTaskData(device, iotMsgNotifyData); + } catch (Exception e) { + log.error("处理按摩任务数据失败,设备: {}", normalizedDeviceId, e); + } + + // 5. 更新运行统计 + try { + deviceRuntimeStatsService.safeUpdateRuntimeStats(device.getDeviceId(), iotMsgNotifyData); + } catch (Exception e) { + log.error("更新运行统计失败,设备: {}", normalizedDeviceId, e); + } + + // 6. 更新按头使用统计 + try { + updateHeadUsageStatistics(device, iotMsgNotifyData); + } catch (Exception e) { + log.error("更新按头使用统计失败,设备: {}", normalizedDeviceId, e); + } + + // 7. 更新最后按摩时间 + try { + updateDeviceLastMassageTime(device, iotMsgNotifyData); + } catch (Exception e) { + log.error("更新最后按摩时间失败,设备: {}", normalizedDeviceId, e); + } + + log.info("设备数据同步完成: {}", normalizedDeviceId); + } catch (Exception e) { - log.error("更新运行统计失败,设备: {}", device.getDeviceId(), e); + log.error("设备数据协调处理异常,设备: {}", normalizedDeviceId, e); + } finally { + if (locked) { + releaseLock(lockKey); + } } } @@ -456,6 +225,8 @@ public class DeviceDataCoordinator { if (device.getLastMassageTime() == null || lastMassageTime.after(device.getLastMassageTime())) { device.setLastMassageTime(lastMassageTime); + device.setUpdateTime(new Date()); + deviceMapper.updateById(device); log.debug("更新设备最后按摩时间: {} -> {}", device.getDeviceId(), lastMassageTime); } } @@ -468,109 +239,498 @@ public class DeviceDataCoordinator { } } - /** - * 统一处理设备数据 - 增强异常处理 - */ - @Transactional(rollbackFor = Exception.class) - public void coordinateDeviceData(IotMsgNotifyDataPro iotMsgNotifyData) { - if (iotMsgNotifyData == null || iotMsgNotifyData.getHeader() == null) { - return; - } + // ========== 辅助方法 ========== - String deviceId = iotMsgNotifyData.getHeader().getDeviceId(); - if (StringUtils.isBlank(deviceId)) { - return; - } - - String normalizedDeviceId = deviceManagerService.normalizeDeviceId(deviceId); - String lockKey = SYNC_LOCK_PREFIX + normalizedDeviceId; - - boolean locked = false; + private boolean acquireLock(String lockKey) { try { - // 获取分布式锁 - locked = acquireLock(lockKey); - if (!locked) { - log.warn("获取设备数据同步锁失败,设备: {}", normalizedDeviceId); - return; - } - - // 1. 确保设备基础信息存在 - Device device = ensureDeviceExists(normalizedDeviceId, iotMsgNotifyData); - if (device == null) { - log.error("设备基础信息创建失败: {}", normalizedDeviceId); - return; - } - - // 2. 处理设备位置信息更新 - try { - updateDeviceLocation(device, iotMsgNotifyData); - } catch (Exception e) { - log.error("处理设备位置信息更新失败,设备: {}", normalizedDeviceId, e); - } - - // 3. 处理设备状态变更 - try { - processDeviceStatusChange(device, iotMsgNotifyData); - } catch (Exception e) { - log.error("处理设备状态变更失败,继续处理其他数据,设备: {}", normalizedDeviceId, e); - } - - // 4. 处理按摩任务数据 - try { - processMassageTaskData(device, iotMsgNotifyData); - } catch (Exception e) { - log.error("处理按摩任务数据失败,设备: {}", normalizedDeviceId, e); - } - - // 5. 更新最后按摩时间(新增) - try { - updateDeviceLastMassageTime(device, iotMsgNotifyData); - } catch (Exception e) { - log.error("更新最后按摩时间失败,设备: {}", normalizedDeviceId, e); - } - - // 6. 更新运行统计 - try { - updateRuntimeStatistics(device, iotMsgNotifyData); - } catch (Exception e) { - log.error("更新运行统计失败,设备: {}", normalizedDeviceId, e); - } - - // 7. 更新按头使用统计 - try { - updateHeadUsageStatistics(device, iotMsgNotifyData); - } catch (Exception e) { - log.error("更新按头使用统计失败,设备: {}", normalizedDeviceId, e); - } - - // 8. 更新设备总统计(移除重复的时长累加) - try { - updateDeviceBasicInfo(device); // 只更新基本信息,不重复累加时长 - } catch (Exception e) { - log.error("更新设备基本信息失败,设备: {}", normalizedDeviceId, e); - } - - log.info("设备数据同步完成: {}", normalizedDeviceId); - + Boolean acquired = redisTemplate.opsForValue().setIfAbsent( + lockKey, + "1", + java.time.Duration.ofSeconds(10)); + return acquired != null && acquired; } catch (Exception e) { - log.error("设备数据协调处理异常,设备: {}", normalizedDeviceId, e); - } finally { - if (locked) { - releaseLock(lockKey); - } + log.warn("获取锁失败: {}", lockKey, e); + return false; } } - /** - * 只更新设备基本信息,不重复累加时长 - */ - private void updateDeviceBasicInfo(Device device) { + private void releaseLock(String lockKey) { try { - device.setUpdateTime(new Date()); + redisTemplate.delete(lockKey); + } catch (Exception e) { + log.warn("释放锁失败: {}", lockKey, e); + } + } + + private Device ensureDeviceExists(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { + return deviceManagerService.getOrCreateDevice(deviceId, (device) -> { + // 初始化设备信息 + if (iotMsgNotifyData.getHeader() != null) { + device.setProductId(iotMsgNotifyData.getHeader().getProductId()); + } + if (iotMsgNotifyData.getBody() != null) { + device.setStatus(iotMsgNotifyData.getBody().getStatus()); + } + device.setCreateTime(new Date()); + device.setCreateBy("系统自动创建"); + }); + } + + private Long calculateMassageDuration(IotMsgNotifyDataPro iotMsgNotifyData) { + if (iotMsgNotifyData.getBody() == null || iotMsgNotifyData.getBody().getServices() == null) { + return 0L; + } + + for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { + if ("StatusChange".equals(service.getServiceId()) && service.getProperties() != null) { + Map props = service.getProperties(); + Long startTime = safeParseTimestamp(props.get("massage_start_time")); + Long endTime = safeParseTimestamp(props.get("massage_end_time")); + + if (startTime != null && endTime != null && startTime < endTime && startTime > 0) { + return (endTime - startTime) / 1000; // 转换为秒 + } + break; + } + } + return 0L; + } + + // 在 DeviceDataCoordinator.java 中,修改 updateDeviceCoreInfo 方法 + private void updateDeviceCoreInfo(Device device, IotMsgNotifyDataPro iotMsgNotifyData) { + boolean updated = false; + Date now = new Date(); + + // 1. 处理设备状态 (原有逻辑) + if (iotMsgNotifyData.getBody() != null && iotMsgNotifyData.getBody().getStatus() != null) { + String newStatus = iotMsgNotifyData.getBody().getStatus(); + String normalizedStatus = "ONLINE".equalsIgnoreCase(newStatus) ? "ONLINE" : "OFFLINE"; + if (!Objects.equals(device.getStatus(), normalizedStatus)) { + device.setStatus(normalizedStatus); + if ("ONLINE".equals(normalizedStatus)) { + device.setLastOnlineTime(now); + } else { + device.setLastOfflineTime(now); + } + updated = true; + } + } + + // 2. 增强:处理物模型属性(Services中的Properties) + if (iotMsgNotifyData.getBody() != null && iotMsgNotifyData.getBody().getServices() != null) { + for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { + if (service.getProperties() != null) { + Map props = service.getProperties(); + + // 2.1 处理位置信息 + updated |= updateFieldIfPresent(props, "longitude", device::setLongitude); + updated |= updateFieldIfPresent(props, "latitude", device::setLatitude); + updated |= updateFieldIfPresent(props, "location", device::setLocation); + updated |= updateFieldIfPresent(props, "shop_id", device::setShopId); + + // 2.2 处理版本信息 + updated |= updateFieldIfPresent(props, "software_version", device::setSoftwareVersion); + updated |= updateFieldIfPresent(props, "vtxdb_version", device::setVtxdbVersion); + + // 2.3 处理许可证信息 + updated |= updateFieldIfPresent(props, "activation_code", device::setActivationCode); + updated |= updateFieldIfPresent(props, "serial_number", device::setSerialNumber); + updated |= updateFieldIfPresent(props, "license_status", device::setLicenseStatus); + // 处理时间戳字段 license_expire_time + if (props.containsKey("license_expire_time")) { + Long expiryTimestamp = safeParseTimestamp(props.get("license_expire_time")); + if (expiryTimestamp != null && expiryTimestamp > 0) { + Date newExpiryDate = new Date(expiryTimestamp); + if (!Objects.equals(device.getLicenseExpireTime(), newExpiryDate)) { + device.setLicenseExpireTime(newExpiryDate); + updated = true; + } + } + } + + // 2.4 处理设备类型和产品信息 + updated |= updateFieldIfPresent(props, "device_type", value -> { + try { + device.setDeviceType(Integer.parseInt(value.toString())); + } catch (NumberFormatException e) { + log.warn("设备类型格式错误: {}", value); + } + }); + updated |= updateFieldIfPresent(props, "product_id", device::setProductId); + updated |= updateFieldIfPresent(props, "product_name", device::setProductName); + + // 找到第一个包含数据的服务即可,通常一个消息只报告一个服务的数据 + break; + } + } + } + + if (updated) { + device.setUpdateTime(now); device.setUpdateBy("系统自动更新"); deviceMapper.updateById(device); - } catch (Exception e) { - log.error("更新设备基本信息失败: {}", device.getDeviceId(), e); + log.info("设备核心信息更新完成: {}", device.getDeviceId()); } } + + // 新增一个辅助方法,用于简化字段更新逻辑 + private boolean updateFieldIfPresent(Map props, String key, Consumer setter) { + if (props.containsKey(key) && props.get(key) != null) { + String newValue = props.get(key).toString(); + setter.accept(newValue); + log.debug("更新设备{}: {}", key, newValue); + return true; + } + return false; + } + + /** + * 更新按头使用统计 - 完整版本 + */ + private void updateHeadUsageStatistics(Device device, IotMsgNotifyDataPro iotMsgNotifyData) { + String deviceId = device.getDeviceId(); + log.debug("开始更新按头使用统计: {}", deviceId); + + try { + if (iotMsgNotifyData.getBody() == null || iotMsgNotifyData.getBody().getServices() == null) { + log.debug("消息体或服务数据为空,跳过按头统计更新: {}", deviceId); + return; + } + + // 遍历所有服务,寻找按摩相关数据 + for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { + if ("StatusChange".equals(service.getServiceId()) && service.getProperties() != null) { + Map props = service.getProperties(); + + // 提取按头类型 + String headType = safeGetString(props, "head_type"); + if (StrUtil.isBlank(headType)) { + log.debug("按头类型为空,跳过按头统计更新: {}", deviceId); + continue; + } + + // 计算按摩时长 + Long duration = calculateMassageDuration(iotMsgNotifyData); + if (duration == null || duration <= 0) { + log.debug("按摩时长无效({}),跳过按头统计更新: {}", duration, deviceId); + continue; + } + + // 检查是否已有该按头的使用记录 + DeviceHeadUsage usage = deviceHeadUsageMapper.selectOne( + new LambdaQueryWrapper() + .eq(DeviceHeadUsage::getDeviceId, deviceId) + .eq(DeviceHeadUsage::getHeadType, headType)); + + Date now = new Date(); + + if (usage == null) { + // 创建新记录 + usage = new DeviceHeadUsage(); + usage.setDeviceId(deviceId); + usage.setHeadType(headType); + usage.setUsageCount(1); + usage.setTotalDuration(duration); + usage.setDailyDuration(duration); + usage.setWeeklyDuration(duration); + usage.setMonthlyDuration(duration); + usage.setLastUsedTime(now); + usage.setCreateTime(now); + usage.setUpdateTime(now); + usage.setCreateBy("系统自动创建"); + int result = deviceHeadUsageMapper.insert(usage); + if (result > 0) { + log.info("新增按头使用记录: 设备{},按头{},次数{},时长{}秒", + deviceId, headType, usage.getUsageCount(), duration); + } else { + log.error("新增按头使用记录失败: 设备{},按头{}", deviceId, headType); + } + } else { + // 更新现有记录 + usage.setUsageCount(usage.getUsageCount() + 1); + usage.setTotalDuration(usage.getTotalDuration() + duration); + usage.setDailyDuration(usage.getDailyDuration() + duration); + usage.setWeeklyDuration(usage.getWeeklyDuration() + duration); + usage.setMonthlyDuration(usage.getMonthlyDuration() + duration); + usage.setLastUsedTime(now); + usage.setUpdateTime(now); + usage.setUpdateBy("系统自动更新"); + + int result = deviceHeadUsageMapper.updateById(usage); + if (result > 0) { + log.info("更新按头使用记录: 设备{},按头{},次数{},总时长{}秒", + deviceId, headType, usage.getUsageCount(), usage.getTotalDuration()); + } else { + log.error("更新按头使用记录失败: 设备{},按头{}", deviceId, headType); + } + } + + // 只处理第一个有效的按摩服务 + break; + } + } + } catch (Exception e) { + log.error("更新按头使用统计异常: {}", deviceId, e); + } + } + + /** + * 处理按摩任务数据 - 增强版,补全缺失字段 + */ + private void processMassageTaskData(Device device, IotMsgNotifyDataPro iotMsgNotifyData) { + if (iotMsgNotifyData.getBody() == null || iotMsgNotifyData.getBody().getServices() == null) { + log.debug("消息体或服务数据为空,设备: {}", device.getDeviceId()); + return; + } + + for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { + if ("StatusChange".equals(service.getServiceId()) && service.getProperties() != null) { + Map props = service.getProperties(); + + // 补全缺失字段 + completeMissingMassageProperties(props, device.getDeviceId()); + + // 原有处理逻辑... + if (!isValidMassageTaskData(device.getDeviceId(), props)) { + log.debug("按摩任务数据无效,设备: {}", device.getDeviceId()); + continue; + } + + // 检查是否重复 + if (isMassageTaskExists(device.getDeviceId(), props)) { + log.debug("按摩任务已存在,跳过: {}", device.getDeviceId()); + continue; + } + + MassageTask task = createMassageTask(device, props); + if (task != null) { + try { + massageTaskMapper.insert(task); + log.info("按摩任务记录成功: 设备{},时长{}秒", device.getDeviceId(), task.getTaskTime()); + + // 更新设备最后按摩时间 + Long endTime = safeParseTimestamp(props.get("massage_end_time")); + if (endTime != null && endTime > 0) { + Date lastMassageTime = new Date(endTime); + if (device.getLastMassageTime() == null || lastMassageTime.after(device.getLastMassageTime())) { + device.setLastMassageTime(lastMassageTime); + device.setUpdateTime(new Date()); + deviceMapper.updateById(device); + log.debug("更新设备最后按摩时间: {} -> {}", device.getDeviceId(), lastMassageTime); + } + } + } catch (Exception e) { + log.error("插入按摩任务失败,设备: {}", device.getDeviceId(), e); + } + } else { + log.warn("创建按摩任务对象失败,设备: {}", device.getDeviceId()); + } + break; + } + } + } + + /** + * 补全按摩任务缺失的属性字段 + */ + private void completeMissingMassageProperties(Map props, String deviceId) { + // 提供默认值或从其他来源获取值 + if (!props.containsKey("head_type") || props.get("head_type") == null) { + props.put("head_type", "default_head"); + log.debug("设备 {} 的 head_type 缺失,使用默认值", deviceId); + } + + if (!props.containsKey("body_part") || props.get("body_part") == null) { + props.put("body_part", "default_body_part"); + log.debug("设备 {} 的 body_part 缺失,使用默认值", deviceId); + } + + if (!props.containsKey("massage_plan") || props.get("massage_plan") == null) { + props.put("massage_plan", "default_plan"); + log.debug("设备 {} 的 massage_plan 缺失,使用默认值", deviceId); + } + + // 检查时间字段,如果缺失,使用当前时间或相关时间 + if (!props.containsKey("massage_start_time") || props.get("massage_start_time") == null) { + // 如果没有开始时间,假设为当前时间减去默认持续时间(例如10分钟) + long startTime = System.currentTimeMillis() - 600000; // 10分钟前 + props.put("massage_start_time", startTime); + log.debug("设备 {} 的 massage_start_time 缺失,使用默认值", deviceId); + } + + if (!props.containsKey("massage_end_time") || props.get("massage_end_time") == null) { + // 如果没有结束时间,使用当前时间 + long endTime = System.currentTimeMillis(); + props.put("massage_end_time", endTime); + log.debug("设备 {} 的 massage_end_time 缺失,使用默认值", deviceId); + } + + // 确保时间戳格式正确(转换为毫秒级) + ensureTimestampFormat(props, "massage_start_time"); + ensureTimestampFormat(props, "massage_end_time"); + } + + /** + * 确保时间戳格式正确(转换为毫秒级) + */ + private void ensureTimestampFormat(Map props, String key) { + if (props.containsKey(key)) { + Object value = props.get(key); + if (value instanceof Number) { + long timestamp = ((Number) value).longValue(); + // 判断是秒级还是毫秒级 + if (timestamp < 10000000000L) { // 秒级时间戳(小于 2286-11-21) + props.put(key, timestamp * 1000); // 转换为毫秒级 + } + } else if (value instanceof String) { + try { + long timestamp = Long.parseLong((String) value); + if (timestamp < 10000000000L) { // 秒级时间戳 + props.put(key, timestamp * 1000); // 转换为毫秒级 + } else { + props.put(key, timestamp); // 已经是毫秒级 + } + } catch (NumberFormatException e) { + log.warn("时间戳格式错误,key: {}, value: {}", key, value); + // 使用当前时间作为默认值 + props.put(key, System.currentTimeMillis()); + } + } + } + } + + /** + * 检查按摩任务数据是否有效 + */ + private boolean isValidMassageTaskData(String deviceId, Map props) { + boolean hasStartTime = props.containsKey("massage_start_time") && props.get("massage_start_time") != null; + boolean hasEndTime = props.containsKey("massage_end_time") && props.get("massage_end_time") != null; + boolean hasHeadType = props.containsKey("head_type") && props.get("head_type") != null; + boolean hasBodyPart = props.containsKey("body_part") && props.get("body_part") != null; + + if (!hasStartTime || !hasEndTime || !hasHeadType || !hasBodyPart) { + log.warn("按摩任务数据不完整,设备: {}", deviceId); + return false; + } + + // 检查时间顺序 + Long startTime = safeParseTimestamp(props.get("massage_start_time")); + Long endTime = safeParseTimestamp(props.get("massage_end_time")); + + if (startTime == null || endTime == null) { + log.warn("按摩任务时间格式错误,设备: {}", deviceId); + return false; + } + + if (startTime >= endTime) { + log.warn("按摩任务开始时间大于等于结束时间,设备: {}", deviceId); + return false; + } + + // 检查持续时间是否合理(最长24小时) + long durationMs = endTime - startTime; + long maxValidDuration = 24 * 60 * 60 * 1000L; // 24小时 + + if (durationMs > maxValidDuration) { + log.warn("按摩任务持续时间过长: {} 毫秒,设备: {}", durationMs, deviceId); + return false; + } + + return true; + } + + /** + * 检查按摩任务是否已存在 + */ + private boolean isMassageTaskExists(String deviceId, Map props) { + Long startTime = safeParseTimestamp(props.get("massage_start_time")); + String headType = safeGetString(props, "head_type"); + String bodyPart = safeGetString(props, "body_part"); + + if (startTime == null || StrUtil.isBlank(headType) || StrUtil.isBlank(bodyPart)) { + return false; + } + + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(MassageTask::getDeviceId, deviceId) + .eq(MassageTask::getStartTime, startTime) + .eq(MassageTask::getHeadType, headType) + .eq(MassageTask::getBodyPart, bodyPart); + + return massageTaskMapper.selectCount(query) > 0; + } + + /** + * 创建按摩任务对象 + */ + private MassageTask createMassageTask(Device device, Map props) { + try { + MassageTask task = new MassageTask(); + task.setDeviceId(device.getDeviceId()); + task.setDeviceName(device.getDeviceName()); + task.setProductName(device.getProductName()); + task.setHeadType(safeGetString(props, "head_type")); + task.setBodyPart(safeGetString(props, "body_part")); + task.setMassagePlan(safeGetString(props, "massage_plan")); + task.setCreateBy("系统自动创建"); + + Long startTime = safeParseTimestamp(props.get("massage_start_time")); + Long endTime = safeParseTimestamp(props.get("massage_end_time")); + task.setCreateTimestamp(System.currentTimeMillis()); + + if (startTime != null && endTime != null && startTime < endTime && startTime > 0) { + task.setStartTime(startTime); + task.setEndTime(endTime); + task.setTaskTime((endTime - startTime) / 1000); // 转换为秒 + task.setCreateTime(new Date()); + return task; + } + } catch (Exception e) { + log.error("创建按摩任务失败", e); + } + return null; + } + + /** + * 安全解析时间戳 + */ + private Long safeParseTimestamp(Object timestampObj) { + if (timestampObj == null) return null; + try { + long timestamp; + if (timestampObj instanceof Number) { + timestamp = ((Number) timestampObj).longValue(); + } else if (timestampObj instanceof String) { + String timestampStr = timestampObj.toString(); + // 处理科学计数法 + if (timestampStr.contains("E") || timestampStr.contains("e")) { + double doubleValue = Double.parseDouble(timestampStr); + timestamp = (long) doubleValue; + } else { + timestamp = Long.parseLong(timestampStr); + } + } else { + return null; + } + + // 判断是秒级还是毫秒级 + if (timestamp < 10000000000L) { // 秒级时间戳(小于 2286-11-21) + return timestamp * 1000; + } else { // 毫秒级时间戳 + return timestamp; + } + } catch (Exception e) { + log.warn("时间戳解析失败: {}", timestampObj, e); + return null; + } + } + + /** + * 安全获取字符串值 + */ + private String safeGetString(Map props, String key) { + if (props == null || !props.containsKey(key)) return null; + Object value = props.get(key); + return value != null ? value.toString() : null; + } } \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/service/impl/DeviceRuntimeStatsServiceImpl.java b/storm-device/src/main/java/com/storm/device/service/impl/DeviceRuntimeStatsServiceImpl.java index 6c0db31..1711f36 100644 --- a/storm-device/src/main/java/com/storm/device/service/impl/DeviceRuntimeStatsServiceImpl.java +++ b/storm-device/src/main/java/com/storm/device/service/impl/DeviceRuntimeStatsServiceImpl.java @@ -1,182 +1,255 @@ package com.storm.device.service.impl; -import java.util.Date; -import java.util.List; - +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.storm.device.domain.po.DeviceRuntimeStats; +import com.storm.device.mapper.DeviceRuntimeStatsMapper; +import com.storm.device.service.IDeviceRuntimeStatsService; import com.storm.device.task.vo.IotMsgNotifyDataPro; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DuplicateKeyException; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; -import com.storm.device.mapper.DeviceRuntimeStatsMapper; -import com.storm.device.service.IDeviceRuntimeStatsService; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import java.time.Duration; import java.util.Arrays; +import java.util.Date; +import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; -/** - * 设备运行时长统计Service业务层处理 - * - * @author storm - * @date 2025-09-25 - */ -@Service @Slf4j -public class DeviceRuntimeStatsServiceImpl extends ServiceImpl implements IDeviceRuntimeStatsService -{ +@Service +public class DeviceRuntimeStatsServiceImpl extends ServiceImpl implements IDeviceRuntimeStatsService { + @Autowired private DeviceRuntimeStatsMapper deviceRuntimeStatsMapper; - /** - * 查询设备运行时长统计 - * - * @param id 设备运行时长统计主键 - * @return 设备运行时长统计 - */ - @Override - public DeviceRuntimeStats selectDeviceRuntimeStatsById(Long id) - { - return getById(id); - } + @Autowired + private RedisTemplate redisTemplate; + private static final String RUNTIME_STATS_PREFIX = "runtime:stats:id:"; + private static final long RUNTIME_STATS_EXPIRE_HOURS = 24; /** - * 查询设备运行时长统计列表 - * - * @param deviceRuntimeStats 设备运行时长统计 - * @return 设备运行时长统计 - */ - @Override - public List selectDeviceRuntimeStatsList(DeviceRuntimeStats deviceRuntimeStats) - { - return deviceRuntimeStatsMapper.selectDeviceRuntimeStatsList(deviceRuntimeStats); - } - - /** - * 新增设备运行时长统计 - * - * @param deviceRuntimeStats 设备运行时长统计 - * @return 结果 - */ - @Override - public int insertDeviceRuntimeStats(DeviceRuntimeStats deviceRuntimeStats) - { - return save(deviceRuntimeStats) ? 1 : 0; - } - - /** - * 修改设备运行时长统计 - * - * @param deviceRuntimeStats 设备运行时长统计 - * @return 结果 - */ - @Override - public int updateDeviceRuntimeStats(DeviceRuntimeStats deviceRuntimeStats) - { - return updateById(deviceRuntimeStats) ? 1 : 0; - } - - /** - * 批量删除设备运行时长统计 - * - * @param ids 需要删除的设备运行时长统计主键 - * @return 结果 - */ - @Override - public int deleteDeviceRuntimeStatsByIds(Long[] ids) - { - return removeByIds(Arrays.asList(ids)) ? 1 : 0; - } - - /** - * 安全更新运行统计 - 修复重复累加问题 + * 安全更新运行统计 - 增强版,防止重复累加 */ @Transactional(propagation = Propagation.REQUIRES_NEW) public boolean safeUpdateRuntimeStats(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { - try { - Date today = new Date(); - Date now = new Date(); + // 生成运行统计唯一标识 + String statsUniqueId = generateStatsUniqueId(deviceId, iotMsgNotifyData); + if (StrUtil.isNotBlank(statsUniqueId)) { + String idempotentKey = RUNTIME_STATS_PREFIX + statsUniqueId; + Boolean isNewStats = redisTemplate.opsForValue().setIfAbsent( + idempotentKey, "1", Duration.ofHours(RUNTIME_STATS_EXPIRE_HOURS)); - // 计算要增加的时长和在线次数 - Long durationToAdd = calculateMassageDuration(iotMsgNotifyData); - Integer onlineCountToAdd = shouldCountOnline(iotMsgNotifyData) ? 1 : 0; + if (isNewStats != null && !isNewStats) { + log.debug("运行统计已更新,跳过处理: {}", statsUniqueId); + return true; // 已更新也算成功 + } + } - // 先尝试查询今日是否已有记录 - DeviceRuntimeStats existingStats = deviceRuntimeStatsMapper.selectTodayStats(deviceId); + // 检查是否重复消息 + if (isDuplicateEvent(deviceId, iotMsgNotifyData)) { + log.debug("检测到重复消息,跳过运行统计更新: {}", deviceId); + return true; + } - if (existingStats == null) { - // 插入新记录 - DeviceRuntimeStats newStats = new DeviceRuntimeStats(); - newStats.setDeviceId(deviceId); - newStats.setStatDate(today); - newStats.setDailyDuration(durationToAdd); - newStats.setWeeklyDuration(durationToAdd); - newStats.setMonthlyDuration(durationToAdd); - newStats.setOnlineCount(onlineCountToAdd); - newStats.setCreateTime(now); - newStats.setUpdateTime(now); - newStats.setCreateBy("系统自动创建"); - - deviceRuntimeStatsMapper.insert(newStats); - log.debug("创建新的运行统计记录: {}", deviceId); - } else { - // 更新现有记录 - 确保不重复累加 - long newDailyDuration = existingStats.getDailyDuration() + durationToAdd; - long newWeeklyDuration = existingStats.getWeeklyDuration() + durationToAdd; - long newMonthlyDuration = existingStats.getMonthlyDuration() + durationToAdd; - int newOnlineCount = existingStats.getOnlineCount() + onlineCountToAdd; - - // 验证数据合理性 - if (isDurationReasonable(newDailyDuration) && - isDurationReasonable(newWeeklyDuration) && - isDurationReasonable(newMonthlyDuration)) { - - existingStats.setDailyDuration(newDailyDuration); - existingStats.setWeeklyDuration(newWeeklyDuration); - existingStats.setMonthlyDuration(newMonthlyDuration); - existingStats.setOnlineCount(newOnlineCount); - existingStats.setUpdateTime(now); - - deviceRuntimeStatsMapper.updateById(existingStats); - log.debug("更新运行统计记录: {} -> 增加{}秒", deviceId, durationToAdd); - } else { - log.warn("运行统计时长异常,跳过更新: {}", deviceId); + for (int attempt = 0; attempt < 3; attempt++) { + try { + boolean result = doUpdateRuntimeStats(deviceId, iotMsgNotifyData, attempt); + if (!result && StrUtil.isNotBlank(statsUniqueId)) { + // 更新失败时,删除幂等键,允许重试 + redisTemplate.delete(RUNTIME_STATS_PREFIX + statsUniqueId); + } + return result; + } catch (DuplicateKeyException e) { + log.warn("运行统计更新重复键异常,尝试次数: {},设备: {}", attempt + 1, deviceId); + if (attempt == 2) { + log.error("运行统计更新最终失败,设备: {}", deviceId, e); + return false; + } + // 等待后重试 + try { + Thread.sleep(100 * (attempt + 1)); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + return false; + } + } catch (Exception e) { + log.error("运行统计更新异常,尝试次数: {},设备: {}", attempt + 1, deviceId, e); + if (attempt == 2) { + if (StrUtil.isNotBlank(statsUniqueId)) { + // 更新失败时,删除幂等键,允许重试 + redisTemplate.delete(RUNTIME_STATS_PREFIX + statsUniqueId); + } return false; } } - return true; - - } catch (DuplicateKeyException e) { - log.debug("运行统计记录已存在(并发情况),设备: {}", deviceId); - // 递归重试一次 - return safeUpdateRuntimeStatsRetry(deviceId, iotMsgNotifyData); - } catch (Exception e) { - log.error("更新运行统计失败,设备: {}", deviceId, e); - return false; } + return false; } /** - * 重试机制 + * 生成运行统计唯一标识 */ - private boolean safeUpdateRuntimeStatsRetry(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { + private String generateStatsUniqueId(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { + if (iotMsgNotifyData.getBody() == null) { + return null; + } + + String eventTime = null; + + // 尝试从body中获取事件时间 + if (iotMsgNotifyData.getBody().getStatusUpdateTime() != null) { + eventTime = String.valueOf(iotMsgNotifyData.getBody().getStatusUpdateTime().getTime()); + } + + // 尝试从services中获取事件时间 + if (StrUtil.isBlank(eventTime) && iotMsgNotifyData.getBody().getServices() != null) { + for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { + if (service.getEventTime() != null) { + eventTime = String.valueOf(service.getEventTime().getTime()); + break; + } + } + } + + // 如果仍然没有事件时间,使用当前时间 + if (StrUtil.isBlank(eventTime)) { + eventTime = String.valueOf(System.currentTimeMillis()); + } + + // 添加服务类型信息 + String serviceTypes = ""; + if (iotMsgNotifyData.getBody().getServices() != null) { + for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { + serviceTypes += service.getServiceId() + ","; + } + } + + return deviceId + ":" + eventTime + ":" + serviceTypes; + } + + /** + * 检查是否为重复事件 + */ + private boolean isDuplicateEvent(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { try { - Thread.sleep(100); // 短暂等待 - return safeUpdateRuntimeStats(deviceId, iotMsgNotifyData); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - return false; + // 使用 massage_start_time 作为唯一标识 + Long startTime = extractMassageStartTime(iotMsgNotifyData); + if (startTime == null) { + return false; + } + + // 使用Redis设置短期过期键,实现幂等性检查 + String eventKey = "device:runtime:event:" + deviceId + ":" + startTime; + Boolean isNewEvent = redisTemplate.opsForValue().setIfAbsent( + eventKey, "1", 5, TimeUnit.MINUTES); // 5分钟幂等窗口 + + return isNewEvent != null && !isNewEvent; } catch (Exception e) { - log.error("运行统计重试也失败,设备: {}", deviceId, e); + log.warn("重复事件检查失败: {}", deviceId, e); + return false; + } + } + + private Long extractMassageStartTime(IotMsgNotifyDataPro iotMsgNotifyData) { + if (iotMsgNotifyData.getBody() == null || iotMsgNotifyData.getBody().getServices() == null) { + return null; + } + + for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { + if ("StatusChange".equals(service.getServiceId()) && service.getProperties() != null) { + Map props = service.getProperties(); + return safeParseTimestamp(props.get("massage_start_time")); + } + } + return null; + } + + private boolean doUpdateRuntimeStats(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData, int attempt) { + Date today = new Date(); + Long durationToAdd = calculateMassageDuration(iotMsgNotifyData); + Integer onlineCountToAdd = shouldCountOnline(iotMsgNotifyData) ? 1 : 0; + + // 方法1: 尝试使用原子更新 + if (attempt == 0) { + try { + int result = deviceRuntimeStatsMapper.atomicUpdateStats( + deviceId, today, durationToAdd, durationToAdd, durationToAdd, onlineCountToAdd); + if (result > 0) { + log.debug("原子更新运行统计成功: {}", deviceId); + return true; + } + } catch (Exception e) { + log.debug("原子更新失败,尝试其他方法: {}", deviceId); + } + } + + // 方法2: 尝试使用 INSERT ... ON DUPLICATE KEY UPDATE + if (attempt <= 1) { + try { + DeviceRuntimeStats stats = createStatsObject(deviceId, today, durationToAdd, onlineCountToAdd); + int result = deviceRuntimeStatsMapper.insertOrUpdate(stats); + if (result > 0) { + log.debug("插入或更新运行统计成功: {}", deviceId); + return true; + } + } catch (DuplicateKeyException e) { + // 继续尝试下一种方法 + if (attempt == 1) { + throw e; // 最后一次尝试时抛出异常 + } + } catch (Exception e) { + log.debug("插入或更新失败: {}", deviceId); + } + } + + // 方法3: 使用查询+更新(最安全但性能较差) + return fallbackUpdate(deviceId, today, durationToAdd, onlineCountToAdd); + } + + private DeviceRuntimeStats createStatsObject(String deviceId, Date statDate, Long duration, Integer onlineCount) { + DeviceRuntimeStats stats = new DeviceRuntimeStats(); + stats.setDeviceId(deviceId); + stats.setStatDate(statDate); + stats.setDailyDuration(duration); + stats.setWeeklyDuration(duration); + stats.setMonthlyDuration(duration); + stats.setOnlineCount(onlineCount); + stats.setCreateTime(new Date()); + stats.setUpdateTime(new Date()); + stats.setCreateBy("系统自动创建"); + return stats; + } + + private boolean fallbackUpdate(String deviceId, Date statDate, Long durationToAdd, Integer onlineCountToAdd) { + try { + // 使用行级锁查询 + DeviceRuntimeStats existingStats = deviceRuntimeStatsMapper.selectByDeviceAndDateForUpdate(deviceId, statDate); + + if (existingStats == null) { + // 插入新记录 + DeviceRuntimeStats newStats = createStatsObject(deviceId, statDate, durationToAdd, onlineCountToAdd); + deviceRuntimeStatsMapper.insert(newStats); + } else { + // 更新现有记录 + existingStats.setDailyDuration(existingStats.getDailyDuration() + durationToAdd); + existingStats.setWeeklyDuration(existingStats.getWeeklyDuration() + durationToAdd); + existingStats.setMonthlyDuration(existingStats.getMonthlyDuration() + durationToAdd); + existingStats.setOnlineCount(existingStats.getOnlineCount() + onlineCountToAdd); + existingStats.setUpdateTime(new Date()); + deviceRuntimeStatsMapper.updateById(existingStats); + } + return true; + } catch (Exception e) { + log.error("回退方案更新运行统计失败: {}", deviceId, e); return false; } } - /** - * 计算按摩时长 - */ private Long calculateMassageDuration(IotMsgNotifyDataPro iotMsgNotifyData) { if (iotMsgNotifyData.getBody() == null || iotMsgNotifyData.getBody().getServices() == null) { return 0L; @@ -189,17 +262,7 @@ public class DeviceRuntimeStatsServiceImpl extends ServiceImpl 0) { - long durationMs = endTime - startTime; - long durationSeconds = durationMs / 1000; - - // 验证时长合理性 - if (isDurationReasonable(durationSeconds)) { - return durationSeconds; - } else { - log.warn("按摩时长异常: {}秒,设备: {}", durationSeconds, - iotMsgNotifyData.getHeader().getDeviceId()); - return 0L; - } + return (endTime - startTime) / 1000; // 转换为秒 } break; } @@ -207,52 +270,47 @@ public class DeviceRuntimeStatsServiceImpl extends ServiceImpl= 0 && durationSeconds <= 24 * 60 * 60; // 24小时 - } - - /** - * 安全解析时间戳 - */ private Long safeParseTimestamp(Object timestampObj) { if (timestampObj == null) return null; - try { if (timestampObj instanceof Number) { - long timestamp = ((Number) timestampObj).longValue(); - if (timestamp < 10000000000L) { // 秒级时间戳 - return timestamp * 1000; - } else { // 毫秒级时间戳 - return timestamp; - } + return ((Number) timestampObj).longValue(); } else if (timestampObj instanceof String) { - String timestampStr = timestampObj.toString(); - if (timestampStr.contains(".")) { - double timestamp = Double.parseDouble(timestampStr); - return (long) (timestamp * 1000); - } else { - long timestamp = Long.parseLong(timestampStr); - if (timestamp < 10000000000L) { - return timestamp * 1000; - } else { - return timestamp; - } - } + return Long.parseLong(timestampObj.toString()); } } catch (Exception e) { log.warn("时间戳解析失败: {}", timestampObj); } return null; } -} + + @Override + public DeviceRuntimeStats selectDeviceRuntimeStatsById(Long id) { + return getById(id); + } + + @Override + public List selectDeviceRuntimeStatsList(DeviceRuntimeStats deviceRuntimeStats) { + return deviceRuntimeStatsMapper.selectDeviceRuntimeStatsList(deviceRuntimeStats); + } + + @Override + public int insertDeviceRuntimeStats(DeviceRuntimeStats deviceRuntimeStats) { + return save(deviceRuntimeStats) ? 1 : 0; + } + + @Override + public int updateDeviceRuntimeStats(DeviceRuntimeStats deviceRuntimeStats) { + return updateById(deviceRuntimeStats) ? 1 : 0; + } + + @Override + public int deleteDeviceRuntimeStatsByIds(Long[] ids) { + return removeByIds(Arrays.asList(ids)) ? 1 : 0; + } +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/service/impl/DeviceStatusLogServiceImpl.java b/storm-device/src/main/java/com/storm/device/service/impl/DeviceStatusLogServiceImpl.java index d96d3fc..40485cc 100644 --- a/storm-device/src/main/java/com/storm/device/service/impl/DeviceStatusLogServiceImpl.java +++ b/storm-device/src/main/java/com/storm/device/service/impl/DeviceStatusLogServiceImpl.java @@ -1,16 +1,19 @@ package com.storm.device.service.impl; +import java.time.Duration; import java.util.*; + +import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.storm.device.domain.po.DeviceStatusLog; -import com.storm.device.manager.DeviceManagerService; import com.storm.device.mapper.DeviceStatusLogMapper; import com.storm.device.service.IDeviceStatusLogService; import com.storm.device.task.vo.IotMsgNotifyDataPro; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DuplicateKeyException; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.transaction.annotation.Propagation; @@ -28,8 +31,9 @@ public class DeviceStatusLogServiceImpl extends ServiceImpl redisTemplate; + private static final String STATUS_LOG_PREFIX = "status:log:id:"; + private static final long STATUS_LOG_EXPIRE_HOURS = 24; @Override public List selectDeviceStatusLogList(DeviceStatusLog deviceStatusLog) { return deviceStatusLogMapper.selectList(Wrappers.lambdaQuery() @@ -41,93 +45,32 @@ public class DeviceStatusLogServiceImpl extends ServiceImpl services) { - for (IotMsgNotifyDataPro.IotMsgService service : services) { - if (service.getProperties() == null) continue; - - Map properties = service.getProperties(); - - // 处理按摩相关属性 - if (properties.containsKey("head_type")) { - statusLog.setHeadType(properties.get("head_type").toString()); - } - if (properties.containsKey("body_part")) { - statusLog.setBodyPart(properties.get("body_part").toString()); - } - if (properties.containsKey("massage_plan")) { - statusLog.setMassagePlan(properties.get("massage_plan").toString()); - } - - // 处理按摩时间 - if (properties.containsKey("massage_start_time")) { - Long startTime = parseTimestampToMillis(properties.get("massage_start_time")); - statusLog.setMassageStartTime(startTime); - } - if (properties.containsKey("massage_end_time")) { - Long endTime = parseTimestampToMillis(properties.get("massage_end_time")); - statusLog.setMassageEndTime(endTime); - - // 计算持续时间 - if (statusLog.getMassageStartTime() != null && endTime != null) { - statusLog.setMassageDuration(endTime - statusLog.getMassageStartTime()); - } - } - - // 如果是按摩任务,设置事件类型 - if (properties.containsKey("massage_start_time") || - properties.containsKey("head_type") || - properties.containsKey("body_part")) { - statusLog.setEventType("MASSAGE_TASK"); - } - } - } - - /** - * 解析时间戳为毫秒 - */ - private Long parseTimestampToMillis(Object timestampObj) { - if (timestampObj == null) return null; - - try { - if (timestampObj instanceof Number) { - long timestamp = ((Number) timestampObj).longValue(); - if (timestamp < 10000000000L) { // 秒级时间戳 - return timestamp * 1000; - } else { // 毫秒级时间戳 - return timestamp; - } - } else if (timestampObj instanceof String) { - String timestampStr = timestampObj.toString(); - if (timestampStr.contains(".")) { - // 处理浮点数时间戳 - double timestamp = Double.parseDouble(timestampStr); - return (long) (timestamp * 1000); - } else { - long timestamp = Long.parseLong(timestampStr); - if (timestamp < 10000000000L) { - return timestamp * 1000; - } else { - return timestamp; - } - } - } - } catch (Exception e) { - log.error("时间戳解析失败: {}", timestampObj, e); - } - return null; - } - - /** - * 安全插入状态日志 - 多层保护机制 - */ - @Transactional(propagation = Propagation.REQUIRES_NEW) @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) public boolean safeInsertStatusLog(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { + // 生成状态日志唯一标识 + String statusLogUniqueId = generateStatusLogUniqueId(deviceId, iotMsgNotifyData); + if (StrUtil.isNotBlank(statusLogUniqueId)) { + String idempotentKey = STATUS_LOG_PREFIX + statusLogUniqueId; + Boolean isNewLog = redisTemplate.opsForValue().setIfAbsent( + idempotentKey, "1", Duration.ofHours(STATUS_LOG_EXPIRE_HOURS)); + + if (isNewLog != null && !isNewLog) { + log.debug("状态日志已存在,跳过插入: {}", statusLogUniqueId); + return true; // 已存在也算成功 + } + } + for (int attempt = 0; attempt < 3; attempt++) { try { - return doInsertStatusLog(deviceId, iotMsgNotifyData, attempt); + boolean result = doInsertStatusLog(deviceId, iotMsgNotifyData, attempt); + if (!result && StrUtil.isNotBlank(statusLogUniqueId)) { + // 插入失败时,删除幂等键,允许重试 + redisTemplate.delete(STATUS_LOG_PREFIX + statusLogUniqueId); + } + return result; } catch (DuplicateKeyException e) { log.debug("状态日志重复键异常,尝试次数: {},设备: {}", attempt + 1, deviceId); if (attempt == 2) { @@ -144,6 +87,10 @@ public class DeviceStatusLogServiceImpl extends ServiceImpl services = iotMsgNotifyData.getBody().getServices(); - if(services!=null&&services.size()>0){ - for (IotMsgNotifyDataPro.IotMsgService service : services) { - if ("StatusChange".equals(service.getServiceId()) && service.getProperties() != null) { - Map props = service.getProperties(); - Long startTime = safeParseTimestamp(props.get("massage_start_time")); - Long endTime = safeParseTimestamp(props.get("massage_end_time")); - statusLog.setMassageStartTime(startTime); - statusLog.setMassageEndTime(endTime); - statusLog.setHeadType(safeGetString(props, "head_type")); - statusLog.setBodyPart(safeGetString(props, "body_part")); - if (startTime != null && endTime != null && startTime < endTime && startTime > 0) { - long duration = (endTime - startTime); - statusLog.setDuration((int)duration); - statusLog.setMassageDuration(duration); - } - } - } - } - // 处理服务数据 - processServiceData(statusLog, iotMsgNotifyData); - - return statusLog; - } - - private String safeGetString(Map props, String key) { - if (props == null || !props.containsKey(key)) return null; - Object value = props.get(key); - return value != null ? value.toString() : null; - } - - /** - * 处理服务数据 - */ - private void processServiceData(DeviceStatusLog statusLog, IotMsgNotifyDataPro iotMsgNotifyData) { - if (iotMsgNotifyData.getBody() == null || iotMsgNotifyData.getBody().getServices() == null) { - return; - } - - for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { - if ("StatusChange".equals(service.getServiceId()) && service.getProperties() != null) { - // 处理按摩相关数据 - processMassageData(statusLog, service.getProperties()); - break; - } - } - } - - /** - * 处理按摩数据 - 修复版本 - */ - private void processMassageData(DeviceStatusLog statusLog, java.util.Map properties) { - if (properties.containsKey("head_type") && properties.get("head_type") != null) { - statusLog.setHeadType(properties.get("head_type").toString()); - } - if (properties.containsKey("body_part") && properties.get("body_part") != null) { - statusLog.setBodyPart(properties.get("body_part").toString()); - } - if (properties.containsKey("massage_plan") && properties.get("massage_plan") != null) { - statusLog.setMassagePlan(properties.get("massage_plan").toString()); - } - - // 处理按摩时间 - 使用安全解析 - Long startTime = safeParseTimestamp(properties.get("massage_start_time")); - Long endTime = safeParseTimestamp(properties.get("massage_end_time")); - - if (startTime != null) { - statusLog.setMassageStartTime(startTime); - } - if (endTime != null) { - statusLog.setMassageEndTime(endTime); - } - - // 安全计算持续时间 - Long duration = safeCalculateDuration(startTime, endTime); - if (duration != null) { - statusLog.setMassageDuration(duration); - statusLog.setDuration(duration.intValue()); - - // 如果是按摩任务,更改事件类型 - statusLog.setEventType("MASSAGE_TASK"); - log.debug("计算按摩持续时间: {} 秒", duration); - } else { - // 设置默认值 - statusLog.setMassageDuration(0L); - statusLog.setDuration(0); - log.warn("无法计算有效的按摩持续时间,startTime: {}, endTime: {}", startTime, endTime); - } - } - private boolean insertWithIgnore(DeviceStatusLog statusLog) { try { int result = deviceStatusLogMapper.insertOrIgnore(statusLog); @@ -379,11 +237,10 @@ public class DeviceStatusLogServiceImpl extends ServiceImpl props = service.getProperties(); + if (props == null) continue; - // 如果是秒级时间戳,转换为毫秒后检查 - if (timestamp < 10000000000L) { - timestamp = timestamp * 1000; + // 补全缺失字段 + completeMissingServiceProperties(props, statusLog.getDeviceId()); + + // 最终判断:只要包含按摩开始时间、按摩头类型、身体部位中任意两个字段,就认为是按摩任务 + boolean hasMassageStart = props.containsKey("massage_start_time"); + boolean hasHeadType = props.containsKey("head_type") && props.get("head_type") != null; + boolean hasBodyPart = props.containsKey("body_part") && props.get("body_part") != null; + + int massageEvidenceCount = (hasMassageStart ? 1 : 0) + (hasHeadType ? 1 : 0) + (hasBodyPart ? 1 : 0); + + if (massageEvidenceCount >= 2) { + statusLog.setEventType("MASSAGE_TASK"); + processMassageData(statusLog, props); // 处理按摩数据 + } else if ("StatusChange".equals(service.getServiceId())) { + // 普通状态变更事件 + statusLog.setEventType("STATUS_CHANGE"); + } else { + // 其他类型的事件 + statusLog.setEventType(service.getServiceId()); + } + // 找到一个服务就跳出循环,通常一个消息只包含一个主要事件 + break; + } + } + + /** + * 补全服务属性中的缺失字段 + */ + private void completeMissingServiceProperties(Map props, String deviceId) { + if (!props.containsKey("head_type")) { + props.put("head_type", "default_head"); + log.debug("设备 {} 的 head_type 缺失,使用默认值", deviceId); + } + if (!props.containsKey("body_part")) { + props.put("body_part", "default_body_part"); + log.debug("设备 {} 的 body_part 缺失,使用默认值", deviceId); + } + if (!props.containsKey("massage_plan")) { + props.put("massage_plan", "default_plan"); + log.debug("设备 {} 的 massage_plan 缺失,使用默认值", deviceId); + } + if (!props.containsKey("massage_start_time")) { + long startTime = System.currentTimeMillis() - 600000; // 10分钟前 + props.put("massage_start_time", startTime); + log.debug("设备 {} 的 massage_start_time 缺失,使用默认值", deviceId); + } + if (!props.containsKey("massage_end_time")) { + long endTime = System.currentTimeMillis(); + props.put("massage_end_time", endTime); + log.debug("设备 {} 的 massage_end_time 缺失,使用默认值", deviceId); + } + } + + /** + * 处理按摩数据 - 修复版本,确保字段正确设置 + */ + private void processMassageData(DeviceStatusLog statusLog, Map properties) { + // 使用补全后的属性 + if (properties.containsKey("head_type") && properties.get("head_type") != null) { + statusLog.setHeadType(properties.get("head_type").toString()); + } + if (properties.containsKey("body_part") && properties.get("body_part") != null) { + statusLog.setBodyPart(properties.get("body_part").toString()); + } + if (properties.containsKey("massage_plan") && properties.get("massage_plan") != null) { + statusLog.setMassagePlan(properties.get("massage_plan").toString()); } - return timestamp >= minValidTimestamp && timestamp <= maxValidTimestamp; + // 处理按摩时间 - 使用安全解析 + Long startTime = safeParseTimestamp(properties.get("massage_start_time")); + Long endTime = safeParseTimestamp(properties.get("massage_end_time")); + + if (startTime != null) { + statusLog.setMassageStartTime(startTime); + } + if (endTime != null) { + statusLog.setMassageEndTime(endTime); + } + + // 安全计算持续时间 + Long duration = safeCalculateDuration(startTime, endTime); + if (duration != null) { + statusLog.setMassageDuration(duration); + statusLog.setDuration(duration.intValue()); + } else { + // 设置默认值 + statusLog.setMassageDuration(0L); + statusLog.setDuration(0); + log.warn("无法计算有效的按摩持续时间,startTime: {}, endTime: {}", startTime, endTime); + } } } diff --git a/storm-device/src/main/java/com/storm/device/task/LocationAmqpClient.java b/storm-device/src/main/java/com/storm/device/task/LocationAmqpClient.java index 4567840..96abb2b 100644 --- a/storm-device/src/main/java/com/storm/device/task/LocationAmqpClient.java +++ b/storm-device/src/main/java/com/storm/device/task/LocationAmqpClient.java @@ -2,6 +2,8 @@ package com.storm.device.task; import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.storm.device.config.properties.HuaWeiIotConfigProperties; @@ -15,6 +17,7 @@ import org.apache.qpid.jms.transports.TransportSupport; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Component; import javax.jms.*; @@ -22,6 +25,7 @@ import java.net.InetAddress; import java.net.URI; import java.net.UnknownHostException; import java.text.MessageFormat; +import java.time.Duration; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -33,13 +37,15 @@ import java.util.stream.Collectors; public class LocationAmqpClient implements ApplicationRunner { @Autowired private HuaWeiIotConfigProperties huaWeiIotConfigProperties; - //业务处理异步线程池,线程池参数可以根据您的业务特点调整,或者您也可以用其他异步方式处理接收到的消息。 @Autowired private ThreadPoolTaskExecutor threadPoolTaskExecutor; @Autowired private DeviceDataCoordinator deviceDataCoordinator; - + @Autowired + private RedisTemplate redisTemplate; + private static final String MESSAGE_IDEMPOTENT_PREFIX = "iot:message:id:"; + private static final long MESSAGE_IDEMPOTENT_EXPIRE_MINUTES = 5; //控制台服务端订阅中消费组状态页客户端ID一栏将显示clientId参数。 //建议使用机器UUID、MAC地址、IP等唯一标识等作为clientId。便于您区分识别不同的客户端。 private static String clientId; @@ -177,12 +183,31 @@ public class LocationAmqpClient implements ApplicationRunner { // 修改processMessage方法 private void processMessage(Message message) { + String contentStr; + String messageId = null; try { - String contentStr = message.getBody(String.class); + contentStr = message.getBody(String.class); String topic = message.getStringProperty("topic"); - String messageId = message.getStringProperty("messageId"); + messageId = message.getStringProperty("messageId"); log.info("receive message,\n topic = {},\n messageId = {},\n content = {}", topic, messageId, contentStr); + // 生成幂等键:如果 messageId 为空,则基于消息内容生成 SHA256 哈希 + if (StrUtil.isBlank(messageId)) { + messageId = DigestUtil.sha256Hex(contentStr); + log.debug("生成基于内容的 messageId: {}", messageId); + } + + // 幂等性检查 + String idempotentKey = MESSAGE_IDEMPOTENT_PREFIX + messageId; + Boolean isNewMessage = redisTemplate.opsForValue().setIfAbsent( + idempotentKey, "1", Duration.ofMinutes(MESSAGE_IDEMPOTENT_EXPIRE_MINUTES)); + + if (isNewMessage != null && !isNewMessage) { + log.info("消息已处理,跳过重复消息,messageId: {}", messageId); + return; + } + + // 处理消息内容 JSONObject jsonMsg = JSONUtil.parseObj(contentStr); JSONObject notifyData = jsonMsg.getJSONObject("notify_data"); if (ObjectUtil.isEmpty(notifyData)) { @@ -190,12 +215,15 @@ public class LocationAmqpClient implements ApplicationRunner { } IotMsgNotifyDataPro iotMsgNotifyData = JSONUtil.toBean(notifyData, IotMsgNotifyDataPro.class); - - // 使用统一处理器处理所有数据 deviceDataCoordinator.processDeviceData(iotMsgNotifyData); + deviceDataCoordinator.coordinateDeviceData(iotMsgNotifyData); } catch (Exception e) { - log.error("处理消息时发生异常", e); + log.error("处理消息时发生异常, messageId: {}", messageId, e); + // 处理失败时,删除幂等键,允许重试 + if (StrUtil.isNotBlank(messageId)) { + redisTemplate.delete(MESSAGE_IDEMPOTENT_PREFIX + messageId); + } } } diff --git a/storm-device/src/main/java/com/storm/device/task/MassageAmqpClient.java b/storm-device/src/main/java/com/storm/device/task/MassageAmqpClient.java index 6b101d5..2de3dcb 100644 --- a/storm-device/src/main/java/com/storm/device/task/MassageAmqpClient.java +++ b/storm-device/src/main/java/com/storm/device/task/MassageAmqpClient.java @@ -2,6 +2,8 @@ package com.storm.device.task; import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.storm.device.config.properties.HuaWeiIotConfigProperties; @@ -15,6 +17,7 @@ import org.apache.qpid.jms.transports.TransportSupport; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Component; import javax.jms.*; @@ -22,6 +25,7 @@ import java.net.InetAddress; import java.net.URI; import java.net.UnknownHostException; import java.text.MessageFormat; +import java.time.Duration; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -34,10 +38,14 @@ public class MassageAmqpClient implements ApplicationRunner { private DeviceDataCoordinator deviceDataProcessor; @Autowired private HuaWeiIotConfigProperties huaWeiIotConfigProperties; - + @Autowired + private DeviceDataCoordinator deviceDataCoordinator; @Autowired private ThreadPoolTaskExecutor threadPoolTaskExecutor; - + @Autowired + private RedisTemplate redisTemplate; + private static final String MESSAGE_IDEMPOTENT_PREFIX = "iot:message:id:"; + private static final long MESSAGE_IDEMPOTENT_EXPIRE_MINUTES = 5; //控制台服务端订阅中消费组状态页客户端ID一栏将显示clientId参数。 //建议使用机器UUID、MAC地址、IP等唯一标识等作为clientId。便于您区分识别不同的客户端。 private static String clientId; @@ -173,12 +181,31 @@ public class MassageAmqpClient implements ApplicationRunner { // 修改processMessage方法 private void processMessage(Message message) { + String contentStr; + String messageId = null; try { - String contentStr = message.getBody(String.class); + contentStr = message.getBody(String.class); String topic = message.getStringProperty("topic"); - String messageId = message.getStringProperty("messageId"); + messageId = message.getStringProperty("messageId"); log.info("receive message,\n topic = {},\n messageId = {},\n content = {}", topic, messageId, contentStr); + // 生成幂等键:如果 messageId 为空,则基于消息内容生成 SHA256 哈希 + if (StrUtil.isBlank(messageId)) { + messageId = DigestUtil.sha256Hex(contentStr); + log.debug("生成基于内容的 messageId: {}", messageId); + } + + // 幂等性检查 + String idempotentKey = MESSAGE_IDEMPOTENT_PREFIX + messageId; + Boolean isNewMessage = redisTemplate.opsForValue().setIfAbsent( + idempotentKey, "1", Duration.ofMinutes(MESSAGE_IDEMPOTENT_EXPIRE_MINUTES)); + + if (isNewMessage != null && !isNewMessage) { + log.info("消息已处理,跳过重复消息,messageId: {}", messageId); + return; + } + + // 处理消息内容 JSONObject jsonMsg = JSONUtil.parseObj(contentStr); JSONObject notifyData = jsonMsg.getJSONObject("notify_data"); if (ObjectUtil.isEmpty(notifyData)) { @@ -186,12 +213,15 @@ public class MassageAmqpClient implements ApplicationRunner { } IotMsgNotifyDataPro iotMsgNotifyData = JSONUtil.toBean(notifyData, IotMsgNotifyDataPro.class); - - // 使用统一处理器处理所有数据 - deviceDataProcessor.processDeviceData(iotMsgNotifyData); + deviceDataCoordinator.processDeviceData(iotMsgNotifyData); + deviceDataCoordinator.coordinateDeviceData(iotMsgNotifyData); } catch (Exception e) { - log.error("处理消息时发生异常", e); + log.error("处理消息时发生异常, messageId: {}", messageId, e); + // 处理失败时,删除幂等键,允许重试 + if (StrUtil.isNotBlank(messageId)) { + redisTemplate.delete(MESSAGE_IDEMPOTENT_PREFIX + messageId); + } } } diff --git a/storm-device/src/main/java/com/storm/device/task/StatusAmqpClient.java b/storm-device/src/main/java/com/storm/device/task/StatusAmqpClient.java index bbcaa35..e03dd46 100644 --- a/storm-device/src/main/java/com/storm/device/task/StatusAmqpClient.java +++ b/storm-device/src/main/java/com/storm/device/task/StatusAmqpClient.java @@ -4,6 +4,7 @@ import cn.hutool.core.date.DateUtil; import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; import cn.hutool.json.JSONArray; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; @@ -18,14 +19,17 @@ import org.apache.qpid.jms.transports.TransportSupport; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Component; + import javax.jms.*; import java.net.InetAddress; import java.net.URI; import java.net.UnknownHostException; import java.text.MessageFormat; import java.text.SimpleDateFormat; +import java.time.Duration; import java.util.*; import java.util.stream.Collectors; @@ -36,13 +40,14 @@ public class StatusAmqpClient implements ApplicationRunner { private DeviceDataCoordinator deviceDataCoordinator; @Autowired private HuaWeiIotConfigProperties huaWeiIotConfigProperties; + @Autowired + private RedisTemplate redisTemplate; + private static final String MESSAGE_IDEMPOTENT_PREFIX = "iot:message:id:"; + private static final long MESSAGE_IDEMPOTENT_EXPIRE_MINUTES = 5; - //业务处理异步线程池,线程池参数可以根据您的业务特点调整,或者您也可以用其他异步方式处理接收到的消息。 @Autowired private ThreadPoolTaskExecutor threadPoolTaskExecutor; - //控制台服务端订阅中消费组状态页客户端ID一栏将显示clientId参数。 - //建议使用机器UUID、MAC地址、IP等唯一标识等作为clientId。便于您区分识别不同的客户端。 private static String clientId; static { @@ -58,42 +63,27 @@ public class StatusAmqpClient implements ApplicationRunner { } public void start() throws Exception { - //参数说明,请参见AMQP客户端接入说明文档。 for (int i = 0; i < huaWeiIotConfigProperties.getConnectionCount(); i++) { - //创建amqp连接 Connection connection = getConnection(); - - //加入监听者 ((JmsConnection) connection).addConnectionListener(myJmsConnectionListener); - // 创建会话。 - // Session.CLIENT_ACKNOWLEDGE: 收到消息后,需要手动调用message.acknowledge()。 - // Session.AUTO_ACKNOWLEDGE: SDK自动ACK(推荐)。 Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); connection.start(); - // 创建Receiver连接。 MessageConsumer consumer = newConsumer(session, connection, "queue-status"); consumer.setMessageListener(messageListener); } - log.info("amqp is started successfully, and will exit after server shutdown "); + log.info("amqp is started successfully, and will exit after server shutdown"); } - /** - * 创建amqp连接 - * - * @return amqp连接 - */ private Connection getConnection() throws Exception { String connectionUrl = generateConnectUrl(); JmsConnectionFactory cf = new JmsConnectionFactory(connectionUrl); - // 信任服务端 TransportOptions to = new TransportOptions(); to.setTrustAll(true); cf.setSslContext(TransportSupport.createJdkSslContext(to)); String userName = "accessKey=" + huaWeiIotConfigProperties.getAccessKey(); cf.setExtension(JmsConnectionExtensions.USERNAME_OVERRIDE.toString(), (connection, uri) -> { - // IoTDA的userName组成格式如下:“accessKey=${accessKey}|timestamp=${timestamp}” String newUserName = userName; if (connection instanceof JmsConnection) { newUserName = ((JmsConnection) connection).getUsername(); @@ -101,15 +91,9 @@ public class StatusAmqpClient implements ApplicationRunner { return newUserName + "|timestamp=" + System.currentTimeMillis(); }); - // 创建连接。 return cf.createConnection(userName, huaWeiIotConfigProperties.getAccessCode()); } - /** - * 生成amqp连接地址 - * - * @return amqp连接地址 - */ public String generateConnectUrl() { String uri = MessageFormat.format("{0}://{1}:{2}", (huaWeiIotConfigProperties.isUseSsl() ? "amqps" : "amqp"), @@ -151,14 +135,6 @@ public class StatusAmqpClient implements ApplicationRunner { return stringBuilder.toString(); } - /** - * 创建消费者 - * - * @param session session - * @param connection amqp连接 - * @param queueName 队列名称 - * @return 消费者 - */ public MessageConsumer newConsumer(Session session, Connection connection, String queueName) throws Exception { if (connection == null || !(connection instanceof JmsConnection) || ((JmsConnection) connection).isClosed()) { throw new Exception("create consumer failed,the connection is disconnected."); @@ -169,34 +145,51 @@ public class StatusAmqpClient implements ApplicationRunner { private final MessageListener messageListener = message -> { try { - //异步处理收到的消息,确保onMessage函数里没有耗时逻辑 threadPoolTaskExecutor.submit(() -> processMessage(message)); } catch (Exception e) { log.error("submit task occurs exception ", e); } }; - // 修改processMessage方法 private void processMessage(Message message) { + String contentStr; + String messageId = null; try { - String contentStr = message.getBody(String.class); + contentStr = message.getBody(String.class); String topic = message.getStringProperty("topic"); - String messageId = message.getStringProperty("messageId"); + messageId = message.getStringProperty("messageId"); log.info("receive message,\n topic = {},\n messageId = {},\n content = {}", topic, messageId, contentStr); + if (StrUtil.isBlank(messageId)) { + messageId = DigestUtil.sha256Hex(contentStr); + log.debug("生成基于内容的 messageId: {}", messageId); + } + + String idempotentKey = MESSAGE_IDEMPOTENT_PREFIX + messageId; + Boolean isNewMessage = redisTemplate.opsForValue().setIfAbsent( + idempotentKey, "1", Duration.ofMinutes(MESSAGE_IDEMPOTENT_EXPIRE_MINUTES)); + + if (isNewMessage != null && !isNewMessage) { + log.info("消息已处理,跳过重复消息,messageId: {}", messageId); + return; + } + JSONObject jsonMsg = JSONUtil.parseObj(contentStr); JSONObject notifyData = jsonMsg.getJSONObject("notify_data"); if (ObjectUtil.isEmpty(notifyData)) { return; } - // 手动处理日期字段,避免Hutool自动转换失败 + // 使用自定义解析方法而不是 Hutool 的自动转换 IotMsgNotifyDataPro iotMsgNotifyData = parseIotMsgNotifyData(notifyData); - // 使用统一处理器处理所有数据 deviceDataCoordinator.processDeviceData(iotMsgNotifyData); + deviceDataCoordinator.coordinateDeviceData(iotMsgNotifyData); } catch (Exception e) { - log.error("处理消息时发生异常", e); + log.error("处理消息时发生异常, messageId: {}", messageId, e); + if (StrUtil.isNotBlank(messageId)) { + redisTemplate.delete(MESSAGE_IDEMPOTENT_PREFIX + messageId); + } } } @@ -297,7 +290,6 @@ public class StatusAmqpClient implements ApplicationRunner { return iotMsgNotifyData; } - // 在 StatusAmqpClient 中修复日期解析方法 /** * 解析ISO日期时间格式(增强版) */ @@ -307,15 +299,20 @@ public class StatusAmqpClient implements ApplicationRunner { } try { + // 处理格式:20251005T073035Z (16位,没有分隔符) + if (dateTimeStr.length() == 16 && dateTimeStr.endsWith("Z") && dateTimeStr.contains("T")) { + String pattern = "yyyyMMdd'T'HHmmss'Z'"; + SimpleDateFormat sdf = new SimpleDateFormat(pattern); + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + return sdf.parse(dateTimeStr); + } + // 处理格式:20250927T070832Z (16位,没有分隔符) if (dateTimeStr.length() == 16 && dateTimeStr.endsWith("Z") && dateTimeStr.contains("T")) { String pattern = "yyyyMMdd'T'HHmmss'Z'"; SimpleDateFormat sdf = new SimpleDateFormat(pattern); sdf.setTimeZone(TimeZone.getTimeZone("UTC")); - Date utcDate = sdf.parse(dateTimeStr); - - // 转换为本地时间(如果需要) - return utcDate; + return sdf.parse(dateTimeStr); } // 处理格式:2025-09-27T07:08:31.394Z (标准ISO格式) @@ -366,33 +363,21 @@ public class StatusAmqpClient implements ApplicationRunner { } private final JmsConnectionListener myJmsConnectionListener = new JmsConnectionListener() { - /** - * 连接成功建立。 - */ @Override public void onConnectionEstablished(URI remoteURI) { log.info("onConnectionEstablished, remoteUri:{}", remoteURI); } - /** - * 尝试过最大重试次数之后,最终连接失败。 - */ @Override public void onConnectionFailure(Throwable error) { log.error("onConnectionFailure, {}", error.getMessage()); } - /** - * 连接中断。 - */ @Override public void onConnectionInterrupted(URI remoteURI) { log.info("onConnectionInterrupted, remoteUri:{}", remoteURI); } - /** - * 连接中断后又自动重连上。 - */ @Override public void onConnectionRestored(URI remoteURI) { log.info("onConnectionRestored, remoteUri:{}", remoteURI); diff --git a/storm-device/src/test/java/com/storm/device/1.json b/storm-device/src/test/java/com/storm/device/1.json deleted file mode 100644 index f70ad2c..0000000 --- a/storm-device/src/test/java/com/storm/device/1.json +++ /dev/null @@ -1,24 +0,0 @@ -[ - JmsSession - [ - ID - : - 51c3b405-6453-46e2-b41a-8aab14796183 - : - 1 - : - 1 - ] - delivery - dispatcher -] INFO com.iot.amqp.examples.AbstractAmqpExample - receive a message,content={"resource": "device.property", "event": "report","event_time": "20250830T062115Z", "event_time_ms": "2025-08-30T06:21:15.731Z", "request_id":"64840a97-683c-495d-af14-19f6f9e18b20", "notify_data": {"header": {"app_id":"9782371cee894c0abbecb30efe4e7cd0", "device_id": "673602f0d14760402fbd033b_jsfb-DA", "node_id": "jsfb-DA","product_id": "673602f0d14760402fbd033b", "gateway_id": "673602f0d14760402fbd033b_jsfb-DA"}, "body": {"services":[{"service_id": "VortXDB", "properties": {"online_time":"20250830T062115Z", "latitude": "112.541115", "software_version": "ioT","vtxdb_version": "1.1.9-beta-ansun", "status": "ONLINE", "longitude":"28.280514"}, "event_time": "20250830T062115Z"}] -} -} -} -{"resource": "device.property", "event": "report", "event_time": "20250830T062115Z", "event_time_ms": "2025-08-30T06:21:15.731Z", "request_id": "64840a97-683c-495d-af14-19f6f9e18b20", -"notify_data": {"header": -{"app_id": "9782371cee894c0abbecb30efe4e7cd0", "device_id": "673602f0d14760402fbd033b_jsfb-DA", "node_id": "jsfb-DA", "product_id": "673602f0d14760402fbd033b", "gateway_id": "673602f0d14760402fbd033b_jsfb-DA"}, -"body": {"services"} -:[{"service_id": "VortXDB", -"properties": {"online_time":"20250830T062115Z", "latitude": "112.541115", "software_version": "ioT","vtxdb_version": "1.1.9-beta-ansun", "status": "ONLINE", "longitude":"28.280514"}, "event_time": "20250830T062115Z" -} \ No newline at end of file diff --git a/storm-device/src/test/java/com/storm/device/2.json b/storm-device/src/test/java/com/storm/device/2.json deleted file mode 100644 index ee8e8b4..0000000 --- a/storm-device/src/test/java/com/storm/device/2.json +++ /dev/null @@ -1,659 +0,0 @@ -{ - "devices": [ - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-DA", - "nodeId": "jsfb-DA", - "gatewayId": "673602f0d14760402fbd033b_jsfb-DA", - "deviceName": "jsfb-DA", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-023486", - "nodeId": "jsfb-023486", - "gatewayId": "673602f0d14760402fbd033b_jsfb-023486", - "deviceName": "jsfb-023486", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-6FF12E", - "nodeId": "jsfb-6FF12E", - "gatewayId": "673602f0d14760402fbd033b_jsfb-6FF12E", - "deviceName": "jsfb-6FF12E", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-62B336", - "nodeId": "jsfb-62B336", - "gatewayId": "673602f0d14760402fbd033b_jsfb-62B336", - "deviceName": "jsfb-62B336", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-F706AD", - "nodeId": "jsfb-F706AD", - "gatewayId": "673602f0d14760402fbd033b_jsfb-F706AD", - "deviceName": "jsfb-F706AD", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-50441C", - "nodeId": "jsfb-50441C", - "gatewayId": "673602f0d14760402fbd033b_jsfb-50441C", - "deviceName": "jsfb-50441C", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-B25A00", - "nodeId": "jsfb-B25A00", - "gatewayId": "673602f0d14760402fbd033b_jsfb-B25A00", - "deviceName": "jsfb-B25A00", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-DCF894", - "nodeId": "jsfb-DCF894", - "gatewayId": "673602f0d14760402fbd033b_jsfb-DCF894", - "deviceName": "jsfb-DCF894", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-6F0F92", - "nodeId": "jsfb-6F0F92", - "gatewayId": "673602f0d14760402fbd033b_jsfb-6F0F92", - "deviceName": "jsfb-6F0F92", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-5A1B1E", - "nodeId": "jsfb-5A1B1E", - "gatewayId": "673602f0d14760402fbd033b_jsfb-5A1B1E", - "deviceName": "jsfb-5A1B1E", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-1DB8E2", - "nodeId": "jsfb-1DB8E2", - "gatewayId": "673602f0d14760402fbd033b_jsfb-1DB8E2", - "deviceName": "jsfb-1DB8E2", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-FD7C6B", - "nodeId": "jsfb-FD7C6B", - "gatewayId": "673602f0d14760402fbd033b_jsfb-FD7C6B", - "deviceName": "jsfb-FD7C6B", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-3554A0", - "nodeId": "jsfb-3554A0", - "gatewayId": "673602f0d14760402fbd033b_jsfb-3554A0", - "deviceName": "jsfb-3554A0", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-BD51A6", - "nodeId": "jsfb-BD51A6", - "gatewayId": "673602f0d14760402fbd033b_jsfb-BD51A6", - "deviceName": "jsfb-BD51A6", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-98BF09", - "nodeId": "jsfb-98BF09", - "gatewayId": "673602f0d14760402fbd033b_jsfb-98BF09", - "deviceName": "jsfb-98BF09", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-B6A9C3", - "nodeId": "jsfb-B6A9C3", - "gatewayId": "673602f0d14760402fbd033b_jsfb-B6A9C3", - "deviceName": "jsfb-B6A9C3", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-EBF1D8", - "nodeId": "jsfb-EBF1D8", - "gatewayId": "673602f0d14760402fbd033b_jsfb-EBF1D8", - "deviceName": "jsfb-EBF1D8", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-65024A", - "nodeId": "jsfb-65024A", - "gatewayId": "673602f0d14760402fbd033b_jsfb-65024A", - "deviceName": "jsfb-65024A", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-7FC7DD", - "nodeId": "jsfb-7FC7DD", - "gatewayId": "673602f0d14760402fbd033b_jsfb-7FC7DD", - "deviceName": "jsfb-7FC7DD", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-7E9B30", - "nodeId": "jsfb-7E9B30", - "gatewayId": "673602f0d14760402fbd033b_jsfb-7E9B30", - "deviceName": "jsfb-7E9B30", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-56A928", - "nodeId": "jsfb-56A928", - "gatewayId": "673602f0d14760402fbd033b_jsfb-56A928", - "deviceName": "jsfb-56A928", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-F8843B", - "nodeId": "jsfb-F8843B", - "gatewayId": "673602f0d14760402fbd033b_jsfb-F8843B", - "deviceName": "jsfb-F8843B", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-8AA2BD", - "nodeId": "jsfb-8AA2BD", - "gatewayId": "673602f0d14760402fbd033b_jsfb-8AA2BD", - "deviceName": "jsfb-8AA2BD", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-C17E68", - "nodeId": "jsfb-C17E68", - "gatewayId": "673602f0d14760402fbd033b_jsfb-C17E68", - "deviceName": "jsfb-C17E68", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-7BE292", - "nodeId": "jsfb-7BE292", - "gatewayId": "673602f0d14760402fbd033b_jsfb-7BE292", - "deviceName": "jsfb-7BE292", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-6A1F29", - "nodeId": "jsfb-6A1F29", - "gatewayId": "673602f0d14760402fbd033b_jsfb-6A1F29", - "deviceName": "jsfb-6A1F29", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-B75756", - "nodeId": "jsfb-B75756", - "gatewayId": "673602f0d14760402fbd033b_jsfb-B75756", - "deviceName": "jsfb-B75756", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-69B0D2", - "nodeId": "jsfb-69B0D2", - "gatewayId": "673602f0d14760402fbd033b_jsfb-69B0D2", - "deviceName": "jsfb-69B0D2", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-D120B0", - "nodeId": "jsfb-D120B0", - "gatewayId": "673602f0d14760402fbd033b_jsfb-D120B0", - "deviceName": "jsfb-D120B0", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-CA0D7A", - "nodeId": "jsfb-CA0D7A", - "gatewayId": "673602f0d14760402fbd033b_jsfb-CA0D7A", - "deviceName": "jsfb-CA0D7A", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-89122A", - "nodeId": "jsfb-89122A", - "gatewayId": "673602f0d14760402fbd033b_jsfb-89122A", - "deviceName": "jsfb-89122A", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-31464E", - "nodeId": "jsfb-31464E", - "gatewayId": "673602f0d14760402fbd033b_jsfb-31464E", - "deviceName": "jsfb-31464E", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-2A1577", - "nodeId": "jsfb-2A1577", - "gatewayId": "673602f0d14760402fbd033b_jsfb-2A1577", - "deviceName": "jsfb-2A1577", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-1EC625", - "nodeId": "jsfb-1EC625", - "gatewayId": "673602f0d14760402fbd033b_jsfb-1EC625", - "deviceName": "jsfb-1EC625", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-36350C", - "nodeId": "jsfb-36350C", - "gatewayId": "673602f0d14760402fbd033b_jsfb-36350C", - "deviceName": "jsfb-36350C", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-F29258", - "nodeId": "jsfb-F29258", - "gatewayId": "673602f0d14760402fbd033b_jsfb-F29258", - "deviceName": "jsfb-F29258", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-8942CE", - "nodeId": "jsfb-8942CE", - "gatewayId": "673602f0d14760402fbd033b_jsfb-8942CE", - "deviceName": "jsfb-8942CE", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-B1398A", - "nodeId": "jsfb-B1398A", - "gatewayId": "673602f0d14760402fbd033b_jsfb-B1398A", - "deviceName": "jsfb-B1398A", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-465F0C", - "nodeId": "jsfb-465F0C", - "gatewayId": "673602f0d14760402fbd033b_jsfb-465F0C", - "deviceName": "jsfb-465F0C", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-E169C2", - "nodeId": "jsfb-E169C2", - "gatewayId": "673602f0d14760402fbd033b_jsfb-E169C2", - "deviceName": "jsfb-E169C2", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-E82E16", - "nodeId": "jsfb-E82E16", - "gatewayId": "673602f0d14760402fbd033b_jsfb-E82E16", - "deviceName": "jsfb-E82E16", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-6FA0C5", - "nodeId": "jsfb-6FA0C5", - "gatewayId": "673602f0d14760402fbd033b_jsfb-6FA0C5", - "deviceName": "jsfb-6FA0C5", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-FC04F2", - "nodeId": "jsfb-FC04F2", - "gatewayId": "673602f0d14760402fbd033b_jsfb-FC04F2", - "deviceName": "jsfb-FC04F2", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-C5D5F4", - "nodeId": "jsfb-C5D5F4", - "gatewayId": "673602f0d14760402fbd033b_jsfb-C5D5F4", - "deviceName": "jsfb-C5D5F4", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-B96E18", - "nodeId": "jsfb-B96E18", - "gatewayId": "673602f0d14760402fbd033b_jsfb-B96E18", - "deviceName": "jsfb-B96E18", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-37285C", - "nodeId": "jsfb-37285C", - "gatewayId": "673602f0d14760402fbd033b_jsfb-37285C", - "deviceName": "jsfb-37285C", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-00A874", - "nodeId": "jsfb-00A874", - "gatewayId": "673602f0d14760402fbd033b_jsfb-00A874", - "deviceName": "jsfb-00A874", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-7D2A0B", - "nodeId": "jsfb-7D2A0B", - "gatewayId": "673602f0d14760402fbd033b_jsfb-7D2A0B", - "deviceName": "jsfb-7D2A0B", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-69681C", - "nodeId": "jsfb-69681C", - "gatewayId": "673602f0d14760402fbd033b_jsfb-69681C", - "deviceName": "jsfb-69681C", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - }, - { - "appId": "9782371cee894c0abbecb30efe4e7cd0", - "appName": "DefaultApp_6736wwjm", - "deviceId": "673602f0d14760402fbd033b_jsfb-6D10F3", - "nodeId": "jsfb-6D10F3", - "gatewayId": "673602f0d14760402fbd033b_jsfb-6D10F3", - "deviceName": "jsfb-6D10F3", - "nodeType": "GATEWAY", - "productId": "673602f0d14760402fbd033b", - "productName": "RS-LL-X1", - "status": "INACTIVE", - "tags": [] - } - ], - "page": { - "count": 227, - "marker": "6881dce05ebba32371ca42f6" - }, - "httpStatusCode": 200 -} diff --git a/storm-device/src/test/java/com/storm/device/3.json b/storm-device/src/test/java/com/storm/device/3.json deleted file mode 100644 index 9661f54..0000000 --- a/storm-device/src/test/java/com/storm/device/3.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - online_time=20250829T113156Z, - latitude=112.541115, - software_version=ioT, - vtxdb_version=1.1.9-beta-ansun, - status=ONLINE, - longitude=28.280514 -} - -[ - class - DeviceShadowData - { - serviceId: ExpectedDelivery - desired: class - DeviceShadowProperties - { - properties: null - eventTime: 20250830T014623Z - } - reported - : - class - DeviceShadowProperties - { - properties: null - eventTime: null - } - version - : - 750 - }, - class - DeviceShadowData - { - serviceId: VortXDB - desired: class - DeviceShadowProperties - { - properties: null - eventTime: null - } - reported - : - class - DeviceShadowProperties - { - properties: { - online_time=20250829T113156Z, - latitude=112.541115, - software_version=ioT, - vtxdb_version=1.1.9-beta-ansun, - status=ONLINE, - longitude=28.280514 - } - eventTime: 20250829T113156Z - } - version - : - 37 - } -] \ No newline at end of file diff --git a/storm-device/src/test/java/com/storm/device/4.json b/storm-device/src/test/java/com/storm/device/4.json deleted file mode 100644 index f19b2f8..0000000 --- a/storm-device/src/test/java/com/storm/device/4.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "pageNo": 1, - "pageSize": 10, - "isAsc": true, - "sortBy": "id", - "deviceNameParam": "", - "statusParam": "", - "softwareVersionParam": "", - "vtxdbVersionParam": "", - "beginTime": "", - "endTime": "" -} \ No newline at end of file diff --git a/storm-device/src/test/java/com/storm/device/5.json b/storm-device/src/test/java/com/storm/device/5.json deleted file mode 100644 index 07bd6db..0000000 --- a/storm-device/src/test/java/com/storm/device/5.json +++ /dev/null @@ -1,3 +0,0 @@ -IotMsgNotifyData(header=IotMsgHeader(productId=673602f0d14760402fbd033b, deviceId=673602f0d14760402fbd033b_jsfb-DA, nodeId=jsfb-DA, appId=9782371cee894c0abbecb30efe4e7cd0, gatewayId=673602f0d14760402fbd033b_jsfb-DA), body=IotMsgBody(services=[IotMsgService(serviceId=VortXDB, properties={online_time=20250901T032819Z, latitude=112.541115, software_version=ioT, vtxdb_version=1.1.9-beta-ansun, status=ONLINE, longitude=28.280514}, eventTime=20250901T032819Z)])) - -IotMsgNotifyData(header=IotMsgHeader(productId=673602f0d14760402fbd033b, deviceId=673602f0d14760402fbd033b_jsfb-DA, nodeId=jsfb-DA, appId=9782371cee894c0abbecb30efe4e7cd0, gatewayId=673602f0d14760402fbd033b_jsfb-DA), body=IotMsgBody(services=[IotMsgService(serviceId=VortXDB, properties={online_time=20250901T041315Z, latitude=112.541115, software_version=ioT, vtxdb_version=1.1.9-beta-ansun, status=ONLINE, longitude=28.280514}, eventTime=20250901T041316Z)])) diff --git a/storm-device/src/test/java/com/storm/device/IotTest.java b/storm-device/src/test/java/com/storm/device/IotTest.java deleted file mode 100644 index 642650d..0000000 --- a/storm-device/src/test/java/com/storm/device/IotTest.java +++ /dev/null @@ -1,181 +0,0 @@ -package com.storm.device; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.date.DatePattern; -import cn.hutool.core.date.LocalDateTimeUtil; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import com.alibaba.nacos.common.utils.CollectionUtils; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.huaweicloud.sdk.iotda.v5.IoTDAClient; -import com.huaweicloud.sdk.iotda.v5.model.*; -import com.storm.common.core.exception.base.BaseException; -import com.storm.common.core.utils.StringUtils; -import com.storm.device.domain.po.Device; -import com.storm.device.service.IDeviceService; -import com.storm.device.service.impl.DeviceServiceImpl; -import com.storm.device.service.impl.MassageTaskServiceImpl; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -import javax.annotation.Resource; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * @author DengAo - * @date 2025/8/27 17:11 - */ -@Slf4j -@SpringBootTest -public class IotTest { - @Resource - private IoTDAClient client; - - @Resource - private DeviceServiceImpl deviceService; - - @Test - public void testSyncDevicesFromIoT() { - List deviceList = new ArrayList<>(); - String marker = null; - boolean hasMore = true; - - try { - while (hasMore) { - // 创建请求对象 - ListDevicesRequest request = new ListDevicesRequest(); - - // 设置分页参数 - if (marker != null) { - request.setMarker(marker); - } - request.setLimit(50); // 每页最大100条 - - // 发起请求获取设备列表 - ListDevicesResponse response = client.listDevices(request); - - if (response.getHttpStatusCode() != 200) { - throw new BaseException("物联网接口 - 查询设备列表失败,HTTP状态码: " + response.getHttpStatusCode()); - } - - log.info("50台设备信息:{}", JSONUtil.toJsonStr(response.getDevices())); - - // 处理返回的设备数据 - if (CollectionUtils.isNotEmpty(response.getDevices())) { - for (QueryDeviceSimplify queryDeviceSimplify : response.getDevices()) { - Device device = new Device(); - device.setDeviceId(queryDeviceSimplify.getDeviceId()); - device.setDeviceName(queryDeviceSimplify.getDeviceName()); - device.setProductId(queryDeviceSimplify.getProductId()); - device.setProductName(queryDeviceSimplify.getProductName()); - - // 可以设置其他默认值 - device.setDeviceType(1); // 默认设备类型 - device.setIsDeleted(0); // 未删除 - - deviceList.add(device); - } - } - - // 检查是否还有更多数据 - if (CollectionUtils.isNotEmpty(response.getDevices()) && StringUtils.isNotEmpty(response.getPage().getMarker())) { - marker = response.getPage().getMarker(); - } else { - hasMore = false; - } - // 休眠100毫秒 - Thread.sleep(100); - } - - log.info("成功同步 {} 台设备信息", deviceList.size()); - } catch (Exception e) { - log.error("同步设备列表失败: {}", e.getMessage(), e); - throw new BaseException("同步设备列表失败: " + e.getMessage()); - } - } - - @Test - public void testSyncDevicesFromIoT2() { - String marker = "675299a3f9024677dbd36a08"; - //请求参数 - ListDevicesRequest request = new ListDevicesRequest(); - - //设置条数 - request.setLimit(50); - - request.setMarker(marker); - - //发起请求 - ListDevicesResponse response = client.listDevices(request); - if (response.getHttpStatusCode() != 200) { - throw new BaseException("物联网接口 - 查询设备,同步失败"); - } - - log.info("50台设备信息:{}", JSONUtil.toJsonStr(response.getDevices())); - - } - - @Test - public void updateDeviceStatusLog() { - //通过client 调用查询设备影子数据 - ShowDeviceShadowRequest request = new ShowDeviceShadowRequest(); - request.setDeviceId("673602f0d14760402fbd033b_jsfb-66E903"); - ShowDeviceShadowResponse response = client.showDeviceShadow(request); - - //解析数据 - List shadow = response.getShadow(); - if (CollUtil.isEmpty(shadow)) { - log.error("设备影子数据为空"); - return; - } - JSONObject jsonObject = JSONUtil.parseObj(shadow.get(1).getReported().getProperties()); - - String eventTimeStr = shadow.get(1).getReported().getEventTime(); - LocalDateTime time = otherLocalDateTime(eventTimeStr); - - //打印返回的结果 - log.info("设备影子数据:{}", jsonObject); - System.out.println("--------------------------------------"); - log.info("设备影子数据时间:{}", time); - // 打印shadow数据 - System.out.println("--------------------------------------"); - log.info("设备影子数据:{}", shadow); - System.out.println("--------------------------------------"); - LocalDateTime onlineTime = otherLocalDateTime(jsonObject.get("online_time", String.class)); - log.info("设备影子online_time时间:{}", onlineTime); - - } - - /** - * 测试百度地图API获取地址 - */ - @Test - public void testGetLocation() { - String location = deviceService.getLocationFromCoordinates(22.624244620774185, 114.03790755307914); - log.info("设备地址:{}", location); - } - - public LocalDateTime toLocalDateTime(String activeTime) { - LocalDateTime time = LocalDateTimeUtil.parse(activeTime, DatePattern.UTC_MS_PATTERN); - time = time.atZone(ZoneId.from(ZoneOffset.UTC)) - .withZoneSameInstant(ZoneId.of("Asia/Shanghai")) - .toLocalDateTime(); - return time; - } - - public LocalDateTime otherLocalDateTime(String activeTime) { - LocalDateTime time = LocalDateTimeUtil.parse(activeTime, "yyyyMMdd'T'HHmmss'Z'"); - time = time.atZone(ZoneId.from(ZoneOffset.UTC)) - .withZoneSameInstant(ZoneId.of("Asia/Shanghai")) - .toLocalDateTime(); - return time; - } - -}