揭秘外卖平台城市区域地理围栏/电子围栏设计

本文将带你从零到一,完整实现一套基于行政区划的地理围栏方案,包含数据获取、坐标解析、点位判断全流程。


前言

在很多业务场景中,我们都需要用到地理围栏(GeoFence)功能:

  • 🚗 出行/物流:判断车辆是否进出某个城市/区域
  • 🏪 电商/本地生活:判断用户是否在配送范围内
  • 📍 营销活动:基于地理位置推送定向活动
  • 🏭 安防监控:判断人员/设备是否越界
  • 🛵 共享出行:电动车禁停区、限行区管控

传统方案通常需要手动在地图上画多边形,成本高且不够精准。

本文的方案是:直接从阿里 DataV 抓取官方行政区划边界坐标,构建精准的城市/区县级地理围栏。


一、方案整体设计

scss 复制代码
Text
┌─────────────────────────────────────────────────────┐
│                    整体流程                          │
│                                                     │
│  DataV API  ──►  解析GeoJSON  ──►  存储边界坐标        │
│                                          │          │
│  用户坐标  ──────────────────────►  R-tree算法判断     │
│                                          │          │
│                                     在围栏内/外       │
└─────────────────────────────────────────────────────┘

技术选型:

模块 技术
边界数据源 Alibaba DataV GeoJSON API
坐标体系 GCJ-02(国内高德/腾讯地图标准)
判断算法 R-tree
后端语言 Java / Python(本文以Java为主)
数据存储 Redis / MySQL

二、数据源:Alibaba DataV 行政区划 API

2.1 API 介绍

阿里 DataV 提供了完整的中国行政区划边界数据,地址如下:

Text 复制代码
https://geo.datav.aliyun.com/areas_v3/bound/{adcode}.json

参数说明:

参数 说明 示例
adcode 行政区划编码 100000(全国)、310000(上海市)、110105(朝阳区)

常用 adcode 参考:

Text 复制代码
    100000  →  全国
    110000  →  北京市
    110105  →  朝阳区
    310000  →  上海市
    310101  →  黄浦区
    440100  →  广州市
    440300  →  深圳市

💡 完整的 adcode 可以从国家统计局查询,也可以通过高德地图 API 获取。

2.2 返回数据结构(GeoJSON)

访问 https://geo.datav.aliyun.com/areas_v3/bound/440300.json(深圳市),返回:

Json 复制代码
    {
      "type": "FeatureCollection",
      "features": [
        {
          "type": "Feature",
          "properties": {
            "adcode": 440300,
            "name": "深圳市",
            "center": [114.085947, 22.547],
            "centroid": [114.054495, 22.542468],
            "childrenNum": 9,
            "level": "city",
            "parent": { "adcode": 440000 },
            "subFeatureIndex": 0,
            "acroutes": [100000, 440000]
          },
          "geometry": {
            "type": "MultiPolygon",
            "coordinates": [
              [
                [
                  [113.817451, 22.638359],
                  [113.818071, 22.637468],
                  // ... 大量坐标点
                  [113.817451, 22.638359]  // 闭合回起点
                ]
              ]
            ]
          }
        }
      ]
    }

2.3 注意 MultiPolygon vs Polygon

DataV 返回的 geometry 类型有两种:

  • Polygon:单个多边形(适用于形状简单的区域)
  • MultiPolygon:多个多边形组合(适用于有岛屿、飞地的区域)

判断时需要对所有子多边形都做检测,任意一个包含则视为在围栏内


三、获取并解析边界数据

3.1 HTTP 请求获取 GeoJSON

Maven 依赖:

Xml 复制代码
    <dependencies>
        <!-- HTTP 请求 -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.14</version>
        </dependency>
        <!-- JSON 解析 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.32</version>
        </dependency>
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.28</version>
        </dependency>
    </dependencies>

GeoFence 数据库模型:

java 复制代码
    /**
     * 地理围栏实体
     */
    @Data
    @Builder
    @TableName("geo_fence")
    public class GeoFenceDO {
        
        /** 行政区划编码 */
        private String code;
        
        /** 区域名称 */
        private String name;
        
        /** 行政级别:country/province/city/district */
        private String level;
        
        /** 中心点 */
        private String center;
        
        /** 上级行政区划编码 */
        private String parentCode;
        
        /** 坐标集类型 MultiPolygon或Polygon */
        private String geoType;
        
        /** 
         * 边界多边形列表(MultiPolygon)
         * 外层List:多个多边形
         * 内层List:单个多边形的顶点坐标
         */
        private String coordinates;
    }

GeoFence 数据库服务:

java 复制代码
package com.example.geofence.service;

import java.util.List;
import org.springframework.stereotype.Service;
import com.example.geofence.entity.GeoFenceDO;
import com.example.geofence.mapper.GeoFenceMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;

/**
 * 行政区域围栏数据库服务
 */
@Slf4j
@Service
public class GeoFenceDbService {

	@Resource
	GeoFenceMapper geoFenceMapper;

	public void save(GeoFenceDO entīty) {
	    geoFenceMapper.insert(entīty);
	}

	public void update(GeoFenceDO entīty) {
	    geoFenceMapper.updateById(entīty);
	}

	public GeoFenceDO getByCode(String code) {
	    return geoFenceMapper.selectByCode(code);
	}

	public List<GeoFenceDO> findByParentCode(String parentCode) {
	    return geoFenceMapper.selectByParentCode(parentCode);
	}
	
	public List<GeoFenceDO> findByAll() {
	    return geoFenceMapper.selectByAll();
	}
	
	public List<GeoFenceDO> findByCodeList(List<String> codeList) {
	    return geoFenceMapper.selectByCodeList(codeList);
	}
}

DataV数据获取并存储服务:

java 复制代码
package com.example.geofence.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.example.geofence.entity.GeoFenceDO;

import cn.hutool.http.HttpUtil;
import jakarta.annotation.PostConstruct;

/**
 * Loads province/city/district boundaries at startup
 * for fast point-in-polygon queries.
 *
 * Cache strategy:
 *   1. If the SQLite region table is non-empty, load from DB (fast path).
 *   2. Otherwise fetch from geo.datav.aliyun.com, persist to SQLite, then build index.
 *
 * Loading hierarchy from datav API:
 *   100000_full.json  → 34 provinces
 *   {province}_full.json → cities / direct districts
 *   {city}_full.json  → districts  (only when childrenNum > 0)
 */
@Service
public class DatavLoadService {

    private static final Logger log = LoggerFactory.getLogger(DatavLoadService.class);

    private static final String CHINA_ADCODE = "100000";

    private final GeoFenceDbService geoFenceDbService;

    @Value("${geofence.load-on-startup:true}")
    private boolean loadOnStartup;

    public DatavLoadService(GeoFenceDbService geoFenceDbService) {
        this.geoFenceDbService = geoFenceDbService;
    }

    @PostConstruct
    public void init() {
      if (!loadOnStartup) return;
      log.info("Initializing geofence ...");
      long t = System.currentTimeMillis();
      doUpdateProvince();
      doUpdateCity();
      doUpdateDistrict();
      log.info("DataV ready, built in {} ms", System.currentTimeMillis() - t);
    }
    
    public void doUpdateProvince() {
      log.info("正在更新全国数据");
      doUpdate(CHINA_ADCODE);
    }
	
    public void doUpdateCity() {
      geoFenceDbService.findByParentCode(CHINA_ADCODE).forEach(areaGeo -> {
	      String adcode = areaGeo.getCode();
	      String name = areaGeo.getName();
	      log.info("正在更新城市数据: {} - {}", adcode, name);
	      doUpdate(adcode);
	  });
    }
	
    public void doUpdateDistrict() {
	    geoFenceDbService.findByParentCode(CHINA_ADCODE).forEach(provinceAreaGeo -> {
         String provinceAdcode = provinceAreaGeo.getCode();
         geoFenceDbService.findByParentCode(provinceAdcode).forEach(cityAreaGeo -> {
           String cityAdcode = cityAreaGeo.getCode();
           String cityName = cityAreaGeo.getName();
           log.info("正在更新城市数据: {} - {}", cityAdcode, cityName);
           try {
             doUpdate(cityAdcode);
           } catch (Exception e) {
             log.error("解析错误,跳过数据: {}", cityAdcode);
           }
	    });		
	  });
    }
	
    public void doUpdateDistrict(String cityAdcode) {
        geoFenceDbService.findByParentCode(cityAdcode).forEach(areaGeo -> {
            String adcode = areaGeo.getCode();
            String name = areaGeo.getName();
            log.info("正在更新区域数据: {} - {}", adcode, name);
            try {
                doUpdate(adcode);
            } catch (Exception e) {
                log.error("解析错误,跳过数据: {}", areaGeo.getCode());
            }
      });
    }

    public void doUpdate(String code) {
        try {
                String url = "https://geo.datav.aliyun.com/areas_v3/bound/" + code + "_full.json"; // 替换为实际URL

                String result = HttpUtil.get(url);

                JSONObject jsonObject = JSONObject.parseObject(result);
                JSONArray features = jsonObject.getJSONArray("features");
                if (features != null) {
                    for (int i = 0; i < features.size(); i++) {
                        JSONObject feature = features.getJSONObject(i);
                        JSONObject properties = feature.getJSONObject("properties");
                        JSONObject geometry = feature.getJSONObject("geometry");
                        String adcode = properties.getString("adcode");
                        GeoFenceDO existingAreaGeo = geoFenceDbService.getByCode(adcode);
                        if (existingAreaGeo != null) {
                            log.info("--------------跳过已经存在数据: {}", adcode);
                            continue;
                        }
                        if(adcode.equals("100000_JD")) {
                            log.info("--------------跳过数据: {}", adcode);
                            continue;
                        }
                        JSONArray center = properties.getJSONArray("center");
                        JSONObject parent =  properties.getJSONObject("parent");
			
                        GeoFenceDO areaGeo = new GeoFenceDO();
                        areaGeo.setName(properties.getString("name"));
                        areaGeo.setCode(properties.getString("adcode"));
                        areaGeo.setCenter(center.toJSONString());
                        areaGeo.setLevel(properties.getString("level"));
                        areaGeo.setParentCode(parent.getString("adcode"));
                        
                        JSONArray coordinates = geometry.getJSONArray("coordinates");
                        String type = geometry.getString("type");
                        areaGeo.setGeoType(type);
                        areaGeo.setCoordinates(coordinates.toJSONString());
                        geoFenceDbService.save(areaGeo);
                    }
                }
		} catch (Exception e) {
			log.error("更新数据失败, 跳过: {}", code);
		}
	}
    
}

四、核心算法:基于 RTree 的地理围栏判断

4.1 为什么选择 RTree?

先看一下几种方案的对比:

算法 原理 时间复杂度 适合场景
射线投射法 逐边遍历判断 O(n) 单次判断、顶点少
RTree 空间索引 空间矩形树分区检索 O(log n) 高并发、多围栏、顶点多
GeoHash 网格化编码 O(1) 近似匹配、精度要求不高
MySQL ST_Contains 数据库空间函数 依赖索引 数据量小、低并发

RTree 核心优势:

markdown 复制代码
Text
传统射线法:每次判断都要遍历所有围栏的所有顶点
              → 100个围栏 × 1000个顶点 = 100,000次计算
RTree索引:   先通过空间分区快速缩小候选范围
              → 只对极少数候选围栏做精确判断
              → 性能提升 10x ~ 100x

RTree 空间原理:

4.2 引入 JTS + RTree 依赖

Xml 复制代码
    <dependencies>
        <!-- JTS 空间几何计算库,提供 Polygon、Point 等空间对象 -->
        <dependency>
            <groupId>org.locationtech.jts</groupId>
            <artifactId>jts-core</artifactId>
            <version>1.19.0</version>
        </dependency>
        
        <!-- HTTP 请求 -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.14</version>
        </dependency>
        
        <!-- JSON 解析 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.32</version>
        </dependency>
        
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.28</version>
        </dependency>
    </dependencies>

💡 JTS(Java Topology Suite)是 Java 生态中最成熟的空间几何库,内置了 RTree 空间索引、contains()intersects() 等空间关系判断能力。

4.3 数据库获取GeoJSON并解析 + 构建RTree空间索引

Java 复制代码
package com.example.geofence.service;

import java.util.ArrayList;
import java.util.List;

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.index.strtree.STRtree;
import org.springframework.stereotype.Service;

import com.alibaba.fastjson2.JSONArray;

import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class GeoFenceIndexService {

    @Resource
    GeoFenceDbService geoFenceDbService;	

    private GeometryFactory factory = new GeometryFactory();
    private STRtree index = new STRtree();
    
    public void init() {
        geoFenceDbService.findByAll().forEach(areaGeo -> {
            String geoType = areaGeo.getGeoType();
            if ("Polygon".equals(geoType)) {
                String coordinates = areaGeo.getCoordinates();
                log.info(areaGeo.getCode() + " - " + areaGeo.getName() + " - " + geoType);
                JSONArray oneArray = JSONArray.parse(coordinates);
                for (int one = 0; one < oneArray.size(); one++) {
                    JSONArray twoArray = oneArray.getJSONArray(one);
                    Coordinate[] shell = new Coordinate[twoArray.size()];
                    for (int two = 0; two < twoArray.size(); two++) {
                        JSONArray point = twoArray.getJSONArray(two);
                        double x = point.getDouble(0);
                        double y = point.getDouble(1);
                        shell[two] = new Coordinate(x, y);
                    }
                    Polygon polygon = factory.createPolygon(shell);
                    polygon.setUserData(areaGeo.getCode());
                    // 插入 R-tree(用 envelope)
                    index.insert(polygon.getEnvelopeInternal(), polygon);
                }
            } else if ("MultiPolygon".equals(geoType)) {
                String coordinates = areaGeo.getCoordinates();
                log.info(areaGeo.getCode() + " - " + areaGeo.getName() + " - " + geoType);
                JSONArray onwArray = JSONArray.parse(coordinates);
                for (int one = 0; one < onwArray.size(); one++) {
                    JSONArray twoArray = onwArray.getJSONArray(one);
                    for (int two = 0; two < twoArray.size(); two++) {
                        JSONArray threeArray = twoArray.getJSONArray(two);
                        Coordinate[] shell = new Coordinate[threeArray.size()];
                        for (int three = 0; three < threeArray.size(); three++) {
                            JSONArray point = threeArray.getJSONArray(three);
                            double x = point.getDouble(0);
                            double y = point.getDouble(1);
                            shell[three] = new Coordinate(x, y);
                        }
                        Polygon polygon = factory.createPolygon(shell);
                        polygon.setUserData(areaGeo.getCode());
                        // 插入 R-tree(用 envelope)
                        index.insert(polygon.getEnvelopeInternal(), polygon);
                    }
                }
            } else {
                log.warn("未知的地理数据类型: {}", geoType);
            }
        });
        log.warn("初始化完成,R-tree 索引中共有 {} 个元素", index.size());
    }
    
    public List<String> query(Double x, Double y) {
        // 查询点
        Point point = factory.createPoint(new Coordinate(x, y));
        // 1️⃣ R-tree 查询候选
        var candidates = index.query(point.getEnvelopeInternal());
        List<String> list = new ArrayList<>();
        // 2️⃣ 精确判断
        for (Object obj : candidates) {
            Polygon p = (Polygon) obj;
            if (p.contains(point)) {
                log.info("命中围栏" + p.getUserData());
                list.add((String) p.getUserData());
            }
        }
        return list;
	}
}

五、RTree 空间索引构建与围栏判断

上面4.3的GeoFenceIndexService的代码已经有Rtree的创建和围栏判断的代码,在这里我在举其他例子

Java 复制代码
import org.locationtech.jts.index.strtree.STRtree; 
import org.locationtech.jts.geom.Envelope; 
import org.locationtech.jts.geom.Point; 
import org.locationtech.jts.geom.GeometryFactory; 

public class RTreeExample {

    public static void main(String[] args) {
        STRtree rTree = new STRtree();
        GeometryFactory geometryFactory = new GeometryFactory(); 
        // 创建示例点 
        Point point1 = geometryFactory.createPoint(new Coordinate(1, 1));
        Point point2 = geometryFactory.createPoint(new Coordinate(2, 2));
        Point point3 = geometryFactory.createPoint(new Coordinate(3, 3));
        // 将点添加到R树中 
        rTree.insert(point1.getEnvelopeInternal(), point1);
        rTree.insert(point2.getEnvelopeInternal(), point2);
        rTree.insert(point3.getEnvelopeInternal(), point3); 
        
        // 查询范围内的点
        Envelope searchEnv = new Envelope(1.5, 3.0, 1.5, 3.0);
        List<Point> result = rTree.query(searchEnv);
        for (Point point : result) {
            System.out.println("查询到点: " + point);
        }
    }
}

六、启动预加载

6.1 启动初始化地理围栏数据

Java 复制代码
@Service
public class DatavLoadService {

    ...

    public void init() {
        if (!loadOnStartup) return;
        log.info("Initializing geofence ...");
        long t = System.currentTimeMillis();
        doUpdateProvince();
        doUpdateCity();
        doUpdateDistrict();
        log.info("DataV ready, built in {} ms", System.currentTimeMillis() - t);
    }
}

7.2 Spring Boot启动时初始化RTree索引

Java 复制代码
package com.example.geofence.config;

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import com.example.geofence.service.GeoFenceIndexService;
import com.example.geofence.service.DatavLoadService;


DatavLoadService datavLoadService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class GeoFenceInitializer implements CommandLineRunner {

    @Resource
    DatavLoadService datavLoadService;
    
    @Resource
    GeoFenceIndexService geoFenceIndexService;

    @Override
    public void run(String... args) {
        log.info("========== 开始获取DataV地理围栏数据 ==========");
        long start = System.currentTimeMillis();
        try {
            datavLoadService.init();
            long cost = System.currentTimeMillis() - start;
            log.info("========== DataV地理围栏数据处理完成,耗时 {}ms ==========", cost);
        } catch (Exception e) {
            log.error("DataV地理围栏数据处理失败,系统将降级运行", e);
        }
        log.info("========== 开始预加载数据库地理围栏数据 ==========");
        start = System.currentTimeMillis();
        try {
            geoFenceIndexService.init();
            long cost = System.currentTimeMillis() - start;
            log.info("========== 数据库地理围栏预加载完成,耗时 {}ms ==========", cost);
        } catch (Exception e) {
            log.error("数据库地理围栏预加载失败,系统将降级运行", e);
        }

    }
}

七、Controller 接口层

Java 复制代码
package com.example.geofence.controller;

import java.util.List;
import java.util.stream.Collectors;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.example.geofence.model.Result
import com.example.geofence.entity.GeoFenceDO;
import com.example.geofence.model.GeoFenceVO;
import com.example.geofence.service.GeoFenceDbService;
import com.example.geofence.service.GeoFenceIndexService;

import jakarta.annotation.Resource;

@RestController
@RequestMapping("/api/geofence")
public class GeoFenceController {

    @Resource
    GeoFenceIndexService geoFenceIndexService;
	
    @Resource
    GeoFenceDbService geoFenceDbService;
	
    /**
     * Query which province/city/district contains the given coordinate.
     *
     * GET /api/geofence/query?lon=116.3974&lat=39.9093
     */
    @GetMapping("/query")
    public Result<List<GeoFenceVO>> query(
            @RequestParam double lon,
            @RequestParam double lat) {

        if (lon < -180 || lon > 180 || lat < -90 || lat > 90) {
            return Result.fail("Invalid coordinates");
        }

        List<String> areaCodeList = geoFenceIndexService.query(lon, lat);
        if (areaCodeList.isEmpty()) {
           return Result.fail("没有命中任何围栏");
        }
        List<GeoFenceDO> fenceList = geoFenceDbService.findByCodeList(areaCodeList);
        List<GeoFenceVO> resultList = fenceList.stream().map(item -> convertToResultVo(item)).collect(Collectors.toList());
        return Result.ok(resultList);
    }
    
    private GeoFenceVO convertToResultVo(GeoFenceDO item) {
		GeoFenceVO result = new GeoFenceVO();
		result.setCode(item.getCode());
		result.setName(item.getName());
		result.setLevel(item.getLevel());
		result.setParentCode(item.getParentCode());
		return result;
	}
    
}

八、VO & 统一返回封装

8.1 区域 VO

Java 复制代码
package com.example.geofence.model;

import java.io.Serializable;
import lombok.Data;

@Data
public class GeoFenceVO implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 名称
     */
    private String name;

    /**
     * 区域编号
     */
    private String code;

    /**
     * 区域层级
     */
    private String level;

    /**
     * 区域上级编号
     */
    private String parentCode;

}

8.2 统一返回体

Java 复制代码
    /**
     * 统一接口返回体
     */
    @Data
    @Builder
    public class Result<T> {
        /** 状态码:200成功,其他失败 */
        private int code;
        /** 提示信息 */
        private String message;
        /** 返回数据 */
        private T data;
        public static <T> Result<T> ok(T data) {
            return Result.<T>builder()
                    .code(200)
                    .message("success")
                    .data(data)
                    .build();
        }
        public static <T> Result<T> fail(String message) {
            return Result.<T>builder()
                    .code(500)
                    .message(message)
                    .build();
        }
    }

九、单元测试

Java 复制代码
package com.example.geofence;

import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List; 
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import com.example.geofence.entity.GeoFenceDO;
import com.example.geofence.service.GeoFenceDbService;
import com.example.geofence.service.GeoFenceIndexService;
import lombok.extern.slf4j.Slf4j; 

/**
* 地理围栏服务单元测试
*/

@Slf4j
@SpringBootTest
@TestPropertySource(properties = "geofence.load-on-startup=true")
public class GeoFenceIndexServiceTest {

    @Autowired
    private GeoFenceIndexService geoFenceIndexService;

    @Autowired
    private GeoFenceDbService geoFenceDbService;

    /**
    * 测试点位(深圳各区典型坐标)
    * 格式:{ 名称, 经度, 纬度 }
    */

    private static final Object[][] TEST_POINTS = {
        {"深圳市民中心(福田)", 114.0579, 22.5494},
        {"深圳湾公园(南山)", 113.9697, 22.5084},
        {"大剧院(罗湖)", 114.1218, 22.5490},
        {"宝安机场(宝安)", 113.8213, 22.6329},
        {"深圳北站(龙华)", 114.0293, 22.6087},
        {"香港(不在深圳)", 114.1694, 22.3193},
        {"东莞(不在深圳)", 113.7518, 23.0207},

    };

    @Test

    public void testQuery() {
        log.info("========== 开始测试 query ==========");
        for (Object[] point : TEST_POINTS) {
            String name = (String) point[0];
            double lng = (double) point[1];
            double lat = (double) point[2];
            List<String> codeList = geoFenceIndexService.query(lng, lat);
            if(codeList.isEmpty()) {
                log.info("【{}】未匹配到围栏(符合预期)", name);
            }
            List<GeoFenceDO> fenceList = geoFenceDbService.findByCodeList(codeList);
            for (GeoFenceDO fence : fenceList) {
                log.info("【{}】匹配结果:{}({})", name, fence.getName(), fence.getCode());
            }
        }
    }

    @Test
    public void testPerformance() {
        log.info("========== 开始性能测试 ==========");
        int total = 10000;
        double lng = 114.0579;
        double lat = 22.5494;
        long start = System.currentTimeMillis();
        for (int i = 0; i < total; i++) {
            geoFenceIndexService.query(lng, lat);
        }
        long cost = System.currentTimeMillis() - start;
        log.info("RTree 性能测试:{}次查询,总耗时 {}ms,平均 {}ms/次", total, cost, (double) cost / total);
        // 平均每次查询应在 1ms 以内
        assertTrue((double) cost / total < 1.0, "RTree 查询性能不达标,平均耗时 " + (double) cost / total + "ms");
    }
}

十、整体流程回顾

scss 复制代码
Text
应用启动
   │
   ▼
GeoFenceInitializer.run()
   │
   ├─→ 请求 DataV API → 解析 GeoJSON → 写入本地SQLite数据库
   │         
   └─→ 读取本地SQLite数据 → 解析 geometryJson → 重建 JTS Geometry → 插入STRtree构建索引
                                                   │
                                                   └─→ 构建 JTS Polygon/MultiPolygon
业务查询请求(lng, lat)                        
   │
   ▼
GeoFenceIndexService.query(lng, lat) 
   │
   ├─→ STRtree.query(envelope)    ← RTree 粗筛(O(log n))
   │         │
   │         └─→ 返回 MBR 相交的候选围栏列表
   │
   └─→ Geometry.contains(point)  ← JTS 精确判断
             │
             └─→ 返回最终匹配结果

十一、注意事项

问题 原因 解决方案
Geometry 无效 部分行政区划边界数据存在自相交 geometry.buffer(0) 修复
边界点判断不准 坐标系不一致(GCJ-02 vs WGS-84) 统一转换为 WGS-84 后再判断
STRtree 线程安全 构建完成后只读,查询是线程安全的 初始化完成后不要再 insert
内存占用过高 加载区域过多或顶点密度过大 按需加载、适当简化几何精度
DataV 请求限流 短时间大量请求会被限制 本地缓存 + 请求间隔 200ms

至此,完整的基于 DataV + JTS STRtree 的地理围栏方案全部实现完毕。

完整源码:gitee.com/waimaitech/...

如果你觉得这篇内容对你有帮助:

  • 👍 点赞支持一下作者熬的这些夜
  • 收藏起来下次选型时翻出来
  • 💬 评论区聊聊你现在在用什么工具?踩过哪些坑?

我们下期见! 🚀


🍱 顺便推荐:如果你和我一样经常加班点外卖,可以微信搜索小程序「美豚外卖」------美团/淘宝闪购订单额外返利,一个月省下的钱够再订一个 AI 编程工具。

相关推荐
覆东流1 小时前
第7天:Python小项目
开发语言·后端·python
xiaogg36782 小时前
springcloud oauth2 自定义token实现
spring boot·后端·spring cloud
pixcarp2 小时前
Nginx实战部署与踩坑总结 附带详细配置教程
服务器·前端·后端·nginx·golang
神奇小汤圆2 小时前
JAVA 面经汇总2026最新版,1100+ 大厂面试题附答案详解
后端
程序员老邢2 小时前
【技术底稿 23】Ollama + Docker + Ubuntu 部署踩坑实录:网络通了,参数还在调
java·经验分享·后端·ubuntu·docker·容器·milvus
JackSparrow4142 小时前
彻底理解Java NIO(一)C语言实现 单进程+多进程+多线程 阻塞式I/O 服务器详解
java·linux·c语言·网络·后端·tcp/ip·nio
小江的记录本2 小时前
【微服务与云原生架构】Serverless架构、FaaS/BaaS、核心原理、优缺点
java·后端·微服务·云原生·架构·系统架构·serverless
神奇小汤圆2 小时前
阿里云社招一面:数据库中有 1000 万数据的时候怎么分页查询?
后端
威迪斯特3 小时前
Cobra框架:Go语言命令行开发的现代化利器
开发语言·前端·后端·golang·cobra·交互模型·命令行框架