diff --git a/fastbee-admin/src/main/java/com/fastbee/web/controller/map/MapLocationController.java b/fastbee-admin/src/main/java/com/fastbee/web/controller/map/MapLocationController.java new file mode 100644 index 00000000..70a79f6e --- /dev/null +++ b/fastbee-admin/src/main/java/com/fastbee/web/controller/map/MapLocationController.java @@ -0,0 +1,72 @@ +package com.fastbee.web.controller.map; + +import com.fastbee.common.core.domain.AjaxResult; +import com.fastbee.common.utils.StringUtils; +import com.fastbee.http.map.ILocationService; +import com.fastbee.http.map.LocationServiceFactory; +import com.fastbee.http.model.AddressInfo; +import com.fastbee.http.model.Coordinate; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +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 javax.annotation.Resource; +import java.math.BigDecimal; + +/** + * 地图定位接口(供项目管理-地图选点使用) + * + * @author Sumuxi + * @date 2026/8/12 + */ +@Api(tags = "地图定位") +@RestController +@RequestMapping("/map/location") +public class MapLocationController { + + @Resource + private LocationServiceFactory locationServiceFactory; + + /** + * 根据中文地址获取经纬度(正地理编码) + */ + @ApiOperation("根据中文地址获取经纬度") + @PreAuthorize("@ss.hasPermi('iot:project:edit')") + @GetMapping("/geocode") + public AjaxResult geocode(@RequestParam("address") String address, + @RequestParam(value = "provider", defaultValue = "tianditu") String provider) { + if (StringUtils.isEmpty(address)) { + return AjaxResult.error("地址不能为空"); + } + ILocationService service = locationServiceFactory.getLocationService(provider); + Coordinate coordinate = service.getCoordinateByAddress(address); + if (coordinate == null || coordinate.getLongitude() == null || coordinate.getLatitude() == null) { + return AjaxResult.error("未找到该地址的坐标,请尝试更详细的地址"); + } + return AjaxResult.success(coordinate); + } + + /** + * 根据经纬度获取中文地址(逆地理编码) + */ + @ApiOperation("根据经纬度获取中文地址") + @PreAuthorize("@ss.hasPermi('iot:project:edit')") + @GetMapping("/reverseGeocode") + public AjaxResult reverseGeocode(@RequestParam("lng") Double lng, @RequestParam("lat") Double lat, + @RequestParam(value = "provider", defaultValue = "tianditu") String provider) { + if (lng == null || lat == null) { + return AjaxResult.error("经纬度不能为空"); + } + ILocationService service = locationServiceFactory.getLocationService(provider); + AddressInfo addressInfo = service.getAddressByCoordinate( + new Coordinate(BigDecimal.valueOf(lng), BigDecimal.valueOf(lat))); + if (addressInfo == null) { + return AjaxResult.error("未获取到该坐标对应的地址"); + } + return AjaxResult.success(addressInfo); + } +} diff --git a/fastbee-admin/src/main/resources/application.yml b/fastbee-admin/src/main/resources/application.yml index 6357b46d..191e5485 100644 --- a/fastbee-admin/src/main/resources/application.yml +++ b/fastbee-admin/src/main/resources/application.yml @@ -230,4 +230,5 @@ map: provider: gaode # 提供者 baidu_ak: 0SD4KrYeMXVu8Ox2pxQGDOw3NmDYiKIw gaode_ak: 3af85be39e2f28334097781349336157 - gaode_secret: d0561ad20142e74f8c59edc5deafdbe4 \ No newline at end of file + gaode_secret: d0561ad20142e74f8c59edc5deafdbe4 + tianditu_ak: b920e6076a06aa279c974f5f76cf746e diff --git a/fastbee-common/src/main/java/com/fastbee/common/constant/FastBeeConstant.java b/fastbee-common/src/main/java/com/fastbee/common/constant/FastBeeConstant.java index d60c0b3a..2478040b 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/constant/FastBeeConstant.java +++ b/fastbee-common/src/main/java/com/fastbee/common/constant/FastBeeConstant.java @@ -389,6 +389,9 @@ public interface FastBeeConstant { // 高德(https://lbs.amap.com/api/webservice/guide/api/georegeo) String GAODE_BASE = "https://restapi.amap.com"; + // 天地图(http://lbs.tianditu.gov.cn/server/geocodinginterface.html) + String TIANDITU_BASE = "https://api.tianditu.gov.cn"; + // 腾讯 TENCENT(预留) } diff --git a/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/map/ILocationService.java b/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/map/ILocationService.java index 7fffc3ba..29bd0850 100644 --- a/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/map/ILocationService.java +++ b/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/map/ILocationService.java @@ -33,7 +33,9 @@ public interface ILocationService { * @param coordinate 不同(BD09,GCJ02,WGS84)实现类坐标 * @return 世界坐标系 */ - Coordinate convertToWgs84(Coordinate coordinate); + default Coordinate convertToWgs84(Coordinate coordinate) { + return coordinate; + } /** @@ -41,5 +43,7 @@ public interface ILocationService { * @param wgs84Coordinate WGS84 坐标 * @return 转换后的目标坐标系坐标 */ - Coordinate convertPlatformCoordinateFromWgs84(Coordinate wgs84Coordinate); + default Coordinate convertPlatformCoordinateFromWgs84(Coordinate wgs84Coordinate) { + return wgs84Coordinate; + } } diff --git a/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/map/LocationServiceFactory.java b/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/map/LocationServiceFactory.java index 0ad76b7b..752bfa5e 100644 --- a/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/map/LocationServiceFactory.java +++ b/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/map/LocationServiceFactory.java @@ -20,6 +20,9 @@ public class LocationServiceFactory { @Resource private ILocationService gaodeLocationService; + @Resource + private ILocationService tiandituLocationService; + // 从配置文件读取当前使用的服务商 @Value("${map.provider:baidu}") private String provider; @@ -40,6 +43,8 @@ public class LocationServiceFactory { } if ("gaode".equalsIgnoreCase(provider)) { return gaodeLocationService; + } else if ("tianditu".equalsIgnoreCase(provider)) { + return tiandituLocationService; } /*else if ("tencent".equalsIgnoreCase(provider)) { return tencentLocationService; }*/ diff --git a/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/map/TiandituLocationService.java b/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/map/TiandituLocationService.java new file mode 100644 index 00000000..b921d6be --- /dev/null +++ b/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/map/TiandituLocationService.java @@ -0,0 +1,192 @@ +package com.fastbee.http.map; + +import com.alibaba.fastjson2.JSONObject; +import com.fastbee.common.constant.FastBeeConstant; +import com.fastbee.http.model.AddressInfo; +import com.fastbee.http.model.Coordinate; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/** + * 天地图地图API服务 + *
+ * 文档:http://lbs.tianditu.gov.cn/server/geocodinginterface.html
+ * 坐标系:天地图使用 CGCS2000,与 WGS84 在实用精度下可视为一致。
+ *
+ * @author Sumuxi
+ * @date 2026/8/12
+ */
+@Slf4j
+@Service("tiandituLocationService")
+public class TiandituLocationService implements ILocationService {
+
+ @Value("${map.tianditu_ak:b920e6076a06aa279c974f5f76cf746e}")
+ private String tiandituAK;
+
+ /** 地理编码/逆地理编码共用的接口路径 */
+ private static final String TIANDITU_PATH_GEOCODER = "/geocoder";
+
+ /**
+ * 正地理编码:中文地址 → 经纬度。
+ * 请求:http://api.tianditu.gov.cn/geocoder?ds={"keyWord":"北京市海淀区莲花池西路28号"}&tk=您的密钥
+ */
+ @Override
+ public Coordinate getCoordinateByAddress(String address) {
+ if (StringUtils.isBlank(address)) {
+ log.warn("天地图地理编码:地址为空");
+ return null;
+ }
+ if (StringUtils.isBlank(tiandituAK)) {
+ log.warn("天地图地理编码:未配置 map.tianditu_ak");
+ return null;
+ }
+
+ JSONObject ds = new JSONObject();
+ ds.put("keyWord", address.trim());
+ String encodedDs = urlEncode(ds.toJSONString());
+ if (encodedDs == null) {
+ return null;
+ }
+
+ String response = sendGet(FastBeeConstant.MAP.TIANDITU_BASE + TIANDITU_PATH_GEOCODER
+ + "?ds=" + encodedDs + "&tk=" + tiandituAK);
+ if (StringUtils.isBlank(response)) {
+ return null;
+ }
+
+ JSONObject root = JSONObject.parseObject(response);
+ if (root == null || !"0".equals(String.valueOf(root.get("status")))) {
+ log.warn("天地图地理编码失败 status={} msg={}", root != null ? root.get("status") : null,
+ root != null ? root.getString("msg") : null);
+ return null;
+ }
+ JSONObject location = root.getJSONObject("location");
+ if (location == null) {
+ log.warn("天地图地理编码:响应缺少 location");
+ return null;
+ }
+ try {
+ BigDecimal lng = new BigDecimal(location.getString("lon"));
+ BigDecimal lat = new BigDecimal(location.getString("lat"));
+ log.info("天地图地理编码:地址 {} -> 经度 {},纬度 {}", address, lng, lat);
+ return new Coordinate(lng, lat);
+ } catch (NumberFormatException | NullPointerException e) {
+ log.warn("天地图地理编码:无法解析坐标 location={}", location.toJSONString());
+ return null;
+ }
+ }
+
+ /**
+ * 逆地理编码:经纬度 → 地址。
+ * 请求:http://api.tianditu.gov.cn/geocoder?postStr={"lon":116.37304,"lat":39.92594,"ver":1}&type=geocode&tk=您的密钥
+ */
+ @Override
+ public AddressInfo getAddressByCoordinate(Coordinate coordinate) {
+ if (coordinate == null || coordinate.getLongitude() == null || coordinate.getLatitude() == null) {
+ log.warn("天地图逆地理编码:坐标为空");
+ return null;
+ }
+ if (StringUtils.isBlank(tiandituAK)) {
+ log.warn("天地图逆地理编码:未配置 map.tianditu_ak");
+ return null;
+ }
+
+ JSONObject postStr = new JSONObject();
+ postStr.put("lon", coordinate.getLongitude().doubleValue());
+ postStr.put("lat", coordinate.getLatitude().doubleValue());
+ postStr.put("ver", 1);
+ String encodedPostStr = urlEncode(postStr.toJSONString());
+ if (encodedPostStr == null) {
+ return null;
+ }
+
+ String response = sendGet(FastBeeConstant.MAP.TIANDITU_BASE + TIANDITU_PATH_GEOCODER
+ + "?postStr=" + encodedPostStr + "&type=geocode&tk=" + tiandituAK);
+ if (StringUtils.isBlank(response)) {
+ return null;
+ }
+
+ JSONObject root = JSONObject.parseObject(response);
+ if (root == null || !"0".equals(String.valueOf(root.get("status")))) {
+ log.warn("天地图逆地理编码失败 status={} msg={}", root != null ? root.get("status") : null,
+ root != null ? root.getString("msg") : null);
+ return null;
+ }
+ JSONObject result = root.getJSONObject("result");
+ if (result == null) {
+ return null;
+ }
+ AddressInfo info = new AddressInfo();
+ info.setFormattedAddress(result.getString("formatted_address"));
+ JSONObject ac = result.getJSONObject("addressComponent");
+ if (ac != null) {
+ info.setCountry(ac.getString("nation"));
+ info.setProvince(ac.getString("province"));
+ info.setCity(ac.getString("city"));
+ info.setDistrict(ac.getString("county"));
+ info.setTown(ac.getString("town"));
+ info.setStreet(ac.getString("road"));
+ info.setBuilding(ac.getString("poi"));
+ }
+ return info;
+ }
+
+ private String urlEncode(String value) {
+ try {
+ return URLEncoder.encode(value, StandardCharsets.UTF_8.toString());
+ } catch (UnsupportedEncodingException e) {
+ log.error("天地图:参数 UTF-8 编码失败", e);
+ return null;
+ }
+ }
+
+ /**
+ * 天地图接口对 User-Agent 敏感:浏览器 UA(含 {@code HttpUtils} 固定的 IE6 UA)会被 WAF
+ * 识别为浏览器访问,从而拒绝服务端 Key(HTTP 403 权限类型错误)。
+ * 因此这里使用非浏览器 UA 直连,且请求必须携带 URL 编码后的完整查询串。
+ */
+ private String sendGet(String url) {
+ HttpURLConnection conn = null;
+ try {
+ conn = (HttpURLConnection) new URL(url).openConnection();
+ conn.setRequestMethod("GET");
+ conn.setConnectTimeout(30000);
+ conn.setReadTimeout(30000);
+ conn.setRequestProperty("Accept", "*/*");
+ conn.setRequestProperty("User-Agent", "fastbee-server/3.8.5");
+ int code = conn.getResponseCode();
+ if (code != HttpURLConnection.HTTP_OK) {
+ log.warn("天地图请求失败 code={}", code);
+ return null;
+ }
+ try (BufferedReader in = new BufferedReader(
+ new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
+ StringBuilder result = new StringBuilder();
+ String line;
+ while ((line = in.readLine()) != null) {
+ result.append(line);
+ }
+ return result.toString();
+ }
+ } catch (IOException e) {
+ log.error("天地图请求异常", e);
+ return null;
+ } finally {
+ if (conn != null) {
+ conn.disconnect();
+ }
+ }
+ }
+}
diff --git a/yack-web/src/api/iot/project.js b/yack-web/src/api/iot/project.js
index 3abfc6da..9e70eae9 100644
--- a/yack-web/src/api/iot/project.js
+++ b/yack-web/src/api/iot/project.js
@@ -61,6 +61,24 @@ export function delProject(projectId) {
})
}
+// 根据中文地址查询坐标(天地图地理编码,走后端服务端密钥)
+export function getCoordinateByAddress(address) {
+ return request({
+ url: '/map/location/geocode',
+ method: 'get',
+ params: { address: address, provider: 'tianditu' }
+ })
+}
+
+// 根据经纬度查询中文地址(天地图逆地理编码,走后端服务端密钥)
+export function getAddressByCoordinate(lng, lat) {
+ return request({
+ url: '/map/location/reverseGeocode',
+ method: 'get',
+ params: { lng: lng, lat: lat, provider: 'tianditu' }
+ })
+}
+
// 根据负责人ID查询项目列表
export function listProjectByUser(userId) {
return request({
diff --git a/yack-web/src/views/iot/project/index.vue b/yack-web/src/views/iot/project/index.vue
index 42296ef9..669395b9 100644
--- a/yack-web/src/views/iot/project/index.vue
+++ b/yack-web/src/views/iot/project/index.vue
@@ -49,7 +49,7 @@