基于Java的小程序地理围栏实现原理

一、什么是地理围栏?

地理围栏是一种基于位置的服务技术,通过在真实世界的地理区域周围建立虚拟边界("围栏"),当移动设备进入或离开该区域时,触发预设的响应动作。


二、核心工作原理

复制代码
┌─────────────────────────────────────────┐
│           地理围栏系统架构               │
├─────────────────────────────────────────┤
│  1. 定义围栏 → 在地图上划定虚拟边界区域   │
│  2. 位置监测 → GPS/WiFi/基站定位追踪     │
│  3. 触发判断 → 检测进入/离开/停留事件     │
│  4. 执行动作 → 推送通知/记录日志/联动控制  │
└─────────────────────────────────────────┘

三、主要应用场景

领域 具体应用
智能家居 离家自动关闭空调/灯光,回家提前开启暖气
儿童/老人安全 设置安全区域,离开范围自动告警通知家长
企业管理 外勤人员考勤打卡、车辆轨迹监控
物流配送 货物到达特定区域自动通知收件人
零售营销 顾客进入商场附近推送优惠券
宠物追踪 宠物跑出设定范围立即提醒主人
游戏娱乐 如 Pokémon GO 的地理位置游戏机制

四、技术实现要点

定位技术

  • GPS(室外高精度)
  • WiFi/蓝牙(室内定位补充)
  • 基站定位(低功耗备用)

触发类型

  • 进入围栏 (Enter)
  • 离开围栏 (Exit)
  • 在围栏内停留 (Dwell)

平台支持

  • iOS: Core Location 框架(CLCircularRegion
  • Android: Geofencing API(Geofence 类)
  • 高德/百度/腾讯地图 SDK 均提供封装接口

五、技术架构概览

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        微信小程序端                              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────────┐ │
│  │ 地图展示组件 │  │ 实时位置采集 │  │ 订阅消息/推送通知        │ │
│  │ (Map组件)   │  │ (wx.getLocation)│  │ (wx.requestSubscribe)  │ │
│  └─────────────┘  └─────────────┘  └─────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│                          通信层                                  │
│              HTTPS REST API + WebSocket (可选实时推送)           │
├─────────────────────────────────────────────────────────────────┤
│                      Java Spring Boot后端                        │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────────┐ │
│  │ 围栏管理API  │  │ 位置计算引擎 │  │ 规则触发系统            │ │
│  │ (CRUD操作)  │  │ (空间算法)   │  │ (事件处理+消息推送)      │ │
│  └─────────────┘  └─────────────┘  └─────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│                        数据存储层                                │
│              ┌─────────────────┐  ┌─────────────────┐           │
│              │   MySQL 8.0+    │  │     Redis       │           │
│              │ (围栏定义+历史记录)│  │ (实时位置缓存)           │ │
│              │  SPATIAL INDEX  │  │  Geo数据结构    │          │ │
│              └─────────────────┘  └─────────────────┘           │
└─────────────────────────────────────────────────────────────────┘

六、数据库设计(完整版)

复制代码
-- ==================== 核心表:地理围栏 ====================
CREATE TABLE geofence (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '围栏ID',
    name VARCHAR(100) NOT NULL COMMENT '围栏名称(如:家、公司)',
    description VARCHAR(500) COMMENT '描述',
    
    -- 几何形状定义
    shape_type ENUM('CIRCLE', 'POLYGON', 'RECTANGLE') NOT NULL DEFAULT 'CIRCLE',
    center_lat DECIMAL(10,8) COMMENT '圆心/中心点纬度',
    center_lng DECIMAL(11,8) COMMENT '圆心/中心点经度',
    radius INT COMMENT '半径(米), 圆形专用',
    coordinates JSON COMMENT '多边形顶点坐标数组 [{"lat":x,"lng":y},...]',
    
    -- 触发规则配置
    monitor_types SET('ENTER','EXIT','DWELL') DEFAULT 'ENTER,EXIT' 
        COMMENT '监听事件类型',
    dwell_time INT DEFAULT 0 COMMENT '停留触发时间(分钟), DWELL专用',
    cooldown_minutes INT DEFAULT 5 COMMENT '事件冷却时间(防重复触发)',
    
    -- 业务属性
    fence_type ENUM('SAFETY','ATTENDANCE','MARKETING','CUSTOM') 
        DEFAULT 'CUSTOM' COMMENT '围栏业务类型',
    create_by BIGINT NOT NULL COMMENT '创建者用户ID',
    status TINYINT DEFAULT 1 COMMENT '状态:0禁用 1启用 2删除',
    
    -- 时间戳
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    -- 空间索引优化查询
    SPATIAL INDEX idx_location (center_lat, center_lng),
    INDEX idx_user_status (create_by, status)
) ENGINE=InnoDB COMMENT='地理围栏定义表';

-- ==================== 用户位置历史记录 ====================
CREATE TABLE user_location_history (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL COMMENT '用户ID',
    geofence_id BIGINT COMMENT '当前所在围栏ID(可为空)',
    
    -- 位置信息
    lat DECIMAL(10,8) NOT NULL COMMENT '纬度',
    lng DECIMAL(11,8) NOT NULL COMMENT '经度',
    accuracy FLOAT COMMENT '定位精度(米)',
    altitude FLOAT COMMENT '海拔',
    speed FLOAT COMMENT '速度(m/s)',
    
    -- 设备信息
    device_type VARCHAR(20) COMMENT '设备类型:ios/android',
    wx_version VARCHAR(20) COMMENT '微信版本',
    
    recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_user_time (user_id, recorded_at),
    INDEX idx_location (lat, lng)
) ENGINE=InnoDB COMMENT='用户位置历史';

-- ==================== 围栏触发事件日志 ====================
CREATE TABLE geofence_event_log (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    geofence_id BIGINT NOT NULL,
    event_type ENUM('ENTER','EXIT','DWELL') NOT NULL,
    
    -- 触发时位置
    trigger_lat DECIMAL(10,8) NOT NULL,
    trigger_lng DECIMAL(11,8) NOT NULL,
    
    -- 事件处理状态
    process_status TINYINT DEFAULT 0 
        COMMENT '0待处理 1已通知 2处理成功 3处理失败',
    notify_method ENUM('WX_MSG','SMS','PHONE','APP_PUSH') 
        DEFAULT 'WX_MSG',
    notify_result VARCHAR(500) COMMENT '通知结果/失败原因',
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_user_fence (user_id, geofence_id),
    INDEX idx_time (created_at)
) ENGINE=InnoDB COMMENT='围栏事件日志';

-- ==================== 用户围栏状态缓存表(持久化备份) ====================
CREATE TABLE user_geofence_state (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL UNIQUE,
    current_fence_id BIGINT COMMENT '当前所在围栏',
    enter_time TIMESTAMP COMMENT '进入时间',
    last_exit_time TIMESTAMP COMMENT '最后离开时间',
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB COMMENT='用户围栏状态(Redis故障时备用)';

七、Java核心代码实现

7.1 实体类层 (Entity/VO/DTO)

复制代码
// ==================== 地理围栏实体 ====================
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Geofence implements Serializable {
    private Long id;
    private String name;
    private String description;
    private ShapeType shapeType;
    private BigDecimal centerLat;
    private BigDecimal centerLng;
    private Integer radius;
    private List<Coordinate> coordinates;
    private Set<MonitorType> monitorTypes;
    private Integer dwellTime;
    private Integer cooldownMinutes;
    private FenceType fenceType;
    private Long createBy;
    private Integer status;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    
    /**
     * 判断点是否在围栏内
     */
    public boolean contains(BigDecimal lat, BigDecimal lng) {
        switch (shapeType) {
            case CIRCLE:
                return GeoCalculator.isInCircle(lat, lng, centerLat, centerLng, radius);
            case POLYGON:
            case RECTANGLE:
                return GeoCalculator.isInPolygon(lat, lng, coordinates);
            default:
                return false;
        }
    }
    
    /**
     * 是否需要检查停留事件
     */
    public boolean needCheckDwell() {
        return monitorTypes.contains(MonitorType.DWELL) && dwellTime > 0;
    }
}

// ==================== 坐标点 ====================
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Coordinate {
    private BigDecimal lat;
    private BigDecimal lng;
}

// ==================== 位置上报DTO ====================
@Data
public class LocationReportDTO {
    @NotNull(message = "用户ID不能为空")
    private Long userId;
    
    @NotNull(message = "纬度不能为空")
    @DecimalMin(value = "-90.0", message = "纬度范围错误")
    @DecimalMax(value = "90.0", message = "纬度范围错误")
    private BigDecimal lat;
    
    @NotNull(message = "经度不能为空")
    @DecimalMin(value = "-180.0", message = "经度范围错误")
    @DecimalMax(value = "180.0", message = "经度范围错误")
    private BigDecimal lng;
    
    private Float accuracy;
    private Float altitude;
    private Float speed;
    private String deviceType;
    private Long timestamp;
}

// ==================== 围栏创建DTO ====================
@Data
public class GeofenceCreateDTO {
    @NotBlank(message = "围栏名称不能为空")
    @Size(max = 100, message = "名称长度不能超过100")
    private String name;
    
    private String description;
    
    @NotNull(message = "形状类型不能为空")
    private ShapeType shapeType;
    
    // 圆形必填
    private BigDecimal centerLat;
    private BigDecimal centerLng;
    private Integer radius;
    
    // 多边形必填
    private List<Coordinate> coordinates;
    
    @NotEmpty(message = "至少选择一种监控类型")
    private Set<MonitorType> monitorTypes;
    
    private Integer dwellTime;
    private FenceType fenceType;
}

// ==================== 枚举定义 ====================
public enum ShapeType {
    CIRCLE,      // 圆形
    POLYGON,     // 多边形
    RECTANGLE    // 矩形
}

public enum MonitorType {
    ENTER,   // 进入
    EXIT,    // 离开
    DWELL    // 停留
}

public enum FenceType {
    SAFETY,     // 安全围栏(儿童/老人)
    ATTENDANCE, // 考勤打卡
    MARKETING,  // 营销推送
    CUSTOM      // 自定义
}

public enum EventType {
    ENTER, EXIT, DWELL
}

7.2 地理计算工具类 (核心算法)

复制代码
@Component
@Slf4j
public class GeoCalculator {
    
    private static final double EARTH_RADIUS = 6371000; // 地球半径(米)
    private static final MathContext MC = new MathContext(10, RoundingMode.HALF_UP);
    
    /**
     * Haversine公式计算两点间距离(米)
     */
    public static double distance(BigDecimal lat1, BigDecimal lng1, 
                                   BigDecimal lat2, BigDecimal lng2) {
        double dLat = Math.toRadians(lat2.subtract(lat1).doubleValue());
        double dLng = Math.toRadians(lng2.subtract(lng1).doubleValue());
        
        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
                + Math.cos(Math.toRadians(lat1.doubleValue()))
                * Math.cos(Math.toRadians(lat2.doubleValue()))
                * Math.sin(dLng / 2) * Math.sin(dLng / 2);
        
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        return EARTH_RADIUS * c;
    }
    
    /**
     * 判断点是否在圆形围栏内
     */
    public static boolean isInCircle(BigDecimal pointLat, BigDecimal pointLng,
                                      BigDecimal centerLat, BigDecimal centerLng, 
                                      Integer radius) {
        if (centerLat == null || centerLng == null || radius == null) {
            return false;
        }
        double dist = distance(pointLat, pointLng, centerLat, centerLng);
        return dist <= radius;
    }
    
    /**
     * 射线法(Ray Casting)判断点是否在多边形内
     */
    public static boolean isInPolygon(BigDecimal lat, BigDecimal lng, 
                                       List<Coordinate> polygon) {
        if (polygon == null || polygon.size() < 3) {
            return false;
        }
        
        boolean inside = false;
        int n = polygon.size();
        double x = lng.doubleValue(), y = lat.doubleValue();
        
        for (int i = 0, j = n - 1; i < n; j = i++) {
            double xi = polygon.get(i).getLng().doubleValue();
            double yi = polygon.get(i).getLat().doubleValue();
            double xj = polygon.get(j).getLng().doubleValue();
            double yj = polygon.get(j).getLat().doubleValue();
            
            // 检查点是否在边的两端点之间(y坐标范围内)
            boolean intersect = ((yi > y) != (yj > y))
                    && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
            
            if (intersect) {
                inside = !inside;
            }
        }
        return inside;
    }
    
    /**
     * 计算GeoHash (用于空间索引优化)
     */
    public static String geoHash(BigDecimal lat, BigDecimal lng, int precision) {
        // 使用h3或geohash-java库实现
        // 这里简化示意
        return lat.setScale(precision, RoundingMode.HALF_UP) + "," 
             + lng.setScale(precision, RoundingMode.HALF_UP);
    }
}

7.3 核心业务服务层

复制代码
@Service
@Slf4j
public class GeofenceService {
    
    @Autowired private GeofenceMapper geofenceMapper;
    @Autowired private UserLocationMapper locationMapper;
    @Autowired private EventLogMapper eventLogMapper;
    @Autowired private StringRedisTemplate redisTemplate;
    @Autowired private ApplicationEventPublisher eventPublisher;
    
    // Redis Key常量
    private static final String USER_CURRENT_FENCE = "geofence:user:%d:current";
    private static final String USER_ENTER_TIME = "geofence:user:%d:fence:%d:enter";
    private static final String USER_DWELL_TRIGGERED = "geofence:user:%d:fence:%d:dwell";
    private static final String EVENT_COOLDOWN = "geofence:event:cooldown:%d:%d:%s";
    
    // ==================== 围栏管理 ====================
    
    @Transactional
    public Long createFence(GeofenceCreateDTO dto, Long userId) {
        // 参数校验
        validateFenceParams(dto);
        
        Geofence fence = Geofence.builder()
                .name(dto.getName())
                .description(dto.getDescription())
                .shapeType(dto.getShapeType())
                .centerLat(dto.getCenterLat())
                .centerLng(dto.getCenterLng())
                .radius(dto.getRadius())
                .coordinates(dto.getCoordinates())
                .monitorTypes(dto.getMonitorTypes())
                .dwellTime(dto.getDwellTime())
                .fenceType(dto.getFenceType())
                .createBy(userId)
                .status(1)
                .build();
        
        geofenceMapper.insert(fence);
        log.info("用户{}创建围栏: {}", userId, fence.getName());
        return fence.getId();
    }
    
    public List<Geofence> listUserFences(Long userId) {
        return geofenceMapper.selectByUserId(userId);
    }
    
    // ==================== 位置处理核心逻辑 ====================
    
    @Transactional
    public void processLocationReport(LocationReportDTO report) {
        Long userId = report.getUserId();
        
        // 1. 保存位置历史
        saveLocationHistory(report);
        
        // 2. 获取用户相关的有效围栏
        List<Geofence> fences = getRelevantFences(userId, report);
        
        // 3. 检查每个围栏的触发条件
        for (Geofence fence : fences) {
            try {
                checkFenceTrigger(userId, report, fence);
            } catch (Exception e) {
                log.error("检查围栏触发失败: user={}, fence={}, error={}", 
                    userId, fence.getId(), e.getMessage());
            }
        }
        
        // 4. 更新实时位置到Redis (用于快速查询)
        updateRealtimeLocation(userId, report);
    }
    
    /**
     * 检查单个围栏的触发条件
     */
    private void checkFenceTrigger(Long userId, LocationReportDTO report, Geofence fence) {
        boolean currentlyInside = fence.contains(report.getLat(), report.getLng());
        Long fenceId = fence.getId();
        
        // 获取之前的状态
        String currentFenceKey = String.format(USER_CURRENT_FENCE, userId);
        String previousFenceData = redisTemplate.opsForValue().get(currentFenceKey);
        Long previousFenceId = previousFenceData != null ? Long.valueOf(previousFenceData) : null;
        boolean wasInside = fenceId.equals(previousFenceId);
        
        // 冷却检查: 防止重复触发
        if (isInCooldown(userId, fenceId, currentlyInside ? EventType.ENTER : EventType.EXIT)) {
            return;
        }
        
        // ===== ENTER 事件 =====
        if (fence.getMonitorTypes().contains(MonitorType.ENTER) 
                && !wasInside && currentlyInside) {
            
            handleEnterEvent(userId, fence, report);
        }
        
        // ===== EXIT 事件 =====
        else if (fence.getMonitorTypes().contains(MonitorType.EXIT) 
                && wasInside && !currentlyInside) {
            
            handleExitEvent(userId, fence, report);
        }
        
        // ===== DWELL 停留事件 =====
        else if (fence.needCheckDwell() && currentlyInside && wasInside) {
            handleDwellEvent(userId, fence, report);
        }
    }
    
    private void handleEnterEvent(Long userId, Geofence fence, LocationReportDTO report) {
        Long fenceId = fence.getId();
        
        // 更新Redis状态
        String currentFenceKey = String.format(USER_CURRENT_FENCE, userId);
        String enterTimeKey = String.format(USER_ENTER_TIME, userId, fenceId);
        
        redisTemplate.opsForValue().set(currentFenceKey, String.valueOf(fenceId), 
                Duration.ofHours(24));
        redisTemplate.opsForValue().set(enterTimeKey, String.valueOf(System.currentTimeMillis()), 
                Duration.ofHours(24));
        
        // 清除停留触发标记(重新计时)
        String dwellKey = String.format(USER_DWELL_TRIGGERED, userId, fenceId);
        redisTemplate.delete(dwellKey);
        
        // 设置冷却
        setCooldown(userId, fenceId, EventType.ENTER, fence.getCooldownMinutes());
        
        // 记录事件并发布
        GeofenceEvent event = GeofenceEvent.builder()
                .userId(userId)
                .geofenceId(fenceId)
                .eventType(EventType.ENTER)
                .lat(report.getLat())
                .lng(report.getLng())
                .fenceName(fence.getName())
                .fenceType(fence.getFenceType())
                .timestamp(LocalDateTime.now())
                .build();
        
        saveEventLog(event);
        eventPublisher.publishEvent(event);
        
        log.info("用户{} 进入围栏: {}", userId, fence.getName());
    }
    
    private void handleExitEvent(Long userId, Geofence fence, LocationReportDTO report) {
        Long fenceId = fence.getId();
        
        // 清除Redis状态
        String currentFenceKey = String.format(USER_CURRENT_FENCE, userId);
        String enterTimeKey = String.format(USER_ENTER_TIME, userId, fenceId);
        String dwellKey = String.format(USER_DWELL_TRIGGERED, userId, fenceId);
        
        redisTemplate.delete(currentFenceKey);
        redisTemplate.delete(enterTimeKey);
        redisTemplate.delete(dwellKey);
        
        // 设置冷却
        setCooldown(userId, fenceId, EventType.EXIT, fence.getCooldownMinutes());
        
        // 发布事件
        GeofenceEvent event = GeofenceEvent.builder()
                .userId(userId)
                .geofenceId(fenceId)
                .eventType(EventType.EXIT)
                .lat(report.getLat())
                .lng(report.getLng())
                .fenceName(fence.getName())
                .fenceType(fence.getFenceType())
                .timestamp(LocalDateTime.now())
                .build();
        
        saveEventLog(event);
        eventPublisher.publishEvent(event);
        
        log.info("用户{} 离开围栏: {}", userId, fence.getName());
    }
    
    private void handleDwellEvent(Long userId, Geofence fence, LocationReportDTO report) {
        Long fenceId = fence.getId();
        String enterTimeKey = String.format(USER_ENTER_TIME, userId, fenceId);
        String dwellKey = String.format(USER_DWELL_TRIGGERED, userId, fenceId);
        
        // 检查是否已触发过
        if (Boolean.TRUE.equals(redisTemplate.hasKey(dwellKey))) {
            return;
        }
        
        // 计算停留时间
        String enterTimeStr = redisTemplate.opsForValue().get(enterTimeKey);
        if (enterTimeStr == null) return;
        
        long enterTime = Long.parseLong(enterTimeStr);
        long dwellMinutes = (System.currentTimeMillis() - enterTime) / 60000;
        
        if (dwellMinutes >= fence.getDwellTime()) {
            // 标记已触发
            redisTemplate.opsForValue().set(dwellKey, "1", Duration.ofHours(12));
            
            // 发布停留事件
            GeofenceEvent event = GeofenceEvent.builder()
                    .userId(userId)
                    .geofenceId(fenceId)
                    .eventType(EventType.DWELL)
                    .lat(report.getLat())
                    .lng(report.getLng())
                    .fenceName(fence.getName())
                    .fenceType(fence.getFenceType())
                    .dwellMinutes((int) dwellMinutes)
                    .timestamp(LocalDateTime.now())
                    .build();
            
            saveEventLog(event);
            eventPublisher.publishEvent(event);
            
            log.info("用户{} 在围栏{} 停留{}分钟", userId, fence.getName(), dwellMinutes);
        }
    }
    
    // ==================== 辅助方法 ====================
    
    private void validateFenceParams(GeofenceCreateDTO dto) {
        if (dto.getShapeType() == ShapeType.CIRCLE) {
            if (dto.getCenterLat() == null || dto.getCenterLng() == null || dto.getRadius() == null) {
                throw new BusinessException("圆形围栏必须设置圆心和半径");
            }
            if (dto.getRadius() <= 0 || dto.getRadius() > 10000) {
                throw new BusinessException("半径范围1-10000米");
            }
        } else if (dto.getCoordinates() == null || dto.getCoordinates().size() < 3) {
            throw new BusinessException("多边形围栏至少需要3个顶点");
        }
    }
    
    private List<Geofence> getRelevantFences(Long userId, LocationReportDTO report) {
        // 优化: 先通过GeoHash或距离粗略筛选附近围栏,再精确计算
        // 这里简化查询用户创建的所有有效围栏
        return geofenceMapper.selectActiveByUserId(userId);
    }
    
    private void saveLocationHistory(LocationReportDTO report) {
        UserLocationHistory history = new UserLocationHistory();
        BeanUtils.copyProperties(report, history);
        history.setRecordedAt(LocalDateTime.now());
        locationMapper.insert(history);
    }
    
    private void updateRealtimeLocation(Long userId, LocationReportDTO report) {
        String key = "user:location:realtime:" + userId;
        String value = String.format("%s,%s,%d", report.getLat(), report.getLng(), 
                System.currentTimeMillis());
        redisTemplate.opsForValue().set(key, value, Duration.ofMinutes(10));
    }
    
    private void saveEventLog(GeofenceEvent event) {
        EventLog logEntry = new EventLog();
        logEntry.setUserId(event.getUserId());
        logEntry.setGeofenceId(event.getGeofenceId());
        logEntry.setEventType(event.getEventType());
        logEntry.setTriggerLat(event.getLat());
        logEntry.setTriggerLng(event.getLng());
        logEntry.setProcessStatus(0);
        eventLogMapper.insert(logEntry);
    }
    
    private boolean isInCooldown(Long userId, Long fenceId, EventType type) {
        String key = String.format(EVENT_COOLDOWN, userId, fenceId, type.name());
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }
    
    private void setCooldown(Long userId, Long fenceId, EventType type, int minutes) {
        if (minutes <= 0) minutes = 5;
        String key = String.format(EVENT_COOLDOWN, userId, fenceId, type.name());
        redisTemplate.opsForValue().set(key, "1", Duration.ofMinutes(minutes));
    }
}

7.4 事件监听与消息推送

复制代码
// ==================== 事件定义 ====================
@Data
@Builder
public class GeofenceEvent {
    private Long userId;
    private Long geofenceId;
    private EventType eventType;
    private BigDecimal lat;
    private BigDecimal lng;
    private String fenceName;
    private FenceType fenceType;
    private Integer dwellMinutes;
    private LocalDateTime timestamp;
}

// ==================== 事件监听器 ====================
@Component
@Slf4j
public class GeofenceEventHandler {
    
    @Autowired private WxMaService wxMaService;
    @Autowired private SmsService smsService;
    @Autowired private UserService userService;
    @Autowired private EventLogMapper eventLogMapper;
    
    /**
     * 处理围栏触发事件
     */
    @EventListener
    @Async("taskExecutor")
    public void onGeofenceTriggered(GeofenceEvent event) {
        log.info("处理围栏事件: user={}, fence={}, type={}", 
            event.getUserId(), event.getFenceName(), event.getEventType());
        
        try {
            switch (event.getFenceType()) {
                case SAFETY:
                    handleSafetyEvent(event);
                    break;
                case ATTENDANCE:
                    handleAttendanceEvent(event);
                    break;
                case MARKETING:
                    handleMarketingEvent(event);
                    break;
                default:
                    handleDefaultEvent(event);
            }
        } catch (Exception e) {
            log.error("处理事件失败", e);
            updateEventStatus(event, 3, e.getMessage());
        }
    }
    
    /**
     * 安全围栏处理 (儿童/老人监护)
     */
    private void handleSafetyEvent(GeofenceEvent event) {
        User user = userService.getById(event.getUserId());
        
        if (event.getEventType() == EventType.EXIT) {
            // 离开安全区域 - 紧急通知
            String msg = String.format("【安全告警】%s 已离开安全区域 %s,当前位置: %s, %s",
                    user.getName(), event.getFenceName(), event.getLat(), event.getLng());
            
            // 1. 推送微信订阅消息
            sendWxSubscribeMsg(event.getUserId(), "安全区域提醒", msg);
            
            // 2. 短信通知紧急联系人
            if (user.getEmergencyContact() != null) {
                smsService.send(user.getEmergencyContact(), msg);
            }
            
            // 3. 记录为紧急事件
            updateEventStatus(event, 2, "已发送安全告警");
        } else if (event.getEventType() == EventType.ENTER) {
            sendWxSubscribeMsg(event.getUserId(), "安全区域提醒",
                String.format("%s 已进入安全区域 %s", user.getName(), event.getFenceName()));
            updateEventStatus(event, 2, "已进入安全区域");
        }
    }
    
    /**
     * 考勤围栏处理
     */
    private void handleAttendanceEvent(GeofenceEvent event) {
        if (event.getEventType() == EventType.ENTER) {
            // 上班打卡
            AttendanceRecord record = new AttendanceRecord();
            record.setUserId(event.getUserId());
            record.setCheckInTime(LocalDateTime.now());
            record.setLocation(event.getLat() + "," + event.getLng());
            record.setFenceId(event.getGeofenceId());
            // 保存考勤记录...
            
            sendWxSubscribeMsg(event.getUserId(), "考勤打卡",
                String.format("上班打卡成功\n时间: %s\n地点: %s", 
                    LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm")),
                    event.getFenceName()));
        } else if (event.getEventType() == EventType.EXIT) {
            // 下班打卡
            sendWxSubscribeMsg(event.getUserId(), "考勤打卡",
                String.format("下班打卡成功\n时间: %s", 
                    LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm"))));
        }
        updateEventStatus(event, 2, "考勤记录已保存");
    }
    
    /**
     * 营销围栏处理
     */
    private void handleMarketingEvent(GeofenceEvent event) {
        if (event.getEventType() == EventType.ENTER) {
            // 推送优惠券
            Coupon coupon = generateNearbyCoupon(event.getGeofenceId());
            
            sendWxSubscribeMsg(event.getUserId(), "附近优惠",
                String.format("您附近有新优惠!\n%s\n点击查看详情", coupon.getTitle()));
        }
        updateEventStatus(event, 2, "营销推送已发送");
    }
    
    /**
     * 默认处理
     */
    private void handleDefaultEvent(GeofenceEvent event) {
        String action = event.getEventType() == EventType.ENTER ? "进入" : 
                       event.getEventType() == EventType.EXIT ? "离开" : "停留";
        
        sendWxSubscribeMsg(event.getUserId(), "位置提醒",
            String.format("您已%s %s", action, event.getFenceName()));
        updateEventStatus(event, 2, "通知已发送");
    }
    
    /**
     * 发送微信小程序订阅消息
     */
    private void sendWxSubscribeMsg(Long userId, String title, String content) {
        try {
            // 获取用户openid
            String openid = userService.getOpenidByUserId(userId);
            
            // 构造订阅消息
            WxMaSubscribeMessage message = WxMaSubscribeMessage.builder()
                    .toUser(openid)
                    .templateId("YOUR_TEMPLATE_ID") // 微信小程序订阅消息模板ID
                    .data(Arrays.asList(
                        new WxMaSubscribeMessage.Data("thing1", title),
                        new WxMaSubscribeMessage.Data("thing2", content),
                        new WxMaSubscribeMessage.Data("time3", 
                            LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")))
                    ))
                    .build();
            
            wxMaService.getMsgService().sendSubscribeMsg(message);
            log.info("订阅消息发送成功: user={}", userId);
        } catch (Exception e) {
            log.error("发送订阅消息失败", e);
            throw e;
        }
    }
    
    private void updateEventStatus(GeofenceEvent event, int status, String result) {
        // 更新事件处理状态
        EventLog update = new EventLog();
        update.setProcessStatus(status);
        update.setNotifyResult(result);
        // 根据userId和时间查询更新...
    }
}

7.5 REST API 控制器

复制代码
@RestController
@RequestMapping("/api/geofence")
@Validated
@Slf4j
public class GeofenceController {
    
    @Autowired private GeofenceService geofenceService;
    @Autowired private GeofenceQueryService queryService;
    
    // ==================== 围栏管理接口 ====================
    
    @PostMapping("/create")
    public Result<Long> createFence(@RequestBody @Valid GeofenceCreateDTO dto,
                                    @RequestAttribute("userId") Long userId) {
        Long fenceId = geofenceService.createFence(dto, userId);
        return Result.success(fenceId);
    }
    
    @PutMapping("/{id}")
    public Result<Void> updateFence(@PathVariable Long id,
                                    @RequestBody GeofenceCreateDTO dto,
                                    @RequestAttribute("userId") Long userId) {
        geofenceService.updateFence(id, dto, userId);
        return Result.success();
    }
    
    @DeleteMapping("/{id}")
    public Result<Void> deleteFence(@PathVariable Long id,
                                    @RequestAttribute("userId") Long userId) {
        geofenceService.deleteFence(id, userId);
        return Result.success();
    }
    
    @GetMapping("/list")
    public Result<List<GeofenceVO>> listFences(@RequestAttribute("userId") Long userId) {
        List<Geofence> list = geofenceService.listUserFences(userId);
        List<GeofenceVO> voList = list.stream().map(this::convertToVO).collect(Collectors.toList());
        return Result.success(voList);
    }
    
    @GetMapping("/{id}")
    public Result<GeofenceVO> getFenceDetail(@PathVariable Long id) {
        Geofence fence = geofenceService.getById(id);
        return Result.success(convertToVO(fence));
    }
    
    // ==================== 位置上报接口 ====================
    
    @PostMapping("/location/report")
    public Result<LocationProcessResult> reportLocation(@RequestBody @Valid LocationReportDTO dto) {
        // 异步处理位置上报
        geofenceService.processLocationReport(dto);
        
        // 返回当前状态
        LocationProcessResult result = queryService.getCurrentStatus(dto.getUserId());
        return Result.success(result);
    }
    
    @PostMapping("/location/batch")
    public Result<Void> batchReportLocation(@RequestBody List<LocationReportDTO> reports) {
        // 批量上报,用于弱网环境下缓存后批量上传
        for (LocationReportDTO report : reports) {
            geofenceService.processLocationReport(report);
        }
        return Result.success();
    }
    
    // ==================== 查询接口 ====================
    
    @GetMapping("/check")
    public Result<List<Long>> checkLocation(@RequestParam BigDecimal lat,
                                            @RequestParam BigDecimal lng,
                                            @RequestAttribute("userId") Long userId) {
        // 检查当前位置在哪些围栏内
        List<Long> fenceIds = queryService.checkPointInFences(userId, lat, lng);
        return Result.success(fenceIds);
    }
    
    @GetMapping("/nearby")
    public Result<List<GeofenceVO>> getNearbyFences(@RequestParam BigDecimal lat,
                                                    @RequestParam BigDecimal lng,
                                                    @RequestParam(defaultValue = "1000") Integer radius,
                                                    @RequestAttribute("userId") Long userId) {
        // 获取附近的围栏
        List<GeofenceVO> list = queryService.getNearbyFences(userId, lat, lng, radius);
        return Result.success(list);
    }
    
    @GetMapping("/history")
    public Result<PageResult<UserLocationVO>> getLocationHistory(
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime start,
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime end,
            @RequestParam(defaultValue = "1") Integer page,
            @RequestParam(defaultValue = "20") Integer size,
            @RequestAttribute("userId") Long userId) {
        
        PageResult<UserLocationVO> result = queryService.getLocationHistory(userId, start, end, page, size);
        return Result.success(result);
    }
    
    @GetMapping("/events")
    public Result<PageResult<GeofenceEventVO>> getEventLogs(
            @RequestParam(required = false) Long fenceId,
            @RequestParam(required = false) EventType eventType,
            @RequestParam(defaultValue = "1") Integer page,
            @RequestParam(defaultValue = "20") Integer size,
            @RequestAttribute("userId") Long userId) {
        
        PageResult<GeofenceEventVO> result = queryService.getEventLogs(userId, fenceId, eventType, page, size);
        return Result.success(result);
    }
    
    // ==================== 辅助方法 ====================
    
    private GeofenceVO convertToVO(Geofence fence) {
        GeofenceVO vo = new GeofenceVO();
        BeanUtils.copyProperties(fence, vo);
        return vo;
    }
}

八、微信小程序端实现

8.1 页面结构 (WXML)

复制代码
<!-- pages/geofence/geofence.wxml -->
<view class="container">
  <!-- 地图组件 -->
  <map 
    id="geofenceMap"
    class="map"
    latitude="{{latitude}}"
    longitude="{{longitude}}"
    scale="{{scale}}"
    show-location
    markers="{{markers}}"
    circles="{{circles}}"
    polygons="{{polygons}}"
    bindmarkertap="onMarkerTap"
    bindregionchange="onRegionChange">
  </map>
  
  <!-- 定位按钮 -->
  <view class="location-btn" bindtap="moveToCurrentLocation">
    <image src="/images/location.png" mode="aspectFit"/>
  </view>
  
  <!-- 围栏列表面板 -->
  <view class="fence-panel {{panelExpanded ? 'expanded' : ''}}">
    <view class="panel-header" bindtap="togglePanel">
      <text class="title">我的围栏 ({{fences.length}})</text>
      <image class="arrow" src="/images/arrow.png" animation="{{arrowAnimation}}"/>
    </view>
    
    <scroll-view class="fence-list" scroll-y>
      <view wx:for="{{fences}}" wx:key="id" class="fence-item" 
            bindtap="focusOnFence" data-fence="{{item}}">
        <view class="fence-info">
          <text class="name">{{item.name}}</text>
          <text class="type">{{item.shapeType === 'CIRCLE' ? '圆形' : '多边形'}}</text>
        </view>
        <view class="fence-status {{item.isInside ? 'inside' : ''}}">
          {{item.isInside ? '范围内' : '范围外'}}
        </view>
      </view>
    </scroll-view>
    
    <button class="add-btn" bindtap="showAddModal">+ 新建围栏</button>
  </view>
  
  <!-- 创建围栏弹窗 -->
  <view class="modal" wx:if="{{showAddModal}}">
    <view class="modal-content">
      <view class="modal-header">创建围栏</view>
      <input placeholder="围栏名称" bindinput="onNameInput"/>
      <picker mode="selector" range="{{shapeTypes}}" bindchange="onShapeChange">
        <view class="picker">形状: {{selectedShape}}</view>
      </picker>
      <input type="digit" placeholder="半径(米)" wx:if="{{selectedShape === '圆形'}}" 
             bindinput="onRadiusInput"/>
      <view class="modal-btns">
        <button size="mini" bindtap="hideAddModal">取消</button>
        <button size="mini" type="primary" bindtap="confirmAdd">确定</button>
      </view>
    </view>
  </view>
</view>

8.2 页面逻辑 (JS)

复制代码
// pages/geofence/geofence.js
const app = getApp();
const API_BASE = 'https://your-api-domain.com/api';

Page({
  data: {
    latitude: 39.9042,  // 默认北京
    longitude: 116.4074,
    scale: 14,
    fences: [],
    markers: [],
    circles: [],
    polygons: [],
    isTracking: false,
    panelExpanded: false,
    showAddModal: false,
    selectedShape: '圆形',
    shapeTypes: ['圆形', '多边形'],
    newFenceRadius: 500
  },

  mapContext: null,
  locationWatcher: null,
  reportingInterval: null,

  onLoad() {
    this.mapContext = wx.createMapContext('geofenceMap');
    this.checkPermissionAndInit();
  },

  onShow() {
    if (!this.data.isTracking) {
      this.startLocationTracking();
    }
  },

  onHide() {
    // 保持后台追踪,不停止
  },

  onUnload() {
    this.stopLocationTracking();
  },

  // ==================== 权限检查与初始化 ====================
  
  async checkPermissionAndInit() {
    try {
      // 检查位置权限
      const setting = await wx.getSetting();
      if (!setting.authSetting['scope.userLocation']) {
        await wx.authorize({ scope: 'scope.userLocation' });
      }
      
      // 申请后台定位权限 (需先在app.json声明)
      if (!setting.authSetting['scope.userLocationBackground']) {
        const res = await wx.showModal({
          title: '需要后台定位',
          content: '地理围栏功能需要在后台持续定位,是否允许?'
        });
        if (res.confirm) {
          await wx.authorize({ scope: 'scope.userLocationBackground' });
        }
      }
      
      // 获取当前位置并加载数据
      await this.getCurrentLocation();
      await this.loadFences();
      this.startLocationTracking();
      
    } catch (err) {
      wx.showToast({ title: '权限获取失败', icon: 'none' });
      console.error('权限错误:', err);
    }
  },

  // ==================== 位置追踪核心 ====================
  
  async startLocationTracking() {
    try {
      // 开启后台定位
      await wx.startLocationUpdateBackground();
      
      this.setData({ isTracking: true });
      
      // 监听位置变化
      wx.onLocationChange(this.handleLocationChange.bind(this));
      
      // 定时上报 (备用,防止onLocationChange不稳定)
      this.reportingInterval = setInterval(() => {
        this.reportCachedLocation();
      }, 10000);
      
      wx.showToast({ title: '定位已开启', icon: 'success' });
      
    } catch (err) {
      console.error('启动定位失败:', err);
      // 降级为前台定位
      this.startForegroundLocation();
    }
  },

  startForegroundLocation() {
    wx.startLocationUpdate();
    wx.onLocationChange(this.handleLocationChange.bind(this));
    this.setData({ isTracking: true });
    wx.showToast({ title: '已切换前台定位', icon: 'none' });
  },

  stopLocationTracking() {
    wx.stopLocationUpdate();
    wx.offLocationChange();
    clearInterval(this.reportingInterval);
    this.setData({ isTracking: false });
  },

  // 位置变化处理
  handleLocationChange(res) {
    const { latitude, longitude, accuracy, speed } = res;
    
    // 精度过滤:误差大于100米的位置丢弃
    if (accuracy > 100) {
      console.log('精度不足,丢弃:', accuracy);
      return;
    }
    
    // 缓存最新位置
    this.lastLocation = {
      lat: latitude,
      lng: longitude,
      accuracy,
      speed,
      timestamp: Date.now()
    };
    
    // 节流上报:每5秒上报一次
    if (!this.lastReportTime || Date.now() - this.lastReportTime > 5000) {
      this.reportLocation(this.lastLocation);
    }
    
    // 更新地图中心(仅在跟随模式)
    if (this.data.followLocation) {
      this.setData({ latitude, longitude });
    }
  },

  // 上报位置到服务器
  async reportLocation(location) {
    this.lastReportTime = Date.now();
    
    try {
      const res = await wx.request({
        url: `${API_BASE}/geofence/location/report`,
        method: 'POST',
        data: {
          userId: app.globalData.userId,
          lat: location.lat,
          lng: location.lng,
          accuracy: location.accuracy,
          speed: location.speed,
          deviceType: wx.getSystemInfoSync().platform,
          timestamp: location.timestamp
        }
      });
      
      // 处理服务器返回的围栏状态
      if (res.data && res.data.data) {
        this.updateFenceStatus(res.data.data.currentFences);
      }
      
    } catch (err) {
      console.error('位置上报失败:', err);
      // 缓存失败位置,稍后批量上报
      this.cacheFailedReport(location);
    }
  },

  // 缓存失败的位置上报
  cacheFailedReport(location) {
    let failed = wx.getStorageSync('failed_locations') || [];
    failed.push(location);
    if (failed.length > 50) failed = failed.slice(-50); // 限制缓存数量
    wx.setStorageSync('failed_locations', failed);
  },

  // 批量上报缓存的位置
  async reportCachedLocation() {
    const failed = wx.getStorageSync('failed_locations');
    if (!failed || failed.length === 0) return;
    
    try {
      await wx.request({
        url: `${API_BASE}/geofence/location/batch`,
        method: 'POST',
        data: failed.map(loc => ({
          userId: app.globalData.userId,
          ...loc
        }))
      });
      wx.removeStorageSync('failed_locations');
    } catch (err) {
      console.error('批量上报失败:', err);
    }
  },

  // ==================== 围栏数据管理 ====================
  
  async loadFences() {
    try {
      const res = await wx.request({
        url: `${API_BASE}/geofence/list`,
        data: { userId: app.globalData.userId }
      });
      
      const fences = res.data.data || [];
      this.setData({ fences });
      this.renderFencesOnMap(fences);
      
    } catch (err) {
      wx.showToast({ title: '加载围栏失败', icon: 'none' });
    }
  },

  // 在地图上渲染围栏
  renderFencesOnMap(fences) {
    const markers = [];
    const circles = [];
    const polygons = [];
    
    fences.forEach(fence => {
      // 标记点
      markers.push({
        id: fence.id,
        latitude: fence.centerLat,
        longitude: fence.centerLng,
        title: fence.name,
        iconPath: '/images/fence-marker.png',
        width: 32,
        height: 32,
        callout: {
          content: fence.name,
          color: '#000',
          fontSize: 14,
          borderRadius: 4,
          padding: 8,
          display: 'BYCLICK'
        }
      });
      
      if (fence.shapeType === 'CIRCLE') {
        circles.push({
          latitude: fence.centerLat,
          longitude: fence.centerLng,
          radius: fence.radius,
          fillColor: '#1AAD1980',  // 半透明绿色
          strokeColor: '#1AAD19',
          strokeWidth: 2
        });
      } else {
        // 多边形
        polygons.push({
          points: fence.coordinates.map(c => ({
            latitude: c.lat,
            longitude: c.lng
          })),
          fillColor: '#1AAD1980',
          strokeColor: '#1AAD19',
          strokeWidth: 2
        });
      }
    });
    
    this.setData({ markers, circles, polygons });
  },

  // 更新围栏状态(高亮当前在范围内的)
  updateFenceStatus(currentFenceIds) {
    const fences = this.data.fences.map(f => ({
      ...f,
      isInside: currentFenceIds.includes(f.id)
    }));
    this.setData({ fences });
  },

  // ==================== 交互操作 ====================
  
  moveToCurrentLocation() {
    this.mapContext.moveToLocation({
      longitude: this.data.longitude,
      latitude: this.data.latitude
    });
    this.setData({ followLocation: true });
  },

  focusOnFence(e) {
    const fence = e.currentTarget.dataset.fence;
    this.setData({
      latitude: fence.centerLat,
      longitude: fence.centerLng,
      scale: fence.shapeType === 'CIRCLE' ? this.getScaleByRadius(fence.radius) : 16,
      followLocation: false
    });
    this.togglePanel();
  },

  getScaleByRadius(radius) {
    // 根据半径计算合适的缩放级别
    if (radius < 100) return 18;
    if (radius < 500) return 16;
    if (radius < 1000) return 15;
    if (radius < 5000) return 13;
    return 12;
  },

  togglePanel() {
    this.setData({ panelExpanded: !this.data.panelExpanded });
  },

  // ==================== 创建围栏 ====================
  
  showAddModal() {
    // 获取地图中心作为默认位置
    this.mapContext.getCenterLocation({
      success: (res) => {
        this.newFenceCenter = {
          lat: res.latitude,
          lng: res.longitude
        };
        this.setData({ showAddModal: true });
      }
    });
  },

  hideAddModal() {
    this.setData({ showAddModal: false });
  },

  onNameInput(e) {
    this.newFenceName = e.detail.value;
  },

  onShapeChange(e) {
    const shapes = ['CIRCLE', 'POLYGON'];
    this.setData({ 
      selectedShape: this.data.shapeTypes[e.detail.value],
      selectedShapeType: shapes[e.detail.value]
    });
  },

  onRadiusInput(e) {
    this.setData({ newFenceRadius: parseInt(e.detail.value) || 500 });
  },

  async confirmAdd() {
    if (!this.newFenceName) {
      wx.showToast({ title: '请输入名称', icon: 'none' });
      return;
    }

    try {
      await wx.request({
        url: `${API_BASE}/geofence/create`,
        method: 'POST',
        data: {
          name: this.newFenceName,
          shapeType: this.data.selectedShapeType || 'CIRCLE',
          centerLat: this.newFenceCenter.lat,
          centerLng: this.newFenceCenter.lng,
          radius: this.data.newFenceRadius,
          monitorTypes: ['ENTER', 'EXIT'],
          fenceType: 'CUSTOM'
        }
      });
      
      wx.showToast({ title: '创建成功', icon: 'success' });
      this.hideAddModal();
      this.loadFences(); // 刷新列表
      
    } catch (err) {
      wx.showToast({ title: '创建失败', icon: 'none' });
    }
  }
});

8.3 样式 (WXSS)

复制代码
/* pages/geofence/geofence.wxss */
.container {
  position: relative;
  width: 100vw;
  height: 100vh;
}

.map {
  width: 100%;
  height: 100%;
}

.location-btn {
  position: absolute;
  right: 20rpx;
  bottom: 300rpx;
  width: 80rpx;
  height: 80rpx;
  background: white;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.1);
}

.location-btn image {
  width: 40rpx;
  height: 40rpx;
}

.fence-panel {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  background: white;
  border-radius: 20rpx 20rpx 0 0;
  box-shadow: 0 -2rpx 20rpx rgba(0,0,0,0.1);
  transition: transform 0.3s;
  transform: translateY(calc(100% - 100rpx));
  max-height: 60vh;
}

.fence-panel.expanded {
  transform: translateY(0);
}

.panel-header {
  height: 100rpx;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 30rpx;
  border-bottom: 1rpx solid #eee;
}

.title {
  font-size: 32rpx;
  font-weight: bold;
}

.arrow {
  width: 32rpx;
  height: 32rpx;
  transition: transform 0.3s;
}

.fence-panel.expanded .arrow {
  transform: rotate(180deg);
}

.fence-list {
  max-height: calc(60vh - 200rpx);
  padding: 20rpx 0;
}

.fence-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20rpx 30rpx;
  border-bottom: 1rpx solid #f5f5f5;
}

.fence-info {
  display: flex;
  flex-direction: column;
}

.name {
  font-size: 30rpx;
  color: #333;
}

.type {
  font-size: 24rpx;
  color: #999;
  margin-top: 6rpx;
}

.fence-status {
  padding: 6rpx 16rpx;
  border-radius: 20rpx;
  font-size: 24rpx;
  color: #999;
  background: #f5f5f5;
}

.fence-status.inside {
  color: #1AAD19;
  background: #E8F5E9;
}

.add-btn {
  margin: 20rpx 30rpx;
  background: #1AAD19;
  color: white;
  border-radius: 40rpx;
}

/* 弹窗样式 */
.modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0,0,0,0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.modal-content {
  width: 80%;
  background: white;
  border-radius: 16rpx;
  padding: 40rpx;
}

.modal-header {
  font-size: 36rpx;
  font-weight: bold;
  text-align: center;
  margin-bottom: 30rpx;
}

.modal input, .picker {
  border: 1rpx solid #ddd;
  border-radius: 8rpx;
  padding: 20rpx;
  margin-bottom: 20rpx;
  font-size: 28rpx;
}

.modal-btns {
  display: flex;
  justify-content: space-between;
  margin-top: 30rpx;
}

.modal-btns button {
  width: 45%;
}

九、配置文件

9.1 小程序 app.json

复制代码
{
  "pages": [
    "pages/geofence/geofence",
    "pages/index/index"
  ],
  "window": {
    "backgroundTextStyle": "light",
    "navigationBarBackgroundColor": "#fff",
    "navigationBarTitleText": "地理围栏",
    "navigationBarTextStyle": "black"
  },
  "permission": {
    "scope.userLocation": {
      "desc": "你的位置信息将用于地理围栏功能"
    }
  },
  "requiredBackgroundModes": ["location"],
  "requiredPrivateInfos": [
    "getLocation",
    "startLocationUpdate",
    "startLocationUpdateBackground"
  ]
}

9.2 Spring Boot 配置 (application.yml)

复制代码
server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/geofence_db?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver
  
  redis:
    host: localhost
    port: 6379
    password: 
    database: 0
    timeout: 2000ms
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0

# 微信小程序配置
wx:
  miniapp:
    appid: your_appid
    secret: your_secret

# 线程池配置
task:
  executor:
    core-pool-size: 10
    max-pool-size: 20
    queue-capacity: 100

# 日志
logging:
  level:
    com.yourpackage.geofence: DEBUG

十、Maven依赖 (pom.xml)

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<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 
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.14</version>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>geofence-service</artifactId>
    <version>1.0.0</version>
    
    <dependencies>
        <!-- Spring Boot Starters -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        
        <!-- MySQL & MyBatis -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>
        
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.3.1</version>
        </dependency>
        
        <!-- 微信小程序SDK -->
        <dependency>
            <groupId>com.github.binarywang</groupId>
            <artifactId>weixin-java-miniapp</artifactId>
            <version>4.5.0</version>
        </dependency>
        
        <!-- 工具类 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <!-- 空间计算库 (可选,复杂几何计算) -->
        <dependency>
            <groupId>org.locationtech.jts</groupId>
            <artifactId>jts-core</artifactId>
            <version>1.19.0</version>
        </dependency>
        
        <!-- 测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

十一、性能优化建议

优化点 具体方案
空间索引 MySQL 8.0+ 使用 SPATIAL INDEX,或引入 PostGIS
GeoHash分片 使用 Redis Geo 命令 GEORADIUS 快速筛选附近围栏
缓存策略 用户围栏列表本地缓存,减少DB查询
批量处理 位置上报先入消息队列,异步批量消费
计算优化 先进行矩形包围盒粗筛,再精确计算
降级方案 Redis故障时自动切换到DB查询

注意事项

  1. 电量消耗 --- GPS持续定位较耗电,建议结合运动传感器优化
  2. 精度问题 --- 城市峡谷、室内环境可能影响定位准确性
  3. 隐私合规 --- 需用户明确授权位置权限,符合 GDPR/个人信息保护法
  4. 围栏数量限制 --- iOS 通常限制同时监测约 20 个围栏
相关推荐
arvin_xiaoting2 小时前
OpenClaw学习总结_II_频道系统_5:Signal集成详解
java·前端·学习·signal·ai agent·openclaw·signal-cli
凌波粒2 小时前
LeetCode--19.删除链表的倒数第 N 个结点(链表)
java·算法·leetcode·链表
哆啦A梦15882 小时前
统一返回包装类 Result和异常处理
java·前端·后端·springboot
Mem0rin2 小时前
[Java/数据结构]顺序表之ArrayList
java·开发语言·数据结构
Kingexpand_com2 小时前
实用技巧:小程序积分体系的功能拆解与高效利用指南
小程序·仓库管理·库存管理·小程序定制开发
毕设源码-赖学姐2 小时前
【开题答辩全过程】以 居家养老服务微信小程序设计与实现为例,包含答辩的问题和答案
微信小程序·小程序
WarrenMondeville2 小时前
4.Unity面向对象-接口隔离原则
java·unity·接口隔离原则
zb200641202 小时前
spring security 超详细使用教程(接入springboot、前后端分离)
java·spring boot·spring
啥咕啦呛2 小时前
java打卡学习3:ArrayList扩容机制
java·python·学习