diff --git a/README.md b/README.md
index 58bee12..b8b2e69 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,49 @@
-# 拿来即用springboot基础项目
+## 拿来即用springboot基础脚手架
---
+### 项目介绍
-技术栈 | 名称
--------- | -----
-Spring boot | 3.3.3
-JWT | 身份验证和授权
-RSA | 数据加密
-MybatisPlus | 持久层框架
-MYSQL | 数据库
-Hikari | 数据库连接池
-Reids | 数据缓存
-springdoc-openapi | 接口文档
-thymeleaf | 模版引擎
-docker | 容器(Dockerfile)
+[](https://blog.csdn.net/qq_39818325?type=blog)
+[](https://github.com/RemainderTime/spring-boot-base-demo)
+
+
+
+
+
+---
+> 这是一个基于 **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 ⭐ 支持一下吧!🎉✨ 你的支持是我持续优化的动力!❤️
diff --git a/pom.xml b/pom.xml
index 42d9fad..e93dc07 100644
--- a/pom.xml
+++ b/pom.xml
@@ -83,7 +83,6 @@
org.springframework.boot
spring-boot-starter-data-redis
- 3.3.3
@@ -145,6 +144,31 @@
provided
+
+
+ co.elastic.clients
+ elasticsearch-java
+ 8.16.0
+
+
+
+
+ com.github.wechatpay-apiv3
+ wechatpay-java
+ 0.2.15
+
+
+ com.github.wechatpay-apiv3
+ wechatpay-apache-httpclient
+ 0.5.0
+
+
+
+
+ com.alipay.sdk
+ alipay-sdk-java
+ 4.40.0.ALL
+
@@ -162,6 +186,14 @@
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 14
+ 14
+
+
diff --git a/src/main/java/cn/xf/basedemo/common/model/EsBaseModel.java b/src/main/java/cn/xf/basedemo/common/model/EsBaseModel.java
new file mode 100644
index 0000000..dd32333
--- /dev/null
+++ b/src/main/java/cn/xf/basedemo/common/model/EsBaseModel.java
@@ -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 {
+
+ public EsBaseModel(String indexName, String documentId, T documentModel, Class 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 clazz;
+
+}
diff --git a/src/main/java/cn/xf/basedemo/common/model/EsSearchModel.java b/src/main/java/cn/xf/basedemo/common/model/EsSearchModel.java
new file mode 100644
index 0000000..13bc030
--- /dev/null
+++ b/src/main/java/cn/xf/basedemo/common/model/EsSearchModel.java
@@ -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 {
+
+ public EsSearchModel() {
+ // 使用 LinkedHashMap 保持插入顺序
+ this.sort = new LinkedHashMap<>();
+ }
+
+ /**
+ * 索引名称
+ */
+ private String indexName;
+
+ /**
+ * 文档类型
+ */
+ private Class clazz;
+
+ /**
+ * 页数
+ */
+ private Integer pageNum;
+
+ /**
+ * 每页数量
+ */
+ private Integer pageSize;
+ /**
+ * 精准查询字段
+ */
+ private Map termQuery;
+
+ /**
+ * 模糊查询字段(一般是text类型)
+ */
+ private Map matchQuery;
+
+ /**
+ * 排序字段规则 ({"age":"desc"})
+ */
+ private Map sort;
+
+ /**
+ * 分组去重字段(支持的字段类型:keyword、numeric、date 和 boolean )
+ */
+ private String repeatField;;
+
+ /**
+ * 分组嵌套查询别名
+ */
+ private String innerAlias;
+
+ /**
+ * 分组嵌套查询数量
+ */
+ private Integer innerSize;
+
+ /**
+ * 指定需要返回的字段
+ */
+ private List includes;
+ /**
+ * 指定需要排除的字段
+ */
+ private List excludes;
+
+}
\ No newline at end of file
diff --git a/src/main/java/cn/xf/basedemo/common/model/pay/AlipayTransRsqVo.java b/src/main/java/cn/xf/basedemo/common/model/pay/AlipayTransRsqVo.java
new file mode 100644
index 0000000..a8f919c
--- /dev/null
+++ b/src/main/java/cn/xf/basedemo/common/model/pay/AlipayTransRsqVo.java
@@ -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;
+}
diff --git a/src/main/java/cn/xf/basedemo/common/model/pay/RefundInfoRsqDTO.java b/src/main/java/cn/xf/basedemo/common/model/pay/RefundInfoRsqDTO.java
new file mode 100644
index 0000000..508fb95
--- /dev/null
+++ b/src/main/java/cn/xf/basedemo/common/model/pay/RefundInfoRsqDTO.java
@@ -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;
+}
diff --git a/src/main/java/cn/xf/basedemo/common/model/pay/TransferQueryVO.java b/src/main/java/cn/xf/basedemo/common/model/pay/TransferQueryVO.java
new file mode 100644
index 0000000..b93c0e8
--- /dev/null
+++ b/src/main/java/cn/xf/basedemo/common/model/pay/TransferQueryVO.java
@@ -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;
+}
diff --git a/src/main/java/cn/xf/basedemo/common/model/pay/WechatCashQueryVo.java b/src/main/java/cn/xf/basedemo/common/model/pay/WechatCashQueryVo.java
new file mode 100644
index 0000000..5ad7bf6
--- /dev/null
+++ b/src/main/java/cn/xf/basedemo/common/model/pay/WechatCashQueryVo.java
@@ -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;
+}
+
diff --git a/src/main/java/cn/xf/basedemo/common/model/pay/WxPayTransRsqVo.java b/src/main/java/cn/xf/basedemo/common/model/pay/WxPayTransRsqVo.java
new file mode 100644
index 0000000..1054040
--- /dev/null
+++ b/src/main/java/cn/xf/basedemo/common/model/pay/WxPayTransRsqVo.java
@@ -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;
+
+}
diff --git a/src/main/java/cn/xf/basedemo/common/utils/EsUtil.java b/src/main/java/cn/xf/basedemo/common/utils/EsUtil.java
new file mode 100644
index 0000000..3b371e1
--- /dev/null
+++ b/src/main/java/cn/xf/basedemo/common/utils/EsUtil.java
@@ -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 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 getDocumentById(EsBaseModel esBaseModel) {
+ try {
+ GetRequest getRequest = new GetRequest.Builder()
+ .index(esBaseModel.getIndexName())
+ .id(esBaseModel.getDocumentId())
+ .build();
+ GetResponse 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 List getDocumentList(EsSearchModel searchModel) {
+ List eslist = new ArrayList<>();
+ try {
+ SearchResponse search = esClient.search(buildSearchRequest(searchModel), searchModel.getClazz());
+ if (Objects.isNull(search)) {
+ return eslist;
+ }
+ HitsMetadata hits = search.hits();
+ if (Objects.isNull(hits)) {
+ return eslist;
+ }
+ List> 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 termQuery, Map matchQuery) {
+ BoolQuery.Builder cQuery = new BoolQuery.Builder();
+ // TermQuery 精准匹配
+ if (termQuery != null) {
+ for (Map.Entry 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 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 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 includes, List 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 buildSort(Map sortMap) {
+ if (sortMap == null) {
+ return null;
+ }
+ List sortList = new ArrayList<>();
+ for (Map.Entry 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();
+ }
+
+}
diff --git a/src/main/java/cn/xf/basedemo/common/utils/StringUtil.java b/src/main/java/cn/xf/basedemo/common/utils/StringUtil.java
new file mode 100644
index 0000000..844163a
--- /dev/null
+++ b/src/main/java/cn/xf/basedemo/common/utils/StringUtil.java
@@ -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();
+ }
+}
diff --git a/src/main/java/cn/xf/basedemo/common/utils/pay/AliPayUtil.java b/src/main/java/cn/xf/basedemo/common/utils/pay/AliPayUtil.java
new file mode 100644
index 0000000..811e702
--- /dev/null
+++ b/src/main/java/cn/xf/basedemo/common/utils/pay/AliPayUtil.java
@@ -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_no,trade_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();
+ }
+
+}
diff --git a/src/main/java/cn/xf/basedemo/common/utils/pay/WxPayUtil.java b/src/main/java/cn/xf/basedemo/common/utils/pay/WxPayUtil.java
new file mode 100644
index 0000000..c5c475f
--- /dev/null
+++ b/src/main/java/cn/xf/basedemo/common/utils/pay/WxPayUtil.java
@@ -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 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 -> "内部错误,请联系管理人员";
+// };
+// }
+
+}
diff --git a/src/main/java/cn/xf/basedemo/config/EsConfig.java b/src/main/java/cn/xf/basedemo/config/EsConfig.java
new file mode 100644
index 0000000..70e2c1c
--- /dev/null
+++ b/src/main/java/cn/xf/basedemo/config/EsConfig.java
@@ -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
+ */
+}
diff --git a/src/main/java/cn/xf/basedemo/config/SwaggerGroupApi.java b/src/main/java/cn/xf/basedemo/config/SwaggerGroupApi.java
new file mode 100644
index 0000000..7648d59
--- /dev/null
+++ b/src/main/java/cn/xf/basedemo/config/SwaggerGroupApi.java
@@ -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();
+ }
+}
diff --git a/src/main/java/cn/xf/basedemo/controller/business/UserController.java b/src/main/java/cn/xf/basedemo/controller/business/UserController.java
index e3a8f49..757ab8a 100644
--- a/src/main/java/cn/xf/basedemo/controller/business/UserController.java
+++ b/src/main/java/cn/xf/basedemo/controller/business/UserController.java
@@ -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);
+ }
+
}
diff --git a/src/main/java/cn/xf/basedemo/interceptor/TokenInterceptor.java b/src/main/java/cn/xf/basedemo/interceptor/TokenInterceptor.java
index 681e54e..dc74858 100644
--- a/src/main/java/cn/xf/basedemo/interceptor/TokenInterceptor.java
+++ b/src/main/java/cn/xf/basedemo/interceptor/TokenInterceptor.java
@@ -32,13 +32,15 @@ public class TokenInterceptor implements HandlerInterceptor {
//不拦截的请求列表
- private static final List EXCLUDE_PATH_LIST = Arrays.asList("/user/login", "/web/login");
+ private static final List 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;
}
//登录处理
diff --git a/src/main/java/cn/xf/basedemo/service/UserService.java b/src/main/java/cn/xf/basedemo/service/UserService.java
index 91d1dbd..7f8dc4f 100644
--- a/src/main/java/cn/xf/basedemo/service/UserService.java
+++ b/src/main/java/cn/xf/basedemo/service/UserService.java
@@ -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);
}
diff --git a/src/main/java/cn/xf/basedemo/service/impl/UserServiceImpl.java b/src/main/java/cn/xf/basedemo/service/impl/UserServiceImpl.java
index 8ae9115..7ddc1dd 100644
--- a/src/main/java/cn/xf/basedemo/service/impl/UserServiceImpl.java
+++ b/src/main/java/cn/xf/basedemo/service/impl/UserServiceImpl.java
@@ -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中不存在该用户");
+ }
}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 6660710..1c17631 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -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:
\ No newline at end of file
+#logging:
+# level:
+# root: DEBUG
\ No newline at end of file
diff --git a/src/main/resources/bootstrap.yml b/src/main/resources/bootstrap.yml
index aa2775d..749f4c5 100644
--- a/src/main/resources/bootstrap.yml
+++ b/src/main/resources/bootstrap.yml
@@ -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:
\ No newline at end of file
diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html
index 05d3f65..9f2dd6a 100644
--- a/src/main/resources/templates/login.html
+++ b/src/main/resources/templates/login.html
@@ -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,
})