文章目录
- 前言
- 一、地图服务
-
- [1.1 为啥要封装地图服务?](#1.1 为啥要封装地图服务?)
- [1.2 地图服务基础功能分析](#1.2 地图服务基础功能分析)
- [1.3 接入腾讯地图服务](#1.3 接入腾讯地图服务)
-
- [1.3.1 登录、创建AccessKey](#1.3.1 登录、创建AccessKey)
- [1.4 地图数据初始化](#1.4 地图数据初始化)
-
- [1.4.1 数据表结构设计](#1.4.1 数据表结构设计)
- [1.4.2 初始化地图数据库](#1.4.2 初始化地图数据库)
- [1.5 创建地图服务](#1.5 创建地图服务)
-
- [1.5.1 在 frameworkjava 下创建 admin 服务](#1.5.1 在 frameworkjava 下创建 admin 服务)
- [1.5.2 创建 admin-api 和 admin-service](#1.5.2 创建 admin-api 和 admin-service)
- [1.5.3 创建包路径和启动类](#1.5.3 创建包路径和启动类)
- [1.5.4 引入依赖](#1.5.4 引入依赖)
- [1.5.5 定义服务POJO](#1.5.5 定义服务POJO)
- [1.5.6 开始实现功能](#1.5.6 开始实现功能)
- [1.5.7 在 controller 包下创建 MapController 类](#1.5.7 在 controller 包下创建 MapController 类)
- [1.5.8 在 admin-api 包下创建 constants 包](#1.5.8 在 admin-api 包下创建 constants 包)
- [1.5.9 在 config 包下配置 RestTemplate](#1.5.9 在 config 包下配置 RestTemplate)
- [1.5.10 在 mapper 包下创建操作数据库的接口](#1.5.10 在 mapper 包下创建操作数据库的接口)
- [1.5.11 在 service 包下创建服务接口以及实现类](#1.5.11 在 service 包下创建服务接口以及实现类)
- [1.6 加入 bootstrap.yml 配置文件](#1.6 加入 bootstrap.yml 配置文件)
- [二、配置 nacos](#二、配置 nacos)
- 三、验证
- END
鸡汤:
● 累了就停一停,该有的光,总会在路上亮起来。
● 别怕走错方向,每一步都是风景的一部分。
前言
前面我们开发了文件服务,今天我们封装地图服务
一、地图服务
1.1 为啥要封装地图服务?
● 地图成为强需求:当前互联网应用和地图关系越来越密切,如京东、淘宝、闲⻥、头条、抖音等对地理服务可以说是强依赖,所以大型公司都有单独的地图采购和接口诉求。
● 公共基础建设,减少重复开发:如果每个部门去开发一套地图接口,存在较强的重复开发。地图数据库和服务的单独建设不是一般公司可以承受的公共一套可以一定程度上降低成本。

1.2 地图服务基础功能分析
● 获取城市列表:这个作为目前主流的城市选择服务,如闲鱼的城市选择

● 获取按照A-Z分开的城市列表信息:该业务主要用来实现A-Z的城市划分,如咸鱼的城市选择组件

● 获取下级行政区划:目前所有的应用都有根据城市或者省份获取下一级子节点列表的基本诉求,如京东的地址选择或者闲鱼的地址选择

● 获取热门城市:这个主要是根据业务来配置对应的热门城市,像12306买火车票

**● 地图搜索:
1 . 在地图上搜索需要的内容,如餐馆、公园等,像腾讯地图搜索地点 **

2 . 根据经纬度定位所在城市,像咸鱼首页默认当前所在地址

1.3 接入腾讯地图服务
1.3.1 登录、创建AccessKey

● 按照以下界面提示步骤操作即可,注册完成之后,登录到控制台

● 创建应用


● 分配额度


1.4 地图数据初始化
1.4.1 数据表结构设计
区划和地图信息往往是按照调用次数收费的,而区划往往相对固定,或者说只需要周期性更新,甚至是年度更新,没必要浪费次数,数据查询优先查询本地数据。所以我们需要在本地存储地图数据。
对应官方文档:

sql
CREATE TABLE `sys_region` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`code` varchar(20) DEFAULT NULL COMMENT '区划编码',
`parent_id` bigint(20) DEFAULT NULL COMMENT '⽗级id',
`parent_code` varchar(20) DEFAULT NULL COMMENT '⽗级编码',
`name` varchar(40) DEFAULT NULL COMMENT '区划名称',
`full_name` varchar(40) DEFAULT NULL COMMENT '区划全称',
`pinyin` varchar(50) DEFAULT NULL COMMENT '城市拼⾳',
`level` varchar(20) DEFAULT NULL COMMENT '省-1,市-2,区-3',
`longitude` decimal(10,7) DEFAULT NULL COMMENT '经度',
`latitude` decimal(10,7) DEFAULT NULL COMMENT '纬度',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
1.4.2 初始化地图数据库
这里我们直接初始化数据库了,执行 sql 脚本
这里的 sql 脚本文件是使用文件服务,上传到我们自己的 OSS 中的
结果:

Mysql 的图形化管理界面软件是DBeaver
1.5 创建地图服务
在我们的脚手架项目中,我们就不单独创建一个地图服务了,直接将地图服务归纳到基础管理服务,也就是 admin 服务当中,所以我们就将创建 admin 服务。
1.5.1 在 frameworkjava 下创建 admin 服务

1.5.2 创建 admin-api 和 admin-service
因为我们的地图服务肯定要对外服务的所以我们要定义 admin-api

1.5.3 创建包路径和启动类

启动类 AdminServiceApplication:
java
package com.my.adminservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AdminServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AdminServiceApplication.class, args);
}
}
1.5.4 引入依赖
admin-api:
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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.my</groupId>
<artifactId>admin</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.my</groupId>
<artifactId>admin-api</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.my</groupId>
<artifactId>common-domain</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
</project>
admin-service:
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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.my</groupId>
<artifactId>admin</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.my</groupId>
<artifactId>admin-service</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- SpringCloud Alibaba Nacos discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- SpringCloud Alibaba Nacos Config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- 是 Spring Cloud 提供的一个引导模块 配置文件的早期加载、配置中心集成-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 启动web项目 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 内置servlet服务器是tomcat -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<!-- mysql连接器 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>com.my</groupId>
<artifactId>common-cache</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.my</groupId>
<artifactId>admin-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.my</groupId>
<artifactId>common-core</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.my</groupId>
<artifactId>common-redis</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.my</groupId>
<artifactId>common-security</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.my</groupId>
<artifactId>common-rabbitmq</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
1.5.5 定义服务POJO
1.5.6 开始实现功能
步骤一:在 admin-api 的 map 包下定义 feign 包并创建 Feign客户端

MapFeignClient:
xml
package com.my.adminapi.map.feign;
import com.my.adminapi.map.domain.dto.LocateReqDTO;
import com.my.adminapi.map.domain.dto.SearchPoiReqDTO;
import com.my.adminapi.map.domain.vo.RegionCityVO;
import com.my.adminapi.map.domain.vo.RegionVO;
import com.my.adminapi.map.domain.vo.SearchPoiVO;
import com.my.commondomain.domain.vo.BasePageVO;
import com.my.commondomain.domain.vo.R;
import jakarta.validation.constraints.NotNull;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
import java.util.Map;
/**
* 地图服务相关远程调用
*/
@FeignClient(contextId = "mapFeignClient",value = "admin")
public interface MapFeignClient {
/**
* 城市列表查询
* @return 城市列表信息
*/
@GetMapping("/map/city_list")
R<List<RegionVO>> getCityList();
/**
* 城市拼音归类查询
* @return 城市字母与城市列表的哈希
*/
@GetMapping("/map/city_pinyin_list")
R<Map<String,List<RegionVO>>> getCityPyList();
/**
* 根据父级区域ID获取子集区域列表
* @param parentId 父级区域ID
* @return 子集区域列表
*/
@GetMapping("/map/region_children_list")
R<List<RegionVO>> getCityChildren(@RequestParam @NotNull Long parentId);
/**
* 获取热门城市列表
* @return 城市列表
*/
@GetMapping("/map/city_hot_list")
R<List<RegionVO>> getHotCityList();
/**
* 根据地点搜索
* @param searchPoiReqDTO 搜索条件
* @return 搜利结果
*/
@PostMapping("/map/search")
R<BasePageVO<SearchPoiVO>> searchPoiOnMap(@RequestBody SearchPoiReqDTO searchPoiReqDTO);
/**
* 根据经纬度来定位城市
* @param locateReqDTO 经纬度信息
* @return 城市信息
*/
@PostMapping("/map/locate_city_by_location")
R<RegionCityVO> locateCityByLocation(@RequestBody LocateReqDTO locateReqDTO);
}
步骤二:在 admin-service 下创建 controller、service、domain、mapper、config 包

步骤三:定义实体类以及数据传输对象
因为有一些数据传输对象比如 VO、请求DTO 是对外提供的所以可以写在 admin-api 中
admin-api 的 map包下的domain:

LocateReqDTO:
java
package com.my.adminapi.map.domain.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
/**
* 位置查询DTO
*/
@Getter
@Setter
public class LocateReqDTO {
/**
* 纬度
*/
@NotNull(message = "纬度不能为空")
private Double lat;
/**
* 经度
*/
@NotNull(message = "经度不能为空")
private Double lng;
/**
* 格式化信息
* @return 格式化后的经纬度
*/
public String formatInfo() {
return lat + "," + lng;
}
}
SearchPoiReqDTO:
java
package com.my.adminapi.map.domain.dto;
import com.my.commondomain.domain.dto.BasePageReqDTO;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
/**
* 根据地点搜索
*/
@Setter
@Getter
public class SearchPoiReqDTO extends BasePageReqDTO {
/**
* 请求的关键字
*/
@NotNull(message = "请求关键字不允许为空")
private String keyword;
/**
* 请求区域ID
*/
@NotNull(message = "请求区域ID不能为空")
private Integer id;
}
RegionCityVO:
java
package com.my.adminapi.map.domain.vo;
import lombok.Getter;
import lombok.Setter;
/**
* 城市信息VO
*/
@Setter
@Getter
public class RegionCityVO {
/**
* 城市ID
*/
private Long id;
/**
* 城市名称
*/
private String name;
/**
* 城市全称
*/
private String fullName;
}
RegionVO:
java
package com.my.adminapi.map.domain.vo;
import lombok.Getter;
import lombok.Setter;
/**
* 区域信息VO
*/
@Setter
@Getter
public class RegionVO {
/**
* 区域ID
*/
private Long id;
/**
* 区域名称
*/
private String name;
/**
* 区域全称
*/
private String fullName;
/**
* 父级区域ID
*/
private Long parentId;
/**
* 拼音
*/
private String pinyin;
/**
* 级别
*/
private Integer level;
/**
* 经度
*/
private Double longitude;
/**
* 纬度
*/
private Double latitude;
}
SearchPoiVO:
java
package com.my.adminapi.map.domain.vo;
import lombok.Getter;
import lombok.Setter;
/**
* 查询结果VO
*/
@Setter
@Getter
public class SearchPoiVO {
/**
* 地点名称
*/
private String title;
/**
* 地点地址
*/
private String address;
/**
* 经度
*/
private Double longitude;
/**
* 纬度
*/
private Double latitude;
}
admin-service 的 map包下的domain:

SuggestRequestDTO:
java
package com.my.adminservice.map.domain.dto.request;
import lombok.Getter;
import lombok.Setter;
/**
* 腾讯地图关键词搜索所需参数
*/
@Setter
@Getter
public class SuggestRequestDTO {
/**
* 限制城市id
*/
private String id;
/**
* 搜索关键词,最多支持96个字符(每个英文字符占1个,中文占3个)
*/
private String keyword;
/**
* 页码,从1开始,最大页码需通过count进行计算,必须与page_size同时使用
*/
private Integer PageIndex;
/**
* 每页条数,取值范围1-20,必须与page_index 同时使用
*/
private Integer PageSize;
}
LocationDTO:
java
package com.my.adminservice.map.domain.dto;
import lombok.Getter;
import lombok.Setter;
/**
* 经纬度DTO
*/
@Setter
@Getter
public class LocationDTO {
/**
* 纬度
*/
private Double lat;
/**
* 经度
*/
private Double lng;
public String formatInfo() {
return lat + "," + lng;
}
}
SysRegionDTO:
java
package com.my.adminservice.map.domain.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class SysRegionDTO {
/**
* 区域ID
*/
private Long id;
/**
* 区域名称
*/
private String name;
/**
* 区域全称
*/
private String fullName;
/**
* 父级区域ID
*/
private Long parentId;
/**
* 拼音
*/
private String pinyin;
/**
* 级别
*/
private Integer level;
/**
* 经度
*/
private Double longitude;
/**
* 纬度
*/
private Double latitude;
}
SysRegion:
java
package com.my.adminservice.map.domain.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.my.commondomain.domain.dataobject.BaseDO;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
/**
* sys_region表对应的实体类
*/
@Getter
@Setter
@EqualsAndHashCode(callSuper = true)
@TableName("sys_region")
public class SysRegion extends BaseDO {
/**
* 区域ID
*/
private Long id;
/**
* 区域名称
*/
private String name;
/**
* 区域全称
*/
private String fullName;
/**
* 父级区域ID
*/
private Long parentId;
/**
* 拼音
*/
private String pinyin;
/**
* 级别
*/
private Integer level;
/**
* 经度
*/
private Double longitude;
/**
* 纬度
*/
private Double latitude;
/**
* 区域编码
*/
private String code;
/**
* 父级区域编码
*/
private String parentCode;
}
**注:
推荐将请求的数据类型和相应的数据类型区分开
BaseDO 和 BasePageReqDTO是定义在 common-domain 的基类
Java脚手架项目通用基类和常量类的封装
1.5.7 在 controller 包下创建 MapController 类

MapController:
java
package com.my.adminservice.map.controller;
import com.my.adminapi.map.domain.dto.LocateReqDTO;
import com.my.adminapi.map.domain.dto.SearchPoiReqDTO;
import com.my.adminapi.map.domain.vo.RegionCityVO;
import com.my.adminapi.map.domain.vo.RegionVO;
import com.my.adminapi.map.domain.vo.SearchPoiVO;
import com.my.adminapi.map.feign.MapFeignClient;
import com.my.adminservice.map.domain.dto.SysRegionDTO;
import com.my.adminservice.map.domain.dto.response.RegionCityDTO;
import com.my.adminservice.map.domain.dto.response.SearchPoiDTO;
import com.my.adminservice.map.service.IMapService;
import com.my.commoncore.utils.BeanCopyUtil;
import com.my.commondomain.domain.dto.BasePageDTO;
import com.my.commondomain.domain.vo.BasePageVO;
import com.my.commondomain.domain.vo.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@RestController
@Slf4j
public class MapController implements MapFeignClient {
@Autowired
private IMapService mapService;
/**
* 城市列表查询
* @return 城市列表信息
*/
@Override
public R<List<RegionVO>> getCityList() {
List<SysRegionDTO> regionDTOS= mapService.getCityList();
List<RegionVO> result = BeanCopyUtil.copyPropertiesToList(regionDTOS,RegionVO::new);
return R.success(result);
}
/**
* 城市拼音归类查询
* @return 城市字母与城市列表的哈希
*/
@Override
public R<Map<String, List<RegionVO>>> getCityPyList() {
Map<String, List<RegionVO>> result = new LinkedHashMap<>();
Map<String, List<SysRegionDTO>> regionMap = mapService.getCityPyList();
for(Map.Entry<String, List<SysRegionDTO>> entry : regionMap.entrySet()){
result.put(entry.getKey(),BeanCopyUtil.copyPropertiesToList(entry.getValue(),RegionVO::new));
}
return R.success(result);
}
/**
* 根据父级区域ID获取子集区域列表
* @param parentId 父级区域ID
* @return 子集区域列表
*/
@Override
public R<List<RegionVO>> getCityChildren(Long parentId) {
List<SysRegionDTO> regionDTOList = mapService.getCityChildren(parentId);
List<RegionVO> result = BeanCopyUtil.copyPropertiesToList(regionDTOList, RegionVO::new);
return R.success(result);
}
/**
* 获取热门城市列表
* @return 城市列表
*/
@Override
public R<List<RegionVO>> getHotCityList() {
List<SysRegionDTO> regionDTOList = mapService.getHotCityList();
List<RegionVO> result = BeanCopyUtil.copyPropertiesToList(regionDTOList, RegionVO::new);
return R.success(result);
}
/**
* 根据地点搜索
* @param searchPoiReqDTO 搜索条件
* @return 搜利结果
*/
@Override
public R<BasePageVO<SearchPoiVO>> searchPoiOnMap(SearchPoiReqDTO searchPoiReqDTO) {
BasePageDTO<SearchPoiDTO> basePageDTO = mapService.searchPoiOnMap(searchPoiReqDTO);
BasePageVO<SearchPoiVO> result = new BasePageVO<>();
BeanCopyUtil.copyProperties(basePageDTO,result);
return R.success(result);
}
/**
* 根据经纬度来定位城市
* @param locateReqDTO 经纬度信息
* @return 城市信息
*/
@Override
public R<RegionCityVO> locateCityByLocation(LocateReqDTO locateReqDTO) {
RegionCityDTO regionCityDTO = mapService.getCityByLocation(locateReqDTO);
RegionCityVO result = new RegionCityVO();
BeanCopyUtil.copyProperties(regionCityDTO,result);
return R.success(result);
}
}
1.5.8 在 admin-api 包下创建 constants 包
在 constants 包下,创建 MapConstants 放入 地图服务所需常量

MapConstants:
java
package com.my.adminapi.map.constants;
/**
* 地图常量信息
*/
public class MapConstants {
/**
* 城市级别
*/
public static final Integer CITY_LEVEL = 2;
/**
* 分布式锁的 Key
*/
public static final String REDISSON_LOCK_CITY_LIST_KEY = "map:city:lock:list";
/**
* 缓存城市列表信息Key
*/
public static final String CACHE_MAP_CITY_KEY = "map:city:id";
/**
* 缓存城市 A-Z 分类信息key
*/
public static final String CACHE_MAP_CITY_PINYIN_KEY = "map:city:pinyin";
/**
* 缓存热门信息的key
*/
public static final String CACHE_MAP_HOT_CITY_KEY = "map:city:hot";
/**
* 根据关键词搜索的接口路由
*/
public static final String QQMAP_API_PLACE_SUGGESTION = "/ws/place/v1/suggestion";
/**
* 根据经纬度来获取区域信息的接口路由
*/
public static final String QQMAP_GEOCODER = "/ws/geocoder/v1";
/**
* 参数服务,热门城市键
*/
public static final String CONFIG_HOT_CITY_KEY = "sys_hot_city";
}
1.5.9 在 config 包下配置 RestTemplate

RestTemplateConfig:
java
package com.my.adminservice.map.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
@ConditionalOnMissingBean(RestTemplate.class)
public RestTemplate getRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
return restTemplate;
}
}
RestTemplate就是 Spring 提供给我们的访问外部的 api ,需要创建然后交给 Spring 进行管理
1.5.10 在 mapper 包下创建操作数据库的接口
RegionMapper:
java
package com.my.adminservice.map.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.my.adminservice.map.domain.entity.SysRegion;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface RegionMapper extends BaseMapper<SysRegion> {
List<SysRegion> selectAllRegion();
}
RegionMapper.xml:
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.my.adminservice.map.mapper.RegionMapper">
<select id="selectAllRegion" resultType="com.my.adminservice.map.domain.entity.SysRegion">
select * from sys_region order by pinyin
</select>
</mapper>
1.5.11 在 service 包下创建服务接口以及实现类

接口:
IMapProvider:
java
package com.my.adminservice.map.service;
import com.my.adminservice.map.domain.dto.LocationDTO;
import com.my.adminservice.map.domain.dto.response.GeocoderResponseDTO;
import com.my.adminservice.map.domain.dto.response.PoiListDTO;
import com.my.adminservice.map.domain.dto.request.SuggestRequestDTO;
/**
* 地图服务提供者
*/
public interface IMapProvider {
/**
* 根据关键词搜索地点
* @param suggestRequestDTO 搜索条件
* @return 搜索结果
*/
PoiListDTO searchPlacesByKeywordInRegion(SuggestRequestDTO suggestRequestDTO);
/**
* 根据经纬度来获取区域信息
* @param locationDTO 经纬度
* @return 区域信息
*/
GeocoderResponseDTO reverseGeocode (LocationDTO locationDTO);
}
IMapService:
java
package com.my.adminservice.map.service;
import com.my.adminapi.map.domain.dto.LocateReqDTO;
import com.my.adminapi.map.domain.dto.SearchPoiReqDTO;
import com.my.adminservice.map.domain.dto.SysRegionDTO;
import com.my.adminservice.map.domain.dto.response.RegionCityDTO;
import com.my.adminservice.map.domain.dto.response.SearchPoiDTO;
import com.my.commondomain.domain.dto.BasePageDTO;
import java.util.List;
import java.util.Map;
public interface IMapService {
List<SysRegionDTO> getCityList();
Map<String, List<SysRegionDTO>> getCityPyList();
List<SysRegionDTO> getCityChildren(Long parentId);
List<SysRegionDTO> getHotCityList();
BasePageDTO<SearchPoiDTO> searchPoiOnMap(SearchPoiReqDTO searchPoiReqDTO);
RegionCityDTO getCityByLocation(LocateReqDTO locateReqDTO);
}
实现类:
MapServiceImpl:
java
package com.my.adminservice.map.service.Impl;
import com.fasterxml.jackson.core.type.TypeReference;
import com.github.benmanes.caffeine.cache.Cache;
import com.my.adminapi.map.constants.MapConstants;
import com.my.adminapi.map.domain.dto.LocateReqDTO;
import com.my.adminapi.map.domain.dto.SearchPoiReqDTO;
import com.my.adminservice.config.service.IArgumentService;
import com.my.adminservice.map.domain.dto.LocationDTO;
import com.my.adminservice.map.domain.dto.SysRegionDTO;
import com.my.adminservice.map.domain.dto.request.SuggestRequestDTO;
import com.my.adminservice.map.domain.dto.response.*;
import com.my.adminservice.map.domain.entity.SysRegion;
import com.my.adminservice.map.mapper.RegionMapper;
import com.my.adminservice.map.service.IMapProvider;
import com.my.adminservice.map.service.IMapService;
import com.my.commoncache.utils.CacheUtil;
import com.my.commoncore.utils.BeanCopyUtil;
import com.my.commoncore.utils.PageUtil;
import com.my.commondomain.domain.dto.BasePageDTO;
import com.my.commonredis.service.RedisService;
import com.my.commonredis.service.RedissonLockService;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 地图服务层的实现类
*/
@Service
@Slf4j
public class MapServiceImpl implements IMapService {
/**
* sys_region的mapper
*/
@Autowired
private RegionMapper regionMapper;
/**
* redis服务对象
*/
@Autowired
private RedisService redisService;
/**
* redisson分布式锁
*/
@Autowired
private RedissonLockService redissonLockService;
/**
* redisson 自带客户端
*/
// @Autowired
// private RedissonClient redissonClient;
/**
* 本地内存服务对象
*/
@Autowired
private Cache<String,Object> caffeineCache;
@Autowired
private IMapProvider mapProvider;
@Autowired
private IArgumentService argumentService;
@PostConstruct
public void init() {
// 1 直接先查数据库
List<SysRegion> sysRegionList = regionMapper.selectAllRegion();
// 2 在服务启动期间,缓存城市列表
initCityCache(sysRegionList);
// 3 在服务启动期间,缓存城市分类列表
initCityPyCache(sysRegionList);
}
/**
* 缓存城市信息
*/
private void initCityCache(List<SysRegion> sysRegionList) {
// 1. 声明一个空的结果列表
List<SysRegionDTO> result = new ArrayList<>();
// 2. 提取城市数据列表,并且做对象转换
for(SysRegion sysRegion : sysRegionList){
if(sysRegion.getLevel().equals(MapConstants.CITY_LEVEL)) {
SysRegionDTO sysRegionDTO = new SysRegionDTO();
BeanCopyUtil.copyProperties(sysRegion,sysRegionDTO);
result.add(sysRegionDTO);
}
}
CacheUtil.setL2Cache(MapConstants.CACHE_MAP_CITY_KEY,result,redisService,
caffeineCache,120, TimeUnit.MINUTES);
}
/**
* 初始化A-Z归类城市列表缓存内容
*/
private void initCityPyCache(List<SysRegion> sysRegionList) {
// 1. 创建结果 map
Map<String,List<SysRegionDTO>> result = new LinkedHashMap<>();
for(SysRegion sysRegion : sysRegionList){
// 2. 将所有城市筛选出来
if(sysRegion.getLevel().equals(MapConstants.CITY_LEVEL)) {
SysRegionDTO sysRegionDTO = new SysRegionDTO();
// 3. 将 SysRegion 转换为结果所需要的 SysRegionDTO
BeanCopyUtil.copyProperties(sysRegion,sysRegionDTO);
String firstChar = sysRegionDTO.getPinyin().toUpperCase().substring(0, 1);
// 4. 将当前的 SysRegionDTO 放入它对应的位置
if(result.containsKey(firstChar)){
result.get(firstChar).add(sysRegionDTO);
}else {
List<SysRegionDTO> list = new ArrayList<>();
list.add(sysRegionDTO);
result.put(firstChar,list);
}
}
}
CacheUtil.setL2Cache(MapConstants.CACHE_MAP_CITY_PINYIN_KEY,result,redisService,
caffeineCache,120, TimeUnit.MINUTES);
}
/**
* 获取城市列表 最终版本
* 如果缓存过期或没有,都会查询数据库
* 使用分布式锁控制数据库的访问
* @return 城市列表信息
*/
@Override
public List<SysRegionDTO> getCityList() {
List<SysRegionDTO> cache = CacheUtil.getL2Cache(MapConstants.CACHE_MAP_CITY_KEY, redisService,
new TypeReference<List<SysRegionDTO>>() {},caffeineCache);
if(cache != null){
return cache;
}
RLock lock = redissonLockService.acquire(MapConstants.REDISSON_LOCK_CITY_LIST_KEY, -1);
try {
if(lock != null) {
log.info("加锁成功");
cache = CacheUtil.getL2Cache(MapConstants.CACHE_MAP_CITY_KEY, redisService,
new TypeReference<List<SysRegionDTO>>() {},caffeineCache);
if(cache != null){
return cache;
}
List<SysRegionDTO> result = getCityListFromDB();
CacheUtil.setL2Cache(MapConstants.CACHE_MAP_CITY_KEY,result,redisService,
caffeineCache,120, TimeUnit.MINUTES);
return result;
}else {
cache = CacheUtil.getL2Cache(MapConstants.CACHE_MAP_CITY_KEY, redisService,
new TypeReference<List<SysRegionDTO>>() {},caffeineCache);
return cache == null ? new ArrayList<>() : cache;
}
} finally {
log.info("正在释放城市列表缓存重建锁");
redissonLockService.releaseLock(lock);
}
// 不使用自定义 Redisson工具类
// RLock lock = redissonClient.getLock(MapConstants.REDISSON_LOCK_CITY_LIST_KEY);
// boolean isLock = false;
// try {
// if(lock.tryLock(1000,TimeUnit.MILLISECONDS)){
// isLock = true;
// log.info("加锁成功");
// cache = CacheUtil.getL2Cache(MapConstants.CACHE_MAP_CITY_KEY, redisService,
// new TypeReference<List<SysRegionDTO>>() {},caffeineCache);
// if(cache != null){
// return cache;
// }
// List<SysRegionDTO> result = getCityListFromDB();
//
// CacheUtil.setL2Cache(MapConstants.CACHE_MAP_CITY_KEY,result,redisService,
// caffeineCache,120, TimeUnit.MINUTES);
// return result;
// }
// cache = CacheUtil.getL2Cache(MapConstants.CACHE_MAP_CITY_KEY, redisService,
// new TypeReference<List<SysRegionDTO>>() {},caffeineCache);
// return cache == null ? new ArrayList<>() : cache;
// } catch (InterruptedException e) {
// log.info("InterruptedException ");
// throw new RuntimeException(e);
// } finally {
// if(isLock){
// log.info("正在释放城市列表缓存重建锁");
// lock.unlock();
// }
// }
}
/**
* 获取城市列表V1
* 从 mysql 中直接查
*/
public List<SysRegionDTO> getCityListV1() {
// 1. 声明一个空的结果列表
List<SysRegionDTO> result = new ArrayList<>();
// 2. 查询数据库
List<SysRegion> sysRegionList = regionMapper.selectAllRegion();
// 3. 提取城市数据列表,并且做对象转换
for(SysRegion sysRegion : sysRegionList){
if(sysRegion.getLevel().equals(MapConstants.CITY_LEVEL)) {
SysRegionDTO sysRegionDTO = new SysRegionDTO();
BeanCopyUtil.copyProperties(sysRegion,sysRegionDTO);
result.add(sysRegionDTO);
}
}
return result;
}
/**
* 获取城市列表V2
* 先从 redis中查,没查到在到 mysql 上查
*/
public List<SysRegionDTO> getCityListV2() {
// 1. 声明一个空的结果列表
List<SysRegionDTO> result = new ArrayList<>();
List<SysRegionDTO> cache = redisService.getCacheObject(MapConstants.CACHE_MAP_CITY_KEY,
new TypeReference<List<SysRegionDTO>>() {});
if(cache != null){
return cache;
}
// 2. 查询数据库
List<SysRegion> sysRegionList = regionMapper.selectAllRegion();
// 3. 提取城市数据列表,并且做对象转换
for(SysRegion sysRegion : sysRegionList){
if(sysRegion.getLevel().equals(MapConstants.CITY_LEVEL)) {
SysRegionDTO sysRegionDTO = new SysRegionDTO();
BeanCopyUtil.copyProperties(sysRegion,sysRegionDTO);
result.add(sysRegionDTO);
}
}
redisService.setCacheObject(MapConstants.CACHE_MAP_CITY_KEY,result);
return result;
}
/**
* 获取城市列表V3
* 先从两级缓存(caffeine,redis)中查,没查到在到 mysql 上查
*/
public List<SysRegionDTO> getCityListV3() {
// 1. 声明一个空的结果列表
List<SysRegionDTO> result = new ArrayList<>();
List<SysRegionDTO> cache = CacheUtil.getL2Cache(MapConstants.CACHE_MAP_CITY_KEY, redisService,
new TypeReference<List<SysRegionDTO>>() {},caffeineCache);
if(cache != null){
return cache;
}
// 2. 查询数据库
List<SysRegion> sysRegionList = regionMapper.selectAllRegion();
// 3. 提取城市数据列表,并且做对象转换
for(SysRegion sysRegion : sysRegionList){
if(sysRegion.getLevel().equals(MapConstants.CITY_LEVEL)) {
SysRegionDTO sysRegionDTO = new SysRegionDTO();
BeanCopyUtil.copyProperties(sysRegion,sysRegionDTO);
result.add(sysRegionDTO);
}
}
CacheUtil.setL2Cache(MapConstants.CACHE_MAP_CITY_KEY,result,redisService,
caffeineCache,120, TimeUnit.MINUTES);
return result;
}
/**
* 获取城市列表V4
* 缓存预热方案
*/
public List<SysRegionDTO> getCityListV4() {
return CacheUtil.getL2Cache(MapConstants.CACHE_MAP_CITY_KEY, redisService,
new TypeReference<List<SysRegionDTO>>() {},caffeineCache);
}
/**
* 获取城市分类列表
* 缓存预热方案
*/
@Override
public Map<String, List<SysRegionDTO>> getCityPyList() {
return CacheUtil.getL2Cache(MapConstants.CACHE_MAP_CITY_PINYIN_KEY, redisService,
new TypeReference<Map<String, List<SysRegionDTO>>>() {},caffeineCache);
}
@Override
public List<SysRegionDTO> getCityChildren(Long parentId) {
//直接调用 getCityList() 接口
List<SysRegionDTO> cache = getCityList();
List<SysRegionDTO> result = new ArrayList<>();
for(SysRegionDTO sysRegionDTO : cache){
if(sysRegionDTO.getParentId() != null && sysRegionDTO.getParentId().equals(parentId)) {
result.add(sysRegionDTO);
}
}
return result;
}
/**
* 获取热门城市列表
* @return 城市列表
*/
@Override
public List<SysRegionDTO> getHotCityList() {
// 1.构建结果 result
List<SysRegionDTO> result = new ArrayList<>();
// 2. 查缓存里有没有
List<SysRegionDTO> cache = CacheUtil.getL2Cache(MapConstants.CACHE_MAP_HOT_CITY_KEY, redisService,
new TypeReference<List<SysRegionDTO>>() {},caffeineCache);
if(cache != null){
// 有 则返回
return cache;
}
String HotIds = argumentService.getByConfigKey(MapConstants.CONFIG_HOT_CITY_KEY).getValue();
List<Long> ids = new ArrayList<>();
for(String id : HotIds.split(",")){
ids.add(Long.valueOf(id));
}
List<SysRegion> hotCityList = regionMapper.selectBatchIds(ids);
for(SysRegion sysRegion : hotCityList){
SysRegionDTO sysRegionDTO = new SysRegionDTO();
BeanCopyUtil.copyProperties(sysRegion,sysRegionDTO);
result.add(sysRegionDTO);
}
CacheUtil.setL2Cache(MapConstants.CACHE_MAP_HOT_CITY_KEY,result,redisService,
caffeineCache,120, TimeUnit.MINUTES);
return result;
}
/**
* 根据地点搜索
* @param searchPoiReqDTO 搜索条件
* @return 搜利结果
*/
@Override
public BasePageDTO<SearchPoiDTO> searchPoiOnMap(SearchPoiReqDTO searchPoiReqDTO) {
// 1 构建查询腾讯位置服务的入参
SuggestRequestDTO suggestRequestDTO = new SuggestRequestDTO();
BeanCopyUtil.copyProperties(searchPoiReqDTO,suggestRequestDTO);
suggestRequestDTO.setPageIndex(searchPoiReqDTO.getPageNo());
suggestRequestDTO.setId(String.valueOf(searchPoiReqDTO.getId()));
// 2 调用地图位置查询接口
PoiListDTO resultList = mapProvider.searchPlacesByKeywordInRegion(suggestRequestDTO);
// 3 做结果对象转换
List<SearchPoiDTO> searchPoiDTOList = new ArrayList<>();
for(PoiDTO poiDTO : resultList.getData()) {
SearchPoiDTO searchPoiDTO = new SearchPoiDTO();
BeanCopyUtil.copyProperties(poiDTO,searchPoiDTO);
searchPoiDTO.setLatitude(poiDTO.getLocation().getLat());
searchPoiDTO.setLongitude(poiDTO.getLocation().getLng());
searchPoiDTOList.add(searchPoiDTO);
}
return new BasePageDTO<>(resultList.getCount(),
PageUtil.getTotalPages(resultList.getCount(),searchPoiReqDTO.getPageSize()),
searchPoiDTOList);
}
/**
* 根据经纬度来定位城市
* @param locateReqDTO 经纬度信息
* @return 城市信息
*/
@Override
public RegionCityDTO getCityByLocation(LocateReqDTO locateReqDTO) {
// 1 构建查询腾讯位置服务的入参
LocationDTO locationDTO = new LocationDTO();
BeanCopyUtil.copyProperties(locateReqDTO,locationDTO);
// 2 调用地图位置查询接口
GeocoderResponseDTO geocoderResponseDTO = mapProvider.reverseGeocode(locationDTO);
RegionCityDTO result = new RegionCityDTO();
if(geocoderResponseDTO != null && geocoderResponseDTO.getResult() != null
&& geocoderResponseDTO.getResult().getAd_info() != null) {
String cityName = geocoderResponseDTO.getResult().getAd_info().getCity();
// 3. 查缓存
List<SysRegionDTO> cityList = getCityList();
for(SysRegionDTO sysRegionDTO : cityList){
if(sysRegionDTO.getFullName().equals(cityName)){
BeanCopyUtil.copyProperties(sysRegionDTO,result);
}
}
}
return result;
}
/***************************** 私有方法 ********************************/
/**
* 从数据库中获取 CityList 数据
* @return CityList 数据
*/
private List<SysRegionDTO> getCityListFromDB() {
List<SysRegionDTO> result = new ArrayList<>();
List<SysRegion> sysRegionList = regionMapper.selectAllRegion();
for(SysRegion sysRegion : sysRegionList){
if(sysRegion.getLevel().equals(MapConstants.CITY_LEVEL)) {
SysRegionDTO sysRegionDTO = new SysRegionDTO();
BeanCopyUtil.copyProperties(sysRegion,sysRegionDTO);
result.add(sysRegionDTO);
}
}
return result;
}
}
注:缓存使用了二级缓存的方法,并且运用了缓存预热,使用 Radisson 分布式锁控制流量,从初始版本到最终版本都已写在上面
QQMapServiceImpl:
java
package com.my.adminservice.map.service.Impl;
import com.my.adminapi.map.constants.MapConstants;
import com.my.adminservice.map.domain.dto.LocationDTO;
import com.my.adminservice.map.domain.dto.response.GeocoderResponseDTO;
import com.my.adminservice.map.domain.dto.response.PoiListDTO;
import com.my.adminservice.map.domain.dto.request.SuggestRequestDTO;
import com.my.adminservice.map.service.IMapProvider;
import com.my.commondomain.domain.vo.ResultCode;
import com.my.commondomain.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Component
@Slf4j
@RefreshScope
@ConditionalOnProperty(value = "map.type",havingValue = "qqmap")
public class QQMapServiceImpl implements IMapProvider {
@Autowired
private RestTemplate restTemplate;
/**
* 调用腾讯位置服务的秘钥
*/
@Value("${qqmap.key}")
private String key;
/**
* 腾讯位置服务域名
*/
@Value("${qqmap.apiServer}")
private String apiServer;
/**
* 根据关键词搜索地点
* @param suggestRequestDTO 搜索条件
* @return 搜索结果
*/
@Override
public PoiListDTO searchPlacesByKeywordInRegion(SuggestRequestDTO suggestRequestDTO) {
// 1 构建请求url
String url = String.format(
apiServer + MapConstants.QQMAP_API_PLACE_SUGGESTION +
"?region=%s&keyword=%s&key=%s&page_index=%s&page_size=%s",
suggestRequestDTO.getId(),
suggestRequestDTO.getKeyword(),
key,
suggestRequestDTO.getPageIndex(),
suggestRequestDTO.getPageSize()
);
// 2 直接发送请求,并拿到返回结果再做对象转换
ResponseEntity<PoiListDTO> response = restTemplate.getForEntity(url, PoiListDTO.class);
if(!response.getStatusCode().is2xxSuccessful()){
log.error("获取关键词查询结果异常", response);
throw new ServiceException(ResultCode.QQMAP_QUERY_FAILED);
}
return response.getBody();
}
/**
* 根据经纬度来获取区域信息
* @param locationDTO 经纬度
* @return 区域信息
*/
public GeocoderResponseDTO reverseGeocode (LocationDTO locationDTO) {
// 1 构建请求url
String url = String.format(
apiServer + MapConstants.QQMAP_GEOCODER +
"?location=%s&key=%s",
locationDTO.formatInfo(),
key
);
// 2 直接发送请求,并拿到返回结果再做对象转换
ResponseEntity<GeocoderResponseDTO> response = restTemplate.getForEntity(url, GeocoderResponseDTO.class);
if(!response.getStatusCode().is2xxSuccessful()){
log.error("获取关键词查询结果异常", response);
throw new ServiceException(ResultCode.QQMAP_QUERY_FAILED);
}
return response.getBody();
}
}
1.6 加入 bootstrap.yml 配置文件
bootstrap.yml:
yaml
server:
port: 18082
spring:
application:
name: admin
profiles:
active: ${RUN_ENV}
cloud:
nacos:
discovery:
username: nacos
password: bite@123
namespace: frameworkjava-${RUN_ENV}
server-addr: ${NACOS_ADDR}
config:
username: nacos
password: bite@123
namespace: frameworkjava-${RUN_ENV}
server-addr: ${NACOS_ADDR}
file-extension: yaml
# 共享配置
shared-configs:
- data-id: share-common-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
refresh: true
- data-id: share-redis-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
refresh: true
- data-id: share-mysql-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
refresh: true
- data-id: share-caffeine-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
refresh: true
- data-id: share-map-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
refresh: true
- data-id: share-rabbitmq-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
refresh: true
logging:
pattern:
console: '[%thread] %-5level %logger{36} - %msg%n'
二、配置 nacos
因为我们腾讯位置服务的 AsseccKey 啥的都要维护在 nacos 中


yaml
map:
type: qqmap
enabled: false
regionenabled: false
qqmap:
key: 自己的腾讯位置服务的 Key
apiServer: https://apis.map.qq.com
三、验证
这里就用 apifox 简单验证
先打开服务


缓存预热起作用了,删除 redis 中的缓存,等本地缓存消失,在获取城市列表


无缓存第一次查询 800 ms

有缓存 54ms,可见缓存的作用

其他接口,我就不一一测试了
END
通用管理服务的地图服务实现完毕,
写的电脑有点卡