diff --git a/src/main/java/com/rj/AISmartCard20251230Application.java b/src/main/java/com/rj/AISmartCard20251230Application.java index 2dff60f..b341a2c 100644 --- a/src/main/java/com/rj/AISmartCard20251230Application.java +++ b/src/main/java/com/rj/AISmartCard20251230Application.java @@ -1,5 +1,6 @@ package com.rj; +import com.rj.config.AmapProperties; import com.rj.config.YihangyiVllmAsrProperties; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; @@ -39,7 +40,7 @@ import org.springframework.scheduling.annotation.EnableScheduling; @MapperScan("com.rj.mapper") @SpringBootApplication @EnableScheduling -@EnableConfigurationProperties(YihangyiVllmAsrProperties.class) +@EnableConfigurationProperties({YihangyiVllmAsrProperties.class, AmapProperties.class}) public class AISmartCard20251230Application { public static void main(String[] args) { diff --git a/src/main/java/com/rj/config/AmapProperties.java b/src/main/java/com/rj/config/AmapProperties.java new file mode 100644 index 0000000..8e47663 --- /dev/null +++ b/src/main/java/com/rj/config/AmapProperties.java @@ -0,0 +1,17 @@ +package com.rj.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 高德开放平台 Web 服务 Key(地理编码、周边搜索等)。 + */ +@Data +@ConfigurationProperties(prefix = "amap") +public class AmapProperties { + + /** + * Web 服务类型的 Key,对应控制台「应用 Key」。 + */ + private String webServiceKey = ""; +} diff --git a/src/main/java/com/rj/controller/CustomerManagementController.java b/src/main/java/com/rj/controller/CustomerManagementController.java index 7100fc0..ecdfaa1 100644 --- a/src/main/java/com/rj/controller/CustomerManagementController.java +++ b/src/main/java/com/rj/controller/CustomerManagementController.java @@ -6,11 +6,13 @@ import com.rj.common.AudioManagementConstants; import com.rj.common.DataEnrichmentUtil; import com.rj.dto.CustomerPhotoPreviewResult; import com.rj.dto.CustomerPhotoUploadResult; +import com.rj.dto.NearbyMerchantSearchResult; import com.rj.entity.AudioManagement; import com.rj.entity.CustomerManagement; import com.rj.entity.SalesManagement; import com.rj.service.IAudioManagementService; import com.rj.service.ICustomerManagementService; +import com.rj.service.INearbyMerchantSearchService; import com.rj.service.ISalesManagementService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -53,6 +55,9 @@ public class CustomerManagementController { @Autowired private ISalesManagementService salesManagementService; + @Autowired + private INearbyMerchantSearchService nearbyMerchantSearchService; + /** * 新增客户 */ @@ -202,6 +207,30 @@ public class CustomerManagementController { @Autowired private DataEnrichmentUtil dataEnrichmentUtil; + /** + * 根据城市与地址做地理编码,再按半径搜索周边商家(高德 place/around,逻辑同 AllShopCollectorV5)。 + */ + @GetMapping("/searchNearbyMerchants") + @Operation(summary = "附近商家检索", description = "根据 CITY、ADDRESS 解析坐标,按 RADIUS、MAX_PAGE、maxRecordsOfpage 调用高德周边搜索") + public ResponseEntity> searchNearbyMerchants( + @Parameter(description = "城市(传给高德地理编码 city)", required = true) @RequestParam("CITY") String city, + @Parameter(description = "地址/地标(传给高德地理编码 address)", required = true) @RequestParam("ADDRESS") String address, + @Parameter(description = "搜索半径(米)", required = true) @RequestParam("RADIUS") int radius, + @Parameter(description = "最大请求页数", required = true) @RequestParam("MAX_PAGE") int maxPage, + @Parameter(description = "每页条数(1~50,高德 offset)", required = true) @RequestParam("maxRecordsOfpage") int maxRecordsOfpage) { + NearbyMerchantSearchResult search = nearbyMerchantSearchService.searchNearbyMerchants( + city, address, radius, maxPage, maxRecordsOfpage); + Map result = new HashMap<>(); + result.put("success", search.isSuccess()); + result.put("message", search.getMessage()); + if (search.isSuccess()) { + result.put("centerLocation", search.getCenterLocation()); + result.put("totalCount", search.getTotalCount()); + result.put("data", search.getMerchants()); + } + return ResponseEntity.status(search.getHttpStatus()).body(result); + } + /** * 分页查询客户列表 */ diff --git a/src/main/java/com/rj/dto/NearbyMerchantItem.java b/src/main/java/com/rj/dto/NearbyMerchantItem.java new file mode 100644 index 0000000..ee4130e --- /dev/null +++ b/src/main/java/com/rj/dto/NearbyMerchantItem.java @@ -0,0 +1,21 @@ +package com.rj.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 高德周边 POI 中的一条商家记录(与 {@code AllShopCollectorV5} 解析字段一致)。 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class NearbyMerchantItem { + + private String name; + private String address; + private String tel; + private String type; + /** 经纬度,格式 "lng,lat" */ + private String location; +} diff --git a/src/main/java/com/rj/dto/NearbyMerchantSearchResult.java b/src/main/java/com/rj/dto/NearbyMerchantSearchResult.java new file mode 100644 index 0000000..9ec2376 --- /dev/null +++ b/src/main/java/com/rj/dto/NearbyMerchantSearchResult.java @@ -0,0 +1,43 @@ +package com.rj.dto; + +import lombok.Getter; + +import java.util.Collections; +import java.util.List; + +/** + * 根据城市+地址做地理编码后,按半径周边搜索商家的结果。 + */ +@Getter +public class NearbyMerchantSearchResult { + + private final int httpStatus; + private final boolean success; + private final String message; + private final String centerLocation; + private final List merchants; + /** 高德返回的本次检索总条数(字符串),可能大于已拉取页数之和 */ + private final String totalCount; + + private NearbyMerchantSearchResult(int httpStatus, boolean success, String message, + String centerLocation, List merchants, String totalCount) { + this.httpStatus = httpStatus; + this.success = success; + this.message = message; + this.centerLocation = centerLocation; + this.merchants = merchants == null ? Collections.emptyList() : merchants; + this.totalCount = totalCount; + } + + public static NearbyMerchantSearchResult ok(String centerLocation, List merchants, String totalCount) { + return new NearbyMerchantSearchResult(200, true, "查询成功", centerLocation, merchants, totalCount); + } + + public static NearbyMerchantSearchResult badRequest(String message) { + return new NearbyMerchantSearchResult(400, false, message, null, Collections.emptyList(), null); + } + + public static NearbyMerchantSearchResult serverError(String message) { + return new NearbyMerchantSearchResult(500, false, message, null, Collections.emptyList(), null); + } +} diff --git a/src/main/java/com/rj/service/INearbyMerchantSearchService.java b/src/main/java/com/rj/service/INearbyMerchantSearchService.java new file mode 100644 index 0000000..db904f4 --- /dev/null +++ b/src/main/java/com/rj/service/INearbyMerchantSearchService.java @@ -0,0 +1,21 @@ +package com.rj.service; + +import com.rj.dto.NearbyMerchantSearchResult; + +/** + * 基于高德地图:地址转坐标 + 周边全行业 POI(商家)检索。 + */ +public interface INearbyMerchantSearchService { + + /** + * 根据城市、详细地址得到中心点,再按半径分页拉取周边商家。 + * + * @param city 城市(传给地理编码 city 参数,如「河北省廊坊市三河市燕郊镇」) + * @param address 地址/地标(传给地理编码 address 参数) + * @param radiusMeters 周边搜索半径(米) + * @param maxPage 最多请求页数(从 1 递增) + * @param maxRecordsPerPage 每页条数,高德允许 1~50,超出会被裁剪 + */ + NearbyMerchantSearchResult searchNearbyMerchants(String city, String address, + int radiusMeters, int maxPage, int maxRecordsPerPage); +} diff --git a/src/main/java/com/rj/service/impl/NearbyMerchantSearchServiceImpl.java b/src/main/java/com/rj/service/impl/NearbyMerchantSearchServiceImpl.java new file mode 100644 index 0000000..5c6f617 --- /dev/null +++ b/src/main/java/com/rj/service/impl/NearbyMerchantSearchServiceImpl.java @@ -0,0 +1,154 @@ +package com.rj.service.impl; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.rj.config.AmapProperties; +import com.rj.dto.NearbyMerchantItem; +import com.rj.dto.NearbyMerchantSearchResult; +import com.rj.service.INearbyMerchantSearchService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.ArrayList; +import java.util.List; + +/** + * 逻辑参考 {@link com.rj.gaode.AllShopCollectorV5}:地理编码 + place/around 全行业周边搜索。 + */ +@Slf4j +@Service +public class NearbyMerchantSearchServiceImpl implements INearbyMerchantSearchService { + + private static final String GEOCODE_URL = "https://restapi.amap.com/v3/geocode/geo"; + private static final String AROUND_URL = "https://restapi.amap.com/v3/place/around"; + private static final int AMAP_OFFSET_MAX = 50; + private static final int AMAP_PAGE_DELAY_MS = 800; + + private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; + private final AmapProperties amapProperties; + + public NearbyMerchantSearchServiceImpl(RestTemplate restTemplate, + ObjectMapper objectMapper, + AmapProperties amapProperties) { + this.restTemplate = restTemplate; + this.objectMapper = objectMapper; + this.amapProperties = amapProperties; + } + + @Override + public NearbyMerchantSearchResult searchNearbyMerchants(String city, String address, + int radiusMeters, int maxPage, int maxRecordsPerPage) { + if (!StringUtils.hasText(amapProperties.getWebServiceKey())) { + return NearbyMerchantSearchResult.badRequest("未配置高德 Web 服务 Key,请在配置中设置 amap.web-service-key"); + } + if (!StringUtils.hasText(city) || !StringUtils.hasText(address)) { + return NearbyMerchantSearchResult.badRequest("CITY 与 ADDRESS 不能为空"); + } + if (radiusMeters <= 0) { + return NearbyMerchantSearchResult.badRequest("RADIUS 须为正整数(米)"); + } + if (maxPage < 1) { + return NearbyMerchantSearchResult.badRequest("MAX_PAGE 须大于等于 1"); + } + int offset = Math.min(AMAP_OFFSET_MAX, Math.max(1, maxRecordsPerPage)); + int pages = Math.min(maxPage, 100); + + try { + String location = geocodeToLocation(city.trim(), address.trim()); + List all = new ArrayList<>(); + String totalCount = null; + for (int page = 1; page <= pages; page++) { + String json = fetchAroundPage(location, radiusMeters, offset, page); + JsonNode root = objectMapper.readTree(json); + if (!"1".equals(root.path("status").asText())) { + String info = root.path("info").asText("高德接口返回失败"); + return NearbyMerchantSearchResult.badRequest("周边搜索失败:" + info); + } + if (totalCount == null) { + totalCount = root.path("count").asText(null); + } + JsonNode pois = root.path("pois"); + if (pois.isArray()) { + for (JsonNode p : pois) { + all.add(new NearbyMerchantItem( + text(p, "name"), + text(p, "address"), + text(p, "tel"), + text(p, "type"), + text(p, "location") + )); + } + } + if (page < pages) { + try { + Thread.sleep(AMAP_PAGE_DELAY_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return NearbyMerchantSearchResult.serverError("查询被中断"); + } + } + } + return NearbyMerchantSearchResult.ok(location, all, totalCount); + } catch (IllegalArgumentException e) { + return NearbyMerchantSearchResult.badRequest(e.getMessage()); + } catch (Exception e) { + log.warn("附近商家检索异常: {}", e.getMessage()); + return NearbyMerchantSearchResult.serverError("附近商家检索异常:" + e.getMessage()); + } + } + + private String geocodeToLocation(String city, String address) throws Exception { + String url = UriComponentsBuilder.fromUriString(GEOCODE_URL) + .queryParam("key", amapProperties.getWebServiceKey()) + .queryParam("city", city) + .queryParam("address", address) + .queryParam("output", "json") + .build() + .encode() + .toUriString(); + String json = restTemplate.getForObject(url, String.class); + JsonNode root = objectMapper.readTree(json); + if (!"1".equals(root.path("status").asText())) { + String info = root.path("info").asText("地理编码失败"); + throw new IllegalArgumentException("地理编码失败:" + info); + } + JsonNode geocodes = root.path("geocodes"); + if (!geocodes.isArray() || geocodes.isEmpty()) { + throw new IllegalArgumentException("未解析到该地址的经纬度,请检查 CITY、ADDRESS"); + } + String loc = geocodes.get(0).path("location").asText(""); + if (!StringUtils.hasText(loc)) { + throw new IllegalArgumentException("地理编码结果中无 location 字段"); + } + return loc; + } + + private String fetchAroundPage(String location, int radius, int offset, int page) { + String url = UriComponentsBuilder.fromUriString(AROUND_URL) + .queryParam("key", amapProperties.getWebServiceKey()) + .queryParam("location", location) + .queryParam("radius", radius) + .queryParam("offset", offset) + .queryParam("page", page) + .queryParam("output", "json") + .build() + .encode() + .toUriString(); + String body = restTemplate.getForObject(url, String.class); + if (body == null) { + throw new IllegalStateException("高德周边搜索返回空响应"); + } + return body; + } + + private static String text(JsonNode node, String field) { + if (node == null || !node.has(field)) { + return ""; + } + return node.get(field).asText(""); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1024bfd..c6ccd5d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -271,6 +271,10 @@ dify: top-sales-alldim-token: app-99nVuuKfcRR2vikxgOTiNQm0 classification-token: app-Id1eOuv5yBb1pexpEWVDqSS3 +# 高德地图 Web 服务(地理编码、周边 POI);本地可设环境变量 AMAP_WEB_SERVICE_KEY +amap: + web-service-key: ${AMAP_WEB_SERVICE_KEY:} + # MinIO 对象存储配置 minio: # MinIO服务器地址 diff --git a/src/test/java/com/rj/gaode/AllShopCollectorV5.java b/src/test/java/com/rj/gaode/AllShopCollectorV5.java new file mode 100644 index 0000000..d2cf42d --- /dev/null +++ b/src/test/java/com/rj/gaode/AllShopCollectorV5.java @@ -0,0 +1,114 @@ +package com.rj.gaode; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; + +public class AllShopCollectorV5 { + + // ==================== 只需要改这里 5 项 ==================== + private static final String AMAP_KEY = "34eaa47387aa9e8c1a8ca0d4c8abaa1d"; // 替换成你的Key + private static final String CITY = "河北省廊坊市三河市燕郊镇"; // 城市 + private static final String ADDRESS = "燕顺路星河皓月小区"; // 地标/街道/路(自动转经纬度) + private static final int RADIUS = 800; // 半径800米(可改300/500/1000) + private static final int MAX_PAGE = 2; // 采集页数 + // ========================================================== + + public static void main(String[] args) { + try { + System.out.println("正在获取地址:" + ADDRESS + " 的经纬度..."); + String location = getLocationByAddress(CITY, ADDRESS); + System.out.println("经纬度获取成功:" + location); + + // 开始全行业采集 + for (int page = 1; page <= MAX_PAGE; page++) { + System.out.println("\n===== 全量采集第 " + page + " 页商家 ====="); + String json = getAllShops(location, page); + parseAndPrint(json); + Thread.sleep(800); // 风控间隔,稳定不封号 + } + System.out.println("\n===== 全行业商家采集完成 ====="); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 1. 地址自动转经纬度(核心:不用手动打开网页) + */ + public static String getLocationByAddress(String city, String address) throws Exception { + String url = "https://restapi.amap.com/v3/geocode/geo" + + "?key=" + AMAP_KEY + + "&city=" + URLEncoder.encode(city, "UTF-8") + + "&address=" + URLEncoder.encode(address, "UTF-8") + + "&output=json"; + String json = sendGet(url); + int start = json.indexOf("\"location\":\"") + 12; + int end = json.indexOf("\"", start); + return json.substring(start, end); + } + + /** + * 2. 【不指定行业】搜索周边所有商家(核心) + */ + public static String getAllShops(String location, int page) throws Exception { + String url = "https://restapi.amap.com/v3/place/around" // 周边搜索(无关键词=全行业) + + "?key=" + AMAP_KEY + + "&location=" + location // 中心点经纬度 + + "&radius=" + RADIUS // 半径范围 + + "&offset=50" // 每页50条 + + "&page=" + page // 页码 + + "&output=json"; + return sendGet(url); + } + + /** + * HTTP请求工具 + */ + public static String sendGet(String url) throws Exception { + HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); + conn.setRequestMethod("GET"); + BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); + String inputLine; + StringBuilder content = new StringBuilder(); + while ((inputLine = in.readLine()) != null) { + content.append(inputLine); + } + in.close(); + conn.disconnect(); + return content.toString(); + } + + /** + * 解析所有商家信息 + */ + public static void parseAndPrint(String json) { + String[] shops = json.split("\"name\":\""); + for (int i = 1; i < shops.length; i++) { + String s = shops[i]; + String name = get(s, ""); + String addr = get(s, "address\":\""); + String tel = get(s, "tel\":\""); + String type = get(s, "type\":\""); + String loc = get(s, "location\":\""); + + System.out.println("店名:" + name); + System.out.println("地址:" + addr); + System.out.println("电话:" + tel); + System.out.println("行业:" + type); + System.out.println("坐标:" + loc); + System.out.println("---------------------------"); + } + } + + private static String get(String s, String pre) { + try { + int i = s.indexOf(pre) + pre.length(); + return s.substring(i, s.indexOf("\"", i)); + } catch (Exception e) { + return "无"; + } + } +} \ No newline at end of file diff --git a/src/test/java/com/rj/gaode/AutoLocationCollector.java b/src/test/java/com/rj/gaode/AutoLocationCollector.java new file mode 100644 index 0000000..d6e82b8 --- /dev/null +++ b/src/test/java/com/rj/gaode/AutoLocationCollector.java @@ -0,0 +1,133 @@ +package com.rj.gaode; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; + +public class AutoLocationCollector { + + // ==================== 配置区(只改这里)==================== + private static final String AMAP_KEY = "你的Web服务Key"; + private static final String CITY = "杭州市"; // 城市 + private static final String ADDRESS = "万达广场"; // 地标/街道/路/建筑(自动转经纬度) + private static final String KEYWORDS = "餐饮"; // 招商行业 + private static final String TYPES = "050000"; // 行业编码 + private static final int RADIUS = 800; // 半径800米 + private static final int MAX_PAGE = 3; // 采集页数 + // ========================================================== + + public static void main(String[] args) { + try { + System.out.println("正在获取地址:" + ADDRESS + " 的经纬度..."); + // 1. 自动获取经纬度 + String location = getLocationByAddress(CITY, ADDRESS); + System.out.println("获取成功:经纬度 = " + location); + + // 2. 自动采集周边商家 + for (int page = 1; page <= MAX_PAGE; page++) { + System.out.println("\n===== 正在采集第 " + page + " 页商家 ====="); + String json = getShopList(location, page); + parseAndPrint(json); + Thread.sleep(800); + } + System.out.println("\n===== 全自动招商采集完成 ====="); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 高德地理编码API:地址 → 自动获取经纬度 + */ + public static String getLocationByAddress(String city, String address) throws Exception { + String url = "https://restapi.amap.com/v3/geocode/geo" + + "?key=" + AMAP_KEY + + "&city=" + URLEncoder.encode(city, "UTF-8") + + "&address=" + URLEncoder.encode(address, "UTF-8") + + "&output=json"; + + String json = sendHttpRequest(url); + return extractLocation(json); + } + + /** + * 调用POI搜索,获取周边商家 + */ + public static String getShopList(String location, int page) throws Exception { + String url = "https://restapi.amap.com/v3/place/text" + + "?key=" + AMAP_KEY + + "&city=" + URLEncoder.encode(CITY, "UTF-8") + + "&keywords=" + URLEncoder.encode(KEYWORDS, "UTF-8") + + "&types=" + TYPES + + "&location=" + location + + "&radius=" + RADIUS + + "&citylimit=true" + + "&offset=50" + + "&page=" + page + + "&output=json"; + return sendHttpRequest(url); + } + + /** + * 统一HTTP请求工具 + */ + public static String sendHttpRequest(String urlStr) throws Exception { + URL url = new URL(urlStr); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream())); + String line; + StringBuilder result = new StringBuilder(); + while ((line = br.readLine()) != null) { + result.append(line); + } + br.close(); + conn.disconnect(); + return result.toString(); + } + + /** + * 从地理编码结果中提取经纬度 + */ + public static String extractLocation(String json) { + int start = json.indexOf("\"location\":\"") + 12; + int end = json.indexOf("\"", start); + return json.substring(start, end); + } + + /** + * 解析并打印商家信息 + */ + public static void parseAndPrint(String json) { + String[] shops = json.split("\"name\":\""); + for (int i = 1; i < shops.length; i++) { + String item = shops[i]; + String name = getItem(item, ""); + String address = getItem(item, "address\":\""); + String tel = getItem(item, "tel\":\""); + String type = getItem(item, "type\":\""); + String location = getItem(item, "location\":\""); + + System.out.println("店名:" + name); + System.out.println("地址:" + address); + System.out.println("电话:" + tel); + System.out.println("类型:" + type); + System.out.println("坐标:" + location); + System.out.println("---------------------------"); + } + } + + public static String getItem(String s, String pre) { + try { + int i = s.indexOf(pre) + pre.length(); + return s.substring(i, s.indexOf("\"", i)); + } catch (Exception e) { + return "无"; + } + } +} \ No newline at end of file