405 changed files with 4619 additions and 20422 deletions
@ -1,403 +0,0 @@ |
|||
#!/usr/bin/env python |
|||
# -*-coding:utf-8 -*- |
|||
from selenium import webdriver |
|||
from selenium.webdriver.common.by import By |
|||
from selenium.webdriver.support.ui import WebDriverWait |
|||
from selenium.webdriver.support import expected_conditions as EC |
|||
from selenium.webdriver.common.action_chains import ActionChains |
|||
from selenium.webdriver.chrome.options import Options |
|||
import time |
|||
import sys |
|||
import getopt |
|||
import json |
|||
import os |
|||
import shutil |
|||
import platform |
|||
import re |
|||
# import traceback |
|||
import base64 |
|||
import io |
|||
|
|||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') |
|||
|
|||
# 默认文件下载目录 改成自己服务器的 |
|||
DEFAULT_BASE_DOWNLOAD_PATH = os.path.dirname(os.path.realpath('__file__')) + os.sep + "downloads" + os.sep |
|||
# update-begin---author:chenrui ---date:20240130 for:[QQYUN-8127]通过接口导出功能要获取token和查询参数------------ |
|||
# 临时下载目录 |
|||
TEMP_DOWNLOAD_PATH = os.sep + "temp" + os.sep |
|||
# update-end---author:chenrui ---date:20240130 for:[QQYUN-8127]通过接口导出功能要获取token和查询参数------------ |
|||
|
|||
# 默认积木报表访问地址 改成自己的 |
|||
DEFAULT_BASE_SHARE_VIEW_URL = "http://localhost:8085/jmreport/view/" |
|||
|
|||
# 结束日志标识 |
|||
LOG_END_MARK = "$JM$END$" |
|||
|
|||
|
|||
def append_common_params(payload: str, token: str) -> str: |
|||
""" |
|||
拼接报表的通用参数 |
|||
:param payload: |
|||
:param token: |
|||
:return: |
|||
""" |
|||
if payload is None or len(payload) == 0: |
|||
return "" |
|||
if token is not None and len(token) > 0: |
|||
token = "token=" + token |
|||
if "?" in payload: |
|||
payload += "&" + token |
|||
else: |
|||
payload += "?" + token |
|||
if "?" in payload: |
|||
payload += "&directDld=1" |
|||
else: |
|||
payload += "?directDld=1" |
|||
return payload |
|||
|
|||
|
|||
def dict_2_str_payload(payload: dict) -> str: |
|||
""" |
|||
字典数据转换为查询payload |
|||
:param payload: |
|||
:return: |
|||
""" |
|||
if payload is not None and len(payload) > 0: |
|||
return str("&".join([key + "=" + val for key, val in payload.items() if key is not None and len(key) > 0])) |
|||
else: |
|||
return "" |
|||
|
|||
|
|||
def init_args(argv=None): |
|||
"""初始化参数""" |
|||
global opts |
|||
print('共有:', len(argv), '个参数。') |
|||
if len(argv) <= 0: |
|||
raise Exception('参数异常') |
|||
arg = argv[0] |
|||
if arg is not None and len(arg) > 0: |
|||
opts = json.loads(base64.b64decode(arg)) |
|||
# 批次号 |
|||
batch_no = None |
|||
# 导出类型 |
|||
export_type = 'PDF' |
|||
# 报表ids |
|||
report_ids = None |
|||
# 报表参数,与report_ids二选一 |
|||
report_params = list() |
|||
# 积木报表预览页面地址 |
|||
base_share_view_url = DEFAULT_BASE_SHARE_VIEW_URL |
|||
# 报表下载基础目录 |
|||
base_download_path = DEFAULT_BASE_DOWNLOAD_PATH |
|||
# token |
|||
token = '' |
|||
|
|||
if 'batch_no' in opts: |
|||
batch_no = opts['batch_no'] |
|||
if 'export_type' in opts: |
|||
export_type = opts['export_type'] |
|||
if 'report_ids' in opts: |
|||
report_ids = opts['report_ids'] |
|||
if 'report_params' in opts: |
|||
report_params = opts['report_params'] |
|||
if 'jimu_view_url' in opts: |
|||
base_share_view_url = opts['jimu_view_url'] |
|||
if 'base_download_path' in opts: |
|||
base_download_path = opts['base_download_path'] |
|||
if 'token' in opts: |
|||
token = opts['token'] |
|||
|
|||
if export_type.upper() == "EXCEL": |
|||
export_type = "Excel" |
|||
elif export_type.upper() == "PDF": |
|||
export_type = "PDF" |
|||
else: |
|||
export_type = "PDF" |
|||
|
|||
# 拼接报表查询参数 |
|||
reports: list[dict] = list() |
|||
if report_params is not None and len(report_params) > 0: |
|||
for report_param in report_params: |
|||
report_query = report_param['id'] |
|||
if 'params' in report_param: |
|||
params = report_param['params'] |
|||
if params is not None and len(params) > 0: |
|||
payload = dict_2_str_payload(params) |
|||
if payload is not None and len(payload) > 0: |
|||
report_query += "?" + payload |
|||
custom_export_type = export_type |
|||
if 'export_type' in report_param: |
|||
custom_export_type = report_param['export_type'] |
|||
custom_export_type = export_type if custom_export_type is None else custom_export_type |
|||
reports.append({"url": append_common_params(report_query, token), "export_type": custom_export_type}) |
|||
elif report_ids is not None and len(report_ids) > 0: |
|||
reports = [{"url": append_common_params(report_id, token), "export_type": export_type} for report_id in |
|||
report_ids] |
|||
|
|||
# 确保传入路径正确,统一修改所有的/ 和 \\为当前系统的盘符 |
|||
base_download_path = base_download_path.replace("/", os.sep).replace("\\", os.sep) |
|||
if not os.path.isabs(base_download_path): |
|||
raise Exception("导出失败,下载目录必须是绝对路径") |
|||
if "windows" in platform.platform().lower() and base_download_path.startswith("\\"): |
|||
# windows 系统下 并且没有写盘符时,拼接盘符 |
|||
run_path = os.path.dirname(os.path.realpath('__file__')) |
|||
base_download_path = os.path.splitdrive(run_path)[0] + base_download_path |
|||
|
|||
options = { |
|||
'batch_no': batch_no, |
|||
'export_type': export_type, |
|||
'reports': reports, |
|||
"base_share_view_url": base_share_view_url, |
|||
"base_download_path": base_download_path |
|||
} |
|||
print("运行参数:" + json.dumps(options)) |
|||
return options |
|||
|
|||
|
|||
def auto_export(options): |
|||
print(" >>> java进入Python 脚本方法,options = ", options) |
|||
""" |
|||
自动导出函数 |
|||
:param options: {batch_no:批次号,export_type:导出类型,reports:[{url:报表url,export_type:导出类型}]} |
|||
""" |
|||
# 整理参数 |
|||
batch_no = options['batch_no'] |
|||
|
|||
export_type = options['export_type'] |
|||
|
|||
reports = None |
|||
# 优先使用report |
|||
if "reports" in options: |
|||
reports = options['reports'] |
|||
|
|||
if not reports or reports is None: |
|||
if "report_ids" in options: |
|||
report_ids = options['report_ids'] |
|||
if not report_ids: |
|||
raise Exception('报表id不能为空') |
|||
else: |
|||
# reports 为空,将report_ids转换为reports |
|||
reports = [{"url": report_id, "export_type": export_type} for report_id in report_ids] |
|||
else: |
|||
raise Exception('报表id不能为空') |
|||
|
|||
if not batch_no or not reports: |
|||
raise Exception('批次编号或报表id不能为空') |
|||
|
|||
# 下载目录 |
|||
download_path = options["base_download_path"] + batch_no |
|||
|
|||
# 获取域名 |
|||
base_share_view_url = options["base_share_view_url"] |
|||
match = re.match(r'(http[s]?://[^/]+)', base_share_view_url) |
|||
if match: |
|||
base_url = match.group(1) |
|||
else: |
|||
base_url = "" |
|||
|
|||
# 获取webDriver |
|||
driver = build_web_driver(download_path, base_url) |
|||
|
|||
# 确保目录存在 |
|||
if os.path.exists(download_path) is False: |
|||
os.makedirs(download_path) |
|||
# update-begin---author:chenrui ---date:20240129 for:[QQYUN-8127]通过接口导出功能要获取token和查询参数------------ |
|||
else: |
|||
# 清空文件夹并重建 |
|||
shutil.rmtree(download_path) |
|||
os.makedirs(download_path) |
|||
|
|||
# 确保临时目录存在 |
|||
if os.path.exists(download_path + TEMP_DOWNLOAD_PATH) is False: |
|||
os.makedirs(download_path + TEMP_DOWNLOAD_PATH) |
|||
# update-end---author:chenrui ---date:20240129 for:[QQYUN-8127]通过接口导出功能要获取token和查询参数------------ |
|||
|
|||
downloaded_count = 0 |
|||
download_failure_rids = [] |
|||
|
|||
# 导出失败的报表标题 |
|||
failure_report_title = "" |
|||
# 开始自动导出 |
|||
for report in reports: |
|||
report_url = report['url'] |
|||
custom_export_type = report['export_type'] |
|||
if not custom_export_type: |
|||
custom_export_type = export_type |
|||
else: |
|||
if custom_export_type.upper() == "EXCEL": |
|||
custom_export_type = "Excel" |
|||
elif custom_export_type.upper() == "PDF": |
|||
custom_export_type = "PDF" |
|||
else: |
|||
custom_export_type = "PDF" |
|||
print("开始导出报表:" + report_url) |
|||
# 打开url网页 |
|||
driver.get(options["base_share_view_url"] + report_url) |
|||
# 等待数据查询完成 |
|||
export_el = WebDriverWait(driver, 10, 0.2).until_not( |
|||
EC.presence_of_element_located((By.CLASS_NAME, "ivu-spin-fullscreen")) |
|||
) |
|||
# 等待导出按钮加载完成 |
|||
export_el = WebDriverWait(driver, 10, 0.2).until( |
|||
EC.presence_of_element_located((By.CLASS_NAME, "export")) |
|||
) |
|||
# 获取title |
|||
title = driver.title |
|||
if len(title) > 0 and "-" in title: |
|||
title = title[:title.rindex("-")].strip() |
|||
print("报表名称:" + title) |
|||
if not download_check(download_path, title, custom_export_type.lower(), 1): |
|||
# 报表不存在,开始下载 |
|||
print("报表{}未下载,开始下载...".format(title)) |
|||
# 等待0.5秒,防止页面未完成渲染 |
|||
time.sleep(0.5) |
|||
# 鼠标移到导出按钮 |
|||
ActionChains(driver).move_to_element(export_el).perform() |
|||
# 点击导出pdf按钮 |
|||
driver.find_element(By.ID, custom_export_type).click() |
|||
# 检查是否下载完成 |
|||
if not download_check(download_path, title, custom_export_type.lower(), 30, 1): |
|||
print("报表{}下载失败".format(title)) |
|||
failure_report_title += title + " " |
|||
download_failure_rids.append(report_url) |
|||
else: |
|||
downloaded_count = downloaded_count + 1 |
|||
print("报表:" + report_url + "导出完成") |
|||
else: |
|||
# 报表存在 |
|||
downloaded_count = downloaded_count + 1 |
|||
# update-begin---author:chenrui ---date:20240129 for:统一py脚本的返回结果格式------------ |
|||
err_msg = "" |
|||
if len(reports) != downloaded_count: |
|||
err_msg = "报表[" + failure_report_title + "]导出失败" |
|||
result = { |
|||
"success": len(reports) == downloaded_count, |
|||
"message": err_msg, |
|||
"result": { |
|||
"report_count": len(reports), |
|||
"downloaded_count": downloaded_count, |
|||
"failure_rids": download_failure_rids, |
|||
"download_path": download_path, |
|||
**options |
|||
} |
|||
} |
|||
# update-end---author:chenrui ---date:20240129 for:统一py脚本的返回结果格式------------ |
|||
|
|||
# 退出浏览器 |
|||
driver.quit() |
|||
return result |
|||
|
|||
|
|||
def build_web_driver(download_path, safe_domain=""): |
|||
""" |
|||
构建webDriver |
|||
:param safe_domain: 安全域名 |
|||
:param download_path: 下载目录 |
|||
:return: webDriver |
|||
""" |
|||
chrome_options = Options() |
|||
# 不使用沙箱 |
|||
chrome_options.add_argument('--no-sandbox') |
|||
# 将浏览器静音 |
|||
chrome_options.add_argument("--mute-audio") |
|||
# 当程序结束时,浏览器不会关闭 |
|||
# chrome_options.add_experimental_option("detach", True) |
|||
# 开启无界面浏览器(minos必须开启无界面) |
|||
chrome_options.add_argument("--headless") |
|||
# 禁用gpu |
|||
chrome_options.add_argument("--disable-gpu") |
|||
# 添加安全域名 |
|||
if safe_domain is not None and bool(safe_domain): |
|||
chrome_options.add_argument("--unsafely-treat-insecure-origin-as-secure=" + safe_domain) |
|||
if 'linux' in platform.platform().lower(): |
|||
# fix:DevToolsActivePort file doesn't |
|||
chrome_options.add_argument('--disable-dev-shm-usage') |
|||
chrome_options.add_argument('--remote-debugging-port=9222') |
|||
# 设置下载路径 |
|||
prefs = {'profile.default_content_settings.popups': 0, |
|||
'download.prompt_for_download': False, |
|||
'safebrowsing.disable_download_protection': True, |
|||
'download.default_directory': download_path + TEMP_DOWNLOAD_PATH} |
|||
chrome_options.add_experimental_option('prefs', prefs) |
|||
# 忽略不安全的错误 |
|||
chrome_options.add_argument('ignore-certificate-errors') |
|||
# Chrome浏览器 |
|||
driver = webdriver.Chrome(options=chrome_options) |
|||
return driver |
|||
|
|||
|
|||
def download_check(check_path, check_file_name, check_ext, check_times=3, check_interval=5): |
|||
""" |
|||
检测函数 |
|||
:param check_path:检测路径 |
|||
:param check_file_name: 检查文件名称 |
|||
:param check_ext:检测扩展名 |
|||
:param check_times:检测次数(默认值:3) |
|||
:param check_interval:检测时间间隔(默认值:5) |
|||
:return:返回真假 |
|||
""" |
|||
temp_check_path = check_path + TEMP_DOWNLOAD_PATH |
|||
if os.path.exists(temp_check_path) is False: |
|||
return False |
|||
else: |
|||
for number in range(0, int(check_times)): |
|||
print("验证文件{}是否存在;第{}次检测.".format(check_file_name, str(number + 1))) |
|||
# time.sleep(0.2) |
|||
# 读取目录下所有文件 |
|||
# update-begin---author:chenrui ---date:20240129 for:[QQYUN-8127]通过接口导出功能要获取token和查询参数------------ |
|||
files = os.listdir(temp_check_path) |
|||
file_number = len(files) |
|||
if file_number > 0: |
|||
# 存在多个文件,检查当前文件是否存在 |
|||
for file in files: |
|||
if str(check_file_name.strip()) in str(file): |
|||
# 文件存在 |
|||
dest_move_file_path = check_path |
|||
if os.path.exists(check_path + os.sep + str(file)): |
|||
print("文件{}存在;重命名该文件.".format(check_file_name, str(number + 1))) |
|||
filename, extension = os.path.splitext(file) |
|||
dest_move_file_path += (os.sep + filename |
|||
+ "({})".format(str(int(time.time()))) |
|||
+ extension) |
|||
shutil.move(temp_check_path + str(file), dest_move_file_path) |
|||
# update-end---author:chenrui ---date:20240129 for:[QQYUN-8127]通过接口导出功能要获取token和查询参数------------ |
|||
return True |
|||
# 文件不存在 |
|||
if check_times != 1 or number < check_times - 1: |
|||
time.sleep(int(check_interval)) # 休眠一会 |
|||
return False |
|||
|
|||
|
|||
if __name__ == '__main__': |
|||
""" |
|||
入口 |
|||
""" |
|||
result = {} |
|||
try: |
|||
print(" >>> java进入Python ==> step.1 进入Main方法") |
|||
args = sys.argv[1:] |
|||
# #本地调试参数 |
|||
# args = ['-b', '1713260060264cubcWF', '-r', |
|||
# '537516331017523200?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MTM1NTgxNDQsInVzZXJuYW1lIjoiMTg2MTE3ODg1MjUifQ.JXZLzqgkuZRcbEYFn0l-L0DXZzOw3hJZIzn0y2EmzQM', |
|||
# '-t', 'PDF', '-s', 'http://localhost:8087/jmreport/view/', '-d', 'E:\\opt\\jmpydownload\\'] |
|||
print(" >>> java进入Python ==> step.2 初始化参数") |
|||
export_options = init_args(args) |
|||
print(" >>> java进入Python ==> step.3 开始执行py脚本") |
|||
result = auto_export(export_options) |
|||
print(" >>> java进入Python ==> step.4 返回执行结果") |
|||
except Exception as e: |
|||
print("异常日志:", e) |
|||
# traceback.print_exc() |
|||
msg = "" |
|||
if hasattr(e, "msg"): |
|||
msg = e.msg |
|||
else: |
|||
msg = str(e) |
|||
# update-begin---author:chenrui ---date:20240129 for:统一py脚本的返回结果格式------------ |
|||
result = { |
|||
"success": False, |
|||
"message": msg, |
|||
"result": None |
|||
} |
|||
# update-end---author:chenrui ---date:20240129 for:统一py脚本的返回结果格式------------ |
|||
print(LOG_END_MARK + json.dumps(result) + LOG_END_MARK) |
@ -0,0 +1,63 @@ |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<parent> |
|||
<groupId>cn.iocoder.boot</groupId> |
|||
<artifactId>cc-admin-master</artifactId> |
|||
<version>${revision}</version> <!-- 1. 修改 version 为 ${revision} --> |
|||
</parent> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
<artifactId>yudao-module-hand-mqtt</artifactId> |
|||
<packaging>jar</packaging> <!-- 2. 新增 packaging 为 jar --> |
|||
|
|||
<name>${project.artifactId}</name> |
|||
|
|||
|
|||
<dependencies> <!-- 5. 新增依赖,这里引入的都是比较常用的业务组件、技术组件 --> |
|||
<!-- Web 相关 --> |
|||
<dependency> |
|||
<groupId>cn.iocoder.boot</groupId> |
|||
<artifactId>yudao-spring-boot-starter-web</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>cn.iocoder.boot</groupId> |
|||
<artifactId>yudao-spring-boot-starter-security</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.eclipse.paho</groupId> |
|||
<artifactId>org.eclipse.paho.client.mqttv3</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.springframework.kafka</groupId> |
|||
<artifactId>spring-kafka</artifactId> |
|||
</dependency> |
|||
<!-- <dependency> |
|||
<groupId>org.apache.rocketmq</groupId> |
|||
<artifactId>rocketmq-spring-boot-starter</artifactId> |
|||
</dependency>--> |
|||
<dependency> |
|||
<groupId>org.apache.rocketmq</groupId> |
|||
<artifactId>rocketmq-acl</artifactId> |
|||
<version>4.9.4</version> <!-- 注意:这里的版本号建议和你的 rocketmq-client 版本保持一致 --> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.apache.rocketmq</groupId> |
|||
<artifactId>rocketmq-client</artifactId> |
|||
<version>4.9.4</version> <!-- 请根据需要选择合适的版本 --> |
|||
</dependency> |
|||
<!-- DB 相关 --> |
|||
<dependency> |
|||
<groupId>cn.iocoder.boot</groupId> |
|||
<artifactId>yudao-spring-boot-starter-mybatis</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>cn.iocoder.boot</groupId> |
|||
<artifactId>yudao-module-hand</artifactId> |
|||
<version>2.6.1-SNAPSHOT</version> |
|||
<scope>compile</scope> |
|||
</dependency> |
|||
|
|||
</dependencies> |
|||
|
|||
</project> |
@ -0,0 +1,52 @@ |
|||
package cn.iocoder.yudao.module.mqtt.config; |
|||
|
|||
import cn.iocoder.yudao.module.hand.service.TdengineService; |
|||
import cn.iocoder.yudao.module.hand.vo.TdengineDataVo; |
|||
import cn.iocoder.yudao.module.hand.vo.HandOriginalLog; |
|||
import jakarta.annotation.Resource; |
|||
import org.springframework.context.annotation.Bean; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.scheduling.annotation.EnableScheduling; |
|||
import org.springframework.scheduling.annotation.Scheduled; |
|||
|
|||
@Configuration |
|||
@EnableScheduling // 确保定时任务被启用
|
|||
public class BatchProcessorConfig { |
|||
|
|||
@Resource |
|||
private TdengineService tdengineService; |
|||
|
|||
// --- 为 handOriginalLog 创建一个专用的批量处理器 Bean ---
|
|||
@Bean |
|||
public TdengineBatchConfig<HandOriginalLog> handLogBatchProcessor() { |
|||
return new TdengineBatchConfig<>( |
|||
"HandLogProcessor", // 处理器名字
|
|||
list -> tdengineService.saveHandLogBatch(list), // 【核心】将具体的保存逻辑作为 Lambda 传入
|
|||
50000, // 队列容量
|
|||
1000, // 批次大小
|
|||
5000 // 执行频率
|
|||
); |
|||
} |
|||
|
|||
@Bean |
|||
public TdengineBatchConfig<TdengineDataVo> handLogBatchDataProcessor() { |
|||
return new TdengineBatchConfig<>( |
|||
"handLogBatchDataProcessor", // 处理器名字
|
|||
list -> tdengineService.saveDataLogBatch(list), // 【核心】将具体的保存逻辑作为 Lambda 传入
|
|||
50000, // 队列容量
|
|||
1000, // 批次大小
|
|||
5000 // 执行频率
|
|||
); |
|||
} |
|||
|
|||
|
|||
// --- 创建一个调度器来统一调用所有处理器的 flush 方法 ---
|
|||
@Scheduled(fixedRateString = "${batch.processor.flush.rate:5000}") // 从配置读取频率,默认5秒
|
|||
public void scheduleFlush() { |
|||
// 调用 handLogProcessor 的刷新方法
|
|||
handLogBatchProcessor().flush(); |
|||
handLogBatchDataProcessor().flush(); |
|||
|
|||
// 如果有其他处理器,也在这里调用
|
|||
} |
|||
} |
@ -0,0 +1,89 @@ |
|||
package cn.iocoder.yudao.module.mqtt.config; |
|||
|
|||
import jakarta.annotation.PreDestroy; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.concurrent.BlockingQueue; |
|||
import java.util.concurrent.LinkedBlockingQueue; |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.function.Consumer; |
|||
|
|||
@Slf4j |
|||
public class TdengineBatchConfig<T> { |
|||
|
|||
// --- 这些参数可以在构造时传入,使其更灵活 ---
|
|||
private final int queueCapacity; |
|||
private final int batchSize; |
|||
private final long fixedRateMs; |
|||
private final long offerTimeoutMs; |
|||
|
|||
private final String processorName; // 用于日志,区分不同的处理器实例
|
|||
private final BlockingQueue<T> dataQueue; |
|||
private final Consumer<List<T>> batchAction; // 【核心】用于处理一个批次的具体业务逻辑
|
|||
|
|||
/** |
|||
* 构造函数 |
|||
* @param processorName 处理器名称,用于日志区分 |
|||
* @param batchAction 一个函数,接收一个 List<T> 并执行相应的批量操作(例如,写入数据库) |
|||
* @param queueCapacity 队列容量 |
|||
* @param batchSize 批次大小 |
|||
* @param fixedRateMs 执行频率 |
|||
*/ |
|||
public TdengineBatchConfig(String processorName, Consumer<List<T>> batchAction, |
|||
int queueCapacity, int batchSize, long fixedRateMs) { |
|||
this.processorName = processorName; |
|||
this.batchAction = batchAction; |
|||
this.queueCapacity = queueCapacity; |
|||
this.batchSize = batchSize; |
|||
this.fixedRateMs = fixedRateMs; |
|||
this.offerTimeoutMs = 100L; |
|||
this.dataQueue = new LinkedBlockingQueue<>(this.queueCapacity); |
|||
} |
|||
|
|||
public void addToBatch(T data) { |
|||
if (data == null) { |
|||
return; |
|||
} |
|||
try { |
|||
if (!dataQueue.offer(data, offerTimeoutMs, TimeUnit.MILLISECONDS)) { |
|||
log.warn("[{}] 缓冲区已满且在 {} 毫秒内无法添加,数据可能被丢弃!当前队列大小: {}", |
|||
this.processorName, this.offerTimeoutMs, dataQueue.size()); |
|||
} |
|||
} catch (InterruptedException e) { |
|||
log.error("[{}] 添加数据到缓冲区时线程被中断", this.processorName, e); |
|||
Thread.currentThread().interrupt(); |
|||
} |
|||
} |
|||
|
|||
// 注意:@Scheduled 注解不能用在非 Spring Bean 的普通类方法上。
|
|||
// 我们将在下一步的配置类中解决这个问题。
|
|||
public void flush() { |
|||
if (dataQueue.isEmpty()) { |
|||
return; |
|||
} |
|||
List<T> batchList = new ArrayList<>(batchSize); |
|||
try { |
|||
int drainedCount = dataQueue.drainTo(batchList, batchSize); |
|||
if (drainedCount > 0) { |
|||
log.debug("[{}] 定时任务触发,准备将 {} 条数据进行批量处理...", this.processorName, drainedCount); |
|||
// 调用构造时传入的业务逻辑
|
|||
this.batchAction.accept(batchList); |
|||
log.debug("[{}] 成功批量处理 {} 条数据。", this.processorName, drainedCount); |
|||
} |
|||
} catch (Exception e) { |
|||
log.error("[{}] 批量处理数据时发生严重错误!数据量: {}", this.processorName, batchList.size(), e); |
|||
} |
|||
} |
|||
|
|||
@PreDestroy |
|||
public void onShutdown() { |
|||
log.info("[{}] 应用即将关闭,开始执行最后的缓冲区数据刷新...", this.processorName); |
|||
while (!dataQueue.isEmpty()) { |
|||
log.debug("[{}] 停机处理中,剩余 {} 条数据待处理...", this.processorName, dataQueue.size()); |
|||
flush(); |
|||
} |
|||
log.debug("[{}] 缓冲区数据已全部处理完毕。", this.processorName); |
|||
} |
|||
} |
@ -0,0 +1,34 @@ |
|||
package cn.iocoder.yudao.module.mqtt.kafka; |
|||
|
|||
import org.apache.kafka.common.TopicPartition; |
|||
import org.springframework.context.annotation.Bean; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.kafka.core.KafkaOperations; |
|||
import org.springframework.kafka.listener.DeadLetterPublishingRecoverer; |
|||
import org.springframework.kafka.listener.DefaultErrorHandler; |
|||
import org.springframework.util.backoff.FixedBackOff; |
|||
|
|||
@Configuration |
|||
public class KafkaConfig { |
|||
|
|||
/** |
|||
* 配置一个通用的错误处理器. |
|||
* 它会在反序列化失败或监听器方法抛出异常时生效. |
|||
* @param operations KafkaTemplate 实例 |
|||
* @return a DefaultErrorHandler |
|||
*/ |
|||
@Bean |
|||
public DefaultErrorHandler errorHandler(KafkaOperations<Object, Object> operations) { |
|||
// 1. 创建 DeadLetterPublishingRecoverer,它负责将失败的消息推送到 DLT
|
|||
// 这里我们为所有失败的消息都指定了一个通用的 DLT 主题
|
|||
DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(operations, |
|||
(rec, ex) -> new TopicPartition(rec.topic() + ".DLT", rec.partition())); |
|||
|
|||
// 根据业务需求选择合适的策略。
|
|||
FixedBackOff backOff = new FixedBackOff(1000L, 2L); |
|||
|
|||
// 3. 创建并返回 DefaultErrorHandler
|
|||
// 当重试耗尽后,会调用 recoverer 将消息送入 DLT
|
|||
return new DefaultErrorHandler(recoverer, backOff); |
|||
} |
|||
} |
@ -0,0 +1,58 @@ |
|||
package cn.iocoder.yudao.module.mqtt.kafka; |
|||
|
|||
import cn.iocoder.yudao.module.mqtt.processor.DeviceMessageProcessor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Qualifier; |
|||
import org.springframework.core.task.TaskExecutor; |
|||
import org.springframework.kafka.annotation.KafkaListener; |
|||
import org.springframework.kafka.core.KafkaTemplate; |
|||
import org.springframework.kafka.support.KafkaHeaders; |
|||
import org.springframework.messaging.handler.annotation.Header; |
|||
import org.springframework.messaging.handler.annotation.Payload; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import static cn.iocoder.yudao.module.mqtt.kafka.KafkaTopicType.DEAD_LETTER_TOPIC; |
|||
|
|||
@Slf4j |
|||
@Service // <--- 添加这个注解!
|
|||
public class KafkaMessageConsumer { |
|||
|
|||
private final TaskExecutor taskExecutor; |
|||
private final DeviceMessageProcessor deviceMessageProcessor; |
|||
private final KafkaTemplate<String, String> kafkaTemplate; |
|||
|
|||
|
|||
public KafkaMessageConsumer(DeviceMessageProcessor deviceMessageProcessor, |
|||
@Qualifier("mqttExecutor") TaskExecutor taskExecutor, |
|||
KafkaTemplate<String, String> kafkaTemplate) { |
|||
this.deviceMessageProcessor = deviceMessageProcessor; |
|||
this.taskExecutor = taskExecutor; |
|||
this.kafkaTemplate = kafkaTemplate; |
|||
|
|||
} |
|||
|
|||
@KafkaListener(topics = "zds_up") |
|||
public void handleMessage(@Payload String payload, |
|||
@Header(KafkaHeaders.RECEIVED_KEY) String key, |
|||
@Header(KafkaHeaders.RECEIVED_TOPIC) String topic) { |
|||
log.debug("从 Kafka Topic 收到消息,准备处理: topic=[{}]", topic); |
|||
|
|||
// 3. 异步执行,保证kafka不阻塞
|
|||
taskExecutor.execute(() -> { |
|||
try { |
|||
// 核心业务逻辑调用
|
|||
this.deviceMessageProcessor.process(key, payload); |
|||
} catch (Exception e) { |
|||
// 1. 捕获所有运行时异常
|
|||
log.error("处理 Kafka 消息时发生严重错误!将消息发送到死信队列. Key: {}, Payload: {}", key, payload, e); |
|||
try { |
|||
// 2. 将失败的消息发送到 DLT
|
|||
// 可以在消息头中添加额外信息,如原始主题、异常信息等,方便排查
|
|||
kafkaTemplate.send(DEAD_LETTER_TOPIC.getValue(), key, payload); |
|||
} catch (Exception dltEx) { |
|||
log.error("无法将消息发送到死信队列!消息可能丢失. Key: {}, Payload: {}", key, payload, dltEx); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
} |
@ -0,0 +1,44 @@ |
|||
package cn.iocoder.yudao.module.mqtt.kafka; |
|||
|
|||
import lombok.Getter; |
|||
import java.util.Arrays; |
|||
import java.util.Map; |
|||
import java.util.function.Function; |
|||
import java.util.stream.Collectors; |
|||
|
|||
/** |
|||
* Kafka 主题枚举类,用于统一管理和引用 Topic 名称 |
|||
*/ |
|||
@Getter // Lombok 注解,自动为所有字段生成 public 的 getter 方法,例如 getValue()
|
|||
public enum KafkaTopicType { |
|||
|
|||
// 1. 常量名和值的含义保持一致,更清晰
|
|||
DEVICE_STATUS_UP("zds_up"), |
|||
DEAD_LETTER_TOPIC("zds_up_dlt"); |
|||
|
|||
// 2. 添加了 final 字段
|
|||
private final String value; |
|||
|
|||
// 3. 【核心修复】添加了私有构造函数,用于初始化 final 字段
|
|||
KafkaTopicType(String value) { |
|||
this.value = value; |
|||
} |
|||
|
|||
// --- 以下是更高效和健壮的 from 方法实现 ---
|
|||
|
|||
// 4. 使用静态 Map 缓存,提高查找性能,避免每次调用都遍历
|
|||
private static final Map<String, KafkaTopicType> LOOKUP_MAP = Arrays.stream(values()) |
|||
.collect(Collectors.toMap(KafkaTopicType::getValue, Function.identity())); |
|||
|
|||
/** |
|||
* 根据字符串值查找对应的枚举常量. |
|||
* |
|||
* @param value topic 字符串值 |
|||
* @return 对应的枚举常量 |
|||
* @throws IllegalArgumentException 如果找不到匹配的常量 |
|||
*/ |
|||
public static KafkaTopicType from(String value) { |
|||
|
|||
return LOOKUP_MAP.get(value); |
|||
} |
|||
} |
@ -0,0 +1,150 @@ |
|||
package cn.iocoder.yudao.module.mqtt.mqtt; |
|||
|
|||
import jakarta.annotation.PostConstruct; |
|||
import jakarta.annotation.PreDestroy; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.eclipse.paho.client.mqttv3.*; |
|||
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.context.event.EventListener; |
|||
import org.springframework.scheduling.annotation.Async; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.nio.charset.StandardCharsets; |
|||
import java.security.InvalidKeyException; |
|||
import java.security.NoSuchAlgorithmException; |
|||
import java.util.UUID; |
|||
import java.util.concurrent.BlockingQueue; |
|||
|
|||
@Slf4j |
|||
@Component |
|||
public class Client { |
|||
|
|||
private final OnMessageCallback onMessageCallback; // 1. 注入我们新的 Kafka 生产者回调
|
|||
// --- 1. 配置注入 (保持不变) ---
|
|||
@Value("${mqtt.enable:false}") |
|||
private Boolean enable; |
|||
@Value("${mqtt.url}") |
|||
private String hostUrl; |
|||
@Value("${mqtt.username:}") |
|||
private String username; |
|||
@Value("${mqtt.password:}") |
|||
private String password; |
|||
@Value("${mqtt.client.id}") |
|||
private String baseClientId; |
|||
@Value("${mqtt.connectionTimeout:10}") |
|||
private int connectionTimeout; |
|||
@Value("${mqtt.keepAliveInterval:60}") |
|||
private int keepAliveInterval; |
|||
@Value("${mqtt.cleanSession:true}") |
|||
private boolean cleanSession; |
|||
@Value("${mqtt.subscribe.topic}") |
|||
private String[] subscribeTopics; |
|||
@Value("${mqtt.subscribe.qos}") |
|||
private int[] subscribeQos; |
|||
@Value("${mqtt.default.publishQos:1}") |
|||
private int publishQos; |
|||
private IMqttClient mqttClient; |
|||
|
|||
// 2. 构造函数注入 Spring Bean
|
|||
public Client(OnMessageCallback onMessageCallback) { |
|||
this.onMessageCallback = onMessageCallback; |
|||
} |
|||
|
|||
@PostConstruct |
|||
public void init() { |
|||
if (!enable) { |
|||
log.info("MQTT 功能已禁用。"); |
|||
return; |
|||
} |
|||
connect(); |
|||
} |
|||
|
|||
private void connect() { |
|||
try { |
|||
String finalClientId = baseClientId + "-" + UUID.randomUUID().toString().substring(0, 8); |
|||
mqttClient = new MqttClient(hostUrl, finalClientId, new MemoryPersistence()); |
|||
|
|||
MqttConnectOptions options = createMqttConnectOptions(); |
|||
|
|||
// 3. 将注入的回调 Bean 设置给 Paho 客户端
|
|||
mqttClient.setCallback(onMessageCallback); |
|||
log.info("正在连接到 MQTT Broker: {}", hostUrl); |
|||
mqttClient.connect(options); |
|||
|
|||
subscribeToTopics(); |
|||
|
|||
} catch (MqttException e) { |
|||
log.error("连接到 MQTT Broker 时发生初始错误: {}", e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
private MqttConnectOptions createMqttConnectOptions() { |
|||
MqttConnectOptions options = new MqttConnectOptions(); |
|||
options.setUserName(username); |
|||
options.setPassword(password.toCharArray()); |
|||
options.setConnectionTimeout(connectionTimeout); |
|||
options.setKeepAliveInterval(keepAliveInterval); |
|||
options.setCleanSession(cleanSession); |
|||
options.setAutomaticReconnect(true); // 启用自动重连
|
|||
return options; |
|||
} |
|||
|
|||
/** |
|||
* 4. 创建一个事件监听器,用于处理连接成功事件 |
|||
* 这取代了之前传递 Runnable 的方式,实现了更好的解耦 |
|||
*/ |
|||
/* @Async |
|||
@EventListener |
|||
public void handleMqttConnectionComplete(MqttConnectedEvent event) { |
|||
log.info("监听到 MQTT 连接成功事件,开始执行订阅操作..."); |
|||
subscribeToTopics(); |
|||
}*/ |
|||
private void subscribeToTopics() { |
|||
if (mqttClient == null || !mqttClient.isConnected()) { |
|||
log.error("无法订阅主题,因为 MQTT 客户端尚未连接。"); |
|||
return; |
|||
} |
|||
try { |
|||
log.info("执行 MQTT 主题订阅: {}", (Object) subscribeTopics); |
|||
mqttClient.subscribe(subscribeTopics, subscribeQos); |
|||
} catch (MqttException e) { |
|||
log.error("订阅 MQTT 主题时发生错误: {}", e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
// --- 4. 发布和清理方法 (保持不变) ---
|
|||
public void publish(String topic, String payload) { |
|||
publish(topic, payload.getBytes(), this.publishQos, false); |
|||
} |
|||
|
|||
public void publish(String topic, byte[] payload, int qos, boolean retained) { |
|||
if (mqttClient == null || !mqttClient.isConnected()) { |
|||
log.error("MQTT 客户端未连接,无法发布消息。"); |
|||
return; |
|||
} |
|||
try { |
|||
MqttMessage message = new MqttMessage(payload); |
|||
message.setQos(qos); |
|||
message.setRetained(retained); |
|||
mqttClient.publish(topic, message); |
|||
} catch (MqttException e) { |
|||
log.error("发布 MQTT 消息到主题 [{}] 时发生错误: {}", topic, e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
@PreDestroy |
|||
public void cleanup() { |
|||
if (mqttClient != null) { |
|||
try { |
|||
if (mqttClient.isConnected()) { |
|||
mqttClient.disconnect(); |
|||
} |
|||
mqttClient.close(); |
|||
log.info("MQTT 客户端已成功关闭。"); |
|||
} catch (MqttException e) { |
|||
log.error("关闭 MQTT 客户端时发生错误: {}", e.getMessage(), e); |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,9 @@ |
|||
package cn.iocoder.yudao.module.mqtt.mqtt; |
|||
|
|||
import org.springframework.context.ApplicationEvent; |
|||
|
|||
public class MqttConnectedEvent extends ApplicationEvent { |
|||
public MqttConnectedEvent(Object source) { |
|||
super(source); |
|||
} |
|||
} |
@ -0,0 +1,68 @@ |
|||
package cn.iocoder.yudao.module.mqtt.mqtt; |
|||
|
|||
import cn.hutool.core.util.ReUtil; |
|||
import cn.iocoder.yudao.module.mqtt.kafka.KafkaTopicType; |
|||
import jakarta.annotation.Resource; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; |
|||
import org.eclipse.paho.client.mqttv3.MqttCallbackExtended; |
|||
import org.eclipse.paho.client.mqttv3.MqttMessage; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.context.ApplicationEventPublisher; |
|||
import org.springframework.kafka.core.KafkaTemplate; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.util.List; |
|||
import java.util.concurrent.BlockingQueue; |
|||
import java.util.regex.Pattern; |
|||
|
|||
@Slf4j |
|||
@Component |
|||
public class OnMessageCallback implements MqttCallbackExtended { |
|||
private final Pattern topicPattern = Pattern.compile("([^/]+)/?(.+)?"); |
|||
@Resource |
|||
private KafkaTemplate<String, String> kafkaTemplate; |
|||
@Resource |
|||
private ApplicationEventPublisher eventPublisher; |
|||
|
|||
@Override |
|||
public void connectComplete(boolean reconnect, String serverURI) { |
|||
log.info("MQTT 连接成功。是否为重连: {}, 服务器 URI: {}", reconnect, serverURI); |
|||
// 3. 发布一个 Spring 事件,通知其他组件(如 Client)连接已成功
|
|||
eventPublisher.publishEvent(new MqttConnectedEvent(this)); |
|||
} |
|||
|
|||
@Override |
|||
public void connectionLost(Throwable cause) { |
|||
log.error("MQTT 连接已断开,正在尝试自动重连...", cause); |
|||
} |
|||
|
|||
@Override |
|||
public void messageArrived(String MqttTopic, MqttMessage message) { |
|||
String payload = new String(message.getPayload()); |
|||
List<String> groups = ReUtil.getAllGroups(topicPattern, MqttTopic, false); |
|||
|
|||
if (groups.get(1) == null) { |
|||
log.warn("无法从topic {} 中获取消息发送者ID", MqttTopic); |
|||
return; |
|||
} |
|||
String sn = groups.get(0); |
|||
String topic = groups.get(1); |
|||
KafkaTopicType from = KafkaTopicType.from(groups.get(1)); |
|||
if (null == from) { |
|||
log.warn("发送的topic无效{}", topic); |
|||
return; |
|||
} |
|||
try { |
|||
kafkaTemplate.send(topic, sn, payload); |
|||
log.debug("成功将消息转发到 Kafka Topic [{}]: payload=[{}]", topic, payload); |
|||
} catch (Exception e) { |
|||
log.error("转发消息到 Kafka Topic [{}] 时发生错误!", topic, e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void deliveryComplete(IMqttDeliveryToken token) { |
|||
// no-op
|
|||
} |
|||
} |
@ -0,0 +1,31 @@ |
|||
package cn.iocoder.yudao.module.mqtt.mqtt; |
|||
|
|||
|
|||
import org.springframework.context.annotation.Bean; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.core.task.TaskExecutor; |
|||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; |
|||
|
|||
import java.util.concurrent.ThreadPoolExecutor; |
|||
|
|||
@Configuration |
|||
|
|||
public class ThreadPoolConfig { |
|||
@Bean("mqttExecutor") |
|||
public TaskExecutor taskExecutor() { |
|||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); |
|||
// 核心线程数:CPU核心数
|
|||
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors()); |
|||
// 最大线程数:CPU核心数 * 2
|
|||
executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 2); |
|||
// 队列大小
|
|||
executor.setQueueCapacity(1000); |
|||
// 线程名称前缀
|
|||
executor.setThreadNamePrefix("mqtt-executor-"); |
|||
// 拒绝策略:由调用者线程处理
|
|||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); |
|||
// 初始化
|
|||
executor.initialize(); |
|||
return executor; |
|||
} |
|||
} |
@ -0,0 +1,505 @@ |
|||
package cn.iocoder.yudao.module.mqtt.processor; |
|||
|
|||
import cn.hutool.json.JSONObject; |
|||
import cn.hutool.json.JSONUtil; |
|||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
|||
import cn.iocoder.yudao.module.hand.dal.*; |
|||
import cn.iocoder.yudao.module.hand.enums.*; |
|||
import cn.iocoder.yudao.module.hand.service.*; |
|||
import cn.iocoder.yudao.module.hand.util.*; |
|||
import cn.iocoder.yudao.module.hand.vo.*; |
|||
import cn.iocoder.yudao.module.mqtt.config.TdengineBatchConfig; |
|||
import jakarta.annotation.Resource; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.apache.commons.lang3.StringUtils; |
|||
import org.redisson.api.RLock; |
|||
import org.redisson.api.RedissonClient; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.sql.Timestamp; |
|||
import java.time.LocalDateTime; |
|||
import java.util.*; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
@Slf4j |
|||
@Component |
|||
public class DeviceMessageProcessor { |
|||
|
|||
@Resource |
|||
private RedisUtil redisUtil; |
|||
@Resource |
|||
private TdengineBatchConfig<HandOriginalLog> tdengineBatchProcessor; |
|||
@Resource |
|||
private TdengineBatchConfig<TdengineDataVo> tdengineBatchConfig; |
|||
@Resource |
|||
private HandDetectorService handDetectorService; |
|||
|
|||
@Resource |
|||
private HandAlarmService handAlarmService; |
|||
|
|||
@Resource |
|||
private AlarmRuleService alarmRuleService; |
|||
|
|||
@Resource |
|||
private RedissonClient redissonClient; |
|||
@Resource |
|||
private FenceService fenceService; |
|||
@Resource |
|||
private FenceAlarmService fenceAlarmService; |
|||
|
|||
|
|||
public void process(String topic, String payload) { |
|||
log.debug("[设备上报] 开始处理 -> 主题: {}, 内容: {}", topic, payload); |
|||
|
|||
Object tenantIdObj = redisUtil.hget(RedisKeyUtil.getDeviceTenantMappingKey(), topic); |
|||
if (tenantIdObj == null || StringUtils.isBlank(tenantIdObj.toString())) { |
|||
log.warn("[设备上报] 无法找到 SN {} 对应的租户信息,设备可能未注册。消息丢弃。", topic); |
|||
return; // 结束处理
|
|||
} |
|||
Long tenantId = Long.parseLong(tenantIdObj.toString()); |
|||
|
|||
// 保存原始日志,这个操作不涉及状态竞争,可以放在锁外
|
|||
logSave(topic, payload, tenantId); |
|||
|
|||
// topic 即为设备 sn,是锁的最佳 key
|
|||
String deviceProcessLockKey = RedisKeyUtil.getDeviceProcessLockKey(tenantId, topic); |
|||
RLock lock = redissonClient.getLock(deviceProcessLockKey); |
|||
try { |
|||
// 尝试在 10 秒内获取锁,防止因锁等待导致 Kafka 消费者线程长时间阻塞
|
|||
if (!lock.tryLock(10, TimeUnit.SECONDS)) { |
|||
log.warn("未能获取设备 {} 的处理锁,消息将被丢弃或重试。", topic); |
|||
// 抛出异常,外层逻辑可以捕获并发送到死信队列
|
|||
throw new RuntimeException("获取设备锁超时: " + topic); |
|||
} |
|||
try { |
|||
// 从 Redis 中获取设备信息
|
|||
Object meterObj = redisUtil.hget(RedisKeyUtil.getTenantDeviceHashKey(tenantId), topic); |
|||
HandDataVo detector; |
|||
|
|||
if (meterObj == null) { // 缓存未命中
|
|||
// 从数据库查询 DO 对象
|
|||
HandDetectorDO one = handDetectorService.getBySn(topic); |
|||
if (one == null) { |
|||
log.warn("[数据不一致] 在租户 {} 的设备列表中未找到 SN {} 的详细信息。", tenantId, topic); |
|||
return; // 直接返回,无需再操作
|
|||
} |
|||
detector = BeanUtils.toBean(one, HandDataVo.class); |
|||
} else { // 缓存命中
|
|||
detector = BeanUtils.toBean(meterObj, HandDataVo.class); |
|||
} |
|||
|
|||
if (EnableStatus.DISABLED.value().equals(detector.getEnableStatus())) { |
|||
log.info("未启用的手持探测器 sn: {}", topic); |
|||
return; |
|||
} |
|||
// 数据解析与转换
|
|||
HandDataVo handVo = dataConvert(topic, payload, detector); |
|||
|
|||
// 获取气种的报警规则
|
|||
Map<Long, List<AlarmRuleDO>> ruleMap = alarmRuleService.selectCacheListMap(tenantId); |
|||
AlarmRuleDO alarmRule = getAlarmRule(handVo, ruleMap); |
|||
|
|||
// 气体报警逻辑处理
|
|||
HandDataVo vo = gasHandAlarm(alarmRule, handVo); |
|||
// 气体报警保存
|
|||
saveGasAlarm(vo, alarmRule); |
|||
|
|||
//围栏报警逻辑
|
|||
fanceAlarm(handVo); |
|||
|
|||
//电量报警逻辑
|
|||
batteryAlarm(handVo); |
|||
|
|||
// 无论是否发生告警,设备的状态(如电量、位置、最新值)都需要更新
|
|||
handDetectorService.updateRedisData(tenantId, topic, handVo); |
|||
handLogSave(handVo); |
|||
|
|||
} finally { |
|||
// 确保锁一定被释放
|
|||
if (lock.isHeldByCurrentThread()) { |
|||
lock.unlock(); |
|||
} |
|||
} |
|||
} catch (InterruptedException e) { |
|||
Thread.currentThread().interrupt(); |
|||
log.error("获取设备 {} 的锁时被中断", topic, e); |
|||
throw new RuntimeException("处理设备消息时线程被中断: " + topic, e); |
|||
} |
|||
} |
|||
|
|||
private void handLogSave(HandDataVo handVo) { |
|||
TdengineDataVo bean = BeanUtils.toBean(handVo, TdengineDataVo.class); |
|||
bean.setTenantId(handVo.getTenantId()); |
|||
bean.setTs(new Timestamp(System.currentTimeMillis())); // 直接创建Timestamp对象
|
|||
tdengineBatchConfig.addToBatch(bean); |
|||
} |
|||
|
|||
private void batteryAlarm(HandDataVo handVo) { |
|||
handVo.setBatteryStatus(EnableStatus.DISABLED.value()); // 默认状态为正常
|
|||
if (null == handVo.getBatteryAlarmValue()) { |
|||
return; |
|||
} |
|||
|
|||
if (null == handVo.getBatteryStatusAlarmId()) { |
|||
return; |
|||
} |
|||
// --- 步骤2: 计算当前电量 ---
|
|||
// 只有在告警功能已配置的情况下,才继续执行后续的电量计算和判断逻辑。
|
|||
int batteryPercentage; |
|||
try { |
|||
batteryPercentage = BatteryConverterUtils.getBatteryPercentage(Integer.parseInt(handVo.getBattery())); |
|||
} catch (NumberFormatException e) { |
|||
log.error("设备电池数据格式无效,设备SN: {},原始电池值: {},异常信息: {}", |
|||
handVo.getSn(), handVo.getBattery(), e.getMessage(), e); |
|||
return; |
|||
} |
|||
|
|||
// --- 步骤3: 执行告警状态判断和转换逻辑 ---
|
|||
int alarmThreshold = handVo.getBatteryAlarmValue().intValue(); // 此处调用是安全的,因为已在步骤1排除了null的可能
|
|||
boolean isCurrentlyInAlarm = Objects.equals(handVo.getBatteryStatus(), EnableStatus.ENABLED.value()); |
|||
boolean shouldBeInAlarm = batteryPercentage < alarmThreshold; |
|||
|
|||
// 场景一:从“正常”变为“告警” -> 创建新告警
|
|||
if (shouldBeInAlarm && !isCurrentlyInAlarm) { |
|||
HandAlarmSaveReqVO alarm = new HandAlarmSaveReqVO(); |
|||
alarm.setAlarmType(AlarmType.BATTERY.getType()); |
|||
alarm.setTAlarmStart(LocalDateTime.now()); |
|||
alarm.setVAlarmFirst(handVo.getFirstValue()); |
|||
alarm.setPicX(handVo.getLatitude()); |
|||
alarm.setPicY(handVo.getLongitude()); |
|||
handAlarmService.createHandAlarm(alarm); |
|||
|
|||
// 场景二:从“告警”变为“正常” -> 结束告警
|
|||
} else if (!shouldBeInAlarm && isCurrentlyInAlarm) { |
|||
HandAlarmSaveReqVO alarm = new HandAlarmSaveReqVO(); |
|||
alarm.setTAlarmEnd(LocalDateTime.now()); |
|||
// alarm.setId(handVo.getBatteryStatusAlarmId()); // 别忘了传递告警ID
|
|||
handAlarmService.updateHandAlarm(alarm); |
|||
} |
|||
// 如果状态未变(持续告警或持续正常),不执行任何数据库操作。
|
|||
// 根据当前是否应该告警来设置最终的正确状态。
|
|||
if (shouldBeInAlarm) { |
|||
handVo.setBatteryStatus(EnableStatus.ENABLED.value()); |
|||
} else { |
|||
handVo.setBatteryStatus(EnableStatus.DISABLED.value()); |
|||
} |
|||
} |
|||
|
|||
private void saveGasAlarm(HandDataVo vo, AlarmRuleDO alarmRule) { |
|||
if (null == alarmRule || null == alarmRule.getAlarmLevel()) { |
|||
log.error("报警规则不存在:sn: {}", vo.getSn()); |
|||
return; |
|||
} |
|||
if (vo.getTAlarmStart() == null) {//没有开始时间不需要报警
|
|||
return; |
|||
} |
|||
//报警恢复和添加报警
|
|||
if (vo.getTAlarmEnd() != null) { |
|||
if (vo.getAlarmId() == null) { |
|||
log.error("报警恢复失败:一个已结束的报警事件在Redis中没有找到 alarmId。sn: {}", vo.getSn()); |
|||
return; |
|||
} |
|||
HandAlarmDO alarm = new HandAlarmDO(); |
|||
alarm.setTAlarmEnd(vo.getTAlarmEnd()); |
|||
alarm.setId(vo.getAlarmId()); |
|||
alarm.setVAlarmMaximum(vo.getMaxValue()); |
|||
alarm.setAlarmLevel(vo.getMaxAlarmLevel()); |
|||
alarm.setTenantId(vo.getTenantId()); |
|||
int count = handAlarmService.updateById(alarm); |
|||
if (count > 0) { |
|||
log.info("报警事件(ID: {})已结束,成功更新数据库。", alarm.getId()); |
|||
// 重置 Redis 数据
|
|||
vo.setAlarmId(null); |
|||
vo.setFirstValue(null); |
|||
vo.setMaxValue(null); |
|||
vo.setTAlarmStart(null); |
|||
vo.setTAlarmEnd(null); |
|||
vo.setMaxAlarmLevel(null); |
|||
vo.setAlarmLevel(0); // 将当前级别设为正常
|
|||
} else { |
|||
log.error("数据库更新失败:尝试结束报警事件(ID: {})时出错。", vo.getAlarmId()); |
|||
} |
|||
} else { |
|||
if (vo.getAlarmId() == null) { |
|||
HandAlarmSaveReqVO alarmDO = BeanUtils.toBean(vo, HandAlarmSaveReqVO.class); |
|||
|
|||
Long count = handAlarmService.createHandAlarm(alarmDO); |
|||
if (count > 0) { |
|||
vo.setAlarmId(count); |
|||
log.info("新报警事件已创建,数据库ID: {},并已回写至Redis。", count); |
|||
} |
|||
} else { |
|||
// -- 更新 --
|
|||
HandAlarmDO alarmToUpdate = new HandAlarmDO(); |
|||
alarmToUpdate.setId(vo.getAlarmId()); |
|||
alarmToUpdate.setVAlarmMaximum(vo.getMaxValue()); |
|||
handAlarmService.updateById(alarmToUpdate); |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 围栏报警逻辑 |
|||
*/ |
|||
private void fanceAlarm(HandDataVo handVo) { |
|||
if (StringUtils.isBlank(handVo.getFenceIds())) { |
|||
log.info("当前设备未绑定围栏,sn{}", handVo.getSn()); |
|||
return; |
|||
} |
|||
List<Long> list = Arrays.stream(handVo.getFenceIds().split(",")) |
|||
.map(Long::parseLong) |
|||
.toList(); |
|||
List<Geofence> fenceList = fenceService.getFenceList(list); |
|||
|
|||
|
|||
FenceType fenceType = FenceType.fromType(handVo.getFenceType());// 转换为枚举
|
|||
|
|||
// a. 判断点是否在任何一个围栏内
|
|||
boolean isCurrentlyInside = GeofenceUtils.isInsideAnyFence(handVo.getLongitude(), handVo.getLatitude(), fenceList); |
|||
|
|||
Boolean isViolating = switch (fenceType) { |
|||
case null -> { |
|||
log.error("围栏类型为 null,无法处理。"); |
|||
yield null; // 使用 yield 从代码块中返回一个值
|
|||
} |
|||
// "进入围栏报警" -> 违规条件是:车辆在围栏内部
|
|||
case INSIDE -> isCurrentlyInside; |
|||
|
|||
// "超出围栏报警" -> 违规条件是:车辆在围栏外部
|
|||
case OUTSIDE -> !isCurrentlyInside; |
|||
|
|||
// default 分支确保了所有未明确处理的枚举类型都能被覆盖
|
|||
default -> { |
|||
log.error("逻辑中未处理的围拦类型: {}。", fenceType); |
|||
yield null; |
|||
} |
|||
}; |
|||
// --- 3. 根据计算出的状态,执行统一的后续逻辑 ---
|
|||
// 如果 isViolating 为 null (因为 fenceType 无效或为 null),则不执行任何操作。
|
|||
if (isViolating != null) { |
|||
handleAlarmLifecycle(isViolating, handVo, fenceType, fenceList); |
|||
} |
|||
|
|||
} |
|||
|
|||
/** |
|||
* 统一处理报警的开始、持续和结束。 |
|||
* |
|||
* @param isViolating 当前状态是否违规 |
|||
* @param handVo 当前位置信息,携带着报警状态 (fenceAlarmId) |
|||
* @param fenceType 围栏类型 |
|||
* @param fenceList 围栏几何对象列表 |
|||
*/ |
|||
private void handleAlarmLifecycle(boolean isViolating, HandDataVo handVo, FenceType fenceType, List<Geofence> fenceList) { |
|||
|
|||
// 关键状态判断:根据 handVo 中是否已有报警ID,判断之前是否有正在进行的报警
|
|||
boolean hasOngoingAlarm = (handVo.getFenceAlarmId() != null); |
|||
|
|||
if (isViolating) { |
|||
// --- 场景:当前处于违规状态 ---
|
|||
// 计算当前的违规距离
|
|||
double currentDistance = GeofenceUtils.fenceDistance(handVo, fenceType, fenceList); |
|||
|
|||
if (!hasOngoingAlarm) { |
|||
// 1. 【触发新报警】: 之前不报警,现在开始违规
|
|||
log.warn("触发 [{}] 类型围栏报警! sn: {}", fenceType, handVo.getSn()); |
|||
|
|||
FenceAlarmSaveReqVO newAlarm = new FenceAlarmSaveReqVO(); |
|||
newAlarm.setDetectorId(handVo.getId()); |
|||
/* newAlarm.setFenceId(fenceDO.getId());*/ |
|||
newAlarm.setType(fenceType.getType()); |
|||
newAlarm.setTAlarmStart(LocalDateTime.now()); // 记录报警开始时间
|
|||
newAlarm.setPicX(handVo.getLongitude()); |
|||
newAlarm.setPicY(handVo.getLatitude()); |
|||
newAlarm.setTenantId(handVo.getTenantId()); |
|||
|
|||
// 初始化距离
|
|||
newAlarm.setDistance(currentDistance); |
|||
newAlarm.setMaxDistance(currentDistance); |
|||
handVo.setDistance(currentDistance); |
|||
handVo.setMaxDistance(currentDistance); |
|||
|
|||
// 保存新报警,并获取返回的ID,用于更新 handVo 的状态
|
|||
Long fenceAlarm = fenceAlarmService.createFenceAlarm(newAlarm); |
|||
if (fenceAlarm != null) { |
|||
// *** 关键:更新内存/缓存中的 handVo 状态,以便下次判断 ***
|
|||
handVo.setFenceAlarmId(fenceAlarm); |
|||
} |
|||
} else { |
|||
// 2. 【更新持续报警】: 之前在报警,现在仍然违规
|
|||
log.info("持续 [{}] 类型围栏报警,更新距离... fenceAlarmId: {}", fenceType, handVo.getFenceAlarmId()); |
|||
|
|||
FenceAlarmDO existingAlarm = fenceAlarmService.getFenceAlarm(handVo.getFenceAlarmId()); |
|||
if (existingAlarm == null) { |
|||
log.error("逻辑错误:handVo 中有报警ID,但数据库中找不到记录!ID: {}", handVo.getFenceAlarmId()); |
|||
// 异常处理:可以选择重置 handVo 状态,或创建新报警
|
|||
handVo.setFenceAlarmId(null); // 重置状态
|
|||
return; |
|||
} |
|||
|
|||
existingAlarm.setDistance(currentDistance); |
|||
if (existingAlarm.getMaxDistance() == null || currentDistance > existingAlarm.getMaxDistance()) { |
|||
existingAlarm.setMaxDistance(currentDistance); |
|||
handVo.setMaxDistance(currentDistance); |
|||
} |
|||
fenceAlarmService.update(existingAlarm); |
|||
} |
|||
handVo.setGasStatus(HandAlarmType.ALARM.getType()); |
|||
} else { |
|||
// --- 场景:当前处于正常状态 ---
|
|||
|
|||
if (hasOngoingAlarm) { |
|||
// 3. 【结束报警】: 之前在报警,现在恢复正常
|
|||
log.warn("结束 [{}] 类型围栏报警。 fenceAlarmId: {}", fenceType, handVo.getFenceAlarmId()); |
|||
// 调用服务去结束报警(例如设置 endTime)
|
|||
FenceAlarmDO fenceAlarmDO = new FenceAlarmDO(); |
|||
fenceAlarmDO.setId(handVo.getFenceAlarmId()); |
|||
fenceAlarmDO.setTAlarmEnd(LocalDateTime.now()); |
|||
fenceAlarmService.update(fenceAlarmDO); |
|||
|
|||
// *** 关键:重置内存/缓存中的 handVo 状态 ***
|
|||
handVo.setFenceAlarmId(null); |
|||
handVo.setDistance(null); |
|||
handVo.setMaxDistance(null); |
|||
handVo.setGasStatus(HandAlarmType.NORMAL.getType()); |
|||
} else { |
|||
log.debug("当前状态正常,且无进行中报警,无需操作。"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
private AlarmRuleDO getAlarmRule(HandDataVo handVo, Map<Long, List<AlarmRuleDO>> ruleMap) { |
|||
double gasValue = handVo.getValue(); |
|||
Long gasTypeId = handVo.getGasTypeId(); |
|||
|
|||
List<AlarmRuleDO> gasAlarmRuleList = ruleMap.get(gasTypeId); |
|||
|
|||
if (gasAlarmRuleList == null || gasAlarmRuleList.isEmpty()) { |
|||
log.error("无报警规则:detector={} gas type={} value={}", handVo.getSn(), handVo.getGasChemical(), gasValue); |
|||
return null; |
|||
} |
|||
|
|||
AlarmRuleDO alarmRule = null; |
|||
for (AlarmRuleDO rule : gasAlarmRuleList) { |
|||
// 最小值或最大值可为空
|
|||
if ((rule.getMin() == null || rule.getMin() <= gasValue) && (rule.getMax() == null || gasValue <= rule.getMax())) { |
|||
// 范围重复高级别报警优先
|
|||
if (alarmRule == null || alarmRule.getAlarmLevel() < rule.getAlarmLevel()) { |
|||
alarmRule = rule; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return alarmRule; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 气体报警逻辑处理 |
|||
*/ |
|||
private HandDataVo gasHandAlarm(AlarmRuleDO alarmRule, HandDataVo redisData) { |
|||
if (null == alarmRule) { |
|||
log.error("无报警规则:sn: {}", redisData.getSn()); |
|||
return redisData; |
|||
} |
|||
|
|||
LocalDateTime now = LocalDateTime.now(); |
|||
//离线报警结束
|
|||
if (OnlineStatusType.ONLINE.getType().equals(redisData.getOnlineStatus()) |
|||
&& AlarmLevelEnum.OFFLINE.value().equals(redisData.getAlarmLevel())) { |
|||
HandAlarmDO handAlarmDO = new HandAlarmDO(); |
|||
handAlarmDO.setId(redisData.getAlarmId()); |
|||
handAlarmDO.setTAlarmEnd(now); |
|||
handAlarmService.updateById(handAlarmDO); |
|||
//删除离线报警
|
|||
redisData.setAlarmId(null); |
|||
} |
|||
|
|||
boolean isCurrentlyAlarming = redisData.getAlarmLevel() != null && redisData.getAlarmLevel() > 0; |
|||
boolean isRuleTriggeringAlarm = alarmRule.getAlarmLevel() > 0; |
|||
// 之前在报警,但现在报警恢复正常
|
|||
if (isCurrentlyAlarming && !isRuleTriggeringAlarm) { |
|||
redisData.setAlarmLevel(0); |
|||
redisData.setTAlarmEnd(now); |
|||
redisData.setGasStatus(HandAlarmType.NORMAL.getType()); |
|||
return redisData; |
|||
} |
|||
|
|||
if (alarmRule.getAlarmLevel() == 0) { |
|||
redisData.setAlarmLevel(0); |
|||
return redisData; |
|||
} |
|||
|
|||
|
|||
if (isRuleTriggeringAlarm) { |
|||
Integer newAlarmLevel = alarmRule.getAlarmLevel(); |
|||
|
|||
// 如果是首次报警 (之前不处于报警状态)
|
|||
if (!isCurrentlyAlarming) { |
|||
redisData.setFirstValue(redisData.getValue()); |
|||
redisData.setTAlarmStart(now); |
|||
redisData.setMaxValue(redisData.getValue()); // 初始值
|
|||
redisData.setMaxAlarmLevel(newAlarmLevel); |
|||
} |
|||
redisData.setAlarmLevel(newAlarmLevel); |
|||
|
|||
// 报警升级
|
|||
if (redisData.getMaxAlarmLevel() == null || newAlarmLevel > redisData.getMaxAlarmLevel()) { |
|||
redisData.setMaxAlarmLevel(newAlarmLevel); |
|||
} |
|||
|
|||
// 更新最大值
|
|||
if (MaxDirection.DOWN.value().equals(alarmRule.getDirection()) && redisData.getValue() < redisData.getMaxValue() |
|||
|| MaxDirection.UP.value().equals(alarmRule.getDirection()) && redisData.getValue() > redisData.getMaxValue()) { |
|||
// 更新最大值
|
|||
redisData.setMaxValue(redisData.getValue()); |
|||
} |
|||
} |
|||
redisData.setGasStatus(HandAlarmType.ALARM.getType()); |
|||
return redisData; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 数据转换 |
|||
*/ |
|||
private HandDataVo dataConvert(String topic, String payload, HandDataVo detector) { |
|||
JSONObject obj = JSONUtil.parseObj(payload); |
|||
|
|||
Double value = obj.getDouble("gas", null);//气体值
|
|||
String loc = obj.getStr("loc"); |
|||
String battery = obj.getStr("battery"); |
|||
String numbersString = loc.substring(1, loc.length() - 1); |
|||
// 将字符串按逗号分割成数组
|
|||
String[] split = numbersString.split(","); |
|||
|
|||
Map<String, Double> wgs84ToGcj02 = CoordinateTransferUtils.wgs84ToGcj02(Double.parseDouble(split[0]), Double.parseDouble(split[1])); |
|||
|
|||
Double lon = wgs84ToGcj02.get("lon"); |
|||
Double lat = wgs84ToGcj02.get("lat"); |
|||
detector.setBattery(battery); |
|||
detector.setLongitude(lon); |
|||
detector.setLatitude(lat); |
|||
detector.setValue(value); |
|||
detector.setSn(topic); |
|||
//添加最新时间
|
|||
detector.setTime(new Date(System.currentTimeMillis())); |
|||
//添加默认状态
|
|||
detector.setOnlineStatus(OnlineStatusType.ONLINE.getType()); |
|||
|
|||
return detector; |
|||
} |
|||
|
|||
private void logSave(String topic, String payload, Long tenantId) { |
|||
HandOriginalLog handOriginalLog = new HandOriginalLog(); |
|||
handOriginalLog.setSn(topic); |
|||
handOriginalLog.setPayload(payload); |
|||
handOriginalLog.setTenantId(tenantId); |
|||
handOriginalLog.setTs(new Timestamp(System.currentTimeMillis())); // 直接创建Timestamp对象
|
|||
tdengineBatchProcessor.addToBatch(handOriginalLog); |
|||
} |
|||
|
|||
|
|||
} |
@ -0,0 +1,107 @@ |
|||
package cn.iocoder.yudao.module.hand.controller.admin; |
|||
|
|||
import cn.iocoder.yudao.module.hand.dal.FactoryDO; |
|||
import cn.iocoder.yudao.module.hand.service.FactoryService; |
|||
import cn.iocoder.yudao.module.hand.vo.FactoryPageReqVO; |
|||
import cn.iocoder.yudao.module.hand.vo.FactoryRespVO; |
|||
import cn.iocoder.yudao.module.hand.vo.FactorySaveReqVO; |
|||
import org.springframework.web.bind.annotation.*; |
|||
import jakarta.annotation.Resource; |
|||
import org.springframework.validation.annotation.Validated; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import io.swagger.v3.oas.annotations.tags.Tag; |
|||
import io.swagger.v3.oas.annotations.Parameter; |
|||
import io.swagger.v3.oas.annotations.Operation; |
|||
|
|||
import jakarta.validation.constraints.*; |
|||
import jakarta.validation.*; |
|||
import jakarta.servlet.http.*; |
|||
import java.util.*; |
|||
import java.io.IOException; |
|||
|
|||
import cn.iocoder.yudao.framework.common.pojo.PageParam; |
|||
import cn.iocoder.yudao.framework.common.pojo.PageResult; |
|||
import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
|||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
|||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
|||
|
|||
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; |
|||
|
|||
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; |
|||
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*; |
|||
|
|||
|
|||
|
|||
@Tag(name = "管理后台 - GAS工厂") |
|||
@RestController |
|||
@RequestMapping("/gas/factory") |
|||
@Validated |
|||
public class FactoryController { |
|||
|
|||
@Resource |
|||
private FactoryService factoryService; |
|||
|
|||
@PostMapping("/create") |
|||
@Operation(summary = "创建GAS工厂") |
|||
@PreAuthorize("@ss.hasPermission('gas:factory:create')") |
|||
public CommonResult<Long> createFactory(@Valid @RequestBody FactorySaveReqVO createReqVO) { |
|||
return success(factoryService.createFactory(createReqVO)); |
|||
} |
|||
|
|||
@PutMapping("/update") |
|||
@Operation(summary = "更新GAS工厂") |
|||
@PreAuthorize("@ss.hasPermission('gas:factory:update')") |
|||
public CommonResult<Boolean> updateFactory(@Valid @RequestBody FactorySaveReqVO updateReqVO) { |
|||
factoryService.updateFactory(updateReqVO); |
|||
return success(true); |
|||
} |
|||
|
|||
@DeleteMapping("/delete") |
|||
@Operation(summary = "删除GAS工厂") |
|||
@Parameter(name = "id", description = "编号", required = true) |
|||
@PreAuthorize("@ss.hasPermission('gas:factory:delete')") |
|||
public CommonResult<Boolean> deleteFactory(@RequestParam("id") Long id) { |
|||
factoryService.deleteFactory(id); |
|||
return success(true); |
|||
} |
|||
|
|||
@DeleteMapping("/delete-list") |
|||
@Parameter(name = "ids", description = "编号", required = true) |
|||
@Operation(summary = "批量删除GAS工厂") |
|||
@PreAuthorize("@ss.hasPermission('gas:factory:delete')") |
|||
public CommonResult<Boolean> deleteFactoryList(@RequestParam("ids") List<Long> ids) { |
|||
factoryService.deleteFactoryListByIds(ids); |
|||
return success(true); |
|||
} |
|||
|
|||
@GetMapping("/get") |
|||
@Operation(summary = "获得GAS工厂") |
|||
@Parameter(name = "id", description = "编号", required = true, example = "1024") |
|||
@PreAuthorize("@ss.hasPermission('gas:factory:query')") |
|||
public CommonResult<FactoryRespVO> getFactory(@RequestParam("id") Long id) { |
|||
FactoryDO factory = factoryService.getFactory(id); |
|||
return success(BeanUtils.toBean(factory, FactoryRespVO.class)); |
|||
} |
|||
|
|||
@GetMapping("/page") |
|||
@Operation(summary = "获得GAS工厂分页") |
|||
@PreAuthorize("@ss.hasPermission('gas:factory:query')") |
|||
public CommonResult<PageResult<FactoryRespVO>> getFactoryPage(@Valid FactoryPageReqVO pageReqVO) { |
|||
PageResult<FactoryDO> pageResult = factoryService.getFactoryPage(pageReqVO); |
|||
return success(BeanUtils.toBean(pageResult, FactoryRespVO.class)); |
|||
} |
|||
|
|||
@GetMapping("/export-excel") |
|||
@Operation(summary = "导出GAS工厂 Excel") |
|||
@PreAuthorize("@ss.hasPermission('gas:factory:export')") |
|||
@ApiAccessLog(operateType = EXPORT) |
|||
public void exportFactoryExcel(@Valid FactoryPageReqVO pageReqVO, |
|||
HttpServletResponse response) throws IOException { |
|||
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); |
|||
List<FactoryDO> list = factoryService.getFactoryPage(pageReqVO).getList(); |
|||
// 导出 Excel
|
|||
ExcelUtils.write(response, "GAS工厂.xls", "数据", FactoryRespVO.class, |
|||
BeanUtils.toBean(list, FactoryRespVO.class)); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,66 @@ |
|||
package cn.iocoder.yudao.module.hand.controller.admin; |
|||
|
|||
import cn.iocoder.yudao.framework.security.core.LoginUser; |
|||
import cn.iocoder.yudao.module.hand.dal.HandDetectorDO; |
|||
import cn.iocoder.yudao.module.hand.service.HandDetectorService; |
|||
import cn.iocoder.yudao.module.hand.service.TdengineService; |
|||
import cn.iocoder.yudao.module.hand.util.RedisKeyUtil; |
|||
import cn.iocoder.yudao.module.hand.util.RedisUtil; |
|||
import cn.iocoder.yudao.module.hand.vo.*; |
|||
import org.springframework.web.bind.annotation.*; |
|||
import jakarta.annotation.Resource; |
|||
import org.springframework.validation.annotation.Validated; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import io.swagger.v3.oas.annotations.tags.Tag; |
|||
import io.swagger.v3.oas.annotations.Parameter; |
|||
import io.swagger.v3.oas.annotations.Operation; |
|||
|
|||
import jakarta.validation.constraints.*; |
|||
import jakarta.validation.*; |
|||
import jakarta.servlet.http.*; |
|||
import java.util.*; |
|||
import java.io.IOException; |
|||
|
|||
import cn.iocoder.yudao.framework.common.pojo.PageParam; |
|||
import cn.iocoder.yudao.framework.common.pojo.PageResult; |
|||
import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
|||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
|||
|
|||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
|||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
|||
|
|||
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; |
|||
|
|||
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; |
|||
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*; |
|||
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUser; |
|||
import static cn.iocoder.yudao.module.hand.enums.ErrorCodeConstants.HAND_DETECTOR_NOT_EXISTS; |
|||
import static cn.iocoder.yudao.module.hand.enums.ErrorCodeConstants.HAND_DETECTOR_REDIS_NOT_EXISTS; |
|||
|
|||
|
|||
@Tag(name = "管理后台 - 时序数据库查询") |
|||
@RestController |
|||
@RequestMapping("/gas/tdengine") |
|||
@Validated |
|||
public class HandTdengineController { |
|||
|
|||
@Resource |
|||
private TdengineService tdengineService; |
|||
|
|||
|
|||
@GetMapping("/page") |
|||
@Operation(summary = "获取历史数据") |
|||
@PreAuthorize("@ss.hasPermission('gas:hand-td:query')") |
|||
public CommonResult<PageResult<TdengineDataVo>> getHandDetectorPage(@Valid HandTdenginePageVO pageReqVO) { |
|||
PageResult<TdengineDataVo> pageResult = tdengineService.getHandDataLog(pageReqVO); |
|||
return success(pageResult); |
|||
} |
|||
@GetMapping("/originalLogPage") |
|||
@Operation(summary = "获取原始数据") |
|||
@PreAuthorize("@ss.hasPermission('gas:hand-td:queryOriginal')") |
|||
public CommonResult<PageResult<HandOriginalLog>> originalLogPage(@Valid HandTdenginePageVO pageReqVO) { |
|||
PageResult<HandOriginalLog> pageResult = tdengineService.getOriginalLogPage(pageReqVO); |
|||
return success(pageResult); |
|||
} |
|||
|
|||
} |
@ -1,4 +1,4 @@ |
|||
/** |
|||
* infra 模块的 web 配置 |
|||
*/ |
|||
package cn.iocoder.yudao.module.lock.controller; |
|||
package cn.iocoder.yudao.module.hand.controller; |
|||
|
@ -0,0 +1,97 @@ |
|||
package cn.iocoder.yudao.module.hand.dal; |
|||
|
|||
import lombok.*; |
|||
import java.util.*; |
|||
import java.time.LocalDateTime; |
|||
import java.time.LocalDateTime; |
|||
import com.baomidou.mybatisplus.annotation.*; |
|||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
|||
|
|||
/** |
|||
* GAS工厂 DO |
|||
* |
|||
* @author 超级管理员 |
|||
*/ |
|||
@TableName("gas_factory") |
|||
@KeySequence("gas_factory_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@ToString(callSuper = true) |
|||
@Builder |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class FactoryDO extends BaseDO { |
|||
|
|||
/** |
|||
* 主键ID |
|||
*/ |
|||
@TableId |
|||
private Long id; |
|||
/** |
|||
* 父节点ID |
|||
*/ |
|||
private Long parentId; |
|||
/** |
|||
* 层级(1:工厂;2:车间;3:班组) |
|||
*/ |
|||
private Integer type; |
|||
/** |
|||
* 名称 |
|||
*/ |
|||
private String name; |
|||
/** |
|||
* 城市 |
|||
*/ |
|||
private String city; |
|||
/** |
|||
* 总警报数 |
|||
*/ |
|||
private Integer alarmTotal; |
|||
/** |
|||
* 已处理警报数 |
|||
*/ |
|||
private Integer alarmDeal; |
|||
/** |
|||
* 区域图 |
|||
*/ |
|||
private String picUrl; |
|||
/** |
|||
* 区域图缩放比例 |
|||
*/ |
|||
private Integer picScale; |
|||
/** |
|||
* 在区域图X坐标值 |
|||
*/ |
|||
private Double picX; |
|||
/** |
|||
* 在区域图X坐标值 |
|||
*/ |
|||
private Double picY; |
|||
/** |
|||
* 经度 |
|||
*/ |
|||
private Double longitude; |
|||
/** |
|||
* 纬度 |
|||
*/ |
|||
private Double latitude; |
|||
/** |
|||
* 区域西南坐标 |
|||
*/ |
|||
private String rectSouthWest; |
|||
/** |
|||
* 区域东北坐标 |
|||
*/ |
|||
private String rectNorthEast; |
|||
/** |
|||
* 排序 |
|||
*/ |
|||
private Integer sortOrder; |
|||
/** |
|||
* 备注 |
|||
*/ |
|||
private String remark; |
|||
|
|||
|
|||
|
|||
} |
@ -0,0 +1,48 @@ |
|||
package cn.iocoder.yudao.module.hand.enums; |
|||
|
|||
import lombok.Getter; |
|||
|
|||
@Getter |
|||
public enum AlarmLevelEnum { |
|||
OFFLINE(-1,"离线报警"), |
|||
NORMAL(0,"正常状态"), |
|||
LEVEL_1(1,"一级报警"), |
|||
LEVEL_2(2,"二级报警"), |
|||
LEVEL_3(3,"弹窗警报"); |
|||
private Integer value; |
|||
private String name; |
|||
AlarmLevelEnum(Integer val, String name) { |
|||
this.value = val; |
|||
this.name = name; |
|||
} |
|||
|
|||
public Integer value() { |
|||
return value; |
|||
} |
|||
/** |
|||
* 根据value获取到枚举类名称 |
|||
* |
|||
* @param value |
|||
* @return |
|||
*/ |
|||
public static String getNameByValue(Integer value) { |
|||
AlarmLevelEnum type = getByValue(value); |
|||
return null == type ? "" : type.getName(); |
|||
} |
|||
/** |
|||
* 根据value获取枚举类型 |
|||
* |
|||
* @param value |
|||
* @return |
|||
*/ |
|||
public static AlarmLevelEnum getByValue(Integer value) { |
|||
if (null != value) { |
|||
for (AlarmLevelEnum type : AlarmLevelEnum.values()) { |
|||
if (type.getValue() == value) { |
|||
return type; |
|||
} |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
} |
@ -0,0 +1,53 @@ |
|||
package cn.iocoder.yudao.module.hand.enums; |
|||
|
|||
import lombok.Getter; |
|||
|
|||
import java.util.Objects; |
|||
|
|||
@Getter |
|||
public enum AlarmType { |
|||
GAS(1, "气体报警"), |
|||
BATTERY(2, "电量报警"), |
|||
OFFLINE(3, "离线报警"); |
|||
|
|||
private final Integer type; |
|||
private final String name; |
|||
|
|||
AlarmType(Integer type, String name) { |
|||
this.type = type; |
|||
this.name = name; |
|||
} |
|||
|
|||
// 通过 int 值获取枚举实例
|
|||
public static AlarmType fromType(Integer type) { |
|||
for (AlarmType valveType : AlarmType.values()) { |
|||
if (Objects.equals(valveType.getType(), type)) { |
|||
return valveType; |
|||
} |
|||
} |
|||
throw new IllegalArgumentException("未知的阀门类型: " + type); |
|||
} |
|||
|
|||
// 通过 int 值获取名称
|
|||
public static String getNameByType(int type) { |
|||
AlarmType valveType = fromType(type); |
|||
return valveType.getName(); |
|||
} |
|||
|
|||
// 通过名称获取枚举实例
|
|||
public static AlarmType fromName(String name) { |
|||
for (AlarmType valveType : AlarmType.values()) { |
|||
if (valveType.getName().equals(name)) { |
|||
return valveType; |
|||
} |
|||
} |
|||
throw new IllegalArgumentException("未知的阀门名称: " + name); |
|||
} |
|||
|
|||
// 通过名称获取 int 值
|
|||
public static Integer getTypeByName(String name) { |
|||
AlarmType valveType = fromName(name); |
|||
return valveType.getType(); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,14 @@ |
|||
package cn.iocoder.yudao.module.hand.enums; |
|||
|
|||
public enum EnableStatus { |
|||
DISABLED(0), ENABLED(1),REPAIR(2); |
|||
private Integer value; |
|||
|
|||
EnableStatus(Integer val) { |
|||
this.value = val; |
|||
} |
|||
|
|||
public Integer value() { |
|||
return value; |
|||
} |
|||
} |
@ -0,0 +1,52 @@ |
|||
package cn.iocoder.yudao.module.hand.enums; |
|||
|
|||
import lombok.Getter; |
|||
|
|||
import java.util.Objects; |
|||
|
|||
@Getter |
|||
public enum FenceStatusType { |
|||
DISABLED(1,"启用"), |
|||
ENABLED(2,"禁用"); |
|||
|
|||
private final Integer type; |
|||
private final String name; |
|||
|
|||
FenceStatusType(Integer type, String name) { |
|||
this.type = type; |
|||
this.name = name; |
|||
} |
|||
|
|||
// 通过 int 值获取枚举实例
|
|||
public static FenceStatusType fromType(Integer type) { |
|||
for (FenceStatusType valveType : FenceStatusType.values()) { |
|||
if (Objects.equals(valveType.getType(), type)) { |
|||
return valveType; |
|||
} |
|||
} |
|||
throw new IllegalArgumentException("未知的阀门类型: " + type); |
|||
} |
|||
|
|||
// 通过 int 值获取名称
|
|||
public static String getNameByType(int type) { |
|||
FenceStatusType valveType = fromType(type); |
|||
return valveType.getName(); |
|||
} |
|||
|
|||
// 通过名称获取枚举实例
|
|||
public static FenceStatusType fromName(String name) { |
|||
for (FenceStatusType valveType : FenceStatusType.values()) { |
|||
if (valveType.getName().equals(name)) { |
|||
return valveType; |
|||
} |
|||
} |
|||
throw new IllegalArgumentException("未知的阀门名称: " + name); |
|||
} |
|||
|
|||
// 通过名称获取 int 值
|
|||
public static Integer getTypeByName(String name) { |
|||
FenceStatusType valveType = fromName(name); |
|||
return valveType.getType(); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,52 @@ |
|||
package cn.iocoder.yudao.module.hand.enums; |
|||
|
|||
import lombok.Getter; |
|||
|
|||
import java.util.Objects; |
|||
|
|||
@Getter |
|||
public enum FenceType { |
|||
OUTSIDE(1,"超出围栏报警"), |
|||
INSIDE(2,"进入围栏报警"); |
|||
|
|||
private final Integer type; |
|||
private final String name; |
|||
|
|||
FenceType(Integer type, String name) { |
|||
this.type = type; |
|||
this.name = name; |
|||
} |
|||
|
|||
// 通过 int 值获取枚举实例
|
|||
public static FenceType fromType(Integer type) { |
|||
for (FenceType valveType : FenceType.values()) { |
|||
if (Objects.equals(valveType.getType(), type)) { |
|||
return valveType; |
|||
} |
|||
} |
|||
throw new IllegalArgumentException("未知的阀门类型: " + type); |
|||
} |
|||
|
|||
// 通过 int 值获取名称
|
|||
public static String getNameByType(int type) { |
|||
FenceType valveType = fromType(type); |
|||
return valveType.getName(); |
|||
} |
|||
|
|||
// 通过名称获取枚举实例
|
|||
public static FenceType fromName(String name) { |
|||
for (FenceType valveType : FenceType.values()) { |
|||
if (valveType.getName().equals(name)) { |
|||
return valveType; |
|||
} |
|||
} |
|||
throw new IllegalArgumentException("未知的阀门名称: " + name); |
|||
} |
|||
|
|||
// 通过名称获取 int 值
|
|||
public static Integer getTypeByName(String name) { |
|||
FenceType valveType = fromName(name); |
|||
return valveType.getType(); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,52 @@ |
|||
package cn.iocoder.yudao.module.hand.enums; |
|||
|
|||
import lombok.Getter; |
|||
|
|||
import java.util.Objects; |
|||
|
|||
@Getter |
|||
public enum HandAlarmType { |
|||
NORMAL(0, "正常"), |
|||
ALARM(1, "报警"); |
|||
|
|||
private final Integer type; |
|||
private final String name; |
|||
|
|||
HandAlarmType(Integer type, String name) { |
|||
this.type = type; |
|||
this.name = name; |
|||
} |
|||
|
|||
// 通过 int 值获取枚举实例
|
|||
public static HandAlarmType fromType(Integer type) { |
|||
for (HandAlarmType valveType : HandAlarmType.values()) { |
|||
if (Objects.equals(valveType.getType(), type)) { |
|||
return valveType; |
|||
} |
|||
} |
|||
throw new IllegalArgumentException("未知的阀门类型: " + type); |
|||
} |
|||
|
|||
// 通过 int 值获取名称
|
|||
public static String getNameByType(int type) { |
|||
HandAlarmType valveType = fromType(type); |
|||
return valveType.getName(); |
|||
} |
|||
|
|||
// 通过名称获取枚举实例
|
|||
public static HandAlarmType fromName(String name) { |
|||
for (HandAlarmType valveType : HandAlarmType.values()) { |
|||
if (valveType.getName().equals(name)) { |
|||
return valveType; |
|||
} |
|||
} |
|||
throw new IllegalArgumentException("未知的阀门名称: " + name); |
|||
} |
|||
|
|||
// 通过名称获取 int 值
|
|||
public static Integer getTypeByName(String name) { |
|||
HandAlarmType valveType = fromName(name); |
|||
return valveType.getType(); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,14 @@ |
|||
package cn.iocoder.yudao.module.hand.enums; |
|||
|
|||
public enum MaxDirection { |
|||
DOWN(0), UP(1); |
|||
private Integer value; |
|||
|
|||
MaxDirection(Integer val) { |
|||
this.value = val; |
|||
} |
|||
|
|||
public Integer value() { |
|||
return value; |
|||
} |
|||
} |
@ -0,0 +1,52 @@ |
|||
package cn.iocoder.yudao.module.hand.enums; |
|||
|
|||
import lombok.Getter; |
|||
|
|||
import java.util.Objects; |
|||
|
|||
@Getter |
|||
public enum OnlineStatusType { |
|||
OFFLINE(0, "离线"), |
|||
ONLINE(1, "正常"); |
|||
|
|||
private final Integer type; |
|||
private final String name; |
|||
|
|||
OnlineStatusType(Integer type, String name) { |
|||
this.type = type; |
|||
this.name = name; |
|||
} |
|||
|
|||
// 通过 int 值获取枚举实例
|
|||
public static OnlineStatusType fromType(Integer type) { |
|||
for (OnlineStatusType valveType : OnlineStatusType.values()) { |
|||
if (Objects.equals(valveType.getType(), type)) { |
|||
return valveType; |
|||
} |
|||
} |
|||
throw new IllegalArgumentException("未知的阀门类型: " + type); |
|||
} |
|||
|
|||
// 通过 int 值获取名称
|
|||
public static String getNameByType(int type) { |
|||
OnlineStatusType valveType = fromType(type); |
|||
return valveType.getName(); |
|||
} |
|||
|
|||
// 通过名称获取枚举实例
|
|||
public static OnlineStatusType fromName(String name) { |
|||
for (OnlineStatusType valveType : OnlineStatusType.values()) { |
|||
if (valveType.getName().equals(name)) { |
|||
return valveType; |
|||
} |
|||
} |
|||
throw new IllegalArgumentException("未知的阀门名称: " + name); |
|||
} |
|||
|
|||
// 通过名称获取 int 值
|
|||
public static Integer getTypeByName(String name) { |
|||
OnlineStatusType valveType = fromName(name); |
|||
return valveType.getType(); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,41 @@ |
|||
package cn.iocoder.yudao.module.hand.mapper; |
|||
|
|||
import java.util.*; |
|||
|
|||
import cn.iocoder.yudao.framework.common.pojo.PageResult; |
|||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
|||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
|||
import cn.iocoder.yudao.module.hand.dal.FactoryDO; |
|||
import cn.iocoder.yudao.module.hand.vo.FactoryPageReqVO; |
|||
import org.apache.ibatis.annotations.Mapper; |
|||
|
|||
/** |
|||
* GAS工厂 Mapper |
|||
* |
|||
* @author 超级管理员 |
|||
*/ |
|||
@Mapper |
|||
public interface FactoryMapper extends BaseMapperX<FactoryDO> { |
|||
|
|||
default PageResult<FactoryDO> selectPage(FactoryPageReqVO reqVO) { |
|||
return selectPage(reqVO, new LambdaQueryWrapperX<FactoryDO>() |
|||
.eqIfPresent(FactoryDO::getParentId, reqVO.getParentId()) |
|||
.eqIfPresent(FactoryDO::getType, reqVO.getType()) |
|||
.likeIfPresent(FactoryDO::getName, reqVO.getName()) |
|||
.eqIfPresent(FactoryDO::getCity, reqVO.getCity()) |
|||
.eqIfPresent(FactoryDO::getAlarmTotal, reqVO.getAlarmTotal()) |
|||
.eqIfPresent(FactoryDO::getAlarmDeal, reqVO.getAlarmDeal()) |
|||
.eqIfPresent(FactoryDO::getPicUrl, reqVO.getPicUrl()) |
|||
.eqIfPresent(FactoryDO::getPicScale, reqVO.getPicScale()) |
|||
.eqIfPresent(FactoryDO::getPicX, reqVO.getPicX()) |
|||
.eqIfPresent(FactoryDO::getPicY, reqVO.getPicY()) |
|||
.eqIfPresent(FactoryDO::getLongitude, reqVO.getLongitude()) |
|||
.eqIfPresent(FactoryDO::getLatitude, reqVO.getLatitude()) |
|||
.eqIfPresent(FactoryDO::getRectSouthWest, reqVO.getRectSouthWest()) |
|||
.eqIfPresent(FactoryDO::getRectNorthEast, reqVO.getRectNorthEast()) |
|||
.eqIfPresent(FactoryDO::getSortOrder, reqVO.getSortOrder()) |
|||
.eqIfPresent(FactoryDO::getRemark, reqVO.getRemark()) |
|||
.orderByDesc(FactoryDO::getId)); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,30 @@ |
|||
package cn.iocoder.yudao.module.hand.mapper; |
|||
|
|||
|
|||
import cn.iocoder.yudao.module.hand.vo.HandOriginalLog; |
|||
import cn.iocoder.yudao.module.hand.vo.HandTdenginePageVO; |
|||
import cn.iocoder.yudao.module.hand.vo.TdengineDataVo; |
|||
import com.baomidou.mybatisplus.annotation.InterceptorIgnore; // 确保导入这个注解
|
|||
import com.baomidou.mybatisplus.core.metadata.IPage; |
|||
import org.apache.ibatis.annotations.Mapper; |
|||
import org.apache.ibatis.annotations.Param; |
|||
|
|||
import java.util.List; |
|||
|
|||
|
|||
@Mapper |
|||
public interface TdengineMapper { |
|||
|
|||
@InterceptorIgnore(tenantLine = "true") |
|||
void insertHandLogBatch(@Param("sn") String sn,@Param("tenantId")Long tenantId, |
|||
@Param("logList") List<HandOriginalLog> groupedLogs); |
|||
|
|||
|
|||
@InterceptorIgnore(tenantLine = "true") |
|||
void saveDataLogBatch(@Param("sn")String sn,@Param("tenantId")Long tenantId, |
|||
@Param("dataVoList")List<TdengineDataVo> dataVoList); |
|||
|
|||
IPage<TdengineDataVo> selectPage(IPage<TdengineDataVo> page,@Param("vo") HandTdenginePageVO pageReqVO); |
|||
|
|||
IPage<HandOriginalLog> selectOriginalPage(IPage<HandOriginalLog> page,@Param("vo") HandTdenginePageVO pageReqVO); |
|||
} |
@ -0,0 +1,64 @@ |
|||
package cn.iocoder.yudao.module.hand.service; |
|||
|
|||
import java.util.*; |
|||
|
|||
import cn.iocoder.yudao.module.hand.dal.FactoryDO; |
|||
import cn.iocoder.yudao.module.hand.vo.FactoryPageReqVO; |
|||
import cn.iocoder.yudao.module.hand.vo.FactorySaveReqVO; |
|||
import jakarta.validation.*; |
|||
import cn.iocoder.yudao.framework.common.pojo.PageResult; |
|||
import cn.iocoder.yudao.framework.common.pojo.PageParam; |
|||
|
|||
/** |
|||
* GAS工厂 Service 接口 |
|||
* |
|||
* @author 超级管理员 |
|||
*/ |
|||
public interface FactoryService { |
|||
|
|||
/** |
|||
* 创建GAS工厂 |
|||
* |
|||
* @param createReqVO 创建信息 |
|||
* @return 编号 |
|||
*/ |
|||
Long createFactory(@Valid FactorySaveReqVO createReqVO); |
|||
|
|||
/** |
|||
* 更新GAS工厂 |
|||
* |
|||
* @param updateReqVO 更新信息 |
|||
*/ |
|||
void updateFactory(@Valid FactorySaveReqVO updateReqVO); |
|||
|
|||
/** |
|||
* 删除GAS工厂 |
|||
* |
|||
* @param id 编号 |
|||
*/ |
|||
void deleteFactory(Long id); |
|||
|
|||
/** |
|||
* 批量删除GAS工厂 |
|||
* |
|||
* @param ids 编号 |
|||
*/ |
|||
void deleteFactoryListByIds(List<Long> ids); |
|||
|
|||
/** |
|||
* 获得GAS工厂 |
|||
* |
|||
* @param id 编号 |
|||
* @return GAS工厂 |
|||
*/ |
|||
FactoryDO getFactory(Long id); |
|||
|
|||
/** |
|||
* 获得GAS工厂分页 |
|||
* |
|||
* @param pageReqVO 分页查询 |
|||
* @return GAS工厂分页 |
|||
*/ |
|||
PageResult<FactoryDO> getFactoryPage(FactoryPageReqVO pageReqVO); |
|||
|
|||
} |
@ -0,0 +1,20 @@ |
|||
package cn.iocoder.yudao.module.hand.service; |
|||
|
|||
import cn.iocoder.yudao.framework.common.pojo.PageResult; |
|||
import cn.iocoder.yudao.module.hand.vo.HandOriginalLog; |
|||
import cn.iocoder.yudao.module.hand.vo.HandTdenginePageVO; |
|||
import cn.iocoder.yudao.module.hand.vo.TdengineDataVo; |
|||
|
|||
import java.util.List; |
|||
|
|||
public interface TdengineService { |
|||
|
|||
void saveHandLogBatch(List<HandOriginalLog> batchList); |
|||
|
|||
|
|||
void saveDataLogBatch(List<TdengineDataVo> batchList); |
|||
|
|||
PageResult<TdengineDataVo> getHandDataLog(HandTdenginePageVO pageReqVO); |
|||
|
|||
PageResult<HandOriginalLog> getOriginalLogPage(HandTdenginePageVO pageReqVO); |
|||
} |
@ -0,0 +1,88 @@ |
|||
package cn.iocoder.yudao.module.hand.service.impl; |
|||
|
|||
import cn.hutool.core.collection.CollUtil; |
|||
import cn.iocoder.yudao.module.hand.dal.FactoryDO; |
|||
import cn.iocoder.yudao.module.hand.mapper.FactoryMapper; |
|||
import cn.iocoder.yudao.module.hand.service.FactoryService; |
|||
import cn.iocoder.yudao.module.hand.vo.FactoryPageReqVO; |
|||
import cn.iocoder.yudao.module.hand.vo.FactorySaveReqVO; |
|||
import org.springframework.stereotype.Service; |
|||
import jakarta.annotation.Resource; |
|||
import org.springframework.validation.annotation.Validated; |
|||
import org.springframework.transaction.annotation.Transactional; |
|||
|
|||
import java.util.*; |
|||
|
|||
import cn.iocoder.yudao.framework.common.pojo.PageResult; |
|||
import cn.iocoder.yudao.framework.common.pojo.PageParam; |
|||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
|||
|
|||
|
|||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
|||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; |
|||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList; |
|||
import static cn.iocoder.yudao.module.hand.enums.ErrorCodeConstants.FACTORY_NOT_EXISTS; |
|||
|
|||
/** |
|||
* GAS工厂 Service 实现类 |
|||
* |
|||
* @author 超级管理员 |
|||
*/ |
|||
@Service |
|||
@Validated |
|||
public class FactoryServiceImpl implements FactoryService { |
|||
|
|||
@Resource |
|||
private FactoryMapper factoryMapper; |
|||
|
|||
@Override |
|||
public Long createFactory(FactorySaveReqVO createReqVO) { |
|||
// 插入
|
|||
FactoryDO factory = BeanUtils.toBean(createReqVO, FactoryDO.class); |
|||
factoryMapper.insert(factory); |
|||
|
|||
// 返回
|
|||
return factory.getId(); |
|||
} |
|||
|
|||
@Override |
|||
public void updateFactory(FactorySaveReqVO updateReqVO) { |
|||
// 校验存在
|
|||
validateFactoryExists(updateReqVO.getId()); |
|||
// 更新
|
|||
FactoryDO updateObj = BeanUtils.toBean(updateReqVO, FactoryDO.class); |
|||
factoryMapper.updateById(updateObj); |
|||
} |
|||
|
|||
@Override |
|||
public void deleteFactory(Long id) { |
|||
// 校验存在
|
|||
validateFactoryExists(id); |
|||
// 删除
|
|||
factoryMapper.deleteById(id); |
|||
} |
|||
|
|||
@Override |
|||
public void deleteFactoryListByIds(List<Long> ids) { |
|||
// 删除
|
|||
factoryMapper.deleteByIds(ids); |
|||
} |
|||
|
|||
|
|||
private void validateFactoryExists(Long id) { |
|||
if (factoryMapper.selectById(id) == null) { |
|||
throw exception(FACTORY_NOT_EXISTS); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public FactoryDO getFactory(Long id) { |
|||
return factoryMapper.selectById(id); |
|||
} |
|||
|
|||
@Override |
|||
public PageResult<FactoryDO> getFactoryPage(FactoryPageReqVO pageReqVO) { |
|||
return factoryMapper.selectPage(pageReqVO); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,100 @@ |
|||
package cn.iocoder.yudao.module.hand.service.impl; |
|||
|
|||
import cn.iocoder.yudao.framework.common.pojo.PageResult; |
|||
import cn.iocoder.yudao.module.hand.mapper.TdengineMapper; |
|||
import cn.iocoder.yudao.module.hand.service.TdengineService; |
|||
import cn.iocoder.yudao.module.hand.vo.HandOriginalLog; |
|||
import cn.iocoder.yudao.module.hand.vo.HandTdenginePageVO; |
|||
import cn.iocoder.yudao.module.hand.vo.TdengineDataVo; |
|||
import com.baomidou.dynamic.datasource.annotation.DS; |
|||
import com.baomidou.mybatisplus.core.metadata.IPage; |
|||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
|||
import jakarta.annotation.Resource; |
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.validation.annotation.Validated; |
|||
|
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.stream.Collectors; |
|||
|
|||
@Service |
|||
@Validated |
|||
@DS("tdengine") |
|||
public class TdengineServiceImpl implements TdengineService { |
|||
|
|||
private static final Logger log = LoggerFactory.getLogger(TdengineServiceImpl.class); |
|||
@Resource |
|||
private TdengineMapper tdengineMapper; |
|||
|
|||
@Override |
|||
public void saveHandLogBatch(List<HandOriginalLog> batchList) { |
|||
|
|||
if (batchList.isEmpty()) { |
|||
return; |
|||
} |
|||
// 1. 按设备SN分组
|
|||
Map<String, List<HandOriginalLog>> groupedBySn = batchList.stream() |
|||
.collect(Collectors.groupingBy(HandOriginalLog::getSn)); |
|||
|
|||
// 遍历Map,每个Entry对应一个子表
|
|||
for (Map.Entry<String, List<HandOriginalLog>> entry : groupedBySn.entrySet()) { |
|||
String sn = entry.getKey(); |
|||
List<HandOriginalLog> logs = entry.getValue(); |
|||
|
|||
Long tenantId = logs.get(0).getTenantId(); |
|||
tdengineMapper.insertHandLogBatch(sn, tenantId, logs); |
|||
|
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void saveDataLogBatch(List<TdengineDataVo> batchList) { |
|||
if (batchList.isEmpty()) { |
|||
return; |
|||
} |
|||
// 1. 按设备SN分组
|
|||
Map<String, List<TdengineDataVo>> groupedBySn = batchList.stream() |
|||
.collect(Collectors.groupingBy(TdengineDataVo::getSn)); |
|||
|
|||
// 遍历Map,每个Entry对应一个子表
|
|||
for (Map.Entry<String, List<TdengineDataVo>> entry : groupedBySn.entrySet()) { |
|||
String sn = entry.getKey(); |
|||
List<TdengineDataVo> logs = entry.getValue(); |
|||
Long tenantId = logs.get(0).getTenantId(); |
|||
|
|||
tdengineMapper.saveDataLogBatch(sn, tenantId, logs); |
|||
|
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public PageResult<TdengineDataVo> getHandDataLog(HandTdenginePageVO pageReqVO) { |
|||
IPage<TdengineDataVo> page = new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize()); |
|||
|
|||
IPage<TdengineDataVo> resultPage = tdengineMapper.selectPage(page, pageReqVO); |
|||
|
|||
List<TdengineDataVo> doList = resultPage.getRecords(); |
|||
|
|||
if (doList == null || doList.isEmpty()) { |
|||
return PageResult.empty(resultPage.getTotal()); |
|||
} |
|||
|
|||
return new PageResult<>(doList, resultPage.getTotal()); |
|||
} |
|||
|
|||
@Override |
|||
public PageResult<HandOriginalLog> getOriginalLogPage(HandTdenginePageVO pageReqVO) { |
|||
IPage<HandOriginalLog> page = new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize()); |
|||
|
|||
IPage<HandOriginalLog> resultPage = tdengineMapper.selectOriginalPage(page, pageReqVO); |
|||
|
|||
List<HandOriginalLog> doList = resultPage.getRecords(); |
|||
if (doList == null || doList.isEmpty()) { |
|||
return PageResult.empty(resultPage.getTotal()); |
|||
} |
|||
|
|||
return new PageResult<>(doList, resultPage.getTotal()); |
|||
} |
|||
} |
@ -0,0 +1,76 @@ |
|||
package cn.iocoder.yudao.module.hand.util; |
|||
|
|||
public class BatteryConverterUtils { |
|||
|
|||
// 定义电压-电量映射点 (电压单位: 毫伏 mV)
|
|||
// 这些值是基于常见的单节锂电池放电曲线估算的
|
|||
// [电压, 电量百分比]
|
|||
private static final int[][] VOLTAGE_MAP = { |
|||
{4200, 100}, // 充满电
|
|||
{4150, 95}, |
|||
{4100, 90}, |
|||
{4050, 85}, |
|||
{4000, 80}, |
|||
{3950, 75}, |
|||
{3900, 70}, |
|||
{3850, 65}, |
|||
{3800, 60}, |
|||
{3750, 50}, |
|||
{3700, 40}, |
|||
{3650, 30}, |
|||
{3600, 20}, |
|||
{3500, 10}, |
|||
{3400, 5}, |
|||
{3300, 0} // 电量耗尽
|
|||
}; |
|||
|
|||
/** |
|||
* 将电池电压 (mV) 转换为电量百分比 (%) |
|||
* |
|||
* @param millivolts 当前电池电压,单位为毫伏 |
|||
* @return 估算的电量百分比 (0-100) |
|||
*/ |
|||
public static int getBatteryPercentage(int millivolts) { |
|||
// 处理边界情况
|
|||
if (millivolts >= VOLTAGE_MAP[0][0]) { |
|||
return 100; |
|||
} |
|||
if (millivolts <= VOLTAGE_MAP[VOLTAGE_MAP.length - 1][0]) { |
|||
return 0; |
|||
} |
|||
|
|||
// 查找电压所在的区间
|
|||
for (int i = 0; i < VOLTAGE_MAP.length - 1; i++) { |
|||
int upperVoltage = VOLTAGE_MAP[i][0]; |
|||
int upperPercentage = VOLTAGE_MAP[i][1]; |
|||
int lowerVoltage = VOLTAGE_MAP[i + 1][0]; |
|||
int lowerPercentage = VOLTAGE_MAP[i + 1][1]; |
|||
|
|||
if (millivolts <= upperVoltage && millivolts > lowerVoltage) { |
|||
// 在找到的区间内进行线性插值计算
|
|||
// 公式: y = y1 + (x - x1) * (y2 - y1) / (x2 - x1)
|
|||
// 这里: x=millivolts, x1=lowerVoltage, x2=upperVoltage
|
|||
// y=percentage, y1=lowerPercentage, y2=upperPercentage
|
|||
double percentage = lowerPercentage + |
|||
((double)(millivolts - lowerVoltage) * (upperPercentage - lowerPercentage)) / |
|||
(double)(upperVoltage - lowerVoltage); |
|||
return (int) Math.round(percentage); |
|||
} |
|||
} |
|||
|
|||
// 如果出现意外情况,返回0
|
|||
return 0; |
|||
} |
|||
|
|||
public static void main(String[] args) { |
|||
int batteryVoltage = 4126; // 输入您设备的值
|
|||
int percentage = getBatteryPercentage(batteryVoltage); |
|||
System.out.println("电压 " + batteryVoltage + "mV 约等于 " + percentage + "% 的电量。"); |
|||
|
|||
// 其他测试用例
|
|||
System.out.println("电压 4200mV 约等于 " + getBatteryPercentage(4200) + "% 的电量。"); |
|||
System.out.println("电压 3750mV 约等于 " + getBatteryPercentage(3750) + "% 的电量。"); |
|||
System.out.println("电压 3300mV 约等于 " + getBatteryPercentage(3300) + "% 的电量。"); |
|||
System.out.println("电压 3980mV 约等于 " + getBatteryPercentage(3980) + "% 的电量。"); |
|||
} |
|||
} |
@ -0,0 +1,188 @@ |
|||
package cn.iocoder.yudao.module.hand.util; |
|||
|
|||
|
|||
import java.math.BigDecimal; |
|||
import java.math.MathContext; |
|||
import java.math.RoundingMode; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
import java.util.Optional; |
|||
import java.util.function.Consumer; |
|||
import java.util.function.Supplier; |
|||
|
|||
public class CoordinateTransferUtils { |
|||
/** |
|||
* 国内坐标边界 |
|||
*/ |
|||
private static final double MIN_LON = 72.004D; |
|||
private static final double MAX_LON = 137.8347D; |
|||
private static final double MIN_LAT = 0.8293D; |
|||
private static final double MAX_LAT = 55.8271D; |
|||
|
|||
/** |
|||
* PI 圆周率 |
|||
*/ |
|||
private static final double PI = 3.14159265358979324D; |
|||
|
|||
/** |
|||
* A WGS 长轴半径 |
|||
*/ |
|||
private static final double A = 6378245.0D; |
|||
|
|||
/** |
|||
* EE WGS 偏心率的平方 |
|||
*/ |
|||
private static final double EE = 0.00669342162296594323D; |
|||
|
|||
|
|||
|
|||
/** |
|||
* WGS84转换GCJ02核心方法 |
|||
* |
|||
* @param fromLon 转换前的经度 |
|||
* @param fromLat 转换前的纬度 |
|||
* @return 转换后的经纬度map对象 |
|||
*/ |
|||
public static Map<String, Double> wgs84ToGcj02(double fromLon, double fromLat) { |
|||
HashMap<String, Double> transformRes = new HashMap<>(2); |
|||
// 国外坐标不用进行加密
|
|||
if (outOfChina(fromLon, fromLat)) { |
|||
transformRes.put("lon", fromLon); |
|||
transformRes.put("lat", fromLat); |
|||
return transformRes; |
|||
} |
|||
// 计算转换后的经纬度坐标
|
|||
double dLat = transformLat(fromLon - 105.0, fromLat - 35.0); |
|||
double dLon = transformLon(fromLon - 105.0, fromLat - 35.0); |
|||
double radLat = fromLat / 180.0 * PI; |
|||
double magic = Math.sin(radLat); |
|||
magic = 1 - EE * magic * magic; |
|||
double sqrtMagic = Math.sqrt(magic); |
|||
dLat = (dLat * 180.0) / ((A * (1 - EE)) / (magic * sqrtMagic) * PI); |
|||
dLon = (dLon * 180.0) / (A / sqrtMagic * Math.cos(radLat) * PI); |
|||
double mgLat = fromLat + dLat; |
|||
double mgLon = fromLon + dLon; |
|||
transformRes.put("lon", new BigDecimal(mgLon + "", new MathContext(9, RoundingMode.HALF_UP)).doubleValue()); |
|||
transformRes.put("lat", new BigDecimal(mgLat + "", new MathContext(9, RoundingMode.HALF_UP)).doubleValue()); |
|||
|
|||
return transformRes; |
|||
} |
|||
|
|||
/** |
|||
* GCJ02转换WGS84 |
|||
* |
|||
* @param lon 转换前的经度 |
|||
* @param lat 转换后的纬度 |
|||
* @return 转换后的经纬度map对象 |
|||
*/ |
|||
public static Map<String, Double> gcj02ToWgs84(double lon, double lat) { |
|||
Map<String, Double> transformRes = new HashMap<>(2); |
|||
double longitude = lon * 2 - wgs84ToGcj02(lon, lat).get("lon"); |
|||
double latitude = lat * 2 - wgs84ToGcj02(lon, lat).get("lat"); |
|||
transformRes.put("lon", longitude); |
|||
transformRes.put("lat", latitude); |
|||
return transformRes; |
|||
} |
|||
|
|||
/** |
|||
* GCJ02转换BD09 |
|||
* |
|||
* @param gcjLat GCL纬度坐标 |
|||
* @param gcjLng GCL经度坐标 |
|||
*/ |
|||
public static Map<String, Double> Gcj02ToBd09(double gcjLat, double gcjLng) { |
|||
Map<String, Double> transformRes = new HashMap<>(2); |
|||
double z = Math.sqrt(gcjLng * gcjLng + gcjLat * gcjLat) + 0.00002 * Math.sin(gcjLat * PI); |
|||
double theta = Math.atan2(gcjLat, gcjLng) + 0.000003 * Math.cos(gcjLng * PI); |
|||
transformRes.put("lon", z * Math.cos(theta) + 0.0065); |
|||
transformRes.put("lat", z * Math.sin(theta) + 0.006); |
|||
return transformRes; |
|||
} |
|||
|
|||
/** |
|||
* BD09转换GCJ02 |
|||
* |
|||
* @param bdLat 百度纬度坐标 |
|||
* @param bdLng 百度经度坐标 |
|||
*/ |
|||
public static Map<String, Double> bd09ToGcj02(double bdLat, double bdLng) { |
|||
Map<String, Double> transformRes = new HashMap<>(2); |
|||
double x = bdLng - 0.0065, y = bdLat - 0.006; |
|||
double z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * PI); |
|||
double theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * PI); |
|||
|
|||
transformRes.put("lon", z * Math.cos(theta)); |
|||
transformRes.put("lat", z * Math.sin(theta)); |
|||
return transformRes; |
|||
} |
|||
|
|||
/** |
|||
* WGS84转换GCJ02优化(处理转换前经纬度坐标为null问题) |
|||
* |
|||
* @param fromLon 转换前的经度 |
|||
* @param fromLat 转换前的纬度 |
|||
* @param toLon 转换后的经度 |
|||
* @param toLat 转换后的纬度 |
|||
*/ |
|||
private static void wgs84ToGcj02(Supplier<BigDecimal> fromLon, Supplier<BigDecimal> fromLat, Consumer<BigDecimal> toLon, Consumer<BigDecimal> toLat) { |
|||
wgs84ToGcj02(Optional.ofNullable(fromLon.get()).orElse(BigDecimal.ZERO), Optional.ofNullable(fromLat.get()).orElse(BigDecimal.ZERO), toLon, toLat); |
|||
} |
|||
|
|||
/** |
|||
* WGS84转换GCJ02优化(处理转换后经纬度坐标) |
|||
* |
|||
* @param fromLon 转换前的经度 |
|||
* @param fromLat 转换前的纬度 |
|||
* @param toLon 转换后的经度 |
|||
* @param toLat 转换后的纬度 |
|||
*/ |
|||
private static void wgs84ToGcj02(BigDecimal fromLon, BigDecimal fromLat, Consumer<BigDecimal> toLon, Consumer<BigDecimal> toLat) { |
|||
final Map<String, Double> transformRes = wgs84ToGcj02(fromLon.doubleValue(), fromLat.doubleValue()); |
|||
toLon.accept(new BigDecimal(transformRes.get("lon") + "")); |
|||
toLat.accept(new BigDecimal(transformRes.get("lat") + "")); |
|||
} |
|||
|
|||
/** |
|||
* 坐标是否在国外 |
|||
* |
|||
* @param lon 经度 |
|||
* @param lat 纬度 |
|||
* @return true 国外坐标 false 国内坐标 |
|||
*/ |
|||
private static boolean outOfChina(double lon, double lat) { |
|||
if (lon < MIN_LON || lon > MAX_LON) { |
|||
return true; |
|||
} |
|||
return lat < MIN_LAT || lat > MAX_LAT; |
|||
} |
|||
|
|||
/** |
|||
* 转换经度坐标 |
|||
* |
|||
* @param x 偏移后的经度 |
|||
* @param y 偏移后的纬度 |
|||
* @return double 转换经度坐标 |
|||
*/ |
|||
private static double transformLat(double x, double y) { |
|||
double transform = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x)); |
|||
transform += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0; |
|||
transform += (20.0 * Math.sin(y * PI) + 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0; |
|||
transform += (160.0 * Math.sin(y / 12.0 * PI) + 320 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0; |
|||
return transform; |
|||
} |
|||
/** |
|||
* 转换纬度坐标 |
|||
* |
|||
* @param x 偏移后的经度 |
|||
* @param y 偏移后的纬度 |
|||
* @return double 转换纬度坐标 |
|||
*/ |
|||
private static double transformLon(double x, double y) { |
|||
double transform = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x)); |
|||
transform += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0; |
|||
transform += (20.0 * Math.sin(x * PI) + 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0; |
|||
transform += (150.0 * Math.sin(x / 12.0 * PI) + 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0; |
|||
return transform; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,284 @@ |
|||
package cn.iocoder.yudao.module.hand.util; |
|||
|
|||
import cn.iocoder.yudao.module.hand.enums.FenceType; |
|||
import cn.iocoder.yudao.module.hand.vo.FencePointVo; |
|||
import cn.iocoder.yudao.module.hand.vo.Geofence; |
|||
import cn.iocoder.yudao.module.hand.vo.HandDataVo; |
|||
import com.google.gson.Gson; |
|||
import com.google.gson.JsonSyntaxException; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.Collections; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 高性能地理围栏计算工具类 (Optimized Geofence Calculation Utility) |
|||
* |
|||
* <p>核心优化:</p> |
|||
* <ol> |
|||
* <li><b>引入 Geofence 对象:</b> 封装了多边形顶点及其预计算的“包围盒”(Bounding Box)。</li> |
|||
* <li><b>包围盒快速排斥:</b> 在进行昂贵的精确计算前,先进行廉价的矩形范围判断,能过滤掉绝大多数不相关的围栏。</li> |
|||
* <li><b>距离计算剪枝 (Pruning):</b> 在计算最短距离时,利用点到包围盒的距离,提前跳过那些不可能存在最短距离的围栏。</li> |
|||
* </ol> |
|||
* |
|||
* <p>使用流程:</p> |
|||
* <pre>{@code |
|||
* // 1. 从JSON或其他数据源创建Geofence列表(这个过程会自动计算包围盒)
|
|||
* List<GeofenceUtils.Geofence> geofences = GeofenceUtils.parseFences(fenceDO.getFenceRange()); |
|||
* |
|||
* // 2. 使用优化后的方法进行高效计算
|
|||
* boolean isInside = GeofenceUtils.isInsideAnyFence(longitude, latitude, geofences); |
|||
* double distance = GeofenceUtils.calculateDistanceToFences(longitude, latitude, geofences); |
|||
* }</pre> |
|||
*/ |
|||
@Slf4j |
|||
public final class GeofenceUtils { |
|||
|
|||
private static final double METERS_PER_DEGREE_LATITUDE = 111132.954; |
|||
private static final Gson GSON_INSTANCE = new Gson(); |
|||
// 私有构造函数,防止实例化工具类
|
|||
private GeofenceUtils() { |
|||
} |
|||
|
|||
/** |
|||
* 从JSON字符串解析并创建Geofence对象列表。 |
|||
* 这是推荐的初始化围栏数据的方式。 |
|||
* |
|||
* @param fenceRangeJson 符合`double[][][]`格式的JSON字符串 |
|||
* @return Geofence对象列表,如果解析失败或无有效多边形则返回空列表 |
|||
*/ |
|||
public static List<Geofence> parseFences(String fenceRangeJson) { |
|||
if (fenceRangeJson == null || fenceRangeJson.trim().isEmpty()) { |
|||
return Collections.emptyList(); |
|||
} |
|||
try { |
|||
List<Geofence> geofences = new ArrayList<>(); |
|||
double[][][] coordinates = GSON_INSTANCE.fromJson(fenceRangeJson, double[][][].class); |
|||
if (coordinates == null) return Collections.emptyList(); |
|||
|
|||
for (double[][] polygonCoordinates : coordinates) { |
|||
List<FencePointVo> polygon = new ArrayList<>(); |
|||
for (double[] coordinate : polygonCoordinates) { |
|||
polygon.add(new FencePointVo(coordinate[0], coordinate[1])); |
|||
} |
|||
// 确保多边形有效才创建Geofence对象
|
|||
if (polygon.size() >= 3) { |
|||
geofences.add(new Geofence(polygon)); |
|||
} |
|||
} |
|||
return geofences; |
|||
} catch (JsonSyntaxException e) { |
|||
// 在实际项目中,这里应该记录日志
|
|||
log.error("解析围栏JSON失败{} ", e.getMessage()); |
|||
return Collections.emptyList(); |
|||
} |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 检查点是否在任何一个电子围栏内部(高性能版)。 |
|||
* |
|||
* @param x 点的经度 |
|||
* @param y 点的纬度 |
|||
* @param geofences Geofence对象列表 |
|||
* @return 如果点在任何一个围栏内,返回 true |
|||
*/ |
|||
public static boolean isInsideAnyFence(double x, double y, List<Geofence> geofences) { |
|||
if (geofences == null || geofences.isEmpty()) { |
|||
return false; |
|||
} |
|||
for (Geofence fence : geofences) { |
|||
// 第一步:快速包围盒排斥。如果点不在包围盒内,则绝不可能在多边形内。
|
|||
if (fence.isPointInBoundingBox(x, y)) { |
|||
// 第二步:通过包围盒测试后,再进行精确的多边形判断。
|
|||
if (isPointInsidePolygon(x, y, fence.getVertices())) { |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* 计算点到一组围栏边界的最短距离(高性能版)。 |
|||
* |
|||
* @param x 点的经度 |
|||
* @param y 点的纬度 |
|||
* @param geofences Geofence对象列表 |
|||
* @return 点到所有围栏边界的最短距离(米) |
|||
*/ |
|||
public static double calculateDistanceToFences(double x, double y, List<Geofence> geofences) { |
|||
if (geofences == null || geofences.isEmpty()) { |
|||
return Double.POSITIVE_INFINITY; |
|||
} |
|||
// 预先计算一次转换因子
|
|||
double metersPerDegreeLongitude = METERS_PER_DEGREE_LATITUDE * Math.cos(Math.toRadians(y)); |
|||
double minDistance = Double.MAX_VALUE; |
|||
|
|||
// 优化:先检查是否在内部,如果在则直接返回0
|
|||
if (isInsideAnyFence(x, y, geofences)) { |
|||
return 0.0; |
|||
} |
|||
|
|||
for (Geofence fence : geofences) { |
|||
// *** 核心优化:剪枝 (Pruning) ***
|
|||
// 计算点到该围栏包围盒的距离。
|
|||
double distToBox = pointToBoundingBoxDistanceInMeters(x, y, fence, metersPerDegreeLongitude); |
|||
// 如果点到包围盒的距离已经比当前找到的最小距离还要大,
|
|||
// 那么这个围栏内的任何一条边都不可能提供更短的距离,直接跳过。
|
|||
if (distToBox >= minDistance) { |
|||
continue; |
|||
} |
|||
|
|||
// 通过剪枝测试后,再对这个围栏的每条边进行精确计算
|
|||
List<FencePointVo> polygon = fence.getVertices(); |
|||
for (int i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) { |
|||
FencePointVo p1 = polygon.get(i); |
|||
FencePointVo p2 = polygon.get(j); |
|||
double distance = pointToLineSegmentDistanceInMeters(x, y, p1, p2, metersPerDegreeLongitude); |
|||
minDistance = Math.min(minDistance, distance); |
|||
} |
|||
} |
|||
return minDistance; |
|||
} |
|||
|
|||
/** |
|||
* 核心判断逻辑:点是否在单个多边形内部(射线法)。 |
|||
*/ |
|||
private static boolean isPointInsidePolygon(double x, double y, List<FencePointVo> polygon) { |
|||
if (isPointOnPolygonBoundary(x, y, polygon)) { |
|||
return true; |
|||
} |
|||
int intersectCount = 0; |
|||
for (int i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) { |
|||
FencePointVo p1 = polygon.get(i); |
|||
FencePointVo p2 = polygon.get(j); |
|||
if ((p1.y > y) != (p2.y > y)) { |
|||
double xIntersection = (p2.x - p1.x) * (y - p1.y) / (p2.y - p1.y) + p1.x; |
|||
if (x < xIntersection) { |
|||
intersectCount++; |
|||
} |
|||
} |
|||
} |
|||
return (intersectCount % 2) == 1; |
|||
} |
|||
|
|||
/** |
|||
* 判断点是否在多边形的边界上。 |
|||
*/ |
|||
private static boolean isPointOnPolygonBoundary(double x, double y, List<FencePointVo> polygon) { |
|||
for (int i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) { |
|||
FencePointVo p1 = polygon.get(i); |
|||
FencePointVo p2 = polygon.get(j); |
|||
double crossProduct = (y - p1.y) * (p2.x - p1.x) - (x - p1.x) * (p2.y - p1.y); |
|||
if (Math.abs(crossProduct) < 1E-9) { // 容忍浮点误差
|
|||
if (Math.min(p1.x, p2.x) <= x && x <= Math.max(p1.x, p2.x) && |
|||
Math.min(p1.y, p2.y) <= y && y <= Math.max(p1.y, p2.y)) { |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* 计算点到线段的最短距离(米)。 |
|||
*/ |
|||
private static double pointToLineSegmentDistanceInMeters(double px, double py, FencePointVo p1, FencePointVo p2, double metersPerDegreeLongitude) { |
|||
double p2_m_x = (p2.x - p1.x) * metersPerDegreeLongitude; |
|||
double p2_m_y = (p2.y - p1.y) * METERS_PER_DEGREE_LATITUDE; |
|||
double p_m_x = (px - p1.x) * metersPerDegreeLongitude; |
|||
double p_m_y = (py - p1.y) * METERS_PER_DEGREE_LATITUDE; |
|||
double lineLenSq = p2_m_x * p2_m_x + p2_m_y * p2_m_y; |
|||
if (lineLenSq == 0) { |
|||
return Math.sqrt(p_m_x * p_m_x + p_m_y * p_m_y); |
|||
} |
|||
double t = Math.max(0, Math.min(1, ((p_m_x * p2_m_x) + (p_m_y * p_m_y)) / lineLenSq)); |
|||
double nearestX = t * p2_m_x; |
|||
double nearestY = t * p2_m_y; |
|||
double dx = p_m_x - nearestX; |
|||
double dy = p_m_y - nearestY; |
|||
return Math.sqrt(dx * dx + dy * dy); |
|||
} |
|||
|
|||
/** |
|||
* 计算点到包围盒的最短距离(米),用于剪枝。 |
|||
*/ |
|||
private static double pointToBoundingBoxDistanceInMeters(double px, double py, Geofence fence, double metersPerDegreeLongitude) { |
|||
double dx = 0.0, dy = 0.0; |
|||
if (px < fence.getMaxX()) dx = fence.getMaxX() - px; |
|||
else if (px > fence.getMaxX()) dx = px - fence.getMaxX(); |
|||
if (py < fence.getMinY()) dy = fence.getMinY() - py; |
|||
else if (py > fence.getMinY()) dy = py - fence.getMinY(); |
|||
|
|||
// 将经纬度差转换为米
|
|||
double dx_meters = dx * metersPerDegreeLongitude; |
|||
double dy_meters = dy * METERS_PER_DEGREE_LATITUDE; |
|||
|
|||
return Math.sqrt(dx_meters * dx_meters + dy_meters * dy_meters); |
|||
} |
|||
|
|||
/** |
|||
* 计算一个点到一组围栏最近边界的“物理”距离(单位:米)。 |
|||
* |
|||
* <p><b>与 calculateDistanceToFences 的关键区别:</b></p> |
|||
* <p>此方法 <b>总是</b> 会计算并返回点到最近边界的实际最短距离, |
|||
* <b>无论这个点是在围栏的内部还是外部。</b></p> |
|||
* |
|||
* <p>例如,如果点在围栏内部,它会返回一个大于0的值,表示该点距离“逃出”围栏的最近距离。</p> |
|||
* |
|||
* @param x 点的经度 |
|||
* @param y 点的纬度 |
|||
* @param geofences Geofence对象列表 |
|||
* @return 点到所有围栏边界的物理最短距离(米)。 |
|||
*/ |
|||
public static double calculateDistanceToNearestBoundary(double x, double y, List<Geofence> geofences) { |
|||
if (geofences == null || geofences.isEmpty()) { |
|||
return Double.POSITIVE_INFINITY; |
|||
} |
|||
|
|||
double minDistance = Double.MAX_VALUE; |
|||
// 预先计算一次转换因子
|
|||
double metersPerDegreeLongitude = METERS_PER_DEGREE_LATITUDE * Math.cos(Math.toRadians(y)); |
|||
|
|||
for (Geofence fence : geofences) { |
|||
// 剪枝优化对于外部点仍然有效,对于内部点虽然效果减弱,但无害且在多围栏场景下依然有用
|
|||
double distToBox = pointToBoundingBoxDistanceInMeters(x, y, fence, metersPerDegreeLongitude); |
|||
if (distToBox >= minDistance) { |
|||
continue; |
|||
} |
|||
|
|||
// 核心逻辑:直接遍历所有边并计算距离
|
|||
List<FencePointVo> polygon = fence.getVertices(); |
|||
for (int i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) { |
|||
FencePointVo p1 = polygon.get(i); |
|||
FencePointVo p2 = polygon.get(j); |
|||
double distance = pointToLineSegmentDistanceInMeters(x, y, p1, p2, metersPerDegreeLongitude); |
|||
minDistance = Math.min(minDistance, distance); |
|||
} |
|||
} |
|||
return minDistance; |
|||
} |
|||
|
|||
/** |
|||
* 计算违规距离。 |
|||
* |
|||
* @param handVo 手部数据对象 |
|||
* @param fenceType 围栏类型 |
|||
* @param fenceList 围栏列表 |
|||
* @return 违规距离(米) |
|||
*/ |
|||
public static double fenceDistance(HandDataVo handVo, FenceType fenceType, List<Geofence> fenceList) { |
|||
if (fenceType == FenceType.INSIDE) { |
|||
// 规则是“必须在内”,违规意味着“在外面”,所以计算点到围栏的外部距离。
|
|||
// 此方法专为外部点设计。calculateDistanceToNearestBoundary
|
|||
return GeofenceUtils.calculateDistanceToNearestBoundary(handVo.getLongitude(), handVo.getLatitude(), fenceList); |
|||
} else { // 规则是OUTSIDE
|
|||
// 违规意味着“在里面”,所以计算点到边界的内部深度。
|
|||
// 此方法能处理内部点。
|
|||
|
|||
return GeofenceUtils.calculateDistanceToFences(handVo.getLongitude(), handVo.getLatitude(), fenceList); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,56 @@ |
|||
package cn.iocoder.yudao.module.hand.util; |
|||
|
|||
import lombok.Data; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.data.redis.connection.RedisConnection; |
|||
import org.springframework.data.redis.connection.RedisConnectionFactory; |
|||
import org.springframework.data.redis.core.*; |
|||
import org.springframework.data.redis.core.script.DefaultRedisScript; |
|||
import org.springframework.data.redis.core.script.RedisScript; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.util.CollectionUtils; |
|||
|
|||
import java.util.*; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
/** |
|||
* redis 工具类 |
|||
* |
|||
* @Author Scott |
|||
*/ |
|||
@Data |
|||
public class RedisKeyUtil { |
|||
|
|||
public static final String REDIS_KEY_PREFIX = "hand_detectorKey"; |
|||
|
|||
public static final String ALARM_RULES_CACHE_NAME = "alarm_rulesKey"; |
|||
|
|||
public static final String DEVICE_TENANT_MAPPING_KEY = "device:tenant:mapping"; |
|||
|
|||
// 基础前缀
|
|||
public static final String DEVICE_INFO_PREFIX = "device:info"; |
|||
|
|||
public static final String LOCK_PREFIX = "lock:device"; |
|||
|
|||
/** |
|||
* 获取特定租户下的设备信息 Hash Key |
|||
* 结构示例: device:info:{tenantId} -> field为sn |
|||
*/ |
|||
public static String getTenantDeviceHashKey(Long tenantId) { |
|||
return DEVICE_INFO_PREFIX + ":" + tenantId; |
|||
} |
|||
|
|||
/** |
|||
* 获取处理锁 Key (加入租户维度更安全) |
|||
* 结构示例: lock:device:{tenantId}:{sn} |
|||
*/ |
|||
public static String getDeviceProcessLockKey(Long tenantId, String sn) { |
|||
return LOCK_PREFIX + ":" + tenantId + ":" + sn; |
|||
} |
|||
|
|||
public static String getDeviceTenantMappingKey() { |
|||
return DEVICE_TENANT_MAPPING_KEY; |
|||
|
|||
} |
|||
} |
@ -0,0 +1,742 @@ |
|||
package cn.iocoder.yudao.module.hand.util; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.data.redis.connection.RedisConnection; |
|||
import org.springframework.data.redis.connection.RedisConnectionFactory; |
|||
import org.springframework.data.redis.core.*; |
|||
import org.springframework.data.redis.core.script.DefaultRedisScript; |
|||
import org.springframework.data.redis.core.script.RedisScript; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.util.CollectionUtils; |
|||
|
|||
import java.util.*; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
/** |
|||
* redis 工具类 |
|||
* @Author Scott |
|||
* |
|||
*/ |
|||
@Component |
|||
@Slf4j |
|||
public class RedisUtil { |
|||
|
|||
@Autowired |
|||
private RedisTemplate<String, Object> redisTemplate; |
|||
@Autowired |
|||
private StringRedisTemplate stringRedisTemplate; |
|||
|
|||
/** |
|||
* 指定缓存失效时间 |
|||
* |
|||
* @param key 键 |
|||
* @param time 时间(秒) |
|||
* @return |
|||
*/ |
|||
public boolean expire(String key, long time) { |
|||
try { |
|||
if (time > 0) { |
|||
redisTemplate.expire(key, time, TimeUnit.SECONDS); |
|||
} |
|||
return true; |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 根据key 获取过期时间 |
|||
* |
|||
* @param key 键 不能为null |
|||
* @return 时间(秒) 返回0代表为永久有效 |
|||
*/ |
|||
public long getExpire(String key) { |
|||
return redisTemplate.getExpire(key, TimeUnit.SECONDS); |
|||
} |
|||
|
|||
/** |
|||
* 判断key是否存在 |
|||
* |
|||
* @param key 键 |
|||
* @return true 存在 false不存在 |
|||
*/ |
|||
public boolean hasKey(String key) { |
|||
try { |
|||
return redisTemplate.hasKey(key); |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 删除缓存 |
|||
* |
|||
* @param key 可以传一个值 或多个 |
|||
*/ |
|||
@SuppressWarnings("unchecked") |
|||
public void del(String... key) { |
|||
if (key != null && key.length > 0) { |
|||
if (key.length == 1) { |
|||
redisTemplate.delete(key[0]); |
|||
} else { |
|||
redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// ============================String=============================
|
|||
/** |
|||
* 普通缓存获取 |
|||
* |
|||
* @param key 键 |
|||
* @return 值 |
|||
*/ |
|||
public Object get(String key) { |
|||
return key == null ? null : redisTemplate.opsForValue().get(key); |
|||
} |
|||
|
|||
/** |
|||
* 普通缓存放入 |
|||
* |
|||
* @param key 键 |
|||
* @param value 值 |
|||
* @return true成功 false失败 |
|||
*/ |
|||
public boolean set(String key, Object value) { |
|||
try { |
|||
redisTemplate.opsForValue().set(key, value); |
|||
return true; |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return false; |
|||
} |
|||
|
|||
} |
|||
|
|||
/** |
|||
* 普通缓存放入并设置时间 |
|||
* |
|||
* @param key 键 |
|||
* @param value 值 |
|||
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 |
|||
* @return true成功 false 失败 |
|||
*/ |
|||
public boolean set(String key, Object value, long time) { |
|||
try { |
|||
if (time > 0) { |
|||
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); |
|||
} else { |
|||
set(key, value); |
|||
} |
|||
return true; |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 递增 |
|||
* |
|||
* @param key 键 |
|||
* @param delta 要增加几(大于0) |
|||
* @return |
|||
*/ |
|||
public long incr(String key, long delta) { |
|||
if (delta < 0) { |
|||
throw new RuntimeException("递增因子必须大于0"); |
|||
} |
|||
return redisTemplate.opsForValue().increment(key, delta); |
|||
} |
|||
|
|||
/** |
|||
* 递减 |
|||
* |
|||
* @param key 键 |
|||
* @param delta 要减少几(小于0) |
|||
* @return |
|||
*/ |
|||
public long decr(String key, long delta) { |
|||
if (delta < 0) { |
|||
throw new RuntimeException("递减因子必须大于0"); |
|||
} |
|||
return redisTemplate.opsForValue().increment(key, -delta); |
|||
} |
|||
|
|||
// ================================Map=================================
|
|||
/** |
|||
* HashGet |
|||
* |
|||
* @param key 键 不能为null |
|||
* @param item 项 不能为null |
|||
* @return 值 |
|||
*/ |
|||
public Object hget(String key, String item) { |
|||
return redisTemplate.opsForHash().get(key, item); |
|||
} |
|||
|
|||
/** |
|||
* 获取hashKey对应的所有键值 |
|||
* |
|||
* @param key 键 |
|||
* @return 对应的多个键值 |
|||
*/ |
|||
public Map<Object, Object> hmget(String key) { |
|||
return redisTemplate.opsForHash().entries(key); |
|||
} |
|||
/** |
|||
* 获取hashKey对应的所有键值 |
|||
* |
|||
* @param key 键 |
|||
* @return 对应的多个键值 |
|||
*/ |
|||
public Object hmget(String key,String item) { |
|||
Map<Object, Object> entries = redisTemplate.opsForHash().entries(key); |
|||
return entries.get(item); |
|||
} |
|||
|
|||
/** |
|||
* HashSet |
|||
* |
|||
* @param key 键 |
|||
* @param map 对应多个键值 |
|||
* @return true 成功 false 失败 |
|||
*/ |
|||
public boolean hmset(String key, Map<Object, Object> map) { |
|||
try { |
|||
redisTemplate.opsForHash().putAll(key, map); |
|||
return true; |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* HashSet 并设置时间 |
|||
* |
|||
* @param key 键 |
|||
* @param map 对应多个键值 |
|||
* @param time 时间(秒) |
|||
* @return true成功 false失败 |
|||
*/ |
|||
public boolean hmset(String key, Map<String, Object> map, long time) { |
|||
try { |
|||
redisTemplate.opsForHash().putAll(key, map); |
|||
if (time > 0) { |
|||
expire(key, time); |
|||
} |
|||
return true; |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 向一张hash表中放入数据,如果不存在将创建 |
|||
* |
|||
* @param key 键 |
|||
* @param item 项 |
|||
* @param value 值 |
|||
* @return true 成功 false失败 |
|||
*/ |
|||
public boolean hset(String key, String item, Object value) { |
|||
try { |
|||
redisTemplate.opsForHash().put(key, item, value); |
|||
return true; |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 向一张hash表中放入数据,如果不存在将创建 |
|||
* |
|||
* @param key 键 |
|||
* @param item 项 |
|||
* @param value 值 |
|||
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间 |
|||
* @return true 成功 false失败 |
|||
*/ |
|||
public boolean hset(String key, String item, Object value, long time) { |
|||
try { |
|||
redisTemplate.opsForHash().put(key, item, value); |
|||
if (time > 0) { |
|||
expire(key, time); |
|||
} |
|||
return true; |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 删除hash表中的值 |
|||
* |
|||
* @param key 键 不能为null |
|||
* @param item 项 可以使多个 不能为null |
|||
*/ |
|||
public void hdel(String key, Object... item) { |
|||
redisTemplate.opsForHash().delete(key, item); |
|||
} |
|||
|
|||
/** |
|||
* 判断hash表中是否有该项的值 |
|||
* |
|||
* @param key 键 不能为null |
|||
* @param item 项 不能为null |
|||
* @return true 存在 false不存在 |
|||
*/ |
|||
public boolean hHasKey(String key, String item) { |
|||
return redisTemplate.opsForHash().hasKey(key, item); |
|||
} |
|||
|
|||
/** |
|||
* hash递增 如果不存在,就会创建一个 并把新增后的值返回 |
|||
* |
|||
* @param key 键 |
|||
* @param item 项 |
|||
* @param by 要增加几(大于0) |
|||
* @return |
|||
*/ |
|||
public double hincr(String key, String item, double by) { |
|||
return redisTemplate.opsForHash().increment(key, item, by); |
|||
} |
|||
|
|||
/** |
|||
* hash递减 |
|||
* |
|||
* @param key 键 |
|||
* @param item 项 |
|||
* @param by 要减少记(小于0) |
|||
* @return |
|||
*/ |
|||
public double hdecr(String key, String item, double by) { |
|||
return redisTemplate.opsForHash().increment(key, item, -by); |
|||
} |
|||
|
|||
// ============================set=============================
|
|||
/** |
|||
* 根据key获取Set中的所有值 |
|||
* |
|||
* @param key 键 |
|||
* @return |
|||
*/ |
|||
public Set<Object> sGet(String key) { |
|||
try { |
|||
return redisTemplate.opsForSet().members(key); |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 根据value从一个set中查询,是否存在 |
|||
* |
|||
* @param key 键 |
|||
* @param value 值 |
|||
* @return true 存在 false不存在 |
|||
*/ |
|||
public boolean sHasKey(String key, Object value) { |
|||
try { |
|||
return redisTemplate.opsForSet().isMember(key, value); |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 将数据放入set缓存 |
|||
* |
|||
* @param key 键 |
|||
* @param values 值 可以是多个 |
|||
* @return 成功个数 |
|||
*/ |
|||
public long sSet(String key, Object... values) { |
|||
try { |
|||
return redisTemplate.opsForSet().add(key, values); |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 将set数据放入缓存 |
|||
* |
|||
* @param key 键 |
|||
* @param time 时间(秒) |
|||
* @param values 值 可以是多个 |
|||
* @return 成功个数 |
|||
*/ |
|||
public long sSetAndTime(String key, long time, Object... values) { |
|||
try { |
|||
Long count = redisTemplate.opsForSet().add(key, values); |
|||
if (time > 0) { |
|||
expire(key, time); |
|||
} |
|||
return count; |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取set缓存的长度 |
|||
* |
|||
* @param key 键 |
|||
* @return |
|||
*/ |
|||
public long sGetSetSize(String key) { |
|||
try { |
|||
return redisTemplate.opsForSet().size(key); |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 移除值为value的 |
|||
* |
|||
* @param key 键 |
|||
* @param values 值 可以是多个 |
|||
* @return 移除的个数 |
|||
*/ |
|||
public long setRemove(String key, Object... values) { |
|||
try { |
|||
Long count = redisTemplate.opsForSet().remove(key, values); |
|||
return count; |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return 0; |
|||
} |
|||
} |
|||
// ===============================list=================================
|
|||
|
|||
/** |
|||
* 获取list缓存的内容 |
|||
* |
|||
* @param key 键 |
|||
* @param start 开始 |
|||
* @param end 结束 0 到 -1代表所有值 |
|||
* @return |
|||
*/ |
|||
public List<Object> lGet(String key, long start, long end) { |
|||
try { |
|||
return redisTemplate.opsForList().range(key, start, end); |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取list缓存的长度 |
|||
* |
|||
* @param key 键 |
|||
* @return |
|||
*/ |
|||
public long lGetListSize(String key) { |
|||
try { |
|||
return redisTemplate.opsForList().size(key); |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 通过索引 获取list中的值 |
|||
* |
|||
* @param key 键 |
|||
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推 |
|||
* @return |
|||
*/ |
|||
public Object lGetIndex(String key, long index) { |
|||
try { |
|||
return redisTemplate.opsForList().index(key, index); |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 将list放入缓存 |
|||
* |
|||
* @param key 键 |
|||
* @param value 值 |
|||
* @return |
|||
*/ |
|||
public boolean lSet(String key, Object value) { |
|||
try { |
|||
redisTemplate.opsForList().rightPush(key, value); |
|||
return true; |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 将list放入缓存 |
|||
* |
|||
* @param key 键 |
|||
* @param value 值 |
|||
* @param time 时间(秒) |
|||
* @return |
|||
*/ |
|||
public boolean lSet(String key, Object value, long time) { |
|||
try { |
|||
redisTemplate.opsForList().rightPush(key, value); |
|||
if (time > 0) { |
|||
expire(key, time); |
|||
} |
|||
return true; |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 将list放入缓存 |
|||
* |
|||
* @param key 键 |
|||
* @param value 值 |
|||
* @return |
|||
*/ |
|||
public boolean lSet(String key, List<Object> value) { |
|||
try { |
|||
redisTemplate.opsForList().rightPushAll(key, value); |
|||
return true; |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return false; |
|||
} |
|||
} |
|||
//
|
|||
// /**
|
|||
// * list缓存全量更新,先删后存
|
|||
// *
|
|||
// * @param key 键
|
|||
// * @param value 值
|
|||
// * @return
|
|||
// */
|
|||
// public boolean lDelAndSet(String key, List<Object> value) {
|
|||
// try {
|
|||
// redisTemplate.execute(new SessionCallback<Object>() {
|
|||
// @Override
|
|||
// public Object execute(RedisOperations operations) throws DataAccessException {
|
|||
// operations.multi();
|
|||
//
|
|||
// operations.delete(key);
|
|||
// redisTemplate.opsForList().rightPushAll(key, value);
|
|||
//
|
|||
// operations.exec();
|
|||
// return null;
|
|||
// }
|
|||
// });
|
|||
// return true;
|
|||
// } catch (Exception e) {
|
|||
// e.printStackTrace();
|
|||
// return false;
|
|||
// }
|
|||
// }
|
|||
|
|||
/** |
|||
* list缓存全量更新,先删后存 |
|||
* |
|||
* @param key 键 |
|||
* @param value 值 |
|||
* @return |
|||
*/ |
|||
public boolean lDelAndSet(String key, List<Object> value) { |
|||
try { |
|||
String script = "redis.call('del', KEYS[1]) " + |
|||
"for i = 1, #ARGV do " + |
|||
" redis.call('rpush', KEYS[1], ARGV[i]) " + |
|||
"end "; |
|||
RedisScript<Void> redisScript = new DefaultRedisScript<>(script, Void.class); |
|||
redisTemplate.execute(redisScript, Collections.singletonList(key), value.toArray()); |
|||
|
|||
return true; |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 将list放入缓存 |
|||
* |
|||
* @param key 键 |
|||
* @param value 值 |
|||
* @param time 时间(秒) |
|||
* @return |
|||
*/ |
|||
public boolean lSet(String key, List<Object> value, long time) { |
|||
try { |
|||
redisTemplate.opsForList().rightPushAll(key, value); |
|||
if (time > 0) { |
|||
expire(key, time); |
|||
} |
|||
return true; |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 根据索引修改list中的某条数据 |
|||
* |
|||
* @param key 键 |
|||
* @param index 索引 |
|||
* @param value 值 |
|||
* @return |
|||
*/ |
|||
public boolean lUpdateIndex(String key, long index, Object value) { |
|||
try { |
|||
redisTemplate.opsForList().set(key, index, value); |
|||
return true; |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 移除N个值为value |
|||
* |
|||
* @param key 键 |
|||
* @param count 移除多少个 |
|||
* @param value 值 |
|||
* @return 移除的个数 |
|||
*/ |
|||
public long lRemove(String key, long count, Object value) { |
|||
try { |
|||
Long remove = redisTemplate.opsForList().remove(key, count, value); |
|||
return remove; |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* removing all values not within the slice between ``start`` and ``end`` |
|||
* |
|||
* @param key 键 |
|||
* @param start 开始 |
|||
* @param end 结束 0 到 -1代表所有值 |
|||
*/ |
|||
public boolean lTrim(String key, long start, long end) { |
|||
try { |
|||
redisTemplate.opsForList().trim(key, start, end); |
|||
return true; |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 查找匹配key |
|||
* |
|||
* @param pattern key |
|||
* @return / |
|||
*/ |
|||
public List<String> scan(String pattern) { |
|||
ScanOptions options = ScanOptions.scanOptions().match(pattern).build(); |
|||
RedisConnectionFactory factory = redisTemplate.getConnectionFactory(); |
|||
RedisConnection rc = Objects.requireNonNull(factory).getConnection(); |
|||
Cursor<byte[]> cursor = rc.scan(options); |
|||
List<String> result = new ArrayList<>(); |
|||
while (cursor.hasNext()) { |
|||
result.add(new String(cursor.next())); |
|||
} |
|||
try { |
|||
RedisConnectionUtils.releaseConnection(rc, factory); |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
// =============================== message queue =================================
|
|||
|
|||
/** |
|||
* 消息入队列 |
|||
* |
|||
* @param queue |
|||
* @param message |
|||
* @return |
|||
*/ |
|||
public Long queuePut(String queue, Object message) { |
|||
try { |
|||
return redisTemplate.opsForList().leftPush(queue, message); |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return 0L; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 消息出队列 |
|||
* |
|||
* @param queue |
|||
* @return |
|||
*/ |
|||
public Object queueGet(String queue) { |
|||
try { |
|||
return redisTemplate.opsForList().rightPop(queue, 1, TimeUnit.SECONDS); |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
// =============================== lock =================================
|
|||
/** |
|||
* 创建锁 |
|||
* |
|||
* @param key 锁的Key |
|||
* @param releaseTime 锁过期时间,防止死锁 |
|||
* @return |
|||
*/ |
|||
public boolean lock(String key, long releaseTime) { |
|||
// 尝试获取锁
|
|||
Boolean success = redisTemplate.opsForValue().setIfAbsent(key, 1, releaseTime, TimeUnit.SECONDS); |
|||
// 判断结果
|
|||
return success != null && success; |
|||
} |
|||
|
|||
/** |
|||
* 根据key释放锁 |
|||
* |
|||
* @param key |
|||
*/ |
|||
public void deleteLock(String key) { |
|||
// 删除key即可释放锁
|
|||
redisTemplate.delete(key); |
|||
} |
|||
|
|||
public void flushDb() { |
|||
redisTemplate.execute((RedisCallback<Object>) connection -> { |
|||
connection.flushDb(); // 清空当前数据库
|
|||
return null; |
|||
}); |
|||
} |
|||
} |
@ -0,0 +1,68 @@ |
|||
package cn.iocoder.yudao.module.hand.vo; |
|||
|
|||
import lombok.*; |
|||
import java.util.*; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import cn.iocoder.yudao.framework.common.pojo.PageParam; |
|||
import org.springframework.format.annotation.DateTimeFormat; |
|||
import java.time.LocalDateTime; |
|||
|
|||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; |
|||
|
|||
@Schema(description = "管理后台 - GAS工厂分页 Request VO") |
|||
@Data |
|||
public class FactoryPageReqVO extends PageParam { |
|||
|
|||
@Schema(description = "父节点ID", example = "997") |
|||
private Long parentId; |
|||
|
|||
@Schema(description = "层级(1:工厂;2:车间;3:班组)", example = "1") |
|||
private Integer type; |
|||
|
|||
@Schema(description = "名称", example = "王五") |
|||
private String name; |
|||
|
|||
@Schema(description = "城市") |
|||
private String city; |
|||
|
|||
@Schema(description = "总警报数") |
|||
private Integer alarmTotal; |
|||
|
|||
@Schema(description = "已处理警报数") |
|||
private Integer alarmDeal; |
|||
|
|||
@Schema(description = "区域图", example = "https://www.iocoder.cn") |
|||
private String picUrl; |
|||
|
|||
@Schema(description = "区域图缩放比例") |
|||
private Integer picScale; |
|||
|
|||
@Schema(description = "在区域图X坐标值") |
|||
private Double picX; |
|||
|
|||
@Schema(description = "在区域图X坐标值") |
|||
private Double picY; |
|||
|
|||
@Schema(description = "经度") |
|||
private Double longitude; |
|||
|
|||
@Schema(description = "纬度") |
|||
private Double latitude; |
|||
|
|||
@Schema(description = "区域西南坐标") |
|||
private String rectSouthWest; |
|||
|
|||
@Schema(description = "区域东北坐标") |
|||
private String rectNorthEast; |
|||
|
|||
@Schema(description = "排序") |
|||
private Integer sortOrder; |
|||
|
|||
@Schema(description = "备注", example = "你猜") |
|||
private String remark; |
|||
|
|||
@Schema(description = "创建时间") |
|||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) |
|||
private LocalDateTime[] createTime; |
|||
|
|||
} |
@ -0,0 +1,86 @@ |
|||
package cn.iocoder.yudao.module.hand.vo; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.*; |
|||
import java.util.*; |
|||
import org.springframework.format.annotation.DateTimeFormat; |
|||
import java.time.LocalDateTime; |
|||
import com.alibaba.excel.annotation.*; |
|||
|
|||
@Schema(description = "管理后台 - GAS工厂 Response VO") |
|||
@Data |
|||
@ExcelIgnoreUnannotated |
|||
public class FactoryRespVO { |
|||
|
|||
@Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "19786") |
|||
@ExcelProperty("主键ID") |
|||
private Long id; |
|||
|
|||
@Schema(description = "父节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "997") |
|||
@ExcelProperty("父节点ID") |
|||
private Long parentId; |
|||
|
|||
@Schema(description = "层级(1:工厂;2:车间;3:班组)", example = "1") |
|||
@ExcelProperty("层级(1:工厂;2:车间;3:班组)") |
|||
private Integer type; |
|||
|
|||
@Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") |
|||
@ExcelProperty("名称") |
|||
private String name; |
|||
|
|||
@Schema(description = "城市") |
|||
@ExcelProperty("城市") |
|||
private String city; |
|||
|
|||
@Schema(description = "总警报数", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
@ExcelProperty("总警报数") |
|||
private Integer alarmTotal; |
|||
|
|||
@Schema(description = "已处理警报数", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
@ExcelProperty("已处理警报数") |
|||
private Integer alarmDeal; |
|||
|
|||
@Schema(description = "区域图", example = "https://www.iocoder.cn") |
|||
@ExcelProperty("区域图") |
|||
private String picUrl; |
|||
|
|||
@Schema(description = "区域图缩放比例") |
|||
@ExcelProperty("区域图缩放比例") |
|||
private Integer picScale; |
|||
|
|||
@Schema(description = "在区域图X坐标值") |
|||
@ExcelProperty("在区域图X坐标值") |
|||
private Double picX; |
|||
|
|||
@Schema(description = "在区域图X坐标值") |
|||
@ExcelProperty("在区域图X坐标值") |
|||
private Double picY; |
|||
|
|||
@Schema(description = "经度") |
|||
@ExcelProperty("经度") |
|||
private Double longitude; |
|||
|
|||
@Schema(description = "纬度") |
|||
@ExcelProperty("纬度") |
|||
private Double latitude; |
|||
|
|||
@Schema(description = "区域西南坐标") |
|||
@ExcelProperty("区域西南坐标") |
|||
private String rectSouthWest; |
|||
|
|||
@Schema(description = "区域东北坐标") |
|||
@ExcelProperty("区域东北坐标") |
|||
private String rectNorthEast; |
|||
|
|||
@Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
@ExcelProperty("排序") |
|||
private Integer sortOrder; |
|||
|
|||
@Schema(description = "备注", example = "你猜") |
|||
@ExcelProperty("备注") |
|||
private String remark; |
|||
|
|||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
@ExcelProperty("创建时间") |
|||
private LocalDateTime createTime; |
|||
} |
@ -0,0 +1,68 @@ |
|||
package cn.iocoder.yudao.module.hand.vo; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.*; |
|||
import java.util.*; |
|||
import jakarta.validation.constraints.*; |
|||
|
|||
@Schema(description = "管理后台 - GAS工厂新增/修改 Request VO") |
|||
@Data |
|||
public class FactorySaveReqVO { |
|||
|
|||
@Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "19786") |
|||
private Long id; |
|||
|
|||
@Schema(description = "父节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "997") |
|||
@NotNull(message = "父节点ID不能为空") |
|||
private Long parentId; |
|||
|
|||
@Schema(description = "层级(1:工厂;2:车间;3:班组)", example = "1") |
|||
private Integer type; |
|||
|
|||
@Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") |
|||
@NotEmpty(message = "名称不能为空") |
|||
private String name; |
|||
|
|||
@Schema(description = "城市") |
|||
private String city; |
|||
|
|||
@Schema(description = "总警报数", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
@NotNull(message = "总警报数不能为空") |
|||
private Integer alarmTotal; |
|||
|
|||
@Schema(description = "已处理警报数", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
@NotNull(message = "已处理警报数不能为空") |
|||
private Integer alarmDeal; |
|||
|
|||
@Schema(description = "区域图", example = "https://www.iocoder.cn") |
|||
private String picUrl; |
|||
|
|||
@Schema(description = "区域图缩放比例") |
|||
private Integer picScale; |
|||
|
|||
@Schema(description = "在区域图X坐标值") |
|||
private Double picX; |
|||
|
|||
@Schema(description = "在区域图X坐标值") |
|||
private Double picY; |
|||
|
|||
@Schema(description = "经度") |
|||
private Double longitude; |
|||
|
|||
@Schema(description = "纬度") |
|||
private Double latitude; |
|||
|
|||
@Schema(description = "区域西南坐标") |
|||
private String rectSouthWest; |
|||
|
|||
@Schema(description = "区域东北坐标") |
|||
private String rectNorthEast; |
|||
|
|||
@Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
@NotNull(message = "排序不能为空") |
|||
private Integer sortOrder; |
|||
|
|||
@Schema(description = "备注", example = "你猜") |
|||
private String remark; |
|||
|
|||
} |
@ -0,0 +1,19 @@ |
|||
package cn.iocoder.yudao.module.hand.vo; |
|||
|
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
public class FencePointVo { |
|||
public double x;//经度
|
|||
public double y;//纬度
|
|||
|
|||
//public Long fenceId;
|
|||
|
|||
public FencePointVo() { |
|||
} |
|||
|
|||
public FencePointVo(double x, double y) { |
|||
this.x = x; |
|||
this.y = y; |
|||
} |
|||
} |
@ -0,0 +1,47 @@ |
|||
package cn.iocoder.yudao.module.hand.vo; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.util.Collections; |
|||
import java.util.List; |
|||
@Data |
|||
public class Geofence { |
|||
private final List<FencePointVo> vertices; |
|||
private final double minX, minY, maxX, maxY; // 包围盒
|
|||
|
|||
public Geofence(List<FencePointVo> vertices) { |
|||
if (vertices == null || vertices.size() < 3) { |
|||
throw new IllegalArgumentException("一个多边形至少需要3个顶点。"); |
|||
} |
|||
this.vertices = Collections.unmodifiableList(vertices); |
|||
|
|||
// 在构造时一次性计算包围盒
|
|||
double tempMinX = Double.MAX_VALUE; |
|||
double tempMinY = Double.MAX_VALUE; |
|||
double tempMaxX = -Double.MAX_VALUE; |
|||
double tempMaxY = -Double.MAX_VALUE; |
|||
|
|||
for (FencePointVo point : vertices) { |
|||
tempMinX = Math.min(tempMinX, point.x); |
|||
tempMinY = Math.min(tempMinY, point.y); |
|||
tempMaxX = Math.max(tempMaxX, point.x); |
|||
tempMaxY = Math.max(tempMaxY, point.y); |
|||
} |
|||
this.minX = tempMinX; |
|||
this.minY = tempMinY; |
|||
this.maxX = tempMaxX; |
|||
this.maxY = tempMaxY; |
|||
} |
|||
|
|||
/** |
|||
* 判断一个点是否在当前围栏的包围盒内。 |
|||
* 这是一个快速的初步检查。 |
|||
*/ |
|||
public boolean isPointInBoundingBox(double x, double y) { |
|||
return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY; |
|||
} |
|||
|
|||
public List<FencePointVo> getVertices() { |
|||
return vertices; |
|||
} |
|||
} |
@ -0,0 +1,104 @@ |
|||
package cn.iocoder.yudao.module.hand.vo; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
import java.math.BigDecimal; |
|||
import java.time.LocalDateTime; |
|||
import java.util.Date; |
|||
|
|||
@Data |
|||
@JsonIgnoreProperties(ignoreUnknown = true) |
|||
public class HandDataVo { |
|||
|
|||
@Schema(description = "id") |
|||
private Long id; |
|||
|
|||
@Schema(description = "设备sn") |
|||
private String sn; |
|||
|
|||
@Schema(description = "电量") |
|||
private String battery; |
|||
|
|||
@Schema(description = "数据") |
|||
private Double value; |
|||
|
|||
@Schema(description = "经度") |
|||
private Double longitude; |
|||
|
|||
@Schema(description = "纬度") |
|||
private Double latitude; |
|||
|
|||
@Schema(description = "更新时间") |
|||
private Date time; |
|||
|
|||
@Schema(description = "持有人姓名") |
|||
private String name; |
|||
|
|||
@Schema(description = "围栏id") |
|||
private String fenceIds; |
|||
|
|||
@Schema(description = "围栏类型(1:超出报警,2:进入报警)") |
|||
private Integer fenceType; |
|||
|
|||
@Schema(description = "气体类型ID") |
|||
private Long gasTypeId; |
|||
|
|||
@Schema(description = "气体化学式") |
|||
private String gasChemical; |
|||
|
|||
@Schema(description = "低于多少电量报警") |
|||
private BigDecimal batteryAlarmValue; |
|||
|
|||
@Schema(description = "数值除数") |
|||
private Integer accuracy; |
|||
|
|||
@Schema(description = "报警等级") |
|||
private Integer alarmLevel; |
|||
|
|||
@Schema(description = "最大报警等级") |
|||
private Integer maxAlarmLevel; |
|||
|
|||
@Schema(description = "首次报警") |
|||
private Double firstValue; |
|||
|
|||
@Schema(description = "最大报警值") |
|||
private Double maxValue; |
|||
|
|||
|
|||
@Schema(description = "报警开始时间") |
|||
private LocalDateTime tAlarmStart; |
|||
@Schema(description = "报警结束时间") |
|||
private LocalDateTime tAlarmEnd; |
|||
|
|||
@Schema(description = "气体报警状态(0:正常;1:报警)") |
|||
private Integer gasStatus; |
|||
@Schema(description = "气体报警Id") |
|||
private Long alarmId; |
|||
|
|||
@Schema(description = "电池报警状态(0:正常;1:报警)") |
|||
private Integer batteryStatus; |
|||
private Integer batteryStatusAlarmId; |
|||
|
|||
@Schema(description = "电子围栏报警状态(0:正常;1:报警)") |
|||
private Integer fenceStatus; |
|||
@Schema(description = "电子围栏报警Id") |
|||
private Long fenceAlarmId; |
|||
|
|||
@Schema(description = "在线状态(0:离线;1:在线)") |
|||
private Integer onlineStatus; |
|||
|
|||
|
|||
@Schema(description = "启用状态(0:备用;1:启用)") |
|||
private Integer enableStatus; |
|||
|
|||
@Schema(description = "超出围栏米数") |
|||
private Double distance; |
|||
|
|||
@Schema(description = "最远超出米数") |
|||
private Double maxDistance; |
|||
|
|||
@Schema(description = "租户id") |
|||
private Long tenantId; |
|||
} |
@ -0,0 +1,17 @@ |
|||
package cn.iocoder.yudao.module.hand.vo; |
|||
|
|||
import lombok.Data; |
|||
import java.sql.Timestamp; // 确保导入的是 java.sql.Timestamp
|
|||
|
|||
|
|||
@Data |
|||
public class HandOriginalLog { |
|||
|
|||
private String sn; |
|||
|
|||
private String payload; |
|||
|
|||
private Timestamp ts; |
|||
|
|||
private Long tenantId; |
|||
} |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue