Compare commits

...

2 Commits

Author SHA1 Message Date
3d33a73462 feat(search): 实现笔记搜索功能
- 添加 Elasticsearch 配置,支持通过 ObjectMapper 注入
- 创建笔记索引字段常量类 NoteIndex
- 新增搜索控制器 NoteController,提供搜索接口
- 实现 NoteService 接口及具体业务逻辑 NoteServiceImpl
- 构建 function_score 查询,结合多字段匹配与评分函数
- 支持搜索结果高亮显示及分页处理
- 添加请求参数校验和响应数据格式化逻辑
- 更新 SearchUserRspVO 使用 JsonAlias 替代 JsonProperty
2025-11-01 14:53:28 +08:00
4b13e52a29 feat(search): 实现用户搜索昵称高亮与粉丝数格式化- 添加昵称高亮字段 highlightNickname 到 SearchUserRspVO
- 修改粉丝总数字段类型为 String,支持格式化显示
- 引入 NumberUtils 工具类,实现数字转“万”单位格式
- 配置 Elasticsearch 查询高亮规则,支持昵称关键词高亮
- 新增 mergeHitToRspVO 方法,合并原始数据与高亮结果
- 优化搜索请求构建逻辑,增强可读性与扩展性
2025-11-01 13:50:18 +08:00
11 changed files with 506 additions and 51 deletions

View File

@@ -5,7 +5,7 @@ import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport; import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport; import co.elastic.clients.transport.rest_client.RestClientTransport;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper; import jakarta.annotation.Resource;
import org.apache.http.HttpHost; import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestClient;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@@ -19,6 +19,9 @@ public class ElasticsearchConfig {
@Value("${elasticsearch.host}") @Value("${elasticsearch.host}")
private String host; private String host;
@Resource
private ObjectMapper objectMapper;
@Bean @Bean
public ElasticsearchClient elasticsearchClient() { public ElasticsearchClient elasticsearchClient() {
// 1. 创建底层 RestClient低级客户端 // 1. 创建底层 RestClient低级客户端
@@ -26,12 +29,8 @@ public class ElasticsearchConfig {
.builder(HttpHost.create(host)) .builder(HttpHost.create(host))
.build(); .build();
// 2. 创建 JSON 映射器
ObjectMapper mapper = JsonMapper.builder().build();
JacksonJsonpMapper jsonpMapper = new JacksonJsonpMapper(mapper);
// 3. 构建传输层 // 3. 构建传输层
ElasticsearchTransport transport = new RestClientTransport(restClient, jsonpMapper); ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper(objectMapper));
// 4. 创建高层次的 Elasticsearch 客户端 // 4. 创建高层次的 Elasticsearch 客户端
return new ElasticsearchClient(transport); return new ElasticsearchClient(transport);

View File

@@ -0,0 +1,30 @@
package com.hanserwei.hannote.search.controller;
import com.hanserwei.framework.biz.operationlog.aspect.ApiOperationLog;
import com.hanserwei.framework.common.response.PageResponse;
import com.hanserwei.hannote.search.model.vo.SearchNoteReqVO;
import com.hanserwei.hannote.search.model.vo.SearchNoteRspVO;
import com.hanserwei.hannote.search.service.NoteService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/search")
@Slf4j
public class NoteController {
@Resource
private NoteService noteService;
@PostMapping("/note")
@ApiOperationLog(description = "搜索笔记")
public PageResponse<SearchNoteRspVO> searchNote(@RequestBody @Validated SearchNoteReqVO searchNoteReqVO) {
return noteService.searchNote(searchNoteReqVO);
}
}

View File

@@ -0,0 +1,70 @@
package com.hanserwei.hannote.search.index;
public class NoteIndex {
/**
* 索引名称
*/
public static final String NAME = "note";
/**
* 笔记ID
*/
public static final String FIELD_NOTE_ID = "id";
/**
* 封面
*/
public static final String FIELD_NOTE_COVER = "cover";
/**
* 头像
*/
public static final String FIELD_NOTE_TITLE = "title";
/**
* 话题名称
*/
public static final String FIELD_NOTE_TOPIC = "topic";
/**
* 发布者昵称
*/
public static final String FIELD_NOTE_NICKNAME = "nickname";
/**
* 发布者头像
*/
public static final String FIELD_NOTE_AVATAR = "avatar";
/**
* 笔记类型
*/
public static final String FIELD_NOTE_TYPE = "type";
/**
* 发布时间
*/
public static final String FIELD_NOTE_CREATE_TIME = "create_time";
/**
* 更新时间
*/
public static final String FIELD_NOTE_UPDATE_TIME = "update_time";
/**
* 笔记被点赞数
*/
public static final String FIELD_NOTE_LIKE_TOTAL = "like_total";
/**
* 笔记被收藏数
*/
public static final String FIELD_NOTE_COLLECT_TOTAL = "collect_total";
/**
* 笔记被评论数
*/
public static final String FIELD_NOTE_COMMENT_TOTAL = "comment_total";
}

View File

@@ -0,0 +1,22 @@
package com.hanserwei.hannote.search.model.vo;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SearchNoteReqVO {
@NotBlank(message = "搜索关键词不能为空")
private String keyword;
@Min(value = 1, message = "页码不能小于 1")
private Integer pageNo = 1; // 默认值为第一页
}

View File

@@ -0,0 +1,62 @@
package com.hanserwei.hannote.search.model.vo;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
public class SearchNoteRspVO {
/**
* 笔记ID
*/
@JsonAlias("id")
private Long noteId;
/**
* 封面
*/
private String cover;
/**
* 标题
*/
private String title;
/**
* 标题:关键词高亮
*/
private String highlightTitle;
/**
* 发布者头像
*/
private String avatar;
/**
* 发布者昵称
*/
private String nickname;
/**
* 最后一次编辑时间
*/
@JsonAlias("update_time")
private LocalDateTime updateTime;
/**
* 被点赞总数
*/
@JsonAlias("like_total")
private String likeTotal;
}

View File

@@ -1,5 +1,6 @@
package com.hanserwei.hannote.search.model.vo; package com.hanserwei.hannote.search.model.vo;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@@ -25,6 +26,11 @@ public class SearchUserRspVO {
*/ */
private String nickname; private String nickname;
/**
* 昵称:关键词高亮
*/
private String highlightNickname;
/** /**
* 头像 * 头像
*/ */
@@ -33,19 +39,19 @@ public class SearchUserRspVO {
/** /**
* 小憨书ID * 小憨书ID
*/ */
@JsonProperty("han_note_id") @JsonAlias("han_note_id")
private String hanNoteId; private String hanNoteId;
/** /**
* 笔记发布总数 * 笔记发布总数
*/ */
@JsonProperty("note_total") @JsonAlias("note_total")
private Integer noteTotal; private Integer noteTotal;
/** /**
* 粉丝总数 * 粉丝总数
*/ */
@JsonProperty("fans_total") @JsonAlias("fans_total")
private Integer fansTotal; private String fansTotal;
} }

View File

@@ -0,0 +1,16 @@
package com.hanserwei.hannote.search.service;
import com.hanserwei.framework.common.response.PageResponse;
import com.hanserwei.hannote.search.model.vo.SearchNoteReqVO;
import com.hanserwei.hannote.search.model.vo.SearchNoteRspVO;
public interface NoteService {
/**
* 搜索笔记
*
* @param searchNoteReqVO 搜索笔记请求
* @return 搜索笔记响应
*/
PageResponse<SearchNoteRspVO> searchNote(SearchNoteReqVO searchNoteReqVO);
}

View File

@@ -0,0 +1,157 @@
package com.hanserwei.hannote.search.service.impl;
import cn.hutool.core.collection.CollUtil;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.query_dsl.*;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.HighlightField;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.util.NamedValue;
import com.hanserwei.framework.common.response.PageResponse;
import com.hanserwei.framework.common.utils.NumberUtils;
import com.hanserwei.hannote.search.index.NoteIndex;
import com.hanserwei.hannote.search.model.vo.SearchNoteReqVO;
import com.hanserwei.hannote.search.model.vo.SearchNoteRspVO;
import com.hanserwei.hannote.search.service.NoteService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@Service
@Slf4j
public class NoteServiceImpl implements NoteService {
@Resource
private ElasticsearchClient client;
@Override
public PageResponse<SearchNoteRspVO> searchNote(SearchNoteReqVO searchNoteReqVO) {
// 查询关键词
String keyword = searchNoteReqVO.getKeyword();
// 当前页码
Integer pageNo = searchNoteReqVO.getPageNo();
int pageSize = 10; // 每页展示数据量
int from = (pageNo - 1) * pageSize; // 偏移量
// 1. 构建基础 Multi-Match Query (原始查询条件)
MultiMatchQuery multiMatchQuery = MultiMatchQuery.of(m -> m
.query(keyword)
// 字段权重设置,与 RHL 的 .field(field, boost) 对应
.fields(NoteIndex.FIELD_NOTE_TITLE + "^2.0", NoteIndex.FIELD_NOTE_TOPIC)
);
Query functionScoreQuery = Query.of(q -> q
.functionScore(fs -> fs
// 设置初始查询条件
.query(innerQuery -> innerQuery.multiMatch(multiMatchQuery))
.scoreMode(FunctionScoreMode.Sum)
.boostMode(FunctionBoostMode.Sum)
// 设置function数组
.functions(List.of(
// 评分函数1
FunctionScore.of(f -> f.fieldValueFactor(fvf -> fvf
.field(NoteIndex.FIELD_NOTE_LIKE_TOTAL)
.factor(0.5)
.modifier(FieldValueFactorModifier.Sqrt)
.missing(0.0))),
// 评分函数2
FunctionScore.of(f -> f.fieldValueFactor(fvf -> fvf
.field(NoteIndex.FIELD_NOTE_COLLECT_TOTAL)
.factor(0.3)
.modifier(FieldValueFactorModifier.Sqrt)
.missing(0.0))),
// 评分函数3
FunctionScore.of(f -> f.fieldValueFactor(fvf -> fvf
.field(NoteIndex.FIELD_NOTE_COMMENT_TOTAL)
.factor(0.2)
.modifier(FieldValueFactorModifier.Sqrt)
.missing(0.0)))
))
)
);
// 3. 构建 Highlight 配置
HighlightField titleHighlight = HighlightField.of(hf -> hf
.preTags("<strong>")
.postTags("</strong>")
);
// 4. 构建最终的 SearchRequest
SearchRequest searchRequest = SearchRequest.of(r -> r
.index(NoteIndex.NAME)
.query(functionScoreQuery) // 设置 function_score 查询
// 排序:按 _score 降序
.sort(s -> s.score(d -> d.order(SortOrder.Desc)))
// 分页
.from(from)
.size(pageSize)
// 高亮
.highlight(h -> h
.fields(NamedValue.of(NoteIndex.FIELD_NOTE_TITLE, titleHighlight))
)
);
// 返参 VO 集合
List<SearchNoteRspVO> searchNoteRspVOS = null;
long total = 0;
try {
log.info("==> NoteSearchRequest: {}", searchRequest.toString());
// ⭐️ 执行查询请求,并自动反序列化文档源到 SearchNoteRspVO
SearchResponse<SearchNoteRspVO> searchResponse =
client.search(searchRequest, SearchNoteRspVO.class);
total = searchResponse.hits().total() != null ? searchResponse.hits().total().value() : 0;
log.info("==> 命中文档总数, hits: {}", total);
// ⭐️ 处理搜索结果:合并原始文档和高亮数据
searchNoteRspVOS = searchResponse.hits().hits().stream()
.map(this::processNoteHit)
.filter(Objects::nonNull)
.collect(Collectors.toList());
} catch (Exception e) {
log.error("==> 查询 Elasticsearch 异常: ", e);
}
return PageResponse.success(searchNoteRspVOS, pageNo, total);
}
/**
* 辅助方法:处理 Hit合并 Source 和 Highlight 数据
*/
private SearchNoteRspVO processNoteHit(Hit<SearchNoteRspVO> hit) {
SearchNoteRspVO rspVO = hit.source();
if (rspVO == null) {
return null;
}
// 2. ⭐️ 处理高亮字段
Map<String, List<String>> highlights = hit.highlight();
if (CollUtil.isNotEmpty(highlights)) {
List<String> titleHighlights = highlights.get(NoteIndex.FIELD_NOTE_TITLE);
if (CollUtil.isNotEmpty(titleHighlights)) {
// 设置高亮标题
rspVO.setHighlightTitle(titleHighlights.getFirst());
}
}
// 3. 确保 highlightTitle 有值 (如果没有高亮结果,使用原始 title)
if (rspVO.getHighlightTitle() == null) {
rspVO.setHighlightTitle(rspVO.getTitle());
}
// 4. 处理特殊格式化(如 RHL 代码中的点赞数格式化)
if (rspVO.getLikeTotal() != null) {
rspVO.setLikeTotal(NumberUtils.formatNumberString(Long.parseLong(rspVO.getLikeTotal())));
}
return rspVO;
}
}

View File

@@ -5,9 +5,12 @@ import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.query_dsl.TextQueryType; import co.elastic.clients.elasticsearch._types.query_dsl.TextQueryType;
import co.elastic.clients.elasticsearch.core.SearchRequest; import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse; import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.HighlightField;
import co.elastic.clients.elasticsearch.core.search.Hit; import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.util.NamedValue;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.hanserwei.framework.common.response.PageResponse; import com.hanserwei.framework.common.response.PageResponse;
import com.hanserwei.framework.common.utils.NumberUtils;
import com.hanserwei.hannote.search.index.UserIndex; import com.hanserwei.hannote.search.index.UserIndex;
import com.hanserwei.hannote.search.model.vo.SearchUserReqVO; import com.hanserwei.hannote.search.model.vo.SearchUserReqVO;
import com.hanserwei.hannote.search.model.vo.SearchUserRspVO; import com.hanserwei.hannote.search.model.vo.SearchUserRspVO;
@@ -17,6 +20,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.List; import java.util.List;
import java.util.Map;
@Slf4j @Slf4j
@Service @Service
@@ -25,80 +29,130 @@ public class UserServiceImpl implements UserService {
@Resource @Resource
private ElasticsearchClient client; private ElasticsearchClient client;
/**
* 获取 SearchUserRspVO
*
* @param hit 搜索结果
* @return SearchUserRspVO
*/
private static SearchUserRspVO getSearchUserRspVO(Hit<SearchUserRspVO> hit) {
SearchUserRspVO searchUserRspVO = new SearchUserRspVO();
SearchUserRspVO source = hit.source();
if (source != null) {
searchUserRspVO.setUserId(source.getUserId());
searchUserRspVO.setNickname(source.getNickname());
searchUserRspVO.setAvatar(source.getAvatar());
searchUserRspVO.setHanNoteId(source.getHanNoteId());
searchUserRspVO.setNoteTotal(source.getNoteTotal());
searchUserRspVO.setFansTotal(source.getFansTotal());
}
return searchUserRspVO;
}
@Override @Override
public PageResponse<SearchUserRspVO> searchUser(SearchUserReqVO searchUserReqVO) { public PageResponse<SearchUserRspVO> searchUser(SearchUserReqVO searchUserReqVO) {
// 查询关键 // 查询关键
String keyword = searchUserReqVO.getKeyword(); String keyword = searchUserReqVO.getKeyword();
// 当前页码 // 当前页码
Integer pageNo = searchUserReqVO.getPageNo(); Integer pageNo = searchUserReqVO.getPageNo();
int pageSize = 10; int pageSize = 10; // 每页展示数据量
int from = (pageNo - 1) * pageSize; // 偏移量
// 构建SearchRequest指定索引 HighlightField nicknameHighlight = HighlightField.of(hf -> hf
SearchRequest searchRequest = new SearchRequest.Builder() .preTags("<strong>")
.postTags("</strong>")
);
SearchRequest searchRequest = SearchRequest.of(r -> r
.index(UserIndex.NAME) .index(UserIndex.NAME)
.query(query -> query
.multiMatch(multiMatch -> multiMatch // 1. 构建 Query: multiMatchQuery (RHL 风格的匹配)
.query(q -> q
.multiMatch(m -> m
.query(keyword) .query(keyword)
.fields(UserIndex.FIELD_USER_NICKNAME, UserIndex.FIELD_USER_HAN_NOTE_ID) .fields(UserIndex.FIELD_USER_NICKNAME, UserIndex.FIELD_USER_HAN_NOTE_ID)
.type(TextQueryType.PhrasePrefix))) // 默认使用 MatchQuery 行为,如果要模糊匹配,请添加 .fuzziness("AUTO")
.sort(sort -> sort .type(TextQueryType.PhrasePrefix)
.field(filedSort -> filedSort.field(UserIndex.FIELD_USER_FANS_TOTAL).order(SortOrder.Desc))) )
.from((pageNo - 1) * pageSize) )
// 2. 构建 Sort
.sort(s -> s
.field(f -> f
.field(UserIndex.FIELD_USER_FANS_TOTAL)
.order(SortOrder.Desc)
)
)
.highlight(h -> h.fields(NamedValue.of(UserIndex.FIELD_USER_NICKNAME, nicknameHighlight)))
// 3. 分页 from 和 size
.from(from)
.size(pageSize) .size(pageSize)
.build(); );
// 返参 VO 集合 // 返参 VO 集合
List<SearchUserRspVO> searchUserRspVOS = null; List<SearchUserRspVO> searchUserRspVOS = Lists.newArrayList();
// 总文档数,默认为 0 // 总文档数,默认为 0
long total = 0; long total = 0;
try { try {
log.info("==> SearchRequest:{}", searchRequest); log.info("==> SearchRequest: {}", searchRequest.toString());
// 执行查询请求 // 执行查询请求
SearchResponse<SearchUserRspVO> searchResponse = client.search(searchRequest, SearchUserRspVO.class); SearchResponse<SearchUserRspVO> searchResponse = client.search(searchRequest, SearchUserRspVO.class);
searchUserRspVOS = Lists.newArrayList();
// 处理搜索结果 // 处理搜索结果
List<Hit<SearchUserRspVO>> hits = searchResponse.hits().hits(); List<Hit<SearchUserRspVO>> hits = searchResponse.hits().hits();
if (searchResponse.hits().total() != null) { if (searchResponse.hits().total() != null) {
total = searchResponse.hits().total().value(); total = searchResponse.hits().total().value();
} }
searchUserRspVOS = Lists.newArrayList();
for (Hit<SearchUserRspVO> hit : hits) { for (Hit<SearchUserRspVO> hit : hits) {
log.info("==> 文档数据: {}", hit.toString()); // 1. 获取原始文档数据 (source)
if (hit.source() != null) { SearchUserRspVO source = hit.source();
SearchUserRspVO searchUserRspVO = getSearchUserRspVO(hit);
// 2. 获取高亮数据 (highlight)
Map<String, List<String>> highlights = hit.highlight();
if (source != null) {
// 3. 调用辅助方法合并数据和高亮
SearchUserRspVO searchUserRspVO = mergeHitToRspVO(source, highlights);
searchUserRspVOS.add(searchUserRspVO); searchUserRspVOS.add(searchUserRspVO);
} }
} }
} catch (Exception e) { } catch (Exception e) {
log.error("==> 查询 Elasticsearch 异常: ", e); log.error("==> 查询 Elasticsearch 异常: ", e);
} }
return PageResponse.success(searchUserRspVOS, pageNo, total); return PageResponse.success(searchUserRspVOS, pageNo, total);
} }
/**
* 将原始文档和高亮数据合并到 SearchUserRspVO
*
* @param source 原始文档数据 (已自动反序列化)
* @param highlights 高亮数据 Map
* @return SearchUserRspVO
*/
private SearchUserRspVO mergeHitToRspVO(SearchUserRspVO source, Map<String, List<String>> highlights) {
if (source == null) {
return null;
}
// 1. 复制原始文档字段 (假设 SearchUserRspVO 使用 Lombok @Data 或 Builder)
SearchUserRspVO searchUserRspVO = SearchUserRspVO.builder()
.userId(source.getUserId())
.nickname(source.getNickname())
.avatar(source.getAvatar())
.hanNoteId(source.getHanNoteId()) // 字段名应与您的 VO 保持一致
.noteTotal(source.getNoteTotal())
.build();
if (source.getFansTotal() != null) {
searchUserRspVO.setFansTotal(NumberUtils.formatNumberString(Long.parseLong(source.getFansTotal())));
}
// 2. ⭐️ 核心逻辑:处理并设置高亮字段
if (highlights != null) {
// 尝试从 highlights Map 中获取 nickname 字段的高亮结果
List<String> nicknameHighlights = highlights.get(UserIndex.FIELD_USER_NICKNAME);
if (nicknameHighlights != null && !nicknameHighlights.isEmpty()) {
searchUserRspVO.setHighlightNickname(nicknameHighlights.getFirst());
}
}
// 3. 如果高亮字段为空,默认使用原始 nickname
if (searchUserRspVO.getHighlightNickname() == null) {
searchUserRspVO.setHighlightNickname(source.getNickname());
}
return searchUserRspVO;
}
} }

View File

@@ -0,0 +1,39 @@
package com.hanserwei.framework.common.utils;
import java.math.RoundingMode;
import java.text.DecimalFormat;
public class NumberUtils {
/**
* 数字转换字符串
*
* @param number 数字
* @return 字符串
*/
public static String formatNumberString(long number) {
if (number < 10000) {
return String.valueOf(number); // 小于 1 万显示原始数字
} else if (number < 100000000) {
// 小于 1 亿,显示万单位
double result = number / 10000.0;
DecimalFormat df = new DecimalFormat("#.#"); // 保留 1 位小数
df.setRoundingMode(RoundingMode.DOWN); // 禁用四舍五入
String formatted = df.format(result);
return formatted + "";
} else {
return "9999万"; // 超过 1 亿,统一显示 9999万
}
}
public static void main(String[] args) {
// 测试
System.out.println(formatNumberString(1000)); // 1000
System.out.println(formatNumberString(11130)); // 1.1万
System.out.println(formatNumberString(26719300)); // 2671.9万
System.out.println(formatNumberString(10000000)); // 1000万
System.out.println(formatNumberString(999999)); // 99.9万
System.out.println(formatNumberString(150000000)); // 超过一亿展示9999万
System.out.println(formatNumberString(99999)); // 9.9万
}
}

View File

@@ -285,6 +285,6 @@ POST http://localhost:8092/search/user
Content-Type: application/json Content-Type: application/json
{ {
"keyword": "憨", "keyword": "憨",
"pageNo": 1 "pageNo": 1
} }