ref 升级脚手架依赖

This commit is contained in:
bootx
2025-08-21 12:48:17 +08:00
parent 1a3abea1e1
commit 6df7782ed2
107 changed files with 1419 additions and 6524 deletions

View File

@@ -7,33 +7,33 @@
<img src='https://gitee.com/bootx/dax-pay/badge/star.svg?theme=dark' alt='star'/>
<img src="https://img.shields.io/badge/Dax%20Pay-3.0.0-success.svg" alt="Build Status"/>
<img src="https://img.shields.io/badge/Author-Daxpay-orange.svg" alt="Build Status"/>
<img src="https://img.shields.io/badge/Spring%20Boot-3.4.3-blue.svg" alt="Downloads"/>
<img src="https://img.shields.io/badge/Spring%20Boot-3.5.4-blue.svg" alt="Downloads"/>
<img src="https://img.shields.io/badge/license-Apache%20License%202.0-green.svg"/>
</p>
# Dromara Dax-Pay(单商户多应用版)
# Dromara Dax-Pay(开源版)
## 使用须知
## 使用须知
`DaxPay`是一款基于`Apache License 2.0`协议分发的开源软件,受中华人民共和国相关法律法规的保护和限制,可以在符合[《用户授权使用协议》](用户授权使用协议.txt)和
[《Apache License 2.0》](LICENSE)开源协议情况下进行免费使用、学习和交流。**在使用前请阅读上述协议,如果不同意请勿进行使用。**
## 🍈项目介绍
## 项目介绍
> DaxPay是一套开源支付网关系统已经对接支付宝、微信支付、云闪付相关的接口。可以独立部署提供接口供业务系统进行调用不对原有系统产生影响。
> 同时与商业版使用同样的底层代码,保证统一接口尽量兼容,可以方便的升级为商业版。
## 🧭 特色功能
## 特色功能
- 支持支付、退款等支付相关的核心能力
- 封装各类支付通道的接口为统一的接口,方便业务系统进行调用,简化对接多种支付方式的复杂度
- 已对接`微信支付``支付宝``云闪付`相关的接口,并以扩展包的方式支持更多类型的通道
- 支持多应用配置,可以同时对接多个支付通道账号,方便多个业务系统对接
- 支持支付、退款、分账等支付相关的能力
- 提供网关支付功能:收银台、聚合支付、收款码牌等功能
- 提供`HTTP`方式接口调用能力,和`Java`版本的`SDK`,方便业务系统进行对接
- 接口请求和响应数据支持启用签名机制,保证交易安全可靠
- 提供管理端,方便运营人员进行管理和操作
## 📃 文档和源码地址
## 文档和源码地址
### 文档地址
在 [DaxPay文档站](https://daxpay.dromara.org/) 下的支付网关(DaxPay)模块下可以进行查阅相关文档,具体链接地址如下:
[快速指南](https://daxpay.dromara.org/single/guides/overview/项目介绍.html)、
@@ -49,7 +49,7 @@
| 网关前端地址 | [GITEE](https://gitee.com/bootx/dax-pay-h5) | [GITHUB](https://github.com/xxm1995/dax-pay-h5) | |
## 🏬 系统演示
## 系统演示
### 开源版:
> 注:演示账号部分功能权限未开放。
@@ -61,43 +61,33 @@
### 商业版
商户端: https://merchant.dax-pay.test.yibeiguangnian.cn/
运营端
https://admin.web.daxpay.cn/
代理端
https://agent.web.daxpay.cn/
商户端
https://merchant.web.daxpay.cn/
运营端: https://daxpay-web.test.yibeiguangnian.cn/
运营端演示用户: csadmin/123123
运营端测试: csadmin/123123
代理端演示用户: csdls/123123
商户端普通商户测试: cspt/123123
商户端普通商户演示: cspt/123123
商户端特约商户测试: csty/123123
商户端特约商户演示: csdl/123123
## 🥞 核心技术栈
## 核心技术栈
| 名称 | 描述 | 版本要求 |
|-------------|--------|------------------|
| Jdk | Java环境 | 21+ |
| Spring Boot | 开发框架 | 3.4.x |
| Redis | 分布式缓存 | 5.x版本及以上 |
| Spring Boot | 开发框架 | 3.5.x |
| Redis | 分布式缓存 | 7.x版本及以上 |
| Postgresql | 数据库 | Postgresql 12及以上 |
| MySQL | 数据库 | MySQL 8.0及以上 |
| Vue | 前端框架 | 3.x |
## 🛠️ 业务系统接入
> 业务系统想接入支付网关的话,不需要集成到业务系统里,只需要单独部署一份支付系统,然后业务系统通过接口调用即可拥有对应的支付能力,
不会对原业务系统的架构产生影响。如果是Java项目可以使用SDK简化接入流程 其他语言可以参照中的说明使用HTTP接口方式接入。
### Java客户端SDK
> SDK版本号与支付网关的版本保持一致如果需要使用请在pom.xml中添加如下依赖。SDK使用方式参考[SDK使用说明](https://daxpay.dromara.org/single/gateway/overview/SDK使用说明.html)。
```xml
<!-- 支付SDK -->
<dependency>
<groupId>org.dromara.daxpay</groupId>
<artifactId>daxpay-sdk</artifactId>
<version>${latest.version}</version>
</dependency>
```
## 🍎 系统截图
## 系统截图
### 通道配置
<img src="https://cdn.jsdelivr.net/gh/xxm1995/picx-images-hosting@master/20250427/wechat_2025-04-27_204334_543.lvxlxz86a.webp" alt="wechat_2025-04-27_204334_543" />
@@ -115,8 +105,6 @@
### 小程序快捷收银
<img src="https://cdn.jsdelivr.net/gh/xxm1995/picx-images-hosting@master/20250427/cbe6e332c55b241215787254951dc7ec.969y3b848r.webp" alt="cbe6e332c55b241215787254951dc7ec" width = "270" height = "570" />
## 🛣️ 路线图
[**历史更新记录**](/_doc/ChangeLog.md)
## 🥪 关于我们
@@ -127,11 +115,6 @@
<img src="https://cdn.jsdmirror.com/gh/xxm1995/picx-images-hosting@master/connect/1733360741745_d.83a33entp3.webp" width = "330" height = "500"/>
</p>
扫码加入钉钉交流群: [加群连接](https://qr.dingtalk.com/action/joingroup?code=v1,k1,AzkcWLa8J/OHXi+nTWwNRc68IAJ0ckWXEEIvrJofq2A=&_dt_no_comment=1&origin=11)
<p>
<img src="https://cdn.jsdmirror.com/gh/xxm1995/picx-images-hosting@master/connect/png-(1).7egk526qnp.webp" width = "400" height = "400"/>
</p>
微信扫码加小助手拉群: sdcit2020
<p>
<img alt="微信图片_20240226144703" height="480" src="https://cdn.jsdmirror.com/gh/xxm1995/picx-images-hosting@master/connect/微信图片_20240412152722.231nkeje2o.webp" width="330"/>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,209 +0,0 @@
# CHANGELOG
## [v3.0.0.beta5] 2025-05-01
## [v3.0.0.beta4] 2025-01-10
- 新增: 微信服务商支付支持
- 新增: 支付宝服务商支付支持
- 新增: 系统首页驾驶舱数据展示页we
- 新增: 微信支持公钥证书方式
- 优化: 商户应用增加停用功能
- 优化: 对各种交易增加新的同步失败异常处理, 防止同步失败后无限进行同步
- fix: 微信支付同步V3接口金额空指针问题
- fix: 订单支付成功重复更新问题
- fix: 优化自动分账没有默认分账组时的处理
## [v3.0.0.beta3] 2024-12-22
- 新增: 支持支付宝分账功能
- 新增: 支持微信分账功能
- 新增: 支持分账发起和分账完结功能
- 新增: 支持分账接收方配置功能
- 新增: 支持支付订单自动分账功能
- 新增: 自动同步分账订单状态功能
- 新增: 分账回调通知和分账消息通知功能
- 新增: 自动完结分账订单功能
- 新增: 分账同步功能
- 优化: 升级wxjava4.6.9.B并处理证书配置问题
- 优化: 调整分账接收者和分账组配置逻辑
- fix: 修复微信v2当面付发起失败问题
- fix: 修复微信v2分账参数未设置问题
- fix: sm3签名校验问题
- fix: SDK参数和返回对象与接口不一致修改
## [v3.0.0.beta2] 2024-12-05
- 新增: 增加PC收银台功能
- 新增: 增加H5收银台功能
- 新增: 增加聚合收银台功能
- 新增: 增加收银台配置功能
- 新增: 交易调试页增加收银台调试选项
- 新增: SDK增加收银台相关接口/认证相关接口
- 重构: 原支付收银台改为支付码牌, 一个应用支持多个码牌
- 重构: SDK命名空间更改为org.dromara.daxpay
- 优化: 微信通道添加单独的认证跳转地址
- 优化: 添加定时任务和事件监听服务
- 优化: 微信支付方式的判断逻辑,提高了系统稳定性
- fix: 系统参数使用到MySQL8保留字
- fix: Mysql 脚本缺少 缺失 表pay_api_const
- fix: H5构建版本限制错误, 限制为最低为node20+
- fix: 修复商户回调和通知的延迟逻辑
- fix: 商户应用类型命名错误
- fix: 修复对账差异逻辑
- fix: 修复微信支付同步金额为空的问题
- fix: 修复 BigDecimal 类型数据序列化和签名异常问题
## [v3.0.0.beta1] 2024-10-24
- 重构: JDK版本升级为21+, Spring Boot 版本升级为3.3.x, 前端组件升级为Antd Vue 4.x + Vite5
- 重构: 数据库更新为PostgreSQL + MySQL8.x 双版本支持
- 重构: 脚手架全新重构, 精简和优化各种功能模块, 支持基于有赞文章实现的Redis延时队列
- 重构: 支持多应用模式, 每个应用都可以配置单独一套支付通道、通知订阅、收款码牌等配置, 可以实现同时对接多个业务系统
- 重构: 项目结构进行重构, 修改为支付核心+通道扩展+功能插件的方式, 实现功能模块的耦合拆分, 便于进行功能扩展和二次开发
- 重构: 对账功能进行重构, 更加简单直观和易用
- 重构: 删除订单调整相关逻辑, 分别放到订单同步和回调处理中, 只保留
- 新增: 微信支付同时支持V2和V3版本的接口, 同时V3版本支持付款码和撤销接口
- 新增: 交易调试接口功能, 用于开发时对交易流程进行测试
- 新增: 获取通道认证信息的测试页, 便于获取微信、支付宝等用户的认证信息
- 新增: 增加简易移动端收款码牌功能, 支持自动跳转到微信或支付宝对应的H5收银台, 支持自主配置所使用的通道和支付方式
- 新增: 增加商户通知功能, 通过订阅指定类型的通知类型, 将会在符合条件时推送到预留的客户系统地址上
- 优化: 各类错误处理进行统一化处理
- 优化: 优化商户回调功能, 简化配置项, 使用延时器优化重复推送的逻辑
- 优化: 减少在各种流程中上下文对象的线程变量使用, 非必需的上下文对象使用方法调用明确传输
- 优化: 对各类状态码进行优化合并, 如转账接收方类型、分账接收方类型等
- 优化: 所有金额统一为元, 保留两位小数
## [v2.0.8] 2024-06-27
- 新增: 撤销接口
- 新增: 转账功能
- 新增: DEMO增加转账演示功能
- 新增: DEMO增加获取OpenID功能
- 新增: 支付宝支持JSAPI方式支付
- 新增: 绑定对账接收方增加扫码获取微信OpenID和支付宝OpenId功能
- 新增: 支付宝微信等消息通知地址支持一键生成
- 新增: 请求IP参数增加正则校验
- 优化: 手动发起分账重试参数修正
- 优化: 细分各种支付异常类和编码
- 优化: 支付宝SDK修改为官方SDK
- 优化: 界面金额统一调整为元
- 优化: 上下文对象进行优化精简
- 优化: 支付接口公共参数添加随机数字段, 预防重放问题
- 优化: 请求接口增加有效期校验, 超时后失效
- 优化: 数据库表进行规则, 字段设置长度, 增加索引, 对应请求参数添加校验
- 优化: 订单和扩展信息进行合并
- 优化: 支付通道两个独立的配置进行合并为一个
- 优化: 平台配置增加接口请求有效时长配置
- 优化: 平台配置和接口配置删除回调地址配置
- 优化: 接口配置删除是否验签配置和回调地址
- 优化: 分账订单相关命名统一为Alloc
- 优化: 支付订单拆分退款状态为单独的字段
- 优化: 策略工厂修改为统一的通用策略工厂
- 优化: 支付和退款达到终态不可以再回退回之前的状态
- 优化: 优化认证授权地址配置, 拆分为支持单独配置
- 优化: 优化各类网址配置, 兼容结尾带/和不带/
- fix: 修复支付关闭参数名称不正确问题
- fix: 退款回调消息字段不一致导致验签不通过问题
- fix: 云闪付空指针问题
## [v2.0.7] 2024-06-05
- 新增: 资金流水记录功能
- 新增: 分账功能支持分账组分账和自己传接收方进行分账
- 新增: 分账接收的添加、删除、查询接口调用
- 新增: 分账发起、完结、同步功能支持接口调用
- 新增: 支持自动分账和手动发起分账两种
- 新增: 分账通知发送功能
- 优化: 对超时订单进行处理(数据库定时同步)
- 优化: 订单金额小于0.01元直接忽略不进行分账,增加新状态,
- 优化: 优化签名注解和上下文初始化注解切面
- 优化: 分账重试会自动根据分账失败和
- 优化: 优化签名注解和上下文初始化注解切面, 更方便初始化上下文
- fix: 对账差异单数据不一致处理异常, 本地待对账订单类型记录错误
- fix: 订单超时任务注册任务错误id改为订单号
- fix: 系统中金额分转元精度异常问题
- fix: 同步回调处理参数订单号接收失败
- fix: 支付和退款消息签名值不一致问题
- fix: 分账发起时错误的使用订单号作为分账号
## [v2.0.6] 2024-05-15
- 新增: 下载原始对账单功能,转换为指定格式进行下载
- 新增: 增加对账结果计算和显示,以及对单差异数据查看功能
- 新增: 自动分账功能,支付完成后自动根据默认分账组将订单分账
- 新增: 三方支付通道订单号规则优化: 支付P、退款R、分账A可以根据环境加前缀DEV_、DEMO_、PRE_
- 优化: 去除组合支付概念,删除现金支付和储值卡支付方式,系统整体复杂度降低一半以上
- 优化: 消息通知发送流程改造,不在使用复杂继承组合关系,只保留一级类继承关系
- 优化: 回调通知处理不再使用继承模式修改为组合模式提高阅读和debug的便利性
- 优化: 支付同步、回调和退款同步、回调去除组合支付导致的特殊处理逻辑
- 优化: 统一公共请求参数和响应参数,同时响应参数格式,便于进行统一处理
- 优化: 统一参数命名规则,包括支付、退款、对账、分账等相关参数的属性,实现风格的统一
- 优化: 使用切面统一处理API调用异常, 做统一包装返回
- 优化: 金额显示统一使用元
- 优化: 前端查询条件适配,统一页面交互逻辑,初步完成管理端的功能完备性
- 优化: 支持自动同步对账结果,并自动对分账单进行完结
- 优化: 基础脚手架从jar集成修改为源码集成
- fix: 自动同步任务不生效
- fix: 算收款金额时对产生退款的支付订单未进行计算
## [v2.0.5] 2024-04-18
- 新增: 支持支付宝分账功能
- 新增: 支持微信分账功能
- 新增: 分账接收者和分账组管理
- 新增: 支持分账结果同步功能
- 新增: 支付通道配置中支持是否支持分账
- 新增: SDK支持分账接口
- 优化: 收银台演示支持设置是否分账
- fix: 修复创建支付订单报错时, 订单保存数据不完整
## [v2.0.4] 2024-03-26
- 新增: 首页驾驶舱功能: 各通道收入和支付情况
- 新增: 云闪付支持对账功能
- 新增: 对账文件支持手动导入
- 新增: 结算台DEMO增加云闪付示例
- 新增: 增加支付限额,包括整单限额和通道限额
- 优化: 支付流程也改为先落库后支付情况, 避免极端情况导致掉单
- 优化: 前端列表状态显示优化
- fix: 对账订单流水号生成规则不是按天生成修改
## [v2.0.3] 2024-03-16
- 增加云闪付通道,支持支付、退款、同步、回调处理
- 增加定时同步退款中的退款订单任务
- 增加通知任务订单的状态类型,例如订单关闭、成功、失败等
- 增加退款操作支持重试
- 增加手动触发通知任务消息的发送功能
## [v2.0.2] 2024-03-06
- 增加微信支付对账功能
- 增加支付宝支付对账功能
- 优化: 修复策略对订单时间和状态字段的变更优化
- fix: 前端支付订单查询条件中"支付ID"条件不生效
## [v2.0.1] 2024-02-27
- 增加支付、退款时客户通知功能,支持多次重发
- 开源文档增加支付通知和退款通知文档
- 增加客户通知任务记录功能
- 支持钱包支付、流水记录、各类操作等功能
- 支持储值卡支付、流水记录、各类操作等功能
- 支持现金支付和流水记录功能
- 增加支付宝流水记录功能
- 增加微信流水记录功能
- 变更: 废弃调用接口时的`version`字段调用时不再进行传输SDK中同步进行删除
- 优化: 订单支持关闭时间记录
- 优化: 增加退款订单扩展记录
- 优化: SDK增加简单退款、多通道退款等多中测试样例
- 优化: IJPay进行Https请求时, TLS版本使用读取JDK中支持的版本
- fix: 同步支付通道订单不能正确生成
- fix: 修复聚合条码支付时付款码未传输问题
- fix: 修复微信退款同步时, 错误信息未保存问题
- fix: 修复手动发起退款时上下文未进行初始化的问题
- fix: 修复简单退款选择全部退款时报错问题
- fix: 修复退款时未检验退款金额问题,导致可以退款余额可以大于可退余额
## [v2.0.0] 2024-02-14
- 支持支付宝支付: 扫码支付、付款码支付、PC支付、H5支付
- 支持微信支付: 扫码支付、WAP支付、公众号支付
- 增加聚合支付演示功能,支持支付宝和微信支付
- 增加PC收银台演示功能各种类型的支付
- 增加手机收银台演示功能,支持在微信、支付宝、浏览器中发起对应的请求
- 提供Java版本SDK简化业务系统对支付网关的调用
- 支持请求参数签名和验签机制已经支持SHA256和MD5
- 支持支付订单超时自动进行关闭
- 支持支付订单手动关闭功能
- 支持支付退款功能,可以进行全部退款或部分退款
- 支持支付同步功能,通过同步接口可以获取第三方支付网关的状态
- 支持支付和退款订单的修复功能,根据取第三方支付网关订单的状态,对订单进行修正,如支付同步、退款同步、消息回调等可触发
- 部分支付对账功能,已经实现支付宝和微信对账单下载解析和保存的功能
- 支持对各支付通道进行管理包括是否启用、显示Logo图等
- 支持对支付网关对外暴露的接口进行管理,支持启停用、是否验签、是否消息通知等功能
- 去除调用时的用户概念,作为独立的支付网关使用
- 组合支付已预先进行支持,支持一个异步支付+多个同步支付通道组合进行收单支付
- 记录支付时出现的回调记录、同步记录、修复记录、关闭记录

View File

@@ -1,9 +0,0 @@
# 单商户
## 3.0.0.beta5 功能优化和服务商支付
- [ ] 网关配套移动端开发
- [ ] 同步回调页
- [ ] 增加首页驾驶舱功能
- [ ] 支付时付款码参数提升到PayParam, 简化调用方式
## 任务池
- [ ] 分账重试
- [ ] 同步接口优化, 返回同步完的数据

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform-common</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<artifactId>common-config</artifactId>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform-common</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<artifactId>common-exception-handler</artifactId>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform-common</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<artifactId>common-header-holder</artifactId>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform-common</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<artifactId>common-jackson</artifactId>

View File

@@ -8,6 +8,7 @@ import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;
/**
* jackson常用工具类封装
@@ -130,4 +131,24 @@ public class JacksonUtil {
throw new RuntimeException("json反序列化失败");
}
}
/**
* 对象转为map
*/
public Map<String, Object> parseObj(Object mchApply) {
return parseObj(mchApply,true);
}
@SuppressWarnings("unchecked")
public Map<String, Object> parseObj(Object mchApply, boolean ignoreNull) {
try {
if (ignoreNull) {
return ignoreNullObjectMapper.convertValue(mchApply, Map.class);
}
return objectMapper.convertValue(mchApply, Map.class);
} catch (IllegalArgumentException e) {
log.error(e.getMessage(), e);
throw new RuntimeException("json反序列化失败");
}
}
}

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform-common</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<artifactId>common-log</artifactId>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform-common</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<artifactId>common-mybatis-plus</artifactId>
@@ -63,6 +63,17 @@
<artifactId>common-spring</artifactId>
<version>${bootx-platform.version}</version>
</dependency>
<!-- hutool 组件 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-db</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
<version>${hutool.version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>

View File

@@ -1,8 +1,8 @@
package cn.bootx.platform.common.mybatisplus.handler;
import cn.bootx.platform.core.util.JsonUtil;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
@@ -35,13 +35,13 @@ public class IntegerListTypeHandler extends AbstractJsonTypeHandler<List<Integer
@Override
public List<Integer> parse(String json) {
if (StrUtil.isNotBlank(json)){
return JsonUtil.toBean(json, new TypeReference<>() {}, false);
return JSONUtil.toBean(json, new TypeReference<>() {}, false);
}
return List.of();
}
@Override
public String toJson(List<Integer> obj) {
return JsonUtil.toJsonStr(obj);
return JSONUtil.toJsonStr(obj);
}
}

View File

@@ -3,6 +3,7 @@ package cn.bootx.platform.common.mybatisplus.handler;
import cn.bootx.platform.core.util.JsonUtil;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
@@ -30,13 +31,13 @@ public class LongListTypeHandler extends AbstractJsonTypeHandler<List<Long>> {
@Override
public List<Long> parse(String json) {
if (StrUtil.isNotBlank(json)){
return JsonUtil.toBean(json, new TypeReference<>() {}, false);
return JSONUtil.toBean(json, new TypeReference<>() {}, false);
}
return List.of();
}
@Override
public String toJson(List<Long> obj) {
return JsonUtil.toJsonStr(obj);
return JSONUtil.toJsonStr(obj);
}
}

View File

@@ -39,6 +39,6 @@ public class StringListTypeHandler extends AbstractJsonTypeHandler<List<String>>
@Override
public String toJson(List<String> obj) {
return JsonUtil.toJsonStr(obj);
return JSONUtil.toJsonStr(obj);
}
}

View File

@@ -2,11 +2,13 @@ package cn.bootx.platform.common.mybatisplus.util;
import cn.bootx.platform.common.mybatisplus.function.ToResult;
import cn.bootx.platform.core.annotation.BigField;
import cn.bootx.platform.core.exception.BizException;
import cn.bootx.platform.core.rest.param.PageParam;
import cn.bootx.platform.core.rest.result.PageResult;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
@@ -20,7 +22,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.experimental.UtilityClass;
import org.apache.ibatis.reflection.property.PropertyNamer;
import javax.sql.DataSource;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -159,4 +164,17 @@ public class MpUtil {
return null;
}
/**
* 获取当前数据库类型, 别忘了关闭, 不然会连接池泄露
*/
public String getDbType(){
try {
try (Connection connection = SpringUtil.getBean(DataSource.class).getConnection()) {
return connection.getMetaData().getDatabaseProductName();
}
} catch (SQLException e) {
throw new BizException("获取数据库类型失败");
}
}
}

View File

@@ -0,0 +1,312 @@
package cn.bootx.platform.common.mybatisplus.util;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.db.Db;
import cn.hutool.db.Entity;
import cn.hutool.db.handler.BeanHandler;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.http.HtmlUtil;
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.PropertyPlaceholderHelper;
import javax.sql.DataSource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* 数据库工具类
*
* @author nn200433
* @date 2024-03-28 09:32:34
*/
@Slf4j
public class MybatisDbUtil {
private static final String PARAM_KEY_PRE = "ew.paramNameValuePairs.";
private static final String UPDATE_SET_PREFIX = "SET";
private static final String SQL_PREFIX_AND_SUFFIX = "'";
public static final String PLACEHOLDER_PREFIX = "#{";
private static final String PLACEHOLDER_SUFFIX = "}";
private static final PropertyPlaceholderHelper PLACEHOLDER_HELPER = new PropertyPlaceholderHelper(PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX);
/**
* 物理删除
*
* @param tableClz 数据表实体类型
* @param ids 主键
* @return int 影响行数
* @author nn200433
*/
public static int delete(Class<?> tableClz, String... ids) {
final TableInfo tableInfo = getTableInfo(tableClz);
final String tableName = tableInfo.getTableName();
final String keyColumn = tableInfo.getKeyColumn();
Assert.notBlank(keyColumn, "{} 未设置 @TableId 注解!", tableClz);
int count = 0;
try {
count = Db.use(dataSource()).del(Entity.create(tableName).set(keyColumn, ids));
} catch (Exception e) {
log.error("", e);
}
return count;
}
/**
* 物理删除
*
* <p>
* 请使用{@link com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper}构造wrapper
* </p>
*
* @param wrapper 包装物
* @return int 影响行数
* @author nn200433
*/
public static <T> int delete(AbstractWrapper<T, ?, ?> wrapper) {
final Class<T> entityClass = (Class<T>) getEntityClass(wrapper);
final TableInfo tableInfo = getTableInfo(entityClass);
final String whereSql = wrapper.getCustomSqlSegment();
final String tableName = tableInfo.getTableName();
final Map<String, Object> paramNameValuePairs = wrapper.getParamNameValuePairs();
int count = 0;
try {
final String sql = getSql(SqlMethod.DELETE, tableName, StrUtil.EMPTY, StrUtil.EMPTY, whereSql, null, paramNameValuePairs);
count = Db.use(dataSource()).execute(sql);
} catch (Exception e) {
log.error("", e);
}
return count;
}
/**
* 通过ID查询数据忽略逻辑删除
*
* @param id 主键id值
* @param entityClass 返回的实体类型
* @return {@link T }
* @author nn200433
*/
public static <T> T selectById(Object id, Class<T> entityClass) {
final TableInfo tableInfo = getTableInfo(entityClass);
final String keyColumn = tableInfo.getKeyColumn();
final QueryWrapper<T> wrapper = new QueryWrapper<T>().eq(keyColumn, id);
final String tableName = tableInfo.getTableName();
final String whereSql = wrapper.getCustomSqlSegment();
final Map<String, Object> paramNameValuePairs = wrapper.getParamNameValuePairs();
T result = null;
try {
final String sql = getSql(SqlMethod.SELECT_BY_MAP, tableName, StrUtil.EMPTY, tableInfo.getAllSqlSelect(), whereSql, null, paramNameValuePairs);
result = Db.use(dataSource()).query(sql, new BeanHandler<T>(entityClass));
} catch (Exception e) {
log.error("", e);
}
return result;
}
/**
* 查询列表(忽略逻辑删除)
*
* <p>
* 请使用{@link com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper}构造wrapper。
* <br/>
* 举例new LambdaQueryWrapper<SmsTemplate>(Entity.class).eq(Entity::getCode, "2");
* </p>
*
* @param wrapper 包装物
* @return {@link List }<{@link T }>
* @author nn200433
*/
public static <T> List<T> selectList(AbstractWrapper<T, ?, ?> wrapper) {
List<T> resultList = new ArrayList<T>();
final Class<T> entityClass = (Class<T>) getEntityClass(wrapper);
final TableInfo tableInfo = getTableInfo(entityClass);
final String whereSql = wrapper.getCustomSqlSegment();
final String columnSql = wrapper.getSqlSelect();
final String tableName = tableInfo.getTableName();
final Map<String, Object> paramNameValuePairs = wrapper.getParamNameValuePairs();
try {
final String sql = getSql(SqlMethod.SELECT_LIST, tableName, StrUtil.EMPTY, StrUtil.blankToDefault(columnSql, tableInfo.getAllSqlSelect()), whereSql, null, paramNameValuePairs);
resultList = Db.use(dataSource()).query(sql, entityClass);
} catch (Exception e) {
log.error("", e);
}
return resultList;
}
/**
* 更新(忽略逻辑删除)
*
* @param entity 实体类
* @return int 影响行数
* @author nn200433
*/
public static <T> int update(T entity) {
return update(entity, null);
}
/**
* 通过条件更新数据(忽略逻辑删除)
* <p>
* 请使用{@link com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper}构造wrapper
* </p>
*
* @param entity 实体类
* @param wrapper 包装物
* @return int 影响行数
* @author nn200433
*/
public static <T> int update(T entity, AbstractWrapper<T, ?, ?> wrapper) {
final TableInfo tableInfo = getTableInfo(entity.getClass());
final String tableName = tableInfo.getTableName();
if (null == wrapper) {
final String keyProperty = tableInfo.getKeyProperty();
Assert.notBlank(keyProperty, "{} 未设置 @TableId 注解!", keyProperty);
wrapper = new UpdateWrapper<T>(entity).eq(keyProperty, ReflectUtil.getFieldValue(entity, keyProperty));
}
final String whereSql = wrapper.getCustomSqlSegment();
final Map<String, Object> paramNameValuePairs = wrapper.getParamNameValuePairs();
int count = 0;
try {
final String sql = getSql(SqlMethod.UPDATE, tableName, StrUtil.EMPTY, tableInfo.getAllSqlSet(Boolean.TRUE, StrUtil.EMPTY), whereSql, BeanUtil.beanToMap(entity), paramNameValuePairs);
count = Db.use(dataSource()).execute(sql);
} catch (Exception e) {
log.error("", e);
}
return count;
}
/**
* 获取sql
*
* @param method SQL方法
* @param tableName 数据库表名称
* @param firstSql firtSql
* @param columnSql 字段sql
* @param whereSql where条件sql
* @param setMap set参数
* @param paramNameValuePairs where条件值
* @return {@link String }
* @author nn200433
*/
private static String getSql(SqlMethod method, String tableName, String firstSql, String columnSql, String whereSql,
Map<String, Object> setMap, Map<String, Object> paramNameValuePairs) {
final Map<String, Object> paramMap = new HashMap<String, Object>(paramNameValuePairs.size());
for (final Map.Entry<String, Object> entry : paramNameValuePairs.entrySet()) {
paramMap.put(PARAM_KEY_PRE + entry.getKey(), entry.getValue());
}
if (CollUtil.isNotEmpty(setMap)) {
paramMap.putAll(setMap);
}
// 害人不浅,包了这么标签
String originalSql = null;
switch (method) {
case DELETE:
originalSql = String.format(method.getSql(), tableName, whereSql, StrUtil.EMPTY);
break;
case UPDATE:
columnSql = StrUtil.addPrefixIfNot(columnSql, UPDATE_SET_PREFIX + StrUtil.SPACE);
columnSql = StrUtil.removeSuffix(columnSql, StrUtil.COMMA);
originalSql = String.format(method.getSql(), tableName, columnSql, whereSql, StrUtil.EMPTY);
break;
case SELECT_LIST:
// 判断个锤子OrderBy注解麻烦
originalSql = String.format(method.getSql(), firstSql, columnSql, tableName, whereSql, StrUtil.EMPTY, StrUtil.EMPTY);
break;
case SELECT_BY_MAP:
// 垃圾 SELECT_BY_ID ,限制那么多
originalSql = String.format(method.getSql(), columnSql, tableName, whereSql, StrUtil.EMPTY);
break;
default:
}
final String preSql = StrUtil.trim(HtmlUtil.cleanHtmlTag(originalSql));
return resolveParams(preSql, paramMap);
}
/**
* 从wrapper中尝试获取实体类型
*
* @param queryWrapper 条件构造器
* @param <T> 实体类型
* @return 实体类型
*/
private static <T> Class<T> getEntityClass(AbstractWrapper<T, ?, ?> queryWrapper) {
Class<T> entityClass = queryWrapper.getEntityClass();
if (entityClass == null) {
T entity = queryWrapper.getEntity();
if (entity != null) {
entityClass = (Class<T>) entity.getClass();
}
}
Assert.notNull(entityClass, "error: can not get entityClass from wrapper");
return entityClass;
}
/**
* 获取表信息,获取不到报错提示
*
* @param entityClass 实体类
* @param <T> 实体类型
* @return 对应表信息
*/
private static <T> TableInfo getTableInfo(Class<T> entityClass) {
return Optional.ofNullable(TableInfoHelper.getTableInfo(entityClass))
.orElseThrow(() -> ExceptionUtils.mpe("error: can not find TableInfo from Class: \"%s\".", entityClass.getName()));
}
/**
* 解析参数
*
* @param str 包含要替换的占位符的值
* @param param 参数
* @return @return {@link String }
* @author nn200433
*/
private static String resolveParams(String str, Map<String, Object> param) {
return PLACEHOLDER_HELPER.replacePlaceholders(str, key -> {
// final String v = MapUtil.(param, key);
final Object v = param.get(key);
if (null == v) {
return "null";
}
final Class<?> clz = v.getClass();
if (clz.equals(Date.class)) {
final Date vd = (Date) v;
return StrUtil.wrap(DateUtil.format(vd, DatePattern.NORM_DATETIME_FORMAT), SQL_PREFIX_AND_SUFFIX);
} else if (clz.equals(LocalDateTime.class)) {
final LocalDateTime vd = (LocalDateTime) v;
return StrUtil.wrap(DateUtil.formatLocalDateTime(vd), SQL_PREFIX_AND_SUFFIX);
}
return StrUtil.wrap(Convert.toStr(v), SQL_PREFIX_AND_SUFFIX);
});
}
/**
* 获取数据源
*
* @return {@link DataSource }
* @author nn200433
*/
private static DataSource dataSource() {
return SpringUtil.getBean(DataSource.class);
}
}

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform-common</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<artifactId>common-redis</artifactId>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform-common</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<artifactId>common-spring</artifactId>

View File

@@ -1,6 +1,7 @@
package cn.bootx.platform.common.spring.configuration;
import cn.bootx.platform.core.util.JsonUtil;
import cn.hutool.json.JSONUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
@@ -39,7 +40,7 @@ public class AsyncExecutorConfiguration implements AsyncConfigurer {
@Override
public void handleUncaughtException(Throwable throwable, Method method, Object... objects) {
log.error("异步方法中发生异常,方法:{},参数:{},异常:{}", method.getName(), JsonUtil.toJsonStr(objects),
log.error("异步方法中发生异常,方法:{},参数:{},异常:{}", method.getName(), JSONUtil.toJsonStr(objects),
throwable.getMessage());
log.error("详细异常信息", throwable);
}

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform-common</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<artifactId>common-swagger</artifactId>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -6,7 +6,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 组件忽略注解
* 忽略租户数据隔离注解
* @author xxm
* @since 2024/6/25
*/

View File

@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
*/
@Getter
@Setter
public class BaseResult {
public class BaseResult{
@Schema(description = "主键")
private Long id;

View File

@@ -8,7 +8,7 @@ import lombok.experimental.UtilityClass;
import java.util.Collection;
/**
* json工具类, 基于hutool的进行封装,
* json工具类, 基于hutool的进行封装, 仅用于与业务系统交互使用, 为了保持与SDK
* 对java8的LocalDateTime时间格式进行转换, 但无法处理LocalDate, LocalTime格式, 需要使用JacksonUtil进行处理
* @author xxm
* @since 2024/6/28
@@ -17,32 +17,6 @@ import java.util.Collection;
public class JsonUtil {
private final JSONConfig JSON_CONFIG = JSONConfig.create().setDateFormat(DatePattern.NORM_DATETIME_PATTERN);
/**
* 转换为实体
*/
public <T> T toBean(String json, Class<T> clazz){
JSONObject jsonObject = new JSONObject(json, JSON_CONFIG);
return JSONUtil.toBean(jsonObject, clazz);
}
/**
* 转换为实体
*/
public <T> T toBean(String json, TypeReference<T> reference){
JSON parse = JSONUtil.parse(json, JSON_CONFIG);
return parse.toBean(reference);
}
/**
* 转换为实体
*/
public <T> T toBean(String json, TypeReference<T> reference, boolean ignoreError){
JSONConfig jsonConfig = JSONConfig.create()
.setDateFormat(DatePattern.NORM_DATETIME_PATTERN)
.setIgnoreError(ignoreError);
JSON parse = JSONUtil.parse(json, jsonConfig);
return parse.toBean(reference);
}
/**
* 序列化为字符串
@@ -61,9 +35,10 @@ public class JsonUtil {
}
/**
* JSON字符串转JSONObject对象
* 转换为实体, 仅供处理验签时使用, 其他场景不要使用
*/
public JSONObject parseObj(String jsonStr){
return JSONUtil.parseObj(jsonStr, JSON_CONFIG);
public <T> T toBean(String json, TypeReference<T> reference) {
JSON parse = JSONUtil.parse(json, JSON_CONFIG);
return parse.toBean(reference);
}
}

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<modules>
<module>service-baseapi</module>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform-service</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<artifactId>service-baseapi</artifactId>

View File

@@ -0,0 +1,97 @@
package cn.bootx.platform.baseapi.controller.protocol;
import cn.bootx.platform.baseapi.param.protocol.UserProtocolParam;
import cn.bootx.platform.baseapi.param.protocol.UserProtocolQuery;
import cn.bootx.platform.baseapi.result.protocol.UserProtocolResult;
import cn.bootx.platform.baseapi.service.protocol.UserProtocolService;
import cn.bootx.platform.core.annotation.IgnoreAuth;
import cn.bootx.platform.core.annotation.RequestGroup;
import cn.bootx.platform.core.annotation.RequestPath;
import cn.bootx.platform.core.rest.Res;
import cn.bootx.platform.core.rest.param.PageParam;
import cn.bootx.platform.core.rest.result.PageResult;
import cn.bootx.platform.core.rest.result.Result;
import cn.bootx.platform.core.validation.ValidationGroup;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 用户协议
* @author xxm
* @since 2025/5/9
*/
@Validated
@Tag(name = "用户协议")
@RequestGroup(groupCode = "UserProtocol", groupName = "用户协议", moduleCode = "baseapi" )
@RestController
@RequestMapping("/user/protocol")
@RequiredArgsConstructor
public class UserProtocolController {
private final UserProtocolService userProtocolService;
@RequestPath("分页")
@Operation(summary = "分页")
@GetMapping("/page")
public Result<PageResult<UserProtocolResult>> page(PageParam pageParam, UserProtocolQuery query){
return Res.ok(userProtocolService.page(pageParam, query));
}
@RequestPath("新增")
@Operation(summary = "新增")
@PostMapping("/add")
public Result<Void> add(@RequestBody @Validated(ValidationGroup.add.class) UserProtocolParam param){
userProtocolService.add(param);
return Res.ok();
}
@RequestPath("修改")
@Operation(summary = "修改")
@PostMapping("/update")
public Result<Void> update(@RequestBody @Validated(ValidationGroup.edit.class) UserProtocolParam param){
userProtocolService.update(param);
return Res.ok();
}
@RequestPath("删除")
@Operation(summary = "删除")
@PostMapping("/delete")
public Result<Void> delete(@NotNull(message = "主键不可为空") Long id){
userProtocolService.delete(id);
return Res.ok();
}
@RequestPath("查询")
@Operation(summary = "查询")
@GetMapping("/findById")
public Result<UserProtocolResult> findById(@NotNull(message = "主键不可为空") Long id){
return Res.ok(userProtocolService.findById(id));
}
@IgnoreAuth
@Operation(summary = "查询默认协议")
@GetMapping("/findDefault")
public Result<UserProtocolResult> findDefault(@NotNull(message = "协议类型不可为空") String type){
return Res.ok(userProtocolService.findDefault(type));
}
@RequestPath("设置默认")
@Operation(summary = "设置默认")
@PostMapping("/setDefault")
public Result<Void> setDefault(@NotNull(message = "主键不可为空") Long id){
userProtocolService.setDefault(id);
return Res.ok();
}
@RequestPath("取消默认")
@Operation(summary = "取消默认")
@PostMapping("/cancelDefault")
public Result<Void> cancelDefault(@NotNull(message = "主键不可为空") Long id){
userProtocolService.cancelDefault(id);
return Res.ok();
}
}

View File

@@ -0,0 +1,21 @@
package cn.bootx.platform.baseapi.convert.protocol;
import cn.bootx.platform.baseapi.entity.protocol.UserProtocol;
import cn.bootx.platform.baseapi.param.protocol.UserProtocolParam;
import cn.bootx.platform.baseapi.result.protocol.UserProtocolResult;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
/**
*用户协议管理
* @author xxm
* @since 2025/5/9
*/
@Mapper
public interface UserProtocolConvert {
UserProtocolConvert CONVERT = Mappers.getMapper(UserProtocolConvert.class);
UserProtocolResult toResult(UserProtocol userProtocol);
UserProtocol toEntity(UserProtocolParam param);
}

View File

@@ -2,6 +2,7 @@ package cn.bootx.platform.baseapi.dao.dict;
import cn.bootx.platform.baseapi.entity.dict.DictionaryItem;
import cn.bootx.platform.common.mybatisplus.base.MpIdEntity;
import cn.bootx.platform.common.mybatisplus.base.MpRealDelEntity;
import cn.bootx.platform.common.mybatisplus.impl.BaseManager;
import cn.bootx.platform.common.mybatisplus.util.MpUtil;
import cn.bootx.platform.core.rest.param.PageParam;
@@ -63,7 +64,11 @@ public class DictionaryItemManager extends BaseManager<DictionaryItemMapper, Dic
}
public void updateDictCode(Long dictId, String dictCode) {
lambdaUpdate().set(DictionaryItem::getDictCode, dictCode).eq(DictionaryItem::getDictId, dictId).update();
lambdaUpdate()
.set(DictionaryItem::getDictCode, dictCode)
.eq(DictionaryItem::getDictId, dictId)
.setIncrBy(MpRealDelEntity::getVersion, 1)
.update();
}
public List<DictionaryItem> findAllByEnable(boolean enable) {

View File

@@ -30,21 +30,21 @@ public class SystemParamManager extends BaseManager<SystemParamMapper, SystemPar
* 根据键名获取键值
*/
public Optional<SystemParameter> findByKey(String key) {
return this.findByField(SystemParameter::getKey, key);
return this.findByField(SystemParameter::getParamKey, key);
}
/**
* key重复检查
*/
public boolean existsByKey(String key) {
return existedByField(SystemParameter::getKey, key);
return existedByField(SystemParameter::getParamKey, key);
}
/**
* key重复检查
*/
public boolean existsByKey(String key, Long id) {
return existedByField(SystemParameter::getKey, key, id);
return existedByField(SystemParameter::getParamKey, key, id);
}
/**
@@ -54,7 +54,7 @@ public class SystemParamManager extends BaseManager<SystemParamMapper, SystemPar
Page<SystemParameter> mpPage = MpUtil.getMpPage(pageParam);
return lambdaQuery().orderByDesc(MpIdEntity::getId)
.like(StrUtil.isNotBlank(param.getName()), SystemParameter::getName, param.getName())
.like(StrUtil.isNotBlank(param.getKey()), SystemParameter::getKey, param.getKey())
.like(StrUtil.isNotBlank(param.getKey()), SystemParameter::getParamKey, param.getKey())
.page(mpPage);
}

View File

@@ -0,0 +1,79 @@
package cn.bootx.platform.baseapi.dao.protocol;
import cn.bootx.platform.baseapi.entity.protocol.UserProtocol;
import cn.bootx.platform.baseapi.param.protocol.UserProtocolQuery;
import cn.bootx.platform.common.mybatisplus.base.MpRealDelEntity;
import cn.bootx.platform.common.mybatisplus.impl.BaseManager;
import cn.bootx.platform.common.mybatisplus.query.generator.QueryGenerator;
import cn.bootx.platform.common.mybatisplus.util.MpUtil;
import cn.bootx.platform.core.rest.param.PageParam;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/**
* 用户协议管理
* @author xxm
* @since 2025/5/9
*/
@Slf4j
@Repository
@RequiredArgsConstructor
public class UserProtocolManager extends BaseManager<UserProtocolMapper, UserProtocol> {
/**
* 分页
*/
public Page<UserProtocol> page(PageParam pageParam, UserProtocolQuery query){
Page<UserProtocol> mpPage = MpUtil.getMpPage(pageParam, UserProtocol.class);
QueryWrapper<UserProtocol> generator = QueryGenerator.generator(query);
return this.page(mpPage,generator);
}
/**
* 根据分类查询默认协议
*/
public Optional<UserProtocol> findDefault(String type){
return this.lambdaQuery()
.eq(UserProtocol::getType,type)
.eq(UserProtocol::getDefaultProtocol,true)
.oneOpt();
}
/**
* 清除默认协议
*/
public void clearDefault(String type){
this.lambdaUpdate()
.eq(UserProtocol::getType,type)
.set(UserProtocol::getDefaultProtocol,false)
.setIncrBy(MpRealDelEntity::getVersion, 1)
.update();
}
/**
* 设置默认协议
*/
public void setDefault(Long id){
this.lambdaUpdate()
.eq(UserProtocol::getId,id)
.set(UserProtocol::getDefaultProtocol,true)
.setIncrBy(MpRealDelEntity::getVersion, 1)
.update();
}
/**
* 取消默认协议
*/
public void cancelDefault(Long id){
this.lambdaUpdate()
.eq(UserProtocol::getId,id)
.set(UserProtocol::getDefaultProtocol,false)
.setIncrBy(MpRealDelEntity::getVersion, 1)
.update();
}
}

View File

@@ -0,0 +1,14 @@
package cn.bootx.platform.baseapi.dao.protocol;
import cn.bootx.platform.baseapi.entity.protocol.UserProtocol;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
*
* @author xxm
* @since 2025/5/9
*/
@Mapper
public interface UserProtocolMapper extends MPJBaseMapper<UserProtocol> {
}

View File

@@ -28,10 +28,10 @@ public class SystemParameter extends MpBaseEntity implements ToResult<SystemPara
private String name;
/** 参数键名 */
private String key;
private String paramKey;
/** 参数值 */
private String value;
private String paramValue;
/** 参数类型 */
private String type;

View File

@@ -0,0 +1,47 @@
package cn.bootx.platform.baseapi.entity.protocol;
import cn.bootx.platform.baseapi.convert.protocol.UserProtocolConvert;
import cn.bootx.platform.baseapi.param.protocol.UserProtocolParam;
import cn.bootx.platform.baseapi.result.protocol.UserProtocolResult;
import cn.bootx.platform.common.mybatisplus.base.MpBaseEntity;
import cn.bootx.platform.common.mybatisplus.function.ToResult;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 用户协议管理
* @author xxm
* @since 2025/5/9
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@TableName("base_user_protocol")
public class UserProtocol extends MpBaseEntity implements ToResult<UserProtocolResult> {
/** 名称 */
private String name;
/** 显示名称 */
private String showName;
/** 类型 */
private String type;
/** 默认协议 */
private Boolean defaultProtocol;
/** 内容 */
private String content;
@Override
public UserProtocolResult toResult() {
return UserProtocolConvert.CONVERT.toResult(this);
}
public static UserProtocol init(UserProtocolParam param) {
return UserProtocolConvert.CONVERT.toEntity(param);
}
}

View File

@@ -0,0 +1,44 @@
package cn.bootx.platform.baseapi.param.protocol;
import cn.bootx.platform.core.validation.ValidationGroup;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Null;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 用户协议管理
* @author xxm
* @since 2025/5/9
*/
@Data
@Accessors(chain = true)
@Schema(title = "用户协议管理")
public class UserProtocolParam {
@Null(message = "Id需要为空", groups = ValidationGroup.add.class)
@NotNull(message = "Id不可为空", groups = ValidationGroup.edit.class)
@Schema(description = "主键")
private Long id;
/** 名称 */
@NotBlank(message = "名称不能为空")
@Schema(description = "名称")
private String name;
/** 显示名称 */
@NotBlank(message = "显示名称不能为空")
@Schema(description = "显示名称")
private String showName;
/** 类型 */
@NotBlank(message = "类型不能为空")
@Schema(description = "类型")
private String type;
/** 内容 */
@Schema(description = "内容")
private String content;
}

View File

@@ -0,0 +1,32 @@
package cn.bootx.platform.baseapi.param.protocol;
import cn.bootx.platform.common.mybatisplus.query.entity.SortParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 用户协议
* @author xxm
* @since 2025/5/11
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@Schema(title = "用户协议")
public class UserProtocolQuery extends SortParam {
/** 名称 */
@Schema(description = "名称")
private String name;
/** 显示名称 */
@Schema(description = "显示名称")
private String showName;
/** 类型 */
@Schema(description = "类型")
private String type;
}

View File

@@ -0,0 +1,39 @@
package cn.bootx.platform.baseapi.result.protocol;
import cn.bootx.platform.core.result.BaseResult;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 用户协议管理
* @author xxm
* @since 2025/5/9
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@Schema(title = "用户协议管理")
public class UserProtocolResult extends BaseResult {
/** 名称 */
@Schema(description = "名称")
private String name;
/** 显示名称 */
@Schema(description = "显示名称")
private String showName;
/** 类型 */
@Schema(description = "类型")
private String type;
/** 默认协议 */
@Schema(description = "默认协议")
private Boolean defaultProtocol;
/** 内容 */
@Schema(description = "内容")
private String content;
}

View File

@@ -36,7 +36,7 @@ public class SystemParamService {
*/
public void add(SystemParameterParam param) {
SystemParameter systemParameter = SystemParameter.init(param);
if (systemParamManager.existsByKey(systemParameter.getKey())) {
if (systemParamManager.existsByKey(systemParameter.getParamKey())) {
throw new BizException("key重复");
}
// 默认非内置
@@ -81,7 +81,7 @@ public class SystemParamService {
if (Objects.equals(param.getEnable(), false)) {
throw new BizException("该参数已停用");
}
return param.getValue();
return param.getParamValue();
}
/**

View File

@@ -0,0 +1,107 @@
package cn.bootx.platform.baseapi.service.protocol;
import cn.bootx.platform.baseapi.dao.protocol.UserProtocolManager;
import cn.bootx.platform.baseapi.entity.protocol.UserProtocol;
import cn.bootx.platform.baseapi.param.protocol.UserProtocolParam;
import cn.bootx.platform.baseapi.param.protocol.UserProtocolQuery;
import cn.bootx.platform.baseapi.result.protocol.UserProtocolResult;
import cn.bootx.platform.common.mybatisplus.util.MpUtil;
import cn.bootx.platform.core.exception.BizException;
import cn.bootx.platform.core.exception.DataNotExistException;
import cn.bootx.platform.core.rest.param.PageParam;
import cn.bootx.platform.core.rest.result.PageResult;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 用户协议管理服务
* @author xxm
* @since 2025/5/9
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class UserProtocolService {
private final UserProtocolManager userProtocolManager;
/**
* 分页
*/
public PageResult<UserProtocolResult> page(PageParam pageParam, UserProtocolQuery query){
return MpUtil.toPageResult(userProtocolManager.page(pageParam,query));
}
/**
* 添加
*/
public void add(UserProtocolParam param){
var userProtocol = UserProtocol.init(param);
userProtocolManager.save(userProtocol);
}
/**
* 更新
*/
public void update(UserProtocolParam param){
var userProtocol = userProtocolManager.findById(param.getId())
.orElseThrow(() -> new DataNotExistException("用户协议不存在"));
BeanUtil.copyProperties(param, userProtocol, CopyOptions.create().ignoreNullValue());
userProtocolManager.updateById(userProtocol);
}
/**
* 删除
*/
public void delete(Long id){
// 默认不可被删除
var userProtocol = userProtocolManager.findById(id)
.orElseThrow(() -> new DataNotExistException("用户协议不存在"));
if (userProtocol.getDefaultProtocol()){
throw new BizException("默认协议不可删除");
}
userProtocolManager.deleteById(id);
}
/**
* 根据ID查询
*/
public UserProtocolResult findById(Long id){
return userProtocolManager.findById(id)
.map(UserProtocol::toResult)
.orElseThrow(() -> new DataNotExistException("用户协议不存在"));
}
/**
* 根据分类查询默认协议
*/
public UserProtocolResult findDefault(String type){
return userProtocolManager.findDefault(type)
.map(UserProtocol::toResult)
.orElseThrow(() -> new DataNotExistException("用户协议不存在"));
}
/**
* 设置默认协议
*/
@Transactional(rollbackFor = Exception.class)
public void setDefault(Long id){
var userProtocol = userProtocolManager.findById(id)
.orElseThrow(() -> new DataNotExistException("用户协议不存在"));
userProtocolManager.clearDefault(userProtocol.getType());
userProtocolManager.setDefault(id);
}
/**
* 取消默认协议
*/
public void cancelDefault(Long id){
var userProtocol = userProtocolManager.findById(id)
.orElseThrow(() -> new DataNotExistException("用户协议不存在"));
userProtocolManager.cancelDefault(id);
}
}

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform-service</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<artifactId>service-iam</artifactId>

View File

@@ -33,8 +33,10 @@ import java.util.Objects;
@SuppressWarnings("FieldCanBeLocal")
public class PasswordLoginHandler implements AbstractAuthentication {
@Getter
private final String ACCOUNT_PARAMETER = "account";
@Getter
private final String PASSWORD_PARAMETER = "password";
@Resource

View File

@@ -70,7 +70,7 @@ public class UserAdminController {
@RequestPath("重置密码")
@Operation(summary = "重置密码")
@PostMapping("/restartPassword")
@OperateLog(title = "重置密码", businessType = OperateLog.BusinessType.UPDATE, saveParam = true)
@OperateLog(title = "重置密码", businessType = OperateLog.BusinessType.UPDATE)
public Result<Void> restartPassword(@RequestBody @Validated RestartPwdParam param) {
userAdminService.restartPassword(param.getUserId(), param.getNewPassword());
return Res.ok();
@@ -79,7 +79,7 @@ public class UserAdminController {
@RequestPath("批量重置密码")
@Operation(summary = "批量重置密码")
@PostMapping("/restartPasswordBatch")
@OperateLog(title = "批量重置密码", businessType = OperateLog.BusinessType.UPDATE, saveParam = true)
@OperateLog(title = "批量重置密码", businessType = OperateLog.BusinessType.UPDATE)
public Result<Void> restartPasswordBatch(@RequestBody @Validated RestartPwdBatchParam param) {
userAdminService.restartPasswordBatch(param.getUserIds(), param.getNewPassword());
return Res.ok();

View File

@@ -1,6 +1,7 @@
package cn.bootx.platform.iam.dao.user;
import cn.bootx.platform.common.mybatisplus.base.MpIdEntity;
import cn.bootx.platform.common.mybatisplus.base.MpRealDelEntity;
import cn.bootx.platform.common.mybatisplus.impl.BaseManager;
import cn.bootx.platform.common.mybatisplus.query.generator.QueryGenerator;
import cn.bootx.platform.common.mybatisplus.util.MpUtil;
@@ -74,7 +75,11 @@ public class UserInfoManager extends BaseManager<UserInfoMapper, UserInfo> {
}
public void setUpStatus(Long userId, String status) {
lambdaUpdate().eq(MpIdEntity::getId, userId).set(UserInfo::getStatus, status).update();
lambdaUpdate()
.eq(MpIdEntity::getId, userId)
.set(UserInfo::getStatus, status)
.setIncrBy(MpRealDelEntity::getVersion, 1)
.update();
}
/**
@@ -86,6 +91,7 @@ public class UserInfoManager extends BaseManager<UserInfoMapper, UserInfo> {
.set(UserInfo::getStatus, status)
.set(UserInfo::getLastModifiedTime, LocalDateTime.now())
.set(UserInfo::getLastModifier, SecurityUtil.getUserIdOrDefaultId())
.setIncrBy(MpRealDelEntity::getVersion, 1)
.update();
}
@@ -98,6 +104,7 @@ public class UserInfoManager extends BaseManager<UserInfoMapper, UserInfo> {
.set(UserInfo::getPassword, password)
.set(UserInfo::getLastModifiedTime, LocalDateTime.now())
.set(UserInfo::getLastModifier, SecurityUtil.getUserIdOrDefaultId())
.setIncrBy(MpRealDelEntity::getVersion, 1)
.update();
}

View File

@@ -1,6 +1,6 @@
package cn.bootx.platform.iam.entity.permission;
import cn.bootx.platform.common.mybatisplus.base.MpCreateEntity;
import cn.bootx.platform.common.mybatisplus.base.MpIdEntity;
import cn.bootx.platform.common.mybatisplus.function.ToResult;
import cn.bootx.platform.iam.convert.permission.PermPathConvert;
import cn.bootx.platform.iam.result.permission.PermPathResult;
@@ -22,7 +22,7 @@ import org.springframework.web.bind.annotation.RequestMethod;
@Data
@Accessors(chain = true)
@TableName("iam_perm_path")
public class PermPath extends MpCreateEntity implements ToResult<PermPathResult> {
public class PermPath extends MpIdEntity implements ToResult<PermPathResult> {
/** 上级编码 */
private String parentCode;

View File

@@ -2,10 +2,8 @@ package cn.bootx.platform.iam.handler;
import cn.bootx.platform.core.entity.UserDetail;
import cn.bootx.platform.iam.code.UserStatusEnum;
import cn.bootx.platform.iam.service.user.UserAdminService;
import cn.bootx.platform.starter.auth.authentication.UserInfoStatusCheck;
import cn.bootx.platform.starter.auth.configuration.AuthProperties;
import cn.bootx.platform.starter.auth.entity.AuthClient;
import cn.bootx.platform.starter.auth.entity.AuthInfoResult;
import cn.bootx.platform.starter.auth.entity.LoginAuthContext;
import cn.bootx.platform.starter.auth.exception.LoginFailureException;
@@ -24,7 +22,6 @@ import java.util.Objects;
@Component
@RequiredArgsConstructor
public class UserInfoStatusCheckImpl implements UserInfoStatusCheck {
private final UserAdminService userAdminService;
/**
* 检查用户状态
@@ -34,7 +31,6 @@ public class UserInfoStatusCheckImpl implements UserInfoStatusCheck {
@Override
public void check(AuthInfoResult authInfoResult, LoginAuthContext context) {
UserDetail userDetail = authInfoResult.getUserDetail();
AuthClient authClient = context.getAuthClient();
AuthProperties authProperties = context.getAuthProperties();
// 判断是否开启了超级管理员
if (!authProperties.isEnableAdmin() && userDetail.isAdmin()) {

View File

@@ -64,12 +64,12 @@ public class PermPathSyncService {
List<String> clientCodes = bootxConfigProperties.getClientCodes();
// 查询数据中的数据并转换为请求信息列表
for (String clientCode : clientCodes) {
sync(clientCode);
this.sync(clientCode);
}
} else {
// 分模块模式同步
String clientCode = clientCodeService.getClientCode();
sync(clientCode);
this.sync(clientCode);
}
}
@@ -82,7 +82,7 @@ public class PermPathSyncService {
// 查询是否包含所有
.filter(o -> o.isAllClient() || CollUtil.contains(o.getClientCodes(),clientCode))
.toList();
List<PermPath> permPaths = permPathManager.findAllByLeafAndClient(true,clientCode);
List<PermPath> permPaths = permPathManager.findAllByLeafAndClient(true, clientCode);
var requestPathMap = requestPathBos.stream()
.collect(Collectors.toMap(o -> o.getPath() + ":" + o.getMethod(), Function.identity()));
var permPathMap = permPaths.stream()
@@ -95,8 +95,8 @@ public class PermPathSyncService {
// 需要更新的数据
List<PermPath> updateData = this.getUpdateData(requestPathMap, permPathMap);
// 保存新增的
addData.forEach(o -> o.setClientCode(clientCode));
// 保存新增的, ID 由不会变更的终端编码+请求方式+请求路径进行
addData.forEach(o -> o.setClientCode(clientCode).setId(this.genPathId(o.getClientCode()+o.getMethod()+o.getPath())));
permPathManager.saveAll(addData);
// 更新存在的
permPathManager.updateAllById(updateData);
@@ -161,8 +161,7 @@ public class PermPathSyncService {
.map(permPathMap::get)
.peek(o -> {
RequestPathBo requestPathBo = requestPathMap.get(o.getPath() + ":" + o.getMethod());
o.setName(requestPathBo.getName())
.setParentCode(requestPathBo.getGroupCode());
o.setName(requestPathBo.getName()).setParentCode(requestPathBo.getGroupCode());
}).toList();
}
@@ -227,7 +226,7 @@ public class PermPathSyncService {
.stream()
.filter(pathKey -> {
HandlerMethod handlerMethod = map.get(pathKey);
return Objects.nonNull(handlerMethod.getMethodAnnotation(cn.bootx.platform.core.annotation.RequestPath.class))
return Objects.nonNull(handlerMethod.getMethodAnnotation(RequestPath.class))
&&Objects.nonNull(handlerMethod.getBeanType().getAnnotation(RequestGroup.class));
}).toList();
@@ -303,7 +302,7 @@ public class PermPathSyncService {
}
/**
* 给分组模块生成ID, 防止每次更新ID都会发生变化
* 给分组/模块/请求路径资源生成ID, 防止每次更新ID都会发生变化
*/
private long genPathId(String str) {
String s = SecureUtil.sha256(str);

View File

@@ -148,7 +148,7 @@ public class RoleQueryService {
// 获取关联的角色和子角色
List<RoleResult> unfold = TreeBuildUtil.unfold(tree, RoleResult::getChildren).stream()
.filter(role -> roleIds.contains(role.getId()))
.collect(Collectors.toList());;
.collect(Collectors.toList());
var list = new ArrayList<>(unfold);
// 将子孙级别的角色移除, 只保留根角色
for (var out : unfold) {

View File

@@ -152,7 +152,7 @@ public class RoleCodeService {
// 权限码列表
List<PermCode> permCodes = allPermCodes.stream()
.filter(PermCode::isLeaf)
.toList();;
.toList();
// 如果有有上级角色, 显示上级角色已分配的权限
if (Objects.nonNull(role.getPid())){
List<Long> codeIds = roleCodeManager.findAllByRole(role.getPid())

View File

@@ -47,7 +47,6 @@ public class UserAdminService {
private final AuthProperties authProperties;
/**
* 分页查询
*/
@@ -143,8 +142,7 @@ public class UserAdminService {
UserInfo userInfo = userInfoManager.findById(userId).orElseThrow(UserInfoNotExistsException::new);
// 新密码进行加密
newPassword = BCrypt.hashpw(newPassword);
userInfo.setPassword(newPassword);
userInfo.setPassword(BCrypt.hashpw(newPassword));
userInfoManager.updateById(userInfo);
}
@@ -154,8 +152,7 @@ public class UserAdminService {
@Transactional(rollbackFor = Exception.class)
public void restartPasswordBatch(List<Long> userIds, String newPassword){
// 新密码进行加密
String password = BCrypt.hashpw(newPassword);
userInfoManager.restartPasswordBatch(userIds,password);
userInfoManager.restartPasswordBatch(userIds,BCrypt.hashpw(newPassword));
}
/**

View File

@@ -98,13 +98,16 @@ public class UserInfoService {
public void updatePassword(String password, String newPassword) {
UserInfo userInfo = userInfoManager.findById(SecurityUtil.getUserId())
.orElseThrow(UserInfoNotExistsException::new);
UserInfo update=new UserInfo();
update.setId(userInfo.getId());
update.setVersion(userInfo.getVersion());
// 判断原密码是否正确
if (!BCrypt.checkpw(password, userInfo.getPassword())) {
throw new BizException("旧密码错误");
}
userInfo.setPassword(newPassword);
userInfoManager.updateById(userInfo);
newPassword=BCrypt.hashpw(newPassword,BCrypt.gensalt());
update.setPassword(newPassword);
userInfoManager.updateById(update);
}
}

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<modules>
<module>starter-auth</module>
@@ -27,7 +27,7 @@
<dependency>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform-core</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</dependency>
<!-- lombok -->
<dependency>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform-starter</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<artifactId>starter-audit-log</artifactId>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform-starter</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<artifactId>starter-auth</artifactId>

View File

@@ -5,6 +5,7 @@ import cn.bootx.platform.starter.auth.entity.LoginAuthContext;
import cn.hutool.extra.spring.SpringUtil;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import java.util.Objects;
/**
@@ -19,11 +20,12 @@ public interface AbstractAuthentication {
* 获取终端编码
*/
String getLoginType();
/**
* 获取用户状态检查接口的实现类
*/
default UserInfoStatusCheck getUserInfoStatusCheck() {
return SpringUtil.getBean(UserInfoStatusCheck.class);
default List<UserInfoStatusCheck> getUserInfoStatusCheck() {
return SpringUtil.getBeansOfType(UserInfoStatusCheck.class).values().stream().toList();
}
/**
@@ -62,7 +64,9 @@ public interface AbstractAuthentication {
// 添加用户信息到上下文中
context.setUserDetail(authInfoResult.getUserDetail());
// 检查用户信息和状态
this.getUserInfoStatusCheck().check(authInfoResult, context);
for (var userInfoStatusCheck : this.getUserInfoStatusCheck()) {
userInfoStatusCheck.check(authInfoResult, context);
}
// 认证后处理
this.authenticationAfter(authInfoResult, context);
return authInfoResult;

View File

@@ -11,7 +11,7 @@ import cn.bootx.platform.starter.auth.entity.LoginAuthContext;
public interface UserInfoStatusCheck {
/**
*
* 检查用户状态
* @param authInfoResult 认证返回结果
* @param context 登录认证上下文
*/

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform-starter</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<artifactId>starter-cache</artifactId>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform-starter</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<artifactId>starter-delay-queue</artifactId>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform-starter</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<artifactId>starter-file</artifactId>
@@ -25,11 +25,10 @@
<version>${x-file-storage.version}</version>
</dependency>
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
<version>${qcloud.version}</version>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>${aws-s3.version}</version>
</dependency>
<!-- 数据库 -->
<dependency>
<groupId>cn.bootx.platform</groupId>

View File

@@ -1,8 +1,12 @@
package cn.bootx.platform.starter.file.code;
import cn.bootx.platform.core.exception.BizException;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Objects;
/**
* 存储平台类型
*
@@ -12,22 +16,32 @@ import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum FilePlatformTypeEnum {
LOCAL("local"),
FTP("ftp"),
SFTP("sftp"),
WEB_DAV("web_dav"),
AMAZON("amazon"),
MINIO("minio"),
ALI("ali"),
HUAWEI("huawei"),
TENCENT("tencent"),
BAIDU("baidu"),
UPYUN("upyun"),
QINIU("qiniu"),
GOOGLE_CLOUD("google_cloud"),
FAST_DFS("fast_dfs"),
AZURE("azure");
LOCAL("local",false),
FTP("ftp",false),
SFTP("sftp",false),
WEB_DAV("web_dav",false),
// S3 存储, 现在系统只支持这一种方式
AMAZON_S3("amazon-s3",true),
MINIO("minio",true),
ALI("ali",true),
HUAWEI("huawei",true),
TENCENT("tencent",true),
BAIDU("baidu",true),
UPYUN("upyun",true),
QINIU("qiniu",true),
GOOGLE_CLOUD("google_cloud",true),
FAST_DFS("fast_dfs",true),
AZURE("azure",true);
private final String code;
/** 前端直传 */
private final boolean frontendUpload;
public static FilePlatformTypeEnum findByCode(String code){
return Arrays.stream(values())
.filter(e -> Objects.equals(e.code, code))
.findFirst()
.orElseThrow(() -> new BizException("不支持的类型"));
}
}

View File

@@ -1,30 +0,0 @@
package cn.bootx.platform.starter.file.configuration;
import cn.hutool.core.util.StrUtil;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 文件上传配置
*
* @author xxm
* @since 2022/1/14
*/
@Data
@Accessors(chain = true)
@ConfigurationProperties(prefix = "bootx-platform.starter.file-upload")
public class FileUploadProperties {
/**
* 文件访问转发地址(当前后端服务地址或被代理后的地址), 流量会经过后端服务的转发
*/
private String forwardServerUrl = "http://127.0.0.1:9999";
/**
* 处理为 / 结尾
*/
public String getForwardServerUrl() {
return StrUtil.removeSuffix(forwardServerUrl, "/");
}
}

View File

@@ -1,19 +0,0 @@
package cn.bootx.platform.starter.file.configuration;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@Accessors(chain = true)
@ConfigurationProperties(prefix = "bootx-platform.starter.oss")
/***
* oss配置
*/
public class OssProperties {
private String filePath;
}

View File

@@ -8,20 +8,30 @@ import cn.bootx.platform.core.rest.Res;
import cn.bootx.platform.core.rest.param.PageParam;
import cn.bootx.platform.core.rest.result.PageResult;
import cn.bootx.platform.core.rest.result.Result;
import cn.bootx.platform.starter.file.param.FileUploadRequestParams;
import cn.bootx.platform.starter.file.param.UploadFileInfoParam;
import cn.bootx.platform.starter.file.param.UploadFileQuery;
import cn.bootx.platform.starter.file.result.FileUploadParamsResult;
import cn.bootx.platform.starter.file.result.UploadFileResult;
import cn.bootx.platform.starter.file.service.FileUploadService;
import org.dromara.core.trans.anno.TransMethodResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.core.trans.anno.TransMethodResult;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* 文件上传
@@ -52,14 +62,14 @@ public class FIleUpLoadController {
@Operation(summary = "获取单条详情")
@GetMapping("/findById")
public Result<UploadFileResult> findById(@NotNull(message = "主键不可为空") Long id) {
return Res.ok(uploadService.findById(id));
return Res.ok(uploadService.findByUrl(id));
}
@IgnoreAuth
@Operation(summary = "根据URL获取单条详情")
@GetMapping("/findByUrl")
public Result<UploadFileResult> findById(@NotBlank(message = "文件URL不可为空") String url) {
return Res.ok(uploadService.findById(url));
return Res.ok(uploadService.findByUrl(url));
}
@Operation(summary = "删除")
@@ -71,31 +81,37 @@ public class FIleUpLoadController {
return Res.ok();
}
@IgnoreAuth
@Operation(summary = "上传")
@PostMapping("/upload")
public Result<UploadFileResult> local(@RequestPart MultipartFile file, String fileName) {
return Res.ok(uploadService.upload(file, fileName));
@IgnoreAuth(login = true)
@Operation(summary = "下载文件")
@GetMapping("/downloadByServer")
public ResponseEntity<byte[]> downloadByServer(String attachName) {
var bytes = uploadService.downloadAndCheck(attachName);
// 设置header信息
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentDispositionFormData("attachment", URLEncoder.encode(attachName, StandardCharsets.UTF_8));
return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
}
@IgnoreAuth(login = true)
@Operation(summary = "获取前端直传参数")
@PostMapping("/getUploadParams")
public Result<FileUploadParamsResult> getUploadParams(@RequestBody @Valid FileUploadRequestParams params) {
return Res.ok(uploadService.getUploadParams(params));
}
@IgnoreAuth(login = true)
@Operation(summary = "前端直传文件信息保存")
@PostMapping("/saveFileInfo")
public Result<Void> saveFileInfo(@RequestBody @Valid UploadFileInfoParam param) {
uploadService.saveFileInfo(param);
return Res.ok();
}
@IgnoreAuth
@Operation(summary = "获取文件预览地址前缀(流量会经过后端)")
@GetMapping("/forward/getFilePreviewUrlPrefix")
public Result<String> getFilePreviewUrlPrefix() {
return Res.ok(uploadService.getServerFilePreviewUrlPrefix());
}
@IgnoreAuth
@Operation(summary = "预览文件(流量会经过后端)")
@GetMapping("/forward/preview/{id}")
public void preview(@PathVariable Long id, HttpServletResponse response) {
uploadService.preview(id, response);
}
@IgnoreAuth
@Operation(summary = "下载文件(流量会经过后端)")
@GetMapping("/forward/download/{id}")
public ResponseEntity<byte[]> download(@PathVariable Long id) {
return uploadService.download(id);
@Operation(summary = "前端直传文件预览/下载(不需要登录)")
@GetMapping("/download/{attachName}")
public void download(HttpServletResponse httpServletResponse, @Schema(description = "附件名") @PathVariable("attachName") String attachName) {
uploadService.ossDownload(httpServletResponse, attachName);
}
}

View File

@@ -13,6 +13,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.x.file.storage.core.FileStorageService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -24,6 +25,7 @@ import java.util.List;
* @since 2024/8/12
*/
@Validated
@Deprecated
@RequestGroup(groupCode = "FilePlatform", groupName = "文件存储平台管理", moduleCode = "starter")
@Tag(name = "文件存储平台")
@RestController
@@ -31,6 +33,7 @@ import java.util.List;
@RequiredArgsConstructor
public class FilePlatformController {
private final FilePlatformService filePlatformService;
private final FileStorageService fileStorageService;
@IgnoreAuth
@Operation(summary = "列表")
@@ -63,4 +66,12 @@ public class FilePlatformController {
filePlatformService.setDefault(id);
return Res.ok();
}
@IgnoreAuth
@Operation(summary = "获取当前默认的文件上传存储平台")
@GetMapping("/getDefaultUpload")
public Result<String> getDefault(){
String defaultPlatform = fileStorageService.getProperties().getDefaultPlatform();
return Res.ok(defaultPlatform);
}
}

View File

@@ -1,36 +0,0 @@
package cn.bootx.platform.starter.file.controller;
import cn.bootx.platform.starter.file.param.FileUploadRequestParams;
import cn.bootx.platform.starter.file.result.FileUploadParamsResult;
import cn.bootx.platform.starter.file.service.OssService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@Tag(name = "对象存储")
@RequestMapping("/oss")
@AllArgsConstructor
public class OssController {
private final OssService ossService;
@Operation(summary = "获取上传参数")
@PostMapping("/getUploadParams")
public FileUploadParamsResult getUploadParams(@RequestBody @Valid FileUploadRequestParams params) {
return ossService.getUploadParams(params);
}
@Operation(summary = "文件预览/文件下载")
@GetMapping("/download/{attachName}")
public void download(HttpServletResponse httpServletResponse, @Schema(description = "附件名") @PathVariable("attachName") String attachName) {
ossService.download(httpServletResponse, attachName);
}
}

View File

@@ -1,6 +1,7 @@
package cn.bootx.platform.starter.file.convert;
import cn.bootx.platform.starter.file.entity.UploadFileInfo;
import cn.bootx.platform.starter.file.param.UploadFileInfoParam;
import cn.bootx.platform.starter.file.result.UploadFileResult;
import org.dromara.x.file.storage.core.FileInfo;
import org.mapstruct.Mapper;
@@ -19,6 +20,8 @@ public interface FileConvert {
UploadFileInfo convert(FileInfo in);
UploadFileInfo convert(UploadFileInfoParam in);
FileInfo toFileInfo(UploadFileInfo in);
UploadFileResult toResult(FileInfo in);

View File

@@ -6,13 +6,23 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/**
*
* 支付平台配置
* @author xxm
* @since 2024/8/12
*/
@Slf4j
@Deprecated
@Repository
@RequiredArgsConstructor
public class FilePlatformManager extends BaseManager<FilePlatformMapper, FilePlatform> {
/**
* 根据平台类型查询
*/
public Optional<FilePlatform> findByType(String platform) {
return findByField(FilePlatform::getType, platform);
}
}

View File

@@ -14,6 +14,8 @@ import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import lombok.experimental.FieldNameConstants;
import java.util.Objects;
/**
* 文件存储平台
* @author xxm
@@ -41,12 +43,20 @@ public class FilePlatform extends MpRealDelEntity implements ToResult<FilePlatfo
private boolean defaultPlatform;
/** 访问地址 */
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private String url;
/** 前端直传 */
private Boolean frontendUpload;
public String getUrl() {
return StrUtil.removeSuffix(url, "/");
}
public Boolean getFrontendUpload() {
return Objects.equals(frontendUpload,true);
}
/**
* 转换
*/

View File

@@ -27,7 +27,7 @@ import java.time.LocalDateTime;
public class UploadFileInfo extends MpIdEntity implements ToResult<UploadFileResult> {
/**
* 文件访问地址
* 文件访问地址(存储在S3中的唯一名称)
*/
private String url;

View File

@@ -8,8 +8,6 @@ import org.dromara.x.file.storage.core.FileInfo;
import org.dromara.x.file.storage.core.recorder.DefaultFileRecorder;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
/**
* x.file.storage 文件上传信息储存
* @author xxm
@@ -27,7 +25,6 @@ public class FileDetailRecordHandler extends DefaultFileRecorder {
@Override
public boolean save(FileInfo fileInfo) {
UploadFileInfo uploadFileInfo = UploadFileInfo.init(fileInfo);
uploadFileInfo.setCreateTime(LocalDateTime.now());
uploadFileManager.save(uploadFileInfo);
fileInfo.setId(String.valueOf(uploadFileInfo.getId()));
return true;

View File

@@ -20,8 +20,11 @@ public class FilePlatformParam {
@Schema(description = "主键")
private Long id;
@NotBlank(message = "平台地址不得为空")
@Schema(description = "平台地址")
private String url;
/** 前端直传 */
@Schema(description = "前端直传")
private Boolean frontendUpload;
}

View File

@@ -14,7 +14,7 @@ public class FileUploadRequestParams {
private String fileName;
@Schema(description = "媒体类型 MEDIA_TYPE")
@NotBlank(message = "媒体类型不可为")
@NotBlank(message = "媒体类型不可为为空")
private String mediaType;
@Schema(description = "文件大小")

View File

@@ -0,0 +1,106 @@
package cn.bootx.platform.starter.file.param;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 上传文件信息
* @author xxm
* @since 2025/5/15
*/
@Data
@Accessors(chain = true)
@Schema(title = "上传文件信息")
public class UploadFileInfoParam {
/**
* 文件访问地址(名称)
*/
@Schema(description = "文件访问地址")
private String url;
/**
* 文件大小,单位字节
*/
@Schema(description = "文件大小,单位字节")
private Long size;
/**
* 文件名称
*/
@Schema(description = "文件名称")
private String filename;
/**
* 原始文件名
*/
@Schema(description = "原始文件名")
private String originalFilename;
/**
* 基础存储路径
*/
@Schema(description = "基础存储路径")
private String basePath;
/**
* 存储路径
*/
@Schema(description = "存储路径")
private String path;
/**
* 文件扩展名
*/
@Schema(description = "文件扩展名")
private String ext;
/**
* MIME 类型
*/
@Schema(description = "MIME 类型")
private String contentType;
/**
* 存储平台
*/
@Schema(description = "存储平台")
private String platform;
/**
* 缩略图访问路径
*/
@Schema(description = "缩略图访问路径")
private String thUrl;
/**
* 缩略图名称
*/
@Schema(description = "缩略图名称")
private String thFilename;
/**
* 缩略图大小,单位字节
*/
@Schema(description = "缩略图大小,单位字节")
private Long thSize;
/**
* 缩略图 MIME 类型
*/
@Schema(description = "缩略图 MIME 类型")
private String thContentType;
/**
* 文件所属对象id
*/
@Schema(description = "文件所属对象id")
private String objectId;
/**
* 文件所属对象类型,例如用户头像,评价图片
*/
@Schema(description = "文件所属对象类型,例如用户头像,评价图片")
private String objectType;
}

View File

@@ -36,4 +36,9 @@ public class FilePlatformResult extends BaseResult {
/** 访问地址 */
@Schema(description = "访问地址")
private String url;
/** 前端直传 */
@Schema(description = "前端直传")
private Boolean frontendUpload;
}

View File

@@ -2,16 +2,24 @@ package cn.bootx.platform.starter.file.result;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.Map;
/**
* 文件上传参数
*/
@Schema(description = "文件上传参数")
@Data
@Accessors(chain = true)
public class FileUploadParamsResult {
@Schema(description = "文件上传地址")
private String url;
@Schema(description = "上传后文件名称")
private String attachName;
@Schema(description = "文件上传请求头,上传时放在请求头里")
private Map<String,String> headers;

View File

@@ -22,12 +22,12 @@ import java.util.List;
* @since 2024/8/12
*/
@Slf4j
@Deprecated
@Service
@RequiredArgsConstructor
public class FilePlatformService {
private final FilePlatformManager filePlatformManager;
/**
* 获取全部存储平台
*/
@@ -62,12 +62,14 @@ public class FilePlatformService {
.eq(FilePlatform::isDefaultPlatform, true)
.set(FilePlatform::getLastModifiedTime, LocalDateTime.now())
.set(MpRealDelEntity::getLastModifier, SecurityUtil.getUserIdOrDefaultId())
.setIncrBy(MpRealDelEntity::getVersion, 1)
.update();
filePlatformManager.lambdaUpdate()
.eq(FilePlatform::getId, id)
.set(FilePlatform::getLastModifiedTime, LocalDateTime.now())
.set(MpRealDelEntity::getLastModifier, SecurityUtil.getUserIdOrDefaultId())
.set(FilePlatform::isDefaultPlatform, true)
.setIncrBy(MpRealDelEntity::getVersion, 1)
.update();
}
}

View File

@@ -1,39 +1,37 @@
package cn.bootx.platform.starter.file.service;
import cn.bootx.platform.common.mybatisplus.util.MpUtil;
import cn.bootx.platform.core.exception.BizException;
import cn.bootx.platform.core.exception.DataNotExistException;
import cn.bootx.platform.core.rest.param.PageParam;
import cn.bootx.platform.core.rest.result.PageResult;
import cn.bootx.platform.starter.file.configuration.FileUploadProperties;
import cn.bootx.platform.starter.file.convert.FileConvert;
import cn.bootx.platform.starter.file.dao.UploadFileManager;
import cn.bootx.platform.starter.file.entity.UploadFileInfo;
import cn.bootx.platform.starter.file.param.FileUploadRequestParams;
import cn.bootx.platform.starter.file.param.UploadFileInfoParam;
import cn.bootx.platform.starter.file.param.UploadFileQuery;
import cn.bootx.platform.starter.file.result.FileUploadParamsResult;
import cn.bootx.platform.starter.file.result.UploadFileResult;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import jakarta.servlet.ServletOutputStream;
import cn.hutool.http.HttpUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.dromara.x.file.storage.core.FileInfo;
import org.dromara.x.file.storage.core.FileStorageService;
import org.dromara.x.file.storage.core.upload.UploadPretreatment;
import org.springframework.http.HttpHeaders;
import org.dromara.x.file.storage.core.constant.Constant;
import org.dromara.x.file.storage.core.presigned.GeneratePresignedUrlResult;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.io.IOException;
import java.util.Date;
/**
* 文件上传管理类
@@ -47,108 +45,134 @@ import java.time.LocalDateTime;
public class FileUploadService {
private final UploadFileManager uploadFileManager;
private final FileStorageService fileStorageService;
private final FileUploadProperties fileUploadProperties;
/**
* 分页
*/
public PageResult<UploadFileResult> page(PageParam pageParam, UploadFileQuery param) {
return MpUtil.toPageResult(uploadFileManager.page(pageParam,param));
return MpUtil.toPageResult(uploadFileManager.page(pageParam, param));
}
/**
* 获取单条详情
*/
public UploadFileResult findById(Long id){
public UploadFileResult findByUrl(Long id) {
return uploadFileManager.findById(id)
.map(UploadFileInfo::toResult)
.orElseThrow(DataNotExistException::new);
.orElseThrow(DataNotExistException::new);
}
/**
* 根据URL获取单条详情
*/
public UploadFileResult findById(String url){
public UploadFileResult findByUrl(String url) {
return uploadFileManager.findByUrl(url)
.map(UploadFileInfo::toResult)
.orElseThrow(DataNotExistException::new);
.orElseThrow(DataNotExistException::new);
}
/**
* 文件删除
*/
@Transactional(rollbackFor = Exception.class)
public void delete(Long id){
public void delete(Long id) {
UploadFileInfo uploadFileInfo = uploadFileManager.findById(id)
.orElseThrow(DataNotExistException::new);
fileStorageService.delete(FileConvert.CONVERT.toFileInfo(uploadFileInfo));
}
/**
* 文件上传
* @param file 文件
* @param fileName 文件名称
* 根据文件名称下载
*/
@Transactional(rollbackFor = Exception.class)
public UploadFileResult upload(@RequestPart MultipartFile file, String fileName) {
UploadPretreatment uploadPretreatment = fileStorageService.of(file);
if (StrUtil.isNotBlank(fileName)){
uploadPretreatment.setOriginalFilename(fileName);
public byte[] download(String attachName) {
String ossFileUrl = this.getOssFileUrl(attachName);
return HttpUtil.downloadBytes(ossFileUrl);
}
/**
* 根据存储的文件对象下载
*/
public byte[] download(FileInfo fileInfo) {
return HttpUtil.downloadBytes(this.getOssFileUrl(fileInfo.getUrl()));
}
/**
* 文件下载, 首先判断文件是否存在, 用在严格的场景中
*/
public byte[] downloadAndCheck(String attachName) {
FileInfo fileInfo = fileStorageService.getFileInfoByUrl(attachName);
if (fileInfo == null){
throw new DataNotExistException("文件不存在");
}
// 按年月日进行分目录
uploadPretreatment.setPath(LocalDateTimeUtil.format(LocalDateTime.now(), "yyyy/MM/dd/"));
FileInfo upload = uploadPretreatment.upload();
return FileConvert.CONVERT.toResult(upload);
return HttpUtil.downloadBytes(this.getOssFileUrl(attachName));
}
/**
* 文件预览
* TODO url需要使用URl
* 获取直传上传参数
*/
@SneakyThrows
public void preview(Long id, HttpServletResponse response) {
FileInfo info = fileStorageService.getFileInfoByUrl(String.valueOf(id));
if (info == null){
log.warn("文件不存在");
return;
public FileUploadParamsResult getUploadParams(FileUploadRequestParams params) {
String fileExtension = FileUtil.extName(params.getFileName());
String attachName = IdUtil.fastSimpleUUID() + (StringUtils.isNotBlank(fileExtension) ? "." + fileExtension : "");
GeneratePresignedUrlResult uploadResult = fileStorageService
.generatePresignedUrl()
.setFilename(attachName) // 设置保存的文件名
.setMethod(Constant.GeneratePresignedUrl.Method.PUT) // 签名方法
.setExpiration(DateUtil.offsetMinute(new Date(), 10)) // 过期时间 10 分钟
.putHeaders(Constant.Metadata.CONTENT_TYPE, params.getMediaType())
.putHeaders(Constant.Metadata.CONTENT_LENGTH, String.valueOf(params.getFileSize()))
.generatePresignedUrl();
FileUploadParamsResult result = new FileUploadParamsResult();
result.setUrl(uploadResult.getUrl())
.setAttachName(attachName)
.setHeaders(uploadResult.getHeaders());
return result;
}
/**
* 保存前端直传的文件信息
*/
public void saveFileInfo(UploadFileInfoParam param) {
UploadFileInfo uploadFileInfo = FileConvert.CONVERT.convert(param);
// 扩展名
String fileExtension = FileUtil.extName(uploadFileInfo.getOriginalFilename());
uploadFileInfo.setExt(fileExtension);
// 平台
String defaultPlatform = fileStorageService.getProperties()
.getDefaultPlatform();
uploadFileInfo.setPlatform(defaultPlatform);
uploadFileManager.save(uploadFileInfo);
}
/**
* 获取oss直传文件下载链接
*/
public String getOssFileUrl(String attachName) {
// 系统中旧数据存储的地址是/开头, 为了兼容旧数据使用oss生成预签名链接的时候需要去掉
// 新数据存储格式就没有/开头
attachName = StrUtil.removePrefix(attachName, "/");
GeneratePresignedUrlResult downloadResult = fileStorageService
.generatePresignedUrl()
.setFilename(attachName) // 文件名
.setMethod(Constant.GeneratePresignedUrl.Method.GET) // 签名方法
.setExpiration(DateUtil.offsetMinute(new Date(), 10)) // 过期时间 10 分钟
.putResponseHeaders(
Constant.Metadata.CONTENT_DISPOSITION, "attachment;filename=" + attachName)
.generatePresignedUrl();
return downloadResult.getUrl();
}
/**
* 前端直传文件下载/预览
*/
public void ossDownload(HttpServletResponse httpServletResponse, String attachName) {
try {
httpServletResponse.sendRedirect(this.getOssFileUrl(attachName));
} catch (IOException e) {
log.error("下载文件失败", e);
httpServletResponse.setStatus(HttpStatus.NOT_FOUND.value());
throw new BizException(e.getMessage());
}
byte[] bytes = fileStorageService.download(info).bytes();
var is = new ByteArrayInputStream(bytes);
// 获取响应输出流
ServletOutputStream os = response.getOutputStream();
IoUtil.copy(is, os);
response.addHeader(HttpHeaders.CONTENT_DISPOSITION, info.getContentType());
IoUtil.close(is);
IoUtil.close(os);
}
/**
* 文件下载
*/
@SneakyThrows
public ResponseEntity<byte[]> download(Long id) {
FileInfo fileInfo = fileStorageService.getFileInfoByUrl(String.valueOf(id));
byte[] bytes = fileStorageService.download(fileInfo).bytes();
// 设置header信息
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
String fileName = fileInfo.getOriginalFilename();
headers.setContentDispositionFormData("attachment", URLEncoder.encode(fileName, StandardCharsets.UTF_8));
return new ResponseEntity<>(bytes,headers,HttpStatus.OK);
}
/**
* 文件访问转发地址(当前后端服务地址或被代理后的地址), 流量会经过后端服务的转发
*/
public String getServerFilePreviewUrlPrefix() {
return this.getForwardServerUrl() + "/file/preview/";
}
/**
* 文件访问转发地址(当前后端服务地址或被代理后的地址), 流量会经过后端服务的转发
*/
private String getForwardServerUrl() {
return fileUploadProperties.getForwardServerUrl();
}
}

View File

@@ -1,88 +0,0 @@
package cn.bootx.platform.starter.file.service;
import cn.bootx.platform.starter.file.configuration.OssProperties;
import cn.bootx.platform.starter.file.param.FileUploadRequestParams;
import cn.bootx.platform.starter.file.result.FileUploadParamsResult;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.dromara.x.file.storage.core.FileStorageService;
import org.dromara.x.file.storage.core.constant.Constant;
import org.dromara.x.file.storage.core.presigned.GeneratePresignedUrlResult;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Date;
/**
* @author yinxucun
*/
@Service
@Slf4j
public class OssService {
private final OssProperties ossProperties;
private final FileStorageService fileStorageService;
private static final Instant expirationTime = new Date().toInstant().atZone(ZoneId.of("Asia/Shanghai"))
.plusMinutes(10).toInstant();
public OssService(OssProperties ossProperties, FileStorageService fileStorageService) {
this.ossProperties = ossProperties;
this.fileStorageService = fileStorageService;
log.info("初始化OSS配置:{}", ossProperties);
}
public FileUploadParamsResult getUploadParams(FileUploadRequestParams params) {
return this.createSignedUrlForStringPut(params);
}
public void download(HttpServletResponse httpServletResponse, String attachName) {
GeneratePresignedUrlResult downloadResult = fileStorageService
.generatePresignedUrl()
.setPath(ossProperties.getFilePath()) // 文件路径
.setFilename(attachName) // 文件名,也可以换成缩略图的文件名
.setMethod(Constant.GeneratePresignedUrl.Method.GET) // 签名方法
.setExpiration(Date.from(expirationTime)) // 过期时间 10 分钟
.putResponseHeaders(
Constant.Metadata.CONTENT_DISPOSITION, "attachment;filename=" + attachName)
.generatePresignedUrl();
try {
httpServletResponse.sendRedirect(downloadResult.getUrl());
} catch (IOException e) {
log.error("下载文件失败", e);
httpServletResponse.setStatus(HttpStatus.NOT_FOUND.value());
throw new RuntimeException(e);
}
}
private FileUploadParamsResult createSignedUrlForStringPut(FileUploadRequestParams params) {
String fileExtension = FileUtil.extName(params.getFileName());
String attachName = IdUtil.fastSimpleUUID() + (StringUtils.isNotBlank(fileExtension) ? "." + fileExtension : "");
Date expirationTime = new Date(System.currentTimeMillis() + 600 * 1000);
GeneratePresignedUrlResult uploadResult = fileStorageService
.generatePresignedUrl()
.setPath(ossProperties.getFilePath()) // 设置路径
.setFilename(attachName) // 设置保存的文件名
.setMethod(Constant.GeneratePresignedUrl.Method.PUT) // 签名方法
.setExpiration(expirationTime) // 设置过期时间 10 分钟
.putHeaders(Constant.Metadata.CONTENT_TYPE, params.getMediaType())
.putHeaders(Constant.Metadata.CONTENT_LENGTH, String.valueOf(params.getFileSize()))
.generatePresignedUrl();
log.info("expirationTime:{},attachName:{}", DateUtil.format(expirationTime, "yyyy-MM-dd HH:mm:ss"), attachName);
log.info("生成上传预签名 URL 结果:{}", uploadResult);
FileUploadParamsResult result = new FileUploadParamsResult();
result.setUrl(uploadResult.getUrl());
result.setHeaders(uploadResult.getHeaders());
return result;
}
}

View File

@@ -0,0 +1,71 @@
package org.dromara.x.file.storage.core.platform;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.dromara.x.file.storage.core.FileStorageProperties.AmazonS3Config;
import java.util.Map;
/**
* 重写 Amazon S3 存储平台的 Client 工厂, 支持 withPathStyleAccessEnabled 配置
*/
@Getter
@Setter
@NoArgsConstructor
public class AmazonS3FileStorageClientFactory implements FileStorageClientFactory<AmazonS3> {
private String platform;
private String accessKey;
private String secretKey;
private String region;
private String endPoint;
private Map<String, Object> attr;
private volatile AmazonS3 client;
public AmazonS3FileStorageClientFactory(AmazonS3Config config) {
platform = config.getPlatform();
accessKey = config.getAccessKey();
secretKey = config.getSecretKey();
region = config.getRegion();
endPoint = config.getEndPoint();
attr = config.getAttr();
}
@Override
public AmazonS3 getClient() {
if (client == null) {
synchronized (this) {
if (client == null) {
AmazonS3ClientBuilder builder = AmazonS3ClientBuilder.standard()
.withCredentials(
new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey)));
if (StrUtil.isNotBlank(endPoint)) {
builder.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endPoint, region));
} else if (StrUtil.isNotBlank(region)) {
builder.withRegion(region);
}
// 使用路径访问样式
Boolean forcePathStyle = MapUtil.getBool(attr, "forcePathStyle", false);
builder.withPathStyleAccessEnabled(forcePathStyle);
client = builder.build();
}
}
}
return client;
}
@Override
public void close() {
if (client != null) {
client.shutdown();
client = null;
}
}
}

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform-starter</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<artifactId>starter-quartz</artifactId>

View File

@@ -7,13 +7,13 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.3</version>
<version>3.5.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.bootx.platform</groupId>
<artifactId>bootx-platform</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
<packaging>pom</packaging>
<description>基础脚手架服务</description>
@@ -25,18 +25,18 @@
</modules>
<properties>
<bootx-platform.version>3.0.0.beta5</bootx-platform.version>
<bootx-platform.version>3.0.0</bootx-platform.version>
<!-- 再高的的新版本与会knife4j 4.5冲突, 目前使用三方的knife4j依赖 -->
<springdoc.version>2.7.0</springdoc.version>
<hutool.version>5.8.31</hutool.version>
<hutool.version>5.8.39</hutool.version>
<bouncycastle.version>1.78.1</bouncycastle.version>
<knife4j.version>4.6.0</knife4j.version>
<mybatis-plus.version>3.5.9</mybatis-plus.version>
<mybatis-plus-join.version>1.4.13</mybatis-plus-join.version>
<mybatis-plus.version>3.5.12</mybatis-plus.version>
<mybatis-plus-join.version>1.5.4</mybatis-plus-join.version>
<dynamic-datasource.version>4.3.1</dynamic-datasource.version>
<lock4j.version>2.2.7</lock4j.version>
<x-file-storage.version>2.2.1</x-file-storage.version>
<minio.version>8.5.4</minio.version>
<x-file-storage.version>2.3.0</x-file-storage.version>
<aws-s3.version>1.12.429</aws-s3.version>
<qcloud.version>5.6.137</qcloud.version>
<sa-token.version>1.39.0</sa-token.version>
<justauth.version>1.16.6</justauth.version>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.dromara.daxpay</groupId>
<artifactId>daxpay-open-channel</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<artifactId>daxpay-open-channel-alipay</artifactId>

View File

@@ -2,6 +2,7 @@ package org.dromara.daxpay.channel.alipay.entity.allocation;
import cn.bootx.platform.common.mybatisplus.function.ToResult;
import cn.bootx.platform.core.util.JsonUtil;
import cn.hutool.json.JSONUtil;
import org.dromara.daxpay.channel.alipay.convert.AlipayAllocReceiverConvert;
import org.dromara.daxpay.channel.alipay.result.allocation.AlipayAllocReceiverResult;
import org.dromara.daxpay.core.enums.AllocReceiverTypeEnum;
@@ -71,7 +72,7 @@ public class AlipayAllocReceiver implements ToResult<AlipayAllocReceiverResult>
* 转换为通道接收方
*/
public static AlipayAllocReceiver convertChannel(AllocReceiver receiver) {
var leshuaAllocReceiver = JsonUtil.toBean(receiver.getExt(), AlipayAllocReceiver.class);
var leshuaAllocReceiver = JSONUtil.toBean(receiver.getExt(), AlipayAllocReceiver.class);
leshuaAllocReceiver.setId(receiver.getId())
.setReceiverNo(receiver.getReceiverNo())
.setReceiverName(receiver.getReceiverName())

View File

@@ -2,6 +2,7 @@ package org.dromara.daxpay.channel.alipay.entity.config;
import cn.bootx.platform.common.mybatisplus.function.ToResult;
import cn.bootx.platform.core.util.JsonUtil;
import cn.hutool.json.JSONUtil;
import org.dromara.daxpay.channel.alipay.code.AlipayCode;
import org.dromara.daxpay.channel.alipay.convert.AlipayConfigConvert;
import org.dromara.daxpay.channel.alipay.result.config.AlipayConfigResult;
@@ -97,7 +98,7 @@ public class AliPayConfig implements ToResult<AlipayConfigResult> {
* 从通道配置转换为支付宝配置
*/
public static AliPayConfig convertConfig(ChannelConfig channelConfig) {
var config = JsonUtil.toBean(channelConfig.getExt(), AliPayConfig.class);
var config = JSONUtil.toBean(channelConfig.getExt(), AliPayConfig.class);
config.setId(channelConfig.getId())
.setAliAppId(channelConfig.getOutAppId())
.setAppId(channelConfig.getAppId())

View File

@@ -1,6 +1,7 @@
package org.dromara.daxpay.channel.alipay.service.payment.notice;
import cn.bootx.platform.core.util.JsonUtil;
import cn.hutool.json.JSONUtil;
import org.dromara.daxpay.channel.alipay.code.AlipayCode;
import org.dromara.daxpay.channel.alipay.result.notice.AlipayOrderChangedResult;
import org.dromara.daxpay.core.enums.CallbackStatusEnum;
@@ -57,7 +58,7 @@ public class AlipayTransferNoticeService {
// 通过 biz_content 获取值
try {
String bizContent = map.get("biz_content");
var response = JsonUtil.toBean(bizContent, AlipayOrderChangedResult.class);
var response = JSONUtil.toBean(bizContent, AlipayOrderChangedResult.class);
callbackInfo.setCallbackData(BeanUtil.beanToMap(response));
this.resolveData(response);
return "success";

View File

@@ -2,6 +2,7 @@ package org.dromara.daxpay.channel.alipay.strategy.merchant;
import cn.bootx.platform.core.exception.ValidationFailedException;
import cn.bootx.platform.core.util.JsonUtil;
import cn.hutool.json.JSONUtil;
import org.dromara.daxpay.channel.alipay.entity.config.AliPayConfig;
import org.dromara.daxpay.channel.alipay.param.pay.AlipayParam;
import org.dromara.daxpay.channel.alipay.service.payment.config.AlipayConfigService;
@@ -49,7 +50,7 @@ public class AliPayStrategy extends AbsPayStrategy {
// 支付宝参数验证
String channelParam = this.getPayParam().getExtraParam();
if (StrUtil.isNotBlank(channelParam)) {
this.aliPayParam = JsonUtil.toBean(channelParam, AlipayParam.class);
this.aliPayParam = JSONUtil.toBean(channelParam, AlipayParam.class);
}
else {
this.aliPayParam = new AlipayParam();

View File

@@ -2,6 +2,7 @@ package org.dromara.daxpay.channel.alipay.strategy.sub;
import cn.bootx.platform.core.exception.ValidationFailedException;
import cn.bootx.platform.core.util.JsonUtil;
import cn.hutool.json.JSONUtil;
import org.dromara.daxpay.channel.alipay.entity.config.AliPayConfig;
import org.dromara.daxpay.channel.alipay.param.pay.AlipayParam;
import org.dromara.daxpay.channel.alipay.service.payment.config.AlipayConfigService;
@@ -49,7 +50,7 @@ public class AlipaySubPayStrategy extends AbsPayStrategy {
// 支付宝参数验证
String channelParam = this.getPayParam().getExtraParam();
if (StrUtil.isNotBlank(channelParam)) {
this.aliPayParam = JsonUtil.toBean(channelParam, AlipayParam.class);
this.aliPayParam = JSONUtil.toBean(channelParam, AlipayParam.class);
}
else {
this.aliPayParam = new AlipayParam();

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.dromara.daxpay</groupId>
<artifactId>daxpay-open-channel</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<artifactId>daxpay-open-channel-union</artifactId>

View File

@@ -3,6 +3,7 @@ package org.dromara.daxpay.channel.union.entity.config;
import cn.bootx.platform.common.mybatisplus.function.ToResult;
import cn.bootx.platform.core.annotation.BigField;
import cn.bootx.platform.core.util.JsonUtil;
import cn.hutool.json.JSONUtil;
import org.dromara.daxpay.channel.union.code.UnionPayCode;
import org.dromara.daxpay.channel.union.convert.UnionPayConfigConvert;
import org.dromara.daxpay.channel.union.result.UnionPayConfigResult;
@@ -101,7 +102,7 @@ public class UnionPayConfig implements ToResult<UnionPayConfigResult> {
* 从通道配置转换为支付宝配置
*/
public static UnionPayConfig convertConfig(ChannelConfig channelConfig) {
UnionPayConfig config = JsonUtil.toBean(channelConfig.getExt(), UnionPayConfig.class);
UnionPayConfig config = JSONUtil.toBean(channelConfig.getExt(), UnionPayConfig.class);
config.setId(channelConfig.getId())
.setUnionMachId(channelConfig.getOutMchNo())
.setEnable(channelConfig.isEnable());

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.dromara.daxpay</groupId>
<artifactId>daxpay-open-channel</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<artifactId>daxpay-open-channel-wechat</artifactId>

View File

@@ -1,505 +0,0 @@
package com.github.binarywang.wxpay.config;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.util.HttpProxyUtils;
import com.github.binarywang.wxpay.util.ResourcesUtils;
import com.github.binarywang.wxpay.v3.WxPayV3HttpClientBuilder;
import com.github.binarywang.wxpay.v3.auth.*;
import com.github.binarywang.wxpay.v3.util.PemUtils;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.SneakyThrows;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RegExUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.ssl.SSLContexts;
import org.dromara.daxpay.channel.wechat.code.WechatPayCode;
import javax.net.ssl.SSLContext;
import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Optional;
/**
* 微信支付配置
*
* @author Binary Wang (<a href="https://github.com/binarywang">...</a>)
*/
@Data
@Slf4j
@ToString(exclude = "verifier")
@EqualsAndHashCode(exclude = "verifier")
public class WxPayConfig {
private static final String DEFAULT_PAY_BASE_URL = "https://api.mch.weixin.qq.com";
private static final String PROBLEM_MSG = "证书文件【%s】有问题请核实";
private static final String NOT_FOUND_MSG = "证书文件【%s】不存在请核实";
/**
* 接口版本, 使用v2还是v3接口
* @see WechatPayCode#API_V2
*/
private String apiVersion;
/**
* 微信支付接口请求地址域名部分.
*/
private String payBaseUrl = DEFAULT_PAY_BASE_URL;
/**
* http请求连接超时时间.
*/
private int httpConnectionTimeout = 5000;
/**
* http请求数据读取等待时间.
*/
private int httpTimeout = 10000;
/**
* 公众号appid.
*/
private String appId;
/**
* 服务商模式下的子商户公众账号ID.
*/
private String subAppId;
/**
* 商户号.
*/
private String mchId;
/**
* 商户密钥.
*/
private String mchKey;
/**
* 企业支付密钥.
*/
private String entPayKey;
/**
* 服务商模式下的子商户号.
*/
private String subMchId;
/**
* 微信支付异步回掉地址通知url必须为直接可访问的url不能携带参数.
*/
private String notifyUrl;
/**
* 交易类型.
* <pre>
* JSAPI--公众号支付
* NATIVE--原生扫码支付
* APP--app支付
* </pre>
*/
private String tradeType;
/**
* 签名方式.
* 有两种HMAC_SHA256 和MD5
*
* @see com.github.binarywang.wxpay.constant.WxPayConstants.SignType
*/
private String signType;
private SSLContext sslContext;
/**
* p12证书base64编码
*/
private String keyString;
/**
* p12证书文件的绝对路径或者以classpath:开头的类路径.
*/
private String keyPath;
/**
* apiclient_key.pem证书base64编码
*/
private String privateKeyString;
/**
* apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径.
*/
private String privateKeyPath;
/**
* apiclient_cert.pem证书base64编码
*/
private String privateCertString;
/**
* apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径.
*/
private String privateCertPath;
/**
* apiclient_key.pem证书文件内容的字节数组.
*/
private byte[] privateKeyContent;
/**
* apiclient_cert.pem证书文件内容的字节数组.
*/
private byte[] privateCertContent;
/**
* 公钥ID
*/
private String publicKeyId;
/**
* pub_key.pem证书base64编码
*/
private String publicKeyString;
/**
* pub_key.pem证书文件的绝对路径或者以classpath:开头的类路径.
*/
private String publicKeyPath;
/**
* pub_key.pem证书文件内容的字节数组.
*/
private byte[] publicKeyContent;
/**
* apiV3 秘钥值.
*/
private String apiV3Key;
/**
* apiV3 证书序列号值
*/
private String certSerialNo;
/**
* 微信支付分serviceId
*/
private String serviceId;
/**
* 微信支付分回调地址
*/
private String payScoreNotifyUrl;
/**
* 微信支付分授权回调地址
*/
private String payScorePermissionNotifyUrl;
private CloseableHttpClient apiV3HttpClient;
/**
* 支持扩展httpClientBuilder
*/
private HttpClientBuilderCustomizer httpClientBuilderCustomizer;
private HttpClientBuilderCustomizer apiV3HttpClientBuilderCustomizer;
/**
* 私钥信息
*/
private PrivateKey privateKey;
/**
* 证书自动更新时间差(分钟),默认一分钟
*/
private int certAutoUpdateTime = 60;
/**
* p12证书文件内容的字节数组.
*/
private byte[] keyContent;
/**
* 微信支付是否使用仿真测试环境.
* 默认不使用
*/
private boolean useSandboxEnv = false;
/**
* 是否将接口请求日志信息保存到threadLocal中.
* 默认不保存
*/
private boolean ifSaveApiData = false;
private String httpProxyHost;
private Integer httpProxyPort;
private String httpProxyUsername;
private String httpProxyPassword;
/**
* v3接口下证书检验对象通过改对象可以获取到X509Certificate进一步对敏感信息加密
* <a href="https://wechatpay-api.gitbook.io/wechatpay-api-v3/qian-ming-zhi-nan-1/min-gan-xin-xi-jia-mi">文档</a>
*/
private Verifier verifier;
/**
* 返回所设置的微信支付接口请求地址域名.
*
* @return 微信支付接口请求地址域名
*/
public String getPayBaseUrl() {
if (StringUtils.isEmpty(this.payBaseUrl)) {
return DEFAULT_PAY_BASE_URL;
}
return this.payBaseUrl;
}
@SneakyThrows
public Verifier getVerifier() {
if (verifier == null) {
//当改对象为null时初始化api v3的请求头
initApiV3HttpClient();
}
return verifier;
}
/**
* 初始化ssl.
*
* @return the ssl context
* @throws WxPayException the wx pay exception
*/
public SSLContext initSSLContext() throws WxPayException {
if (StringUtils.isBlank(this.getMchId())) {
throw new WxPayException("请确保商户号mchId已设置");
}
try (InputStream inputStream = this.loadConfigInputStream(this.keyString, this.getKeyPath(),
this.keyContent, "p12证书")) {
KeyStore keystore = KeyStore.getInstance("PKCS12");
char[] partnerId2charArray = this.getMchId().toCharArray();
keystore.load(inputStream, partnerId2charArray);
this.sslContext = SSLContexts.custom().loadKeyMaterial(keystore, partnerId2charArray).build();
return this.sslContext;
} catch (Exception e) {
throw new WxPayException("证书文件有问题,请核实!", e);
}
}
/**
* 初始化api v3请求头 自动签名验签
* 方法参照 <a href="https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient">微信支付官方api项目</a>
*
* @author doger.wang
**/
public CloseableHttpClient initApiV3HttpClient() throws WxPayException {
if (StringUtils.isBlank(this.getApiV3Key())) {
throw new WxPayException("请确保apiV3Key值已设置");
}
// 尝试从p12证书中加载私钥和证书
PrivateKey merchantPrivateKey = null;
X509Certificate certificate = null;
Object[] objects = this.p12ToPem();
if (objects != null) {
merchantPrivateKey = (PrivateKey) objects[0];
certificate = (X509Certificate) objects[1];
this.certSerialNo = certificate.getSerialNumber().toString(16).toUpperCase();
}
try {
if (merchantPrivateKey == null) {
try (InputStream keyInputStream = this.loadConfigInputStream(this.getPrivateKeyString(), this.getPrivateKeyPath(),
this.privateKeyContent, "privateKeyPath")) {
merchantPrivateKey = PemUtils.loadPrivateKey(keyInputStream);
}
}
if (certificate == null && StringUtils.isBlank(this.getCertSerialNo())) {
try (InputStream certInputStream = this.loadConfigInputStream(this.getPrivateCertString(), this.getPrivateCertPath(),
this.privateCertContent, "privateCertPath")) {
certificate = PemUtils.loadCertificate(certInputStream);
}
this.certSerialNo = certificate.getSerialNumber().toString(16).toUpperCase();
}
PublicKey publicKey = null;
if (this.getPublicKeyString() != null || this.getPublicKeyPath() != null || this.publicKeyContent != null) {
try (InputStream pubInputStream =
this.loadConfigInputStream(this.getPublicKeyString(), this.getPublicKeyPath(),
this.publicKeyContent, "publicKeyPath")) {
publicKey = PemUtils.loadPublicKey(pubInputStream);
}
}
//构造Http Proxy正向代理
WxPayHttpProxy wxPayHttpProxy = getWxPayHttpProxy();
Verifier certificatesVerifier = getVerifier(merchantPrivateKey, wxPayHttpProxy, publicKey);
WxPayV3HttpClientBuilder wxPayV3HttpClientBuilder = WxPayV3HttpClientBuilder.create()
.withMerchant(mchId, certSerialNo, merchantPrivateKey)
.withValidator(new WxPayValidator(certificatesVerifier));
//初始化V3接口正向代理设置
HttpProxyUtils.initHttpProxy(wxPayV3HttpClientBuilder, wxPayHttpProxy);
// 提供自定义wxPayV3HttpClientBuilder的能力
Optional.ofNullable(apiV3HttpClientBuilderCustomizer).ifPresent(e -> {
e.customize(wxPayV3HttpClientBuilder);
});
CloseableHttpClient httpClient = wxPayV3HttpClientBuilder.build();
this.apiV3HttpClient = httpClient;
this.verifier = certificatesVerifier;
this.privateKey = merchantPrivateKey;
return httpClient;
} catch (WxPayException e) {
throw e;
} catch (Exception e) {
throw new WxPayException("v3请求构造异常", e);
}
}
private Verifier getVerifier(PrivateKey merchantPrivateKey, WxPayHttpProxy wxPayHttpProxy, PublicKey publicKey) {
Verifier certificatesVerifier = null;
// 如果配置了平台证书则初始化验证器以备v2版本接口验签公钥灰度实现
boolean pathB = this.getPrivateCertPath() != null && this.getPrivateKeyPath() != null;
boolean pathC = this.getPrivateCertContent() != null && this.getPrivateKeyContent() != null;
boolean pathS = this.getPrivateCertString() != null && this.getPrivateKeyString() != null;
if(WechatPayCode.API_V2.equals(this.getApiVersion())){
if (pathB || pathC || pathS) {
certificatesVerifier = new AutoUpdateCertificatesVerifier(
new WxPayCredentials(mchId, new PrivateKeySigner(certSerialNo, merchantPrivateKey)),
this.getApiV3Key().getBytes(StandardCharsets.UTF_8), this.getCertAutoUpdateTime(),
this.getPayBaseUrl(), wxPayHttpProxy);
}
}else{
if (publicKey != null) {
Verifier publicCertificatesVerifier = new PublicCertificateVerifier(publicKey, publicKeyId);
publicCertificatesVerifier.setOtherVerifier(certificatesVerifier);
certificatesVerifier = publicCertificatesVerifier;
}
}
return certificatesVerifier;
}
/**
* 初始化一个WxPayHttpProxy对象
*
* @return 返回封装的WxPayHttpProxy对象。如未指定代理主机和端口则默认返回null
*/
private WxPayHttpProxy getWxPayHttpProxy() {
if (StringUtils.isNotBlank(this.getHttpProxyHost()) && this.getHttpProxyPort() > 0) {
return new WxPayHttpProxy(getHttpProxyHost(), getHttpProxyPort(), getHttpProxyUsername(), getHttpProxyPassword());
}
return null;
}
/**
* 从指定参数加载输入流
*
* @param configString 证书内容进行Base64加密后的字符串
* @param configPath 证书路径
* @param configContent 证书内容的字节数组
* @param certName 证书的标识
* @return 输入流
* @throws WxPayException 异常
*/
private InputStream loadConfigInputStream(String configString, String configPath, byte[] configContent,
String certName) throws WxPayException {
if (configContent != null) {
return new ByteArrayInputStream(configContent);
}
if (StringUtils.isNotEmpty(configString)) {
configContent = Base64.getDecoder().decode(configString);
return new ByteArrayInputStream(configContent);
}
if (StringUtils.isBlank(configPath)) {
throw new WxPayException(String.format("请确保【%s】的文件地址【%s】存在", certName, configPath));
}
return this.loadConfigInputStream(configPath);
}
/**
* 从配置路径 加载配置 信息(支持 classpath、本地路径、网络url
*
* @param configPath 配置路径
* @return .
* @throws WxPayException .
*/
private InputStream loadConfigInputStream(String configPath) throws WxPayException {
String fileHasProblemMsg = String.format(PROBLEM_MSG, configPath);
String fileNotFoundMsg = String.format(NOT_FOUND_MSG, configPath);
final String prefix = "classpath:";
InputStream inputStream;
if (configPath.startsWith(prefix)) {
String path = RegExUtils.removeFirst(configPath, prefix);
if (!path.startsWith("/")) {
path = "/" + path;
}
try {
inputStream = ResourcesUtils.getResourceAsStream(path);
if (inputStream == null) {
throw new WxPayException(fileNotFoundMsg);
}
return inputStream;
} catch (Exception e) {
throw new WxPayException(fileNotFoundMsg, e);
}
}
if (configPath.startsWith("http://") || configPath.startsWith("https://")) {
try {
inputStream = new URL(configPath).openStream();
if (inputStream == null) {
throw new WxPayException(fileNotFoundMsg);
}
return inputStream;
} catch (IOException e) {
throw new WxPayException(fileNotFoundMsg, e);
}
} else {
try {
File file = new File(configPath);
if (!file.exists()) {
throw new WxPayException(fileNotFoundMsg);
}
//使用Files.newInputStream打开公私钥文件会存在无法释放句柄的问题
//return Files.newInputStream(file.toPath());
return new FileInputStream(file);
} catch (IOException e) {
throw new WxPayException(fileHasProblemMsg, e);
}
}
}
/**
* 分解p12证书文件
*/
private Object[] p12ToPem() {
String key = getMchId();
if (StringUtils.isBlank(key) ||
(StringUtils.isBlank(this.getKeyPath()) && this.keyContent == null && StringUtils.isBlank(this.keyString))) {
return null;
}
// 分解p12证书文件
try (InputStream inputStream = this.loadConfigInputStream(this.keyString, this.getKeyPath(),
this.keyContent, "p12证书")) {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(inputStream, key.toCharArray());
String alias = keyStore.aliases().nextElement();
PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, key.toCharArray());
Certificate certificate = keyStore.getCertificate(alias);
X509Certificate x509Certificate = (X509Certificate) certificate;
return new Object[]{privateKey, x509Certificate};
} catch (Exception e) {
log.error("加载p12证书时发生异常", e);
}
return null;
}
}

View File

@@ -2,6 +2,7 @@ package org.dromara.daxpay.channel.wechat.entity.allocation;
import cn.bootx.platform.common.mybatisplus.function.ToResult;
import cn.bootx.platform.core.util.JsonUtil;
import cn.hutool.json.JSONUtil;
import lombok.Data;
import lombok.experimental.Accessors;
import org.dromara.daxpay.channel.wechat.convert.WechatAllocReceiverConvert;
@@ -66,7 +67,7 @@ public class WechatAllocReceiver implements ToResult<WechatAllocReceiverResult>
* 转换为通道接收方
*/
public static WechatAllocReceiver convertChannel(AllocReceiver receiver) {
var leshuaAllocReceiver = JsonUtil.toBean(receiver.getExt(), WechatAllocReceiver.class);
var leshuaAllocReceiver = JSONUtil.toBean(receiver.getExt(), WechatAllocReceiver.class);
leshuaAllocReceiver.setId(receiver.getId())
.setReceiverNo(receiver.getReceiverNo())
.setReceiverName(receiver.getReceiverName())

View File

@@ -3,6 +3,7 @@ package org.dromara.daxpay.channel.wechat.entity.config;
import cn.bootx.platform.common.mybatisplus.function.ToResult;
import cn.bootx.platform.core.util.JsonUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import lombok.Data;
import lombok.experimental.Accessors;
import org.dromara.daxpay.channel.wechat.code.WechatPayCode;
@@ -114,7 +115,7 @@ public class WechatPayConfig implements ToResult<WechatPayConfigResult> {
* 从通道配置转换为微信支付配置
*/
public static WechatPayConfig convertConfig(ChannelConfig channelConfig) {
WechatPayConfig config = JsonUtil.toBean(channelConfig.getExt(), WechatPayConfig.class);
WechatPayConfig config = JSONUtil.toBean(channelConfig.getExt(), WechatPayConfig.class);
config.setId(channelConfig.getId())
.setWxAppId(channelConfig.getOutAppId())

View File

@@ -166,7 +166,7 @@ public class WechatPayConfigService {
payConfig.setSubAppId(wechatPayConfig.getSubAppId());
payConfig.setMchKey(wechatPayConfig.getApiKeyV2());
payConfig.setApiV3Key(wechatPayConfig.getApiKeyV3());
payConfig.setApiVersion(wechatPayConfig.getApiVersion());
// payConfig.setApiVersion(wechatPayConfig.getApiVersion());
// 注意不要使用base64的方式进行配置, 因为wxjava 是直接读取文本并不会进行解码, 会导致证书异常
if (StrUtil.isNotBlank(wechatPayConfig.getPublicKey())){
payConfig.setPublicKeyContent(Base64.decode(wechatPayConfig.getPublicKey()));

View File

@@ -6,13 +6,13 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.3</version>
<version>3.5.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.dromara.daxpay</groupId>
<artifactId>daxpay-open-channel</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
<packaging>pom</packaging>
<modelVersion>4.0.0</modelVersion>
<description>开源版支付通道功能实现</description>
@@ -35,8 +35,8 @@
<easypoi.version>4.5.0</easypoi.version>
<wxjava.version>4.7.4.B</wxjava.version>
<bootx-platform.version>3.0.0.beta5</bootx-platform.version>
<daxpay.version>3.0.0.beta5</daxpay.version>
<bootx-platform.version>3.0.0</bootx-platform.version>
<daxpay.version>3.0.0</daxpay.version>
</properties>
<dependencies>

View File

@@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.dromara.daxpay</groupId>
<artifactId>daxpay-open-sdk</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
<packaging>jar</packaging>
<!-- 项目信息 -->

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.3</version>
<version>3.5.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
@@ -19,8 +19,8 @@
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<bootx-platform.version>3.0.0.beta5</bootx-platform.version>
<daxpay.version>3.0.0.beta5</daxpay.version>
<bootx-platform.version>3.0.0</bootx-platform.version>
<daxpay.version>3.0.0</daxpay.version>
<minio.version>8.5.2</minio.version>
</properties>
@@ -46,16 +46,9 @@
</dependency>
<!-- 数据库驱动 MySQL -->
<!-- <dependency>-->
<!-- <groupId>com.mysql</groupId>-->
<!-- <artifactId>mysql-connector-j</artifactId>-->
<!-- </dependency>-->
<!--文件存储 (minio方式)-->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${minio.version}</version>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- 支付通道 -->

View File

@@ -68,9 +68,6 @@ bootx-platform:
- '/css/**'
- '/error'
- '/favicon.ico'
file-upload:
# 使用后端代理访问, 线上请使用 Nginx 配置或者直连方式,效率更高
forward-server-url: http://127.0.0.1:9999
dax-pay:
env: DEV_
machine-no: 70

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.dromara.daxpay</groupId>
<artifactId>daxpay-open</artifactId>
<version>3.0.0.beta5</version>
<version>3.0.0</version>
</parent>
<artifactId>daxpay-open-controller</artifactId>

Some files were not shown because too many files have changed in this diff Show More