From bcc78fe2405c5ea04d4714b4c1c435ba5e3fccd1 Mon Sep 17 00:00:00 2001 From: George Date: Sun, 29 Mar 2026 09:31:27 +0800 Subject: [PATCH] =?UTF-8?q?20260329:1=E3=80=81=E8=A7=84=E8=8C=83=E5=9C=B0?= =?UTF-8?q?=E5=9B=BE=EF=BC=8C=E7=B3=BB=E7=BB=9F=E5=B7=B2=E7=BB=8F=E8=9E=8D?= =?UTF-8?q?=E5=85=A5=E9=AB=98=E5=BE=B7=E3=80=81=E7=99=BE=E5=BA=A6=E6=AD=A3?= =?UTF-8?q?=E9=80=86=E5=9C=B0=E5=9D=80=E7=BC=96=E7=A0=81=EF=BC=9B2?= =?UTF-8?q?=E3=80=81Modbus=E8=AE=BE=E5=A4=87=E7=BB=8F=E7=BA=AC=E5=BA=A6?= =?UTF-8?q?=E7=94=A8LBS=E6=8C=87=E4=BB=A4=E8=AF=BB=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/backend/BackendController.java | 49 +++- .../src/main/resources/application-dev.yml | 4 - .../src/main/resources/application-prod.yml | 4 - .../src/main/resources/application-test.yml | 6 +- .../src/main/resources/application.yml | 8 +- .../common/constant/FastBeeConstant.java | 13 +- .../utils/dtu/ATCommandParserUtils.java | 6 +- .../iot/data/aspect/DeviceDebugLogAspect.java | 13 +- .../executor/PowerOnTriggerTaskExecutor.java | 5 + .../iot/data/push/ThingsModelPushJob.java | 2 +- .../com/fastbee/http/ip/IPAddressService.java | 6 +- .../http/map/BaiduILocationService.java | 70 +++++- .../http/map/GaodeLocationService.java | 219 +++++++++++++++++- .../fastbee/http/map/ILocationService.java | 19 +- .../http/map/LocationServiceFactory.java | 21 +- .../com/fastbee/http/model/AddressInfo.java | 59 +++++ .../java/com/fastbee/iot/domain/Device.java | 2 +- .../iot/service/impl/DeviceServiceImpl.java | 20 +- 18 files changed, 478 insertions(+), 48 deletions(-) create mode 100644 fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/model/AddressInfo.java diff --git a/fastbee-admin/src/main/java/com/fastbee/web/controller/backend/BackendController.java b/fastbee-admin/src/main/java/com/fastbee/web/controller/backend/BackendController.java index 453c387a..c2e556d9 100644 --- a/fastbee-admin/src/main/java/com/fastbee/web/controller/backend/BackendController.java +++ b/fastbee-admin/src/main/java/com/fastbee/web/controller/backend/BackendController.java @@ -2,16 +2,18 @@ package com.fastbee.web.controller.backend; 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.data.redis.core.RedisTemplate; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; +import java.math.BigDecimal; import java.util.concurrent.TimeUnit; /** @@ -28,6 +30,9 @@ public class BackendController { @Resource private RedisTemplate redisTemplate; + @Resource + private LocationServiceFactory locationServiceFactory; + @ApiOperation("设置缓存值(调试/运维)") @PreAuthorize("@ss.hasPermi('monitor:cache:remove')") @PostMapping("/set") @@ -64,4 +69,40 @@ public class BackendController { redisTemplate.delete(key); return AjaxResult.success("ok"); } + + // getAddressByCoordinate + @ApiOperation("根据坐标获取地址(逆地理编码)") + @PreAuthorize("@ss.hasPermi('monitor:cache:remove')") + @GetMapping("/getAddressByCoordinate") + public AjaxResult getAddressByCoordinate(@RequestParam("coordinate") String coordinate, @RequestParam("provider") String provider) { + if (StringUtils.isEmpty(coordinate)) { + return AjaxResult.error("coordinate不能为空"); + } + + String[] split = coordinate.split(","); + + // 工厂模式获取定位服务 + ILocationService service = locationServiceFactory.getLocationService(provider); + Coordinate c = new Coordinate(BigDecimal.valueOf(Double.parseDouble(split[0])), BigDecimal.valueOf(Double.parseDouble(split[1]))); + AddressInfo addressByCoordinate = service.getAddressByCoordinate(c); + + // TODO:用地图服务根据坐标获取地址 + return AjaxResult.success(addressByCoordinate); + } + + // getCoordinateByAddress + @ApiOperation("根据地址获取坐标(正地理编码)") + @PreAuthorize("@ss.hasPermi('monitor:cache:remove')") + @GetMapping("/getCoordinateByAddress") + public AjaxResult getCoordinateByAddress(@RequestParam("address") String address, @RequestParam("provider") String provider) { + if (StringUtils.isEmpty(address)) { + return AjaxResult.error("address不能为空"); + } + // TODO:用地图服务根据地址获取坐标 + + ILocationService service = locationServiceFactory.getLocationService(provider); + Coordinate coordinateByAddress = service.getCoordinateByAddress(address); + + return AjaxResult.success(coordinateByAddress); + } } diff --git a/fastbee-admin/src/main/resources/application-dev.yml b/fastbee-admin/src/main/resources/application-dev.yml index 1edf2dc5..71cc42f5 100644 --- a/fastbee-admin/src/main/resources/application-dev.yml +++ b/fastbee-admin/src/main/resources/application-dev.yml @@ -154,7 +154,3 @@ logging: swagger: enabled: true # 是否开启swagger pathMapping: /dev-api # 请求前缀 - -# 地图配置 -map: - provider: baidu \ No newline at end of file diff --git a/fastbee-admin/src/main/resources/application-prod.yml b/fastbee-admin/src/main/resources/application-prod.yml index 9ff7919b..c936a670 100644 --- a/fastbee-admin/src/main/resources/application-prod.yml +++ b/fastbee-admin/src/main/resources/application-prod.yml @@ -153,7 +153,3 @@ logging: swagger: enabled: true # 是否开启swagger pathMapping: /prod-api # 请求前缀 - -# 地图配置 -map: - provider: baidu \ No newline at end of file diff --git a/fastbee-admin/src/main/resources/application-test.yml b/fastbee-admin/src/main/resources/application-test.yml index 711b866f..c90208b3 100644 --- a/fastbee-admin/src/main/resources/application-test.yml +++ b/fastbee-admin/src/main/resources/application-test.yml @@ -102,8 +102,4 @@ logging: # Swagger配置 swagger: enabled: true # 是否开启swagger - pathMapping: /test-api # 请求前缀 - -# 地图配置 -map: - provider: baidu \ No newline at end of file + pathMapping: /test-api # 请求前缀 \ No newline at end of file diff --git a/fastbee-admin/src/main/resources/application.yml b/fastbee-admin/src/main/resources/application.yml index 35f649a9..1790faef 100644 --- a/fastbee-admin/src/main/resources/application.yml +++ b/fastbee-admin/src/main/resources/application.yml @@ -217,5 +217,11 @@ liteflow: scriptTypeField: script_type scriptLanguageField: script_language scriptEnableField: enable -other: + + +# 地图配置 +map: + provider: gaode # 提供者 baidu_ak: 0SD4KrYeMXVu8Ox2pxQGDOw3NmDYiKIw + gaode_ak: 3af85be39e2f28334097781349336157 + gaode_secret: d0561ad20142e74f8c59edc5deafdbe4 \ No newline at end of file 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 026cda53..b3a1aff4 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 @@ -371,14 +371,17 @@ public interface FastBeeConstant { String GB28181 = "GB28181"; } - // 地图相关 + /** + * 第三方地图 Web 服务:仅存放根地址与各接口路径,完整 URL = BASE + PATH。 + */ interface MAP { - // 百度 BAIDU - String BAIDU_GEOCODING = "http://api.map.baidu.com/geocoding/v3/"; + // 百度(https://lbs.baidu.com/faq/api?title=webapi/guide/webservice-geocoding) + String BAIDU_BASE = "https://api.map.baidu.com"; - // 高德 GAODE + // 高德(https://lbs.amap.com/api/webservice/guide/api/georegeo) + String GAODE_BASE = "https://restapi.amap.com"; - // 腾讯 TENCENT + // 腾讯 TENCENT(预留) } } diff --git a/fastbee-common/src/main/java/com/fastbee/common/utils/dtu/ATCommandParserUtils.java b/fastbee-common/src/main/java/com/fastbee/common/utils/dtu/ATCommandParserUtils.java index 6aff19b1..ebaf6f91 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/utils/dtu/ATCommandParserUtils.java +++ b/fastbee-common/src/main/java/com/fastbee/common/utils/dtu/ATCommandParserUtils.java @@ -136,10 +136,10 @@ public class ATCommandParserUtils { if (lbsValue != null && lbsValue.contains(",")) { String[] parts = lbsValue.split(","); try { - // 注意:实际顺序可能是 [纬度,经度] 或 [经度,纬度],根据文档通常为经度,纬度 + // 返回 [经度,纬度] double latitude = Double.parseDouble(parts[0]); double longitude = Double.parseDouble(parts[1]); - return new double[]{latitude, longitude}; + return new double[]{longitude, latitude}; } catch (NumberFormatException e) { return null; } @@ -292,7 +292,7 @@ public class ATCommandParserUtils { System.out.println("LBS解析结果: " + parseLBSString(lbsHex)); double[] lbs = parseLBS(lbsHex); if (lbs != null) { - System.out.println("纬度: " + lbs[0] + ", 经度: " + lbs[1]); + System.out.println("经度: " + lbs[0] + ", 纬度: " + lbs[1]); } // 测试通用解析 diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/aspect/DeviceDebugLogAspect.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/aspect/DeviceDebugLogAspect.java index 6a32f077..2d177f5d 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/aspect/DeviceDebugLogAspect.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/aspect/DeviceDebugLogAspect.java @@ -33,6 +33,7 @@ import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import javax.annotation.Resource; +import java.math.BigDecimal; import java.util.*; import java.util.function.Function; @@ -312,7 +313,7 @@ public class DeviceDebugLogAspect { return; } - // 跟新IMEI、ICCID + // 更新IMEI、ICCID、longitude、latitude if (StringUtils.isEmpty(device.getImei()) || StringUtils.isEmpty(device.getIccid())) { Map atCommandMap = ATCommandParserUtils.parseATResponseFromHex(context.sourceData); Device updateDevice = new Device(); @@ -322,6 +323,16 @@ public class DeviceDebugLogAspect { if (StringUtils.isEmpty(device.getIccid())) { updateDevice.setIccid(atCommandMap.get("ICCID")); } + if (device.getLongitude() == null && device.getLatitude() == null) { + String lbsValue = atCommandMap.get("LBS"); + if (lbsValue != null) { + String[] lbsArray = lbsValue.split(","); + if (lbsArray.length == 2) { + updateDevice.setLongitude(new BigDecimal(lbsArray[1])); + updateDevice.setLatitude(new BigDecimal(lbsArray[0])); + } + } + } updateDevice.setDeviceId(device.getDeviceId()); updateDevice.setUpdateBy("DTU"); updateDevice.setUpdateTime(DateUtils.getNowDate()); diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/executor/PowerOnTriggerTaskExecutor.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/executor/PowerOnTriggerTaskExecutor.java index 7c8ec626..141611fe 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/executor/PowerOnTriggerTaskExecutor.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/executor/PowerOnTriggerTaskExecutor.java @@ -118,6 +118,11 @@ public class PowerOnTriggerTaskExecutor { deviceMessage.setMessage("41542b49434349445c725c6e"); deviceMessageService.messagePost(deviceMessage); } + + if (device.getLongitude() == null || device.getLatitude() == null) { + deviceMessage.setMessage("41542b4c42535c725c6e"); // AT+LBS\r\n + deviceMessageService.messagePost(deviceMessage); + } } /** diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/push/ThingsModelPushJob.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/push/ThingsModelPushJob.java index 08e8c42d..6d14a807 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/push/ThingsModelPushJob.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/push/ThingsModelPushJob.java @@ -29,7 +29,7 @@ public class ThingsModelPushJob { /** * 每分钟执行一次,检查并触发到期的推送任务 */ - @Scheduled(fixedDelay = 60_000) + @Scheduled(fixedDelay = 300_000) public void execute() { List configs = pushConfigService.selectAllEnabled(); if (CollectionUtils.isEmpty(configs)) { diff --git a/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/ip/IPAddressService.java b/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/ip/IPAddressService.java index 8a032ac7..17976e47 100644 --- a/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/ip/IPAddressService.java +++ b/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/ip/IPAddressService.java @@ -1,8 +1,4 @@ -package com.fastbee.http.ip;/** - * @description: IP地址相关客户端调用 - * @author Sumuxi - * @date 2026/3/8 - */ +package com.fastbee.http.ip; import com.alibaba.fastjson2.JSONObject; import com.fastbee.common.constant.Constants; diff --git a/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/map/BaiduILocationService.java b/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/map/BaiduILocationService.java index 33d0e62f..59e730c6 100644 --- a/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/map/BaiduILocationService.java +++ b/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/map/BaiduILocationService.java @@ -5,6 +5,7 @@ import com.fastbee.common.constant.Constants; import com.fastbee.common.constant.ErrorCodeConstants; import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.common.utils.http.HttpUtils; +import com.fastbee.http.model.AddressInfo; import com.fastbee.http.model.Coordinate; import com.fastbee.http.utils.CoordinateTransformUtils; import lombok.extern.slf4j.Slf4j; @@ -16,6 +17,7 @@ import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.util.Locale; import java.util.Objects; import static com.fastbee.common.exception.ServiceExceptionUtil.exception; @@ -30,9 +32,15 @@ import static com.fastbee.common.exception.ServiceExceptionUtil.exception; @Service("baiduLocationService") public class BaiduILocationService implements ILocationService { - @Value("${other.baidu_ak}") + @Value("${map.baidu_ak}") private String baiduAK; + // 地理编码 + private static final String BAIDU_PATH_GEOCODING = "/geocoding/v3/"; + + // 逆地理编码 + private static final String BAIDU_PATH_REVERSE_GEOCODING = "/reverse_geocoding/v3/"; + /** * 根据结构化地址获取地理编码(即经纬度) * 地理编码服务提供将结构化地址数据(如:北京市海淀区上地十街十号)转换为对应坐标点(经纬度)功能。 @@ -43,7 +51,7 @@ public class BaiduILocationService implements ILocationService { public Coordinate getCoordinateByAddress(String address) { log.info("结构化地址:{}", address); - String baseUrl = FastBeeConstant.MAP.BAIDU_GEOCODING; + String baseUrl = FastBeeConstant.MAP.BAIDU_BASE + BAIDU_PATH_GEOCODING; address = StringUtils.isEmpty(address) || Objects.equals("内网IP", address) ? "湖南省长沙市岳麓区" : address; String encodedAddress; try { @@ -61,6 +69,55 @@ public class BaiduILocationService implements ILocationService { return null; } + /** + * 逆地理编码:坐标系为 BD09,与百度地理编码返回及本类 {@link #convertPlatformCoordinateFromWgs84} 一致。 + * 文档:https://lbs.baidu.com/faq/api?title=webapi/guide/webservice-geocoding + */ + @Override + public AddressInfo getAddressByCoordinate(Coordinate coordinate) { + if (coordinate == null || coordinate.getLongitude() == null || coordinate.getLatitude() == null) { + log.warn("百度逆地理编码:坐标为空"); + return null; + } + if (StringUtils.isBlank(baiduAK)) { + log.warn("百度逆地理编码:未配置 map.baidu_ak"); + return null; + } + double lat = coordinate.getLatitude().doubleValue(); + double lng = coordinate.getLongitude().doubleValue(); + // location:纬度,经度;coordtype 与坐标系一致 + String location = String.format(Locale.US, "%.6f,%.6f", lat, lng); + String param = "ak=" + baiduAK + "&output=json&coordtype=bd09ll&location=" + location; + String response = HttpUtils.sendGet(FastBeeConstant.MAP.BAIDU_BASE + BAIDU_PATH_REVERSE_GEOCODING, param, Constants.UTF8); + if (StringUtils.isEmpty(response)) { + return null; + } + JSONObject root = JSONObject.parseObject(response); + if (root == null || root.getIntValue("status") != 0) { + log.warn("百度逆地理编码失败 status={} message={}", root != null ? root.get("status") : null, + root != null ? root.getString("message") : 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("country")); + info.setProvince(ac.getString("province")); + info.setCity(ac.getString("city")); + info.setDistrict(ac.getString("district")); + info.setTown(ac.getString("town")); + info.setStreet(ac.getString("street")); + info.setStreetNumber(ac.getString("street_number")); + } + info.setBuilding(result.getString("business")); + return info; + } + @Override public Coordinate convertToWgs84(Coordinate baiduCoordinate) { @@ -71,4 +128,13 @@ public class BaiduILocationService implements ILocationService { return new Coordinate(BigDecimal.valueOf(doubles[0]), BigDecimal.valueOf(doubles[1])); } + @Override + public Coordinate convertPlatformCoordinateFromWgs84(Coordinate wgs84Coordinate) { + + //世界坐标系(WGS84)的坐标转换为百度坐标系(BD09) + double[] doubles = CoordinateTransformUtils.wgs84ToBd09(wgs84Coordinate.getLongitude().doubleValue(), + wgs84Coordinate.getLatitude().doubleValue()); + return new Coordinate(BigDecimal.valueOf(doubles[0]), BigDecimal.valueOf(doubles[1])); + } + } diff --git a/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/map/GaodeLocationService.java b/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/map/GaodeLocationService.java index c7ac5324..d9743955 100644 --- a/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/map/GaodeLocationService.java +++ b/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/map/GaodeLocationService.java @@ -1,10 +1,27 @@ package com.fastbee.http.map; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.fastbee.common.constant.Constants; +import com.fastbee.common.constant.FastBeeConstant; +import com.fastbee.common.utils.http.HttpUtils; +import com.fastbee.common.utils.sign.Md5Utils; +import com.fastbee.http.model.AddressInfo; import com.fastbee.http.model.Coordinate; import com.fastbee.http.utils.CoordinateTransformUtils; +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.UnsupportedEncodingException; import java.math.BigDecimal; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; /** * @description: 高德地图API实现类 @@ -12,15 +29,205 @@ import java.math.BigDecimal; * @author Sumuxi * @date 2026/3/8 */ +@Slf4j @Service("gaodeLocationService") public class GaodeLocationService implements ILocationService { + @Value("${map.gaode_ak:3af85be39e2f28334097781349336157}") + private String gaodeAK; + /** + * 数字签名私钥(控制台开启「数字签名」后必填)。 + * 算法参见:https://lbs.amap.com/faq/quota-key/key/41181 + */ + @Value("${map.gaode_secret:d0561ad20142e74f8c59edc5deafdbe4}") + private String gaodeSecret; + /** 地理编码:地址 → 经纬度(GCJ-02) */ + private static final String GAODE_PATH_GEO = "/v3/geocode/geo"; + /** 逆地理编码:经纬度 → 地址 */ + private static final String GAODE_PATH_REGEO = "/v3/geocode/regeo"; + + /** + * 地理编码:结构化地址转经纬度,坐标系为 GCJ-02(火星坐标),与高德 Web 服务一致。 + * 文档:https://lbs.amap.com/api/webservice/guide/api/georegeo + * 数字签名:MD5 使用 UTF-8 下原始参数值拼接(与控制台一致),请求中的 address 需单独 URL 编码,参见 + * https://lbs.amap.com/faq/quota-key/key/41181 + */ @Override public Coordinate getCoordinateByAddress(String address) { - return null; + if (StringUtils.isBlank(address)) { + log.warn("高德地理编码:地址为空"); + return null; + } + if (StringUtils.isBlank(gaodeAK)) { + log.warn("高德地理编码:未配置 map.gaode_ak"); + return null; + } + String raw = address.trim(); + TreeMap forSign = new TreeMap<>(); + forSign.put("address", raw); + forSign.put("output", "json"); + forSign.put("key", gaodeAK); + String sig = null; + if (StringUtils.isNotBlank(gaodeSecret)) { + sig = Md5Utils.hash(joinKeyValueSorted(forSign) + gaodeSecret); + } + String encodedAddress; + try { + encodedAddress = URLEncoder.encode(raw, StandardCharsets.UTF_8.toString()); + } catch (UnsupportedEncodingException e) { + log.error("高德地理编码:地址 UTF-8 编码失败", e); + return null; + } + StringBuilder qb = new StringBuilder(); + qb.append("address=").append(encodedAddress); + qb.append("&key=").append(gaodeAK); + qb.append("&output=json"); + if (sig != null) { + qb.append("&sig=").append(sig); + } + String response = HttpUtils.sendGet(FastBeeConstant.MAP.GAODE_BASE + GAODE_PATH_GEO, qb.toString(), Constants.UTF8); + if (StringUtils.isBlank(response)) { + return null; + } + JSONObject root = JSONObject.parseObject(response); + if (root == null || !"1".equals(String.valueOf(root.get("status")))) { + log.warn("高德地理编码失败 status={} info={}", root != null ? root.get("status") : null, + root != null ? root.getString("info") : null); + return null; + } + JSONArray geocodes = root.getJSONArray("geocodes"); + if (geocodes == null || geocodes.isEmpty()) { + return null; + } + JSONObject first = geocodes.getJSONObject(0); + if (first == null) { + return null; + } + String location = first.getString("location"); + if (StringUtils.isBlank(location)) { + return null; + } + String[] parts = location.split(","); + if (parts.length != 2) { + return null; + } + try { + BigDecimal lng = new BigDecimal(parts[0].trim()); + BigDecimal lat = new BigDecimal(parts[1].trim()); + return new Coordinate(lng, lat); + } catch (NumberFormatException e) { + log.warn("高德地理编码:无法解析坐标 location={}", location); + return null; + } + } + + /** + * 逆地理编码:坐标系为 GCJ-02(火星坐标),与高德 Web 服务一致。 + * 文档:https://lbs.amap.com/api/webservice/guide/api/georegeo + */ + @Override + public AddressInfo getAddressByCoordinate(Coordinate coordinate) { + if (coordinate == null || coordinate.getLongitude() == null || coordinate.getLatitude() == null) { + log.warn("高德逆地理编码:坐标为空"); + return null; + } + if (StringUtils.isBlank(gaodeAK)) { + log.warn("高德逆地理编码:未配置 map.gaode_key"); + return null; + } + double lng = coordinate.getLongitude().doubleValue(); + double lat = coordinate.getLatitude().doubleValue(); + // 经度在前、纬度在后,小数点后不超过 6 位 + String location = String.format(Locale.US, "%.6f,%.6f", lng, lat); + Map regeoParams = new HashMap<>(4); + regeoParams.put("extensions", "base"); + regeoParams.put("location", location); + regeoParams.put("output", "json"); + String param = buildGaodeWebServiceQuery(regeoParams); + String response = HttpUtils.sendGet(FastBeeConstant.MAP.GAODE_BASE + GAODE_PATH_REGEO, param, Constants.UTF8); + if (StringUtils.isBlank(response)) { + return null; + } + JSONObject root = JSONObject.parseObject(response); + if (root == null || !"1".equals(String.valueOf(root.get("status")))) { + log.warn("高德逆地理编码失败 status={} info={}", root != null ? root.get("status") : null, + root != null ? root.getString("info") : null); + return null; + } + JSONObject regeocode = root.getJSONObject("regeocode"); + if (regeocode == null) { + return null; + } + AddressInfo info = new AddressInfo(); + info.setFormattedAddress(regeocode.getString("formatted_address")); + JSONObject ac = regeocode.getJSONObject("addressComponent"); + if (ac != null) { + info.setCountry(ac.getString("country")); + info.setProvince(ac.getString("province")); + info.setCity(parseGaodeCity(ac)); + info.setDistrict(ac.getString("district")); + info.setTown(ac.getString("township")); + JSONObject streetNumber = ac.getJSONObject("streetNumber"); + if (streetNumber != null) { + info.setStreet(streetNumber.getString("street")); + info.setStreetNumber(streetNumber.getString("number")); + } + JSONObject building = ac.getJSONObject("building"); + if (building != null) { + info.setBuilding(building.getString("name")); + } + + } + return info; + } + + /** + * 直辖市等场景下 city 可能为空数组,需回退到省名等。 + */ + private static String parseGaodeCity(JSONObject ac) { + Object cityVal = ac.get("city"); + if (cityVal instanceof String) { + String s = (String) cityVal; + if (StringUtils.isNotBlank(s)) { + return s; + } + } else if (cityVal instanceof JSONArray) { + JSONArray arr = (JSONArray) cityVal; + if (!arr.isEmpty()) { + return arr.getString(0); + } + } + String province = ac.getString("province"); + return StringUtils.isNotBlank(province) ? province : null; + } + + /** + * 组装高德 Web 服务 GET 查询串:参数名升序拼接后接私钥做 MD5 得到 sig(与官方数字签名说明一致)。 + * 未配置 {@code map.gaode_secret} 时不传 sig,适用于未开启数字签名的 Key。 + */ + private String buildGaodeWebServiceQuery(Map paramsWithoutKey) { + TreeMap sorted = new TreeMap<>(paramsWithoutKey); + sorted.put("key", gaodeAK); + if (StringUtils.isNotBlank(gaodeSecret)) { + String signSource = joinKeyValueSorted(sorted) + gaodeSecret; + sorted.put("sig", Md5Utils.hash(signSource)); + } + + return joinKeyValueSorted(sorted); + } + + private static String joinKeyValueSorted(TreeMap sorted) { + StringBuilder sb = new StringBuilder(); + for (Map.Entry e : sorted.entrySet()) { + if (sb.length() > 0) { + sb.append('&'); + } + sb.append(e.getKey()).append('=').append(e.getValue()); + } + return sb.toString(); } @Override @@ -33,4 +240,14 @@ public class GaodeLocationService implements ILocationService { return new Coordinate(BigDecimal.valueOf(doubles[0]), BigDecimal.valueOf(doubles[1])); } + + @Override + public Coordinate convertPlatformCoordinateFromWgs84(Coordinate wgs84Coordinate) { + // 将世界坐标 转化为 高德(火星)坐标 + double[] doubles = CoordinateTransformUtils.wgs84ToGcj02( + wgs84Coordinate.getLongitude().doubleValue(), + wgs84Coordinate.getLatitude().doubleValue() + ); + return new Coordinate(BigDecimal.valueOf(doubles[0]), BigDecimal.valueOf(doubles[1])); + } } 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 737fe321..7fffc3ba 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 @@ -1,5 +1,6 @@ package com.fastbee.http.map; +import com.fastbee.http.model.AddressInfo; import com.fastbee.http.model.Coordinate; /** @@ -18,11 +19,13 @@ public interface ILocationService { Coordinate getCoordinateByAddress(String address); /** - * 根据经纬度获取详细地址 - * @param coordinate 经纬度 - * @return 详细地址信息 + * 根据经纬度逆地理编码获取结构化地址。 + * 坐标系因实现而异:{@code gaodeLocationService} 使用 GCJ-02;{@code baiduLocationService} 使用 BD09。 + * + * @param coordinate 经纬度(经度、纬度) + * @return 详细地址信息;参数无效、配置缺失或第三方失败时可能为 {@code null} */ - //AddressInfo getAddressByCoordinate(Coordinate coordinate); + AddressInfo getAddressByCoordinate(Coordinate coordinate); /** @@ -31,4 +34,12 @@ public interface ILocationService { * @return 世界坐标系 */ Coordinate convertToWgs84(Coordinate coordinate); + + + /** + * 将世界坐标系(WGS84)的坐标转换为指定平台(高德、百度、腾讯)坐标系。 + * @param wgs84Coordinate WGS84 坐标 + * @return 转换后的目标坐标系坐标 + */ + Coordinate convertPlatformCoordinateFromWgs84(Coordinate 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 31aa8e1c..0ad76b7b 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 @@ -26,14 +26,23 @@ public class LocationServiceFactory { /** * 获取当前配置的 LocationService 实例 + * @param providerArr 指定地图提供者,优先级高于配置文件 * @return 具体服务实现 */ - public ILocationService getLocationService() { -// if ("gaode".equalsIgnoreCase(provider)) { -// return gaodeLocationService; -// } else if ("tencent".equalsIgnoreCase(provider)) { -// return tencentLocationService; -// } + public ILocationService getLocationService(String... providerArr) { + + String provider; + // 优先参数指定服务商 + if (providerArr != null && providerArr.length > 0){ + provider = providerArr[0]; + } else { + provider = this.provider; + } + if ("gaode".equalsIgnoreCase(provider)) { + return gaodeLocationService; + } /*else if ("tencent".equalsIgnoreCase(provider)) { + return tencentLocationService; + }*/ // 默认返回百度 return baiduLocationService; } diff --git a/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/model/AddressInfo.java b/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/model/AddressInfo.java new file mode 100644 index 00000000..1d748d91 --- /dev/null +++ b/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/model/AddressInfo.java @@ -0,0 +1,59 @@ +package com.fastbee.http.model; + +import lombok.Data; + +/** + * @description: 地址信息 + * 国家、省份、城市、区县、城镇、乡村、街道、门牌号码、屋邨、大厦 + * + * @author Sumuxi + * @date 2026/3/28 + */ +@Data +public class AddressInfo { + + /** + * 格式化地址 + */ + private String formattedAddress; + + /** + * 国家 + */ + private String country; + + /** + * 省份 + */ + private String province; + + /** + * 城市 + */ + private String city; + + /** + * 区/县 + */ + private String district; + + /** + * 乡/镇 + */ + private String town; + + /** + * 街道 + */ + private String street; + + /** + * 门牌号 + */ + private String streetNumber; + + /** + * 建筑/楼栋 + */ + private String building; +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Device.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Device.java index 58b8e386..16ec4ef2 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Device.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Device.java @@ -181,7 +181,7 @@ public class Device extends BaseEntity { /** * 是否自定义位置 **/ - @ApiModelProperty("定位方式(1=ip自动定位,2=设备定位,3=自定义)") + @ApiModelProperty("定位方式(1=自动定位[ip地址、modbus LBS],2=设备定位,3=自定义)") private Integer locationWay; /** diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java index fa3a6e98..9c969bb2 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java @@ -27,6 +27,7 @@ import com.fastbee.common.utils.json.JsonUtils; import com.fastbee.http.ip.IPAddressService; 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 com.fastbee.iot.cache.IDeviceCache; import com.fastbee.iot.cache.ITSLCache; @@ -1099,6 +1100,7 @@ public class DeviceServiceImpl implements IDeviceService { // 随机经纬度和地址 //SysUser user = getLoginUser().getUser(); device.setNetworkIp(sysUser.getLoginIp()); + device.setTransport(product.getTransport()); // 便于Modbus设备LBS定位 setLocation(sysUser.getLoginIp(), device); deviceMapper.insertDevice(device); // 处理modbus产品 @@ -1632,13 +1634,29 @@ public class DeviceServiceImpl implements IDeviceService { * @param device 设备信息 */ private void setLocation(String ip, Device device) { + + // 工厂模式获取定位服务 + ILocationService service = locationServiceFactory.getLocationService(); + + // Modbus设备采用LBS定位(从设备获取经纬度) + if (Objects.equals(1, device.getLocationWay()) && FastBeeConstant.PROTOCOL.ModbusRtu.equals(device.getTransport())) { + log.info("Modbus设备采用LBS定位{}", device.getSerialNumber()); + // 将经纬度转换为地址编码 networkAddress + if (StringUtils.isEmpty(device.getNetworkAddress()) && device.getLongitude() != null && device.getLatitude() != null) { + Coordinate platformCoordinate = service.convertPlatformCoordinateFromWgs84(new Coordinate(device.getLongitude(), device.getLatitude())); + AddressInfo addressInfo = service.getAddressByCoordinate(platformCoordinate); + String networkAddress = StringUtils.isEmpty(addressInfo.getBuilding()) ? addressInfo.getFormattedAddress() : addressInfo.getBuilding(); + device.setNetworkAddress(networkAddress); + log.info("{}- 设置地址:{}", device.getSerialNumber(), networkAddress); + } + return; + } try { // 根据IP获取地址 String address = ipAddressService.getAddressByIP(ip); device.setNetworkAddress(address); log.info("{}- 设置地址:{}", device.getSerialNumber(), address); // 查询经纬度 - ILocationService service = locationServiceFactory.getLocationService(); Coordinate coordinate = service.getCoordinateByAddress(address); Coordinate wgs84Coordinate = service.convertToWgs84(coordinate); device.setLongitude(wgs84Coordinate.getLongitude());