From 266350f7da12968b26879c189ef4fe884647244a Mon Sep 17 00:00:00 2001 From: liujiang <569804566@qq.com> Date: Sun, 23 Nov 2025 23:25:33 +0800 Subject: [PATCH] =?UTF-8?q?master=EF=BC=9A=E7=B3=BB=E7=BB=9F=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E6=95=B0=E6=8D=AE=E5=88=B0ES=E5=8F=8A=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2ES=E6=95=B0=E6=8D=AE=E8=B0=83=E4=BC=98=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xkt/ElasticSearchController.java | 43 ++ .../web/controller/xkt/NoticeController.java | 4 +- .../GtAndFhbBiz22222222Controller.java | 1 + .../xkt/migartion/GtAndFhbBizController.java | 1 + .../migartion/GtAndTyBiz222222Controller.java | 1 + .../xkt/migartion/GtAndTyBizController.java | 1 + .../xkt/migartion/GtOnlyBizController.java | 1 + .../migartion/GtOtherBizAfterController.java | 1 + .../xkt/service/IElasticSearchService.java | 31 ++ .../impl/ElasticSearchServiceImpl.java | 519 ++++++++++++++++++ .../service/impl/WebsitePCServiceImpl.java | 117 +++- 11 files changed, 697 insertions(+), 23 deletions(-) create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/ElasticSearchController.java create mode 100644 xkt/src/main/java/com/ruoyi/xkt/service/IElasticSearchService.java create mode 100644 xkt/src/main/java/com/ruoyi/xkt/service/impl/ElasticSearchServiceImpl.java diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/ElasticSearchController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/ElasticSearchController.java new file mode 100644 index 000000000..fcfcbc93a --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/ElasticSearchController.java @@ -0,0 +1,43 @@ +package com.ruoyi.web.controller.xkt; + +import cn.hutool.core.bean.BeanUtil; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.controller.XktBaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.Page; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.web.controller.xkt.vo.notice.*; +import com.ruoyi.xkt.dto.notice.*; +import com.ruoyi.xkt.service.IElasticSearchService; +import com.ruoyi.xkt.service.INoticeService; +import com.ruoyi.xkt.service.IStoreProductService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * ES相关处理 + * + * @author ruoyi + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/rest/v1/es") +public class ElasticSearchController extends XktBaseController { + + final IElasticSearchService esService; + + @PreAuthorize("@ss.hasAnyRoles('admin,general_admin')") + @Log(title = "往ES中批量新增数据", businessType = BusinessType.INSERT) + @PutMapping("/batch") + public R batchCreate() { + return R.ok(esService.batchCreate()); + } + +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/NoticeController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/NoticeController.java index b47993cb4..43d7bba97 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/NoticeController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/NoticeController.java @@ -2,7 +2,7 @@ package com.ruoyi.web.controller.xkt; import cn.hutool.core.bean.BeanUtil; import com.ruoyi.common.annotation.Log; -import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.controller.XktBaseController; import com.ruoyi.common.core.domain.R; import com.ruoyi.common.core.page.Page; import com.ruoyi.common.enums.BusinessType; @@ -27,7 +27,7 @@ import java.util.List; @RestController @RequiredArgsConstructor @RequestMapping("/rest/v1/notice") -public class NoticeController extends BaseController { +public class NoticeController extends XktBaseController { final INoticeService noticeService; diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtAndFhbBiz22222222Controller.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtAndFhbBiz22222222Controller.java index f30592010..473394141 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtAndFhbBiz22222222Controller.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtAndFhbBiz22222222Controller.java @@ -524,6 +524,7 @@ public class GtAndFhbBiz22222222Controller extends BaseController { .setSeason(ObjectUtils.isNotEmpty(cateAttr) ? cateAttr.getSuitableSeason() : "") .setProdStatus(product.getProdStatus().toString()) .setStoreId(product.getStoreId().toString()) + .setStoreWeight(ObjectUtils.isNotEmpty(store) ? store.getStoreWeight().toString() : WEIGHT_DEFAULT_ZERO.toString()) .setStoreName(ObjectUtils.isNotEmpty(store) ? store.getStoreName() : "") .setStyle(ObjectUtils.isNotEmpty(cateAttr) ? cateAttr.getStyle() : "") .setProdTitle(product.getProdTitle()); diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtAndFhbBizController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtAndFhbBizController.java index 09363128f..0201ca754 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtAndFhbBizController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtAndFhbBizController.java @@ -590,6 +590,7 @@ public class GtAndFhbBizController extends BaseController { .setSeason(ObjectUtils.isNotEmpty(cateAttr) ? cateAttr.getSuitableSeason() : "") .setProdStatus(product.getProdStatus().toString()) .setStoreId(product.getStoreId().toString()) + .setStoreWeight(ObjectUtils.isNotEmpty(store) ? store.getStoreWeight().toString() : WEIGHT_DEFAULT_ZERO.toString()) .setStoreName(ObjectUtils.isNotEmpty(store) ? store.getStoreName() : "") .setStyle(ObjectUtils.isNotEmpty(cateAttr) ? cateAttr.getStyle() : "") .setProdTitle(product.getProdTitle()); diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtAndTyBiz222222Controller.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtAndTyBiz222222Controller.java index c9029c458..bf6544477 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtAndTyBiz222222Controller.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtAndTyBiz222222Controller.java @@ -532,6 +532,7 @@ public class GtAndTyBiz222222Controller extends BaseController { .setSeason(ObjectUtils.isNotEmpty(cateAttr) ? cateAttr.getSuitableSeason() : "") .setProdStatus(product.getProdStatus().toString()) .setStoreId(product.getStoreId().toString()) + .setStoreWeight(ObjectUtils.isNotEmpty(store) ? store.getStoreWeight().toString() : WEIGHT_DEFAULT_ZERO.toString()) .setStoreName(ObjectUtils.isNotEmpty(store) ? store.getStoreName() : "") .setStyle(ObjectUtils.isNotEmpty(cateAttr) ? cateAttr.getStyle() : "") .setProdTitle(product.getProdTitle()); diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtAndTyBizController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtAndTyBizController.java index 312d79d57..038019f38 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtAndTyBizController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtAndTyBizController.java @@ -574,6 +574,7 @@ public class GtAndTyBizController extends BaseController { .setSeason(ObjectUtils.isNotEmpty(cateAttr) ? cateAttr.getSuitableSeason() : "") .setProdStatus(product.getProdStatus().toString()) .setStoreId(product.getStoreId().toString()) + .setStoreWeight(ObjectUtils.isNotEmpty(store) ? store.getStoreWeight().toString() : WEIGHT_DEFAULT_ZERO.toString()) .setStoreName(ObjectUtils.isNotEmpty(store) ? store.getStoreName() : "") .setStyle(ObjectUtils.isNotEmpty(cateAttr) ? cateAttr.getStyle() : "") .setProdTitle(product.getProdTitle()); diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtOnlyBizController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtOnlyBizController.java index d9fa99d20..939a466c8 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtOnlyBizController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtOnlyBizController.java @@ -284,6 +284,7 @@ public class GtOnlyBizController extends BaseController { .setSeason(ObjectUtils.isNotEmpty(cateAttr) ? cateAttr.getSuitableSeason() : "") .setProdStatus(product.getProdStatus().toString()) .setStoreId(product.getStoreId().toString()) + .setStoreWeight(ObjectUtils.isNotEmpty(store) ? store.getStoreWeight().toString() : WEIGHT_DEFAULT_ZERO.toString()) .setStoreName(ObjectUtils.isNotEmpty(store) ? store.getStoreName() : "") .setStyle(ObjectUtils.isNotEmpty(cateAttr) ? cateAttr.getStyle() : "") .setProdTitle(product.getProdTitle()); diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtOtherBizAfterController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtOtherBizAfterController.java index dab20afb7..c0755a02d 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtOtherBizAfterController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/migartion/GtOtherBizAfterController.java @@ -315,6 +315,7 @@ public class GtOtherBizAfterController extends BaseController { .setSeason(ObjectUtils.isNotEmpty(cateAttr) ? cateAttr.getSuitableSeason() : "") .setProdStatus(product.getProdStatus().toString()) .setStoreId(product.getStoreId().toString()) + .setStoreWeight(ObjectUtils.isNotEmpty(store) ? store.getStoreWeight().toString() : WEIGHT_DEFAULT_ZERO.toString()) .setStoreName(ObjectUtils.isNotEmpty(store) ? store.getStoreName() : "") .setStyle(ObjectUtils.isNotEmpty(cateAttr) ? cateAttr.getStyle() : "") .setProdTitle(product.getProdTitle()); diff --git a/xkt/src/main/java/com/ruoyi/xkt/service/IElasticSearchService.java b/xkt/src/main/java/com/ruoyi/xkt/service/IElasticSearchService.java new file mode 100644 index 000000000..c100f19e3 --- /dev/null +++ b/xkt/src/main/java/com/ruoyi/xkt/service/IElasticSearchService.java @@ -0,0 +1,31 @@ +package com.ruoyi.xkt.service; + +import com.ruoyi.common.core.page.Page; +import com.ruoyi.xkt.dto.es.ESProductDTO; +import com.ruoyi.xkt.dto.website.IndexSearchDTO; + +import java.io.IOException; + +/** + * 档口商品Service接口 + * + * @author ruoyi + * @date 2025-03-26 + */ +public interface IElasticSearchService { + + /** + * 批量往ES新增商品数据 + * + * @return Integer + */ + Integer batchCreate(); + + /** + * 网站首页搜索 + * + * @param searchDTO 查询入参 + */ + Page search(IndexSearchDTO searchDTO) throws IOException; + +} diff --git a/xkt/src/main/java/com/ruoyi/xkt/service/impl/ElasticSearchServiceImpl.java b/xkt/src/main/java/com/ruoyi/xkt/service/impl/ElasticSearchServiceImpl.java new file mode 100644 index 000000000..0786ef4e6 --- /dev/null +++ b/xkt/src/main/java/com/ruoyi/xkt/service/impl/ElasticSearchServiceImpl.java @@ -0,0 +1,519 @@ +package com.ruoyi.xkt.service.impl; + +import co.elastic.clients.elasticsearch._types.FieldValue; +import co.elastic.clients.elasticsearch._types.SortOptions; +import co.elastic.clients.elasticsearch._types.SortOrder; +import co.elastic.clients.elasticsearch._types.query_dsl.*; +import co.elastic.clients.elasticsearch.core.BulkResponse; +import co.elastic.clients.elasticsearch.core.SearchResponse; +import co.elastic.clients.elasticsearch.core.bulk.BulkOperation; +import co.elastic.clients.elasticsearch.core.bulk.BulkResponseItem; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.page.Page; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.framework.es.EsClientWrapper; +import com.ruoyi.framework.notice.fs.FsNotice; +import com.ruoyi.xkt.domain.*; +import com.ruoyi.xkt.dto.es.ESProductDTO; +import com.ruoyi.xkt.dto.storeProdColorPrice.StoreProdMinPriceDTO; +import com.ruoyi.xkt.dto.storeProductFile.StoreProdMainPicDTO; +import com.ruoyi.xkt.dto.website.IndexSearchDTO; +import com.ruoyi.xkt.enums.FileType; +import com.ruoyi.xkt.mapper.*; +import com.ruoyi.xkt.service.IElasticSearchService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +import static com.ruoyi.common.constant.Constants.WEIGHT_DEFAULT_ZERO; + +/** + * 公告 服务层实现 + * + * @author ruoyi + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ElasticSearchServiceImpl implements IElasticSearchService { + + final StoreProductColorSizeMapper prodColorSizeMapper; + final StoreProductMapper storeProdMapper; + final StoreMapper storeMapper; + final StoreProductCategoryAttributeMapper prodCateAttrMapper; + final SysProductCategoryMapper prodCateMapper; + final EsClientWrapper esClientWrapper; + final StoreProductFileMapper prodFileMapper; + final FsNotice fsNotice; + @Value("${es.indexName}") + private String ES_INDEX_NAME; + + /** + * 批量往ES新增商品数据 + * + * @return Integer + */ + @Override + @Transactional + public Integer batchCreate() { + List storeProdList = this.storeProdMapper.selectList(new LambdaQueryWrapper() + .eq(StoreProduct::getDelFlag, Constants.UNDELETED)); + if (CollectionUtils.isEmpty(storeProdList)) { + return 0; + } + final List storeProdIdList = storeProdList.stream().map(StoreProduct::getId).map(String::valueOf).collect(Collectors.toList()); + // 所有的分类 + List prodCateList = this.prodCateMapper.selectList(new LambdaQueryWrapper() + .eq(SysProductCategory::getDelFlag, Constants.UNDELETED)); + Map prodCateMap = prodCateList.stream().collect(Collectors.toMap(SysProductCategory::getId, x -> x)); + List mainPicDTOList = this.prodFileMapper.selectMainPicByStoreProdIdList(storeProdIdList.stream() + .map(Long::valueOf).collect(Collectors.toList()), FileType.MAIN_PIC.getValue(), Constants.ORDER_NUM_1); + Map mainPicMap = mainPicDTOList.stream().collect(Collectors.toMap(StoreProdMainPicDTO::getStoreProdId, StoreProdMainPicDTO::getFileUrl)); + // 获取当前商品最低价格 + Map prodMinPriceMap = this.prodColorSizeMapper.selectStoreProdMinPriceList(storeProdIdList).stream().collect(Collectors + .toMap(StoreProdMinPriceDTO::getStoreProdId, StoreProdMinPriceDTO::getPrice)); + // 档口商品的属性map + Map cateAttrMap = this.prodCateAttrMapper.selectList(new LambdaQueryWrapper() + .eq(StoreProductCategoryAttribute::getDelFlag, Constants.UNDELETED).in(StoreProductCategoryAttribute::getStoreProdId, storeProdIdList)) + .stream().collect(Collectors.toMap(StoreProductCategoryAttribute::getStoreProdId, x -> x)); + // 档口商品对应的档口 + Map storeMap = this.storeMapper.selectList(new LambdaQueryWrapper().eq(Store::getDelFlag, Constants.UNDELETED) + .in(Store::getId, storeProdList.stream().map(StoreProduct::getStoreId).collect(Collectors.toList()))) + .stream().collect(Collectors.toMap(Store::getId, x -> x)); + List esProductDTOList = new ArrayList<>(); + for (StoreProduct product : storeProdList) { + final SysProductCategory cate = prodCateMap.get(product.getProdCateId()); + final SysProductCategory parCate = ObjectUtils.isEmpty(cate) ? null : prodCateMap.get(cate.getParentId()); + final Store store = storeMap.get(product.getStoreId()); + final BigDecimal prodMinPrice = prodMinPriceMap.get(product.getId()); + final StoreProductCategoryAttribute cateAttr = cateAttrMap.get(product.getId()); + ESProductDTO esProductDTO = new ESProductDTO().setStoreProdId(product.getId().toString()).setProdArtNum(product.getProdArtNum()) + .setHasVideo(Boolean.FALSE).setProdCateId(product.getProdCateId().toString()).setCreateTime(DateUtils.getTime()) + .setProdCateName(ObjectUtils.isNotEmpty(cate) ? cate.getName() : "") + .setSaleWeight(WEIGHT_DEFAULT_ZERO.toString()).setRecommendWeight(WEIGHT_DEFAULT_ZERO.toString()) + .setPopularityWeight(WEIGHT_DEFAULT_ZERO.toString()) + .setMainPicUrl(mainPicMap.get(product.getId())).setMainPicName("").setMainPicSize(BigDecimal.ZERO) + .setParCateId(ObjectUtils.isNotEmpty(parCate) ? parCate.getId().toString() : "") + .setParCateName(ObjectUtils.isNotEmpty(parCate) ? parCate.getName() : "") + .setProdPrice(ObjectUtils.isNotEmpty(prodMinPrice) ? prodMinPrice.toString() : "") + .setSeason(ObjectUtils.isNotEmpty(cateAttr) ? cateAttr.getSuitableSeason() : "") + .setProdStatus(product.getProdStatus().toString()).setStoreId(product.getStoreId().toString()) + .setStoreWeight(ObjectUtils.isNotEmpty(store) ? store.getStoreWeight().toString() : WEIGHT_DEFAULT_ZERO.toString()) + .setStoreName(ObjectUtils.isNotEmpty(store) ? store.getStoreName() : "") + .setStyle(ObjectUtils.isNotEmpty(cateAttr) ? cateAttr.getStyle() : "") + .setProdTitle(product.getProdTitle()); + if (ObjectUtils.isNotEmpty(cateAttr) && StringUtils.isNotBlank(cateAttr.getStyle())) { + esProductDTO.setTags(Collections.singletonList(cateAttr.getStyle())); + } + esProductDTOList.add(esProductDTO); + } + // 构建批量操作请求 + List bulkOperations = new ArrayList<>(); + for (ESProductDTO esProductDTO : esProductDTOList) { + BulkOperation bulkOperation = new BulkOperation.Builder() + .index(i -> i.id(esProductDTO.getStoreProdId()).index(ES_INDEX_NAME).document(esProductDTO)) + .build(); + bulkOperations.add(bulkOperation); + } + // 执行批量插入 + try { + BulkResponse response = esClientWrapper.getEsClient().bulk(b -> b.index(ES_INDEX_NAME).operations(bulkOperations)); + log.info("全量新增到 ES 成功的 id列表: {}", response.items().stream().map(BulkResponseItem::id).collect(Collectors.toList())); + // 有哪些没执行成功的,需要发飞书通知 + List successIdList = response.items().stream().map(BulkResponseItem::id).collect(Collectors.toList()); + List unExeIdList = storeProdIdList.stream().map(String::valueOf).filter(x -> !successIdList.contains(x)).collect(Collectors.toList()); + if (CollectionUtils.isNotEmpty(unExeIdList)) { + fsNotice.sendMsg2DefaultChat("全量新增商品到 ES 失败", "以下storeProdId未执行成功: " + unExeIdList); + } else { + fsNotice.sendMsg2DefaultChat("全量新增商品到 ES 成功", "共处理 " + response.items().size() + " 条记录"); + } + } catch (Exception e) { + log.error("批量新增到 ES 失败", e); + fsNotice.sendMsg2DefaultChat("全量新增商品到 ES 失败", e.getMessage()); + } + return 1; + } + + + + /** + * 网站首页搜索 + * + * @param searchDTO 搜索入参 + */ + @Override + @Transactional(readOnly = true) + public Page search(IndexSearchDTO searchDTO) throws IOException { + // 构建 bool 查询 + BoolQuery.Builder boolQueryBuilder = new BoolQuery.Builder(); + // 添加 price 范围查询 + if (ObjectUtils.isNotEmpty(searchDTO.getMinPrice()) && ObjectUtils.isNotEmpty(searchDTO.getMaxPrice())) { + RangeQuery.Builder rangeBuilder = new RangeQuery.Builder(); + rangeBuilder.number(NumberRangeQuery.of(n -> n.field("prodPrice").gte(Double.valueOf(searchDTO.getMinPrice())) + .lte(Double.valueOf(searchDTO.getMaxPrice())))); + boolQueryBuilder.filter(rangeBuilder.build()._toQuery()); + } + if (StringUtils.isNotBlank(searchDTO.getSearch())) { + String searchTerm = searchDTO.getSearch().trim(); + // 创建专门的搜索 bool query + BoolQuery.Builder searchBoolQuery = new BoolQuery.Builder(); + // 判断搜索词类型 + boolean hasChinese = containsChinese(searchTerm); + boolean hasEnglishOrNumber = containsEnglishOrNumber(searchTerm); + boolean isShortText = searchTerm.length() <= 3; + // 核心策略:根据搜索词类型选择最优查询方式 + if (hasChinese && hasEnglishOrNumber) { + // 混合文本(如"娇点高筒MaMaT1")- 智能拆分搜索 + handleMixedTextSearch(searchTerm, searchBoolQuery); + } else if (isShortText && hasEnglishOrNumber) { + // 短英文/数字 - 使用通配符和前缀匹配 + handleShortEnglishNumberSearch(searchTerm, searchBoolQuery); + } else if (isShortText && hasChinese) { + // 短中文 - 使用分词匹配 + handleShortChineseSearch(searchTerm, searchBoolQuery); + } else { + // 长文本 - 使用标准分词搜索 + handleStandardTextSearch(searchTerm, searchBoolQuery); + } + // 设置最小匹配条件 + searchBoolQuery.minimumShouldMatch("1"); + // 将搜索条件添加到主查询 + boolQueryBuilder.must(searchBoolQuery.build()._toQuery()); + } + + + + + + + + + + /*// 优化搜索逻辑 - 修复版 + if (StringUtils.isNotBlank(searchDTO.getSearch())) { + String searchTerm = searchDTO.getSearch().trim(); + + // 创建专门的搜索 bool query + BoolQuery.Builder searchBoolQuery = new BoolQuery.Builder(); + + // 方案1:使用 MultiMatchQuery 进行智能分词搜索(主要方式) + MultiMatchQuery multiMatchQuery = MultiMatchQuery.of(m -> m + .query(searchTerm) + .fields("storeName^3", "prodTitle^2", "prodCateName", "parCateName", "prodArtNum^2") + .type(TextQueryType.BestFields) // 使用最佳字段匹配 + ); + searchBoolQuery.should(multiMatchQuery._toQuery()); + + // 方案2:对每个字段单独使用 MatchQuery(作为补充) + String[] matchFields = {"storeName", "prodTitle", "prodCateName", "parCateName", "prodArtNum"}; + for (String field : matchFields) { + MatchQuery matchQuery = MatchQuery.of(m -> m + .field(field) + .query(searchTerm) + .operator(Operator.Or) // 使用 OR 操作符,匹配任意分词 + ); + searchBoolQuery.should(matchQuery._toQuery()); + } + + // 方案3:对于包含英文或数字的情况,添加通配符查询到 keyword 字段 + if (containsEnglishOrNumber(searchTerm)) { + String wildcardPattern = "*" + searchTerm + "*"; + String[] keywordFields = { + "storeName.keyword", "prodTitle.keyword", "prodArtNum.keyword" + }; + + for (String field : keywordFields) { + WildcardQuery wildcardQuery = WildcardQuery.of(w -> w + .field(field) + .value(wildcardPattern) + .boost(0.3f) // 降低权重 + ); + searchBoolQuery.should(wildcardQuery._toQuery()); + } + } + + // 方案4:拆分成单个字符进行通配符匹配(针对混合字符串) + if (searchTerm.length() > 1) { + // 对每个字符单独进行通配符匹配 + for (int i = 0; i < searchTerm.length(); i++) { + char c = searchTerm.charAt(i); + if (Character.isLetterOrDigit(c)) { + String charPattern = "*" + c + "*"; + WildcardQuery charQuery = WildcardQuery.of(w -> w + .field("storeName.keyword") + .value(charPattern) + .boost(0.1f) // 很低权重,确保不影响主要结果 + ); + searchBoolQuery.should(charQuery._toQuery()); + } + } + } + + // 设置最小匹配条件 + searchBoolQuery.minimumShouldMatch("1"); + + // 将搜索条件添加到主查询 + boolQueryBuilder.must(searchBoolQuery.build()._toQuery()); + }*/ + + + + + + + + + + + + + + + + + + + // 档口ID 过滤条件 + if (CollectionUtils.isNotEmpty(searchDTO.getStoreIdList())) { + TermsQueryField termsQueryField = new TermsQueryField.Builder() + .value(searchDTO.getStoreIdList().stream() + .map(FieldValue::of) + .collect(Collectors.toList())) + .build(); + boolQueryBuilder.filter(f -> f.terms(t -> t.field("storeId").terms(termsQueryField))); + } + // 添加prodStatus 过滤条件 + if (CollectionUtils.isNotEmpty(searchDTO.getProdStatusList())) { + TermsQueryField termsQueryField = new TermsQueryField.Builder() + .value(searchDTO.getProdStatusList().stream() + .map(FieldValue::of) + .collect(Collectors.toList())) + .build(); + boolQueryBuilder.filter(f -> f.terms(t -> t.field("prodStatus").terms(termsQueryField))); + } + // 添加 prodCateId 过滤条件 + if (CollectionUtils.isNotEmpty(searchDTO.getProdCateIdList())) { + TermsQueryField termsQueryField = new TermsQueryField.Builder() + .value(searchDTO.getProdCateIdList().stream() + .map(FieldValue::of) + .collect(Collectors.toList())) + .build(); + boolQueryBuilder.filter(f -> f.terms(t -> t.field("prodCateId").terms(termsQueryField))); + } + // 添加 parCateId 过滤条件 + if (CollectionUtils.isNotEmpty(searchDTO.getParCateIdList())) { + TermsQueryField termsQueryField = new TermsQueryField.Builder() + .value(searchDTO.getParCateIdList().stream() + .map(FieldValue::of) + .collect(Collectors.toList())) + .build(); + boolQueryBuilder.filter(f -> f.terms(t -> t.field("parCateId").terms(termsQueryField))); + } + // 添加 style 过滤条件 + if (CollectionUtils.isNotEmpty(searchDTO.getStyleList())) { + TermsQueryField termsQueryField = new TermsQueryField.Builder() + .value(searchDTO.getStyleList().stream() + .map(FieldValue::of) + .collect(Collectors.toList())) + .build(); + boolQueryBuilder.filter(f -> f.terms(t -> t.field("style.keyword").terms(termsQueryField))); + } + // 添加 season 过滤条件 + if (CollectionUtils.isNotEmpty(searchDTO.getSeasonList())) { + TermsQueryField termsQueryField = new TermsQueryField.Builder() + .value(searchDTO.getSeasonList().stream() + .map(FieldValue::of) + .collect(Collectors.toList())) + .build(); + boolQueryBuilder.filter(f -> f.terms(t -> t.field("season.keyword").terms(termsQueryField))); + } + // 如果是按照时间过滤,则表明是"新品",则限制 时间范围 30天前到现在 + if (Objects.equals(searchDTO.getSort(), "createTime")) { + // 当前时间 + final String nowStr = DateUtils.getTime(); + // 当前时间往前推30天,获取当天的0点0分0秒 + LocalDateTime ago = LocalDateTime.now().minusDays(30).withHour(0).withMinute(0).withSecond(0); + // ago 转化为 yyyy-MM-dd HH:mm:ss + String agoStr = ago.format(DateTimeFormatter.ofPattern(DateUtils.YYYY_MM_DD_HH_MM_SS)); + RangeQuery.Builder rangeBuilder = new RangeQuery.Builder(); + rangeBuilder.date(DateRangeQuery.of(d -> d.field("createTime").gte(agoStr).lte(nowStr))); + boolQueryBuilder.filter(rangeBuilder.build()._toQuery()); + } + // 构建最终的 bool query + BoolQuery boolQuery = boolQueryBuilder.build(); + // 构建最终的查询 + Query query = new Query.Builder().bool(boolQuery).build(); + long start = System.currentTimeMillis(); + // 执行搜索 + SearchResponse resList = esClientWrapper.getEsClient() + .search(s -> s.index(ES_INDEX_NAME) + .query(query) + .from((searchDTO.getPageNum() - 1) * searchDTO.getPageSize()) + .size(searchDTO.getPageSize()) + .sort(Arrays.asList( + SortOptions.of(so -> so.field(f -> f.field(searchDTO.getSort()).order(searchDTO.getOrder()))), + SortOptions.of(so -> so.field(f -> f.field("storeWeight").order(SortOrder.Desc))) + )), + ESProductDTO.class); + long second = System.currentTimeMillis(); + System.err.println("查询耗时:" + (second - start)); + final long total = resList.hits().total().value(); + final List esProdList = resList.hits().hits().stream().map(x -> x.source().setStoreProdId(x.id())).collect(Collectors.toList()); + return new Page<>(searchDTO.getPageNum(), searchDTO.getPageSize(), total / searchDTO.getPageSize() + 1, total, esProdList); + } + + + + // 处理混合文本搜索(中英文数字混合) + private void handleMixedTextSearch(String searchTerm, BoolQuery.Builder searchBoolQuery) { + // 1. 整体多字段匹配(主要方式) + MultiMatchQuery multiMatchQuery = MultiMatchQuery.of(m -> m + .query(searchTerm) + .fields("storeName^3", "prodTitle^2", "prodCateName", "parCateName", "prodArtNum^2") + .type(TextQueryType.BestFields) + ); + searchBoolQuery.should(multiMatchQuery._toQuery()); + // 2. 提取英文数字部分进行通配符匹配 + String englishNumberPart = extractEnglishNumberPart(searchTerm); + if (StringUtils.isNotBlank(englishNumberPart)) { + String wildcardPattern = "*" + englishNumberPart + "*"; + WildcardQuery wildcardQuery = WildcardQuery.of(w -> w + .field("storeName.keyword") + .value(wildcardPattern) + .boost(2.0f) // 提高权重,因为这是用户明确输入的英文部分 + ); + searchBoolQuery.should(wildcardQuery._toQuery()); + // 同时在商品编号字段搜索 + WildcardQuery artNumQuery = WildcardQuery.of(w -> w + .field("prodArtNum.keyword") + .value(wildcardPattern) + .boost(1.5f) + ); + searchBoolQuery.should(artNumQuery._toQuery()); + } + // 3. 中文部分单独匹配 + String chinesePart = extractChinesePart(searchTerm); + if (StringUtils.isNotBlank(chinesePart)) { + MatchQuery chineseQuery = MatchQuery.of(m -> m + .field("storeName") + .query(chinesePart) + .boost(1.0f) + ); + searchBoolQuery.should(chineseQuery._toQuery()); + } + } + + // 处理短英文数字搜索 + private void handleShortEnglishNumberSearch(String searchTerm, BoolQuery.Builder searchBoolQuery) { + // 1. 前缀匹配(最高优先级) + String prefixPattern = searchTerm + "*"; + WildcardQuery prefixQuery = WildcardQuery.of(w -> w + .field("storeName.keyword") + .value(prefixPattern) + .boost(3.0f) + ); + searchBoolQuery.should(prefixQuery._toQuery()); + // 2. 通配符匹配 + String wildcardPattern = "*" + searchTerm + "*"; + WildcardQuery wildcardQuery = WildcardQuery.of(w -> w + .field("storeName.keyword") + .value(wildcardPattern) + .boost(2.0f) + ); + searchBoolQuery.should(wildcardQuery._toQuery()); + // 3. 在商品编号字段搜索 + WildcardQuery artNumQuery = WildcardQuery.of(w -> w + .field("prodArtNum.keyword") + .value(wildcardPattern) + .boost(2.5f) + ); + searchBoolQuery.should(artNumQuery._toQuery()); + } + + // 处理短中文搜索 + private void handleShortChineseSearch(String searchTerm, BoolQuery.Builder searchBoolQuery) { + // 使用 MultiMatchQuery 进行分词匹配 + MultiMatchQuery multiMatchQuery = MultiMatchQuery.of(m -> m + .query(searchTerm) + .fields("storeName^3", "prodTitle^2", "prodCateName", "parCateName") + .type(TextQueryType.MostFields) // 使用最多字段匹配 + ); + searchBoolQuery.should(multiMatchQuery._toQuery()); + } + + // 处理标准长文本搜索 + private void handleStandardTextSearch(String searchTerm, BoolQuery.Builder searchBoolQuery) { + // 使用标准的 MultiMatchQuery + MultiMatchQuery multiMatchQuery = MultiMatchQuery.of(m -> m + .query(searchTerm) + .fields("storeName^3", "prodTitle", "prodCateName", "parCateName", "prodArtNum^2") + .type(TextQueryType.BestFields) + ); + searchBoolQuery.should(multiMatchQuery._toQuery()); + } + + + // 判断是否包含中文 + private boolean containsChinese(String str) { + if (str == null) return false; + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (c >= '\u4e00' && c <= '\u9fa5') { + return true; + } + } + return false; + } + + // 判断是否包含英文或数字 + private boolean containsEnglishOrNumber(String str) { + if (str == null) return false; + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) { + return true; + } + } + return false; + } + + // 提取英文数字部分 + private String extractEnglishNumberPart(String str) { + if (str == null) return ""; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) { + sb.append(c); + } + } + return sb.toString(); + } + + // 提取中文部分 + private String extractChinesePart(String str) { + if (str == null) return ""; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (c >= '\u4e00' && c <= '\u9fa5') { + sb.append(c); + } + } + return sb.toString(); + } + +} diff --git a/xkt/src/main/java/com/ruoyi/xkt/service/impl/WebsitePCServiceImpl.java b/xkt/src/main/java/com/ruoyi/xkt/service/impl/WebsitePCServiceImpl.java index 3f97defab..3921c2cc4 100644 --- a/xkt/src/main/java/com/ruoyi/xkt/service/impl/WebsitePCServiceImpl.java +++ b/xkt/src/main/java/com/ruoyi/xkt/service/impl/WebsitePCServiceImpl.java @@ -9,6 +9,7 @@ import co.elastic.clients.elasticsearch._types.SortOrder; import co.elastic.clients.elasticsearch._types.query_dsl.*; import co.elastic.clients.elasticsearch.core.SearchResponse; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.fasterxml.jackson.databind.ObjectMapper; import com.ruoyi.common.constant.CacheConstants; import com.ruoyi.common.constant.Constants; import com.ruoyi.common.core.domain.model.LoginUser; @@ -42,6 +43,7 @@ import com.ruoyi.xkt.enums.AdDisplayType; import com.ruoyi.xkt.enums.AdLaunchStatus; import com.ruoyi.xkt.enums.SearchPlatformType; import com.ruoyi.xkt.mapper.*; +import com.ruoyi.xkt.service.IElasticSearchService; import com.ruoyi.xkt.service.IPictureService; import com.ruoyi.xkt.service.IWebsitePCService; import lombok.RequiredArgsConstructor; @@ -87,6 +89,8 @@ public class WebsitePCServiceImpl implements IWebsitePCService { final IPictureService pictureService; final UserSubscriptionsMapper userSubsMapper; final SysProductCategoryMapper prodCateMapper; + final ObjectMapper jsonMapper; + final IElasticSearchService esService; @Value("${es.indexName}") private String ES_INDEX_NAME; @@ -833,22 +837,77 @@ public class WebsitePCServiceImpl implements IWebsitePCService { @Override @Transactional(readOnly = true) public Page search(IndexSearchDTO searchDTO) throws IOException { + + return this.esService.search(searchDTO); + /* // 构建 bool 查询 - BoolQuery.Builder boolQuery = new BoolQuery.Builder(); + BoolQuery.Builder boolQueryBuilder = new BoolQuery.Builder(); // 添加 price 范围查询 if (ObjectUtils.isNotEmpty(searchDTO.getMinPrice()) && ObjectUtils.isNotEmpty(searchDTO.getMaxPrice())) { - RangeQuery.Builder builder = new RangeQuery.Builder(); - builder.number(NumberRangeQuery.of(n -> n.field("prodPrice").gte(Double.valueOf(searchDTO.getMinPrice())) + RangeQuery.Builder rangeBuilder = new RangeQuery.Builder(); + rangeBuilder.number(NumberRangeQuery.of(n -> n.field("prodPrice").gte(Double.valueOf(searchDTO.getMinPrice())) .lte(Double.valueOf(searchDTO.getMaxPrice())))); - boolQuery.filter(builder.build()._toQuery()); + boolQueryBuilder.filter(rangeBuilder.build()._toQuery()); } - // 添加 multiMatch 查询 + // 优化搜索逻辑 if (StringUtils.isNotBlank(searchDTO.getSearch())) { - MultiMatchQuery multiMatchQuery = MultiMatchQuery.of(m -> m - .query(searchDTO.getSearch()) - .fields("storeName", "prodTitle", "prodCateName", "parCateName", "prodArtNum") - ); - boolQuery.must(multiMatchQuery._toQuery()); + String searchTerm = searchDTO.getSearch().trim(); + // 创建专门的搜索 bool query + BoolQuery.Builder searchBoolQuery = new BoolQuery.Builder(); + // 1. 使用 MatchQuery 利用 IK 分词器(主要搜索方式) + String[] matchFields = {"storeName", "prodTitle", "prodCateName", "parCateName", "prodArtNum"}; + for (String field : matchFields) { + MatchQuery matchQuery = MatchQuery.of(m -> m + .field(field) + .query(searchTerm) + .boost(field.equals("storeName") || field.equals("prodArtNum") ? 2.0f : 1.0f) + ); + searchBoolQuery.should(matchQuery._toQuery()); + } + // 2. 对于短字符(1-2个字符),添加通配符查询到 keyword 字段 + if (searchTerm.length() <= 2) { + String wildcardPattern = "*" + searchTerm + "*"; + String[] keywordFields = { + "storeName.keyword", "prodTitle.keyword", "prodCateName.keyword", + "parCateName.keyword", "prodArtNum.keyword" + }; + for (String field : keywordFields) { + WildcardQuery wildcardQuery = WildcardQuery.of(w -> w + .field(field) + .value(wildcardPattern) + .boost(0.5f) // 降低权重,优先使用分词匹配 + ); + searchBoolQuery.should(wildcardQuery._toQuery()); + } + // 3. 额外添加前缀匹配,对短字符特别有效 + String prefixPattern = searchTerm + "*"; + WildcardQuery prefixQuery = WildcardQuery.of(w -> w + .field("storeName.keyword") + .value(prefixPattern) + .boost(1.0f) + ); + searchBoolQuery.should(prefixQuery._toQuery()); + } + // 4. 对于中文单字,添加专门的匹配 + if (searchTerm.length() == 1 && isChineseCharacter(searchTerm)) { + // 使用 term 查询在 keyword 字段上精确匹配 + String[] keywordFields = { + "storeName.keyword", "prodTitle.keyword", "prodCateName.keyword", + "parCateName.keyword", "prodArtNum.keyword" + }; + for (String field : keywordFields) { + WildcardQuery charQuery = WildcardQuery.of(w -> w + .field(field) + .value("*" + searchTerm + "*") + .boost(0.8f) + ); + searchBoolQuery.should(charQuery._toQuery()); + } + } + // 设置最小匹配条件 + searchBoolQuery.minimumShouldMatch("1"); + // 将搜索条件添加到主查询 + boolQueryBuilder.must(searchBoolQuery.build()._toQuery()); } // 档口ID 过滤条件 if (CollectionUtils.isNotEmpty(searchDTO.getStoreIdList())) { @@ -857,7 +916,7 @@ public class WebsitePCServiceImpl implements IWebsitePCService { .map(WebsitePCServiceImpl::newFieldValue) .collect(Collectors.toList())) .build(); - boolQuery.filter(f -> f.terms(t -> t.field("storeId").terms(termsQueryField))); + boolQueryBuilder.filter(f -> f.terms(t -> t.field("storeId").terms(termsQueryField))); } // 添加prodStatus 过滤条件 if (CollectionUtils.isNotEmpty(searchDTO.getProdStatusList())) { @@ -866,7 +925,7 @@ public class WebsitePCServiceImpl implements IWebsitePCService { .map(WebsitePCServiceImpl::newFieldValue) .collect(Collectors.toList())) .build(); - boolQuery.filter(f -> f.terms(t -> t.field("prodStatus").terms(termsQueryField))); + boolQueryBuilder.filter(f -> f.terms(t -> t.field("prodStatus").terms(termsQueryField))); } // 添加 prodCateId 过滤条件 if (CollectionUtils.isNotEmpty(searchDTO.getProdCateIdList())) { @@ -875,7 +934,7 @@ public class WebsitePCServiceImpl implements IWebsitePCService { .map(WebsitePCServiceImpl::newFieldValue) .collect(Collectors.toList())) .build(); - boolQuery.filter(f -> f.terms(t -> t.field("prodCateId").terms(termsQueryField))); + boolQueryBuilder.filter(f -> f.terms(t -> t.field("prodCateId").terms(termsQueryField))); } // 添加 parCateId 过滤条件 if (CollectionUtils.isNotEmpty(searchDTO.getParCateIdList())) { @@ -884,7 +943,7 @@ public class WebsitePCServiceImpl implements IWebsitePCService { .map(WebsitePCServiceImpl::newFieldValue) .collect(Collectors.toList())) .build(); - boolQuery.filter(f -> f.terms(t -> t.field("parCateId").terms(termsQueryField))); + boolQueryBuilder.filter(f -> f.terms(t -> t.field("parCateId").terms(termsQueryField))); } // 添加 style 过滤条件 if (CollectionUtils.isNotEmpty(searchDTO.getStyleList())) { @@ -893,7 +952,7 @@ public class WebsitePCServiceImpl implements IWebsitePCService { .map(WebsitePCServiceImpl::newFieldValue) .collect(Collectors.toList())) .build(); - boolQuery.filter(f -> f.terms(t -> t.field("style.keyword").terms(termsQueryField))); + boolQueryBuilder.filter(f -> f.terms(t -> t.field("style.keyword").terms(termsQueryField))); } // 添加 season 过滤条件 if (CollectionUtils.isNotEmpty(searchDTO.getSeasonList())) { @@ -902,9 +961,9 @@ public class WebsitePCServiceImpl implements IWebsitePCService { .map(WebsitePCServiceImpl::newFieldValue) .collect(Collectors.toList())) .build(); - boolQuery.filter(f -> f.terms(t -> t.field("season.keyword").terms(termsQueryField))); + boolQueryBuilder.filter(f -> f.terms(t -> t.field("season.keyword").terms(termsQueryField))); } - // 如果是按照时间过滤,则表明是“新品”,则限制 时间范围 30天前到现在 + // 如果是按照时间过滤,则表明是"新品",则限制 时间范围 30天前到现在 if (Objects.equals(searchDTO.getSort(), "createTime")) { // 当前时间 final String nowStr = DateUtils.getTime(); @@ -912,12 +971,14 @@ public class WebsitePCServiceImpl implements IWebsitePCService { LocalDateTime ago = LocalDateTime.now().minusDays(30).withHour(0).withMinute(0).withSecond(0); // ago 转化为 yyyy-MM-dd HH:mm:ss String agoStr = ago.format(DateTimeFormatter.ofPattern(DateUtils.YYYY_MM_DD_HH_MM_SS)); - RangeQuery.Builder builder = new RangeQuery.Builder(); - builder.date(DateRangeQuery.of(d -> d.field("createTime").gte(agoStr).lte(nowStr))); - boolQuery.filter(builder.build()._toQuery()); + RangeQuery.Builder rangeBuilder = new RangeQuery.Builder(); + rangeBuilder.date(DateRangeQuery.of(d -> d.field("createTime").gte(agoStr).lte(nowStr))); + boolQueryBuilder.filter(rangeBuilder.build()._toQuery()); } + // 构建最终的 bool query + BoolQuery boolQuery = boolQueryBuilder.build(); // 构建最终的查询 - Query query = new Query.Builder().bool(boolQuery.build()).build(); + Query query = new Query.Builder().bool(boolQuery).build(); // 执行搜索 SearchResponse resList = esClientWrapper.getEsClient() .search(s -> s.index(ES_INDEX_NAME) @@ -932,6 +993,7 @@ public class WebsitePCServiceImpl implements IWebsitePCService { final long total = resList.hits().total().value(); final List esProdList = resList.hits().hits().stream().map(x -> x.source().setStoreProdId(x.id())).collect(Collectors.toList()); return new Page<>(searchDTO.getPageNum(), searchDTO.getPageSize(), total / searchDTO.getPageSize() + 1, total, esProdList); + */ } /** @@ -975,6 +1037,10 @@ public class WebsitePCServiceImpl implements IWebsitePCService { if (StringUtils.isEmpty(search)) { return; } + Long userId = SecurityUtils.getUserIdSafe(); + if (ObjectUtils.isEmpty(userId)) { + return; + } // 将用户搜索的数据存放到redis中,每晚统一存到数据库中 LoginUser loginUser = SecurityUtils.getLoginUser(); // 获取用户在redis中的搜索数据 @@ -985,4 +1051,13 @@ public class WebsitePCServiceImpl implements IWebsitePCService { redisCache.setCacheObject(CacheConstants.USER_SEARCH_HISTORY + loginUser.getUserId(), userSearchList); } + // 判断是否为中文字符 + private boolean isChineseCharacter(String str) { + if (str == null || str.length() != 1) { + return false; + } + char c = str.charAt(0); + return (c >= '\u4e00' && c <= '\u9fa5'); + } + }