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;
+ }
+
+}
diff --git a/cc-admin-master/yudao-module-hand/src/main/java/cn/iocoder/yudao/module/hand/util/GeofenceUtils.java b/cc-admin-master/yudao-module-hand/src/main/java/cn/iocoder/yudao/module/hand/util/GeofenceUtils.java
new file mode 100644
index 0000000..6c9bf71
--- /dev/null
+++ b/cc-admin-master/yudao-module-hand/src/main/java/cn/iocoder/yudao/module/hand/util/GeofenceUtils.java
@@ -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)
+ *
+ * 核心优化:
+ *
+ * - 引入 Geofence 对象: 封装了多边形顶点及其预计算的“包围盒”(Bounding Box)。
+ * - 包围盒快速排斥: 在进行昂贵的精确计算前,先进行廉价的矩形范围判断,能过滤掉绝大多数不相关的围栏。
+ * - 距离计算剪枝 (Pruning): 在计算最短距离时,利用点到包围盒的距离,提前跳过那些不可能存在最短距离的围栏。
+ *
+ *
+ * 使用流程:
+ * {@code
+ * // 1. 从JSON或其他数据源创建Geofence列表(这个过程会自动计算包围盒)
+ * List geofences = GeofenceUtils.parseFences(fenceDO.getFenceRange());
+ *
+ * // 2. 使用优化后的方法进行高效计算
+ * boolean isInside = GeofenceUtils.isInsideAnyFence(longitude, latitude, geofences);
+ * double distance = GeofenceUtils.calculateDistanceToFences(longitude, latitude, geofences);
+ * }
+ */
+@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 parseFences(String fenceRangeJson) {
+ if (fenceRangeJson == null || fenceRangeJson.trim().isEmpty()) {
+ return Collections.emptyList();
+ }
+ try {
+ List geofences = new ArrayList<>();
+ double[][][] coordinates = GSON_INSTANCE.fromJson(fenceRangeJson, double[][][].class);
+ if (coordinates == null) return Collections.emptyList();
+
+ for (double[][] polygonCoordinates : coordinates) {
+ List 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 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 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 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 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 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);
+ }
+
+ /**
+ * 计算一个点到一组围栏最近边界的“物理”距离(单位:米)。
+ *
+ * 与 calculateDistanceToFences 的关键区别:
+ * 此方法 总是 会计算并返回点到最近边界的实际最短距离,
+ * 无论这个点是在围栏的内部还是外部。
+ *
+ * 例如,如果点在围栏内部,它会返回一个大于0的值,表示该点距离“逃出”围栏的最近距离。
+ *
+ * @param x 点的经度
+ * @param y 点的纬度
+ * @param geofences Geofence对象列表
+ * @return 点到所有围栏边界的物理最短距离(米)。
+ */
+ public static double calculateDistanceToNearestBoundary(double x, double y, List 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 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 fenceList) {
+ if (fenceType == FenceType.INSIDE) {
+ // 规则是“必须在内”,违规意味着“在外面”,所以计算点到围栏的外部距离。
+ // 此方法专为外部点设计。calculateDistanceToNearestBoundary
+ return GeofenceUtils.calculateDistanceToNearestBoundary(handVo.getLongitude(), handVo.getLatitude(), fenceList);
+ } else { // 规则是OUTSIDE
+ // 违规意味着“在里面”,所以计算点到边界的内部深度。
+ // 此方法能处理内部点。
+
+ return GeofenceUtils.calculateDistanceToFences(handVo.getLongitude(), handVo.getLatitude(), fenceList);
+ }
+ }
+}
\ No newline at end of file
diff --git a/cc-admin-master/yudao-module-hand/src/main/java/cn/iocoder/yudao/module/hand/util/RedisKeyUtil.java b/cc-admin-master/yudao-module-hand/src/main/java/cn/iocoder/yudao/module/hand/util/RedisKeyUtil.java
new file mode 100644
index 0000000..57a61f3
--- /dev/null
+++ b/cc-admin-master/yudao-module-hand/src/main/java/cn/iocoder/yudao/module/hand/util/RedisKeyUtil.java
@@ -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;
+
+ }
+}
diff --git a/cc-admin-master/yudao-module-hand/src/main/java/cn/iocoder/yudao/module/hand/util/RedisUtil.java b/cc-admin-master/yudao-module-hand/src/main/java/cn/iocoder/yudao/module/hand/util/RedisUtil.java
new file mode 100644
index 0000000..036f833
--- /dev/null
+++ b/cc-admin-master/yudao-module-hand/src/main/java/cn/iocoder/yudao/module/hand/util/RedisUtil.java
@@ -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 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) 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