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 配置 |
* 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