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