Merge remote-tracking branch 'origin/master'

# Conflicts:
#	src/main/resources/application-dev.yml
This commit is contained in:
xiongfeng
2025-08-17 18:53:06 +08:00
22 changed files with 1472 additions and 34 deletions

View File

@@ -1,21 +1,49 @@
# 拿来即用springboot基础项目
## 拿来即用springboot基础脚手架
---
### 项目介绍
技术栈 | 名称
-------- | -----
Spring boot | 3.3.3
JWT | 身份验证和授权
RSA | 数据加密
MybatisPlus | 持久层框架
MYSQL | 数据库
Hikari | 数据库连接池
Reids | 数据缓存
springdoc-openapi | 接口文档
thymeleaf | 模版引擎
docker | 容器Dockerfile
[![](https://img.shields.io/badge/-@remaindertime-FC5531?style=flat&logo=csdn&logoColor=FC5531&labelColor=424242)](https://blog.csdn.net/qq_39818325?type=blog)
[![GitHub Stars](https://img.shields.io/github/stars/RemainderTime/spring-boot-base-demo?style=social)](https://github.com/RemainderTime/spring-boot-base-demo)
![](https://img.shields.io/badge/jdk-1.8+-blue.svg)
![](https://img.shields.io/badge/springboot-3.3.3-{徽标颜色}.svg)
![](https://img.shields.io/badge/springdoc-2.6.0-{徽标颜色}.svg)
![](https://img.shields.io/badge/elasticsearch-8.16.0-005571.svg)
![](https://img.shields.io/badge/redis-3.3.3-FF4438.svg)
---
> 这是一个基于 **Spring Boot 3.3.3** 的快速构建单体架构脚手架,旨在帮助开发者快速搭建高效、稳定的项目基础框架。项目集成了多种常用的技术组件与功能,涵盖从用户认证到数据加密、从全局异常处理到搜索引擎操作,适合个人学习与企业级单体应用开发。
- #### 版本更新 2024-10-12
### 集成技术与功能亮点
- 身份认证与授权JWT基于 JWT 实现用户认证与授权,确保系统安全性。
- 数据加密RSA提供 RSA 非对称加密支持,保障敏感数据安全。
- 持久层框架MyBatis Plus简化数据库操作提供高效的 CRUD 支持。
- 数据库MySQL采用 MySQL 作为默认数据库,易于扩展和维护。
- 数据连接池Hikari高性能数据源管理优化数据库连接效率。
- 缓存Redis支持分布式缓存提升系统响应速度与并发能力。
- 接口文档springdoc-openapi自动生成标准化 API 文档,便于调试与集成。
- 模板引擎Thymeleaf支持动态页面渲染提升前后端协同效率。
- 容器化支持Docker内置 Dockerfile轻松实现环境部署与迁移。
- 搜索引擎Elasticsearch 8.x集成最新版本 Elasticsearch Java 客户端,提供高效的全文检索与复杂查询功能。
- 全局异常处理:统一管理异常,提升代码可维护性与调试效率。
- 拦截器支持:轻松实现请求拦截与权限控制。
### 项目优势
**全面适配 Spring Boot 3.x**
- 所有组件已全面升级为支持 Spring Boot 3.x 的最新版本。解决了开发者在版本升级中遇到的各种不兼容和适配问题,大大减少了升级带来的额外工作量,让项目开发更加顺畅。
**初学者友好**
- 提供清晰的代码结构与详细的配置说明,帮助初学者快速上手微服务与单体架构的开发实践。
**高扩展性**
- 丰富的功能集成,涵盖了开发中常见的场景,减少重复开发工作量,同时为定制化需求预留了扩展空间。
**稀缺的最新技术操作示例**
- 最新版本的 Elasticsearch 8.x 集成、Java 客户端操作示例和现代化 API 设计,让开发者能够轻松掌握分布式搜索引擎的使用。
### 版本更新 2024-10-12
---
1. springboot版本升级3.x
2. mybatis plus版本升级3.x
3. dynamic mybatis plus版本升级3.x
@@ -27,6 +55,5 @@ docker | 容器Dockerfile
9. 新增本地日志配置文件
---
编码不易欢迎点击右上角star支持
如果这个项目对你有帮助,请随手点个 Star ⭐ 支持一下吧!🎉✨ 你的支持是我持续优化的动力!❤️

34
pom.xml
View File

@@ -83,7 +83,6 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>3.3.3</version>
</dependency>
<!--redis连接池需要依赖-->
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
@@ -145,6 +144,31 @@
<scope>provided</scope> <!-- 在Web服务器环境中由服务器提供 -->
</dependency>
<!-- elasticsearch8.x 搜索引擎 -->
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>8.16.0</version>
</dependency>
<!--微信支付-->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.15</version>
</dependency>
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.5.0</version>
</dependency>
<!--支付宝-->
<!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-sdk-java -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.40.0.ALL</version>
</dependency>
</dependencies>
@@ -162,6 +186,14 @@
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>14</source>
<target>14</target>
</configuration>
</plugin>
</plugins>
</build>

View File

@@ -0,0 +1,42 @@
package cn.xf.basedemo.common.model;
import lombok.Data;
/**
* packageName cn.xf.basedemo.common.model
* @author remaindertime
* @className EsModel
* @date 2024/12/10
* @description es基础模型
*/
@Data
public class EsBaseModel<T> {
public EsBaseModel(String indexName, String documentId, T documentModel, Class<T> clazz) {
this.indexName = indexName;
this.documentId = documentId;
this.documentModel = documentModel;
this.clazz = clazz;
}
/**
* 索引名称
*/
private String indexName;
/**
* 文档id
*/
private String documentId;
/**
* 映射对象
*/
private T documentModel;
/**
* 映射对象类对象
*/
private Class<T> clazz;
}

View File

@@ -0,0 +1,81 @@
package cn.xf.basedemo.common.model;
import lombok.Data;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* packageName cn.xf.basedemo.common.model
* @author remaindertime
* @className EsSearchModel
* @date 2024/12/11
* @description es 搜索模型
*/
@Data
public class EsSearchModel<T> {
public EsSearchModel() {
// 使用 LinkedHashMap 保持插入顺序
this.sort = new LinkedHashMap<>();
}
/**
* 索引名称
*/
private String indexName;
/**
* 文档类型
*/
private Class<T> clazz;
/**
* 页数
*/
private Integer pageNum;
/**
* 每页数量
*/
private Integer pageSize;
/**
* 精准查询字段
*/
private Map<String, Object> termQuery;
/**
* 模糊查询字段(一般是text类型)
*/
private Map<String, Object> matchQuery;
/**
* 排序字段规则 ({"age":"desc"})
*/
private Map<String, String> sort;
/**
* 分组去重字段支持的字段类型keyword、numeric、date 和 boolean
*/
private String repeatField;;
/**
* 分组嵌套查询别名
*/
private String innerAlias;
/**
* 分组嵌套查询数量
*/
private Integer innerSize;
/**
* 指定需要返回的字段
*/
private List<String> includes;
/**
* 指定需要排除的字段
*/
private List<String> excludes;
}

View File

@@ -0,0 +1,18 @@
package cn.xf.basedemo.common.model.pay;
import lombok.Data;
@Data
public class AlipayTransRsqVo {
private boolean success = false;
private String code;
private String subCode;
private String msg;
private String orderId;
private String status;
private String payFundOrderID;
private String outBizNo;
private String transDate;
private String settleSerialNo;
private String amount;
}

View File

@@ -0,0 +1,14 @@
package cn.xf.basedemo.common.model.pay;
import com.wechat.pay.java.service.refund.model.Refund;
import lombok.Data;
@Data
public class RefundInfoRsqDTO {
private boolean isSuccess;
private String msg;
private Refund refund;
}

View File

@@ -0,0 +1,12 @@
package cn.xf.basedemo.common.model.pay;
import lombok.Data;
@Data
public class TransferQueryVO {
private boolean isSuccess;
private String resCode;
private String resCodeDes;
private WechatCashQueryVo cashQueryVo;
}

View File

@@ -0,0 +1,24 @@
package cn.xf.basedemo.common.model.pay;
import lombok.Data;
import java.time.OffsetDateTime;
@Data
public class WechatCashQueryVo {
private String appid;
private String batch_id;
private String detail_id;
private String detail_status;
private OffsetDateTime initiate_time;
private String mchid;
private String openid;
private String out_batch_no;
private String out_detail_no;
private long transfer_amount;
private String transfer_remark;
private OffsetDateTime update_time;
private String user_name;
}

View File

@@ -0,0 +1,11 @@
package cn.xf.basedemo.common.model.pay;
import lombok.Data;
@Data
public class WxPayTransRsqVo {
private boolean success = false;
private String code;
private String msg;
}

View File

@@ -0,0 +1,499 @@
package cn.xf.basedemo.common.utils;
import cn.xf.basedemo.common.model.EsBaseModel;
import cn.xf.basedemo.common.model.EsSearchModel;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.*;
import co.elastic.clients.elasticsearch._types.query_dsl.*;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.search.*;
import co.elastic.clients.elasticsearch.indices.CreateIndexResponse;
import co.elastic.clients.elasticsearch.indices.ExistsRequest;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.transport.endpoints.BooleanResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.*;
import java.util.stream.Collectors;
/**
* packageName cn.xf.basedemo.common.utils
* @author remaindertime
* @className EsUtil
* @date 2024/12/10
* @description elasticsearch工具类
*/
@Slf4j
@Component
public class EsUtil {
public static ElasticsearchClient esClient;
{
esClient = (ElasticsearchClient) ApplicationContextUtils.getBean("elasticsearchClient");
}
/**
* 判断索引是否存在
* @param indexName
* @return
*/
public static boolean existIndex(String indexName) {
try {
// 创建 ExistsRequest 请求
ExistsRequest request = new ExistsRequest.Builder()
.index(indexName)
.build();
// 发送请求并获取响应
BooleanResponse response = esClient.indices().exists(request);
// 返回索引是否存在
return response.value();
} catch (Exception e) {
// 处理异常
e.printStackTrace();
return false;
}
}
/**
* 删除索引
*
* @param indexName
*/
@SneakyThrows
public static void delIndex(String indexName) {
if (existIndex(indexName)) {
return;
}
esClient.indices().delete(d -> d.index(indexName));
}
/**
* 创建索引
*
* @param indexName
* @return
*/
public static void createIndex(String indexName) {
if (existIndex(indexName)) {
throw new RuntimeException("索引已经存在");
}
try {
CreateIndexResponse createIndexResponse = esClient.indices().create(c -> c.index(indexName));
// 处理响应
if (createIndexResponse.acknowledged()) {
log.info(" indexed create successfully.");
} else {
log.info("Failed to create index.");
}
} catch (Exception e) {
// 捕获异常并打印详细错误信息
e.printStackTrace();
throw new RuntimeException("创建索引失败,索引名:" + indexName + ",错误信息:" + e.getMessage(), e);
}
}
/**
* 新增文档
* @param esBaseModel
* @return
*/
public static boolean addDocument(EsBaseModel esBaseModel) {
try {
ObjectMapper objectMapper = new ObjectMapper();
String jsonString = objectMapper.writeValueAsString(esBaseModel.getDocumentModel());
log.info("es新增文档文档内容{}", jsonString);
// 创建 IndexRequest 实例
IndexRequest request = new IndexRequest.Builder()
.index(esBaseModel.getIndexName())
.id(esBaseModel.getDocumentId()) //指定文档id,不指定会自动生成
.document(esBaseModel.getDocumentModel())
.opType(OpType.Create) // 只会在文档 ID 不存在时创建文档
.build();
IndexResponse response = esClient.index(request);
if ("created".equals(response.result())) {
log.info("Document created: " + response.id());
return true;
} else {
log.info("Document already exists or failed to create.");
return false;
}
} catch (Exception e) {
log.error("es新增文档失败", e);
e.printStackTrace();
}
return false;
}
/**
* 更新文档
* @param esBaseModel
* @return
*/
public boolean updateDocument(EsBaseModel esBaseModel) {
try {
UpdateRequest updateRequest = new UpdateRequest.Builder<>()
.index(esBaseModel.getIndexName())
.id(esBaseModel.getDocumentId())
.doc(esBaseModel.getDocumentModel()).build();
UpdateResponse updateResponse = esClient.update(updateRequest, esBaseModel.getClazz());
log.info("Document updated: " + updateResponse.id());
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 更新文档指定字段script 脚本)
* @param esBaseModel
* @param script 脚本内容
* @param params 传递参数内容
*/
public void updateDocumentWithScript(EsBaseModel esBaseModel, String script, Map<String, JsonData> params) {
try {
UpdateRequest updateRequest = new UpdateRequest.Builder<>()
.index(esBaseModel.getIndexName())
.id(esBaseModel.getDocumentId())
.script(s ->
s.source(script)// 脚本内容:.source("ctx._source.age += params.increment")
.params(params)) // 传递参数内容:.params("increment",sonData.of(5))
.build();
UpdateResponse updateResponse = esClient.update(updateRequest, esBaseModel.getClazz());
log.info("Document updated: " + updateResponse.id());
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 根据id查询文档
* @param esBaseModel
* @return
*/
public static <T> T getDocumentById(EsBaseModel esBaseModel) {
try {
GetRequest getRequest = new GetRequest.Builder()
.index(esBaseModel.getIndexName())
.id(esBaseModel.getDocumentId())
.build();
GetResponse<T> getResponse = esClient.get(getRequest, esBaseModel.getClazz());
if (getResponse.found()) {
return getResponse.source();
}
} catch (Exception e) {
log.error("es列表查询失败", e);
}
return null;
}
/**
* 查询文档列表
* @param searchModel
* @return
*/
public static <T> List<T> getDocumentList(EsSearchModel searchModel) {
List<T> eslist = new ArrayList<>();
try {
SearchResponse<T> search = esClient.search(buildSearchRequest(searchModel), searchModel.getClazz());
if (Objects.isNull(search)) {
return eslist;
}
HitsMetadata<T> hits = search.hits();
if (Objects.isNull(hits)) {
return eslist;
}
List<Hit<T>> sourceHitList = hits.hits();
if (CollectionUtils.isEmpty(sourceHitList)) {
return eslist;
}
sourceHitList.forEach(item -> {
// 处理每个命中
eslist.add(item.source());
});
return eslist;
} catch (Exception e) {
log.error("es列表查询失败", e);
}
return eslist;
}
/**
* 查询文档数量
* @param searchModel
* @return
*/
public static long getDocumentCount(EsSearchModel searchModel) {
try {
CountRequest.Builder countRequest = new CountRequest.Builder();
countRequest.index(searchModel.getIndexName());
countRequest.query(createBoolQuery(searchModel.getTermQuery(), searchModel.getMatchQuery()));
CountResponse count = esClient.count(countRequest.build());
if (Objects.isNull(count)) {
log.info("es列表数量查询异常{}", searchModel);
return 0;
}
return count.count();
} catch (Exception e) {
log.error("es列表数量查询失败", e);
}
return 0;
}
/**
* 根据id删除文档
* @param esBaseModel
* @return
*/
public static Boolean deleteDocumentById(EsBaseModel esBaseModel) {
try {
DeleteRequest deleteRequest = new DeleteRequest.Builder()
.index(esBaseModel.getDocumentId())
.id(esBaseModel.getDocumentId())
.build();
DeleteResponse deleteResponse = esClient.delete(deleteRequest);
if ("deleted".equals(deleteResponse.result())) {
log.info("Document deleted: " + deleteResponse.id());
return true;
} else {
log.info("Document delete failed: " + deleteResponse.id());
return false;
}
} catch (Exception e) {
log.error("es列表删除失败", e);
}
return false;
}
/**
* 根据条件删除文档
* @param searchModel
* @return 删除数量
*/
public static long deleteDocumentByQuery(EsSearchModel searchModel) {
try {
DeleteByQueryRequest.Builder deleteRequest = new DeleteByQueryRequest.Builder();
deleteRequest.index(searchModel.getIndexName());
deleteRequest.query(createBoolQuery(searchModel.getTermQuery(), searchModel.getMatchQuery()));
deleteRequest.refresh(true); //设置删除操作后是否立即刷新索引,使删除结果立即可见
deleteRequest.timeout(new Time.Builder().time("2s").build()); //设置删除操作的超时时间
deleteRequest.conflicts(Conflicts.Proceed); //Conflicts.Proceed在版本冲突时继续删除操作;Conflicts.Abort在版本冲突时中止删除操作
DeleteByQueryResponse dResponse = esClient.deleteByQuery(deleteRequest.build());
if (Objects.nonNull(dResponse)) {
log.info("es条件删除成功删除数量{}", dResponse.deleted());
return dResponse.deleted();
}
} catch (Exception e) {
log.error("es条件删除数据失败", e);
}
return 0;
}
/**
* 构建搜索请求对象
* @param searchModel
* @return
*/
private static SearchRequest buildSearchRequest(EsSearchModel searchModel) {
//定义查询对象
SearchRequest.Builder searchRequest = new SearchRequest.Builder();
//设置索引名称
searchRequest.index(searchModel.getIndexName());
//分组去重
if (StringUtils.isNotBlank(searchModel.getRepeatField())) {
searchRequest.collapse(buildCollapse(searchModel));
}
//设置查询条件
searchRequest.query(createBoolQuery(searchModel.getTermQuery(), searchModel.getMatchQuery()));
//设置排序规则
if (searchModel.getSort() != null) {
searchRequest.sort(buildSort(searchModel.getSort()));
}
//设置分页参数
if (searchModel.getPageSize() != null && searchModel.getPageSize() != null) {
searchRequest.from(searchModel.getPageSize() * (searchModel.getPageNum() - 1));
searchRequest.size(searchModel.getPageSize());
}
//设置查询字段/排查字段
SourceConfig sourceConfig = buildSourceConfig(searchModel.getIncludes(), searchModel.getExcludes());
if (Objects.nonNull(sourceConfig)) {
searchRequest.source(sourceConfig);
}
return searchRequest.build();
}
/**
* 构建查询条件
* @param termQuery
* @param matchQuery
* @return
*/
private static Query createBoolQuery(Map<String, Object> termQuery, Map<String, Object> matchQuery) {
BoolQuery.Builder cQuery = new BoolQuery.Builder();
// TermQuery 精准匹配
if (termQuery != null) {
for (Map.Entry<String, Object> entry : termQuery.entrySet()) {
if (Objects.isNull(entry.getValue())) {
continue;
}
String key = entry.getKey();
Object value = entry.getValue();
if (value.getClass().isArray()) { //数组查询,使用 TermsQuery
Object[] values = (Object[]) entry.getValue();
List<FieldValue> objs = Arrays.stream(values)
.map(v -> FieldValue.of(v)) // 将每个对象转换为 FieldValue
.collect(Collectors.toList());
cQuery.must(new TermsQuery.Builder()
.field(key)
.terms(t -> t.value(objs))
.build()
._toQuery());
} else if (value.toString().contains(" ")) { // 短语查询,使用 MatchPhraseQuery (要严格按照单词顺序字符串中有空格,短信需匹配)
cQuery.must(new MatchPhraseQuery.Builder()
.field(key)
.query(value.toString())
.build()
._toQuery());
} else { // 其他情况,使用 TermQuery 精准匹配
cQuery.must(new TermQuery.Builder()
.field(key)
.value(value.toString())
.build()
._toQuery());
}
}
}
// MatchQuery 模糊匹配全文检索分词查询
if (matchQuery != null) {
for (Map.Entry<String, Object> entry : matchQuery.entrySet()) {
if (Objects.isNull(entry.getValue())) {
continue;
}
cQuery.must(new MatchQuery.Builder()
.field(entry.getKey())
.query(entry.getValue().toString())
.build()
._toQuery());
}
}
return cQuery.build()._toQuery();
}
/**
* 构建时间区间查询
* @param startTime 开始时间
* @param endTime 结束时间
* @param fieldName 时间字段
* @return
*/
public static Query createTimeQuery(String startTime, String endTime, String fieldName) {
DateRangeQuery dataQuery = new DateRangeQuery.Builder()
.field(fieldName)
.build();
// 时间区间查询
dataQuery.of(o -> o.gte(startTime));
dataQuery.of(o -> o.lte(endTime));
return dataQuery._toRangeQuery()._toQuery();
}
/**
* 设置查询字段/排查字段
* @param includes 需要字段
* @param excludes 排除字段
* @return
*/
private static SourceConfig buildSourceConfig(List<String> includes, List<String> excludes) {
boolean isIncludes = CollectionUtils.isEmpty(includes);
boolean isExcludes = CollectionUtils.isEmpty(excludes);
//设置查询字段/排查字段
if (isIncludes || isExcludes) {
SourceFilter.Builder sourceFilter = new SourceFilter.Builder();
if (isIncludes)
sourceFilter.includes(includes);
if (isExcludes)
sourceFilter.excludes(excludes);
return new SourceConfig.Builder().filter(sourceFilter.build()).build();
}
return null;
}
/**
* 构建分组去重
* @param searchModel
* @return
*/
private static FieldCollapse buildCollapse(EsSearchModel searchModel) {
FieldCollapse.Builder fieldCollapse = new FieldCollapse.Builder();
//设置分组字段
fieldCollapse.field(searchModel.getRepeatField());
//设置嵌套配置
if (StringUtils.isNotBlank(searchModel.getInnerAlias())) {
InnerHits.Builder innerHits = new InnerHits.Builder();
//设置别名
innerHits.name(searchModel.getInnerAlias());
//设置查询数量
if (searchModel.getInnerSize() != null) {
innerHits.size(searchModel.getInnerSize());
}
fieldCollapse.innerHits(InnerHits.of(i -> i.name(searchModel.getInnerAlias()).size(10)));
}
return fieldCollapse.build();
}
/**
* 构建排序规则
* @param sortMap
* @return
*/
private static List<SortOptions> buildSort(Map<String, String> sortMap) {
if (sortMap == null) {
return null;
}
List<SortOptions> sortList = new ArrayList<>();
for (Map.Entry<String, String> sort : sortMap.entrySet()) {
sortList.add(new SortOptions.Builder().field(f -> f.field(sort.getKey()).order(SortOrder.valueOf(sort.getValue()))).build());
}
return sortList;
}
/**
* 案例:组合多条件查询(关于 must、mustNot、should 条件的使用)
*/
public Query combinationQueryTest() {
//query.must()and 文档必须满足该条件,如果不满足,文档将不匹配。 and
//query.should()or 文档可以不满足该条件,但满足该条件时会得分更高;即使不满足,文档也会出现在查询结果中,只是查询结果靠后。
//场景1文档必须符合所有 must 条件和 mustNot 条件,同时至少满足一个 should 条件。如果 should 条件都不满足,文档将被排除不查询出来。
BoolQuery.Builder query = new BoolQuery.Builder();
//数字范围查询
NumberRangeQuery.Builder numberQuery = new NumberRangeQuery.Builder();
numberQuery.field("age").lte(30.0).build();
// 构建查询条件
query.must(o -> o.term(t -> t.field("status").value("active"))) // 必须满足的条件
.mustNot(o -> o.term(t -> t.field("country").value("China"))) // 不能满足的条件
.filter(f -> f.bool(bo -> bo
.should(so -> so.range(r -> r.number(numberQuery.build()))) // 至少满足一个 should 条件
.should(so -> so.term(t -> t.field("gender").value("male"))) // 至少满足一个 should 条件
.minimumShouldMatch("1") // 至少满足一个 should 条件 也可设置百分比 “50%”
));
//场景2文档必须符合所有 must 条件和 mustNot 条件,同时至少满足一个 should 条件。如果 should 条件都不满足不用做额外的过滤按照should原生特性处理
query.must(o -> o.bool(bo -> bo
.should(so -> so.range(r -> r.number(numberQuery.build()))) // 至少满足一个 should 条件
.should(so -> so.term(t -> t.field("gender").value("male"))) // 至少满足一个 should 条件
.minimumShouldMatch("1") // 至少满足一个 should 条件
))
.must(o -> o.term(t -> t.field("status").value("active"))) // 必须满足的条件
.mustNot(o -> o.term(t -> t.field("country").value("China"))); // 不能满足的条件
return query.build()._toQuery();
}
}

View File

@@ -0,0 +1,29 @@
package cn.xf.basedemo.common.utils;
/**
* packageName cn.xf.basedemo.common.utils
* @author remaindertime
* @className StringUtil
* @date 2024/12/11
* @description 字符串工具类
*/
public class StringUtil {
/**
* 驼峰命名法转下划线命名法
*
* @param camelCase 驼峰命名法字符串
* @return 下划线命名法字符串
*/
public static String camelToKebabCase(String camelCase) {
if (camelCase == null || camelCase.isEmpty()) {
return camelCase;
}
// 使用正则表达式将大写字母前插入一个"-"
String result = camelCase.replaceAll("([a-z])([A-Z])", "$1-$2");
// 转换为小写
return result.toLowerCase();
}
}

View File

@@ -0,0 +1,210 @@
package cn.xf.basedemo.common.utils.pay;
import cn.xf.basedemo.common.model.pay.AlipayTransRsqVo;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.CertAlipayRequest;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayFundTransUniTransferModel;
import com.alipay.api.domain.Participant;
import com.alipay.api.request.AlipayFundTransUniTransferRequest;
import com.alipay.api.response.AlipayFundTransUniTransferResponse;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
@Slf4j
public class AliPayUtil {
//支付路径
private static String payPath;
private static PayInfo payInfo;
private static String gatewayUrl = "https://openapi.alipay.com/gateway.do";//"https://openapi.alipaydev.com/gateway.do";
private static String format = "json";
private static String charset = "utf-8";
private static String signType = "RSA2";
@Data
static class PayInfo {
String appId;
String appCertPath;
String alipayCertPath;
String rootCertPath;
String rsaPrivateKey;
}
public AliPayUtil(String path) {
payPath = path;
String path1 = "d:/pay/";
path1 = "";
if (path.equals("a")) {
payInfo = new PayInfo();
payInfo.setAppId("");
payInfo.setAppCertPath(path1 + "15080.crt");
payInfo.setAlipayCertPath(path1 + "alipayCertPublicKey_RSA2.crt");
payInfo.setRootCertPath(path1 + "alipayRootCert.crt");
payInfo.setRsaPrivateKey(readFileContent(path1 + "private.key"));
}
}
@SneakyThrows
public AlipayTransRsqVo transfer(String trans_no, String account, BigDecimal money, int type, String trueName, String mark) {
AlipayTransRsqVo resVo = new AlipayTransRsqVo();
// 创建API客户端实例
CertAlipayRequest certAlipayRequest = new CertAlipayRequest();
certAlipayRequest.setServerUrl(gatewayUrl);
certAlipayRequest.setAppId(payInfo.appId);
certAlipayRequest.setPrivateKey(payInfo.rsaPrivateKey);
certAlipayRequest.setFormat(format);
certAlipayRequest.setCharset(charset);
certAlipayRequest.setSignType(signType);
//设置应用公钥证书路径
certAlipayRequest.setCertPath(payInfo.getAppCertPath());
//设置支付宝公钥证书路径
certAlipayRequest.setAlipayPublicCertPath(payInfo.alipayCertPath);
//certAlipayRequest.setAlipayPublicCertContent(AlipaySignature.getAlipayPublicKey(payInfo.alipayCertPath));
//设置支付宝根证书路径
certAlipayRequest.setRootCertPath(payInfo.getRootCertPath());
AlipayClient alipayClient = new DefaultAlipayClient(certAlipayRequest);
/** 实例化具体API对应的request类类名称和接口名称对应,当前调用接口名称alipay.fund.trans.uni.transfer(单笔转账接口) **/
AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest();
AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel();
/******必传参数******/
// 商户端的唯一订单号,对于同一笔转账请求,商户需保证该订单号唯一
model.setOutBizNo(trans_no);
// 转账金额TRANS_ACCOUNT_NO_PWD产品取值最低0.1
model.setTransAmount(String.valueOf(money.setScale(2, RoundingMode.HALF_UP)));
// 销售产品码。单笔无密转账固定为 TRANS_ACCOUNT_NO_PWD。
model.setProductCode("TRANS_ACCOUNT_NO_PWD");
// 业务场景。单笔无密转账固定为 DIRECT_TRANSFER
model.setBizScene("DIRECT_TRANSFER");
// 转账业务的标题,用于在支付宝用户的账单里显示。
model.setOrderTitle("订单标题");
// 收款方信息
Participant payeeInfo = new Participant();
// 参与方的标识类型,设置ALIPAY_USER_ID或者ALIPAY_LOGON_ID
// ALIPAY_USER_ID支付宝会员的用户 ID可通过 获取会员信息 获取https://opendocs.alipay.com/open/284/106000
// ALIPAY_LOGON_ID支付宝登录号支持邮箱和手机号格式。
if (type == 1) {
payeeInfo.setIdentityType("ALIPAY_USER_ID");
}
if (type == 2) {
payeeInfo.setIdentityType("ALIPAY_LOGON_ID");
}
// 参与方的标识 ID根据identity_type类型选择对应信息
// 当 identity_type=ALIPAY_USER_ID 时,填写支付宝用户 UID。示例值2088123412341234。
// 当 identity_type=ALIPAY_LOGON_ID 时填写支付宝登录号。示例值186xxxxxxxx。
payeeInfo.setIdentity(account);
// 参与方真实姓名。如果非空,将校验收款支付宝账号姓名一致性。
// 当 identity_type=ALIPAY_LOGON_ID 时,本字段必填。
payeeInfo.setName(trueName);
model.setPayeeInfo(payeeInfo);
/******可选参数******/
// 业务备注
model.setRemark(mark);
// 转账业务请求的扩展参数
// payer_show_name_use_alias是否展示付款方别名可选收款方在支付宝账单中可见。枚举支持
// * true展示别名将展示商家支付宝在商家中心 商户信息 > 商户基本信息 页面配置的 商户别名。
// * false不展示别名。默认为 false。
// model.setBusinessParams("{\"payer_show_name_use_alias\":\"true\"}");
request.setBizModel(model);
try {
// 发送请求并获取响应
// AlipayFundTransToaccountTransferResponse response = alipayClient.certificateExecute(request);
AlipayFundTransUniTransferResponse response = alipayClient.certificateExecute(request);
// 处理响应通常需要检查out_biz_notrade_no和resultCode
if (response.isSuccess()) {
resVo.setSuccess(true);
resVo.setMsg(response.getSubMsg());
//resVo.setBody(JSON.parseObject(response.getBody(), AliPayBodyVo.class));
resVo.setOrderId(response.getOrderId());
resVo.setStatus(response.getStatus());
resVo.setPayFundOrderID(response.getPayFundOrderId());
resVo.setTransDate(response.getTransDate());
resVo.setOutBizNo(response.getOutBizNo());
resVo.setSettleSerialNo(response.getSettleSerialNo());
resVo.setAmount(response.getAmount());
log.info("转账成功: " + response.getBody());
} else {
resVo.setSuccess(false);
resVo.setMsg(response.getSubMsg());
resVo.setCode(response.getCode());
resVo.setSubCode(response.getSubCode());
log.info("转账失败: " + response.getSubCode() + " - " + response.getSubMsg());
}
} catch (AlipayApiException e) {
//e.printStackTrace();
resVo.setSuccess(false);
resVo.setMsg(e.getMessage());
}
return resVo;
}
public static PrivateKey getPrivateKeyFromPath(String path) {
try {
FileInputStream fis = new FileInputStream(path);
byte[] keyBytes = new byte[fis.available()];
fis.read(keyBytes);
fis.close();
String privateKeyPEM = new String(keyBytes);
// 移除PEM头部和尾部
privateKeyPEM = privateKeyPEM.replace("-----BEGIN PRIVATE KEY-----", "");
privateKeyPEM = privateKeyPEM.replace("-----END PRIVATE KEY-----", "");
// 对于Windows平台请使用"\r\n"作为分隔符对于Linux/Mac平台应使用"\n"
privateKeyPEM = privateKeyPEM.replaceAll("\\r\\n|\\n|\\r", "");
byte[] decoded = Base64.decodeBase64(privateKeyPEM);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decoded);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(keySpec);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static String readFileContent(String filePath) {
StringBuilder content = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
} catch (IOException e) {
e.printStackTrace();
}
return content.toString();
}
}

View File

@@ -0,0 +1,270 @@
package cn.xf.basedemo.common.utils.pay;
import cn.xf.basedemo.common.model.pay.RefundInfoRsqDTO;
import cn.xf.basedemo.common.model.pay.TransferQueryVO;
import cn.xf.basedemo.common.model.pay.WechatCashQueryVo;
import cn.xf.basedemo.common.model.pay.WxPayTransRsqVo;
import com.alibaba.fastjson2.JSON;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.exception.HttpException;
import com.wechat.pay.java.core.exception.MalformedMessageException;
import com.wechat.pay.java.core.exception.ServiceException;
import com.wechat.pay.java.service.refund.RefundService;
import com.wechat.pay.java.service.refund.model.AmountReq;
import com.wechat.pay.java.service.refund.model.CreateRequest;
import com.wechat.pay.java.service.refund.model.QueryByOutRefundNoRequest;
import com.wechat.pay.java.service.refund.model.Refund;
import com.wechat.pay.java.service.transferbatch.TransferBatchService;
import com.wechat.pay.java.service.transferbatch.model.*;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class WxPayUtil {
private static String payPath;
private static String appId;
private static String wxMerchantId;
private static String wxApiV3Key;
private static String wxApiSerialNo;
private static String wxMerchantApiCertificate;
private static String wxMerchantApiPrivateKey;
private static String plantSerialNo;
private static String notifyUrl; //通知URL
private static String refundUrl; //退款充值URL
public WxPayUtil(String path, int type) {
payPath = path;
type = type;
String path1 = "d:/pay";
path1 = "";
refundUrl = "" + payPath;
switch (payPath) {
case "a"://提现打款用-文撩
appId = "";
wxMerchantId = "";
wxApiV3Key = "";
wxApiSerialNo = "";
wxMerchantApiCertificate = path1 + "9DE56.pem";
wxMerchantApiPrivateKey = path1 + "apiclient_key.pem";
plantSerialNo = "1A1D8C3A474A29DE56";
break;
}
}
@SneakyThrows
public WxPayTransRsqVo transfer(String trans_no, String account, BigDecimal money, String trueName, String mark) {
WxPayTransRsqVo rsqVo = new WxPayTransRsqVo();
rsqVo.setSuccess(false);
log.info("开始退款,{}-{}-{}-{}-{}", wxMerchantId, wxMerchantApiPrivateKey, wxApiSerialNo, wxApiV3Key, money);
Config config = new RSAAutoCertificateConfig.Builder()
.merchantId(wxMerchantId)
.privateKeyFromPath(wxMerchantApiPrivateKey)
.merchantSerialNumber(wxApiSerialNo)
.apiV3Key(wxApiV3Key)
.build();
TransferBatchService service = new TransferBatchService.Builder().config(config).build();
//数据封装
InitiateBatchTransferRequest initiateBatchTransferRequest = new InitiateBatchTransferRequest();
initiateBatchTransferRequest.setAppid(appId);
initiateBatchTransferRequest.setOutBatchNo(trans_no);
initiateBatchTransferRequest.setBatchName(mark);
initiateBatchTransferRequest.setBatchRemark(mark);
initiateBatchTransferRequest.setTotalAmount(money.multiply(BigDecimal.valueOf(100)).longValue());
initiateBatchTransferRequest.setTotalNum(1);
//initiateBatchTransferRequest.setTransferSceneId("1001");
{
List<TransferDetailInput> transferDetailListList = new ArrayList<>();
{
TransferDetailInput transferDetailInput = new TransferDetailInput();
transferDetailInput.setTransferAmount(money.multiply(BigDecimal.valueOf(100)).longValue());//金额为分 需要乘以100
transferDetailInput.setOutDetailNo(trans_no);
transferDetailInput.setOpenid(account);
transferDetailInput.setUserName(trueName);
transferDetailInput.setTransferRemark(mark);
transferDetailListList.add(transferDetailInput);
}
initiateBatchTransferRequest.setTransferDetailList(
transferDetailListList);
}
//发起商家转账
InitiateBatchTransferResponse response;
try {
response = service.initiateBatchTransfer(initiateBatchTransferRequest);
log.info("转账:", response.toString());
// if (response.getBatchStatus().equals("ACCEPTED")) {
// log.info("initiateBatchTransfer:", response.getBatchStatus());
// }
// log.error("initiateBatchTransfer:", response.getBatchStatus());
rsqVo.setSuccess(true);
} catch (ServiceException e) {
log.info("出错了:", e.getErrorMessage());
// e.printStackTrace();
rsqVo.setCode(e.getErrorCode());
rsqVo.setMsg(e.getErrorMessage());
}
return rsqVo;
}
@SneakyThrows
public TransferQueryVO transferQuery(String outBatchNo, String outDetailNo) {
TransferQueryVO transferQueryVO = new TransferQueryVO();
//商家转账批次单号
//商家转账明细单号
transferQueryVO.setSuccess(false);
Config config = new RSAAutoCertificateConfig.Builder()
.merchantId(wxMerchantId)
.privateKeyFromPath(wxMerchantApiPrivateKey)
.merchantSerialNumber(wxApiSerialNo)
.apiV3Key(wxApiV3Key)
.build();
try {
TransferBatchService transferBatchService = new TransferBatchService.Builder().config(config).build();
GetTransferDetailByOutNoRequest request = new GetTransferDetailByOutNoRequest();
request.setOutBatchNo(outBatchNo);
request.setOutDetailNo(outDetailNo);
TransferDetailEntity response = transferBatchService.getTransferDetailByOutNo(request);
log.info("转账返回信息:{}", JSON.toJSONString(response));
transferQueryVO.setCashQueryVo(JSON.parseObject(JSON.toJSONString(response), WechatCashQueryVo.class));
transferQueryVO.setResCode(response.getDetailStatus());
transferQueryVO.setResCodeDes(response.getDetailStatus());
if ("FAIL".equals(response.getDetailStatus())) {
// transferQueryVO.setResCodeDes(getErrorMsg(String.valueOf(response.getFailReason())));
transferQueryVO.setSuccess(false);
} else {
transferQueryVO.setSuccess(true);
}
log.info("转账返回信息II{}", JSON.toJSONString(transferQueryVO));
} catch (HttpException e) { // 发送HTTP请求失败
// 调用e.getHttpRequest()获取请求打印日志或上报监控更多方法见HttpException定义
log.error(e.getMessage());
transferQueryVO.setResCodeDes(e.getMessage());
// throw new RuntimeException("微信转账失败");
} catch (ServiceException e) { // 服务返回状态小于200或大于等于300例如500
// 调用e.getResponseBody()获取返回体打印日志或上报监控更多方法见ServiceException定义
log.error(e.getMessage());
transferQueryVO.setResCodeDes(e.getMessage());
//throw new RuntimeException("微信转账失败");
} catch (MalformedMessageException e) { // 服务返回成功,返回体类型不合法,或者解析返回体失败
// 调用e.getMessage()获取信息打印日志或上报监控更多方法见MalformedMessageException定义
log.error(e.getMessage());
transferQueryVO.setResCodeDes(e.getMessage());
//throw new RuntimeException("微信转账失败");
} catch (Exception e) {
log.error(e.getMessage());
// e.printStackTrace();
transferQueryVO.setResCodeDes(e.getMessage());
//throw new RuntimeException("微信转账失败");
}
return transferQueryVO;
}
@SneakyThrows
public RefundInfoRsqDTO refundUser(String outTradeNo, String outRefundNo, long totalFee, BigDecimal refundFee) {
RefundInfoRsqDTO refundInfoRsqDTO = new RefundInfoRsqDTO();
refundInfoRsqDTO.setSuccess(false);
Config config = new RSAAutoCertificateConfig.Builder()
.merchantId(wxMerchantId)
.privateKeyFromPath(wxMerchantApiPrivateKey)
.merchantSerialNumber(wxApiSerialNo)
.apiV3Key(wxApiV3Key)
.build();
RefundService service = new RefundService.Builder().config(config).build();
CreateRequest request = new CreateRequest();
request.setOutTradeNo(outTradeNo);
request.setOutRefundNo(outRefundNo);
request.setNotifyUrl(refundUrl);
AmountReq amount = new AmountReq();
amount.setRefund(refundFee.longValue());
amount.setTotal(totalFee);
amount.setCurrency("CNY");
request.setAmount(amount);
try {
Refund refund = service.create(request);
log.info("refund:{}", refund.toString());
if (refund.getStatus().toString().equals("PROCESSING")) {
refundInfoRsqDTO.setSuccess(true);
}
refundInfoRsqDTO.setRefund(refund);
} catch (ServiceException e) {
log.error("退款失败:{}------{}-----{}", e.getErrorMessage(), e.getErrorCode(), e.getResponseBody());
e.printStackTrace();
refundInfoRsqDTO.setMsg(e.getErrorMessage());
}
return refundInfoRsqDTO;
}
public static RefundInfoRsqDTO queryRefund(String outRefundNo) {
RefundInfoRsqDTO refundInfoRsqDTO = new RefundInfoRsqDTO();
refundInfoRsqDTO.setSuccess(false);
Config config = new RSAAutoCertificateConfig.Builder()
.merchantId(wxMerchantId)
.privateKeyFromPath(wxMerchantApiPrivateKey)
.merchantSerialNumber(wxApiSerialNo)
.apiV3Key(wxApiV3Key)
.build();
RefundService service = new RefundService.Builder().config(config).build();
QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest();
request.setOutRefundNo(outRefundNo);
try {
Refund refund = service.queryByOutRefundNo(request);
log.info("refund:{}", refund.toString());
if (refund.getStatus().toString().equals("SUCCESS")) {
refundInfoRsqDTO.setSuccess(true);
}
refundInfoRsqDTO.setRefund(refund);
} catch (ServiceException e) {
log.error("查询失败:{}------{}-----{}", e.getErrorMessage(), e.getErrorCode(), e.getResponseBody());
e.printStackTrace();
refundInfoRsqDTO.setMsg(e.getErrorMessage());
}
return refundInfoRsqDTO;
}
// private String getErrorMsg(String errCode) {
// return switch (errCode) {
// case "ACCOUNT_FROZEN" -> "账户冻结";
// case "REAL_NAME_CHECK_FAIL" -> "用户未实名";
// case "NAME_NOT_CORRECT" -> "用户姓名校验失败";
// case "OPENID_INVALID" -> "Openid校验失败";
// case "TRANSFER_QUOTA_EXCEED" -> "超过用户单笔收款额度";
// case "DAY_RECEIVED_QUOTA_EXCEED" -> "超过用户单日收款额度";
// case "MONTH_RECEIVED_QUOTA_EXCEED" -> "超过用户单月收款额度";
// case "DAY_RECEIVED_COUNT_EXCEED" -> "超过用户单日收款次数";
// case "PRODUCT_AUTH_CHECK_FAIL" -> "产品权限校验失败";
// case "OVERDUE_CLOSE" -> "转账关闭";
// case "ID_CARD_NOT_CORRECT" -> "用户身份证校验失败";
// case "ACCOUNT_NOT_EXIST" -> "用户账户不存在";
// case "TRANSFER_RISK" -> "转账存在风险";
// case "REALNAME_ACCOUNT_RECEIVED_QUOTA_EXCEED" -> "用户账户收款受限,请引导用户在微信支付查看详情";
// case "RECEIVE_ACCOUNT_NOT_PERMMIT" -> "未配置该用户为转账收款人";
// case "PAYER_ACCOUNT_ABNORMAL" -> "商户账户付款受限,可前往商户平台-违约记录获取解除功能限制指引";
// case "PAYEE_ACCOUNT_ABNORMAL" -> "用户账户收款异常,请引导用户完善其在微信支付的身份信息以继续收款";
// case "TRANSFER_REMARK_SET_FAIL" -> "转账备注设置失败,请调整对应文案后重新再试";
// default -> "内部错误,请联系管理人员";
// };
// }
}

View File

@@ -0,0 +1,79 @@
package cn.xf.basedemo.config;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
/**
* packageName cn.xf.basedemo.config
* @author remaindertime
* @className ElasticsearchConfig
* @date 2024/12/9
* @description es工具类
*/
@Component
public class EsConfig {
@Value("${elasticsearch.host}")
private String elasticsearchHost;
@Value("${elasticsearch.port}")
private int elasticsearchPort;
@Value("${elasticsearch.username}")
private String username;
@Value("${elasticsearch.password}")
private String password;
/**
-最大连接数 (maxConnTotal)设置总的最大连接数取决于业务的并发量。500-2000 之间较为合理。
-每个节点的最大连接数 (maxConnPerRoute):控制每个节点的最大连接数,建议 50-100 之间。
-IO 线程数 (setIoThreadCount):根据 CPU 核心数设置,通常为 2-4 倍 CPU 核心数。
-连接超时、套接字超时、获取连接超时:一般设置为 10-30 秒,复杂查询或大数据量操作可适当增加到 20-60 秒。
-失败监听器 (setFailureListener):自定义重试和故障处理逻辑,确保高可用性。
*/
@Bean
public ElasticsearchClient elasticsearchClient() {
// 创建凭证提供者
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(
AuthScope.ANY,
new UsernamePasswordCredentials(username, password)
);
// 自定义 RestClientBuilder 配置
RestClientBuilder restClientBuilder = RestClient.builder(
new HttpHost(elasticsearchHost, elasticsearchPort, "http")
).setHttpClientConfigCallback(httpClientBuilder ->
httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider) // 配置认证信息
);
// 配置连接超时、套接字超时、获取连接超时
restClientBuilder.setRequestConfigCallback(builder ->
builder.setConnectTimeout(20000)
.setSocketTimeout(20000)
.setConnectionRequestTimeout(20000)
);
// 创建 RestClientTransport 和 ElasticsearchClient
RestClient restClient = restClientBuilder.build();
ElasticsearchTransport transport = new RestClientTransport(
restClient,
new JacksonJsonpMapper() // 使用 Jackson 进行 JSON 处理
);
return new ElasticsearchClient(transport);
}
/**
window系统本地启动 es8.x 重置密码命令:.\elasticsearch-reset-password -u elastic
*/
}

View File

@@ -0,0 +1,42 @@
package cn.xf.basedemo.config;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
/**
* packageName cn.xf.basedemo.config
* @author remaindertime
* @className SwaggerGroupApi
* @date 2024/12/9
* @description swagger分组配置
*/
@Component
public class SwaggerGroupApi {
@Bean
public OpenAPI springShopOpenAPI() {
return new OpenAPI()
.info(new Info().title("Spring boot脚手架 API")
.description("开箱即用的Spring boot脚手架 API")
.version("v0.0.1")
.contact(new Contact().name("remaindertime").url("https://blog.csdn.net/qq_39818325"))
.license(new License().name("Apache 2.0").url("http://springdoc.org")))
.externalDocs(new ExternalDocumentation()
.description("Spring boot脚手架 Wiki Documentation")
.url("https://springshop.wiki.github.org/docs"));
}
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("用戶相关分组")
.pathsToMatch("/user/**")
.build();
}
}

View File

@@ -6,11 +6,9 @@ import cn.xf.basedemo.interceptor.SessionContext;
import cn.xf.basedemo.model.res.LoginInfoRes;
import cn.xf.basedemo.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
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;
import org.springframework.web.bind.annotation.*;
/**
* @program: xf-boot-base
@@ -21,6 +19,7 @@ import org.springframework.web.bind.annotation.RestController;
**/
@RestController(value = "用户控制器")
@RequestMapping("/user")
@Tag(name = "用户控制器")
public class UserController {
@Autowired
@@ -39,4 +38,17 @@ public class UserController {
LoginUser loginUser = SessionContext.getInstance().get();
return RetObj.success(loginUser);
}
@Operation(summary = "es同步用户信息", description = "用户信息")
@GetMapping("/syncEs")
public RetObj syncEs(Long userId){
return userService.syncEs(userId);
}
@Operation(summary = "es查询用户信息", description = "用户信息")
@GetMapping("/getEsId")
public RetObj getEsId(Long userId){
return userService.getEsId(userId);
}
}

View File

@@ -32,13 +32,15 @@ public class TokenInterceptor implements HandlerInterceptor {
//不拦截的请求列表
private static final List<String> EXCLUDE_PATH_LIST = Arrays.asList("/user/login", "/web/login");
private static final List<String> EXCLUDE_PATH_LIST = Arrays.asList("/user/login", "/web/login","/swagger-ui.html","/v3/api-docs","/swagger-ui/index.html");
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
if (EXCLUDE_PATH_LIST.contains(requestURI)) {
if (EXCLUDE_PATH_LIST.contains(requestURI) ||
requestURI.contains("/swagger-ui") ||
requestURI.contains("/v3/api-docs")) {
return true;
}
//登录处理

View File

@@ -13,4 +13,8 @@ import cn.xf.basedemo.model.res.LoginInfoRes;
public interface UserService {
RetObj login(LoginInfoRes res);
RetObj syncEs(Long userId);
RetObj getEsId(Long userId);
}

View File

@@ -1,10 +1,13 @@
package cn.xf.basedemo.service.impl;
import cn.xf.basedemo.common.model.EsBaseModel;
import cn.xf.basedemo.common.model.LoginInfo;
import cn.xf.basedemo.common.model.LoginUser;
import cn.xf.basedemo.common.model.RetObj;
import cn.xf.basedemo.common.utils.EsUtil;
import cn.xf.basedemo.common.utils.JwtTokenUtils;
import cn.xf.basedemo.common.utils.RSAUtils;
import cn.xf.basedemo.common.utils.StringUtil;
import cn.xf.basedemo.config.GlobalConfig;
import cn.xf.basedemo.mappers.UserMapper;
import cn.xf.basedemo.model.domain.User;
@@ -90,4 +93,27 @@ public class UserServiceImpl implements UserService {
return RetObj.success(loginUser);
}
@Override
public RetObj syncEs(Long userId) {
User user = userMapper.selectById(userId);
if (Objects.isNull(user)) {
return RetObj.error("用户不存在");
}
String index = StringUtil.camelToKebabCase(user.getClass().getSimpleName());
if (!EsUtil.existIndex(index)) {
EsUtil.createIndex(index);
}
EsUtil.addDocument(new EsBaseModel(index, String.valueOf(user.getId()), user, user.getClass()));
return RetObj.success();
}
@Override
public RetObj getEsId(Long userId) {
Object user = EsUtil.getDocumentById(new EsBaseModel("user", String.valueOf(userId), null, User.class));
if(Objects.nonNull(user)){
return RetObj.success(user);
}
return RetObj.error("es中不存在该用户");
}
}

View File

@@ -18,6 +18,12 @@ spring:
WRITE_DATES_AS_TIMESTAMPS: false
FAIL_ON_EMPTY_BEANS: false
elasticsearch:
host: localhost
port: 9200
username: elastic
password: kVgA7eeLyNQKh_IyV*mW #window系统本地启动 es8.x 重置密码命令:.\elasticsearch-reset-password -u elastic
springdoc:
api-docs:
path: /v3/api-docs # 自定义 API 文档路径
@@ -46,6 +52,9 @@ mybatis-plus:
# 参考文章 https://zhuanlan.zhihu.com/p/145359625
management:
health:
elasticsearch: #禁用健康检查
enabled: false
endpoints:
web:
exposure:
@@ -55,6 +64,6 @@ management:
show-details: always
# 日志设置
logging:
level:
root:
#logging:
# level:
# root: DEBUG

View File

@@ -9,10 +9,5 @@ spring:
file-extension: yml
namespace: 34f368d5-a6c6-4f57-a80a-5402de295695
group: DEFAULT_GROUP
username:
password:
discovery:
server-addr: 9.9.9.9:9
namespace: 34f368d5-a6c6-4f57-a80a-5402de295695
username:
password:
username: nacos
password:

View File

@@ -54,7 +54,7 @@
var json = JSON.stringify(data);
var cipher = this.encryptByPublicKey(json);
console.log("密文 " + cipher);
var url = "http://117.72.35.70:8089/user/login";
var url = "http://localhost:8089/user/login";
axios.post(url, {
encryptedData: cipher,
})