浏览代码

bugfix:ai二手搜索

刘云鑫 1 月之前
父节点
当前提交
f831b53599

+ 43 - 2
alien-second/src/main/java/shop/alien/second/service/impl/SecondGoodsServiceImpl.java

@@ -32,6 +32,7 @@ import shop.alien.mapper.*;
 import shop.alien.mapper.second.*;
 import shop.alien.second.feign.AlienStoreFeign;
 import shop.alien.second.service.*;
+import shop.alien.second.util.SecondHandSearchAiUtils;
 import shop.alien.util.common.Constants;
 import shop.alien.util.common.VideoUtils;
 import shop.alien.util.common.safe.*;
@@ -156,6 +157,11 @@ public class SecondGoodsServiceImpl extends ServiceImpl<SecondGoodsMapper, Secon
     private final PlatformSecondTradeService platformSecondTradeService;
 
     /**
+     * 二手搜索 AI 接口工具,用于调用 life-manager 智能搜索
+     */
+    private final SecondHandSearchAiUtils secondHandSearchAiUtils;
+
+    /**
      * 风控配置属性,包含各种风控规则的配置参数
      */
     @Autowired
@@ -1433,8 +1439,32 @@ public class SecondGoodsServiceImpl extends ServiceImpl<SecondGoodsMapper, Secon
         secondGoodsVo.setShieldedGoodsIds(shieldedGoodsIds);
         secondGoodsVo.setUserIdList(userIdList);
 
-        // 获取搜索结果分页列表
-        IPage<SecondGoodsVo> searchGoodsList = getSecondGoodsVoIPage(page, secondGoodsVo, shieldedGoodsIds, userIdList);
+        // 优先调用 AI 二手搜索接口;有搜索词且 AI 返回有效结果时直接使用 AI 数据并处理成对应格式,否则走本地分页查询
+        IPage<SecondGoodsVo> searchGoodsList = null;
+        if (!StringUtils.isEmpty(secondGoodsVo.getSearchData())) {
+            String releaseTimeStr = null;
+            if (secondGoodsVo.getReleaseTime() != null) {
+                releaseTimeStr = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").format(secondGoodsVo.getReleaseTime());
+            }
+            SecondHandSearchAiUtils.SecondHandSearchResult aiResult = secondHandSearchAiUtils.search(
+                    secondGoodsVo.getSearchData(),
+                    secondGoodsVo.getCurrentLatitude(),
+                    secondGoodsVo.getCurrentLongitude(),
+                    (int) page.getCurrent(),
+                    (int) page.getSize(),
+                    secondGoodsVo.getOrderType(),
+                    releaseTimeStr,
+                    shieldedGoodsIds,
+                    userIdList);
+            if (aiResult != null && CollectionUtil.isNotEmpty(aiResult.getRecords())) {
+                searchGoodsList = buildPageFromAiResult(page, aiResult);
+            }
+        }
+        if (searchGoodsList == null) {
+//            log.info("使用本地查询");
+//            searchGoodsList = getSecondGoodsVoIPage(page, secondGoodsVo, shieldedGoodsIds, userIdList);
+            return searchGoodsList = new Page<SecondGoodsVo>();
+        }
 
         // 判空
         if (CollectionUtil.isNotEmpty(searchGoodsList.getRecords())) {
@@ -1451,6 +1481,17 @@ public class SecondGoodsServiceImpl extends ServiceImpl<SecondGoodsMapper, Secon
     }
 
     /**
+     * 将 AI 搜索返回的 SecondGoodsVo 列表组装为分页结果(AI 已按 shieldedGoodsIds、userIdList 过滤,此处仅封装分页)
+     */
+    private IPage<SecondGoodsVo> buildPageFromAiResult(IPage<SecondGoodsVo> page,
+                                                       SecondHandSearchAiUtils.SecondHandSearchResult aiResult) {
+        Page<SecondGoodsVo> resultPage = new Page<>(page.getCurrent(), page.getSize());
+        resultPage.setRecords(aiResult.getRecords() != null ? aiResult.getRecords() : Collections.emptyList());
+        resultPage.setTotal(aiResult.getTotal());
+        return resultPage;
+    }
+
+    /**
      * 查询搜索结果
      * @param page 分页参数
      * @param secondGoodsVo 查询参数

+ 208 - 0
alien-second/src/main/java/shop/alien/second/util/SecondHandSearchAiUtils.java

@@ -0,0 +1,208 @@
+package shop.alien.second.util;
+
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+import shop.alien.entity.second.vo.SecondGoodsVo;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 二手商品搜索 AI 接口调用工具
+ * 调用 life-manager 的 second_hand/search 接口进行智能搜索
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+@RefreshScope
+public class SecondHandSearchAiUtils {
+
+    private final RestTemplate restTemplate;
+    private final AiTaskUtils aiTaskUtils;
+
+    @Value("${second-hand-search.base-url:http://124.93.18.180:9000/ai/life-manager/api/v1/second_hand/search}")
+    private String secondHandSearchUrl;
+
+    /**
+     * 调用 AI 二手搜索接口(先获取 token 再请求)
+     *
+     * @param query            搜索关键词,如 "我想要二手手机"
+     * @param latitude         纬度
+     * @param longitude        经度
+     * @param page             页码,从 1 开始
+     * @param pageSize         每页条数
+     * @param distanceOrder    距离排序,如 1
+     * @param releaseTime      发布时间,ISO 格式如 "2025-12-01T00:00:00",可为 null
+     * @param shieldedGoodsIds 屏蔽商品 id 列表
+     * @param userIdList       拉黑用户 id 列表
+     * @return 搜索结果的 SecondGoodsVo 列表与总数;若调用失败返回 null
+     */
+    public SecondHandSearchResult search(String query, Double latitude, Double longitude, int page, int pageSize,
+                                         Integer distanceOrder, String releaseTime,
+                                         List<Integer> shieldedGoodsIds, List<Integer> userIdList) {
+        if (query == null) {
+            query = "";
+        }
+        if (latitude == null) {
+            latitude = 39.914885;
+        }
+        if (longitude == null) {
+            longitude = 116.403874;
+        }
+
+        String token = aiTaskUtils.getAccessToken();
+        if (token == null) {
+            log.warn("获取 AI 服务 token 失败,无法调用二手搜索接口");
+            return null;
+        }
+
+        log.info("调用 AI 二手搜索接口, url={}, query={}, page={}, pageSize={}", secondHandSearchUrl, query, page, pageSize);
+
+        Map<String, Object> body = new HashMap<>();
+        body.put("query", query);
+        body.put("latitude", latitude);
+        body.put("longitude", longitude);
+        body.put("page", page);
+        body.put("page_size", pageSize);
+        if (distanceOrder != null) {
+            body.put("distance_order", distanceOrder);
+        }
+        if (releaseTime != null && !releaseTime.isEmpty()) {
+            body.put("release_time", releaseTime);
+        }
+        body.put("shieldedGoodsIds", shieldedGoodsIds != null ? shieldedGoodsIds : new ArrayList<>());
+        body.put("userIdList", userIdList != null ? userIdList : new ArrayList<>());
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        headers.set("Authorization", "Bearer " + token);
+        HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(body, headers);
+
+        try {
+            ResponseEntity<String> response = restTemplate.postForEntity(secondHandSearchUrl, requestEntity, String.class);
+            if (response.getStatusCode() != HttpStatus.OK || response.getBody() == null) {
+                log.warn("AI 二手搜索接口返回异常, status={}", response.getStatusCode());
+                return null;
+            }
+            return parseSearchResponse(response.getBody());
+        } catch (Exception e) {
+            log.error("调用 AI 二手搜索接口失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 解析 AI 接口返回体,将 list 中每项转为 SecondGoodsVo
+     * 兼容常见格式:data.list[]、data.items[],字段支持 id/title/price 等及下划线命名
+     */
+    private SecondHandSearchResult parseSearchResponse(String responseBody) {
+        try {
+            JSONObject root = JSONObject.parseObject(responseBody);
+            JSONObject data = root.getJSONObject("data");
+            if (data == null) {
+                data = root;
+            }
+            long total = data.getLongValue("total");
+            JSONArray list = data.getJSONArray("list");
+            if (list == null) {
+                list = data.getJSONArray("items");
+            }
+            List<SecondGoodsVo> records = new ArrayList<>();
+            if (list != null && !list.isEmpty()) {
+                for (int i = 0; i < list.size(); i++) {
+                    Object item = list.get(i);
+                    if (item instanceof JSONObject) {
+                        SecondGoodsVo vo = jsonItemToSecondGoodsVo((JSONObject) item);
+                        if (vo != null) {
+                            records.add(vo);
+                        }
+                    }
+                }
+            }
+            return new SecondHandSearchResult(records, total);
+        } catch (Exception e) {
+            log.warn("解析 AI 二手搜索返回失败, body={}", responseBody, e);
+            return null;
+        }
+    }
+
+    /**
+     * 将 AI 返回的单条 JSON 转为 SecondGoodsVo(兼容 id/user_id/title/price 等及下划线命名)
+     */
+    private SecondGoodsVo jsonItemToSecondGoodsVo(JSONObject o) {
+        Integer id = o.getInteger("id");
+        if (id == null) {
+            return null;
+        }
+        SecondGoodsVo vo = new SecondGoodsVo();
+        vo.setId(id);
+        vo.setUserId(o.getInteger("user_id") != null ? o.getInteger("user_id") : o.getInteger("userId"));
+        vo.setTitle(o.getString("title"));
+        vo.setHomeImage(o.getString("home_image") != null ? o.getString("home_image") : o.getString("homeImage"));
+        vo.setDescription(o.getString("description"));
+        String priceStr = o.getString("price");
+        if (priceStr != null) {
+            vo.setPrice(priceStr);
+        }
+        if (o.get("price") instanceof Number) {
+            vo.setAmount(o.getBigDecimal("price"));
+        }
+        vo.setPosition(o.getString("position"));
+        vo.setAddressText(o.getString("address_text") != null ? o.getString("address_text") : o.getString("addressText"));
+        vo.setLikeCount(o.getInteger("like_count") != null ? o.getInteger("like_count") : o.getInteger("likeCount"));
+        vo.setCollectCount(o.getInteger("collect_count") != null ? o.getInteger("collect_count") : o.getInteger("collectCount"));
+        vo.setCategoryOneId(o.getInteger("category_one_id") != null ? o.getInteger("category_one_id") : o.getInteger("categoryOneId"));
+        vo.setCategoryTwoId(o.getInteger("category_two_id") != null ? o.getInteger("category_two_id") : o.getInteger("categoryTwoId"));
+        vo.setLabel(o.getString("label"));
+        vo.setTopic(o.getString("topic"));
+        vo.setGoodsStatus(o.getInteger("goods_status") != null ? o.getInteger("goods_status") : o.getInteger("goodsStatus"));
+        vo.setReleaseTime(o.getDate("release_time") != null ? o.getDate("release_time") : o.getDate("releaseTime"));
+        vo.setCreatedTime(o.getDate("created_time") != null ? o.getDate("created_time") : o.getDate("createdTime"));
+        if (o.get("distance") != null) {
+            vo.setDistance(o.get("distance") instanceof Number ? String.valueOf(o.getDouble("distance")) : o.getString("distance"));
+        }
+        if (o.getJSONArray("image_urls") != null) {
+            vo.setImageUrls(o.getJSONArray("image_urls").toJavaList(String.class));
+        } else if (o.getJSONArray("imageUrls") != null) {
+            vo.setImageUrls(o.getJSONArray("imageUrls").toJavaList(String.class));
+        } else if (o.getJSONArray("img_url") != null) {
+            vo.setImgUrl(o.getJSONArray("img_url").toJavaList(String.class));
+        }
+        vo.setUserName(o.getString("user_name") != null ? o.getString("user_name") : o.getString("userName"));
+        vo.setUserPhone(o.getString("user_phone") != null ? o.getString("user_phone") : o.getString("userPhone"));
+        vo.setUserImage(o.getString("user_image") != null ? o.getString("user_image") : o.getString("userImage"));
+        vo.setCategoryOneName(o.getString("category_one_name") != null ? o.getString("category_one_name") : o.getString("categoryOneName"));
+        vo.setCategoryTwoName(o.getString("category_two_name") != null ? o.getString("category_two_name") : o.getString("categoryTwoName"));
+        return vo;
+    }
+
+    /**
+     * AI 搜索返回结果:当前页 SecondGoodsVo 列表 + 总条数
+     */
+    public static class SecondHandSearchResult {
+        private final List<SecondGoodsVo> records;
+        private final long total;
+
+        public SecondHandSearchResult(List<SecondGoodsVo> records, long total) {
+            this.records = records != null ? records : new ArrayList<>();
+            this.total = total;
+        }
+
+        public List<SecondGoodsVo> getRecords() {
+            return records;
+        }
+
+        public long getTotal() {
+            return total;
+        }
+    }
+}