# digital_map **Repository Path**: chd/digital_map ## Basic Information - **Project Name**: digital_map - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-09 - **Last Updated**: 2026-08-25 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # QTBASE #### 介绍 qtbase #### 软件架构 软件架构说明 #### 功能描述 - [x] 统一异常处理 - [x] 日志操作地址 - [x] Graceful Response - [x] 注解重试机制 - [x] 防刷 - [x] 重复提交 - [x] 数据库敏感数据过滤 - [x] 日志 - [x] gzip - [x] 限流 - [x] 定时任务 - [x] ip解析 - [x] 异步线程 - [x] Graceful Response 数据验证 - [x] @WebFilter 路径配置 - [x] 自动填充 - [x] 授权 - [x] 数据库定时备份 - [x] 操作日志 - [x] 通用搜索 - [x] 敏感词过滤 - [x] xss过滤 - [x] 数据版本 - [x] 接口幂等性 (通过数据版本控制,通过唯一主键控制,通过token控制) - [x] 接口加密 (https://gitee.com/isuperag/rsa-encrypt-body-spring-boot https://github.com/ishuibo/rsa-encrypt-body-spring-boot) - [x] jar 加密 (https://gitee.com/roseboy/classfinal) - [x] 缓存(按需添加) - [x] mybatis 通用枚举 #### 安装教程 1. xxxx 2. xxxx 3. xxxx #### 使用说明 1. xxxx 2. xxxx 3. xxxx #### 参与贡献 1. Fork 本仓库 2. 新建 Feat_xxx 分支 3. 提交代码 4. 新建 Pull Request #### 特技 ``` package cn.shujuhai.qtbase.platform.controller.general; import cn.hutool.core.text.CharSequenceUtil; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import java.util.Map; /** * PageController * * @author dch798 {@code @date} 2021/09/29 21:38 **/ @Slf4j @Controller public class GeneralPageController { /** * @param path 页面 * @return 视图 */ /* @GetMapping(value = {"/{root:^(?!lib|error|platform|favicon).*}/{*path}", "/{root:^(?!lib|error|platform|favicon).*}/{folder}/{*path}", "/"}) public String showPage(@PathVariable(value = "root", required = false) String root, @PathVariable(value = "folder", required = false) String folder, @PathVariable(value = "path", required = false) String path, @RequestParam(required = false) Map parms, @NotNull ModelMap result) { result.mergeAttributes(parms); path = (CharSequenceUtil.isNotBlank(root) ? root : "") + (CharSequenceUtil.isNotBlank(folder) ? folder : "") + (CharSequenceUtil.isNotBlank(path) ? path : ""); path = CharSequenceUtil.isNotBlank(path) ? path : "redirect:/general/sign-in"; log.debug("get path:" + path + "======================================================="); return path; }*/ @GetMapping(value = {"/page}/{*path}", "/page/{folder}/{*path}", "/"}) public String showPage(@PathVariable(value = "folder", required = false) String folder, @PathVariable(value = "path", required = false) String path, @RequestParam(required = false) Map parms, @NotNull ModelMap result) { result.mergeAttributes(parms); path = (CharSequenceUtil.isNotBlank(folder) ? folder : "") + (CharSequenceUtil.isNotBlank(path) ? path : ""); path = CharSequenceUtil.isNotBlank(path) ? path : "redirect:/page/general/sign-in"; log.debug("get path:" + path + "======================================================="); return path; } } ``` ``` package cn.shujuhai.qtbase.platform.controller.general; import cn.hutool.core.text.CharSequenceUtil; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import java.util.Map; /** * PageController * * @author dch798 {@code @date} 2021/09/29 21:38 **/ @Slf4j @Controller public class GeneralPageController { /** * @param path 页面 * @return 视图 */ @GetMapping(value = {"/page/{*path}", "/"}) public String showPage(@PathVariable(value = "path", required = false) String path, @RequestParam(required = false) Map parms, @NotNull ModelMap result) { result.mergeAttributes(parms); path = (CharSequenceUtil.isNotBlank(path) ? path : ""); path = CharSequenceUtil.isNotBlank(path) ? "page" + path : "redirect:/page/general/sign-in"; log.debug("get path:" + path + "======================================================="); return path; } } ``` ``` package cn.shujuhai.qtbase.platform.controller.general; import cn.hutool.core.text.CharSequenceUtil; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import java.util.Map; /** * PageController * * @author dch798 {@code @date} 2021/09/29 21:38 **/ @Slf4j @Controller public class GeneralPageController { /** * @param path 页面 * @return 视图 */ @GetMapping(value = {"/page/{*path}", "/"}) public String showPage(@PathVariable(value = "path", required = false) String path, @RequestParam(required = false) Map parms, @NotNull ModelMap result) { result.mergeAttributes(parms); path = (CharSequenceUtil.isNotBlank(path) ? path : ""); path = CharSequenceUtil.isNotBlank(path) ? "page" + path : "redirect:/page/general/sign-in"; log.debug("get path:" + path + "======================================================="); return path; } } ``` XssFilter ``` package cn.shujuhai.qtbase.platform.pluging.xss; import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.util.BooleanUtil; import jakarta.servlet.*; import jakarta.servlet.annotation.WebFilter; import jakarta.servlet.annotation.WebInitParam; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.springframework.core.annotation.Order; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author dch */ @Slf4j @Order(1) @WebFilter(urlPatterns = {"/page/*","/user/*","/admin/*"},initParams = {@WebInitParam(name = "isIncludeRichText", value = "true"), @WebInitParam(name = "excludes", value = "")}) public class XssFilter implements Filter { private final List excludes = new ArrayList<>(); private boolean richText = true; @Override public void doFilter(ServletRequest request, ServletResponse response, @NotNull FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; if (handleExcludeUrl(req)) { filterChain.doFilter(request, response); return; } XssHttpServletRequestWraper xssRequest = new XssHttpServletRequestWraper((HttpServletRequest) request, richText); filterChain.doFilter(xssRequest, response); } private boolean handleExcludeUrl(@NotNull HttpServletRequest request) { if (excludes.isEmpty()) { return false; } String url = request.getServletPath(); for (String pattern : excludes) { Pattern p = Pattern.compile("^" + pattern); Matcher m = p.matcher(url); if (m.find()) { return true; } } return false; } @Override public void init(@NotNull FilterConfig filterConfig) { String isIncludeRichText = filterConfig.getInitParameter("isIncludeRichText"); if (CharSequenceUtil.isNotBlank(isIncludeRichText)) { richText = BooleanUtil.toBoolean(isIncludeRichText); } String temp = filterConfig.getInitParameter("excludes"); if (temp != null) { String[] url = temp.split(","); excludes.addAll(Arrays.asList(url)); } } @Override public void destroy() { log.debug("xss filter destroy~~~~~~~~~~~~"); } } ``` ``` spring: mvc: static-path-pattern: /res/** web: resources: static-locations: classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,file:${file.dir.upload} ``` | 标签名 | 作用 | 示例 | |-------------|----------------------------------------------|-----------------------------------------------| | th:fragment | 布局标签,定义一个代码片段,方便其它地方引用 | ```
``` | | th:include | 保留自己的主标签,不保留th:fragment的主标签 | ```
``` | | th:replace | 不保留自己的主标签,保留th:fragment的主标签 | ```
``` | | th:insert | 保留自己的主标签,保留th:fragment的主标签 | ```
``` | 帮我实现advancedSearchByView方法,该方法的功能是实现一个多条件的高级检索功能,userAdvancedSearch是前端传过来的查询条件,其中key是查询关键词集合,val是查询值的集合,key和val之间的关系是eq的关系,condition是第一个查询条件和第二个查询条件之间的连接条件,取值有与、或、非,查询条件的构成如下: key[0]eq val[0] condition[1] key[1]eq val[1] condition[2] key[2]eq val[2] 依次类推,mediaType是要查询的资源类型的集合,所有数据都存储在VUserResourceData对应的视图中,userAdvancedSearch中的key对应VUserResourceData中的各个字段,userAdvancedSearch中的val对应VUserResourceData中的各字段的值,userAdvancedSearch中的mediaType对应VUserResourceData的type字段。请用mybatis flex 的查询方式实现advancedSearchByView方法,期望的结果是 (key[0]eq val[0] condition[1] key[1]eq val[1] condition[2] key[2]eq val[2]) and (type in mediaType) and publishDate between startDate and endDate ``` import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.builder.SearchSourceBuilder; import java.io.IOException; public class ElasticsearchQueryExample { private final RestHighLevelClient client; public ElasticsearchQueryExample(RestHighLevelClient client) { this.client = client; } public void executeQuery(String indexName, BoolQueryBuilder boolQueryBuilder) throws IOException { SearchRequest searchRequest = new SearchRequest(indexName); SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); sourceBuilder.query(boolQueryBuilder); searchRequest.source(sourceBuilder); SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT); System.out.println(searchResponse); } public static void main(String[] args) throws IOException { RestHighLevelClient client = new RestHighLevelClient(/* your client configuration */); ElasticsearchQueryExample example = new ElasticsearchQueryExample(client); // 示例:只有 and BoolQueryBuilder andQueryBuilder = QueryBuilders.boolQuery() .must(QueryBuilders.matchQuery("field1", "value1")) .must(QueryBuilders.matchQuery("field2", "value2")); example.executeQuery("your_index_name", andQueryBuilder); // 示例:只有 or BoolQueryBuilder orQueryBuilder = QueryBuilders.boolQuery() .should(QueryBuilders.matchQuery("field1", "value1")) .should(QueryBuilders.matchQuery("field2", "value2")) .minimumShouldMatch(1); example.executeQuery("your_index_name", orQueryBuilder); // 示例:只有 not BoolQueryBuilder notQueryBuilder = QueryBuilders.boolQuery() .mustNot(QueryBuilders.matchQuery("field1", "value1")); example.executeQuery("your_index_name", notQueryBuilder); // 示例:and 和 or BoolQueryBuilder andOrQueryBuilder = QueryBuilders.boolQuery() .must(QueryBuilders.boolQuery() .must(QueryBuilders.matchQuery("field1", "value1")) .must(QueryBuilders.matchQuery("field2", "value2")) ) .should(QueryBuilders.matchQuery("field3", "value3")) .minimumShouldMatch(1); example.executeQuery("your_index_name", andOrQueryBuilder); // 示例:and 和 not BoolQueryBuilder andNotQueryBuilder = QueryBuilders.boolQuery() .must(QueryBuilders.boolQuery() .must(QueryBuilders.matchQuery("field1", "value1")) .must(QueryBuilders.matchQuery("field2", "value2")) ) .mustNot(QueryBuilders.matchQuery("field3", "value3")); example.executeQuery("your_index_name", andNotQueryBuilder); // 示例:or 和 not BoolQueryBuilder orNotQueryBuilder = QueryBuilders.boolQuery() .should(QueryBuilders.matchQuery("field1", "value1")) .should(QueryBuilders.matchQuery("field2", "value2")) .minimumShouldMatch(1) .mustNot(QueryBuilders.matchQuery("field3", "value3")); example.executeQuery("your_index_name", orNotQueryBuilder); // 示例:and、or 和 not BoolQueryBuilder andOrNotQueryBuilder = QueryBuilders.boolQuery() .must(QueryBuilders.boolQuery() .must(QueryBuilders.matchQuery("field1", "value1")) .must(QueryBuilders.matchQuery("field2", "value2")) ) .should(QueryBuilders.matchQuery("field3", "value3")) .minimumShouldMatch(1) .mustNot(QueryBuilders.matchQuery("field4", "value4")); example.executeQuery("your_index_name", andOrNotQueryBuilder); client.close(); } } ``` 解释 单一条件: 只有 and:使用 must 子句来组合多个条件。 只有 or:使用 should 子句来组合多个条件,并设置 minimumShouldMatch 为 1 以确保至少匹配一个条件。 只有 not:使用 mustNot 子句来排除特定条件。 两两组合: and 和 or:使用嵌套的 bool 查询来组合 and 和 or 条件。 and 和 not:使用 must 子句来组合 and 条件,并使用 mustNot 子句来排除特定条件。 or 和 not:使用 should 子句来组合 or 条件,并使用 mustNot 子句来排除特定条件。 三者组合: and、or 和 not:使用嵌套的 bool 查询来组合 and 和 or 条件,并使用 mustNot 子句来排除特定条件。 通过这种方式,可以灵活地构建复杂的查询条件,确保所有组合都能正确处理。 es查询方法: 1. spring data es 2. RestHighLevelClient 3. restful API 4. script 5. queryString 6. search template 7. eazy-es 8. eazy-es 混合查询 9. eazy-es 原生查询 ```java package cn.shujuhai.tsingtu.platform.service.impl; import cn.hutool.core.util.ObjectUtil; import cn.shujuhai.tsingtu.platform.domain.EsDocument; import cn.shujuhai.tsingtu.platform.domain.UserAdvancedSearch; import cn.shujuhai.tsingtu.platform.kit.EsDocumentConverter; import cn.shujuhai.tsingtu.platform.mapperes.DocumentMapper; import cn.shujuhai.tsingtu.platform.service.UserEsSearchService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.dromara.easyes.common.enums.Link; import org.dromara.easyes.common.enums.Query; import org.dromara.easyes.common.params.SFunction; import org.dromara.easyes.core.biz.EsPageInfo; import org.dromara.easyes.core.conditions.select.LambdaEsQueryWrapper; import org.dromara.easyes.core.kernel.EsWrappers; import org.dromara.easyes.core.toolkit.QueryUtils; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.MatchQueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import static cn.shujuhai.tsingtu.platform.kit.Constant.SEARCH_KEY_MAP; /** * 存储期刊内容的特定信息 服务层实现。 * * @author dch * @since 1.0 */ @Slf4j @Service @RequiredArgsConstructor public class UserEsSearchServiceImpl implements UserEsSearchService { private final DocumentMapper documentMapper; private final RestHighLevelClient restHighLevelClient; @Value("${file.dir.upload}") private String upload; /** * 根据给定的键获取对应的EsDocument类的函数接口 * 该方法用于动态确定基于传入的字符串键,应该访问EsDocument对象的哪个属性 * * @param key 搜索键,用于确定返回哪个属性的getter方法,如"title"对应标题 * @return SFunction 返回一个函数接口,用于获取EsDocument的指定属性 * @throws IllegalArgumentException 如果传入的键不合法,抛出此异常 */ private static SFunction getFunc(String key) { // 根据不同的键值,返回对应的函数接口实现 return switch (key.toLowerCase()) { case "title" -> EsDocument::getTitle; case "summary" -> EsDocument::getSummary; case "keyword" -> EsDocument::getKeyword; case "author" -> EsDocument::getAuthor; case "author_affiliation" -> EsDocument::getAuthorAffiliation; case "bibliography" -> EsDocument::getBibliography; case "content" -> EsDocument::getContent; case "file_content" -> EsDocument::getFileContent; // 如果键值不匹配任何预定义的属性,抛出异常 default -> throw new IllegalArgumentException("Invalid search key: " + key); }; } /** * 保存文档到 Elasticsearch *

* 该方法接收一个参数对象,将其转换为EsDocument实体,并插入到Elasticsearch数据库中 * 主要用于在系统中创建新的文档记录 * * @param parameterObject 包含文档信息的参数对象,具体结构和内容由EsDocumentConverter类处理 * @return 返回插入操作的影响行数,通常为1表示成功,0表示失败 */ @Override public Integer saveEsDocument(Object parameterObject) { EsDocument documentEntity = EsDocumentConverter.getDocumentEntity(parameterObject, this.upload); return this.documentMapper.insert(documentEntity); } /** * 更新Elasticsearch文档信息 *

* 该方法接收一个参数对象,将其转换为EsDocument实体,并更新到Elasticsearch中 * 主要用于当资源信息发生变化时,同步更新搜索索引 * * @param parameterObject 包含更新信息的参数对象,具体类型取决于转换逻辑 * @return 返回更新影响的文档数量 */ @Override public Integer updateEsDocument(Object parameterObject) { // 将参数对象转换为EsDocument实体,upload标志指示是否上传文件 EsDocument documentEntity = EsDocumentConverter.getDocumentEntity(parameterObject, this.upload); // 执行更新操作,仅更新与给定资源ID和类型匹配的文档 return this.documentMapper.update(documentEntity, EsWrappers.lambdaChainUpdate(documentMapper).eq(EsDocument::getResId, documentEntity.getResId()).eq(EsDocument::getType, documentEntity.getType())); } /** * 删除Elasticsearch文档 *

* 该方法用于根据给定的参数对象和方法名来删除Elasticsearch文档它首先将参数对象转换为参数映射, * 然后使用该映射中的参数来构建查询条件,最后执行删除操作 * * @param parameterObject 包含删除所需参数的对象,具体结构未知,因此需要转换为映射 * @param methodName 用于确定文档类型的方法名,基于方法名来转换获取文档类型 * @return 返回删除的文档数量 */ @Override public Integer deleteEsDocument(Object parameterObject, String methodName) { // 将参数对象转换为参数映射,以便于后续的查询条件构建 @SuppressWarnings("unchecked") Map parameterMap = (Map) parameterObject; // 执行删除操作,使用lambda链式查询构建器来构建查询条件 // 查询条件包括资源ID和文档类型,其中资源ID从参数映射中获取,文档类型通过方法名转换获取 return this.documentMapper.delete(EsWrappers.lambdaChainQuery(documentMapper) .eq(EsDocument::getResId, parameterMap.get("param1")) .eq(EsDocument::getType, EsDocumentConverter.getTypeByMethodName(methodName))); } /** * 执行多字段模糊搜索的函数 *

* 该函数使用给定的关键词在多个字段中进行模糊搜索,并返回匹配的文档列表 * 它还允许指定页码和页面大小,以便进行分页查询 * * @param keyword 搜索关键词 * @param pageNumber 页码 * @param pageSize 页面大小 * @return 包含匹配文档的EsPageInfo对象 */ @Override public EsPageInfo multiFieldFuzzySearch(String keyword, Integer pageNumber, Integer pageSize) { LambdaEsQueryWrapper wrapper = new LambdaEsQueryWrapper<>(); // 从所有字段中查询包含关键词的文档 wrapper.queryStringQuery(keyword); return documentMapper.pageQuery(wrapper, pageNumber, pageSize); } /** * 执行高级搜索查询 *

* 该方法根据用户提供的高级搜索条件,构建查询条件并执行搜索返回分页结果 * 它处理包括关键字、值、条件的匹配,时间范围过滤,以及类型过滤等 * * @param userAdvancedSearch 包含用户高级搜索条件的对象,包括关键字、值、条件等 * @param pageNumber 分页查询的页码 * @param pageSize 每页显示的结果数量 * @return 返回一个EsPageInfo对象,包含查询结果和分页信息 * @throws IllegalArgumentException 如果提供的关键字、值或它们的数量不匹配,抛出此异常 */ @Override public EsPageInfo esAdvancedSearch(UserAdvancedSearch userAdvancedSearch, Integer pageNumber, Integer pageSize) { LambdaEsQueryWrapper wrapper = new LambdaEsQueryWrapper<>(); List keys = userAdvancedSearch.getKey(); List values = userAdvancedSearch.getVal(); List conditions = userAdvancedSearch.getCondition(); // 边界条件检查 if (keys == null || values == null || keys.size() != values.size()) { throw new IllegalArgumentException("Keys, values, and size mismatch or null"); } // 构建查询条件 buildQueryConditionsWithEeAndEs(wrapper, keys, values, conditions); // 处理时间范围 // 处理时间范围 if (userAdvancedSearch.getStartTime() != null && userAdvancedSearch.getEndTime() != null) { wrapper.between(EsDocument::getPublishDate, userAdvancedSearch.getStartTime(), userAdvancedSearch.getEndTime()); } else if (userAdvancedSearch.getStartTime() != null || userAdvancedSearch.getEndTime() != null) { throw new IllegalArgumentException("Both start time and end time must be provided"); } // 处理类型过滤 List mediaTypes = userAdvancedSearch.splitMediaTypes(); if (ObjectUtil.isNotEmpty(mediaTypes)) { mediaTypes = mediaTypes.stream().filter(type -> type != null && !type.trim().isEmpty()).collect(Collectors.toList()); if (!mediaTypes.isEmpty()) { wrapper.in(EsDocument::getType, mediaTypes); } } log.info("Executing advanced search with parameters: pageNumber={}, pageSize={}", pageNumber, pageSize); return documentMapper.pageQuery(wrapper, pageNumber, pageSize); } /** * 根据给定的参数构建查询条件 * 此方法用于根据Eazy-Es相关的查询条件来构建查询包装器 * 它通过结合关键字、值和逻辑条件来定制查询 * 用eazy-es 查询的方式构建查询条件 * @param wrapper LambdaEsQueryWrapper类型的查询包装器,用于构建查询条件 * @param keys 一个包含搜索关键字的列表,这些关键字用于在文档中查找匹配项 * @param values 一个与关键字相对应的值的列表,用于匹配关键字 * @param conditions 一个包含逻辑条件(如"and"、"or"、"not")的列表,用于组合查询条件 */ private void buildQueryConditionsByEe(LambdaEsQueryWrapper wrapper, List keys, List values, List conditions) { wrapper.and(w -> { if (ObjectUtil.isEmpty(conditions)) { w.match(SEARCH_KEY_MAP.get(keys.getFirst()), values.getFirst()); } else { String condition; for (int i = 0; i < keys.size(); i++) { String key = SEARCH_KEY_MAP.get(keys.get(i)); String val = values.get(i); condition = i == 0 ? conditions.get(i + 1) : conditions.get(i); // 默认为 "and" switch (condition.toLowerCase()) { case "and": w.must(w1 -> w1.match(key, val)); break; case "or": w.or().match(key, val); break; case "not": w.not(n -> n.match(key, val)); break; default: throw new IllegalArgumentException("Unsupported condition: " + condition); } } } }); } /** * 根据Ee和Es构建查询条件 * 此方法用于根据提供的关键字、值和条件构建一个查询条件,用于查询EsDocument对象 * 它通过LambdaEsQueryWrapper来构建查询条件,支持and、or和not条件 * 用RestHighLevelClient和eazy-es 混合查询的方式构建查询条件 * @param wrapper 查询包装器,用于构建EsDocument的查询条件 * @param keys 关键字列表,表示要查询的字段 * @param values 值列表,与关键字一一对应,表示查询的值 * @param conditions 条件列表,表示关键字和值之间的逻辑关系 */ private void buildQueryConditionsByEeAndEs(LambdaEsQueryWrapper wrapper, List keys, List values, List conditions) { // 创建一个布尔查询构建器,用于构建复杂的查询条件 BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); // 使用Lambda表达式和QueryWrapper构建查询条件 wrapper.and(w -> { // 如果条件列表为空,则直接匹配第一个关键字和值 if (ObjectUtil.isEmpty(conditions)) { w.match(SEARCH_KEY_MAP.get(keys.getFirst()), values.getFirst()); } else { String condition; // 遍历关键字列表,根据条件构建查询 for (int i = 0; i < keys.size(); i++) { String key = SEARCH_KEY_MAP.get(keys.get(i)); String val = values.get(i); // 获取当前关键字和值之间的条件,如果为第一个关键字,则获取第二个条件,否则获取当前关键字对应的条件 condition = i == 0 ? conditions.get(i + 1) : conditions.get(i); // 默认为 "and" // 根据条件构建查询 switch (condition.toLowerCase()) { case "and": // 如果条件为and,则在布尔查询中添加必须条件 boolQueryBuilder.must(new MatchQueryBuilder(key, val)); break; case "or": // 如果条件为or,则在查询中添加或条件 w.or().match(key, val); break; case "not": // 如果条件为not,则在查询中添加非条件 w.not(n -> n.match(key, val)); break; default: // 如果条件不支持,则抛出异常 throw new IllegalArgumentException("Unsupported condition: " + condition); } } } // 如果布尔查询构建器中有条件,则将其添加到查询中 if (boolQueryBuilder.hasClauses()) { w.mix(boolQueryBuilder); } }); } /** * 构建查询条件,结合使用Elasticsearch和Elasticsearch的Lambda查询封装 * 此方法用于根据提供的键、值和条件列表构建复杂的查询条件 * 用RestHighLevelClient和eazy-es 混合查询的方式构建查询条件 * @param wrapper LambdaEsQueryWrapper对象,用于封装查询条件 * @param keys 搜索键的列表,对应于文档中的字段 * @param values 与每个搜索键关联的值列表 * @param conditions 搜索条件的列表,支持 "and"、"or" 和 "not" */ private void buildQueryConditionsWithEeAndEs(LambdaEsQueryWrapper wrapper, List keys, List values, List conditions) { // 创建内部的BoolQueryBuilder对象,用于构建复杂的布尔查询 BoolQueryBuilder innerBoolQueryBuilder = new BoolQueryBuilder(); // 创建必须满足的BoolQueryBuilder对象,用于处理包含 "or" 条件的情况 BoolQueryBuilder mustBoolQueryBuilder = new BoolQueryBuilder(); // 如果条件列表为空,则直接使用第一个键值对构建查询条件 if (ObjectUtil.isEmpty(conditions)) { innerBoolQueryBuilder.must(new MatchQueryBuilder(keys.getFirst(), values.getFirst())); } else { String condition; boolean containsOr = conditions.contains("or"); for (int i = 0; i < keys.size(); i++) { String key = SEARCH_KEY_MAP.get(keys.get(i)); String val = values.get(i); condition = i == 0 ? conditions.get(i + 1) : conditions.get(i); // 默认为 "and" switch (condition) { case "and": if (containsOr) { mustBoolQueryBuilder.must(new MatchQueryBuilder(key, val)); } else { innerBoolQueryBuilder.must(new MatchQueryBuilder(key, val)); } break; case "or": innerBoolQueryBuilder.should(new MatchQueryBuilder(key, val)); break; case "not": if (i == 0) { if (containsOr) { mustBoolQueryBuilder.must(new MatchQueryBuilder(key, val)); } else { innerBoolQueryBuilder.must(new MatchQueryBuilder(key, val)); } } else { if (containsOr) { mustBoolQueryBuilder.mustNot(new MatchQueryBuilder(key, val)); } else { innerBoolQueryBuilder.mustNot(new MatchQueryBuilder(key, val)); } } break; default: throw new IllegalArgumentException("Invalid condition: " + condition); } } } // 如果条件中包含 "or",则设置最低匹配条件,并根据情况添加必须满足的查询条件 if (!innerBoolQueryBuilder.should().isEmpty()) { innerBoolQueryBuilder.minimumShouldMatch("1"); if (mustBoolQueryBuilder.hasClauses()) { innerBoolQueryBuilder.should(mustBoolQueryBuilder); } } // 将构建好的查询条件添加到LambdaEsQueryWrapper对象中 wrapper.mix(innerBoolQueryBuilder); } /** * 构建查询条件 * 用纯eazy-es的方式实现 * 根据提供的关键字、值和条件列表,构建一个复杂的查询条件表达式 * 此方法使用LambdaEsQueryWrapper来构建查询条件,支持and、or和not等逻辑运算符 * * @param wrapper 查询包装器,用于构建查询条件 * @param keys 关键字列表,代表查询的字段 * @param values 值列表,与关键字一一对应,代表查询的值 * @param conditions 条件列表,包含and、or、not等逻辑运算符,用于组合查询条件 */ private void buildQueryConditions(LambdaEsQueryWrapper wrapper, List keys, List values, List conditions) { wrapper.and(w -> { int conditionSize = conditions.size(); // 当条件数量少于2时,直接使用第一个关键字和值进行匹配查询 if (conditionSize < 2) { w.match(SEARCH_KEY_MAP.get(keys.getFirst()), values.getFirst()); } else { boolean hasAnd = conditions.contains("and"); boolean hasOr = conditions.contains("or"); boolean hasNot = conditions.contains("not"); // 根据条件列表中是否包含and、or、not来构建不同的查询逻辑 if (hasAnd && hasOr) { w.and(w1 -> { String condition; for (int i = 0; i < keys.size(); i++) { condition = i == 0 ? conditions.get(i + 1) : conditions.get(i); if (condition.equalsIgnoreCase("and")) { w1.match(SEARCH_KEY_MAP.get(keys.get(i)), values.get(i)); } else if (condition.equalsIgnoreCase("not")) { w1.not().match(SEARCH_KEY_MAP.get(keys.get(i)), values.get(i)); } } }); String condition; for (int i = 0; i < keys.size(); i++) { condition = i == 0 ? conditions.get(i + 1) : conditions.get(i); if (condition.equalsIgnoreCase("or")) { w.or().match(SEARCH_KEY_MAP.get(keys.get(i)), values.get(i)); } } } else if (hasAnd) { String condition; for (int i = 0; i < keys.size(); i++) { condition = i == 0 ? conditions.get(i + 1) : conditions.get(i); if (condition.equalsIgnoreCase("and")) { w.match(SEARCH_KEY_MAP.get(keys.get(i)), values.get(i)); } else if (condition.equalsIgnoreCase("not")) { w.not().match(SEARCH_KEY_MAP.get(keys.get(i)), values.get(i)); } } } else if (hasOr) { String condition; for (int i = 0; i < keys.size(); i++) { condition = i == 0 ? conditions.get(i + 1) : conditions.get(i); if (condition.equalsIgnoreCase("or")) { w.or().match(SEARCH_KEY_MAP.get(keys.get(i)), values.get(i)); } else if (condition.equalsIgnoreCase("not")) { w.not().match(SEARCH_KEY_MAP.get(keys.get(i)), values.get(i)); } } } else if (hasNot) { String condition; for (int i = 0; i < keys.size(); i++) { condition = i == 0 ? conditions.get(i + 1) : conditions.get(i); if (condition.equalsIgnoreCase("not")) { if (i == 0) { w.match(SEARCH_KEY_MAP.get(keys.get(i)), values.get(i)); } else { w.not().match(SEARCH_KEY_MAP.get(keys.get(i)), values.get(i)); } } } } } }); } /** * 使用查询字符串进行高级搜索 * 用于高级搜索,根据用户提供的关键字、值和逻辑条件,构建复杂的查询字符串,并结合其他搜索条件(如时间范围和类型过滤) * 用queryString的查询方式实现高级搜索 * @param userAdvancedSearch 包含用户搜索条件的对象,包括关键字、值和逻辑条件 * @param pageNumber 分页参数,指定当前页码 * @param pageSize 分页参数,指定每页大小 * @return 返回包含搜索结果的EsPageInfo对象 *

* 此方法根据用户提供的高级搜索条件,构建复杂的查询字符串,并结合其他搜索条件(如时间范围和类型过滤), * 执行弹性搜索并返回分页结果 */ @Override public EsPageInfo esAdvancedSearchByQueryString(UserAdvancedSearch userAdvancedSearch, Integer pageNumber, Integer pageSize) { LambdaEsQueryWrapper wrapper = new LambdaEsQueryWrapper<>(); List keys = userAdvancedSearch.getKey(); List values = userAdvancedSearch.getVal(); List conditions = userAdvancedSearch.getCondition(); // 边界条件检查 if (keys == null || values == null || keys.size() != values.size()) { throw new IllegalArgumentException("Keys and values must have the same length and cannot be null"); } StringBuilder andStr = new StringBuilder(); StringBuilder orStr = new StringBuilder(); // 根据条件构建查询字符串 if (ObjectUtil.isNotEmpty(conditions)) { for (int i = 0; i < keys.size(); i++) { String condition = i == 0 ? conditions.get(i + 1) : conditions.get(i); switch (condition) { case "and": if (!andStr.isEmpty()) { andStr.append(Link.AND); } andStr.append(QueryUtils.buildQueryString(getFunc(SEARCH_KEY_MAP.get(keys.get(i))), values.get(i), Query.MATCH)); break; case "or": if (!orStr.isEmpty()) { orStr.append(Link.OR); } orStr.append(QueryUtils.buildQueryString(getFunc(SEARCH_KEY_MAP.get(keys.get(i))), values.get(i), Query.MATCH)); break; case "not": wrapper.not().match(SEARCH_KEY_MAP.get(keys.get(i)), values.get(i)); break; default: throw new IllegalArgumentException("Unsupported condition: " + condition); } } } else { andStr = new StringBuilder(QueryUtils.buildQueryString(getFunc(SEARCH_KEY_MAP.get(keys.getFirst())), values.getFirst(), Query.MATCH)); } // 合并查询字符串 String queryStr = ""; if ((!andStr.isEmpty()) && (!orStr.isEmpty())) { queryStr = QueryUtils.combine(Link.OR, andStr.toString()) + orStr; } if (!andStr.isEmpty()) { queryStr = andStr.toString(); } if (!orStr.isEmpty()) { queryStr = orStr.toString(); } // 处理时间范围 if (userAdvancedSearch.getStartTime() != null && userAdvancedSearch.getEndTime() != null) { wrapper.between(EsDocument::getPublishDate, userAdvancedSearch.getStartTime(), userAdvancedSearch.getEndTime()); } // 处理类型过滤 List mediaTypes = userAdvancedSearch.splitMediaTypes(); if (mediaTypes != null && !mediaTypes.isEmpty()) { wrapper.in(EsDocument::getType, mediaTypes); } wrapper.queryStringQuery(queryStr); return documentMapper.pageQuery(wrapper, pageNumber, pageSize); } /** * 根据传入的 keyList, conditionList, valList 来完成 and, or, not 的动态组合 * 用RestHighLevelClient和eazy-es 混合查询的方式构建查询条件 * * @param wrapper LambdaEsQueryWrapper 对象,用于封装查询条件 * @param keyList 关键字列表,对应于文档中的字段 * @param valList 与每个搜索键关联的值列表 * @param conditionList 搜索条件的列表,支持 "and"、"or" 和 "not" */ private void buildDynamicQueryConditions(LambdaEsQueryWrapper wrapper, List keyList, List valList, List conditionList) { // 边界条件检查 if (keyList == null || valList == null || conditionList == null) { throw new IllegalArgumentException("Input lists cannot be null"); } if (keyList.size() != valList.size()) { throw new IllegalArgumentException("Keys and values must have the same size"); } if (keyList.size() - 1 != conditionList.size()) { throw new IllegalArgumentException("Conditions size should be one less than keys size"); } BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); for (int i = 0; i < keyList.size(); i++) { String key = SEARCH_KEY_MAP.get(keyList.get(i)); String val = valList.get(i); String condition = i == 0 ? "and" : conditionList.get(i - 1).toLowerCase(); switch (condition) { case "and": boolQueryBuilder.must(new MatchQueryBuilder(key, val)); break; case "or": boolQueryBuilder.should(new MatchQueryBuilder(key, val)); break; case "not": boolQueryBuilder.mustNot(new MatchQueryBuilder(key, val)); break; default: throw new IllegalArgumentException("Unsupported condition: " + condition); } } // 如果使用了 "or" 条件,确保至少匹配一个条件 if (!boolQueryBuilder.should().isEmpty()) { boolQueryBuilder.minimumShouldMatch(1); } wrapper.mix(boolQueryBuilder); } } ``` 以下是两个类功能的对比分析(IdempotentFilter,RateLimiterAop): ### 1. 功能定位对比 | 类名 | IdempotentFilter | RateLimiterAop | |--------------|-----------------------------------------|-----------------------------------| | **功能类型** | 幂等性控制(防重复提交) | 流量控制(防高频请求) | | **技术实现** | Guava本地缓存 + 请求指纹算法 | Resilience4j分布式限流 + AOP | | **作用层面** | Servlet过滤器层(请求到达Controller前) | AOP切面层(Controller方法执行时) | | **判断维度** | 请求内容+客户端标识+时间窗口 | 请求频率(单位时间请求次数) | ### 2. 关键差异点 | 维度 | IdempotentFilter | RateLimiterAop | |--------------|------------------------------------|--------------------------------------| | **核心目标** | 防止相同请求的重复提交 | 控制单位时间内的请求总量 | | **判断依据** | 请求指纹(内容+客户端+时间)唯一性 | 请求频率阈值(如100次/秒) | | **典型场景** | 订单重复提交、表单重复提交 | API防刷、系统过载保护 | | **配置维度** | 基于请求内容的5秒时间窗口 | 基于方法/全局的QPS限制 | | **异常响应** | 429错误(明确提示操作频繁) | 返回限流异常(可能包装成特定错误码) | ### 3. 是否存在功能重叠? **不重叠**,二者为互补关系: - 当`高频不同请求`到达时:仅触发RateLimiterAop的限流 - 当`相同请求重复提交`时:仅触发IdempotentFilter的拦截 - 当`高频重复请求`到达时:二者会同时生效 ### 4. 协同工作建议 ```java // 配置示例:在application.yml中明确分工 resilience4j: ratelimiter: instances: globalApiLimiter: limitForPeriod:100 #每秒100次全局请求 limitRefreshPeriod:1s ``` | 防护类型 | 配置建议 | 业务价值 | |----------|--------------------------|----------------| | 幂等过滤 | 5秒时间窗口 + 客户端IP | 保证数据一致性 | | 接口限流 | 100 QPS + 动态限流器选择 | 保障系统稳定性 | ### 5. 升级建议 若需进一步区分功能,可增加注解配置: ```java // 自定义组合注解示例 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ApiProtection { boolean idempotent() default false; int rateLimit() default -1; } // 使用示例 @ApiProtection(idempotent = true, rateLimit = 50) public ResponseEntity createOrder() {...} ``` 通过这种设计: - 需要防重复的方法启用`idempotent` - 需要特殊限流的方法设置`rateLimit` - 两者可独立启用或组合使用 ``` package cn.shujuhai.tsingtu.platform.pluging.idempotent; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebFilter; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.digest.DigestUtils; import org.jetbrains.annotations.NotNull; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** * IdempotentFilter 同一客户端在短时间内重复提交相同请求的情况,确保操作的幂等性 * * @author dfq_2 * @date 2025/04/10 16:50 **/ @Slf4j @WebFilter(urlPatterns = {"/general/*", "/sys/*", "/user/*"}) public class IdempotentFilter extends OncePerRequestFilter { private static final Cache requestCache = CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(5, TimeUnit.SECONDS).build(); @Override protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain chain) throws IOException, ServletException { String fingerprint = buildFingerprint(request); if (requestCache.asMap().putIfAbsent(fingerprint, true) == null) { chain.doFilter(request, response); } else { response.sendError(429, "操作过于频繁"); } } private String buildFingerprint(HttpServletRequest request) { String clientId = request.getRemoteAddr(); String paramHash = DigestUtils.sha256Hex(getSortedParams(request)); String timeWindow = String.valueOf(System.currentTimeMillis() / 5000); return String.join("|", clientId, // 新增客户端标识 request.getMethod(), request.getRequestURI(), paramHash, timeWindow); } private String getSortedParams(HttpServletRequest request) { return request.getParameterMap().entrySet().stream() .sorted(Map.Entry.comparingByKey()) .map(e -> e.getKey() + "=" + String.join(",", e.getValue())) .collect(Collectors.joining("&")); } } ``` ``` package cn.shujuhai.tsingtu.platform.pluging.ratelimit; import io.github.resilience4j.ratelimiter.RateLimiter; import io.github.resilience4j.ratelimiter.RateLimiterRegistry; import io.github.resilience4j.ratelimiter.RequestNotPermitted; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; /** * RateLimiterAspect 全局或基于方法的请求频率限制,不管请求是否相同,只要达到限流阈值就会被限制。例如,即使不同的请求,如果频率过高,RateLimiterAop也会触发限流,而IdempotentFilter不会处理这种情况,除非请求内容相同。 * * @author dfq_2 * @date 2025/04/08 11:44 **/ @Aspect @Component @RequiredArgsConstructor @Slf4j public class RateLimiterAop { private final RateLimiterRegistry rateLimiterRegistry; // 定义切入点:拦截所有Controller层的公共方法 @Pointcut("@within(org.springframework.stereotype.Controller) || @within(org.springframework.web.bind.annotation.RestController)") public void apiMethods() { } @SneakyThrows //@Around("apiMethods()") public Object applyRateLimiter(ProceedingJoinPoint joinPoint) { log.debug("限流拦截方法: {}", joinPoint.getSignature().toShortString()); RateLimiter rateLimiter = rateLimiterRegistry.rateLimiter(determineRateLimiter(joinPoint)); try { return rateLimiter.executeSupplier(() -> sneakyProceed(joinPoint)); } catch (RequestNotPermitted e) { log.warn("接口触发限流: {}", joinPoint.getSignature().toShortString()); throw RequestNotPermitted.createRequestNotPermitted(rateLimiter); } } @SneakyThrows private Object sneakyProceed(ProceedingJoinPoint joinPoint) { return joinPoint.proceed(); // 此处自动穿透所有异常类型 } private String determineRateLimiter(ProceedingJoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); String methodName = signature.getMethod().getName(); if (methodName.startsWith("highPriority")) { return "highPriorityApiLimiter"; } return "globalApiLimiter"; } } ``` ``` package cn.shujuhai.tsingtu.platform.pluging.ratelimit; import io.github.resilience4j.ratelimiter.RateLimiter; import io.github.resilience4j.ratelimiter.RateLimiterRegistry; import jakarta.servlet.*; import jakarta.servlet.annotation.WebFilter; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import java.io.IOException; /** * RateLimiterFilter * * @author dfq_2 * @date 2025/04/16 17:01 **/ @Order(Ordered.HIGHEST_PRECEDENCE + 1) // 控制执行顺序 @RequiredArgsConstructor @WebFilter(urlPatterns = {"/general/*", "/sys/*", "/user/*"}) public class RateLimiterFilter implements Filter { private final RateLimiterRegistry rateLimiterRegistry; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletResponse httpResponse = (HttpServletResponse) response; RateLimiter rateLimiter = rateLimiterRegistry.rateLimiter("globalApiLimiter"); if (rateLimiter.acquirePermission()) { chain.doFilter(request, response); } else { httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Rate limit exceeded"); } } } ``` ### 策略模式 vs 责任链模式详解 #### 一、策略模式(Strategy Pattern) **定义**:定义算法族,封装每个算法,使它们可以互相替换,让算法的变化独立于使用它的客户端。 **核心思想**: 1. 将相似的算法抽象为统一接口 2. 通过组合而非继承实现行为扩展 3. 运行时动态切换算法 **适用场景**: - 系统需要多种算法变体 - 需要消除复杂的条件判断语句 - 算法需要自由切换的场景 **Java 示例 - 电商折扣策略**: ```java // 策略接口 interface DiscountStrategy { double applyDiscount(double originalPrice); } // 具体策略实现 class PercentageDiscount implements DiscountStrategy { private double percentage; public PercentageDiscount(double percentage) { this.percentage = percentage; } @Override public double applyDiscount(double originalPrice) { return originalPrice * (1 - percentage); } } class FixedDiscount implements DiscountStrategy { private double discountAmount; public FixedDiscount(double discountAmount) { this.discountAmount = discountAmount; } @Override public double applyDiscount(double originalPrice) { return originalPrice - discountAmount; } } // 上下文类 class PricingContext { private DiscountStrategy strategy; public void setStrategy(DiscountStrategy strategy) { this.strategy = strategy; } public double calculatePrice(double originalPrice) { return strategy.applyDiscount(originalPrice); } } // 使用示例 public class Main { public static void main(String[] args) { PricingContext context = new PricingContext(); // 使用百分比折扣 context.setStrategy(new PercentageDiscount(0.2)); // 20% off System.out.println(context.calculatePrice(100)); // 输出:80.0 // 切换为固定折扣 context.setStrategy(new FixedDiscount(30)); System.out.println(context.calculatePrice(100)); // 输出:70.0 } } ``` **结构图**: ``` ┌──────────────────┐ ┌─────────────────────┐ │ DiscountStrategy│<>-----│ PricingContext │ ├──────────────────┤ ├─────────────────────┤ │+applyDiscount() │ │-strategy: Strategy │ └──────────────────┘ │+setStrategy() │ ▲ │+calculatePrice() │ │ └─────────────────────┘ | ┌──────┴───────┐ │ Percentage │ ┌──────────────────┐ │ Discount │ │ FixedDiscount │ ├──────────────┤ ├──────────────────┤ │-percentage │ │-discountAmount │ └──────────────┘ └──────────────────┘ ``` #### 二、责任链模式(Chain of Responsibility) **定义**:将请求的发送者和接收者解耦,使多个对象都有机会处理请求,将这些对象连成链,并沿着链传递请求直到被处理。 **核心思想**: 1. 每个处理者持有下一个处理者的引用 2. 请求沿链传递直到被处理 3. 动态调整处理流程 **适用场景**: - 多级审批流程 - 异常处理机制 - 日志记录链 - 过滤器链 **Java 示例 - 日志处理链**: ```java // 处理者接口 abstract class LogHandler { protected LogHandler nextHandler; protected LogLevel level; public LogHandler(LogLevel level) { this.level = level; } public void setNext(LogHandler handler) { this.nextHandler = handler; } public void handleRequest(LogMessage message) { if (this.level.ordinal() >= message.getLevel().ordinal()) { write(message); } if (nextHandler != null) { nextHandler.handleRequest(message); } } protected abstract void write(LogMessage message); } // 具体处理者 class DebugHandler extends LogHandler { public DebugHandler() { super(LogLevel.DEBUG); } @Override protected void write(LogMessage message) { System.out.println("[DEBUG] " + message.getContent()); } } class InfoHandler extends LogHandler { public InfoHandler() { super(LogLevel.INFO); } @Override protected void write(LogMessage message) { System.out.println("[INFO] " + message.getContent()); } } class ErrorHandler extends LogHandler { public ErrorHandler() { super(LogLevel.ERROR); } @Override protected void write(LogMessage message) { System.out.println("[ERROR] " + message.getContent()); // 发送邮件通知 EmailService.sendAlert(message.getContent()); } } // 使用示例 public class Main { public static void main(String[] args) { // 构建处理链:Debug -> Info -> Error LogHandler debug = new DebugHandler(); LogHandler info = new InfoHandler(); LogHandler error = new ErrorHandler(); debug.setNext(info); info.setNext(error); // 处理不同级别的日志 debug.handleRequest(new LogMessage("System started", LogLevel.DEBUG)); debug.handleRequest(new LogMessage("User login", LogLevel.INFO)); debug.handleRequest(new LogMessage("Database connection failed", LogLevel.ERROR)); } } enum LogLevel {DEBUG, INFO, WARN, ERROR} class LogMessage { private String content; private LogLevel level; // 构造方法、getters } ``` **结构图**: ``` ┌───────────────┐ ┌───────────────┐ │ LogHandler │<>-----│ LogHandler │ ├───────────────┤ ├───────────────┤ │+handleRequest()│ │-nextHandler │ └───────────────┘ └───────────────┘ ▲ | ┌──────┴───────┐ │ DebugHandler │ ┌───────────────┐ ├──────────────┤ │ InfoHandler │ │+write() │ ├───────────────┤ └──────────────┘ │+write() │ └───────────────┘ ▲ | ┌──────┴───────┐ │ ErrorHandler │ ├──────────────┤ │+write() │ └──────────────┘ ``` #### 三、模式对比 | 特性 | 策略模式 | 责任链模式 | |--------------|------------------------|------------------------------| | **核心目的** | 算法替换 | 请求传递处理 | | **关系结构** | 平行关系 | 链式结构 | | **执行方式** | 通常只选择一个策略执行 | 可能被多个处理者处理 | | **典型应用** | 支付方式、排序算法 | 审批流程、过滤器链 | | **扩展方向** | 新增算法实现 | 新增处理节点 | | **终止条件** | 必定执行选定策略 | 可中途终止或传递到底 | | **对象关系** | 上下文持有策略 | 处理者持有下一个处理者的引用 | | **复杂度** | 较低(单一职责) | 较高(链式逻辑) | #### 四、组合使用案例:电商订单处理 **场景需求**: 1. 根据订单类型选择不同的价格计算策略 2. 订单需要经过多级验证处理(库存检查、支付验证、风控审核) ```java // 策略模式:价格计算 interface PricingStrategy { double calculatePrice(Order order); } class NormalPricing implements PricingStrategy { public double calculatePrice(Order order) { return order.getBasePrice() * order.getQuantity(); } } class GroupBuyPricing implements PricingStrategy { public double calculatePrice(Order order) { return order.getBasePrice() * order.getQuantity() * 0.8; } } // 责任链模式:订单验证 abstract class OrderValidator { private OrderValidator next; public OrderValidator linkWith(OrderValidator next) { this.next = next; return next; } public abstract boolean validate(Order order); protected boolean checkNext(Order order) { return next == null || next.validate(order); } } class StockValidator extends OrderValidator { public boolean validate(Order order) { boolean valid = order.getItem().getStock() >= order.getQuantity(); if (!valid) System.out.println("库存不足"); return valid && checkNext(order); } } class PaymentValidator extends OrderValidator { public boolean validate(Order order) { boolean valid = order.getPayment().isValid(); if (!valid) System.out.println("支付验证失败"); return valid && checkNext(order); } } class RiskValidator extends OrderValidator { public boolean validate(Order order) { boolean valid = !RiskService.isHighRisk(order.getUser()); if (!valid) System.out.println("风控验证未通过"); return valid && checkNext(order); } } // 使用示例 public class OrderProcessor { public void processOrder(Order order) { // 选择价格策略 PricingStrategy strategy = order.isGroupBuy() ? new GroupBuyPricing() : new NormalPricing(); double price = strategy.calculatePrice(order); order.setFinalPrice(price); // 构建验证链 OrderValidator validatorChain = new StockValidator() .linkWith(new PaymentValidator()) .linkWith(new RiskValidator()); if (validatorChain.validate(order)) { order.confirm(); System.out.println("订单处理成功"); } else { order.cancel(); System.out.println("订单验证失败"); } } } ``` #### 五、最佳实践建议 **策略模式最佳实践**: 1. 当发现多个类仅在行为表现不同时使用 2. 优先使用组合而非继承来实现算法扩展 3. 使用枚举来管理策略类型 4. 结合工厂模式创建策略对象 **责任链模式最佳实践**: 1. 明确每个处理者的责任范围 2. 设置合理的链长度(通常3-5个节点) 3. 提供默认处理逻辑或最终兜底处理 4. 使用建造者模式构建处理链 5. 添加链监控能力(记录处理路径) **性能优化方向**: 1. 策略模式:缓存策略对象避免重复创建 2. 责任链:对高频处理者进行排序(常用在前) 3. 使用享元模式共享无状态处理者 4. 对链处理过程进行异步化改造 ```java // 策略工厂示例 class PricingStrategyFactory { private static final Map strategies = new HashMap<>(); static { strategies.put("NORMAL", new NormalPricing()); strategies.put("GROUP_BUY", new GroupBuyPricing()); strategies.put("PROMOTION", new PromotionPricing()); } public static PricingStrategy getStrategy(String type) { return strategies.getOrDefault(type, new NormalPricing()); } } // 建造者模式构建责任链 class ValidatorChainBuilder { private OrderValidator first; private OrderValidator current; public ValidatorChainBuilder addValidator(OrderValidator validator) { if (first == null) { first = validator; current = validator; } else { current.linkWith(validator); current = validator; } return this; } public OrderValidator build() { return first; } } // 使用建造者 OrderValidator chain = new ValidatorChainBuilder() .addValidator(new StockValidator()) .addValidator(new PaymentValidator()) .addValidator(new RiskValidator()) .build(); ``` 通过合理应用策略模式和责任链模式,可以显著提升代码的可维护性和扩展性。策略模式适合需要动态切换算法的场景,而责任链模式则擅长处理需要多级处理的业务流程。理解它们的差异并掌握组合使用技巧,是构建灵活架构的关键。 ``` security: enable-ip-check: false api-rate-limit: 1000 # 每秒请求数限制 ip-white-list: # IP白名单列表 - 192.168.1.1 - 192.168.1.2 login: exclude-paths: - "/user/resource/**" - "/general/**" - "/page/general/**" - "/page/user/**" - "/api/**" - "/" role-rules: - paths: [ "/sys/**", "/page/admin/**" ] roles: "admin" exclude-paths: [ "/sys/public/**" ] perm-rules: - paths: [ "/sys/manage/**", "/page/admin/manage/**" ] permissions: "sys_manage" exclude-paths: - "/sys/manage/statistics/**" - "/sys/manage/menu/userMenus" - "/sys/manage/logging/visitsCount" - "/sys/manage/logging/visitsByMonth" - paths: [ "/sys/resource/**","/page/admin/resource/**" ] permissions: "content_manage,info_manage" check-type: OR ``` ``` package cn.shujuhai.tsingtu.platform.service; import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.util.ObjectUtil; import cn.shujuhai.tsingtu.platform.domain.EsDocument; import cn.shujuhai.tsingtu.platform.domain.UserAdvancedSearch; import cn.shujuhai.tsingtu.platform.mapperes.DocumentMapper; import cn.shujuhai.tsingtu.platform.shared.constant.Constant; import cn.shujuhai.tsingtu.platform.shared.kit.EsDocumentConverter; import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.dromara.easyes.core.biz.EsPageInfo; import org.dromara.easyes.core.conditions.select.LambdaEsQueryWrapper; import org.dromara.easyes.core.conditions.update.LambdaEsUpdateWrapper; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; import static cn.shujuhai.tsingtu.platform.shared.constant.Constant.SEARCH_KEY_MAP; /** * 存储期刊内容的特定信息 服务层实现。 * * @author dch * @since 1.0 */ @Slf4j @Service @RequiredArgsConstructor public class UserEsSearchService { private final DocumentMapper documentMapper; @Value("${file.dir.upload}") private String upload; @Async("esThreadPool") // 指定ES专用线程池 public void asyncHandleEsOperation(String sqlId, Object parameterObject) { try { if (Constant.INSERT_PATTERN.matcher(sqlId).matches()) { this.saveEsDocument(parameterObject); } else if (Constant.UPDATE_PATTERN.matcher(sqlId).matches()) { this.updateEsDocument(parameterObject); } else if (Constant.DELETE_PATTERN.matcher(sqlId).matches()) { this.deleteEsDocument(parameterObject, sqlId); } } catch (Exception e) { String errorMsg = String.format("ES异步操作失败 | SQLID: %s | 参数: %s | 错误详情: %s", sqlId, parameterObject, e.getCause() != null ? e.getCause().getMessage() : e.getMessage()); log.error(errorMsg, e); } } /** * 保存文档到 Elasticsearch *

* 该方法接收一个参数对象,将其转换为EsDocument实体,并插入到Elasticsearch数据库中 * 主要用于在系统中创建新的文档记录 * * @param parameterObject 包含文档信息的参数对象,具体结构和内容由EsDocumentConverter类处理 * 返回插入操作的影响行数,通常为1表示成功,0表示失败 */ public void saveEsDocument(Object parameterObject) { if (ObjectUtil.isEmpty(parameterObject)) { log.warn("Parameter object is null, cannot save EsDocument."); return; } EsDocument documentEntity = EsDocumentConverter.getDocumentEntity(parameterObject, this.upload); if (ObjectUtil.isEmpty(documentEntity)) { log.warn("Converted document entity is null, cannot save EsDocument."); return; } this.documentMapper.insert(documentEntity); } /** * 更新Elasticsearch文档信息 *

* 该方法接收一个参数对象,将其转换为EsDocument实体,并更新到Elasticsearch中 * 主要用于当资源信息发生变化时,同步更新搜索索引 * * @param parameterObject 包含更新信息的参数对象,具体类型取决于转换逻辑 * 返回更新影响的文档数量 */ public void updateEsDocument(Object parameterObject) { if (ObjectUtil.isEmpty(parameterObject)) { log.warn("Parameter object is null, cannot update EsDocument."); return; } // 将参数对象转换为EsDocument实体,upload标志指示是否上传文件 EsDocument documentEntity = EsDocumentConverter.getDocumentEntity(parameterObject, this.upload); if (ObjectUtil.isEmpty(documentEntity)) { log.warn("Converted document entity is null, cannot update EsDocument."); return; } // 执行更新操作,仅更新与给定资源ID和类型匹配的文档 LambdaEsUpdateWrapper wrapper = new LambdaEsUpdateWrapper<>(); wrapper.eq(EsDocument::getResId, documentEntity.getResId()).eq(EsDocument::getType, documentEntity.getType()); this.documentMapper.update(documentEntity, wrapper); } /** * 删除Elasticsearch文档 *

* 该方法用于根据给定的参数对象和方法名来删除Elasticsearch文档它首先将参数对象转换为参数映射, * 然后使用该映射中的参数来构建查询条件,最后执行删除操作 * * @param parameterObject 包含删除所需参数的对象,具体结构未知,因此需要转换为映射 * @param methodName 用于确定文档类型的方法名,基于方法名来转换获取文档类型 * 返回删除的文档数量 */ public void deleteEsDocument(Object parameterObject, String methodName) { // 参数验证 if (ObjectUtil.isEmpty(parameterObject) || !(parameterObject instanceof Map)) { log.warn("Parameter object must be a non-null Map."); return; } if (CharSequenceUtil.isEmpty(methodName)) { log.warn("Method name cannot be null or empty."); return; } // 将参数对象转换为参数映射,以便于后续的查询条件构建 @SuppressWarnings("unchecked") Map parameterMap = (Map) parameterObject; // 执行删除操作,使用lambda链式查询构建器来构建查询条件 // 查询条件包括资源ID和文档类型,其中资源ID从参数映射中获取,文档类型通过方法名转换获取 LambdaEsUpdateWrapper wrapper = new LambdaEsUpdateWrapper<>(); wrapper.eq(EsDocument::getResId, parameterMap.get("param1")) .eq(EsDocument::getType, EsDocumentConverter.getTypeByMethodName(methodName)); this.documentMapper.delete(wrapper); } /** * 执行多字段模糊搜索的函数 *

* 该函数使用给定的关键词在多个字段中进行模糊搜索,并返回匹配的文档列表 * 它还允许指定页码和页面大小,以便进行分页查询 * * @param keyword 搜索关键词 * @param pageNumber 页码 * @param pageSize 页面大小 * @return 包含匹配文档的EsPageInfo对象 */ public EsPageInfo multiFieldFuzzySearch(String keyword, Integer pageNumber, Integer pageSize) { if (CharSequenceUtil.isEmpty(keyword)) { log.warn("Keyword cannot be null or empty."); return null; } LambdaEsQueryWrapper wrapper = new LambdaEsQueryWrapper<>(); // 从所有字段中查询包含关键词的文档 wrapper.queryStringQuery(keyword).minScore(5d); return documentMapper.pageQuery(wrapper, pageNumber, pageSize); } /** * 执行高级搜索查询 *

* 该方法根据用户提供的高级搜索条件,构建查询条件并执行搜索返回分页结果 * 它处理包括关键字、值、条件的匹配,时间范围过滤,以及类型过滤等 * * @param userAdvancedSearch 包含用户高级搜索条件的对象,包括关键字、值、条件等 * @param pageNumber 分页查询的页码 * @param pageSize 每页显示的结果数量 * @return 返回一个EsPageInfo对象,包含查询结果和分页信息 * @throws IllegalArgumentException 如果提供的关键字、值或它们的数量不匹配,抛出此异常 */ public EsPageInfo esAdvancedSearch(UserAdvancedSearch userAdvancedSearch, Integer pageNumber, Integer pageSize) { LambdaEsQueryWrapper wrapper = new LambdaEsQueryWrapper<>(); List keys = userAdvancedSearch.getKey(); List values = userAdvancedSearch.getVal(); List conditions = userAdvancedSearch.getCondition(); // 边界条件检查 if (keys == null || values == null || keys.size() != values.size()) { throw new IllegalArgumentException("Keys, values, and size mismatch or null"); } // 构建查询条件 buildQueryConditionsWithEeAndEs(wrapper, keys, values, conditions); // 处理时间范围 // 处理时间范围 if (userAdvancedSearch.getStartTime() != null && userAdvancedSearch.getEndTime() != null) { wrapper.between(EsDocument::getPublishDate, userAdvancedSearch.getStartTime(), userAdvancedSearch.getEndTime()); } else if (userAdvancedSearch.getStartTime() != null || userAdvancedSearch.getEndTime() != null) { // 不抛出异常,返回空结果集 log.warn("EsAdvancedSearch Either start time or end time is missing. Returning empty result set."); return new EsPageInfo<>(); } // 处理类型过滤 List mediaTypes = userAdvancedSearch.splitMediaTypes(); if (ObjectUtil.isNotEmpty(mediaTypes)) { mediaTypes = mediaTypes.stream().filter(type -> type != null && !type.trim().isEmpty()).collect(Collectors.toList()); if (!mediaTypes.isEmpty()) { wrapper.in(EsDocument::getType, mediaTypes); } } wrapper.minScore(4d); log.info("Executing advanced search with parameters: pageNumber={}, pageSize={}", pageNumber, pageSize); return documentMapper.pageQuery(wrapper, pageNumber, pageSize); } private void buildQueryConditionsWithEeAndEs(LambdaEsQueryWrapper wrapper, List keys, List values, List conditions) { // 参数校验 if (keys == null || values == null || keys.size() != values.size()) { throw new IllegalArgumentException("Keys与values数量必须一致"); } if (conditions != null && conditions.size() != Math.max(0, keys.size() - 1)) { throw new IllegalArgumentException("Conditions数量应为keys数量减一"); } // 使用Easy-ES的链式API构建查询 wrapper.and(root -> { boolean containsOr = conditions != null && conditions.contains("or"); BoolQuery.Builder mustBuilder = new BoolQuery.Builder(); // 处理第一个条件(无条件运算符) if (!keys.isEmpty()) { String firstKey = SEARCH_KEY_MAP.get(keys.getFirst()); String firstVal = values.getFirst(); if (containsOr) { mustBuilder.must(q -> q.match(m -> m.field(firstKey).query(firstVal))); } else { root.must(q -> q.match(firstKey, firstVal)); } } // 处理后续条件 for (int i = 1; i < keys.size(); i++) { String key = SEARCH_KEY_MAP.get(keys.get(i)); String val = values.get(i); String condition = Objects.requireNonNull(conditions).get(i - 1).toLowerCase(); switch (condition) { case "and" -> { if (containsOr) { mustBuilder.must(q -> q.match(m -> m.field(key).query(val))); } else { root.must(q -> q.match(key,val)); } } case "or" -> root.should(q -> q.match(key,val)); case "not" -> { if (containsOr) { mustBuilder.mustNot(q -> q.match(m -> m.field(key).query(val))); } else { root.not(q -> q.match(key,val)); } } default -> throw new IllegalArgumentException("无效条件: " + condition); } } // 处理OR逻辑的特殊规则 if (containsOr) { mustBuilder.minimumShouldMatch("1"); if (mustBuilder.hasClauses()) { root.mix(mustBuilder.build()._toQuery()); } } }); } /** * 构建查询条件,结合使用Elasticsearch和Elasticsearch的Lambda查询封装 * 此方法用于根据提供的键、值和条件列表构建复杂的查询条件 * 用RestHighLevelClient和eazy-es 混合查询的方式构建查询条件 * * @param wrapper LambdaEsQueryWrapper对象,用于封装查询条件 * @param keys 搜索键的列表,对应于文档中的字段 * @param values 与每个搜索键关联的值列表 * @param conditions 搜索条件的列表,支持 "and"、"or" 和 "not" */ private void buildQueryConditionsWithEeAndEs1(LambdaEsQueryWrapper wrapper, List keys, List values, List conditions) { // 创建内部的BoolQueryBuilder对象,用于构建复杂的布尔查询 BoolQuery.Builder innerBoolQueryBuilder = new BoolQuery.Builder(); // 创建必须满足的BoolQueryBuilder对象,用于处理包含 "or" 条件的情况 BoolQuery.Builder mustBoolQueryBuilder = new BoolQuery.Builder(); // 如果条件列表为空,则直接使用第一个键值对构建查询条件 if (ObjectUtil.isEmpty(conditions)) { innerBoolQueryBuilder.must(q -> q.match(m -> m.field(keys.getFirst()).query(values.getFirst()))); } else { String condition; boolean containsOr = conditions.contains("or"); for (int i = 0; i < keys.size(); i++) { String key = SEARCH_KEY_MAP.get(keys.get(i)); String val = values.get(i); condition = i == 0 ? conditions.get(i + 1) : conditions.get(i); // 默认为 "and" switch (condition) { case "and": if (containsOr) { mustBoolQueryBuilder.must(q -> q.match(m -> m.field(key).query(val))); } else { innerBoolQueryBuilder.must(q -> q.match(m -> m.field(key).query(val))); } break; case "or": innerBoolQueryBuilder.should(q -> q.match(m -> m.field(key).query(val))); break; case "not": if (i == 0) { if (containsOr) { mustBoolQueryBuilder.must(q -> q.match(m -> m.field(key).query(val))); } else { innerBoolQueryBuilder.must(q -> q.match(m -> m.field(key).query(val))); } } else { if (containsOr) { mustBoolQueryBuilder.mustNot(q -> q.match(m -> m.field(key).query(val))); } else { innerBoolQueryBuilder.mustNot(q -> q.match(m -> m.field(key).query(val))); } } break; default: throw new IllegalArgumentException("Invalid condition: " + condition); } } } // 如果条件中包含 "or",则设置最低匹配条件,并根据情况添加必须满足的查询条件 if (!innerBoolQueryBuilder.build().should().isEmpty()) { innerBoolQueryBuilder.minimumShouldMatch("1"); if (mustBoolQueryBuilder.hasClauses()) { innerBoolQueryBuilder.should(mustBoolQueryBuilder.build()._toQuery()); } } // 将构建好的查询条件添加到LambdaEsQueryWrapper对象中 wrapper.mix(innerBoolQueryBuilder.build()._toQuery()); } } ```