|
|
@@ -0,0 +1,326 @@
|
|
|
+package com.malk.service.customer.impl;
|
|
|
+
|
|
|
+import com.malk.server.common.McException;
|
|
|
+import com.malk.server.customer.CustomerConf;
|
|
|
+import com.malk.server.customer.CustomerEnrichMeta;
|
|
|
+import com.malk.server.customer.CustomerEnrichRequest;
|
|
|
+import com.malk.server.customer.CustomerEnrichResult;
|
|
|
+import com.malk.server.customer.CustomerSearchItem;
|
|
|
+import com.malk.server.customer.CustomerSearchResult;
|
|
|
+import com.malk.server.tianyancha.TycCompany;
|
|
|
+import com.malk.server.tianyancha.TycSearchItem;
|
|
|
+import com.malk.server.tianyancha.TycSearchResponse;
|
|
|
+import com.malk.service.customer.CustomerEnrichService;
|
|
|
+import com.malk.service.tianyancha.TycClient;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Locale;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.regex.Matcher;
|
|
|
+import java.util.regex.Pattern;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 客户档案工商信息查询与字段映射服务实现。
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+public class CustomerEnrichServiceImpl implements CustomerEnrichService {
|
|
|
+
|
|
|
+ private static final String F_CREDIT_CODE = "creditCode";
|
|
|
+ private static final String F_LEGAL_PERSON = "legalPerson";
|
|
|
+ private static final String F_COMPANY_TYPE = "companyType";
|
|
|
+ private static final String F_INDUSTRY = "industry";
|
|
|
+ private static final String F_REG_CAPITAL = "regCapital";
|
|
|
+ private static final String F_REG_STATUS = "regStatus";
|
|
|
+ private static final String F_ESTABLISH_TIME = "establishTime";
|
|
|
+ private static final String F_TERM = "term";
|
|
|
+ private static final String F_APPROVED_TIME = "approvedTime";
|
|
|
+ private static final String F_REG_INSTITUTE = "regInstitute";
|
|
|
+ private static final String F_REG_LOCATION = "regLocation";
|
|
|
+ private static final String F_PHONE = "phone";
|
|
|
+ private static final String F_WEBSITE = "website";
|
|
|
+ private static final String F_TYC_URL = "tycUrl";
|
|
|
+ private static final String F_STAFF_SCALE = "staffScale";
|
|
|
+ private static final String F_BUSINESS_SCOPE = "businessScope";
|
|
|
+ private static final List<String> UNAVAILABLE_BIZ_KEYS = java.util.Arrays.asList(
|
|
|
+ "area", "businessAddress", "fax", "email", "postalCode");
|
|
|
+ private static final Pattern NUMBER_PATTERN = Pattern.compile("(\\d+(?:\\.\\d+)?)");
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private TycClient tycClient;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private CustomerConf conf;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 搜索工商候选企业,不自动选择候选。
|
|
|
+ *
|
|
|
+ * @param customerName 客户名称关键词
|
|
|
+ * @param pageNum 页码
|
|
|
+ * @param pageSize 每页数量
|
|
|
+ * @return 候选列表
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public CustomerSearchResult search(String customerName, Integer pageNum, Integer pageSize) {
|
|
|
+ TycSearchResponse response = tycClient.searchCompanies(customerName, pageNum, pageSize);
|
|
|
+ List<CustomerSearchItem> candidates = new ArrayList<>();
|
|
|
+ if (response.getItems() != null) {
|
|
|
+ for (TycSearchItem item : response.getItems()) {
|
|
|
+ candidates.add(CustomerSearchItem.builder()
|
|
|
+ .companyId(item.getId())
|
|
|
+ .companyName(item.getName())
|
|
|
+ .creditCode(item.getCreditCode())
|
|
|
+ .legalPersonName(item.getLegalPersonName())
|
|
|
+ .regStatus(item.getRegStatus())
|
|
|
+ .build());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return CustomerSearchResult.builder()
|
|
|
+ .pageNum(pageNum)
|
|
|
+ .pageSize(pageSize)
|
|
|
+ .total(response.getTotal())
|
|
|
+ .candidates(candidates)
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按企业 ID 查询工商详情并生成回填字段。
|
|
|
+ *
|
|
|
+ * @param companyId 天眼查企业 ID
|
|
|
+ * @param existingValues 宜搭现有字段值
|
|
|
+ * @return 工商详情及回填计划
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public CustomerEnrichResult detail(Long companyId, Map<String, Object> existingValues) {
|
|
|
+ long startedAt = System.currentTimeMillis();
|
|
|
+ return buildResult(tycClient.getCompanyById(companyId), existingValues, "tianyancha:1001:id", startedAt);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 兼容旧入口,按 ID、信用代码或严格一致企业名称查询。
|
|
|
+ *
|
|
|
+ * @param request 回填请求
|
|
|
+ * @return 工商详情及回填计划
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public CustomerEnrichResult enrich(CustomerEnrichRequest request) {
|
|
|
+ if (request == null) {
|
|
|
+ throw new McException("CUSTOMER_QUERY_REQUIRED", "请求体不能为空");
|
|
|
+ }
|
|
|
+ if (request.getCompanyId() != null) {
|
|
|
+ return detail(request.getCompanyId(), request.getExisting());
|
|
|
+ }
|
|
|
+ long startedAt = System.currentTimeMillis();
|
|
|
+ if (StringUtils.isNotBlank(request.getCreditCode())) {
|
|
|
+ TycCompany company = tycClient.getCompanyByName(request.getCreditCode().trim());
|
|
|
+ return buildResult(company, request.getExisting(), "tianyancha:1001:creditCode", startedAt);
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(request.getCustomerName())) {
|
|
|
+ TycCompany company = tycClient.getCompanyByName(request.getCustomerName().trim());
|
|
|
+ if (company != null && !namesExactlyMatch(request.getCustomerName(), company.getName())) {
|
|
|
+ throw new McException("CUSTOMER_COMPANY_SELECTION_REQUIRED", "工商名称不完全一致,请先搜索并选择企业");
|
|
|
+ }
|
|
|
+ return buildResult(company, request.getExisting(), "tianyancha:1001:name", startedAt);
|
|
|
+ }
|
|
|
+ throw new McException("CUSTOMER_QUERY_REQUIRED", "companyId、creditCode、customerName 至少填写一个");
|
|
|
+ }
|
|
|
+
|
|
|
+ CustomerEnrichResult buildResult(TycCompany company, Map<String, Object> existingValues,
|
|
|
+ String source, long startedAt) {
|
|
|
+ Map<String, Object> filled = new LinkedHashMap<>();
|
|
|
+ List<String> skipped = new ArrayList<>();
|
|
|
+ List<String> unavailable = unavailableFieldIds();
|
|
|
+ if (company == null) {
|
|
|
+ return CustomerEnrichResult.builder()
|
|
|
+ .hit(false)
|
|
|
+ .filled(filled)
|
|
|
+ .skipped(skipped)
|
|
|
+ .unavailable(unavailable)
|
|
|
+ .meta(metaOf(source, startedAt))
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+ Map<String, Object> existing = existingValues == null ? Collections.emptyMap() : existingValues;
|
|
|
+ putIfEmpty(filled, skipped, existing, F_CREDIT_CODE, company.getCreditCode());
|
|
|
+ putIfEmpty(filled, skipped, existing, F_LEGAL_PERSON, company.getLegalPersonName());
|
|
|
+ putIfEmpty(filled, skipped, existing, F_COMPANY_TYPE, company.getCompanyOrgType());
|
|
|
+ putIfEmpty(filled, skipped, existing, F_INDUSTRY, company.getIndustry());
|
|
|
+ putIfEmpty(filled, skipped, existing, F_REG_CAPITAL, company.getRegCapital());
|
|
|
+ putIfEmpty(filled, skipped, existing, F_REG_STATUS, company.getRegStatus());
|
|
|
+ putIfEmpty(filled, skipped, existing, F_ESTABLISH_TIME, company.getEstiblishTime());
|
|
|
+ putIfEmpty(filled, skipped, existing, F_TERM, formatTerm(company.getFromTime(), company.getToTime()));
|
|
|
+ putIfEmpty(filled, skipped, existing, F_APPROVED_TIME, company.getApprovedTime());
|
|
|
+ putIfEmpty(filled, skipped, existing, F_REG_INSTITUTE, company.getRegInstitute());
|
|
|
+ putIfEmpty(filled, skipped, existing, F_REG_LOCATION, company.getRegLocation());
|
|
|
+ putIfEmpty(filled, skipped, existing, F_PHONE, company.getPhoneNumber());
|
|
|
+ putIfEmpty(filled, skipped, existing, F_WEBSITE, firstNonBlank(company.getWebsiteList()));
|
|
|
+ putIfEmpty(filled, skipped, existing, F_TYC_URL, buildTycUrl(company.getId()));
|
|
|
+ putIfEmpty(filled, skipped, existing, F_STAFF_SCALE, mapStaffScale(company.getStaffNumRange()));
|
|
|
+ putIfEmpty(filled, skipped, existing, F_BUSINESS_SCOPE, company.getBusinessScope());
|
|
|
+ return CustomerEnrichResult.builder()
|
|
|
+ .hit(true)
|
|
|
+ .companyId(company.getId())
|
|
|
+ .companyName(company.getName())
|
|
|
+ .creditCode(company.getCreditCode())
|
|
|
+ .filled(filled)
|
|
|
+ .skipped(skipped)
|
|
|
+ .unavailable(unavailable)
|
|
|
+ .meta(metaOf(source, startedAt))
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+
|
|
|
+ boolean namesExactlyMatch(String requestedName, String actualName) {
|
|
|
+ String requested = normalizeCompanyName(requestedName);
|
|
|
+ String actual = normalizeCompanyName(actualName);
|
|
|
+ return StringUtils.isNotBlank(requested) && requested.equals(actual);
|
|
|
+ }
|
|
|
+
|
|
|
+ String normalizeCompanyName(String name) {
|
|
|
+ if (name == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return name.trim()
|
|
|
+ .replace('(', '(')
|
|
|
+ .replace(')', ')')
|
|
|
+ .replaceAll("\\s+", "")
|
|
|
+ .toLowerCase(Locale.ROOT);
|
|
|
+ }
|
|
|
+
|
|
|
+ String mapStaffScale(String staffNumRange) {
|
|
|
+ if (StringUtils.isBlank(staffNumRange)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ Matcher matcher = NUMBER_PATTERN.matcher(staffNumRange.replace(",", ""));
|
|
|
+ List<Integer> numbers = new ArrayList<>();
|
|
|
+ while (matcher.find()) {
|
|
|
+ try {
|
|
|
+ numbers.add((int) Math.floor(Double.parseDouble(matcher.group(1))));
|
|
|
+ } catch (NumberFormatException exception) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (numbers.isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ int representative;
|
|
|
+ if (numbers.size() >= 2) {
|
|
|
+ representative = numbers.get(1);
|
|
|
+ } else if (containsUpperBoundWord(staffNumRange)) {
|
|
|
+ representative = numbers.get(0);
|
|
|
+ } else if (containsLowerBoundWord(staffNumRange)) {
|
|
|
+ representative = numbers.get(0) + 1;
|
|
|
+ } else {
|
|
|
+ representative = numbers.get(0);
|
|
|
+ }
|
|
|
+ if (representative < 100) {
|
|
|
+ return "<100";
|
|
|
+ }
|
|
|
+ if (representative <= 500) {
|
|
|
+ return "100-500";
|
|
|
+ }
|
|
|
+ if (representative <= 1000) {
|
|
|
+ return "500>1000";
|
|
|
+ }
|
|
|
+ if (representative <= 5000) {
|
|
|
+ return "1000-5000";
|
|
|
+ }
|
|
|
+ if (representative <= 10000) {
|
|
|
+ return "5000-10000";
|
|
|
+ }
|
|
|
+ return ">10000";
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean containsUpperBoundWord(String value) {
|
|
|
+ return value.contains("以下") || value.contains("以内") || value.contains("少于")
|
|
|
+ || value.contains("小于") || value.contains("<") || value.contains("≤");
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean containsLowerBoundWord(String value) {
|
|
|
+ return value.contains("以上") || value.contains("多于") || value.contains("大于")
|
|
|
+ || value.contains(">") || value.contains("≥");
|
|
|
+ }
|
|
|
+
|
|
|
+ private void putIfEmpty(Map<String, Object> filled, List<String> skipped,
|
|
|
+ Map<String, Object> existing, String bizKey, Object newValue) {
|
|
|
+ String fieldId = fieldIdOf(bizKey);
|
|
|
+ if (StringUtils.isBlank(fieldId) || isBlank(newValue)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!isBlank(existing.get(fieldId))) {
|
|
|
+ skipped.add(fieldId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ filled.put(fieldId, newValue);
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<String> unavailableFieldIds() {
|
|
|
+ List<String> unavailable = new ArrayList<>();
|
|
|
+ for (String bizKey : UNAVAILABLE_BIZ_KEYS) {
|
|
|
+ String fieldId = fieldIdOf(bizKey);
|
|
|
+ unavailable.add(StringUtils.isBlank(fieldId) ? bizKey : fieldId);
|
|
|
+ }
|
|
|
+ return unavailable;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String fieldIdOf(String bizKey) {
|
|
|
+ return conf.getFields() == null ? null : conf.getFields().get(bizKey);
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isBlank(Object value) {
|
|
|
+ if (value == null) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (value instanceof String) {
|
|
|
+ return StringUtils.isBlank((String) value);
|
|
|
+ }
|
|
|
+ if (value instanceof Number) {
|
|
|
+ return ((Number) value).doubleValue() == 0D;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String formatTerm(Long from, Long to) {
|
|
|
+ if (from == null || from == 0L) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
|
|
|
+ String end = to == null || to == 0L ? "长期" : format.format(new Date(to));
|
|
|
+ return format.format(new Date(from)) + " 至 " + end;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String firstNonBlank(String values) {
|
|
|
+ if (StringUtils.isBlank(values)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ for (String value : values.split(",")) {
|
|
|
+ if (StringUtils.isNotBlank(value)) {
|
|
|
+ return value.trim();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildTycUrl(Long companyId) {
|
|
|
+ if (companyId == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ String template = StringUtils.defaultIfBlank(
|
|
|
+ conf.getTycCompanyUrl(), "https://www.tianyancha.com/company/{id}");
|
|
|
+ return template.replace("{id}", companyId.toString());
|
|
|
+ }
|
|
|
+
|
|
|
+ private CustomerEnrichMeta metaOf(String source, long startedAt) {
|
|
|
+ return CustomerEnrichMeta.builder()
|
|
|
+ .source(source)
|
|
|
+ .latencyMs(System.currentTimeMillis() - startedAt)
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+}
|