补充客户信息

This commit is contained in:
2026-05-03 09:36:00 +08:00
parent 7b2691b98f
commit 29b97839ba
10 changed files with 538 additions and 1 deletions

View File

@@ -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) {

View File

@@ -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 = "";
}

View File

@@ -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<Map<String, Object>> 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 = "每页条数150高德 offset", required = true) @RequestParam("maxRecordsOfpage") int maxRecordsOfpage) {
NearbyMerchantSearchResult search = nearbyMerchantSearchService.searchNearbyMerchants(
city, address, radius, maxPage, maxRecordsOfpage);
Map<String, Object> 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);
}
/**
* 分页查询客户列表
*/

View File

@@ -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;
}

View File

@@ -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<NearbyMerchantItem> merchants;
/** 高德返回的本次检索总条数(字符串),可能大于已拉取页数之和 */
private final String totalCount;
private NearbyMerchantSearchResult(int httpStatus, boolean success, String message,
String centerLocation, List<NearbyMerchantItem> 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<NearbyMerchantItem> 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);
}
}

View File

@@ -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 每页条数,高德允许 150超出会被裁剪
*/
NearbyMerchantSearchResult searchNearbyMerchants(String city, String address,
int radiusMeters, int maxPage, int maxRecordsPerPage);
}

View File

@@ -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<NearbyMerchantItem> 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("");
}
}