把客户信息补充到数据库
This commit is contained in:
@@ -227,6 +227,8 @@ public class CustomerManagementController {
|
||||
result.put("centerLocation", search.getCenterLocation());
|
||||
result.put("totalCount", search.getTotalCount());
|
||||
result.put("data", search.getMerchants());
|
||||
result.put("detailedReport", search.getDetailedReport());
|
||||
result.put("customerInsertedCount", search.getCustomerInsertedCount());
|
||||
}
|
||||
return ResponseEntity.status(search.getHttpStatus()).body(result);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 高德周边 POI 中的一条商家记录(与 {@code AllShopCollectorV5} 解析字段一致)。
|
||||
* 高德周边 POI 中的一条商家记录(与 {@code AllShopCollectorV5} 核心字段一致,并补充 distance 等 POI 字段)。
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@@ -18,4 +20,53 @@ public class NearbyMerchantItem {
|
||||
private String type;
|
||||
/** 经纬度,格式 "lng,lat" */
|
||||
private String location;
|
||||
/** 高德 POI id */
|
||||
private String id;
|
||||
/** 距检索中心距离(米),place/around 返回的 distance */
|
||||
private String distance;
|
||||
private String typecode;
|
||||
/** 高德 business_area */
|
||||
private String businessArea;
|
||||
/** 高德 POI 照片链接(extensions=all 时 photos[].url) */
|
||||
private List<String> photoUrls;
|
||||
|
||||
/**
|
||||
* 与 {@link com.rj.gaode.AllShopCollectorV5#parseAndPrint} 单条输出格式一致(店名/地址/电话/行业/坐标),并附加扩展行。
|
||||
*/
|
||||
public String toDetailBlock() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("店名:").append(dashIfEmpty(name));
|
||||
sb.append("\n地址:").append(dashIfEmpty(address));
|
||||
sb.append("\n电话:").append(dashIfEmpty(tel));
|
||||
sb.append("\n行业:").append(dashIfEmpty(type));
|
||||
sb.append("\n坐标:").append(dashIfEmpty(location));
|
||||
if (isPresent(distance)) {
|
||||
sb.append("\n距离(米):").append(distance);
|
||||
}
|
||||
if (isPresent(id)) {
|
||||
sb.append("\nPOI ID:").append(id);
|
||||
}
|
||||
if (isPresent(typecode)) {
|
||||
sb.append("\n类型编码:").append(typecode);
|
||||
}
|
||||
if (isPresent(businessArea)) {
|
||||
sb.append("\n商圈:").append(businessArea);
|
||||
}
|
||||
if (photoUrls != null && !photoUrls.isEmpty()) {
|
||||
sb.append("\n照片URL:");
|
||||
for (int i = 0; i < photoUrls.size(); i++) {
|
||||
sb.append("\n ").append(i + 1).append(". ").append(photoUrls.get(i));
|
||||
}
|
||||
}
|
||||
sb.append("\n---------------------------");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static boolean isPresent(String s) {
|
||||
return s != null && !s.isEmpty();
|
||||
}
|
||||
|
||||
private static String dashIfEmpty(String s) {
|
||||
return isPresent(s) ? s : "无";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,26 +18,37 @@ public class NearbyMerchantSearchResult {
|
||||
private final List<NearbyMerchantItem> merchants;
|
||||
/** 高德返回的本次检索总条数(字符串),可能大于已拉取页数之和 */
|
||||
private final String totalCount;
|
||||
/**
|
||||
* 与 {@link com.rj.gaode.AllShopCollectorV5#parseAndPrint} 类似的商铺明细文本(含检索参数与每家店块)。
|
||||
*/
|
||||
private final String detailedReport;
|
||||
/** 本次成功写入 {@code customer_management} 的条数(按 contact 匹配,含新增与更新) */
|
||||
private final Integer customerInsertedCount;
|
||||
|
||||
private NearbyMerchantSearchResult(int httpStatus, boolean success, String message,
|
||||
String centerLocation, List<NearbyMerchantItem> merchants, String totalCount) {
|
||||
String centerLocation, List<NearbyMerchantItem> merchants, String totalCount,
|
||||
String detailedReport, Integer customerInsertedCount) {
|
||||
this.httpStatus = httpStatus;
|
||||
this.success = success;
|
||||
this.message = message;
|
||||
this.centerLocation = centerLocation;
|
||||
this.merchants = merchants == null ? Collections.emptyList() : merchants;
|
||||
this.totalCount = totalCount;
|
||||
this.detailedReport = detailedReport;
|
||||
this.customerInsertedCount = customerInsertedCount;
|
||||
}
|
||||
|
||||
public static NearbyMerchantSearchResult ok(String centerLocation, List<NearbyMerchantItem> merchants, String totalCount) {
|
||||
return new NearbyMerchantSearchResult(200, true, "查询成功", centerLocation, merchants, totalCount);
|
||||
public static NearbyMerchantSearchResult ok(String centerLocation, List<NearbyMerchantItem> merchants,
|
||||
String totalCount, String detailedReport, int customerInsertedCount) {
|
||||
return new NearbyMerchantSearchResult(200, true, "查询成功", centerLocation, merchants, totalCount,
|
||||
detailedReport, customerInsertedCount);
|
||||
}
|
||||
|
||||
public static NearbyMerchantSearchResult badRequest(String message) {
|
||||
return new NearbyMerchantSearchResult(400, false, message, null, Collections.emptyList(), null);
|
||||
return new NearbyMerchantSearchResult(400, false, message, null, Collections.emptyList(), null, null, null);
|
||||
}
|
||||
|
||||
public static NearbyMerchantSearchResult serverError(String message) {
|
||||
return new NearbyMerchantSearchResult(500, false, message, null, Collections.emptyList(), null);
|
||||
return new NearbyMerchantSearchResult(500, false, message, null, Collections.emptyList(), null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.rj.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
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.entity.CustomerManagement;
|
||||
import com.rj.service.ICustomerManagementService;
|
||||
import com.rj.service.INearbyMerchantSearchService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -14,11 +17,13 @@ import org.springframework.web.client.RestTemplate;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 逻辑参考 {@link com.rj.gaode.AllShopCollectorV5}:地理编码 + place/around 全行业周边搜索。
|
||||
* 逻辑参考 {@link com.rj.gaode AllShopCollectorV5}:地理编码 + place/around 全行业周边搜索。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -28,17 +33,30 @@ public class NearbyMerchantSearchServiceImpl implements INearbyMerchantSearchSer
|
||||
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;
|
||||
/** 与 customer_management 正面/侧面/生活照三字段一致 */
|
||||
private static final int MAX_PHOTO_URLS = 3;
|
||||
|
||||
/** 高德采集写入客户时的默认门店/销售/意向(可按业务调整) */
|
||||
private static final String DEFAULT_DEALERSHIP_ID = "1957424710587330562";
|
||||
private static final String DEFAULT_DEALERSHIP_NAME = "北京移多旺科技有限公司";
|
||||
private static final String DEFAULT_SALES_ID = "2";
|
||||
private static final String DEFAULT_SALES_NAME = "15810181776";
|
||||
private static final String DEFAULT_SALES_PHONE = "15810181776";
|
||||
private static final String DEFAULT_INTENDED_MODEL = "线索";
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final AmapProperties amapProperties;
|
||||
private final ICustomerManagementService customerManagementService;
|
||||
|
||||
public NearbyMerchantSearchServiceImpl(RestTemplate restTemplate,
|
||||
ObjectMapper objectMapper,
|
||||
AmapProperties amapProperties) {
|
||||
AmapProperties amapProperties,
|
||||
ICustomerManagementService customerManagementService) {
|
||||
this.restTemplate = restTemplate;
|
||||
this.objectMapper = objectMapper;
|
||||
this.amapProperties = amapProperties;
|
||||
this.customerManagementService = customerManagementService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -76,12 +94,21 @@ public class NearbyMerchantSearchServiceImpl implements INearbyMerchantSearchSer
|
||||
JsonNode pois = root.path("pois");
|
||||
if (pois.isArray()) {
|
||||
for (JsonNode p : pois) {
|
||||
String tel = text(p, "tel").trim();
|
||||
if (!StringUtils.hasText(tel)) {
|
||||
continue;
|
||||
}
|
||||
all.add(new NearbyMerchantItem(
|
||||
text(p, "name"),
|
||||
text(p, "address"),
|
||||
text(p, "tel"),
|
||||
tel,
|
||||
text(p, "type"),
|
||||
text(p, "location")
|
||||
text(p, "location"),
|
||||
text(p, "id"),
|
||||
text(p, "distance"),
|
||||
text(p, "typecode"),
|
||||
text(p, "business_area"),
|
||||
extractPhotoUrls(p)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -94,7 +121,15 @@ public class NearbyMerchantSearchServiceImpl implements INearbyMerchantSearchSer
|
||||
}
|
||||
}
|
||||
}
|
||||
return NearbyMerchantSearchResult.ok(location, all, totalCount);
|
||||
String cTrim = city.trim();
|
||||
String aTrim = address.trim();
|
||||
String report = buildDetailedReport(cTrim, aTrim, radiusMeters, location, totalCount, all);
|
||||
log.info("附近商家检索明细(与 AllShopCollectorV5 格式对齐)\n{}", report);
|
||||
int customerWriteCount = persistMerchantsAsCustomers(all, cTrim, aTrim);
|
||||
if (customerWriteCount > 0) {
|
||||
log.info("高德采集结果已同步 customer_management,成功 {} 条(按 contact 新增或更新)", customerWriteCount);
|
||||
}
|
||||
return NearbyMerchantSearchResult.ok(location, all, totalCount, report, customerWriteCount);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return NearbyMerchantSearchResult.badRequest(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
@@ -104,7 +139,7 @@ public class NearbyMerchantSearchServiceImpl implements INearbyMerchantSearchSer
|
||||
}
|
||||
|
||||
/**
|
||||
* 地理编码。URL 拼法与 {@link com.rj.gaode#} 一致:
|
||||
* 地理编码。URL 拼法与 {@link com.rj.gaode AllShopCollectorV5} 一致:
|
||||
* 使用 {@link URLEncoder#encode(String, java.nio.charset.Charset)} 处理中文查询参数。
|
||||
* Spring {@code UriComponentsBuilder} 的编码规则与 {@code URLEncoder} 不一致,易导致高德返回 ENGINE_RESPONSE_DATA_ERROR(如 infocode=30001)。
|
||||
* 首次失败后不传 {@code city},仅用合并后的完整地址再请求一次。
|
||||
@@ -185,6 +220,7 @@ public class NearbyMerchantSearchServiceImpl implements INearbyMerchantSearchSer
|
||||
+ "&radius=" + radius
|
||||
+ "&offset=" + offset
|
||||
+ "&page=" + page
|
||||
+ "&extensions=all"
|
||||
+ "&output=json";
|
||||
String body = restTemplate.getForObject(URI.create(url), String.class);
|
||||
if (body == null) {
|
||||
@@ -199,4 +235,158 @@ public class NearbyMerchantSearchServiceImpl implements INearbyMerchantSearchSer
|
||||
}
|
||||
return node.get(field).asText("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 高德 POI {@code photos} 为数组:元素可为对象 {@code { "url": "..." }} 或个别环境下为 URL 字符串。
|
||||
*/
|
||||
private static List<String> extractPhotoUrls(JsonNode poi) {
|
||||
List<String> urls = new ArrayList<>();
|
||||
JsonNode photos = poi.path("photos");
|
||||
if (!photos.isArray()) {
|
||||
return urls;
|
||||
}
|
||||
for (JsonNode ph : photos) {
|
||||
if (ph == null || ph.isNull()) {
|
||||
continue;
|
||||
}
|
||||
if (ph.isTextual()) {
|
||||
String u = ph.asText().trim();
|
||||
if (StringUtils.hasText(u)) {
|
||||
urls.add(u);
|
||||
if (urls.size() >= MAX_PHOTO_URLS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
String u = ph.path("url").asText("").trim();
|
||||
if (StringUtils.hasText(u)) {
|
||||
urls.add(u);
|
||||
}
|
||||
if (urls.size() >= MAX_PHOTO_URLS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 {@code contact} 与 {@code customer_management} 对齐:已存在则更新,不存在则插入。
|
||||
* 店名/地址/电话/三张照片/门店与销售默认值等见 {@link #applyMerchantDataToCustomer};更新时保留主键、创建时间及录音/联系次数。
|
||||
*/
|
||||
private int persistMerchantsAsCustomers(List<NearbyMerchantItem> items, String searchCity, String searchAddress) {
|
||||
int affected = 0;
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (NearbyMerchantItem item : items) {
|
||||
String contact = item.getTel();
|
||||
if (!StringUtils.hasText(contact)) {
|
||||
continue;
|
||||
}
|
||||
String contactTrim = contact.trim();
|
||||
try {
|
||||
LambdaQueryWrapper<CustomerManagement> w = new LambdaQueryWrapper<CustomerManagement>()
|
||||
.eq(CustomerManagement::getContact, contactTrim)
|
||||
.last("LIMIT 1");
|
||||
CustomerManagement existing = customerManagementService.getOne(w, false);
|
||||
if (existing == null) {
|
||||
CustomerManagement row = new CustomerManagement();
|
||||
row.setId(UUID.randomUUID().toString());
|
||||
applyMerchantDataToCustomer(row, item, contactTrim, searchCity, searchAddress);
|
||||
row.setRecordingCount(0);
|
||||
row.setContactCount(0);
|
||||
row.setCreateTime(now);
|
||||
row.setUpdateTime(now);
|
||||
if (customerManagementService.save(row)) {
|
||||
affected++;
|
||||
}
|
||||
} else {
|
||||
applyMerchantDataToCustomer(existing, item, contactTrim, searchCity, searchAddress);
|
||||
existing.setUpdateTime(now);
|
||||
if (customerManagementService.updateById(existing)) {
|
||||
affected++;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("写入 customer_management 失败, contact={}: {}", contactTrim, e.getMessage());
|
||||
}
|
||||
}
|
||||
return affected;
|
||||
}
|
||||
|
||||
private static void applyMerchantDataToCustomer(CustomerManagement c, NearbyMerchantItem item, String contactTrim,
|
||||
String searchCity, String searchAddress) {
|
||||
String name = item.getName() != null ? item.getName().trim() : "";
|
||||
c.setCustomerName(name.isEmpty() ? "高德POI" : name);
|
||||
c.setDetailedAddress(blankToNull(item.getAddress()));
|
||||
c.setContact(contactTrim);
|
||||
c.setRemark(remarkFromSearchParams(searchCity, searchAddress));
|
||||
List<String> urls = item.getPhotoUrls();
|
||||
if (urls != null && !urls.isEmpty()) {
|
||||
int n = Math.min(urls.size(), MAX_PHOTO_URLS);
|
||||
c.setFrontPhotoUrl(n > 0 ? urls.get(0) : null);
|
||||
c.setSidePhotoUrl(n > 1 ? urls.get(1) : null);
|
||||
c.setLifePhotoUrl(n > 2 ? urls.get(2) : null);
|
||||
} else {
|
||||
c.setFrontPhotoUrl(null);
|
||||
c.setSidePhotoUrl(null);
|
||||
c.setLifePhotoUrl(null);
|
||||
}
|
||||
c.setCustomerSource("AMAP_GaoDe");
|
||||
c.setDealershipId(DEFAULT_DEALERSHIP_ID);
|
||||
c.setDealershipName(DEFAULT_DEALERSHIP_NAME);
|
||||
c.setSalesId(DEFAULT_SALES_ID);
|
||||
c.setSalesName(DEFAULT_SALES_NAME);
|
||||
c.setSalesPhone(DEFAULT_SALES_PHONE);
|
||||
c.setIntendedModel(DEFAULT_INTENDED_MODEL);
|
||||
}
|
||||
|
||||
/** 接口检索参数 CITY、ADDRESS 两段拼接写入 {@code remark} */
|
||||
private static String remarkFromSearchParams(String searchCity, String searchAddress) {
|
||||
String c = searchCity != null ? searchCity.trim() : "";
|
||||
String a = searchAddress != null ? searchAddress.trim() : "";
|
||||
if (!StringUtils.hasText(c) && !StringUtils.hasText(a)) {
|
||||
return null;
|
||||
}
|
||||
if (!StringUtils.hasText(c)) {
|
||||
return a;
|
||||
}
|
||||
if (!StringUtils.hasText(a)) {
|
||||
return c;
|
||||
}
|
||||
return c + " " + a;
|
||||
}
|
||||
|
||||
private static String blankToNull(String s) {
|
||||
if (s == null) {
|
||||
return null;
|
||||
}
|
||||
String t = s.trim();
|
||||
return t.isEmpty() ? null : t;
|
||||
}
|
||||
|
||||
/**
|
||||
* 参考 {@link com.rj.gaode AllShopCollectorV5 parseAndPrint}:每家店为「店名/地址/电话/行业/坐标… + ---------------------------」。
|
||||
*/
|
||||
private static String buildDetailedReport(String city, String address, int radiusMeters, String centerLocation,
|
||||
String totalCount, List<NearbyMerchantItem> merchants) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("===== 附近商家检索(输出格式参考 AllShopCollectorV5)=====\n");
|
||||
sb.append("说明:仅保留联系电话(tel)非空的 POI;高德 count 为接口原始命中总数。\n");
|
||||
sb.append("城市(city):").append(city).append('\n');
|
||||
sb.append("地址(address):").append(address).append('\n');
|
||||
sb.append("搜索半径(米):").append(radiusMeters).append('\n');
|
||||
sb.append("中心经纬度(location):").append(centerLocation).append('\n');
|
||||
sb.append("高德返回总条数(count):").append(totalCount != null ? totalCount : "无").append('\n');
|
||||
sb.append("本响应保留条数(有电话):").append(merchants.size()).append('\n');
|
||||
sb.append("===== 各商铺信息 =====\n");
|
||||
int n = 1;
|
||||
for (NearbyMerchantItem m : merchants) {
|
||||
sb.append("\n【第 ").append(n++).append(" 家】\n");
|
||||
sb.append(m.toDetailBlock()).append('\n');
|
||||
}
|
||||
if (merchants.isEmpty()) {
|
||||
sb.append("\n(无 POI 结果)\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user