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://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 ⭐ 支持一下吧!🎉✨ 你的支持是我持续优化的动力!❤️ 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, })