From dfd912bf07b378082773bb136e0c483e05021fb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=96=87=E6=98=8A?= Date: Thu, 16 Apr 2026 11:05:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(security):=20=E6=96=B0=E5=A2=9E=E9=AB=98?= =?UTF-8?q?=E5=BE=B7=E9=80=86=E5=9C=B0=E7=90=86=E7=BC=96=E7=A0=81=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E5=B9=B6=E9=85=8D=E7=BD=AERestTemplate=E8=B6=85?= =?UTF-8?q?=E6=97=B6=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.yml | 2 + .../framework/config/RestTemplateConfig.java | 9 +- .../ruoyi/oa/controller/OaAmapController.java | 46 +++++++ .../ruoyi/oa/domain/vo/AmapCityNameVo.java | 23 ++++ .../oa/service/IOaAmapGeocodeService.java | 18 +++ .../impl/OaAmapGeocodeServiceImpl.java | 118 ++++++++++++++++++ 6 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 ruoyi-oa/src/main/java/com/ruoyi/oa/controller/OaAmapController.java create mode 100644 ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/AmapCityNameVo.java create mode 100644 ruoyi-oa/src/main/java/com/ruoyi/oa/service/IOaAmapGeocodeService.java create mode 100644 ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/OaAmapGeocodeServiceImpl.java diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index a62f541..c89b963 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -170,6 +170,8 @@ security: - /oa/attendanceRecord/** - /oa/oaWarehouse/** - /oa/oaWarehouseMaster/** + # 高德逆地理(经纬度转城市等,供前端/H5 调用) + - /oa/amap/** # MyBatisPlus配置 # https://baomidou.com/config/ diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RestTemplateConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RestTemplateConfig.java index ac067dd..2cd0935 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RestTemplateConfig.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RestTemplateConfig.java @@ -2,13 +2,20 @@ package com.ruoyi.framework.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; @Configuration public class RestTemplateConfig { + private static final int CONNECT_TIMEOUT_MS = 5_000; + private static final int READ_TIMEOUT_MS = 15_000; + @Bean public RestTemplate restTemplate() { - return new RestTemplate(); + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(CONNECT_TIMEOUT_MS); + factory.setReadTimeout(READ_TIMEOUT_MS); + return new RestTemplate(factory); } } \ No newline at end of file diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/controller/OaAmapController.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/controller/OaAmapController.java new file mode 100644 index 0000000..7ecec8d --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/controller/OaAmapController.java @@ -0,0 +1,46 @@ +package com.ruoyi.oa.controller; + +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.oa.domain.vo.AmapCityNameVo; +import com.ruoyi.oa.service.IOaAmapGeocodeService; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +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.validation.constraints.DecimalMax; +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.NotNull; + +/** + * 高德地图:经纬度逆地理编码(城市名等) + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/oa/amap") +public class OaAmapController extends BaseController { + + private final IOaAmapGeocodeService oaAmapGeocodeService; + + /** + * 根据经纬度获取城市名称(高德逆地理编码) + */ + @GetMapping("/city") + public R cityByLocation( + @NotNull(message = "经度不能为空") + @DecimalMin(value = "-180.0", message = "经度范围无效") + @DecimalMax(value = "180.0", message = "经度范围无效") + @RequestParam Double longitude, + @NotNull(message = "纬度不能为空") + @DecimalMin(value = "-90.0", message = "纬度范围无效") + @DecimalMax(value = "90.0", message = "纬度范围无效") + @RequestParam Double latitude + ) { + AmapCityNameVo vo = oaAmapGeocodeService.reverseGeocodeCity(longitude, latitude); + return R.ok(vo); + } +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/AmapCityNameVo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/AmapCityNameVo.java new file mode 100644 index 0000000..970b1f6 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/AmapCityNameVo.java @@ -0,0 +1,23 @@ +package com.ruoyi.oa.domain.vo; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 高德逆地理编码:城市名称(及可选行政区信息,便于展示/调试) + */ +@Data +public class AmapCityNameVo implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 城市名(直辖市、省直辖等场景可能为省名或区名,与高德 addressComponent 一致) */ + private String cityName; + + /** 省 */ + private String province; + + /** 区/县 */ + private String district; +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/IOaAmapGeocodeService.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/IOaAmapGeocodeService.java new file mode 100644 index 0000000..8e094bd --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/IOaAmapGeocodeService.java @@ -0,0 +1,18 @@ +package com.ruoyi.oa.service; + +import com.ruoyi.oa.domain.vo.AmapCityNameVo; + +/** + * 高德地图逆地理编码(经纬度 → 城市等) + */ +public interface IOaAmapGeocodeService { + + /** + * 根据经纬度解析城市名称等信息 + * + * @param longitude 经度 + * @param latitude 纬度 + * @return 非 null;解析失败时 cityName 等可能为空 + */ + AmapCityNameVo reverseGeocodeCity(Double longitude, Double latitude); +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/OaAmapGeocodeServiceImpl.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/OaAmapGeocodeServiceImpl.java new file mode 100644 index 0000000..974322e --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/OaAmapGeocodeServiceImpl.java @@ -0,0 +1,118 @@ +package com.ruoyi.oa.service.impl; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.oa.domain.vo.AmapCityNameVo; +import com.ruoyi.oa.service.IOaAmapGeocodeService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * 高德逆地理编码:restapi.amap.com/v3/geocode/regeo + *

+ * 配置项:{@code fad.amap.key}(与 {@code application.yml} 中一致) + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class OaAmapGeocodeServiceImpl implements IOaAmapGeocodeService { + + private static final String AMAP_REGEO_URL = "https://restapi.amap.com/v3/geocode/regeo"; + + private final RestTemplate restTemplate; + + @Value("${fad.amap.key:}") + private String amapKey; + + @Override + public AmapCityNameVo reverseGeocodeCity(Double longitude, Double latitude) { + AmapCityNameVo vo = new AmapCityNameVo(); + if (longitude == null || latitude == null) { + return vo; + } + if (!StringUtils.hasText(amapKey)) { + log.warn("fad.amap.key 未配置,无法调用高德逆地理编码"); + return vo; + } + + try { + // 高德要求:location = 经度,纬度 + String location = longitude + "," + latitude; + String url = UriComponentsBuilder.fromHttpUrl(AMAP_REGEO_URL) + .queryParam("key", amapKey) + .queryParam("location", location) + .queryParam("extensions", "base") + .queryParam("batch", "false") + .queryParam("output", "JSON") + .build(true) + .toUriString(); + + String body = restTemplate.getForObject(url, String.class); + if (!StringUtils.hasText(body)) { + log.warn("高德逆地理编码响应为空, location={}", location); + return vo; + } + JSONObject response = JSONObject.parseObject(body); + if (response == null) { + log.warn("高德逆地理编码 JSON 解析失败, location={}", location); + return vo; + } + if (!"1".equals(response.getString("status"))) { + String info = response.getString("info"); + log.warn("高德逆地理编码失败: status={}, info={}, infocode={}", + response.getString("status"), info, response.getString("infocode")); + return vo; + } + + JSONObject regeocode = response.getJSONObject("regeocode"); + if (regeocode == null) { + return vo; + } + JSONObject addressComponent = regeocode.getJSONObject("addressComponent"); + if (addressComponent == null) { + return vo; + } + + String province = nullToEmpty(addressComponent.getString("province")); + String district = nullToEmpty(addressComponent.getString("district")); + vo.setProvince(province); + vo.setDistrict(district); + + String cityName = resolveCityName(addressComponent, province); + vo.setCityName(cityName); + return vo; + } catch (Exception e) { + log.warn("高德逆地理编码异常, longitude={}, latitude={}, err={}", + longitude, latitude, e.getMessage(), e); + return vo; + } + } + + /** + * 城市字段:普通城市为字符串;部分省直辖为 [];直辖市可能用省名表示 + */ + private static String resolveCityName(JSONObject addressComponent, String province) { + Object cityRaw = addressComponent.get("city"); + if (cityRaw instanceof JSONArray) { + JSONArray arr = (JSONArray) cityRaw; + if (!arr.isEmpty()) { + return nullToEmpty(arr.getString(0)); + } + return province; + } + String city = addressComponent.getString("city"); + if (StringUtils.hasText(city) && !"[]".equals(city)) { + return city.trim(); + } + return province; + } + + private static String nullToEmpty(String s) { + return s == null ? "" : s.trim(); + } +}