当前位置: 首页 > news >正文

MongoDB GEO 项目场景 ms-scope 实战

大家好,我是此林。

关于 MongoDB,我们在这篇文章中已经详细讲述了其基本增删改查、SpringBoot 整合等内容。一文快速入门 MongoDB 、MongoDB 8.2 下载安装、增删改查操作、索引、SpringBoot整合 Spring Data MongoDB

今天我们分享的是 MongoDB GEO 项目场景实战。

1. 场景描述

小王在某物流平台上下单,系统需要快速找到能为他服务的快递员。通过对比小王的位置与快递员的服务范围,平台能准确匹配到合适人员,解决了订单分配效率低、匹配不精准的问题。

那系统具体怎么做呢?

首先快递网点会有个作业范围,比如图中红色框标注的多边形,一个由多个坐标点组成的多边形,并且必须是闭合的多边形。小王位置为灰色的竖线。

这个就比较适合用MongoDB来存储。

用户小王下了订单,如何找到属于该服务范围内的快递员呢?我们使用MongoDB的$geoIntersects查询操作,其原理就是查找小王的位置坐标点与哪个多边形有交叉,这个就是为其服务的快递员。

2. 定义 MongoDB 实体

机构和快递员的作业范围逻辑一般是一样的,所以可以共存一张表中,通过type进行区分,1-机构,2-快递员。

/*** 服务范围实体*/
@Data
@Document("service_scope")
public class ServiceScopeEntity {@Id@JsonIgnoreprivate ObjectId id;/*** 业务id,可以是机构或快递员*/@Indexedprivate Long bid;/*** 类型 {@link com.ms.scope.enums.ServiceTypeEnum}*/@Indexedprivate Integer type;/*** 多边形范围,是闭合的范围,开始经纬度与结束经纬度必须一样* x: 经度,y:纬度*/@GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE)private GeoJsonPolygon polygon;private Long created; //创建时间private Long updated; //更新时间
}

这里面的 @Document("service_scope")  指定 MongoDB 集合名称(类似于关系型数据库中的表名),里面的每条记录对应一片服务区域。

id:ObjectId 类型。ObjectId 是 MongoDB 内置的唯一标识类型,@Id 表示该字段是主键,@JsonIgnore 表示序列化为 JSON 时忽略该字段(对外不暴露)。

bid:某个网点 ID,或某个快递员的 ID, @Indexed 表示该字段会在 MongoDB 中建立索引,加快查询速度。

type:1=网点,2=快递员,也加了索引,提高查询效率。

polygon:GeoJsonPolygon 类型。用 GeoJsonPolygon 来存储一个多边形(由多个经纬度点组成)。多边形必须闭合,即起点和终点的经纬度必须相同。@GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE) 用于建立地理空间索引(GEO_2DSPHERE)。

created:创建时间(时间戳,毫秒值)

updated:更新时间(时间戳,毫秒值)

这里再贴一下服务类型枚举:

import cn.hutool.core.util.EnumUtil;/*** 服务类型枚举*/
public enum ServiceTypeEnum {ORGAN(1, "机构"),COURIER(2, "快递员");/*** 类型编码*/private final Integer code;/*** 类型值*/private final String value;ServiceTypeEnum(Integer code, String value) {this.code = code;this.value = value;}public Integer getCode() {return code;}public String getValue() {return value;}public static ServiceTypeEnum codeOf(Integer code) {return EnumUtil.getBy(ServiceTypeEnum::getCode, code);}
}

3. ScopeService 接口编写

我们 Service 要编写哪些方法呢?

主要有下面几个:

1. 新增或更新服务范围。即新增或更新服务范围的多边形各个顶点坐标。

2. 根据主键id、业务id、类型删除+查询服务范围

3. 根据坐标点查询所属哪个服务范围。这个坐标点就是用户位置坐标。

4. 根据用户详细地址查询所属的服务范围。

所以我们定义如下接口:

/*** 服务范围Service*/
public interface ScopeService {/*** 新增或更新服务范围** @param bid     业务id* @param type    类型* @param polygon 多边形坐标点* @return 是否成功*/Boolean saveOrUpdate(Long bid, ServiceTypeEnum type, GeoJsonPolygon polygon);/*** 根据主键id删除数据** @param id 主键* @return 是否成功*/Boolean delete(String id);/*** 根据业务id和类型删除数据** @param bid  业务id* @param type 类型* @return 是否成功*/Boolean delete(Long bid, ServiceTypeEnum type);/*** 根据主键查询数据** @param id 主键* @return 服务范围数据*/ServiceScopeEntity queryById(String id);/*** 根据业务id和类型查询数据** @param bid  业务id* @param type 类型* @return 服务范围数据*/ServiceScopeEntity queryByBidAndType(Long bid, ServiceTypeEnum type);/*** 根据坐标点查询所属的服务对象** @param type  类型* @param point 坐标点* @return 服务范围数据*/List<ServiceScopeEntity> queryListByPoint(ServiceTypeEnum type, GeoJsonPoint point);/*** 根据详细地址查询所属的服务对象** @param type    类型* @param address 详细地址,如:石家庄市桥西区XXX小区XX号楼XXX室* @return 服务范围数据*/List<ServiceScopeEntity> queryListByPoint(ServiceTypeEnum type, String address);
}

4. ScopeController API 编写

@Api(tags = "服务范围")
@RestController
@RequestMapping("/scopes")
@Validated
public class ScopeController {@Resourceprivate ScopeService scopeService;/*** 新增或更新服务服务范围** @return REST标准响应*/@ApiOperation(value = "新增/更新", notes = "新增或更新服务服务范围")@PostMappingpublic ResponseEntity<Void> saveScope(@RequestBody ServiceScopeDTO serviceScopeDTO) {ServiceScopeEntity serviceScopeEntity = EntityUtils.toEntity(serviceScopeDTO);Long bid = serviceScopeEntity.getBid();ServiceTypeEnum type = ServiceTypeEnum.codeOf(serviceScopeEntity.getType());Boolean result = this.scopeService.saveOrUpdate(bid, type, serviceScopeEntity.getPolygon());if (result) {return ResponseEntityUtils.ok();}return ResponseEntityUtils.error();}/*** 删除服务范围** @param bid  业务id* @param type 类型* @return REST标准响应*/@ApiImplicitParams({@ApiImplicitParam(name = "bid", value = "业务id,可以是机构或快递员", dataTypeClass = Long.class),@ApiImplicitParam(name = "type", value = "类型,1-机构,2-快递员", dataTypeClass = Integer.class)})@ApiOperation(value = "删除", notes = "删除服务范围")@DeleteMapping("{bid}/{type}")public ResponseEntity<Void> delete(@NotNull(message = "bid不能为空") @PathVariable("bid") Long bid,@NotNull(message = "type不能为空") @PathVariable("type") Integer type) {Boolean result = this.scopeService.delete(bid, ServiceTypeEnum.codeOf(type));if (result) {return ResponseEntityUtils.ok();}return ResponseEntityUtils.error();}/*** 查询服务范围** @param bid  业务id* @param type 类型* @return 服务范围数据*/@ApiImplicitParams({@ApiImplicitParam(name = "bid", value = "业务id,可以是机构或快递员", dataTypeClass = Long.class),@ApiImplicitParam(name = "type", value = "类型,1-机构,2-快递员", dataTypeClass = Integer.class)})@ApiOperation(value = "查询", notes = "查询服务范围")@GetMapping("{bid}/{type}")public ResponseEntity<ServiceScopeDTO> queryServiceScope(@NotNull(message = "bid不能为空") @PathVariable("bid") Long bid,@NotNull(message = "type不能为空") @PathVariable("type") Integer type) {ServiceScopeEntity serviceScopeEntity = this.scopeService.queryByBidAndType(bid, ServiceTypeEnum.codeOf(type));return ResponseEntityUtils.ok(EntityUtils.toDTO(serviceScopeEntity));}/*** 地址查询服务范围** @param type    类型,1-机构,2-快递员* @param address 详细地址,如:北京市昌平区金燕龙办公楼传智教育总部* @return 服务范围数据列表*/@ApiImplicitParams({@ApiImplicitParam(name = "type", value = "类型,1-机构,2-快递员", dataTypeClass = Integer.class),@ApiImplicitParam(name = "address", value = "详细地址,如:北京市昌平区金燕龙办公楼传智教育总部", dataTypeClass = String.class)})@ApiOperation(value = "地址查询服务范围", notes = "地址查询服务范围")@GetMapping("address")public ResponseEntity<List<ServiceScopeDTO>> queryListByAddress(@NotNull(message = "type不能为空") @RequestParam("type") Integer type,@NotNull(message = "address不能为空") @RequestParam("address") String address) {List<ServiceScopeEntity> serviceScopeEntityList = this.scopeService.queryListByPoint(ServiceTypeEnum.codeOf(type), address);return ResponseEntityUtils.ok(EntityUtils.toDTOList(serviceScopeEntityList));}/*** 位置查询服务范围** @param type      类型,1-机构,2-快递员* @param longitude 经度* @param latitude  纬度* @return 服务范围数据列表*/@ApiImplicitParams({@ApiImplicitParam(name = "type", value = "类型,1-机构,2-快递员", dataTypeClass = Integer.class),@ApiImplicitParam(name = "longitude", value = "经度", dataTypeClass = Double.class),@ApiImplicitParam(name = "latitude", value = "纬度", dataTypeClass = Double.class)})@ApiOperation(value = "位置查询服务范围", notes = "位置查询服务范围")@GetMapping("location")public ResponseEntity<List<ServiceScopeDTO>> queryListByAddress(@NotNull(message = "type不能为空") @RequestParam("type") Integer type,@NotNull(message = "longitude不能为空") @RequestParam("longitude") Double longitude,@NotNull(message = "latitude不能为空") @RequestParam("latitude") Double latitude) {List<ServiceScopeEntity> serviceScopeEntityList = this.scopeService.queryListByPoint(ServiceTypeEnum.codeOf(type), new GeoJsonPoint(longitude, latitude));return ResponseEntityUtils.ok(EntityUtils.toDTOList(serviceScopeEntityList));}
}

Spring MVC 的 @RequestParam@PathVariable 默认不会自动生成 Swagger 文档里参数的详细信息。

@ApiImplicitParam 是 Swagger(API 文档生成工具) 的注解,用于描述接口的请求参数,让 Swagger UI 能够自动生成可交互的 API 文档。

  • name:参数名,对应接口方法里的 @RequestParam@PathVariable 名称。
  • value:参数说明,会显示在 Swagger 文档里。
  • dataTypeClass:参数的数据类型(如 Integer.classString.class)。
  • 还可以写 required = true 表示必填。

5. ScopeServiceImpl 实现

1. 删除或更新服务范围

    @Overridepublic Boolean saveOrUpdate(Long bid, ServiceTypeEnum type, GeoJsonPolygon polygon) {Query query = Query.query(Criteria.where("bid").is(bid).and("type").is(type.getCode())); //构造查询条件ServiceScopeEntity serviceScopeEntity = this.mongoTemplate.findOne(query, ServiceScopeEntity.class);if (ObjectUtil.isEmpty(serviceScopeEntity)) {//新增serviceScopeEntity = new ServiceScopeEntity();serviceScopeEntity.setBid(bid);serviceScopeEntity.setType(type.getCode());serviceScopeEntity.setPolygon(polygon);serviceScopeEntity.setCreated(System.currentTimeMillis());serviceScopeEntity.setUpdated(serviceScopeEntity.getCreated());} else {//更新serviceScopeEntity.setPolygon(polygon);serviceScopeEntity.setUpdated(System.currentTimeMillis());}try {this.mongoTemplate.save(serviceScopeEntity);return true;} catch (Exception e) {log.error("新增/更新服务范围数据失败! bid = {}, type = {}, points = {}", bid, type, polygon.getPoints(), e);}return false;}

2. 删除服务范围

    @Overridepublic Boolean delete(String id) {Query query = Query.query(Criteria.where("id").is(new ObjectId(id))); //构造查询条件return this.mongoTemplate.remove(query, ServiceScopeEntity.class).getDeletedCount() > 0;}@Overridepublic Boolean delete(Long bid, ServiceTypeEnum type) {Query query = Query.query(Criteria.where("bid").is(bid).and("type").is(type.getCode())); //构造查询条件return this.mongoTemplate.remove(query, ServiceScopeEntity.class).getDeletedCount() > 0;}

3. 基础查询

    @Overridepublic ServiceScopeEntity queryById(String id) {return this.mongoTemplate.findById(new ObjectId(id), ServiceScopeEntity.class);}@Overridepublic ServiceScopeEntity queryByBidAndType(Long bid, ServiceTypeEnum type) {Query query = Query.query(Criteria.where("bid").is(bid).and("type").is(type.getCode())); //构造查询条件return this.mongoTemplate.findOne(query, ServiceScopeEntity.class);}

4. 根据用户经纬度坐标查询所属哪个服务范围

    @Overridepublic List<ServiceScopeEntity> queryListByPoint(ServiceTypeEnum type, GeoJsonPoint point) {Query query = Query.query(Criteria.where("polygon").intersects(point).and("type").is(type.getCode()));return this.mongoTemplate.find(query, ServiceScopeEntity.class);}

GeoJsonPoint point 是用户传过来的经纬度坐标,比如:

new GeoJsonPoint(116.395645, 39.929986) // 北京天安门

Criteria.where("polygon").intersects(point)

  • polygon 是我们之前在 ServiceScopeEntity 中定义的服务范围多边形(闭合区域)。

  • intersects(point) 的意思是:查询所有 polygon 多边形包含该点的记录

  • MongoDB 的地理空间查询会自动判断点是否落在多边形内部。

5. 根据详细地址查询坐标

    @Overridepublic List<ServiceScopeEntity> queryListByPoint(ServiceTypeEnum type, String address) {//根据详细地址查询坐标GeoResult geoResult = this.mapTemplate.opsForBase().geoCode(ProviderEnum.AMAP, address, null);Coordinate coordinate = geoResult.getLocation();return this.queryListByPoint(type, new GeoJsonPoint(coordinate.getLongitude(), coordinate.getLatitude()));}

这里我们要调用地图服务的接口(比如高德或百度地图服务)。只要传入地址,高德或百度都可以返回经纬度。具体接口调用可以看看官方文档。

6. 测试

@SpringBootTest
public class ScopeServiceTest {@Resourceprivate ScopeService scopeService;@Testvoid saveOrUpdate() {List<Point> pointList = Arrays.asList(new Point(116.340064,40.061245),new Point(116.347081,40.061836),new Point(116.34751,40.05842),new Point(116.342446,40.058092),new Point(116.340064,40.061245));Boolean result = this.scopeService.saveOrUpdate(2L, ServiceTypeEnum.ORGAN, new GeoJsonPolygon(pointList));System.out.println(result);}@Testvoid testQueryListByPoint() {GeoJsonPoint point = new GeoJsonPoint(116.344828,40.05911);List<ServiceScopeEntity> serviceScopeEntities = this.scopeService.queryListByPoint(ServiceTypeEnum.ORGAN, point);serviceScopeEntities.forEach(serviceScopeEntity -> System.out.println(serviceScopeEntity));}@Testvoid testQueryListByPoint2() {String address = "石家庄市桥西区永德小区2号楼802室";List<ServiceScopeEntity> serviceScopeEntities = this.scopeService.queryListByPoint(ServiceTypeEnum.ORGAN, address);serviceScopeEntities.forEach(serviceScopeEntity -> System.out.println(serviceScopeEntity));}
}

最终返回类似如下的匹配到的服务范围。

7. 其他思考点

其实目前代码所做,基本上是确定出用户被分派给哪个机构,后续还需要把派件任务委派给快递员。

确定好了起始的 网点,还需要确定终点网点。

同样的,取 收件人地址 → 转坐标 (lng, lat),查询 MongoDB:找哪个机构的服务范围包含这个点,结果 = 终点网点。

中途还会有 路线规划(用于调度),起点网点 → 中转中心(多级)→ 终点网点。这些后续会介绍。

为什么会用 MongoDB 存储服务范围?

  • 快递员、网点、机构都有“服务范围”,这些范围通常是多边形(polygon)。
  • 如果用 MySQL 来存:我们可能想到用 JSON 字符串存经纬度点,但是查询时需要自己写数学算法判断点是否在多边形内,比较复杂且慢。
  • MongoDB 支持 GeoJSON 和 地理空间索引,自带点、多边形、圆形的查询能力,查询效率高。

常见应用:

  • “附近的餐馆/快递员/加油站”
  • “这个地址属于哪个网点”
  • “用户当前位置在不在配送范围”

所以,这类“地理空间场景”适合 MongoDB,既然有开箱即用的工具为啥不用呢?

不过,话虽然这么说。

MySQL 从 5.7 开始其实也支持 GIS,可以存储 POINTPOLYGON 等几何类型。除了 MySQL、MongoDB,还有其他的技术实现对比:

技术支持点查询支持多边形性能适用场景
MongoDB中等普通互联网业务,范围查询、附近的人
MySQL GIS一般小规模场景,已有 MySQL 就能用
PostGIS需要复杂地理计算的大型系统
Elasticsearch搜索 + 地理混合场景
Redis GEO❌(无多边形)超高附近的人,高并发场景

Redis GEO 性能超高的原因还是主要是因为它是内存计算。

不管哪种技术选型,适合自己的业务才是王道,项目里之所以用 MongoDB 其实也是因为 MongoDB 不仅仅用于地理范围搜索,还有其他用途。

当然比如说项目里有百万千万级别的全文搜索 + 地理位置搜索场景,用 Elasticsearch 也是可以的。

今天的分享就到这里了。

我是此林,关注我吧!带你看不一样的世界!

http://www.dtcms.com/a/438349.html

相关文章:

  • 医美三方网站怎么做网站外链建设可以提升网站
  • 在算法比赛中高效处理多行输入
  • MySQL 管理与配置详解:从安装到架构解析
  • 构建工具webpack
  • 深入理解 Rust 的内存模型:变量、值与指针
  • 单位网站备案要等多久湖南住建云网站
  • 浦口区网站建设售后服务有没有做卡商的网站
  • 可达鸭模拟赛1
  • LINUX复习资料(一)
  • 专业做酒的网站有哪些互联网营销培训班 考证
  • 串扰09-Er与串扰
  • HarmonyOS应用开发深度解析:ArkTS语法与组件化开发实践
  • 免费的简历制作网站100大看免费行情的软件
  • seo站内优化站外优化vs做网站如何输出
  • 【学习K230-例程43】GT6700-AI视觉-人体关键点检测
  • 网站域名所有权wordpress文章截断
  • HTMLz设计一个电压电流波形显示界面
  • 大模型原理与实践:第三章-预训练语言模型详解_第1部分-Encoder-only(BERT、RoBERTa、ALBERT)
  • MySQL 慢查询日志slow query log
  • 刷赞抖音推广网站长沙网站seo分析
  • 怎么做网站界面设计如何推广店铺呢
  • C++笔记(面向对象)六(4+2C++11)个缺省函数详解
  • CTFHub 信息泄露通关笔记7:Git泄露 Log
  • 【Svelte】如何自定义路径别名(alias)?
  • 公司做哪个网站比较好西安外贸网站建设公司
  • DeepSeek-V3.2-Exp + PH8:国产大模型的性价比革命
  • 第二十三讲:特殊类和类型转换
  • 如何区分数学中的定理、引理、命题?
  • 森东网站建设南昌网站排名优化软件
  • 深圳网站建设制作开发公司开发公司app