Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
T
travel-cloud
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
cloud
travel-cloud
Commits
44629dc1
提交
44629dc1
authored
2月 14, 2025
作者:
hzh
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
商城模块集成
上级
10b339a4
隐藏空白字符变更
内嵌
并排
正在显示
141 个修改的文件
包含
9219 行增加
和
2 行删除
+9219
-2
pom.xml
ruoyi-api/pom.xml
+1
-0
pom.xml
ruoyi-api/ruoyi-api-bom/pom.xml
+7
-0
pom.xml
ruoyi-api/ruoyi-api-mall/pom.xml
+33
-0
RemoteMember.java
...java/org/dromara/mall/api/domain/member/RemoteMember.java
+33
-0
RemoteMemberSave.java
.../org/dromara/mall/api/domain/member/RemoteMemberSave.java
+36
-0
RemoteMemberService.java
...java/org/dromara/mall/api/member/RemoteMemberService.java
+29
-0
RemotePayNotifyService.java
...java/org/dromara/mall/api/pay/RemotePayNotifyService.java
+17
-0
RemotePayOrderService.java
.../java/org/dromara/mall/api/pay/RemotePayOrderService.java
+26
-0
RemotePayRefundService.java
...java/org/dromara/mall/api/pay/RemotePayRefundService.java
+15
-0
RemotePayTransferService.java
...va/org/dromara/mall/api/pay/RemotePayTransferService.java
+15
-0
RemoteCombinationRecordService.java
...ra/mall/api/promotion/RemoteCombinationRecordService.java
+19
-0
RemoteCouponService.java
...a/org/dromara/mall/api/promotion/RemoteCouponService.java
+17
-0
RemoteProductStatisticsService.java
...a/mall/api/statistics/RemoteProductStatisticsService.java
+17
-0
RemoteTradeStatisticsService.java
...ara/mall/api/statistics/RemoteTradeStatisticsService.java
+18
-0
RemoteBrokerageRecordService.java
.../dromara/mall/api/trade/RemoteBrokerageRecordService.java
+15
-0
RemoteTradeOrderUpdateService.java
...dromara/mall/api/trade/RemoteTradeOrderUpdateService.java
+30
-0
pom.xml
ruoyi-auth/pom.xml
+5
-0
AbstractMallStrategy.java
...a/org/dromara/auth/service/impl/AbstractMallStrategy.java
+69
-0
XcxAuthStrategy.java
...n/java/org/dromara/auth/service/impl/XcxAuthStrategy.java
+5
-1
XcxPhoneAuthStrategy.java
...a/org/dromara/auth/service/impl/XcxPhoneAuthStrategy.java
+5
-1
pom.xml
ruoyi-common/pom.xml
+3
-0
pom.xml
ruoyi-common/ruoyi-common-allPay/pom.xml
+78
-0
TransferBatchesRequest.java
...inarywang/wxpay/bean/transfer/TransferBatchesRequest.java
+120
-0
YudaoPayAutoConfiguration.java
.../dromara/common/pay/config/YudaoPayAutoConfiguration.java
+22
-0
PayClient.java
...in/java/org/dromara/common/pay/core/client/PayClient.java
+111
-0
PayClientConfig.java
...a/org/dromara/common/pay/core/client/PayClientConfig.java
+25
-0
PayClientFactory.java
.../org/dromara/common/pay/core/client/PayClientFactory.java
+40
-0
PayOrderRespDTO.java
...ara/common/pay/core/client/dto/order/PayOrderRespDTO.java
+142
-0
PayOrderUnifiedReqDTO.java
...mmon/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java
+93
-0
PayRefundRespDTO.java
...a/common/pay/core/client/dto/refund/PayRefundRespDTO.java
+117
-0
PayRefundUnifiedReqDTO.java
...on/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java
+69
-0
PayTransferRespDTO.java
...mmon/pay/core/client/dto/transfer/PayTransferRespDTO.java
+109
-0
PayTransferUnifiedReqDTO.java
...ay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java
+90
-0
WxPayTransferPartnerNotifyV3Result.java
...ient/dto/transfer/WxPayTransferPartnerNotifyV3Result.java
+129
-0
PayException.java
...romara/common/pay/core/client/exception/PayException.java
+17
-0
AbstractPayClient.java
...romara/common/pay/core/client/impl/AbstractPayClient.java
+268
-0
NonePayClientConfig.java
...mara/common/pay/core/client/impl/NonePayClientConfig.java
+31
-0
PayClientFactoryImpl.java
...ara/common/pay/core/client/impl/PayClientFactoryImpl.java
+98
-0
AbstractAlipayPayClient.java
.../pay/core/client/impl/alipay/AbstractAlipayPayClient.java
+348
-0
AlipayAppPayClient.java
...ommon/pay/core/client/impl/alipay/AlipayAppPayClient.java
+61
-0
AlipayBarPayClient.java
...ommon/pay/core/client/impl/alipay/AlipayBarPayClient.java
+87
-0
AlipayPayClientConfig.java
...on/pay/core/client/impl/alipay/AlipayPayClientConfig.java
+128
-0
AlipayPcPayClient.java
...common/pay/core/client/impl/alipay/AlipayPcPayClient.java
+70
-0
AlipayQrPayClient.java
...common/pay/core/client/impl/alipay/AlipayQrPayClient.java
+68
-0
AlipayWapPayClient.java
...ommon/pay/core/client/impl/alipay/AlipayWapPayClient.java
+59
-0
MockPayClient.java
...omara/common/pay/core/client/impl/mock/MockPayClient.java
+86
-0
AbstractWxPayClient.java
...mmon/pay/core/client/impl/weixin/AbstractWxPayClient.java
+556
-0
WxAppPayClient.java
...ra/common/pay/core/client/impl/weixin/WxAppPayClient.java
+65
-0
WxBarPayClient.java
...ra/common/pay/core/client/impl/weixin/WxBarPayClient.java
+108
-0
WxLitePayClient.java
...a/common/pay/core/client/impl/weixin/WxLitePayClient.java
+22
-0
WxNativePayClient.java
...common/pay/core/client/impl/weixin/WxNativePayClient.java
+60
-0
WxPayClientConfig.java
...common/pay/core/client/impl/weixin/WxPayClientConfig.java
+103
-0
WxPubPayClient.java
...ra/common/pay/core/client/impl/weixin/WxPubPayClient.java
+81
-0
WxWapPayClient.java
...ra/common/pay/core/client/impl/weixin/WxWapPayClient.java
+63
-0
PayChannelEnum.java
...dromara/common/pay/core/enums/channel/PayChannelEnum.java
+69
-0
PayOrderDisplayModeEnum.java
.../common/pay/core/enums/order/PayOrderDisplayModeEnum.java
+29
-0
PayOrderStatusRespEnum.java
...a/common/pay/core/enums/order/PayOrderStatusRespEnum.java
+56
-0
PayRefundStatusRespEnum.java
...common/pay/core/enums/refund/PayRefundStatusRespEnum.java
+32
-0
PayTransferStatusRespEnum.java
...on/pay/core/enums/transfer/PayTransferStatusRespEnum.java
+45
-0
PayTransferTypeEnum.java
...a/common/pay/core/enums/transfer/PayTransferTypeEnum.java
+44
-0
org.springframework.boot.autoconfigure.AutoConfiguration.imports
...ingframework.boot.autoconfigure.AutoConfiguration.imports
+1
-0
pom.xml
ruoyi-common/ruoyi-common-bom/pom.xml
+21
-0
pom.xml
ruoyi-common/ruoyi-common-core/pom.xml
+15
-0
FileUtils.java
...in/java/org/dromara/common/core/utils/file/FileUtils.java
+48
-0
JsonUtils.java
...in/java/org/dromara/common/core/utils/json/JsonUtils.java
+202
-0
NumberSerializer.java
...ara/common/core/utils/json/databind/NumberSerializer.java
+37
-0
TimestampLocalDateTimeDeserializer.java
...ils/json/databind/TimestampLocalDateTimeDeserializer.java
+27
-0
TimestampLocalDateTimeSerializer.java
...utils/json/databind/TimestampLocalDateTimeSerializer.java
+26
-0
StrUtils.java
...n/java/org/dromara/common/core/utils/string/StrUtils.java
+80
-0
DictUtils.java
...rc/main/java/org/dromara/common/dict/utils/DictUtils.java
+30
-0
MoneyConvert.java
...n/java/org/dromara/common/excel/convert/MoneyConvert.java
+39
-0
pom.xml
ruoyi-common/ruoyi-common-mall/pom.xml
+46
-0
IntArrayValuable.java
...n/java/org/dromara/common/mall/core/IntArrayValuable.java
+15
-0
KeyValue.java
.../src/main/java/org/dromara/common/mall/core/KeyValue.java
+22
-0
Area.java
...ll/src/main/java/org/dromara/common/mall/domain/Area.java
+63
-0
AreaTypeEnum.java
...main/java/org/dromara/common/mall/enums/AreaTypeEnum.java
+39
-0
CommonStatusEnum.java
.../java/org/dromara/common/mall/enums/CommonStatusEnum.java
+45
-0
DateIntervalEnum.java
.../java/org/dromara/common/mall/enums/DateIntervalEnum.java
+46
-0
TerminalEnum.java
...main/java/org/dromara/common/mall/enums/TerminalEnum.java
+40
-0
UserTypeEnum.java
...main/java/org/dromara/common/mall/enums/UserTypeEnum.java
+39
-0
ErrorCode.java
...ain/java/org/dromara/common/mall/exception/ErrorCode.java
+31
-0
GlobalErrorCodeConstants.java
...common/mall/exception/enums/GlobalErrorCodeConstants.java
+42
-0
ServiceErrorCodeRange.java
...ra/common/mall/exception/enums/ServiceErrorCodeRange.java
+48
-0
ServiceExceptionUtil.java
...mara/common/mall/exception/util/ServiceExceptionUtil.java
+75
-0
CacheUtils.java
...n/java/org/dromara/common/mall/util/cache/CacheUtils.java
+49
-0
CollectionUtils.java
.../dromara/common/mall/util/collection/CollectionUtils.java
+323
-0
MapUtils.java
...ava/org/dromara/common/mall/util/collection/MapUtils.java
+68
-0
DateUtils.java
...ain/java/org/dromara/common/mall/util/date/DateUtils.java
+149
-0
LocalDateTimeUtils.java
...org/dromara/common/mall/util/date/LocalDateTimeUtils.java
+309
-0
AreaUtils.java
.../main/java/org/dromara/common/mall/util/ip/AreaUtils.java
+215
-0
IPUtils.java
...rc/main/java/org/dromara/common/mall/util/ip/IPUtils.java
+87
-0
MoneyUtils.java
.../java/org/dromara/common/mall/util/number/MoneyUtils.java
+131
-0
NumberUtils.java
...java/org/dromara/common/mall/util/number/NumberUtils.java
+64
-0
BeanUtils.java
...n/java/org/dromara/common/mall/util/object/BeanUtils.java
+53
-0
ObjectUtils.java
...java/org/dromara/common/mall/util/object/ObjectUtils.java
+63
-0
ValidationUtils.java
.../dromara/common/mall/util/validation/ValidationUtils.java
+55
-0
InEnum.java
.../main/java/org/dromara/common/mall/validation/InEnum.java
+35
-0
InEnumCollectionValidator.java
...ara/common/mall/validation/InEnumCollectionValidator.java
+42
-0
InEnumValidator.java
...a/org/dromara/common/mall/validation/InEnumValidator.java
+44
-0
Mobile.java
.../main/java/org/dromara/common/mall/validation/Mobile.java
+29
-0
MobileValidator.java
...a/org/dromara/common/mall/validation/MobileValidator.java
+24
-0
Telephone.java
...in/java/org/dromara/common/mall/validation/Telephone.java
+29
-0
TelephoneValidator.java
...rg/dromara/common/mall/validation/TelephoneValidator.java
+24
-0
org.springframework.boot.autoconfigure.AutoConfiguration.imports
...ingframework.boot.autoconfigure.AutoConfiguration.imports
+0
-0
area.csv
ruoyi-common/ruoyi-common-mall/src/main/resources/area.csv
+0
-0
ip2region.xdb
...common/ruoyi-common-mall/src/main/resources/ip2region.xdb
+0
-0
BaseMapperPlus.java
...rg/dromara/common/mybatis/core/mapper/BaseMapperPlus.java
+165
-0
pom.xml
ruoyi-common/ruoyi-common-satoken/pom.xml
+5
-0
LoginHelper.java
...in/java/org/dromara/common/satoken/utils/LoginHelper.java
+16
-0
pom.xml
ruoyi-common/ruoyi-common-yudao-mybatis/pom.xml
+37
-0
BaseMapperPlusPlus.java
...romara/common/mybatis/core/mapper/BaseMapperPlusPlus.java
+54
-0
PageParam.java
.../java/org/dromara/common/mybatis/core/page/PageParam.java
+36
-0
PageResult.java
...java/org/dromara/common/mybatis/core/page/PageResult.java
+55
-0
SortablePageParam.java
...g/dromara/common/mybatis/core/page/SortablePageParam.java
+19
-0
SortingField.java
...va/org/dromara/common/mybatis/core/page/SortingField.java
+37
-0
LambdaQueryWrapperX.java
...romara/common/mybatis/core/query/LambdaQueryWrapperX.java
+135
-0
MPJLambdaWrapperX.java
.../dromara/common/mybatis/core/query/MPJLambdaWrapperX.java
+313
-0
QueryWrapperX.java
.../org/dromara/common/mybatis/core/query/QueryWrapperX.java
+166
-0
EncryptTypeHandler.java
.../dromara/common/mybatis/core/type/EncryptTypeHandler.java
+75
-0
IntegerListTypeHandler.java
...mara/common/mybatis/core/type/IntegerListTypeHandler.java
+56
-0
LongListTypeHandler.java
...dromara/common/mybatis/core/type/LongListTypeHandler.java
+57
-0
StringListTypeHandler.java
...omara/common/mybatis/core/type/StringListTypeHandler.java
+58
-0
DbTypeEnum.java
...ain/java/org/dromara/common/mybatis/enums/DbTypeEnum.java
+95
-0
BeanUtils.java
.../main/java/org/dromara/common/mybatis/util/BeanUtils.java
+36
-0
JdbcUtils.java
.../main/java/org/dromara/common/mybatis/util/JdbcUtils.java
+94
-0
MyBatisUtils.java
...in/java/org/dromara/common/mybatis/util/MyBatisUtils.java
+106
-0
PageUtils.java
.../main/java/org/dromara/common/mybatis/util/PageUtils.java
+67
-0
pom.xml
ruoyi-modules/ruoyi-job/pom.xml
+5
-0
PayNotifyJob.java
...rg/dromara/job/snailjob/mall/pay/notify/PayNotifyJob.java
+30
-0
PayOrderExpireJob.java
...romara/job/snailjob/mall/pay/order/PayOrderExpireJob.java
+32
-0
PayOrderSyncJob.java
.../dromara/job/snailjob/mall/pay/order/PayOrderSyncJob.java
+45
-0
PayRefundSyncJob.java
...romara/job/snailjob/mall/pay/refund/PayRefundSyncJob.java
+32
-0
PayTransferSyncJob.java
...ra/job/snailjob/mall/pay/transfer/PayTransferSyncJob.java
+31
-0
CombinationRecordExpireJob.java
...all/promotion/combination/CombinationRecordExpireJob.java
+31
-0
CouponExpireJob.java
...a/job/snailjob/mall/promotion/coupon/CouponExpireJob.java
+30
-0
ProductStatisticsJob.java
...nailjob/mall/statistics/product/ProductStatisticsJob.java
+50
-0
TradeStatisticsJob.java
...ob/snailjob/mall/statistics/trade/TradeStatisticsJob.java
+50
-0
BrokerageRecordUnfreezeJob.java
...ljob/mall/trade/brokerage/BrokerageRecordUnfreezeJob.java
+30
-0
TradeOrderAutoCancelJob.java
...ob/snailjob/mall/trade/order/TradeOrderAutoCancelJob.java
+29
-0
TradeOrderAutoCommentJob.java
...b/snailjob/mall/trade/order/TradeOrderAutoCommentJob.java
+29
-0
TradeOrderAutoReceiveJob.java
...b/snailjob/mall/trade/order/TradeOrderAutoReceiveJob.java
+29
-0
没有找到文件。
ruoyi-api/pom.xml
浏览文件 @
44629dc1
...
@@ -15,6 +15,7 @@
...
@@ -15,6 +15,7 @@
<module>
ruoyi-api-workflow
</module>
<module>
ruoyi-api-workflow
</module>
<module>
ruoyi-api-server
</module>
<module>
ruoyi-api-server
</module>
<module>
ruoyi-api-order
</module>
<module>
ruoyi-api-order
</module>
<module>
ruoyi-api-mall
</module>
</modules>
</modules>
<artifactId>
ruoyi-api
</artifactId>
<artifactId>
ruoyi-api
</artifactId>
...
...
ruoyi-api/ruoyi-api-bom/pom.xml
浏览文件 @
44629dc1
...
@@ -55,6 +55,13 @@
...
@@ -55,6 +55,13 @@
<version>
${revision}
</version>
<version>
${revision}
</version>
</dependency>
</dependency>
<!-- 商城接口 -->
<dependency>
<groupId>
org.dromara
</groupId>
<artifactId>
ruoyi-api-mall
</artifactId>
<version>
${revision}
</version>
</dependency>
</dependencies>
</dependencies>
</dependencyManagement>
</dependencyManagement>
</project>
</project>
ruoyi-api/ruoyi-api-mall/pom.xml
0 → 100644
浏览文件 @
44629dc1
<?xml version="1.0" encoding="UTF-8"?>
<project
xmlns:xsi=
"http://www.w3.org/2001/XMLSchema-instance"
xmlns=
"http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation=
"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
>
<parent>
<groupId>
org.dromara
</groupId>
<artifactId>
ruoyi-api
</artifactId>
<version>
${revision}
</version>
</parent>
<modelVersion>
4.0.0
</modelVersion>
<artifactId>
ruoyi-api-mall
</artifactId>
<description>
ruoyi-api-resource 商城接口模块
</description>
<dependencies>
<!-- RuoYi Common Core-->
<dependency>
<groupId>
org.dromara
</groupId>
<artifactId>
ruoyi-common-core
</artifactId>
</dependency>
<dependency>
<groupId>
org.dromara
</groupId>
<artifactId>
ruoyi-common-mall
</artifactId>
</dependency>
</dependencies>
</project>
ruoyi-api/ruoyi-api-mall/src/main/java/org/dromara/mall/api/domain/member/RemoteMember.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
mall
.
api
.
domain
.
member
;
import
lombok.Data
;
import
java.io.Serial
;
import
java.io.Serializable
;
/**
* @author hzh
* @date 2025-02-08
**/
@Data
public
class
RemoteMember
implements
Serializable
{
@Serial
private
static
final
long
serialVersionUID
=
1L
;
/**
* 用户id
*/
private
Long
id
;
/**
* 第三方用户ID
*/
private
Long
thirdUserId
;
/**
* 手机号
*/
private
String
phonenumber
;
}
ruoyi-api/ruoyi-api-mall/src/main/java/org/dromara/mall/api/domain/member/RemoteMemberSave.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
mall
.
api
.
domain
.
member
;
import
lombok.Data
;
import
lombok.experimental.Accessors
;
import
java.io.Serial
;
import
java.io.Serializable
;
/**
* @author hzh
* @date 2025-02-08
**/
@Data
@Accessors
(
chain
=
true
)
public
class
RemoteMemberSave
implements
Serializable
{
@Serial
private
static
final
long
serialVersionUID
=
1L
;
/**
* 第三方用户ID
*/
private
Long
thirdUserId
;
/**
* 手机号
*/
private
String
phonenumber
;
/**
* 注册IP
*/
private
String
registerIp
;
}
ruoyi-api/ruoyi-api-mall/src/main/java/org/dromara/mall/api/member/RemoteMemberService.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
mall
.
api
.
member
;
import
org.dromara.mall.api.domain.member.RemoteMember
;
import
org.dromara.mall.api.domain.member.RemoteMemberSave
;
/**
* 会员中心
*
* @author wenhe
*/
public
interface
RemoteMemberService
{
/**
* 根据第三方用户ID查询会员信息
*
* @param thirdUserId 第三方用户ID
* @return 会员信息
*/
RemoteMember
selectByThirdUserId
(
Long
thirdUserId
);
/**
* 保存会员信息
*
* @param member 会员信息
* @return 会员信息
*/
RemoteMember
saveMember
(
RemoteMemberSave
member
);
}
ruoyi-api/ruoyi-api-mall/src/main/java/org/dromara/mall/api/pay/RemotePayNotifyService.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
mall
.
api
.
pay
;
/**
* @author wenhe
*/
public
interface
RemotePayNotifyService
{
/**
* 执行回调通知
*
* 注意,该方法提供给定时任务调用。目前是 yudao-server 进行调用
* @return 通知数量
*/
int
executeNotify
()
throws
InterruptedException
;
}
ruoyi-api/ruoyi-api-mall/src/main/java/org/dromara/mall/api/pay/RemotePayOrderService.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
mall
.
api
.
pay
;
import
java.time.LocalDateTime
;
/**
* @author wenhe
*/
public
interface
RemotePayOrderService
{
/**
* 将已过期的订单,状态修改为已关闭
*
* @return 过期的订单数量
*/
int
expireOrder
();
/**
* 同步订单的支付状态
*
* @param minCreateTime 最小创建时间
* @return 同步到已支付的订单数量
*/
int
syncOrder
(
LocalDateTime
minCreateTime
);
}
ruoyi-api/ruoyi-api-mall/src/main/java/org/dromara/mall/api/pay/RemotePayRefundService.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
mall
.
api
.
pay
;
/**
* @author wenhe
*/
public
interface
RemotePayRefundService
{
/**
* 同步渠道退款的退款状态
*
* @return 同步到状态的退款数量,包括退款成功、退款失败
*/
int
syncRefund
();
}
ruoyi-api/ruoyi-api-mall/src/main/java/org/dromara/mall/api/pay/RemotePayTransferService.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
mall
.
api
.
pay
;
/**
* @author wenhe
*/
public
interface
RemotePayTransferService
{
/**
* 同步渠道转账单状态
*
* @return 同步到状态的转账数量,包括转账成功、转账失败、转账中的
*/
int
syncTransfer
();
}
ruoyi-api/ruoyi-api-mall/src/main/java/org/dromara/mall/api/promotion/RemoteCombinationRecordService.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
mall
.
api
.
promotion
;
import
org.dromara.common.mall.core.KeyValue
;
/**
* @author hzh
* @date 2025-02-11
* @desc 拼团过期
**/
public
interface
RemoteCombinationRecordService
{
/**
* 处理过期拼团
*
* @return key 过期拼团数量, value 虚拟成团数量
*/
KeyValue
<
Integer
,
Integer
>
expireCombinationRecord
();
}
ruoyi-api/ruoyi-api-mall/src/main/java/org/dromara/mall/api/promotion/RemoteCouponService.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
mall
.
api
.
promotion
;
/**
* 优惠券过期
*
* @author wenhe
*/
public
interface
RemoteCouponService
{
/**
* 过期优惠券
*
* @return 过期数量
*/
int
expireCoupon
();
}
ruoyi-api/ruoyi-api-mall/src/main/java/org/dromara/mall/api/statistics/RemoteProductStatisticsService.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
mall
.
api
.
statistics
;
/**
* 商品统计
*
* @author wenhe
*/
public
interface
RemoteProductStatisticsService
{
/**
* 统计指定天数的商品数据
*
* @return 统计结果
*/
String
statisticsProduct
(
Integer
days
);
}
ruoyi-api/ruoyi-api-mall/src/main/java/org/dromara/mall/api/statistics/RemoteTradeStatisticsService.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
mall
.
api
.
statistics
;
/**
* 交易统计
*
* @author wenhe
*/
public
interface
RemoteTradeStatisticsService
{
/**
* 统计指定天数的交易数据
*
* @param days 天数
* @return 统计结果
*/
String
statisticsTrade
(
Integer
days
);
}
ruoyi-api/ruoyi-api-mall/src/main/java/org/dromara/mall/api/trade/RemoteBrokerageRecordService.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
mall
.
api
.
trade
;
/**
* @author wenhe
*/
public
interface
RemoteBrokerageRecordService
{
/**
* 解冻佣金:将待结算的佣金记录,状态修改为已结算
*
* @return 解冻佣金的数量
*/
int
unfreezeRecord
();
}
ruoyi-api/ruoyi-api-mall/src/main/java/org/dromara/mall/api/trade/RemoteTradeOrderUpdateService.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
mall
.
api
.
trade
;
/**
* @author wenhe
*/
public
interface
RemoteTradeOrderUpdateService
{
/**
* 【系统】自动取消订单
*
* @return 取消数量
*/
int
cancelOrderBySystem
();
/**
* 【系统】创建订单项的评论
*
* @return 被评论的订单数
*/
int
createOrderItemCommentBySystem
();
/**
* 【系统】自动收货交易订单
*
* @return 收货数量
*/
int
receiveOrderBySystem
();
}
ruoyi-auth/pom.xml
浏览文件 @
44629dc1
...
@@ -91,6 +91,11 @@
...
@@ -91,6 +91,11 @@
<artifactId>
ruoyi-api-server
</artifactId>
<artifactId>
ruoyi-api-server
</artifactId>
</dependency>
</dependency>
<dependency>
<groupId>
org.dromara
</groupId>
<artifactId>
ruoyi-api-mall
</artifactId>
</dependency>
<dependency>
<dependency>
<groupId>
org.dromara
</groupId>
<groupId>
org.dromara
</groupId>
<artifactId>
ruoyi-common-weixin
</artifactId>
<artifactId>
ruoyi-common-weixin
</artifactId>
...
...
ruoyi-auth/src/main/java/org/dromara/auth/service/impl/AbstractMallStrategy.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
auth
.
service
.
impl
;
import
lombok.RequiredArgsConstructor
;
import
org.apache.dubbo.config.annotation.DubboReference
;
import
org.dromara.auth.service.IAuthStrategy
;
import
org.dromara.common.core.domain.model.LoginBody
;
import
org.dromara.common.core.enums.UserType
;
import
org.dromara.common.core.utils.StringUtils
;
import
org.dromara.mall.api.domain.member.RemoteMember
;
import
org.dromara.mall.api.domain.member.RemoteMemberSave
;
import
org.dromara.mall.api.member.RemoteMemberService
;
import
org.dromara.system.api.RemoteUserService
;
import
org.dromara.system.api.domain.bo.RemoteUserBo
;
import
org.dromara.system.api.model.XcxLoginUser
;
import
org.jetbrains.annotations.Nullable
;
import
org.springframework.stereotype.Service
;
import
static
org
.
dromara
.
common
.
core
.
constant
.
TenantConstants
.
DEFAULT_TENANT_ID
;
import
static
org
.
dromara
.
common
.
core
.
utils
.
ServletUtils
.
getClientIP
;
/**
* @author hzh
* @date 2025-02-08
* @desc TODO
**/
@Service
@RequiredArgsConstructor
public
abstract
class
AbstractMallStrategy
implements
IAuthStrategy
{
@DubboReference
private
RemoteUserService
remoteUserService
;
@DubboReference
private
RemoteMemberService
remoteMemberService
;
@Nullable
Long
getMemberId
(
LoginBody
loginBody
,
XcxLoginUser
loginUser
)
{
// 增加商城逻辑
Long
thirdUserId
;
if
(
StringUtils
.
equals
(
loginBody
.
getTenantId
(),
DEFAULT_TENANT_ID
)
&&
StringUtils
.
equals
(
loginUser
.
getUserType
(),
UserType
.
APP_USER
.
getUserType
()))
{
thirdUserId
=
loginUser
.
getUserId
();
}
else
{
// 创建用户
RemoteUserBo
remoteUserBo
=
new
RemoteUserBo
();
remoteUserBo
.
setTenantId
(
DEFAULT_TENANT_ID
);
remoteUserBo
.
setUserName
(
StringUtils
.
isNotEmpty
(
loginUser
.
getPhone
())
?
loginUser
.
getPhone
()
:
loginUser
.
getUsername
());
remoteUserBo
.
setNickName
(
remoteUserBo
.
getUserName
());
remoteUserBo
.
setUserType
(
UserType
.
APP_USER
.
getUserType
());
remoteUserBo
.
setPhonenumber
(
loginUser
.
getPhone
());
thirdUserId
=
remoteUserService
.
registerUserInfo
(
remoteUserBo
);
}
Long
memberId
=
null
;
if
(
thirdUserId
!=
null
)
{
RemoteMember
rm
=
remoteMemberService
.
selectByThirdUserId
(
thirdUserId
);
if
(
rm
==
null
)
{
rm
=
remoteMemberService
.
saveMember
(
new
RemoteMemberSave
()
.
setThirdUserId
(
thirdUserId
)
.
setPhonenumber
(
loginUser
.
getPhone
())
.
setRegisterIp
(
getClientIP
())
);
}
memberId
=
rm
.
getId
();
}
return
memberId
;
}
}
ruoyi-auth/src/main/java/org/dromara/auth/service/impl/XcxAuthStrategy.java
浏览文件 @
44629dc1
...
@@ -32,7 +32,7 @@ import java.util.Optional;
...
@@ -32,7 +32,7 @@ import java.util.Optional;
@Slf4j
@Slf4j
@Service
(
"xcx"
+
IAuthStrategy
.
BASE_NAME
)
@Service
(
"xcx"
+
IAuthStrategy
.
BASE_NAME
)
@RequiredArgsConstructor
@RequiredArgsConstructor
public
class
XcxAuthStrategy
implements
IAuthStrategy
{
public
class
XcxAuthStrategy
extends
AbstractMallStrategy
implements
IAuthStrategy
{
private
final
SysLoginService
loginService
;
private
final
SysLoginService
loginService
;
...
@@ -70,6 +70,9 @@ public class XcxAuthStrategy implements IAuthStrategy {
...
@@ -70,6 +70,9 @@ public class XcxAuthStrategy implements IAuthStrategy {
loginUser
.
setClientKey
(
client
.
getClientKey
());
loginUser
.
setClientKey
(
client
.
getClientKey
());
loginUser
.
setDeviceType
(
client
.
getDeviceType
());
loginUser
.
setDeviceType
(
client
.
getDeviceType
());
//获取会员id
Long
memberId
=
getMemberId
(
loginBody
,
loginUser
);
SaLoginModel
model
=
new
SaLoginModel
();
SaLoginModel
model
=
new
SaLoginModel
();
model
.
setDevice
(
client
.
getDeviceType
());
model
.
setDevice
(
client
.
getDeviceType
());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
...
@@ -81,6 +84,7 @@ public class XcxAuthStrategy implements IAuthStrategy {
...
@@ -81,6 +84,7 @@ public class XcxAuthStrategy implements IAuthStrategy {
model
.
setExtra
(
LoginHelper
.
CLIENT_KEY
,
client
.
getClientId
());
model
.
setExtra
(
LoginHelper
.
CLIENT_KEY
,
client
.
getClientId
());
model
.
setExtra
(
LoginHelper
.
YS_USER_ID
,
Optional
.
ofNullable
(
ru
).
map
(
RemoteUser:
:
getUserNo
).
orElse
(
null
));
model
.
setExtra
(
LoginHelper
.
YS_USER_ID
,
Optional
.
ofNullable
(
ru
).
map
(
RemoteUser:
:
getUserNo
).
orElse
(
null
));
model
.
setExtra
(
LoginHelper
.
OPEN_ID
,
openid
);
model
.
setExtra
(
LoginHelper
.
OPEN_ID
,
openid
);
model
.
setExtra
(
LoginHelper
.
MEMBER_ID
,
memberId
);
// 生成token
// 生成token
LoginHelper
.
login
(
loginUser
,
model
);
LoginHelper
.
login
(
loginUser
,
model
);
...
...
ruoyi-auth/src/main/java/org/dromara/auth/service/impl/XcxPhoneAuthStrategy.java
浏览文件 @
44629dc1
...
@@ -32,7 +32,7 @@ import java.util.Optional;
...
@@ -32,7 +32,7 @@ import java.util.Optional;
@Slf4j
@Slf4j
@Service
(
"xcxPhone"
+
IAuthStrategy
.
BASE_NAME
)
@Service
(
"xcxPhone"
+
IAuthStrategy
.
BASE_NAME
)
@RequiredArgsConstructor
@RequiredArgsConstructor
public
class
XcxPhoneAuthStrategy
implements
IAuthStrategy
{
public
class
XcxPhoneAuthStrategy
extends
AbstractMallStrategy
implements
IAuthStrategy
{
private
final
SysLoginService
loginService
;
private
final
SysLoginService
loginService
;
...
@@ -65,6 +65,9 @@ public class XcxPhoneAuthStrategy implements IAuthStrategy {
...
@@ -65,6 +65,9 @@ public class XcxPhoneAuthStrategy implements IAuthStrategy {
loginUser
.
setClientKey
(
client
.
getClientKey
());
loginUser
.
setClientKey
(
client
.
getClientKey
());
loginUser
.
setDeviceType
(
client
.
getDeviceType
());
loginUser
.
setDeviceType
(
client
.
getDeviceType
());
//获取会员id
Long
memberId
=
getMemberId
(
loginBody
,
loginUser
);
SaLoginModel
model
=
new
SaLoginModel
();
SaLoginModel
model
=
new
SaLoginModel
();
model
.
setDevice
(
client
.
getDeviceType
());
model
.
setDevice
(
client
.
getDeviceType
());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
...
@@ -75,6 +78,7 @@ public class XcxPhoneAuthStrategy implements IAuthStrategy {
...
@@ -75,6 +78,7 @@ public class XcxPhoneAuthStrategy implements IAuthStrategy {
model
.
setExtra
(
LoginHelper
.
CLIENT_KEY
,
client
.
getClientId
());
model
.
setExtra
(
LoginHelper
.
CLIENT_KEY
,
client
.
getClientId
());
model
.
setExtra
(
LoginHelper
.
YS_USER_ID
,
Optional
.
ofNullable
(
ru
).
map
(
RemoteUser:
:
getUserNo
).
orElse
(
null
));
model
.
setExtra
(
LoginHelper
.
YS_USER_ID
,
Optional
.
ofNullable
(
ru
).
map
(
RemoteUser:
:
getUserNo
).
orElse
(
null
));
model
.
setExtra
(
LoginHelper
.
OPEN_ID
,
loginBody
.
getOpenId
());
model
.
setExtra
(
LoginHelper
.
OPEN_ID
,
loginBody
.
getOpenId
());
model
.
setExtra
(
LoginHelper
.
MEMBER_ID
,
memberId
);
// 生成token
// 生成token
LoginHelper
.
login
(
loginUser
,
model
);
LoginHelper
.
login
(
loginUser
,
model
);
...
...
ruoyi-common/pom.xml
浏览文件 @
44629dc1
...
@@ -48,6 +48,9 @@
...
@@ -48,6 +48,9 @@
<module>
ruoyi-common-weixin
</module>
<module>
ruoyi-common-weixin
</module>
<module>
ruoyi-common-ys
</module>
<module>
ruoyi-common-ys
</module>
<module>
ruoyi-common-pay
</module>
<module>
ruoyi-common-pay
</module>
<module>
ruoyi-common-allPay
</module>
<module>
ruoyi-common-mall
</module>
<module>
ruoyi-common-yudao-mybatis
</module>
</modules>
</modules>
<artifactId>
ruoyi-common
</artifactId>
<artifactId>
ruoyi-common
</artifactId>
...
...
ruoyi-common/ruoyi-common-allPay/pom.xml
0 → 100644
浏览文件 @
44629dc1
<?xml version="1.0" encoding="UTF-8"?>
<project
xmlns=
"http://maven.apache.org/POM/4.0.0"
xmlns:xsi=
"http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=
"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
>
<parent>
<groupId>
org.dromara
</groupId>
<artifactId>
ruoyi-common
</artifactId>
<version>
${revision}
</version>
</parent>
<modelVersion>
4.0.0
</modelVersion>
<artifactId>
ruoyi-common-allPay
</artifactId>
<description>
ruoyi-common-allPay 支付模块
支付拓展,接入国内多个支付渠道
1. 支付宝,基于官方 SDK 接入
2. 微信支付,基于 weixin-java-pay 接入
</description>
<dependencies>
<dependency>
<groupId>
org.dromara
</groupId>
<artifactId>
ruoyi-common-core
</artifactId>
</dependency>
<dependency>
<groupId>
org.dromara
</groupId>
<artifactId>
ruoyi-common-mall
</artifactId>
</dependency>
<dependency>
<groupId>
org.slf4j
</groupId>
<artifactId>
slf4j-api
</artifactId>
</dependency>
<dependency>
<groupId>
cn.hutool
</groupId>
<artifactId>
hutool-all
</artifactId>
<version>
5.8.35
</version>
</dependency>
<dependency>
<groupId>
javax.servlet
</groupId>
<artifactId>
servlet-api
</artifactId>
<version>
2.5
</version>
<scope>
provided
</scope>
</dependency>
<dependency>
<groupId>
com.fasterxml.jackson.core
</groupId>
<artifactId>
jackson-databind
</artifactId>
</dependency>
<dependency>
<groupId>
com.fasterxml.jackson.core
</groupId>
<artifactId>
jackson-core
</artifactId>
</dependency>
<!-- 三方云服务相关 -->
<dependency>
<groupId>
com.alipay.sdk
</groupId>
<artifactId>
alipay-sdk-java
</artifactId>
<version>
4.35.79.ALL
</version>
<exclusions>
<exclusion>
<groupId>
org.bouncycastle
</groupId>
<artifactId>
bcprov-jdk15on
</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>
com.github.binarywang
</groupId>
<artifactId>
weixin-java-pay
</artifactId>
<version>
4.6.0
</version>
</dependency>
</dependencies>
</project>
ruoyi-common/ruoyi-common-allPay/src/main/java/com/github/binarywang/wxpay/bean/transfer/TransferBatchesRequest.java
0 → 100644
浏览文件 @
44629dc1
package
com
.
github
.
binarywang
.
wxpay
.
bean
.
transfer
;
import
com.github.binarywang.wxpay.v3.SpecEncrypt
;
import
com.google.gson.annotations.SerializedName
;
import
lombok.AllArgsConstructor
;
import
lombok.Builder
;
import
lombok.Data
;
import
lombok.NoArgsConstructor
;
import
lombok.experimental.Accessors
;
import
java.io.Serializable
;
import
java.util.List
;
/**
* 发起商家转账API参数
*
* @author zhongjun
* created on 2022/6/17
**/
@Data
@Accessors
(
chain
=
true
)
@Builder
(
builderMethodName
=
"newBuilder"
)
@NoArgsConstructor
@AllArgsConstructor
public
class
TransferBatchesRequest
implements
Serializable
{
private
static
final
long
serialVersionUID
=
-
2175582517588397426L
;
/**
* 直连商户的appid
*/
@SerializedName
(
"appid"
)
private
String
appid
;
/**
* 商家批次单号
*/
@SerializedName
(
"out_batch_no"
)
private
String
outBatchNo
;
/**
* 批次名称
*/
@SerializedName
(
"batch_name"
)
private
String
batchName
;
/**
* 批次备注
*/
@SerializedName
(
"batch_remark"
)
private
String
batchRemark
;
/**
* 转账总金额
*/
@SerializedName
(
"total_amount"
)
private
Integer
totalAmount
;
/**
* 转账总笔数
*/
@SerializedName
(
"total_num"
)
private
Integer
totalNum
;
/**
* 转账明细列表
*/
@SpecEncrypt
@SerializedName
(
"transfer_detail_list"
)
private
List
<
TransferDetail
>
transferDetailList
;
/**
* 转账场景ID
*/
@SerializedName
(
"transfer_scene_id"
)
private
String
transferSceneId
;
/**
* 通知地址 说明:异步接收微信支付结果通知的回调地址,通知url必须为公网可访问的url,必须为https,不能携带参数。
*/
@SerializedName
(
"notify_url"
)
private
String
notifyUrl
;
@Data
@Builder
(
builderMethodName
=
"newBuilder"
)
@AllArgsConstructor
@NoArgsConstructor
public
static
class
TransferDetail
{
/**
* 商家明细单号
*/
@SerializedName
(
"out_detail_no"
)
private
String
outDetailNo
;
/**
* 转账金额
*/
@SerializedName
(
"transfer_amount"
)
private
Integer
transferAmount
;
/**
* 转账备注
*/
@SerializedName
(
"transfer_remark"
)
private
String
transferRemark
;
/**
* 用户在直连商户应用下的用户标示
*/
@SerializedName
(
"openid"
)
private
String
openid
;
/**
* 收款用户姓名
*/
@SpecEncrypt
@SerializedName
(
"user_name"
)
private
String
userName
;
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/config/YudaoPayAutoConfiguration.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
config
;
import
org.dromara.common.pay.core.client.PayClientFactory
;
import
org.dromara.common.pay.core.client.impl.PayClientFactoryImpl
;
import
org.springframework.boot.autoconfigure.AutoConfiguration
;
import
org.springframework.context.annotation.Bean
;
/**
* 支付配置类
*
* @author 芋道源码
*/
@AutoConfiguration
public
class
YudaoPayAutoConfiguration
{
@Bean
public
PayClientFactory
payClientFactory
()
{
return
new
PayClientFactoryImpl
();
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/PayClient.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO
;
import
org.dromara.common.pay.core.client.dto.refund.PayRefundRespDTO
;
import
org.dromara.common.pay.core.client.dto.refund.PayRefundUnifiedReqDTO
;
import
org.dromara.common.pay.core.client.dto.transfer.PayTransferRespDTO
;
import
org.dromara.common.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO
;
import
org.dromara.common.pay.core.enums.transfer.PayTransferTypeEnum
;
import
java.util.Map
;
/**
* 支付客户端,用于对接各支付渠道的 SDK,实现发起支付、退款等功能
*
* @author 芋道源码
*/
public
interface
PayClient
{
/**
* 获得渠道编号
*
* @return 渠道编号
*/
Long
getId
();
// ============ 支付相关 ==========
/**
* 调用支付渠道,统一下单
*
* @param reqDTO 下单信息
* @return 支付订单信息
*/
PayOrderRespDTO
unifiedOrder
(
PayOrderUnifiedReqDTO
reqDTO
);
/**
* 解析 order 回调数据
*
* @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
* @param body HTTP 回调接口的 request body
* @return 支付订单信息
*/
PayOrderRespDTO
parseOrderNotify
(
Map
<
String
,
String
>
params
,
String
body
);
/**
* 获得支付订单信息
*
* @param outTradeNo 外部订单号
* @return 支付订单信息
*/
PayOrderRespDTO
getOrder
(
String
outTradeNo
);
// ============ 退款相关 ==========
/**
* 调用支付渠道,进行退款
*
* @param reqDTO 统一退款请求信息
* @return 退款信息
*/
PayRefundRespDTO
unifiedRefund
(
PayRefundUnifiedReqDTO
reqDTO
);
/**
* 解析 refund 回调数据
*
* @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
* @param body HTTP 回调接口的 request body
* @return 支付订单信息
*/
PayRefundRespDTO
parseRefundNotify
(
Map
<
String
,
String
>
params
,
String
body
);
/**
* 获得退款订单信息
*
* @param outTradeNo 外部订单号
* @param outRefundNo 外部退款号
* @return 退款订单信息
*/
PayRefundRespDTO
getRefund
(
String
outTradeNo
,
String
outRefundNo
);
// ============ 转账相关 ==========
/**
* 调用渠道,进行转账
*
* @param reqDTO 统一转账请求信息
* @return 转账信息
*/
PayTransferRespDTO
unifiedTransfer
(
PayTransferUnifiedReqDTO
reqDTO
);
/**
* 获得转账订单信息
*
* @param outTradeNo 外部订单号
* @param type 转账类型
* @return 转账信息
*/
PayTransferRespDTO
getTransfer
(
String
outTradeNo
,
PayTransferTypeEnum
type
);
/**
* 解析 transfer 回调数据
*
* @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
* @param body HTTP 回调接口的 request body
* @return 转账信息
*/
PayTransferRespDTO
parseTransferNotify
(
Map
<
String
,
String
>
params
,
String
body
);
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/PayClientConfig.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
;
import
com.fasterxml.jackson.annotation.JsonTypeInfo
;
import
jakarta.validation.Validator
;
/**
* 支付客户端的配置,本质是支付渠道的配置
* 每个不同的渠道,需要不同的配置,通过子类来定义
*
* @author 芋道源码
*/
@JsonTypeInfo
(
use
=
JsonTypeInfo
.
Id
.
CLASS
)
// @JsonTypeInfo 注解的作用,Jackson 多态
// 1. 序列化到时数据库时,增加 @class 属性。
// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型
public
interface
PayClientConfig
{
/**
* 参数校验
*
* @param validator 校验对象
*/
void
validate
(
Validator
validator
);
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/PayClientFactory.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
;
import
org.dromara.common.pay.core.enums.channel.PayChannelEnum
;
/**
* 支付客户端的工厂接口
*
* @author 芋道源码
*/
public
interface
PayClientFactory
{
/**
* 获得支付客户端
*
* @param channelId 渠道编号
* @return 支付客户端
*/
PayClient
getPayClient
(
Long
channelId
);
/**
* 创建支付客户端
*
* @param channelId 渠道编号
* @param channelCode 渠道编码
* @param config 支付配置
* @return 支付客户端
*/
<
Config
extends
PayClientConfig
>
PayClient
createOrUpdatePayClient
(
Long
channelId
,
String
channelCode
,
Config
config
);
/**
* 注册支付客户端 Class,用于模块中实现的 PayClient
*
* @param channel 支付渠道的编码的枚举
* @param payClientClass 支付客户端 class
*/
void
registerPayClientClass
(
PayChannelEnum
channel
,
Class
<?>
payClientClass
);
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/dto/order/PayOrderRespDTO.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
dto
.
order
;
import
lombok.Data
;
import
lombok.experimental.Accessors
;
import
org.dromara.common.pay.core.enums.order.PayOrderStatusRespEnum
;
import
java.time.LocalDateTime
;
/**
* 渠道支付订单 Response DTO
*
* @author 芋道源码
*/
@Data
@Accessors
(
chain
=
true
)
public
class
PayOrderRespDTO
{
/**
* 支付状态
*
* 枚举:{@link org.dromara.common.pay.core.enums.order.PayOrderStatusRespEnum}
*/
private
Integer
status
;
/**
* 外部订单号
*
* 对应 PayOrderExtensionDO 的 no 字段
*/
private
String
outTradeNo
;
/**
* 支付渠道编号
*/
private
String
channelOrderNo
;
/**
* 支付渠道用户编号
*/
private
String
channelUserId
;
/**
* 支付成功时间
*/
private
LocalDateTime
successTime
;
/**
* 原始的同步/异步通知结果
*/
private
Object
rawData
;
// ========== 主动发起支付时,会返回的字段 ==========
/**
* 展示模式
*
* 枚举 {@link org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum} 类
*/
private
String
displayMode
;
/**
* 展示内容
*/
private
String
displayContent
;
/**
* 调用渠道的错误码
*
* 注意:这里返回的是业务异常,而是不系统异常。
* 如果是系统异常,则会抛出 {@link org.dromara.common.pay.core.client.exception.PayException}
*/
private
String
channelErrorCode
;
/**
* 调用渠道报错时,错误信息
*/
private
String
channelErrorMsg
;
public
PayOrderRespDTO
()
{
}
/**
* 创建【WAITING】状态的订单返回
*/
public
static
PayOrderRespDTO
waitingOf
(
String
displayMode
,
String
displayContent
,
String
outTradeNo
,
Object
rawData
)
{
PayOrderRespDTO
respDTO
=
new
PayOrderRespDTO
();
respDTO
.
status
=
PayOrderStatusRespEnum
.
WAITING
.
getStatus
();
respDTO
.
displayMode
=
displayMode
;
respDTO
.
displayContent
=
displayContent
;
// 相对通用的字段
respDTO
.
outTradeNo
=
outTradeNo
;
respDTO
.
rawData
=
rawData
;
return
respDTO
;
}
/**
* 创建【SUCCESS】状态的订单返回
*/
public
static
PayOrderRespDTO
successOf
(
String
channelOrderNo
,
String
channelUserId
,
LocalDateTime
successTime
,
String
outTradeNo
,
Object
rawData
)
{
PayOrderRespDTO
respDTO
=
new
PayOrderRespDTO
();
respDTO
.
status
=
PayOrderStatusRespEnum
.
SUCCESS
.
getStatus
();
respDTO
.
channelOrderNo
=
channelOrderNo
;
respDTO
.
channelUserId
=
channelUserId
;
respDTO
.
successTime
=
successTime
;
// 相对通用的字段
respDTO
.
outTradeNo
=
outTradeNo
;
respDTO
.
rawData
=
rawData
;
return
respDTO
;
}
/**
* 创建指定状态的订单返回,适合支付渠道回调时
*/
public
static
PayOrderRespDTO
of
(
Integer
status
,
String
channelOrderNo
,
String
channelUserId
,
LocalDateTime
successTime
,
String
outTradeNo
,
Object
rawData
)
{
PayOrderRespDTO
respDTO
=
new
PayOrderRespDTO
();
respDTO
.
status
=
status
;
respDTO
.
channelOrderNo
=
channelOrderNo
;
respDTO
.
channelUserId
=
channelUserId
;
respDTO
.
successTime
=
successTime
;
// 相对通用的字段
respDTO
.
outTradeNo
=
outTradeNo
;
respDTO
.
rawData
=
rawData
;
return
respDTO
;
}
/**
* 创建【CLOSED】状态的订单返回,适合调用支付渠道失败时
*/
public
static
PayOrderRespDTO
closedOf
(
String
channelErrorCode
,
String
channelErrorMsg
,
String
outTradeNo
,
Object
rawData
)
{
PayOrderRespDTO
respDTO
=
new
PayOrderRespDTO
();
respDTO
.
status
=
PayOrderStatusRespEnum
.
CLOSED
.
getStatus
();
respDTO
.
channelErrorCode
=
channelErrorCode
;
respDTO
.
channelErrorMsg
=
channelErrorMsg
;
// 相对通用的字段
respDTO
.
outTradeNo
=
outTradeNo
;
respDTO
.
rawData
=
rawData
;
return
respDTO
;
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
dto
.
order
;
import
jakarta.validation.constraints.DecimalMin
;
import
jakarta.validation.constraints.NotEmpty
;
import
jakarta.validation.constraints.NotNull
;
import
lombok.Data
;
import
lombok.experimental.Accessors
;
import
org.hibernate.validator.constraints.Length
;
import
org.hibernate.validator.constraints.URL
;
import
java.time.LocalDateTime
;
import
java.util.Map
;
/**
* 统一下单 Request DTO
*
* @author 芋道源码
*/
@Data
@Accessors
(
chain
=
true
)
public
class
PayOrderUnifiedReqDTO
{
/**
* 用户 IP
*/
@NotEmpty
(
message
=
"用户 IP 不能为空"
)
private
String
userIp
;
// ========== 商户相关字段 ==========
/**
* 外部订单号
*
* 对应 PayOrderExtensionDO 的 no 字段
*/
@NotEmpty
(
message
=
"外部订单编号不能为空"
)
private
String
outTradeNo
;
/**
* 商品标题
*/
@NotEmpty
(
message
=
"商品标题不能为空"
)
@Length
(
max
=
32
,
message
=
"商品标题不能超过 32"
)
private
String
subject
;
/**
* 商品描述信息
*/
@Length
(
max
=
128
,
message
=
"商品描述信息长度不能超过128"
)
private
String
body
;
/**
* 支付结果的 notify 回调地址
*/
@NotEmpty
(
message
=
"支付结果的回调地址不能为空"
)
@URL
(
message
=
"支付结果的 notify 回调地址必须是 URL 格式"
)
private
String
notifyUrl
;
/**
* 支付结果的 return 回调地址
*/
@URL
(
message
=
"支付结果的 return 回调地址必须是 URL 格式"
)
private
String
returnUrl
;
// ========== 订单相关字段 ==========
/**
* 支付金额,单位:分
*/
@NotNull
(
message
=
"支付金额不能为空"
)
@DecimalMin
(
value
=
"0"
,
inclusive
=
false
,
message
=
"支付金额必须大于零"
)
private
Integer
price
;
/**
* 支付过期时间
*/
@NotNull
(
message
=
"支付过期时间不能为空"
)
private
LocalDateTime
expireTime
;
// ========== 拓展参数 ==========
/**
* 支付渠道的额外参数
*
* 例如说,微信公众号需要传递 openid 参数
*/
private
Map
<
String
,
String
>
channelExtras
;
/**
* 展示模式
*
* 如果不传递,则每个支付渠道使用默认的方式
*
* 枚举 {@link org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum}
*/
private
String
displayMode
;
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/dto/refund/PayRefundRespDTO.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
dto
.
refund
;
import
lombok.AllArgsConstructor
;
import
lombok.Data
;
import
org.dromara.common.pay.core.enums.refund.PayRefundStatusRespEnum
;
import
java.time.LocalDateTime
;
/**
* 渠道退款订单 Response DTO
*
* @author jason
*/
@Data
@AllArgsConstructor
public
class
PayRefundRespDTO
{
/**
* 退款状态
*
* 枚举 {@link PayRefundStatusRespEnum}
*/
private
Integer
status
;
/**
* 外部退款号
*
* 对应 PayRefundDO 的 no 字段
*/
private
String
outRefundNo
;
/**
* 渠道退款单号
*
* 对应 PayRefundDO.channelRefundNo 字段
*/
private
String
channelRefundNo
;
/**
* 退款成功时间
*/
private
LocalDateTime
successTime
;
/**
* 原始的异步通知结果
*/
private
Object
rawData
;
/**
* 调用渠道的错误码
*
* 注意:这里返回的是业务异常,而是不系统异常。
* 如果是系统异常,则会抛出 {@link org.dromara.common.pay.core.client.exception.PayException}
*/
private
String
channelErrorCode
;
/**
* 调用渠道报错时,错误信息
*/
private
String
channelErrorMsg
;
private
PayRefundRespDTO
()
{
}
/**
* 创建【WAITING】状态的退款返回
*/
public
static
PayRefundRespDTO
waitingOf
(
String
channelRefundNo
,
String
outRefundNo
,
Object
rawData
)
{
PayRefundRespDTO
respDTO
=
new
PayRefundRespDTO
();
respDTO
.
status
=
PayRefundStatusRespEnum
.
WAITING
.
getStatus
();
respDTO
.
channelRefundNo
=
channelRefundNo
;
// 相对通用的字段
respDTO
.
outRefundNo
=
outRefundNo
;
respDTO
.
rawData
=
rawData
;
return
respDTO
;
}
/**
* 创建【SUCCESS】状态的退款返回
*/
public
static
PayRefundRespDTO
successOf
(
String
channelRefundNo
,
LocalDateTime
successTime
,
String
outRefundNo
,
Object
rawData
)
{
PayRefundRespDTO
respDTO
=
new
PayRefundRespDTO
();
respDTO
.
status
=
PayRefundStatusRespEnum
.
SUCCESS
.
getStatus
();
respDTO
.
channelRefundNo
=
channelRefundNo
;
respDTO
.
successTime
=
successTime
;
// 相对通用的字段
respDTO
.
outRefundNo
=
outRefundNo
;
respDTO
.
rawData
=
rawData
;
return
respDTO
;
}
/**
* 创建【FAILURE】状态的退款返回
*/
public
static
PayRefundRespDTO
failureOf
(
String
outRefundNo
,
Object
rawData
)
{
return
failureOf
(
null
,
null
,
outRefundNo
,
rawData
);
}
/**
* 创建【FAILURE】状态的退款返回
*/
public
static
PayRefundRespDTO
failureOf
(
String
channelErrorCode
,
String
channelErrorMsg
,
String
outRefundNo
,
Object
rawData
)
{
PayRefundRespDTO
respDTO
=
new
PayRefundRespDTO
();
respDTO
.
status
=
PayRefundStatusRespEnum
.
FAILURE
.
getStatus
();
respDTO
.
channelErrorCode
=
channelErrorCode
;
respDTO
.
channelErrorMsg
=
channelErrorMsg
;
// 相对通用的字段
respDTO
.
outRefundNo
=
outRefundNo
;
respDTO
.
rawData
=
rawData
;
return
respDTO
;
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
dto
.
refund
;
import
jakarta.validation.constraints.DecimalMin
;
import
jakarta.validation.constraints.NotEmpty
;
import
jakarta.validation.constraints.NotNull
;
import
lombok.AllArgsConstructor
;
import
lombok.Builder
;
import
lombok.Data
;
import
lombok.NoArgsConstructor
;
import
lombok.experimental.Accessors
;
import
org.hibernate.validator.constraints.URL
;
/**
* 统一 退款 Request DTO
*
* @author jason
*/
@Accessors
(
chain
=
true
)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public
class
PayRefundUnifiedReqDTO
{
/**
* 外部订单号
*
* 对应 PayOrderExtensionDO 的 no 字段
*/
@NotEmpty
(
message
=
"外部订单编号不能为空"
)
private
String
outTradeNo
;
/**
* 外部退款号
*
* 对应 PayRefundDO 的 no 字段
*/
@NotEmpty
(
message
=
"退款请求单号不能为空"
)
private
String
outRefundNo
;
/**
* 退款原因
*/
@NotEmpty
(
message
=
"退款原因不能为空"
)
private
String
reason
;
/**
* 支付金额,单位:分
*
* 目前微信支付在退款的时候,必须传递该字段
*/
@NotNull
(
message
=
"支付金额不能为空"
)
@DecimalMin
(
value
=
"0"
,
inclusive
=
false
,
message
=
"支付金额必须大于零"
)
private
Integer
payPrice
;
/**
* 退款金额,单位:分
*/
@NotNull
(
message
=
"退款金额不能为空"
)
@DecimalMin
(
value
=
"0"
,
inclusive
=
false
,
message
=
"支付金额必须大于零"
)
private
Integer
refundPrice
;
/**
* 退款结果的 notify 回调地址
*/
@NotEmpty
(
message
=
"支付结果的回调地址不能为空"
)
@URL
(
message
=
"支付结果的 notify 回调地址必须是 URL 格式"
)
private
String
notifyUrl
;
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/dto/transfer/PayTransferRespDTO.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
dto
.
transfer
;
import
lombok.Data
;
import
org.dromara.common.pay.core.enums.transfer.PayTransferStatusRespEnum
;
import
java.time.LocalDateTime
;
/**
* 统一转账 Response DTO
*
* @author jason
*/
@Data
public
class
PayTransferRespDTO
{
/**
* 转账状态
*
* 关联 {@link PayTransferStatusRespEnum#getStatus()}
*/
private
Integer
status
;
/**
* 外部转账单号
*
*/
private
String
outTransferNo
;
/**
* 支付渠道编号
*/
private
String
channelTransferNo
;
/**
* 支付成功时间
*/
private
LocalDateTime
successTime
;
/**
* 原始的返回结果
*/
private
Object
rawData
;
/**
* 调用渠道的错误码
*/
private
String
channelErrorCode
;
/**
* 调用渠道报错时,错误信息
*/
private
String
channelErrorMsg
;
/**
* 创建【WAITING】状态的转账返回
*/
public
static
PayTransferRespDTO
waitingOf
(
String
channelTransferNo
,
String
outTransferNo
,
Object
rawData
)
{
PayTransferRespDTO
respDTO
=
new
PayTransferRespDTO
();
respDTO
.
status
=
PayTransferStatusRespEnum
.
WAITING
.
getStatus
();
respDTO
.
channelTransferNo
=
channelTransferNo
;
respDTO
.
outTransferNo
=
outTransferNo
;
respDTO
.
rawData
=
rawData
;
return
respDTO
;
}
/**
* 创建【IN_PROGRESS】状态的转账返回
*/
public
static
PayTransferRespDTO
dealingOf
(
String
channelTransferNo
,
String
outTransferNo
,
Object
rawData
)
{
PayTransferRespDTO
respDTO
=
new
PayTransferRespDTO
();
respDTO
.
status
=
PayTransferStatusRespEnum
.
IN_PROGRESS
.
getStatus
();
respDTO
.
channelTransferNo
=
channelTransferNo
;
respDTO
.
outTransferNo
=
outTransferNo
;
respDTO
.
rawData
=
rawData
;
return
respDTO
;
}
/**
* 创建【CLOSED】状态的转账返回
*/
public
static
PayTransferRespDTO
closedOf
(
String
channelErrorCode
,
String
channelErrorMsg
,
String
outTransferNo
,
Object
rawData
)
{
PayTransferRespDTO
respDTO
=
new
PayTransferRespDTO
();
respDTO
.
status
=
PayTransferStatusRespEnum
.
CLOSED
.
getStatus
();
respDTO
.
channelErrorCode
=
channelErrorCode
;
respDTO
.
channelErrorMsg
=
channelErrorMsg
;
// 相对通用的字段
respDTO
.
outTransferNo
=
outTransferNo
;
respDTO
.
rawData
=
rawData
;
return
respDTO
;
}
/**
* 创建【SUCCESS】状态的转账返回
*/
public
static
PayTransferRespDTO
successOf
(
String
channelTransferNo
,
LocalDateTime
successTime
,
String
outTransferNo
,
Object
rawData
)
{
PayTransferRespDTO
respDTO
=
new
PayTransferRespDTO
();
respDTO
.
status
=
PayTransferStatusRespEnum
.
SUCCESS
.
getStatus
();
respDTO
.
channelTransferNo
=
channelTransferNo
;
respDTO
.
successTime
=
successTime
;
// 相对通用的字段
respDTO
.
outTransferNo
=
outTransferNo
;
respDTO
.
rawData
=
rawData
;
return
respDTO
;
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
dto
.
transfer
;
import
jakarta.validation.constraints.Min
;
import
jakarta.validation.constraints.NotBlank
;
import
jakarta.validation.constraints.NotEmpty
;
import
jakarta.validation.constraints.NotNull
;
import
lombok.Data
;
import
lombok.experimental.Accessors
;
import
org.dromara.common.mall.validation.InEnum
;
import
org.dromara.common.pay.core.enums.transfer.PayTransferTypeEnum
;
import
org.hibernate.validator.constraints.Length
;
import
org.hibernate.validator.constraints.URL
;
import
java.util.Map
;
/**
* 统一转账 Request DTO
*
* @author jason
*/
@Data
@Accessors
(
chain
=
true
)
public
class
PayTransferUnifiedReqDTO
{
/**
* 转账类型
*
* 关联 {@link PayTransferTypeEnum#getType()}
*/
@NotNull
(
message
=
"转账类型不能为空"
)
@InEnum
(
PayTransferTypeEnum
.
class
)
private
Integer
type
;
/**
* 用户 IP
*/
@NotEmpty
(
message
=
"用户 IP 不能为空"
)
private
String
userIp
;
@NotEmpty
(
message
=
"外部转账单编号不能为空"
)
private
String
outTransferNo
;
/**
* 转账金额,单位:分
*/
@NotNull
(
message
=
"转账金额不能为空"
)
@Min
(
value
=
1
,
message
=
"转账金额必须大于零"
)
private
Integer
price
;
/**
* 转账标题
*/
@NotEmpty
(
message
=
"转账标题不能为空"
)
@Length
(
max
=
128
,
message
=
"转账标题不能超过 128"
)
private
String
subject
;
/**
* 收款人姓名
*/
@NotBlank
(
message
=
"收款人姓名不能为空"
,
groups
=
{
PayTransferTypeEnum
.
Alipay
.
class
})
private
String
userName
;
/**
* 支付宝登录号
*/
@NotBlank
(
message
=
"支付宝登录号不能为空"
,
groups
=
{
PayTransferTypeEnum
.
Alipay
.
class
})
private
String
alipayLogonId
;
/**
* 微信 openId
*/
@NotBlank
(
message
=
"微信 openId 不能为空"
,
groups
=
{
PayTransferTypeEnum
.
WxPay
.
class
})
private
String
openid
;
/**
* 支付渠道的额外参数
*/
private
Map
<
String
,
String
>
channelExtras
;
/**
* 转账结果的 notify 回调地址
*/
@NotEmpty
(
message
=
"转账结果的回调地址不能为空"
)
@URL
(
message
=
"转账结果的 notify 回调地址必须是 URL 格式"
)
private
String
notifyUrl
;
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/dto/transfer/WxPayTransferPartnerNotifyV3Result.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
dto
.
transfer
;
import
com.github.binarywang.wxpay.bean.notify.OriginNotifyResponse
;
import
com.github.binarywang.wxpay.bean.notify.WxPayBaseNotifyV3Result
;
import
com.google.gson.annotations.SerializedName
;
import
lombok.Data
;
import
lombok.NoArgsConstructor
;
import
java.io.Serializable
;
// TODO @luchi:这个可以复用 wxjava 里的类么?
@NoArgsConstructor
public
class
WxPayTransferPartnerNotifyV3Result
implements
Serializable
,
WxPayBaseNotifyV3Result
<
WxPayTransferPartnerNotifyV3Result
.
TransferNotifyResult
>
{
private
static
final
long
serialVersionUID
=
-
1L
;
/**
* 源数据
*/
private
OriginNotifyResponse
rawData
;
/**
* 解密后的数据
*/
private
TransferNotifyResult
result
;
@Override
public
void
setRawData
(
OriginNotifyResponse
rawData
)
{
this
.
rawData
=
rawData
;
}
@Override
public
void
setResult
(
TransferNotifyResult
data
)
{
this
.
result
=
data
;
}
public
TransferNotifyResult
getResult
()
{
return
result
;
}
public
OriginNotifyResponse
getRawData
()
{
return
rawData
;
}
@Data
@NoArgsConstructor
public
static
class
TransferNotifyResult
implements
Serializable
{
private
static
final
long
serialVersionUID
=
1L
;
/*********************** 公共字段 ********************
/**
* 商家批次单号
*/
@SerializedName
(
value
=
"out_batch_no"
)
protected
String
outBatchNo
;
/**
* 微信批次单号
*/
@SerializedName
(
value
=
"batch_id"
)
protected
String
batchId
;
/**
* 批次状态
*/
@SerializedName
(
value
=
"batch_status"
)
protected
String
batchStatus
;
/**
* 批次总笔数
*/
@SerializedName
(
value
=
"total_num"
)
protected
Integer
totalNum
;
/**
* 批次总金额
*/
@SerializedName
(
value
=
"total_amount"
)
protected
Integer
totalAmount
;
/**
* 批次更新时间
*/
@SerializedName
(
value
=
"update_time"
)
private
String
updateTime
;
/*********************** FINISHED ********************
/**
* 转账成功金额
*/
@SerializedName
(
value
=
"success_amount"
)
protected
Integer
successAmount
;
/**
* 转账成功笔数
*/
@SerializedName
(
value
=
"success_num"
)
protected
Integer
successNum
;
/**
* 转账失败金额
*/
@SerializedName
(
value
=
"fail_amount"
)
protected
Integer
failAmount
;
/**
* 转账失败笔数
*/
@SerializedName
(
value
=
"fail_num"
)
protected
Integer
failNum
;
/*********************** CLOSED ********************
/**
* 商户号
*/
@SerializedName
(
value
=
"mchid"
)
protected
String
mchId
;
/**
* 批次关闭原因
*/
@SerializedName
(
value
=
"close_reason"
)
protected
String
closeReason
;
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/exception/PayException.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
exception
;
import
lombok.Data
;
import
lombok.EqualsAndHashCode
;
/**
* 支付系统异常 Exception
*/
@Data
@EqualsAndHashCode
(
callSuper
=
true
)
public
class
PayException
extends
RuntimeException
{
public
PayException
(
Throwable
cause
)
{
super
(
cause
);
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/impl/AbstractPayClient.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
;
import
lombok.extern.slf4j.Slf4j
;
import
org.dromara.common.core.exception.ServiceException
;
import
org.dromara.common.mall.util.validation.ValidationUtils
;
import
org.dromara.common.pay.core.client.PayClient
;
import
org.dromara.common.pay.core.client.PayClientConfig
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO
;
import
org.dromara.common.pay.core.client.dto.refund.PayRefundRespDTO
;
import
org.dromara.common.pay.core.client.dto.refund.PayRefundUnifiedReqDTO
;
import
org.dromara.common.pay.core.client.dto.transfer.PayTransferRespDTO
;
import
org.dromara.common.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO
;
import
org.dromara.common.pay.core.client.exception.PayException
;
import
org.dromara.common.pay.core.enums.transfer.PayTransferTypeEnum
;
import
java.util.Map
;
import
static
org
.
dromara
.
common
.
core
.
utils
.
json
.
JsonUtils
.
toJsonString
;
import
static
org
.
dromara
.
common
.
mall
.
exception
.
enums
.
GlobalErrorCodeConstants
.
NOT_IMPLEMENTED
;
import
static
org
.
dromara
.
common
.
mall
.
exception
.
util
.
ServiceExceptionUtil
.
exception
;
/**
* 支付客户端的抽象类,提供模板方法,减少子类的冗余代码
*
* @author 芋道源码
*/
@Slf4j
public
abstract
class
AbstractPayClient
<
Config
extends
PayClientConfig
>
implements
PayClient
{
/**
* 渠道编号
*/
private
final
Long
channelId
;
/**
* 渠道编码
*/
@SuppressWarnings
(
"FieldCanBeLocal"
)
private
final
String
channelCode
;
/**
* 支付配置
*/
protected
Config
config
;
public
AbstractPayClient
(
Long
channelId
,
String
channelCode
,
Config
config
)
{
this
.
channelId
=
channelId
;
this
.
channelCode
=
channelCode
;
this
.
config
=
config
;
}
/**
* 初始化
*/
public
final
void
init
()
{
doInit
();
log
.
debug
(
"[init][客户端({}) 初始化完成]"
,
getId
());
}
/**
* 自定义初始化
*/
protected
abstract
void
doInit
();
public
final
void
refresh
(
Config
config
)
{
// 判断是否更新
if
(
config
.
equals
(
this
.
config
))
{
return
;
}
log
.
info
(
"[refresh][客户端({})发生变化,重新初始化]"
,
getId
());
this
.
config
=
config
;
// 初始化
this
.
init
();
}
@Override
public
Long
getId
()
{
return
channelId
;
}
// ============ 支付相关 ==========
@Override
public
final
PayOrderRespDTO
unifiedOrder
(
PayOrderUnifiedReqDTO
reqDTO
)
{
ValidationUtils
.
validate
(
reqDTO
);
// 执行统一下单
PayOrderRespDTO
resp
;
try
{
resp
=
doUnifiedOrder
(
reqDTO
);
}
catch
(
ServiceException
ex
)
{
// 业务异常,都是实现类已经翻译,所以直接抛出即可
throw
ex
;
}
catch
(
Throwable
ex
)
{
// 系统异常,则包装成 PayException 异常抛出
log
.
error
(
"[unifiedOrder][客户端({}) request({}) 发起支付异常]"
,
getId
(),
toJsonString
(
reqDTO
),
ex
);
throw
buildPayException
(
ex
);
}
return
resp
;
}
protected
abstract
PayOrderRespDTO
doUnifiedOrder
(
PayOrderUnifiedReqDTO
reqDTO
)
throws
Throwable
;
@Override
public
final
PayOrderRespDTO
parseOrderNotify
(
Map
<
String
,
String
>
params
,
String
body
)
{
try
{
return
doParseOrderNotify
(
params
,
body
);
}
catch
(
ServiceException
ex
)
{
// 业务异常,都是实现类已经翻译,所以直接抛出即可
throw
ex
;
}
catch
(
Throwable
ex
)
{
log
.
error
(
"[parseOrderNotify][客户端({}) params({}) body({}) 解析失败]"
,
getId
(),
params
,
body
,
ex
);
throw
buildPayException
(
ex
);
}
}
protected
abstract
PayOrderRespDTO
doParseOrderNotify
(
Map
<
String
,
String
>
params
,
String
body
)
throws
Throwable
;
@Override
public
final
PayOrderRespDTO
getOrder
(
String
outTradeNo
)
{
try
{
return
doGetOrder
(
outTradeNo
);
}
catch
(
ServiceException
ex
)
{
// 业务异常,都是实现类已经翻译,所以直接抛出即可
throw
ex
;
}
catch
(
Throwable
ex
)
{
log
.
error
(
"[getOrder][客户端({}) outTradeNo({}) 查询支付单异常]"
,
getId
(),
outTradeNo
,
ex
);
throw
buildPayException
(
ex
);
}
}
protected
abstract
PayOrderRespDTO
doGetOrder
(
String
outTradeNo
)
throws
Throwable
;
// ============ 退款相关 ==========
@Override
public
final
PayRefundRespDTO
unifiedRefund
(
PayRefundUnifiedReqDTO
reqDTO
)
{
ValidationUtils
.
validate
(
reqDTO
);
// 执行统一退款
PayRefundRespDTO
resp
;
try
{
resp
=
doUnifiedRefund
(
reqDTO
);
}
catch
(
ServiceException
ex
)
{
// 业务异常,都是实现类已经翻译,所以直接抛出即可
throw
ex
;
}
catch
(
Throwable
ex
)
{
// 系统异常,则包装成 PayException 异常抛出
log
.
error
(
"[unifiedRefund][客户端({}) request({}) 发起退款异常]"
,
getId
(),
toJsonString
(
reqDTO
),
ex
);
throw
buildPayException
(
ex
);
}
return
resp
;
}
protected
abstract
PayRefundRespDTO
doUnifiedRefund
(
PayRefundUnifiedReqDTO
reqDTO
)
throws
Throwable
;
@Override
public
final
PayRefundRespDTO
parseRefundNotify
(
Map
<
String
,
String
>
params
,
String
body
)
{
try
{
return
doParseRefundNotify
(
params
,
body
);
}
catch
(
ServiceException
ex
)
{
// 业务异常,都是实现类已经翻译,所以直接抛出即可
throw
ex
;
}
catch
(
Throwable
ex
)
{
log
.
error
(
"[parseRefundNotify][客户端({}) params({}) body({}) 解析失败]"
,
getId
(),
params
,
body
,
ex
);
throw
buildPayException
(
ex
);
}
}
protected
abstract
PayRefundRespDTO
doParseRefundNotify
(
Map
<
String
,
String
>
params
,
String
body
)
throws
Throwable
;
@Override
public
final
PayRefundRespDTO
getRefund
(
String
outTradeNo
,
String
outRefundNo
)
{
try
{
return
doGetRefund
(
outTradeNo
,
outRefundNo
);
}
catch
(
ServiceException
ex
)
{
// 业务异常,都是实现类已经翻译,所以直接抛出即可
throw
ex
;
}
catch
(
Throwable
ex
)
{
log
.
error
(
"[getRefund][客户端({}) outTradeNo({}) outRefundNo({}) 查询退款单异常]"
,
getId
(),
outTradeNo
,
outRefundNo
,
ex
);
throw
buildPayException
(
ex
);
}
}
protected
abstract
PayRefundRespDTO
doGetRefund
(
String
outTradeNo
,
String
outRefundNo
)
throws
Throwable
;
@Override
public
final
PayTransferRespDTO
unifiedTransfer
(
PayTransferUnifiedReqDTO
reqDTO
)
{
validatePayTransferReqDTO
(
reqDTO
);
PayTransferRespDTO
resp
;
try
{
resp
=
doUnifiedTransfer
(
reqDTO
);
}
catch
(
ServiceException
ex
)
{
// 业务异常,都是实现类已经翻译,所以直接抛出即可
throw
ex
;
}
catch
(
Throwable
ex
)
{
// 系统异常,则包装成 PayException 异常抛出
log
.
error
(
"[unifiedTransfer][客户端({}) request({}) 发起转账异常]"
,
getId
(),
toJsonString
(
reqDTO
),
ex
);
throw
buildPayException
(
ex
);
}
return
resp
;
}
private
void
validatePayTransferReqDTO
(
PayTransferUnifiedReqDTO
reqDTO
)
{
PayTransferTypeEnum
transferType
=
PayTransferTypeEnum
.
typeOf
(
reqDTO
.
getType
());
switch
(
transferType
)
{
case
ALIPAY_BALANCE:
{
ValidationUtils
.
validate
(
reqDTO
,
PayTransferTypeEnum
.
Alipay
.
class
);
break
;
}
case
WX_BALANCE:
{
ValidationUtils
.
validate
(
reqDTO
,
PayTransferTypeEnum
.
WxPay
.
class
);
break
;
}
default
:
{
throw
exception
(
NOT_IMPLEMENTED
);
}
}
}
@Override
public
final
PayTransferRespDTO
parseTransferNotify
(
Map
<
String
,
String
>
params
,
String
body
)
{
try
{
return
doParseTransferNotify
(
params
,
body
);
}
catch
(
ServiceException
ex
)
{
// 业务异常,都是实现类已经翻译,所以直接抛出即可
throw
ex
;
}
catch
(
Throwable
ex
)
{
log
.
error
(
"[doParseTransferNotify][客户端({}) params({}) body({}) 解析失败]"
,
getId
(),
params
,
body
,
ex
);
throw
buildPayException
(
ex
);
}
}
protected
abstract
PayTransferRespDTO
doParseTransferNotify
(
Map
<
String
,
String
>
params
,
String
body
)
throws
Throwable
;
@Override
public
final
PayTransferRespDTO
getTransfer
(
String
outTradeNo
,
PayTransferTypeEnum
type
)
{
try
{
return
doGetTransfer
(
outTradeNo
,
type
);
}
catch
(
ServiceException
ex
)
{
// 业务异常,都是实现类已经翻译,所以直接抛出即可
throw
ex
;
}
catch
(
Throwable
ex
)
{
log
.
error
(
"[getTransfer][客户端({}) outTradeNo({}) type({}) 查询转账单异常]"
,
getId
(),
outTradeNo
,
type
,
ex
);
throw
buildPayException
(
ex
);
}
}
protected
abstract
PayTransferRespDTO
doUnifiedTransfer
(
PayTransferUnifiedReqDTO
reqDTO
)
throws
Throwable
;
protected
abstract
PayTransferRespDTO
doGetTransfer
(
String
outTradeNo
,
PayTransferTypeEnum
type
)
throws
Throwable
;
// ========== 各种工具方法 ==========
private
PayException
buildPayException
(
Throwable
ex
)
{
if
(
ex
instanceof
PayException
)
{
return
(
PayException
)
ex
;
}
throw
new
PayException
(
ex
);
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/impl/NonePayClientConfig.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
;
import
jakarta.validation.Validator
;
import
lombok.Data
;
import
org.dromara.common.pay.core.client.PayClientConfig
;
/**
* 无需任何配置 PayClientConfig 实现类
*
* @author jason
*/
@Data
public
class
NonePayClientConfig
implements
PayClientConfig
{
/**
* 配置名称
* <p>
* 如果不加任何属性,JsonUtils.parseObject2 解析会报错,所以暂时加个名称
*/
private
String
name
;
public
NonePayClientConfig
(){
this
.
name
=
"none-config"
;
}
@Override
public
void
validate
(
Validator
validator
)
{
// 无任何配置不需要校验
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/impl/PayClientFactoryImpl.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
;
import
cn.hutool.core.lang.Assert
;
import
cn.hutool.core.util.ReflectUtil
;
import
lombok.extern.slf4j.Slf4j
;
import
org.dromara.common.pay.core.client.PayClient
;
import
org.dromara.common.pay.core.client.PayClientConfig
;
import
org.dromara.common.pay.core.client.PayClientFactory
;
import
org.dromara.common.pay.core.client.impl.alipay.*
;
import
org.dromara.common.pay.core.client.impl.mock.MockPayClient
;
import
org.dromara.common.pay.core.client.impl.weixin.*
;
import
org.dromara.common.pay.core.enums.channel.PayChannelEnum
;
import
java.util.Map
;
import
java.util.concurrent.ConcurrentHashMap
;
import
java.util.concurrent.ConcurrentMap
;
import
static
org
.
dromara
.
common
.
pay
.
core
.
enums
.
channel
.
PayChannelEnum
.*;
/**
* 支付客户端的工厂实现类
*
* @author 芋道源码
*/
@Slf4j
public
class
PayClientFactoryImpl
implements
PayClientFactory
{
/**
* 支付客户端 Map
*
* key:渠道编号
*/
private
final
ConcurrentMap
<
Long
,
AbstractPayClient
<?>>
clients
=
new
ConcurrentHashMap
<>();
/**
* 支付客户端 Class Map
*/
private
final
Map
<
PayChannelEnum
,
Class
<?>>
clientClass
=
new
ConcurrentHashMap
<>();
public
PayClientFactoryImpl
()
{
// 微信支付客户端
clientClass
.
put
(
WX_PUB
,
WxPubPayClient
.
class
);
clientClass
.
put
(
WX_LITE
,
WxLitePayClient
.
class
);
clientClass
.
put
(
WX_APP
,
WxAppPayClient
.
class
);
clientClass
.
put
(
WX_BAR
,
WxBarPayClient
.
class
);
clientClass
.
put
(
WX_NATIVE
,
WxNativePayClient
.
class
);
clientClass
.
put
(
WX_WAP
,
WxWapPayClient
.
class
);
// 支付包支付客户端
clientClass
.
put
(
ALIPAY_WAP
,
AlipayWapPayClient
.
class
);
clientClass
.
put
(
ALIPAY_QR
,
AlipayQrPayClient
.
class
);
clientClass
.
put
(
ALIPAY_APP
,
AlipayAppPayClient
.
class
);
clientClass
.
put
(
ALIPAY_PC
,
AlipayPcPayClient
.
class
);
clientClass
.
put
(
ALIPAY_BAR
,
AlipayBarPayClient
.
class
);
// Mock 支付客户端
clientClass
.
put
(
MOCK
,
MockPayClient
.
class
);
}
@Override
public
void
registerPayClientClass
(
PayChannelEnum
channel
,
Class
<?>
payClientClass
)
{
clientClass
.
put
(
channel
,
payClientClass
);
}
@Override
public
PayClient
getPayClient
(
Long
channelId
)
{
AbstractPayClient
<?>
client
=
clients
.
get
(
channelId
);
if
(
client
==
null
)
{
log
.
error
(
"[getPayClient][渠道编号({}) 找不到客户端]"
,
channelId
);
}
return
client
;
}
@Override
@SuppressWarnings
(
"unchecked"
)
public
<
Config
extends
PayClientConfig
>
PayClient
createOrUpdatePayClient
(
Long
channelId
,
String
channelCode
,
Config
config
)
{
AbstractPayClient
<
Config
>
client
=
(
AbstractPayClient
<
Config
>)
clients
.
get
(
channelId
);
if
(
client
==
null
)
{
client
=
this
.
createPayClient
(
channelId
,
channelCode
,
config
);
client
.
init
();
clients
.
put
(
client
.
getId
(),
client
);
}
else
{
client
.
refresh
(
config
);
}
return
client
;
}
@SuppressWarnings
(
"unchecked"
)
private
<
Config
extends
PayClientConfig
>
AbstractPayClient
<
Config
>
createPayClient
(
Long
channelId
,
String
channelCode
,
Config
config
)
{
PayChannelEnum
channelEnum
=
PayChannelEnum
.
getByCode
(
channelCode
);
Assert
.
notNull
(
channelEnum
,
String
.
format
(
"支付渠道(%s) 为空"
,
channelCode
));
Class
<?>
payClientClass
=
clientClass
.
get
(
channelEnum
);
Assert
.
notNull
(
payClientClass
,
String
.
format
(
"支付渠道(%s) Class 为空"
,
channelCode
));
return
(
AbstractPayClient
<
Config
>)
ReflectUtil
.
newInstance
(
payClientClass
,
channelId
,
config
);
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
.
alipay
;
import
cn.hutool.core.bean.BeanUtil
;
import
cn.hutool.core.date.LocalDateTimeUtil
;
import
cn.hutool.core.lang.Assert
;
import
cn.hutool.core.map.MapUtil
;
import
cn.hutool.core.util.StrUtil
;
import
cn.hutool.http.HttpUtil
;
import
com.alipay.api.AlipayApiException
;
import
com.alipay.api.AlipayConfig
;
import
com.alipay.api.AlipayResponse
;
import
com.alipay.api.DefaultAlipayClient
;
import
com.alipay.api.domain.*
;
import
com.alipay.api.internal.util.AlipaySignature
;
import
com.alipay.api.request.*
;
import
com.alipay.api.response.*
;
import
lombok.Getter
;
import
lombok.SneakyThrows
;
import
lombok.extern.slf4j.Slf4j
;
import
org.dromara.common.core.utils.json.JsonUtils
;
import
org.dromara.common.mall.util.object.ObjectUtils
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO
;
import
org.dromara.common.pay.core.client.dto.refund.PayRefundRespDTO
;
import
org.dromara.common.pay.core.client.dto.refund.PayRefundUnifiedReqDTO
;
import
org.dromara.common.pay.core.client.dto.transfer.PayTransferRespDTO
;
import
org.dromara.common.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO
;
import
org.dromara.common.pay.core.client.impl.AbstractPayClient
;
import
org.dromara.common.pay.core.enums.order.PayOrderStatusRespEnum
;
import
org.dromara.common.pay.core.enums.transfer.PayTransferTypeEnum
;
import
java.nio.charset.StandardCharsets
;
import
java.time.LocalDateTime
;
import
java.util.Collections
;
import
java.util.Map
;
import
java.util.Objects
;
import
java.util.function.Supplier
;
import
static
cn
.
hutool
.
core
.
date
.
DatePattern
.
NORM_DATETIME_FORMATTER
;
import
static
org
.
dromara
.
common
.
mall
.
exception
.
enums
.
GlobalErrorCodeConstants
.*;
import
static
org
.
dromara
.
common
.
mall
.
exception
.
util
.
ServiceExceptionUtil
.
exception
;
import
static
org
.
dromara
.
common
.
mall
.
exception
.
util
.
ServiceExceptionUtil
.
exception0
;
import
static
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
.
alipay
.
AlipayPayClientConfig
.
MODE_CERTIFICATE
;
/**
* 支付宝抽象类,实现支付宝统一的接口、以及部分实现(退款)
*
* @author jason
*/
@Slf4j
public
abstract
class
AbstractAlipayPayClient
extends
AbstractPayClient
<
AlipayPayClientConfig
>
{
@Getter
// 仅用于单测场景
protected
DefaultAlipayClient
client
;
public
AbstractAlipayPayClient
(
Long
channelId
,
String
channelCode
,
AlipayPayClientConfig
config
)
{
super
(
channelId
,
channelCode
,
config
);
}
@Override
@SneakyThrows
protected
void
doInit
()
{
AlipayConfig
alipayConfig
=
new
AlipayConfig
();
BeanUtil
.
copyProperties
(
config
,
alipayConfig
,
false
);
this
.
client
=
new
DefaultAlipayClient
(
alipayConfig
);
}
// ============ 支付相关 ==========
/**
* 构造支付关闭的 {@link PayOrderRespDTO} 对象
*
* @return 支付关闭的 {@link PayOrderRespDTO} 对象
*/
protected
PayOrderRespDTO
buildClosedPayOrderRespDTO
(
PayOrderUnifiedReqDTO
reqDTO
,
AlipayResponse
response
)
{
Assert
.
isFalse
(
response
.
isSuccess
());
return
PayOrderRespDTO
.
closedOf
(
response
.
getSubCode
(),
response
.
getSubMsg
(),
reqDTO
.
getOutTradeNo
(),
response
);
}
@Override
public
PayOrderRespDTO
doParseOrderNotify
(
Map
<
String
,
String
>
params
,
String
body
)
throws
Throwable
{
// 1. 校验回调数据
Map
<
String
,
String
>
bodyObj
=
HttpUtil
.
decodeParamMap
(
body
,
StandardCharsets
.
UTF_8
);
AlipaySignature
.
rsaCheckV1
(
bodyObj
,
config
.
getAlipayPublicKey
(),
StandardCharsets
.
UTF_8
.
name
(),
config
.
getSignType
());
// 2. 解析订单的状态
// 额外说明:支付宝不仅仅支付成功会回调,再各种触发支付单数据变化时,都会进行回调,所以这里 status 的解析会写的比较复杂
Integer
status
=
parseStatus
(
bodyObj
.
get
(
"trade_status"
));
// 特殊逻辑: 支付宝没有退款成功的状态,所以,如果有退款金额,我们认为是退款成功
if
(
MapUtil
.
getDouble
(
bodyObj
,
"refund_fee"
,
0
D
)
>
0
)
{
status
=
PayOrderStatusRespEnum
.
REFUND
.
getStatus
();
}
Assert
.
notNull
(
status
,
(
Supplier
<
Throwable
>)
()
->
{
throw
new
IllegalArgumentException
(
StrUtil
.
format
(
"body({}) 的 trade_status 不正确"
,
body
));
});
return
PayOrderRespDTO
.
of
(
status
,
bodyObj
.
get
(
"trade_no"
),
bodyObj
.
get
(
"seller_id"
),
parseTime
(
params
.
get
(
"gmt_payment"
)),
bodyObj
.
get
(
"out_trade_no"
),
body
);
}
@Override
protected
PayOrderRespDTO
doGetOrder
(
String
outTradeNo
)
throws
Throwable
{
// 1.1 构建 AlipayTradeRefundModel 请求
AlipayTradeQueryModel
model
=
new
AlipayTradeQueryModel
();
model
.
setOutTradeNo
(
outTradeNo
);
// 1.2 构建 AlipayTradeQueryRequest 请求
AlipayTradeQueryRequest
request
=
new
AlipayTradeQueryRequest
();
request
.
setBizModel
(
model
);
AlipayTradeQueryResponse
response
;
if
(
Objects
.
equals
(
config
.
getMode
(),
MODE_CERTIFICATE
))
{
// 证书模式
response
=
client
.
certificateExecute
(
request
);
}
else
{
response
=
client
.
execute
(
request
);
}
if
(!
response
.
isSuccess
())
{
// 不成功,例如说订单不存在
return
PayOrderRespDTO
.
closedOf
(
response
.
getSubCode
(),
response
.
getSubMsg
(),
outTradeNo
,
response
);
}
// 2.2 解析订单的状态
Integer
status
=
parseStatus
(
response
.
getTradeStatus
());
Assert
.
notNull
(
status
,
()
->
{
throw
new
IllegalArgumentException
(
StrUtil
.
format
(
"body({}) 的 trade_status 不正确"
,
response
.
getBody
()));
});
return
PayOrderRespDTO
.
of
(
status
,
response
.
getTradeNo
(),
response
.
getBuyerUserId
(),
LocalDateTimeUtil
.
of
(
response
.
getSendPayDate
()),
outTradeNo
,
response
);
}
private
static
Integer
parseStatus
(
String
tradeStatus
)
{
return
Objects
.
equals
(
"WAIT_BUYER_PAY"
,
tradeStatus
)
?
PayOrderStatusRespEnum
.
WAITING
.
getStatus
()
:
ObjectUtils
.
equalsAny
(
tradeStatus
,
"TRADE_FINISHED"
,
"TRADE_SUCCESS"
)
?
PayOrderStatusRespEnum
.
SUCCESS
.
getStatus
()
:
Objects
.
equals
(
"TRADE_CLOSED"
,
tradeStatus
)
?
PayOrderStatusRespEnum
.
CLOSED
.
getStatus
()
:
null
;
}
// ============ 退款相关 ==========
/**
* 支付宝统一的退款接口 alipay.trade.refund
*
* @param reqDTO 退款请求 request DTO
* @return 退款请求 Response
*/
@Override
protected
PayRefundRespDTO
doUnifiedRefund
(
PayRefundUnifiedReqDTO
reqDTO
)
throws
AlipayApiException
{
// 1.1 构建 AlipayTradeRefundModel 请求
AlipayTradeRefundModel
model
=
new
AlipayTradeRefundModel
();
model
.
setOutTradeNo
(
reqDTO
.
getOutTradeNo
());
model
.
setOutRequestNo
(
reqDTO
.
getOutRefundNo
());
model
.
setRefundAmount
(
formatAmount
(
reqDTO
.
getRefundPrice
()));
model
.
setRefundReason
(
reqDTO
.
getReason
());
// 1.2 构建 AlipayTradePayRequest 请求
AlipayTradeRefundRequest
request
=
new
AlipayTradeRefundRequest
();
request
.
setBizModel
(
model
);
// 2.1 执行请求
AlipayTradeRefundResponse
response
;
if
(
Objects
.
equals
(
config
.
getMode
(),
MODE_CERTIFICATE
))
{
// 证书模式
response
=
client
.
certificateExecute
(
request
);
}
else
{
response
=
client
.
execute
(
request
);
}
if
(!
response
.
isSuccess
())
{
// 当出现 ACQ.SYSTEM_ERROR, 退款可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询
if
(
ObjectUtils
.
equalsAny
(
response
.
getSubCode
(),
"ACQ.SYSTEM_ERROR"
,
"SYSTEM_ERROR"
))
{
return
PayRefundRespDTO
.
waitingOf
(
null
,
reqDTO
.
getOutRefundNo
(),
response
);
}
return
PayRefundRespDTO
.
failureOf
(
response
.
getSubCode
(),
response
.
getSubMsg
(),
reqDTO
.
getOutRefundNo
(),
response
);
}
// 2.2 创建返回结果
// 支付宝只要退款调用返回 success,就认为退款成功,不需要回调。具体可见 parseNotify 方法的说明。
// 另外,支付宝没有退款单号,所以不用设置
return
PayRefundRespDTO
.
successOf
(
null
,
LocalDateTimeUtil
.
of
(
response
.
getGmtRefundPay
()),
reqDTO
.
getOutRefundNo
(),
response
);
}
@Override
public
PayRefundRespDTO
doParseRefundNotify
(
Map
<
String
,
String
>
params
,
String
body
)
{
// 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。
// ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调
// ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有
// 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。
// 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。
throw
new
UnsupportedOperationException
(
"支付宝无退款回调"
);
}
@Override
protected
PayRefundRespDTO
doGetRefund
(
String
outTradeNo
,
String
outRefundNo
)
throws
AlipayApiException
{
// 1.1 构建 AlipayTradeFastpayRefundQueryModel 请求
AlipayTradeFastpayRefundQueryModel
model
=
new
AlipayTradeFastpayRefundQueryModel
();
model
.
setOutTradeNo
(
outTradeNo
);
model
.
setOutRequestNo
(
outRefundNo
);
model
.
setQueryOptions
(
Collections
.
singletonList
(
"gmt_refund_pay"
));
// 1.2 构建 AlipayTradeFastpayRefundQueryRequest 请求
AlipayTradeFastpayRefundQueryRequest
request
=
new
AlipayTradeFastpayRefundQueryRequest
();
request
.
setBizModel
(
model
);
// 2.1 执行请求
AlipayTradeFastpayRefundQueryResponse
response
;
if
(
Objects
.
equals
(
config
.
getMode
(),
MODE_CERTIFICATE
))
{
// 证书模式
response
=
client
.
certificateExecute
(
request
);
}
else
{
response
=
client
.
execute
(
request
);
}
if
(!
response
.
isSuccess
())
{
// 明确不存在的情况,应该就是失败,可进行关闭
if
(
ObjectUtils
.
equalsAny
(
response
.
getSubCode
(),
"TRADE_NOT_EXIST"
,
"ACQ.TRADE_NOT_EXIST"
))
{
return
PayRefundRespDTO
.
failureOf
(
outRefundNo
,
response
);
}
// 可能存在“ACQ.SYSTEM_ERROR”系统错误等情况,所以返回 WAIT 继续等待
return
PayRefundRespDTO
.
waitingOf
(
null
,
outRefundNo
,
response
);
}
// 2.2 创建返回结果
if
(
Objects
.
equals
(
response
.
getRefundStatus
(),
"REFUND_SUCCESS"
))
{
return
PayRefundRespDTO
.
successOf
(
null
,
LocalDateTimeUtil
.
of
(
response
.
getGmtRefundPay
()),
outRefundNo
,
response
);
}
return
PayRefundRespDTO
.
waitingOf
(
null
,
outRefundNo
,
response
);
}
@Override
protected
PayTransferRespDTO
doUnifiedTransfer
(
PayTransferUnifiedReqDTO
reqDTO
)
throws
AlipayApiException
{
// 1.1 校验公钥类型 必须使用公钥证书模式
if
(!
Objects
.
equals
(
config
.
getMode
(),
MODE_CERTIFICATE
))
{
throw
exception0
(
ERROR_CONFIGURATION
.
getCode
(),
"支付宝单笔转账必须使用公钥证书模式"
);
}
// 1.2 构建 AlipayFundTransUniTransferModel
AlipayFundTransUniTransferModel
model
=
new
AlipayFundTransUniTransferModel
();
// ① 通用的参数
model
.
setTransAmount
(
formatAmount
(
reqDTO
.
getPrice
()));
// 转账金额
model
.
setOrderTitle
(
reqDTO
.
getSubject
());
// 转账业务的标题,用于在支付宝用户的账单里显示。
model
.
setOutBizNo
(
reqDTO
.
getOutTransferNo
());
model
.
setProductCode
(
"TRANS_ACCOUNT_NO_PWD"
);
// 销售产品码。单笔无密转账固定为 TRANS_ACCOUNT_NO_PWD
model
.
setBizScene
(
"DIRECT_TRANSFER"
);
// 业务场景 单笔无密转账固定为 DIRECT_TRANSFER
if
(
reqDTO
.
getChannelExtras
()
!=
null
)
{
model
.
setBusinessParams
(
JsonUtils
.
toJsonString
(
reqDTO
.
getChannelExtras
()));
}
// ② 个性化的参数
Participant
payeeInfo
=
new
Participant
();
PayTransferTypeEnum
transferType
=
PayTransferTypeEnum
.
typeOf
(
reqDTO
.
getType
());
switch
(
transferType
)
{
// TODO @jason:是不是不用传递 transferType 参数哈?因为应该已经明确是支付宝啦?
// @芋艿。 是不是还要考虑转账到银行卡。所以传 transferType 但是转账到银行卡不知道要如何测试??
case
ALIPAY_BALANCE:
{
payeeInfo
.
setIdentityType
(
"ALIPAY_LOGON_ID"
);
payeeInfo
.
setIdentity
(
reqDTO
.
getAlipayLogonId
());
// 支付宝登录号
payeeInfo
.
setName
(
reqDTO
.
getUserName
());
// 支付宝账号姓名
model
.
setPayeeInfo
(
payeeInfo
);
break
;
}
case
BANK_CARD:
{
payeeInfo
.
setIdentityType
(
"BANKCARD_ACCOUNT"
);
// TODO 待实现
throw
exception
(
NOT_IMPLEMENTED
);
}
default
:
{
throw
exception0
(
BAD_REQUEST
.
getCode
(),
"不正确的转账类型: {}"
,
transferType
);
}
}
// 1.3 构建 AlipayFundTransUniTransferRequest
AlipayFundTransUniTransferRequest
request
=
new
AlipayFundTransUniTransferRequest
();
request
.
setBizModel
(
model
);
// 执行请求
AlipayFundTransUniTransferResponse
response
=
client
.
certificateExecute
(
request
);
// 处理结果
if
(!
response
.
isSuccess
())
{
// 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询,或相同 outBizNo 重新发起转账
// 发现 outBizNo 相同 两次请求参数相同. 会返回 "PAYMENT_INFO_INCONSISTENCY", 不知道哪里的问题. 暂时返回 WAIT. 后续job 会轮询
if
(
ObjectUtils
.
equalsAny
(
response
.
getSubCode
(),
"PAYMENT_INFO_INCONSISTENCY"
,
"SYSTEM_ERROR"
,
"ACQ.SYSTEM_ERROR"
))
{
return
PayTransferRespDTO
.
waitingOf
(
null
,
reqDTO
.
getOutTransferNo
(),
response
);
}
return
PayTransferRespDTO
.
closedOf
(
response
.
getSubCode
(),
response
.
getSubMsg
(),
reqDTO
.
getOutTransferNo
(),
response
);
}
else
{
if
(
ObjectUtils
.
equalsAny
(
response
.
getStatus
(),
"REFUND"
,
"FAIL"
))
{
// 转账到银行卡会出现 "REFUND" "FAIL"
return
PayTransferRespDTO
.
closedOf
(
response
.
getSubCode
(),
response
.
getSubMsg
(),
reqDTO
.
getOutTransferNo
(),
response
);
}
if
(
Objects
.
equals
(
response
.
getStatus
(),
"DEALING"
))
{
// 转账到银行卡会出现 "DEALING" 处理中
return
PayTransferRespDTO
.
dealingOf
(
response
.
getOrderId
(),
reqDTO
.
getOutTransferNo
(),
response
);
}
return
PayTransferRespDTO
.
successOf
(
response
.
getOrderId
(),
parseTime
(
response
.
getTransDate
()),
response
.
getOutBizNo
(),
response
);
}
}
@Override
protected
PayTransferRespDTO
doGetTransfer
(
String
outTradeNo
,
PayTransferTypeEnum
type
)
throws
Throwable
{
// 1.1 构建 AlipayFundTransCommonQueryModel
AlipayFundTransCommonQueryModel
model
=
new
AlipayFundTransCommonQueryModel
();
model
.
setProductCode
(
type
==
PayTransferTypeEnum
.
BANK_CARD
?
"TRANS_BANKCARD_NO_PWD"
:
"TRANS_ACCOUNT_NO_PWD"
);
model
.
setBizScene
(
"DIRECT_TRANSFER"
);
//业务场景
model
.
setOutBizNo
(
outTradeNo
);
// 1.2 构建 AlipayFundTransCommonQueryRequest
AlipayFundTransCommonQueryRequest
request
=
new
AlipayFundTransCommonQueryRequest
();
request
.
setBizModel
(
model
);
// 2.1 执行请求
AlipayFundTransCommonQueryResponse
response
;
if
(
Objects
.
equals
(
config
.
getMode
(),
MODE_CERTIFICATE
))
{
// 证书模式
response
=
client
.
certificateExecute
(
request
);
}
else
{
response
=
client
.
execute
(
request
);
}
// 2.2 处理返回结果
if
(
response
.
isSuccess
())
{
if
(
ObjectUtils
.
equalsAny
(
response
.
getStatus
(),
"REFUND"
,
"FAIL"
))
{
// 转账到银行卡会出现 "REFUND" "FAIL"
return
PayTransferRespDTO
.
closedOf
(
response
.
getSubCode
(),
response
.
getSubMsg
(),
outTradeNo
,
response
);
}
if
(
Objects
.
equals
(
response
.
getStatus
(),
"DEALING"
))
{
// 转账到银行卡会出现 "DEALING" 处理中
return
PayTransferRespDTO
.
dealingOf
(
response
.
getOrderId
(),
outTradeNo
,
response
);
}
return
PayTransferRespDTO
.
successOf
(
response
.
getOrderId
(),
parseTime
(
response
.
getPayDate
()),
response
.
getOutBizNo
(),
response
);
}
else
{
// 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账
// 当出现 ORDER_NOT_EXIST 可能是转账还在处理中,也可能是转账处理失败. 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账
if
(
ObjectUtils
.
equalsAny
(
response
.
getSubCode
(),
"ORDER_NOT_EXIST"
,
"SYSTEM_ERROR"
,
"ACQ.SYSTEM_ERROR"
))
{
return
PayTransferRespDTO
.
waitingOf
(
null
,
outTradeNo
,
response
);
}
return
PayTransferRespDTO
.
closedOf
(
response
.
getSubCode
(),
response
.
getSubMsg
(),
outTradeNo
,
response
);
}
}
// TODO @chihuo:这里是不是也要实现,支付宝的。
@Override
protected
PayTransferRespDTO
doParseTransferNotify
(
Map
<
String
,
String
>
params
,
String
body
)
throws
Throwable
{
throw
new
UnsupportedOperationException
(
"未实现"
);
}
// ========== 各种工具方法 ==========
protected
String
formatAmount
(
Integer
amount
)
{
return
String
.
valueOf
(
amount
/
100.0
);
}
protected
String
formatTime
(
LocalDateTime
time
)
{
return
LocalDateTimeUtil
.
format
(
time
,
NORM_DATETIME_FORMATTER
);
}
protected
LocalDateTime
parseTime
(
String
str
)
{
return
LocalDateTimeUtil
.
parse
(
str
,
NORM_DATETIME_FORMATTER
);
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/impl/alipay/AlipayAppPayClient.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
.
alipay
;
import
com.alipay.api.AlipayApiException
;
import
com.alipay.api.domain.AlipayTradeAppPayModel
;
import
com.alipay.api.request.AlipayTradeAppPayRequest
;
import
com.alipay.api.response.AlipayTradeAppPayResponse
;
import
lombok.extern.slf4j.Slf4j
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO
;
import
org.dromara.common.pay.core.enums.channel.PayChannelEnum
;
import
org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum
;
/**
* 支付宝【App 支付】的 PayClient 实现类
*
* 文档:<a href="https://opendocs.alipay.com/open/02e7gq">App 支付</a>
*
* // TODO 芋艿:未详细测试,因为手头没 App
*
* @author 芋道源码
*/
@Slf4j
public
class
AlipayAppPayClient
extends
AbstractAlipayPayClient
{
public
AlipayAppPayClient
(
Long
channelId
,
AlipayPayClientConfig
config
)
{
super
(
channelId
,
PayChannelEnum
.
ALIPAY_APP
.
getCode
(),
config
);
}
@Override
public
PayOrderRespDTO
doUnifiedOrder
(
PayOrderUnifiedReqDTO
reqDTO
)
throws
AlipayApiException
{
// 1.1 构建 AlipayTradeAppPayModel 请求
AlipayTradeAppPayModel
model
=
new
AlipayTradeAppPayModel
();
// ① 通用的参数
model
.
setOutTradeNo
(
reqDTO
.
getOutTradeNo
());
model
.
setSubject
(
reqDTO
.
getSubject
());
model
.
setBody
(
reqDTO
.
getBody
()
+
"test"
);
model
.
setTotalAmount
(
formatAmount
(
reqDTO
.
getPrice
()));
model
.
setTimeExpire
(
formatTime
(
reqDTO
.
getExpireTime
()));
model
.
setProductCode
(
"QUICK_MSECURITY_PAY"
);
// 销售产品码:无线快捷支付产品
// ② 个性化的参数【无】
// ③ 支付宝扫码支付只有一种展示
String
displayMode
=
PayOrderDisplayModeEnum
.
APP
.
getMode
();
// 1.2 构建 AlipayTradePrecreateRequest 请求
AlipayTradeAppPayRequest
request
=
new
AlipayTradeAppPayRequest
();
request
.
setBizModel
(
model
);
request
.
setNotifyUrl
(
reqDTO
.
getNotifyUrl
());
request
.
setReturnUrl
(
reqDTO
.
getReturnUrl
());
// 2.1 执行请求
AlipayTradeAppPayResponse
response
=
client
.
sdkExecute
(
request
);
// 2.2 处理结果
if
(!
response
.
isSuccess
())
{
return
buildClosedPayOrderRespDTO
(
reqDTO
,
response
);
}
return
PayOrderRespDTO
.
waitingOf
(
displayMode
,
response
.
getBody
(),
reqDTO
.
getOutTradeNo
(),
response
);
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/impl/alipay/AlipayBarPayClient.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
.
alipay
;
import
cn.hutool.core.date.LocalDateTimeUtil
;
import
cn.hutool.core.map.MapUtil
;
import
cn.hutool.core.util.StrUtil
;
import
com.alipay.api.AlipayApiException
;
import
com.alipay.api.domain.AlipayTradePayModel
;
import
com.alipay.api.request.AlipayTradePayRequest
;
import
com.alipay.api.response.AlipayTradePayResponse
;
import
lombok.extern.slf4j.Slf4j
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO
;
import
org.dromara.common.pay.core.enums.channel.PayChannelEnum
;
import
org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum
;
import
java.time.LocalDateTime
;
import
java.util.Objects
;
import
static
org
.
dromara
.
common
.
mall
.
exception
.
enums
.
GlobalErrorCodeConstants
.
BAD_REQUEST
;
import
static
org
.
dromara
.
common
.
mall
.
exception
.
util
.
ServiceExceptionUtil
.
exception0
;
import
static
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
.
alipay
.
AlipayPayClientConfig
.
MODE_CERTIFICATE
;
/**
* 支付宝【条码支付】的 PayClient 实现类
*
* 文档:<a href="https://opendocs.alipay.com/open/194/105072">当面付</a>
*
* @author 芋道源码
*/
@Slf4j
public
class
AlipayBarPayClient
extends
AbstractAlipayPayClient
{
public
AlipayBarPayClient
(
Long
channelId
,
AlipayPayClientConfig
config
)
{
super
(
channelId
,
PayChannelEnum
.
ALIPAY_BAR
.
getCode
(),
config
);
}
@Override
public
PayOrderRespDTO
doUnifiedOrder
(
PayOrderUnifiedReqDTO
reqDTO
)
throws
AlipayApiException
{
String
authCode
=
MapUtil
.
getStr
(
reqDTO
.
getChannelExtras
(),
"auth_code"
);
if
(
StrUtil
.
isEmpty
(
authCode
))
{
throw
exception0
(
BAD_REQUEST
.
getCode
(),
"条形码不能为空"
);
}
// 1.1 构建 AlipayTradePayModel 请求
AlipayTradePayModel
model
=
new
AlipayTradePayModel
();
// ① 通用的参数
model
.
setOutTradeNo
(
reqDTO
.
getOutTradeNo
());
model
.
setSubject
(
reqDTO
.
getSubject
());
model
.
setBody
(
reqDTO
.
getBody
());
model
.
setTotalAmount
(
formatAmount
(
reqDTO
.
getPrice
()));
model
.
setScene
(
"bar_code"
);
// 当面付条码支付场景
// ② 个性化的参数
model
.
setAuthCode
(
authCode
);
// ③ 支付宝条码支付只有一种展示
String
displayMode
=
PayOrderDisplayModeEnum
.
BAR_CODE
.
getMode
();
// 1.2 构建 AlipayTradePayRequest 请求
AlipayTradePayRequest
request
=
new
AlipayTradePayRequest
();
request
.
setBizModel
(
model
);
request
.
setNotifyUrl
(
reqDTO
.
getNotifyUrl
());
request
.
setReturnUrl
(
reqDTO
.
getReturnUrl
());
// 2.1 执行请求
AlipayTradePayResponse
response
;
if
(
Objects
.
equals
(
config
.
getMode
(),
MODE_CERTIFICATE
))
{
// 证书模式
response
=
client
.
certificateExecute
(
request
);
}
else
{
response
=
client
.
execute
(
request
);
}
// 2.2 处理结果
if
(!
response
.
isSuccess
())
{
return
buildClosedPayOrderRespDTO
(
reqDTO
,
response
);
}
if
(
"10000"
.
equals
(
response
.
getCode
()))
{
// 免密支付
LocalDateTime
successTime
=
LocalDateTimeUtil
.
of
(
response
.
getGmtPayment
());
return
PayOrderRespDTO
.
successOf
(
response
.
getTradeNo
(),
response
.
getBuyerUserId
(),
successTime
,
response
.
getOutTradeNo
(),
response
)
.
setDisplayMode
(
displayMode
).
setDisplayContent
(
""
);
}
// 大额支付,需要用户输入密码,所以返回 waiting。此时,前端一般会进行轮询
return
PayOrderRespDTO
.
waitingOf
(
displayMode
,
""
,
reqDTO
.
getOutTradeNo
(),
response
);
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/impl/alipay/AlipayPayClientConfig.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
.
alipay
;
import
jakarta.validation.Validator
;
import
jakarta.validation.constraints.NotBlank
;
import
jakarta.validation.constraints.NotNull
;
import
lombok.Data
;
import
org.dromara.common.mall.util.validation.ValidationUtils
;
import
org.dromara.common.pay.core.client.PayClientConfig
;
/**
* 支付宝的 PayClientConfig 实现类
* 属性主要来自 {@link com.alipay.api.AlipayConfig} 的必要属性
*
* @author 芋道源码
*/
@Data
public
class
AlipayPayClientConfig
implements
PayClientConfig
{
/**
* 公钥类型 - 公钥模式
*/
public
static
final
Integer
MODE_PUBLIC_KEY
=
1
;
/**
* 公钥类型 - 证书模式
*/
public
static
final
Integer
MODE_CERTIFICATE
=
2
;
/**
* 接口内容加密方式 - AES 加密
*/
public
static
final
String
ENC_TYPE_AES
=
"AES"
;
/**
* 签名算法类型 - RSA
*/
public
static
final
String
SIGN_TYPE_DEFAULT
=
"RSA2"
;
/**
* 网关地址
*
* 1. <a href="https://openapi.alipay.com/gateway.do">生产环境</a>
* 2. <a href="https://openapi-sandbox.dl.alipaydev.com/gateway.do">沙箱环境</a>
*/
@NotBlank
(
message
=
"网关地址不能为空"
,
groups
=
{
ModePublicKey
.
class
,
ModeCertificate
.
class
})
private
String
serverUrl
;
/**
* 开放平台上创建的应用的 ID
*/
@NotBlank
(
message
=
"开放平台上创建的应用的 ID不能为空"
,
groups
=
{
ModePublicKey
.
class
,
ModeCertificate
.
class
})
private
String
appId
;
/**
* 签名算法类型,推荐:RSA2
* <p>
* {@link #SIGN_TYPE_DEFAULT}
*/
@NotBlank
(
message
=
"签名算法类型不能为空"
,
groups
=
{
ModePublicKey
.
class
,
ModeCertificate
.
class
})
private
String
signType
;
/**
* 公钥类型
* 1. {@link #MODE_PUBLIC_KEY} 情况,privateKey + alipayPublicKey
* 2. {@link #MODE_CERTIFICATE} 情况,appCertContent + alipayPublicCertContent + rootCertContent
*/
@NotNull
(
message
=
"公钥类型不能为空"
,
groups
=
{
ModePublicKey
.
class
,
ModeCertificate
.
class
})
private
Integer
mode
;
// ========== 公钥模式 ==========
/**
* 商户私钥
*/
@NotBlank
(
message
=
"商户私钥不能为空"
,
groups
=
{
ModePublicKey
.
class
})
private
String
privateKey
;
/**
* 支付宝公钥字符串
*/
@NotBlank
(
message
=
"支付宝公钥字符串不能为空"
,
groups
=
{
ModePublicKey
.
class
})
private
String
alipayPublicKey
;
// ========== 证书模式 ==========
/**
* 指定商户公钥应用证书内容字符串
*/
@NotBlank
(
message
=
"指定商户公钥应用证书内容不能为空"
,
groups
=
{
ModeCertificate
.
class
})
private
String
appCertContent
;
/**
* 指定支付宝公钥证书内容字符串
*/
@NotBlank
(
message
=
"指定支付宝公钥证书内容不能为空"
,
groups
=
{
ModeCertificate
.
class
})
private
String
alipayPublicCertContent
;
/**
* 指定根证书内容字符串
*/
@NotBlank
(
message
=
"指定根证书内容字符串不能为空"
,
groups
=
{
ModeCertificate
.
class
})
private
String
rootCertContent
;
/**
* 接口内容加密方式
*
* 1. 如果为空,将使用无加密方式
* 2. 如果要加密,目前支付宝只有 AES 一种加密方式
*
* @see <a href="https://opendocs.alipay.com/common/02mse3">支付宝开放平台</a>
* @see AlipayPayClientConfig#ENC_TYPE_AES
*/
private
String
encryptType
;
/**
* 接口内容加密的私钥
*/
private
String
encryptKey
;
public
interface
ModePublicKey
{
}
public
interface
ModeCertificate
{
}
@Override
public
void
validate
(
Validator
validator
)
{
ValidationUtils
.
validate
(
validator
,
this
,
MODE_PUBLIC_KEY
.
equals
(
this
.
getMode
())
?
ModePublicKey
.
class
:
ModeCertificate
.
class
);
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/impl/alipay/AlipayPcPayClient.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
.
alipay
;
import
cn.hutool.core.util.ObjectUtil
;
import
cn.hutool.http.Method
;
import
com.alipay.api.AlipayApiException
;
import
com.alipay.api.domain.AlipayTradePagePayModel
;
import
com.alipay.api.request.AlipayTradePagePayRequest
;
import
com.alipay.api.response.AlipayTradePagePayResponse
;
import
lombok.extern.slf4j.Slf4j
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO
;
import
org.dromara.common.pay.core.enums.channel.PayChannelEnum
;
import
org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum
;
import
java.util.Objects
;
/**
* 支付宝【PC 网站】的 PayClient 实现类
*
* 文档:<a href="https://opendocs.alipay.com/open/270/105898">电脑网站支付</a>
*
* @author XGD
*/
@Slf4j
public
class
AlipayPcPayClient
extends
AbstractAlipayPayClient
{
public
AlipayPcPayClient
(
Long
channelId
,
AlipayPayClientConfig
config
)
{
super
(
channelId
,
PayChannelEnum
.
ALIPAY_PC
.
getCode
(),
config
);
}
@Override
public
PayOrderRespDTO
doUnifiedOrder
(
PayOrderUnifiedReqDTO
reqDTO
)
throws
AlipayApiException
{
// 1.1 构建 AlipayTradePagePayModel 请求
AlipayTradePagePayModel
model
=
new
AlipayTradePagePayModel
();
// ① 通用的参数
model
.
setOutTradeNo
(
reqDTO
.
getOutTradeNo
());
model
.
setSubject
(
reqDTO
.
getSubject
());
model
.
setBody
(
reqDTO
.
getBody
());
model
.
setTotalAmount
(
formatAmount
(
reqDTO
.
getPrice
()));
model
.
setTimeExpire
(
formatTime
(
reqDTO
.
getExpireTime
()));
model
.
setProductCode
(
"FAST_INSTANT_TRADE_PAY"
);
// 销售产品码. 目前 PC 支付场景下仅支持 FAST_INSTANT_TRADE_PAY
// ② 个性化的参数
// 如果想弄更多个性化的参数,可参考 https://www.pingxx.com/api/支付渠道 extra 参数说明.html 的 alipay_pc_direct 部分进行拓展
model
.
setQrPayMode
(
"2"
);
// 跳转模式 - 订单码,效果参见:https://help.pingxx.com/article/1137360/
// ③ 支付宝 PC 支付有两种展示模式:FORM、URL
String
displayMode
=
ObjectUtil
.
defaultIfNull
(
reqDTO
.
getDisplayMode
(),
PayOrderDisplayModeEnum
.
URL
.
getMode
());
// 1.2 构建 AlipayTradePagePayRequest 请求
AlipayTradePagePayRequest
request
=
new
AlipayTradePagePayRequest
();
request
.
setBizModel
(
model
);
request
.
setNotifyUrl
(
reqDTO
.
getNotifyUrl
());
request
.
setReturnUrl
(
reqDTO
.
getReturnUrl
());
// 2.1 执行请求
AlipayTradePagePayResponse
response
;
if
(
Objects
.
equals
(
displayMode
,
PayOrderDisplayModeEnum
.
FORM
.
getMode
()))
{
response
=
client
.
pageExecute
(
request
,
Method
.
POST
.
name
());
// 需要特殊使用 POST 请求
}
else
{
response
=
client
.
pageExecute
(
request
,
Method
.
GET
.
name
());
}
// 2.2 处理结果
if
(!
response
.
isSuccess
())
{
return
buildClosedPayOrderRespDTO
(
reqDTO
,
response
);
}
return
PayOrderRespDTO
.
waitingOf
(
displayMode
,
response
.
getBody
(),
reqDTO
.
getOutTradeNo
(),
response
);
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/impl/alipay/AlipayQrPayClient.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
.
alipay
;
import
com.alipay.api.AlipayApiException
;
import
com.alipay.api.domain.AlipayTradePrecreateModel
;
import
com.alipay.api.request.AlipayTradePrecreateRequest
;
import
com.alipay.api.response.AlipayTradePrecreateResponse
;
import
lombok.extern.slf4j.Slf4j
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO
;
import
org.dromara.common.pay.core.enums.channel.PayChannelEnum
;
import
org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum
;
import
java.util.Objects
;
import
static
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
.
alipay
.
AlipayPayClientConfig
.
MODE_CERTIFICATE
;
/**
* 支付宝【扫码支付】的 PayClient 实现类
*
* 文档:<a href="https://opendocs.alipay.com/apis/02890k">扫码支付</a>
*
* @author 芋道源码
*/
@Slf4j
public
class
AlipayQrPayClient
extends
AbstractAlipayPayClient
{
public
AlipayQrPayClient
(
Long
channelId
,
AlipayPayClientConfig
config
)
{
super
(
channelId
,
PayChannelEnum
.
ALIPAY_QR
.
getCode
(),
config
);
}
@Override
public
PayOrderRespDTO
doUnifiedOrder
(
PayOrderUnifiedReqDTO
reqDTO
)
throws
AlipayApiException
{
// 1.1 构建 AlipayTradePrecreateModel 请求
AlipayTradePrecreateModel
model
=
new
AlipayTradePrecreateModel
();
// ① 通用的参数
model
.
setOutTradeNo
(
reqDTO
.
getOutTradeNo
());
model
.
setSubject
(
reqDTO
.
getSubject
());
model
.
setBody
(
reqDTO
.
getBody
());
model
.
setTotalAmount
(
formatAmount
(
reqDTO
.
getPrice
()));
model
.
setProductCode
(
"FACE_TO_FACE_PAYMENT"
);
// 销售产品码. 目前扫码支付场景下仅支持 FACE_TO_FACE_PAYMENT
// ② 个性化的参数【无】
// ③ 支付宝扫码支付只有一种展示,考虑到前端可能希望二维码扫描后,手机打开
String
displayMode
=
PayOrderDisplayModeEnum
.
QR_CODE
.
getMode
();
// 1.2 构建 AlipayTradePrecreateRequest 请求
AlipayTradePrecreateRequest
request
=
new
AlipayTradePrecreateRequest
();
request
.
setBizModel
(
model
);
request
.
setNotifyUrl
(
reqDTO
.
getNotifyUrl
());
request
.
setReturnUrl
(
reqDTO
.
getReturnUrl
());
// 2.1 执行请求
AlipayTradePrecreateResponse
response
;
if
(
Objects
.
equals
(
config
.
getMode
(),
MODE_CERTIFICATE
))
{
// 证书模式
response
=
client
.
certificateExecute
(
request
);
}
else
{
response
=
client
.
execute
(
request
);
}
// 2.2 处理结果
if
(!
response
.
isSuccess
())
{
return
buildClosedPayOrderRespDTO
(
reqDTO
,
response
);
}
return
PayOrderRespDTO
.
waitingOf
(
displayMode
,
response
.
getQrCode
(),
reqDTO
.
getOutTradeNo
(),
response
);
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/impl/alipay/AlipayWapPayClient.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
.
alipay
;
import
cn.hutool.http.Method
;
import
com.alipay.api.AlipayApiException
;
import
com.alipay.api.domain.AlipayTradeWapPayModel
;
import
com.alipay.api.request.AlipayTradeWapPayRequest
;
import
com.alipay.api.response.AlipayTradeWapPayResponse
;
import
lombok.extern.slf4j.Slf4j
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO
;
import
org.dromara.common.pay.core.enums.channel.PayChannelEnum
;
import
org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum
;
/**
* 支付宝【Wap 网站】的 PayClient 实现类
* <p>
* 文档:<a href="https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay">手机网站支付接口</a>
*
* @author 芋道源码
*/
@Slf4j
public
class
AlipayWapPayClient
extends
AbstractAlipayPayClient
{
public
AlipayWapPayClient
(
Long
channelId
,
AlipayPayClientConfig
config
)
{
super
(
channelId
,
PayChannelEnum
.
ALIPAY_WAP
.
getCode
(),
config
);
}
@Override
public
PayOrderRespDTO
doUnifiedOrder
(
PayOrderUnifiedReqDTO
reqDTO
)
throws
AlipayApiException
{
// 1.1 构建 AlipayTradeWapPayModel 请求
AlipayTradeWapPayModel
model
=
new
AlipayTradeWapPayModel
();
// ① 通用的参数
model
.
setOutTradeNo
(
reqDTO
.
getOutTradeNo
());
model
.
setSubject
(
reqDTO
.
getSubject
());
model
.
setBody
(
reqDTO
.
getBody
());
model
.
setTotalAmount
(
formatAmount
(
reqDTO
.
getPrice
()));
model
.
setProductCode
(
"QUICK_WAP_PAY"
);
// 销售产品码. 目前 Wap 支付场景下仅支持 QUICK_WAP_PAY
// ② 个性化的参数【无】
// ③ 支付宝 Wap 支付只有一种展示:URL
String
displayMode
=
PayOrderDisplayModeEnum
.
URL
.
getMode
();
// 1.2 构建 AlipayTradeWapPayRequest 请求
AlipayTradeWapPayRequest
request
=
new
AlipayTradeWapPayRequest
();
request
.
setBizModel
(
model
);
request
.
setNotifyUrl
(
reqDTO
.
getNotifyUrl
());
request
.
setReturnUrl
(
reqDTO
.
getReturnUrl
());
model
.
setQuitUrl
(
reqDTO
.
getReturnUrl
());
model
.
setTimeExpire
(
formatTime
(
reqDTO
.
getExpireTime
()));
// 2.1 执行请求
AlipayTradeWapPayResponse
response
=
client
.
pageExecute
(
request
,
Method
.
GET
.
name
());
// 2.2 处理结果
if
(!
response
.
isSuccess
())
{
return
buildClosedPayOrderRespDTO
(
reqDTO
,
response
);
}
return
PayOrderRespDTO
.
waitingOf
(
displayMode
,
response
.
getBody
(),
reqDTO
.
getOutTradeNo
(),
response
);
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/impl/mock/MockPayClient.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
.
mock
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO
;
import
org.dromara.common.pay.core.client.dto.refund.PayRefundRespDTO
;
import
org.dromara.common.pay.core.client.dto.refund.PayRefundUnifiedReqDTO
;
import
org.dromara.common.pay.core.client.dto.transfer.PayTransferRespDTO
;
import
org.dromara.common.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO
;
import
org.dromara.common.pay.core.client.impl.AbstractPayClient
;
import
org.dromara.common.pay.core.client.impl.NonePayClientConfig
;
import
org.dromara.common.pay.core.enums.channel.PayChannelEnum
;
import
org.dromara.common.pay.core.enums.transfer.PayTransferTypeEnum
;
import
java.time.LocalDateTime
;
import
java.util.Map
;
/**
* 模拟支付的 PayClient 实现类
*
* 模拟支付返回结果都是成功,方便大家日常流畅
*
* @author jason
*/
public
class
MockPayClient
extends
AbstractPayClient
<
NonePayClientConfig
>
{
private
static
final
String
MOCK_RESP_SUCCESS_DATA
=
"MOCK_SUCCESS"
;
public
MockPayClient
(
Long
channelId
,
NonePayClientConfig
config
)
{
super
(
channelId
,
PayChannelEnum
.
MOCK
.
getCode
(),
config
);
}
@Override
protected
void
doInit
()
{
}
@Override
protected
PayOrderRespDTO
doUnifiedOrder
(
PayOrderUnifiedReqDTO
reqDTO
)
{
return
PayOrderRespDTO
.
successOf
(
"MOCK-P-"
+
reqDTO
.
getOutTradeNo
(),
""
,
LocalDateTime
.
now
(),
reqDTO
.
getOutTradeNo
(),
MOCK_RESP_SUCCESS_DATA
);
}
@Override
protected
PayOrderRespDTO
doGetOrder
(
String
outTradeNo
)
{
return
PayOrderRespDTO
.
successOf
(
"MOCK-P-"
+
outTradeNo
,
""
,
LocalDateTime
.
now
(),
outTradeNo
,
MOCK_RESP_SUCCESS_DATA
);
}
@Override
protected
PayRefundRespDTO
doUnifiedRefund
(
PayRefundUnifiedReqDTO
reqDTO
)
{
return
PayRefundRespDTO
.
successOf
(
"MOCK-R-"
+
reqDTO
.
getOutRefundNo
(),
LocalDateTime
.
now
(),
reqDTO
.
getOutRefundNo
(),
MOCK_RESP_SUCCESS_DATA
);
}
@Override
protected
PayRefundRespDTO
doGetRefund
(
String
outTradeNo
,
String
outRefundNo
)
{
return
PayRefundRespDTO
.
successOf
(
"MOCK-R-"
+
outRefundNo
,
LocalDateTime
.
now
(),
outRefundNo
,
MOCK_RESP_SUCCESS_DATA
);
}
@Override
protected
PayTransferRespDTO
doParseTransferNotify
(
Map
<
String
,
String
>
params
,
String
body
)
throws
Throwable
{
throw
new
UnsupportedOperationException
(
"未实现"
);
}
@Override
protected
PayRefundRespDTO
doParseRefundNotify
(
Map
<
String
,
String
>
params
,
String
body
)
{
throw
new
UnsupportedOperationException
(
"模拟支付无退款回调"
);
}
@Override
protected
PayOrderRespDTO
doParseOrderNotify
(
Map
<
String
,
String
>
params
,
String
body
)
{
throw
new
UnsupportedOperationException
(
"模拟支付无支付回调"
);
}
@Override
protected
PayTransferRespDTO
doUnifiedTransfer
(
PayTransferUnifiedReqDTO
reqDTO
)
{
throw
new
UnsupportedOperationException
(
"待实现"
);
}
@Override
protected
PayTransferRespDTO
doGetTransfer
(
String
outTradeNo
,
PayTransferTypeEnum
type
)
{
throw
new
UnsupportedOperationException
(
"待实现"
);
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/impl/weixin/AbstractWxPayClient.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
.
weixin
;
import
cn.hutool.core.bean.BeanUtil
;
import
cn.hutool.core.codec.Base64
;
import
cn.hutool.core.collection.CollUtil
;
import
cn.hutool.core.date.LocalDateTimeUtil
;
import
cn.hutool.core.date.TemporalAccessorUtil
;
import
cn.hutool.core.util.StrUtil
;
import
com.github.binarywang.wxpay.bean.notify.WxPayNotifyV3Result
;
import
com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult
;
import
com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult
;
import
com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result
;
import
com.github.binarywang.wxpay.bean.request.*
;
import
com.github.binarywang.wxpay.bean.result.*
;
import
com.github.binarywang.wxpay.bean.transfer.QueryTransferBatchesRequest
;
import
com.github.binarywang.wxpay.bean.transfer.QueryTransferBatchesResult
;
import
com.github.binarywang.wxpay.bean.transfer.TransferBatchesRequest
;
import
com.github.binarywang.wxpay.bean.transfer.TransferBatchesResult
;
import
com.github.binarywang.wxpay.config.WxPayConfig
;
import
com.github.binarywang.wxpay.exception.WxPayException
;
import
com.github.binarywang.wxpay.service.WxPayService
;
import
com.github.binarywang.wxpay.service.impl.WxPayServiceImpl
;
import
lombok.extern.slf4j.Slf4j
;
import
org.dromara.common.core.utils.file.FileUtils
;
import
org.dromara.common.mall.util.object.ObjectUtils
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO
;
import
org.dromara.common.pay.core.client.dto.refund.PayRefundRespDTO
;
import
org.dromara.common.pay.core.client.dto.refund.PayRefundUnifiedReqDTO
;
import
org.dromara.common.pay.core.client.dto.transfer.PayTransferRespDTO
;
import
org.dromara.common.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO
;
import
org.dromara.common.pay.core.client.dto.transfer.WxPayTransferPartnerNotifyV3Result
;
import
org.dromara.common.pay.core.client.impl.AbstractPayClient
;
import
org.dromara.common.pay.core.enums.order.PayOrderStatusRespEnum
;
import
org.dromara.common.pay.core.enums.transfer.PayTransferTypeEnum
;
import
java.time.LocalDateTime
;
import
java.time.ZoneId
;
import
java.util.Collections
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.Objects
;
import
static
cn
.
hutool
.
core
.
date
.
DatePattern
.*;
import
static
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
.
weixin
.
WxPayClientConfig
.
API_VERSION_V2
;
import
static
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
.
weixin
.
WxPayClientConfig
.
API_VERSION_V3
;
/**
* 微信支付抽象类,实现微信统一的接口、以及部分实现(退款)
*
* @author 遇到源码
*/
@Slf4j
public
abstract
class
AbstractWxPayClient
extends
AbstractPayClient
<
WxPayClientConfig
>
{
protected
WxPayService
client
;
public
AbstractWxPayClient
(
Long
channelId
,
String
channelCode
,
WxPayClientConfig
config
)
{
super
(
channelId
,
channelCode
,
config
);
}
/**
* 初始化 client 客户端
*
* @param tradeType 交易类型
*/
protected
void
doInit
(
String
tradeType
)
{
// 创建 config 配置
WxPayConfig
payConfig
=
new
WxPayConfig
();
BeanUtil
.
copyProperties
(
config
,
payConfig
,
"keyContent"
,
"privateKeyContent"
);
payConfig
.
setTradeType
(
tradeType
);
// weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决
if
(
Objects
.
equals
(
config
.
getApiVersion
(),
API_VERSION_V2
))
{
payConfig
.
setKeyPath
(
FileUtils
.
createTempFile
(
Base64
.
decode
(
config
.
getKeyContent
())).
getPath
());
}
else
if
(
Objects
.
equals
(
config
.
getApiVersion
(),
API_VERSION_V3
))
{
payConfig
.
setPrivateKeyPath
(
FileUtils
.
createTempFile
(
config
.
getPrivateKeyContent
()).
getPath
());
}
// 创建 client 客户端
client
=
new
WxPayServiceImpl
();
client
.
setConfig
(
payConfig
);
}
// ============ 支付相关 ==========
@Override
protected
PayOrderRespDTO
doUnifiedOrder
(
PayOrderUnifiedReqDTO
reqDTO
)
throws
Exception
{
try
{
switch
(
config
.
getApiVersion
())
{
case
API_VERSION_V2:
return
doUnifiedOrderV2
(
reqDTO
);
case
API_VERSION_V3:
return
doUnifiedOrderV3
(
reqDTO
);
default
:
throw
new
IllegalArgumentException
(
String
.
format
(
"未知的 API 版本(%s)"
,
config
.
getApiVersion
()));
}
}
catch
(
WxPayException
e
)
{
log
.
error
(
"[doUnifiedOrder][退款({}) 发起微信支付异常"
,
reqDTO
,
e
);
String
errorCode
=
getErrorCode
(
e
);
String
errorMessage
=
getErrorMessage
(
e
);
return
PayOrderRespDTO
.
closedOf
(
errorCode
,
errorMessage
,
reqDTO
.
getOutTradeNo
(),
e
.
getXmlString
());
}
}
/**
* 【V2】调用支付渠道,统一下单
*
* @param reqDTO 下单信息
* @return 各支付渠道的返回结果
*/
protected
abstract
PayOrderRespDTO
doUnifiedOrderV2
(
PayOrderUnifiedReqDTO
reqDTO
)
throws
Exception
;
/**
* 【V3】调用支付渠道,统一下单
*
* @param reqDTO 下单信息
* @return 各支付渠道的返回结果
*/
protected
abstract
PayOrderRespDTO
doUnifiedOrderV3
(
PayOrderUnifiedReqDTO
reqDTO
)
throws
WxPayException
;
/**
* 【V2】创建微信下单请求
*
* @param reqDTO 下信息
* @return 下单请求
*/
protected
WxPayUnifiedOrderRequest
buildPayUnifiedOrderRequestV2
(
PayOrderUnifiedReqDTO
reqDTO
)
{
return
WxPayUnifiedOrderRequest
.
newBuilder
()
.
outTradeNo
(
reqDTO
.
getOutTradeNo
())
.
body
(
reqDTO
.
getSubject
())
.
detail
(
reqDTO
.
getBody
())
.
totalFee
(
reqDTO
.
getPrice
())
// 单位分
.
timeExpire
(
formatDateV2
(
reqDTO
.
getExpireTime
()))
.
spbillCreateIp
(
reqDTO
.
getUserIp
())
.
notifyUrl
(
reqDTO
.
getNotifyUrl
())
.
build
();
}
/**
* 【V3】创建微信下单请求
*
* @param reqDTO 下信息
* @return 下单请求
*/
protected
WxPayUnifiedOrderV3Request
buildPayUnifiedOrderRequestV3
(
PayOrderUnifiedReqDTO
reqDTO
)
{
WxPayUnifiedOrderV3Request
request
=
new
WxPayUnifiedOrderV3Request
();
request
.
setOutTradeNo
(
reqDTO
.
getOutTradeNo
());
request
.
setDescription
(
reqDTO
.
getSubject
());
request
.
setAmount
(
new
WxPayUnifiedOrderV3Request
.
Amount
().
setTotal
(
reqDTO
.
getPrice
()));
// 单位分
request
.
setTimeExpire
(
formatDateV3
(
reqDTO
.
getExpireTime
()));
request
.
setSceneInfo
(
new
WxPayUnifiedOrderV3Request
.
SceneInfo
().
setPayerClientIp
(
reqDTO
.
getUserIp
()));
request
.
setNotifyUrl
(
reqDTO
.
getNotifyUrl
());
return
request
;
}
@Override
public
PayOrderRespDTO
doParseOrderNotify
(
Map
<
String
,
String
>
params
,
String
body
)
throws
WxPayException
{
switch
(
config
.
getApiVersion
())
{
case
API_VERSION_V2:
return
doParseOrderNotifyV2
(
body
);
case
API_VERSION_V3:
return
doParseOrderNotifyV3
(
body
);
default
:
throw
new
IllegalArgumentException
(
String
.
format
(
"未知的 API 版本(%s)"
,
config
.
getApiVersion
()));
}
}
private
PayOrderRespDTO
doParseOrderNotifyV2
(
String
body
)
throws
WxPayException
{
// 1. 解析回调
WxPayOrderNotifyResult
response
=
client
.
parseOrderNotifyResult
(
body
);
// 2. 构建结果
// V2 微信支付的回调,只有 SUCCESS 支付成功、CLOSED 支付失败两种情况,无需像支付宝一样解析的比较复杂
Integer
status
=
Objects
.
equals
(
response
.
getResultCode
(),
"SUCCESS"
)
?
PayOrderStatusRespEnum
.
SUCCESS
.
getStatus
()
:
PayOrderStatusRespEnum
.
CLOSED
.
getStatus
();
return
PayOrderRespDTO
.
of
(
status
,
response
.
getTransactionId
(),
response
.
getOpenid
(),
parseDateV2
(
response
.
getTimeEnd
()),
response
.
getOutTradeNo
(),
body
);
}
private
PayOrderRespDTO
doParseOrderNotifyV3
(
String
body
)
throws
WxPayException
{
// 1. 解析回调
WxPayNotifyV3Result
response
=
client
.
parseOrderNotifyV3Result
(
body
,
null
);
WxPayNotifyV3Result
.
DecryptNotifyResult
result
=
response
.
getResult
();
// 2. 构建结果
Integer
status
=
parseStatus
(
result
.
getTradeState
());
String
openid
=
result
.
getPayer
()
!=
null
?
result
.
getPayer
().
getOpenid
()
:
null
;
return
PayOrderRespDTO
.
of
(
status
,
result
.
getTransactionId
(),
openid
,
parseDateV3
(
result
.
getSuccessTime
()),
result
.
getOutTradeNo
(),
body
);
}
@Override
protected
PayOrderRespDTO
doGetOrder
(
String
outTradeNo
)
throws
Throwable
{
try
{
switch
(
config
.
getApiVersion
())
{
case
API_VERSION_V2:
return
doGetOrderV2
(
outTradeNo
);
case
API_VERSION_V3:
return
doGetOrderV3
(
outTradeNo
);
default
:
throw
new
IllegalArgumentException
(
String
.
format
(
"未知的 API 版本(%s)"
,
config
.
getApiVersion
()));
}
}
catch
(
WxPayException
e
)
{
if
(
ObjectUtils
.
equalsAny
(
e
.
getErrCode
(),
"ORDERNOTEXIST"
,
"ORDER_NOT_EXIST"
))
{
String
errorCode
=
getErrorCode
(
e
);
String
errorMessage
=
getErrorMessage
(
e
);
return
PayOrderRespDTO
.
closedOf
(
errorCode
,
errorMessage
,
outTradeNo
,
e
.
getXmlString
());
}
throw
e
;
}
}
private
PayOrderRespDTO
doGetOrderV2
(
String
outTradeNo
)
throws
WxPayException
{
// 构建 WxPayUnifiedOrderRequest 对象
WxPayOrderQueryRequest
request
=
WxPayOrderQueryRequest
.
newBuilder
()
.
outTradeNo
(
outTradeNo
).
build
();
// 执行请求
WxPayOrderQueryResult
response
=
client
.
queryOrder
(
request
);
// 转换结果
Integer
status
=
parseStatus
(
response
.
getTradeState
());
return
PayOrderRespDTO
.
of
(
status
,
response
.
getTransactionId
(),
response
.
getOpenid
(),
parseDateV2
(
response
.
getTimeEnd
()),
outTradeNo
,
response
);
}
private
PayOrderRespDTO
doGetOrderV3
(
String
outTradeNo
)
throws
WxPayException
{
// 构建 WxPayUnifiedOrderRequest 对象
WxPayOrderQueryV3Request
request
=
new
WxPayOrderQueryV3Request
()
.
setOutTradeNo
(
outTradeNo
);
// 执行请求
WxPayOrderQueryV3Result
response
=
client
.
queryOrderV3
(
request
);
// 转换结果
Integer
status
=
parseStatus
(
response
.
getTradeState
());
String
openid
=
response
.
getPayer
()
!=
null
?
response
.
getPayer
().
getOpenid
()
:
null
;
return
PayOrderRespDTO
.
of
(
status
,
response
.
getTransactionId
(),
openid
,
parseDateV3
(
response
.
getSuccessTime
()),
outTradeNo
,
response
);
}
private
static
Integer
parseStatus
(
String
tradeState
)
{
switch
(
tradeState
)
{
case
"NOTPAY"
:
case
"USERPAYING"
:
// 支付中,等待用户输入密码(条码支付独有)
return
PayOrderStatusRespEnum
.
WAITING
.
getStatus
();
case
"SUCCESS"
:
return
PayOrderStatusRespEnum
.
SUCCESS
.
getStatus
();
case
"REFUND"
:
return
PayOrderStatusRespEnum
.
REFUND
.
getStatus
();
case
"CLOSED"
:
case
"REVOKED"
:
// 已撤销(刷卡支付独有)
case
"PAYERROR"
:
// 支付失败(其它原因,如银行返回失败)
return
PayOrderStatusRespEnum
.
CLOSED
.
getStatus
();
default
:
throw
new
IllegalArgumentException
(
StrUtil
.
format
(
"未知的支付状态({})"
,
tradeState
));
}
}
// ============ 退款相关 ==========
@Override
protected
PayRefundRespDTO
doUnifiedRefund
(
PayRefundUnifiedReqDTO
reqDTO
)
throws
Throwable
{
try
{
switch
(
config
.
getApiVersion
())
{
case
API_VERSION_V2:
return
doUnifiedRefundV2
(
reqDTO
);
case
API_VERSION_V3:
return
doUnifiedRefundV3
(
reqDTO
);
default
:
throw
new
IllegalArgumentException
(
String
.
format
(
"未知的 API 版本(%s)"
,
config
.
getApiVersion
()));
}
}
catch
(
WxPayException
e
)
{
String
errorCode
=
getErrorCode
(
e
);
String
errorMessage
=
getErrorMessage
(
e
);
return
PayRefundRespDTO
.
failureOf
(
errorCode
,
errorMessage
,
reqDTO
.
getOutRefundNo
(),
e
.
getXmlString
());
}
}
private
PayRefundRespDTO
doUnifiedRefundV2
(
PayRefundUnifiedReqDTO
reqDTO
)
throws
Throwable
{
// 1. 构建 WxPayRefundRequest 请求
WxPayRefundRequest
request
=
new
WxPayRefundRequest
()
.
setOutTradeNo
(
reqDTO
.
getOutTradeNo
())
.
setOutRefundNo
(
reqDTO
.
getOutRefundNo
())
.
setRefundFee
(
reqDTO
.
getRefundPrice
())
.
setRefundDesc
(
reqDTO
.
getReason
())
.
setTotalFee
(
reqDTO
.
getPayPrice
())
.
setNotifyUrl
(
reqDTO
.
getNotifyUrl
());
// 2.1 执行请求
WxPayRefundResult
response
=
client
.
refundV2
(
request
);
// 2.2 创建返回结果
if
(
Objects
.
equals
(
"SUCCESS"
,
response
.
getResultCode
()))
{
// V2 情况下,不直接返回退款成功,而是等待异步通知
return
PayRefundRespDTO
.
waitingOf
(
response
.
getRefundId
(),
reqDTO
.
getOutRefundNo
(),
response
);
}
return
PayRefundRespDTO
.
failureOf
(
reqDTO
.
getOutRefundNo
(),
response
);
}
private
PayRefundRespDTO
doUnifiedRefundV3
(
PayRefundUnifiedReqDTO
reqDTO
)
throws
Throwable
{
// 1. 构建 WxPayRefundRequest 请求
WxPayRefundV3Request
request
=
new
WxPayRefundV3Request
()
.
setOutTradeNo
(
reqDTO
.
getOutTradeNo
())
.
setOutRefundNo
(
reqDTO
.
getOutRefundNo
())
.
setAmount
(
new
WxPayRefundV3Request
.
Amount
().
setRefund
(
reqDTO
.
getRefundPrice
())
.
setTotal
(
reqDTO
.
getPayPrice
()).
setCurrency
(
"CNY"
))
.
setReason
(
reqDTO
.
getReason
())
.
setNotifyUrl
(
reqDTO
.
getNotifyUrl
());
// 2.1 执行请求
WxPayRefundV3Result
response
=
client
.
refundV3
(
request
);
// 2.2 创建返回结果
if
(
Objects
.
equals
(
"SUCCESS"
,
response
.
getStatus
()))
{
return
PayRefundRespDTO
.
successOf
(
response
.
getRefundId
(),
parseDateV3
(
response
.
getSuccessTime
()),
reqDTO
.
getOutRefundNo
(),
response
);
}
if
(
Objects
.
equals
(
"PROCESSING"
,
response
.
getStatus
()))
{
return
PayRefundRespDTO
.
waitingOf
(
response
.
getRefundId
(),
reqDTO
.
getOutRefundNo
(),
response
);
}
return
PayRefundRespDTO
.
failureOf
(
reqDTO
.
getOutRefundNo
(),
response
);
}
@Override
public
PayRefundRespDTO
doParseRefundNotify
(
Map
<
String
,
String
>
params
,
String
body
)
throws
WxPayException
{
switch
(
config
.
getApiVersion
())
{
case
API_VERSION_V2:
return
doParseRefundNotifyV2
(
body
);
case
API_VERSION_V3:
return
parseRefundNotifyV3
(
body
);
default
:
throw
new
IllegalArgumentException
(
String
.
format
(
"未知的 API 版本(%s)"
,
config
.
getApiVersion
()));
}
}
private
PayRefundRespDTO
doParseRefundNotifyV2
(
String
body
)
throws
WxPayException
{
// 1. 解析回调
WxPayRefundNotifyResult
response
=
client
.
parseRefundNotifyResult
(
body
);
WxPayRefundNotifyResult
.
ReqInfo
result
=
response
.
getReqInfo
();
// 2. 构建结果
if
(
Objects
.
equals
(
"SUCCESS"
,
result
.
getRefundStatus
()))
{
return
PayRefundRespDTO
.
successOf
(
result
.
getRefundId
(),
parseDateV2B
(
result
.
getSuccessTime
()),
result
.
getOutRefundNo
(),
response
);
}
return
PayRefundRespDTO
.
failureOf
(
result
.
getOutRefundNo
(),
response
);
}
private
PayRefundRespDTO
parseRefundNotifyV3
(
String
body
)
throws
WxPayException
{
// 1. 解析回调
WxPayRefundNotifyV3Result
response
=
client
.
parseRefundNotifyV3Result
(
body
,
null
);
WxPayRefundNotifyV3Result
.
DecryptNotifyResult
result
=
response
.
getResult
();
// 2. 构建结果
if
(
Objects
.
equals
(
"SUCCESS"
,
result
.
getRefundStatus
()))
{
return
PayRefundRespDTO
.
successOf
(
result
.
getRefundId
(),
parseDateV3
(
result
.
getSuccessTime
()),
result
.
getOutRefundNo
(),
response
);
}
return
PayRefundRespDTO
.
failureOf
(
result
.
getOutRefundNo
(),
response
);
}
@Override
public
PayTransferRespDTO
doParseTransferNotify
(
Map
<
String
,
String
>
params
,
String
body
)
throws
WxPayException
{
switch
(
config
.
getApiVersion
())
{
case
API_VERSION_V3:
return
parseTransferNotifyV3
(
body
);
case
API_VERSION_V2:
throw
new
UnsupportedOperationException
(
"V2 版本暂不支持,建议使用 V3 版本"
);
default
:
throw
new
IllegalArgumentException
(
String
.
format
(
"未知的 API 版本(%s)"
,
config
.
getApiVersion
()));
}
}
private
PayTransferRespDTO
parseTransferNotifyV3
(
String
body
)
throws
WxPayException
{
// 1. 解析回调
// TODO @luchi:这个可以复用 wxjava 里的类么?
WxPayTransferPartnerNotifyV3Result
response
=
client
.
baseParseOrderNotifyV3Result
(
body
,
null
,
WxPayTransferPartnerNotifyV3Result
.
class
,
WxPayTransferPartnerNotifyV3Result
.
TransferNotifyResult
.
class
);
WxPayTransferPartnerNotifyV3Result
.
TransferNotifyResult
result
=
response
.
getResult
();
// 2. 构建结果
if
(
Objects
.
equals
(
"FINISHED"
,
result
.
getBatchStatus
()))
{
if
(
result
.
getFailNum
()
<=
0
)
{
return
PayTransferRespDTO
.
successOf
(
result
.
getBatchId
(),
parseDateV3
(
result
.
getUpdateTime
()),
result
.
getOutBatchNo
(),
response
);
}
}
return
PayTransferRespDTO
.
closedOf
(
result
.
getBatchStatus
(),
result
.
getCloseReason
(),
result
.
getOutBatchNo
(),
response
);
}
@Override
protected
PayRefundRespDTO
doGetRefund
(
String
outTradeNo
,
String
outRefundNo
)
throws
WxPayException
{
try
{
switch
(
config
.
getApiVersion
())
{
case
API_VERSION_V2:
return
doGetRefundV2
(
outTradeNo
,
outRefundNo
);
case
API_VERSION_V3:
return
doGetRefundV3
(
outTradeNo
,
outRefundNo
);
default
:
throw
new
IllegalArgumentException
(
String
.
format
(
"未知的 API 版本(%s)"
,
config
.
getApiVersion
()));
}
}
catch
(
WxPayException
e
)
{
if
(
ObjectUtils
.
equalsAny
(
e
.
getErrCode
(),
"REFUNDNOTEXIST"
,
"RESOURCE_NOT_EXISTS"
))
{
String
errorCode
=
getErrorCode
(
e
);
String
errorMessage
=
getErrorMessage
(
e
);
return
PayRefundRespDTO
.
failureOf
(
errorCode
,
errorMessage
,
outRefundNo
,
e
.
getXmlString
());
}
throw
e
;
}
}
private
PayRefundRespDTO
doGetRefundV2
(
String
outTradeNo
,
String
outRefundNo
)
throws
WxPayException
{
// 1. 构建 WxPayRefundRequest 请求
WxPayRefundQueryRequest
request
=
WxPayRefundQueryRequest
.
newBuilder
()
.
outTradeNo
(
outTradeNo
)
.
outRefundNo
(
outRefundNo
)
.
build
();
// 2.1 执行请求
WxPayRefundQueryResult
response
=
client
.
refundQuery
(
request
);
// 2.2 创建返回结果
if
(!
Objects
.
equals
(
"SUCCESS"
,
response
.
getResultCode
()))
{
return
PayRefundRespDTO
.
waitingOf
(
null
,
outRefundNo
,
response
);
}
WxPayRefundQueryResult
.
RefundRecord
refund
=
CollUtil
.
findOne
(
response
.
getRefundRecords
(),
record
->
record
.
getOutRefundNo
().
equals
(
outRefundNo
));
if
(
refund
==
null
)
{
return
PayRefundRespDTO
.
failureOf
(
outRefundNo
,
response
);
}
switch
(
refund
.
getRefundStatus
())
{
case
"SUCCESS"
:
return
PayRefundRespDTO
.
successOf
(
refund
.
getRefundId
(),
parseDateV2B
(
refund
.
getRefundSuccessTime
()),
outRefundNo
,
response
);
case
"PROCESSING"
:
return
PayRefundRespDTO
.
waitingOf
(
refund
.
getRefundId
(),
outRefundNo
,
response
);
case
"CHANGE"
:
// 退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,资金回流到商户的现金帐号,需要商户人工干预,通过线下或者财付通转账的方式进行退款
case
"FAIL"
:
return
PayRefundRespDTO
.
failureOf
(
outRefundNo
,
response
);
default
:
throw
new
IllegalArgumentException
(
String
.
format
(
"未知的退款状态(%s)"
,
refund
.
getRefundStatus
()));
}
}
private
PayRefundRespDTO
doGetRefundV3
(
String
outTradeNo
,
String
outRefundNo
)
throws
WxPayException
{
// 1. 构建 WxPayRefundRequest 请求
WxPayRefundQueryV3Request
request
=
new
WxPayRefundQueryV3Request
();
request
.
setOutRefundNo
(
outRefundNo
);
// 2.1 执行请求
WxPayRefundQueryV3Result
response
=
client
.
refundQueryV3
(
request
);
// 2.2 创建返回结果
switch
(
response
.
getStatus
())
{
case
"SUCCESS"
:
return
PayRefundRespDTO
.
successOf
(
response
.
getRefundId
(),
parseDateV3
(
response
.
getSuccessTime
()),
outRefundNo
,
response
);
case
"PROCESSING"
:
return
PayRefundRespDTO
.
waitingOf
(
response
.
getRefundId
(),
outRefundNo
,
response
);
case
"ABNORMAL"
:
// 退款异常
case
"CLOSED"
:
return
PayRefundRespDTO
.
failureOf
(
outRefundNo
,
response
);
default
:
throw
new
IllegalArgumentException
(
String
.
format
(
"未知的退款状态(%s)"
,
response
.
getStatus
()));
}
}
@Override
protected
PayTransferRespDTO
doUnifiedTransfer
(
PayTransferUnifiedReqDTO
reqDTO
)
throws
WxPayException
{
// 1. 构建 TransferBatchesRequest 请求
List
<
TransferBatchesRequest
.
TransferDetail
>
transferDetailList
=
Collections
.
singletonList
(
TransferBatchesRequest
.
TransferDetail
.
newBuilder
()
.
outDetailNo
(
reqDTO
.
getOutTransferNo
())
.
transferAmount
(
reqDTO
.
getPrice
())
.
transferRemark
(
reqDTO
.
getSubject
())
.
openid
(
reqDTO
.
getOpenid
())
.
build
());
// TODO @luchi:能不能我们搞个 TransferBatchesRequestX extends TransferBatchesRequest,这样更简洁一点。
TransferBatchesRequest
transferBatches
=
TransferBatchesRequest
.
newBuilder
()
.
appid
(
this
.
config
.
getAppId
())
.
outBatchNo
(
reqDTO
.
getOutTransferNo
())
.
batchName
(
reqDTO
.
getSubject
())
.
batchRemark
(
reqDTO
.
getSubject
())
.
totalAmount
(
reqDTO
.
getPrice
())
.
totalNum
(
transferDetailList
.
size
())
.
transferDetailList
(
transferDetailList
).
build
()
.
setNotifyUrl
(
reqDTO
.
getNotifyUrl
());
// 2.1 执行请求
TransferBatchesResult
transferBatchesResult
=
client
.
getTransferService
().
transferBatches
(
transferBatches
);
// 2.2 创建返回结果
return
PayTransferRespDTO
.
dealingOf
(
transferBatchesResult
.
getBatchId
(),
reqDTO
.
getOutTransferNo
(),
transferBatchesResult
);
}
@Override
protected
PayTransferRespDTO
doGetTransfer
(
String
outTradeNo
,
PayTransferTypeEnum
type
)
throws
WxPayException
{
QueryTransferBatchesRequest
request
=
QueryTransferBatchesRequest
.
newBuilder
()
.
outBatchNo
(
outTradeNo
).
needQueryDetail
(
true
).
offset
(
0
).
limit
(
20
).
detailStatus
(
"ALL"
)
.
build
();
QueryTransferBatchesResult
response
=
client
.
getTransferService
().
transferBatchesOutBatchNo
(
request
);
QueryTransferBatchesResult
.
TransferBatch
transferBatch
=
response
.
getTransferBatch
();
if
(
Objects
.
equals
(
"FINISHED"
,
transferBatch
.
getBatchStatus
()))
{
// 明细中全部成功则成功,任一失败则失败
if
(
response
.
getTransferDetailList
().
stream
().
allMatch
(
detail
->
Objects
.
equals
(
"SUCCESS"
,
detail
.
getDetailStatus
())))
{
return
PayTransferRespDTO
.
successOf
(
transferBatch
.
getBatchId
(),
parseDateV3
(
transferBatch
.
getUpdateTime
()),
transferBatch
.
getOutBatchNo
(),
response
);
}
if
(
response
.
getTransferDetailList
().
stream
().
anyMatch
(
detail
->
Objects
.
equals
(
"FAIL"
,
detail
.
getDetailStatus
())))
{
return
PayTransferRespDTO
.
closedOf
(
transferBatch
.
getBatchStatus
(),
transferBatch
.
getCloseReason
(),
transferBatch
.
getOutBatchNo
(),
response
);
}
}
if
(
Objects
.
equals
(
"CLOSED"
,
transferBatch
.
getBatchStatus
()))
{
return
PayTransferRespDTO
.
closedOf
(
transferBatch
.
getBatchStatus
(),
transferBatch
.
getCloseReason
(),
transferBatch
.
getOutBatchNo
(),
response
);
}
return
PayTransferRespDTO
.
dealingOf
(
transferBatch
.
getBatchId
(),
transferBatch
.
getOutBatchNo
(),
response
);
}
// ========== 各种工具方法 ==========
static
String
formatDateV2
(
LocalDateTime
time
)
{
return
TemporalAccessorUtil
.
format
(
time
.
atZone
(
ZoneId
.
systemDefault
()),
PURE_DATETIME_PATTERN
);
}
static
LocalDateTime
parseDateV2
(
String
time
)
{
return
LocalDateTimeUtil
.
parse
(
time
,
PURE_DATETIME_PATTERN
);
}
static
LocalDateTime
parseDateV2B
(
String
time
)
{
return
LocalDateTimeUtil
.
parse
(
time
,
NORM_DATETIME_PATTERN
);
}
static
String
formatDateV3
(
LocalDateTime
time
)
{
return
TemporalAccessorUtil
.
format
(
time
.
atZone
(
ZoneId
.
systemDefault
()),
UTC_WITH_XXX_OFFSET_PATTERN
);
}
static
LocalDateTime
parseDateV3
(
String
time
)
{
return
LocalDateTimeUtil
.
parse
(
time
,
UTC_WITH_XXX_OFFSET_PATTERN
);
}
static
String
getErrorCode
(
WxPayException
e
)
{
if
(
StrUtil
.
isNotEmpty
(
e
.
getErrCode
()))
{
return
e
.
getErrCode
();
}
if
(
StrUtil
.
isNotEmpty
(
e
.
getCustomErrorMsg
()))
{
return
"CUSTOM_ERROR"
;
}
return
e
.
getReturnCode
();
}
static
String
getErrorMessage
(
WxPayException
e
)
{
if
(
StrUtil
.
isNotEmpty
(
e
.
getErrCode
()))
{
return
e
.
getErrCodeDes
();
}
if
(
StrUtil
.
isNotEmpty
(
e
.
getCustomErrorMsg
()))
{
return
e
.
getCustomErrorMsg
();
}
return
e
.
getReturnMsg
();
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/impl/weixin/WxAppPayClient.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
.
weixin
;
import
com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult
;
import
com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest
;
import
com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request
;
import
com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result
;
import
com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum
;
import
com.github.binarywang.wxpay.constant.WxPayConstants
;
import
com.github.binarywang.wxpay.exception.WxPayException
;
import
lombok.extern.slf4j.Slf4j
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO
;
import
org.dromara.common.pay.core.enums.channel.PayChannelEnum
;
import
org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum
;
import
static
org
.
dromara
.
common
.
core
.
utils
.
json
.
JsonUtils
.
toJsonString
;
/**
* 微信支付【App 支付】的 PayClient 实现类
*
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_5_3.shtml">App 支付</a>
*
* // TODO 芋艿:未详细测试,因为手头没 App
*
* @author 芋道源码
*/
@Slf4j
public
class
WxAppPayClient
extends
AbstractWxPayClient
{
public
WxAppPayClient
(
Long
channelId
,
WxPayClientConfig
config
)
{
super
(
channelId
,
PayChannelEnum
.
WX_APP
.
getCode
(),
config
);
}
@Override
protected
void
doInit
()
{
super
.
doInit
(
WxPayConstants
.
TradeType
.
APP
);
}
@Override
protected
PayOrderRespDTO
doUnifiedOrderV2
(
PayOrderUnifiedReqDTO
reqDTO
)
throws
WxPayException
{
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest
request
=
buildPayUnifiedOrderRequestV2
(
reqDTO
);
// 执行请求
WxPayMpOrderResult
response
=
client
.
createOrder
(
request
);
// 转换结果
return
PayOrderRespDTO
.
waitingOf
(
PayOrderDisplayModeEnum
.
APP
.
getMode
(),
toJsonString
(
response
),
reqDTO
.
getOutTradeNo
(),
response
);
}
@Override
protected
PayOrderRespDTO
doUnifiedOrderV3
(
PayOrderUnifiedReqDTO
reqDTO
)
throws
WxPayException
{
// 构建 WxPayUnifiedOrderV3Request 对象
WxPayUnifiedOrderV3Request
request
=
buildPayUnifiedOrderRequestV3
(
reqDTO
);
// 执行请求
WxPayUnifiedOrderV3Result
.
AppResult
response
=
client
.
createOrderV3
(
TradeTypeEnum
.
APP
,
request
);
// 转换结果
return
PayOrderRespDTO
.
waitingOf
(
PayOrderDisplayModeEnum
.
APP
.
getMode
(),
toJsonString
(
response
),
reqDTO
.
getOutTradeNo
(),
response
);
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/impl/weixin/WxBarPayClient.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
.
weixin
;
import
cn.hutool.core.map.MapUtil
;
import
cn.hutool.core.thread.ThreadUtil
;
import
cn.hutool.core.util.StrUtil
;
import
com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest
;
import
com.github.binarywang.wxpay.bean.result.WxPayMicropayResult
;
import
com.github.binarywang.wxpay.constant.WxPayConstants
;
import
com.github.binarywang.wxpay.exception.WxPayException
;
import
lombok.extern.slf4j.Slf4j
;
import
org.dromara.common.mall.util.date.LocalDateTimeUtils
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO
;
import
org.dromara.common.pay.core.enums.channel.PayChannelEnum
;
import
org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum
;
import
java.time.Duration
;
import
java.time.LocalDateTime
;
import
java.util.concurrent.TimeUnit
;
import
static
org
.
dromara
.
common
.
core
.
utils
.
json
.
JsonUtils
.
toJsonString
;
import
static
org
.
dromara
.
common
.
mall
.
exception
.
util
.
ServiceExceptionUtil
.
invalidParamException
;
/**
* 微信支付【付款码支付】的 PayClient 实现类
*
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_10&index=1">付款码支付</a>
*
* @author 芋道源码
*/
@Slf4j
public
class
WxBarPayClient
extends
AbstractWxPayClient
{
/**
* 微信付款码的过期时间
*/
private
static
final
Duration
AUTH_CODE_EXPIRE
=
Duration
.
ofMinutes
(
3
);
public
WxBarPayClient
(
Long
channelId
,
WxPayClientConfig
config
)
{
super
(
channelId
,
PayChannelEnum
.
WX_BAR
.
getCode
(),
config
);
}
@Override
protected
void
doInit
()
{
super
.
doInit
(
WxPayConstants
.
TradeType
.
MICROPAY
);
}
@Override
protected
PayOrderRespDTO
doUnifiedOrderV2
(
PayOrderUnifiedReqDTO
reqDTO
)
throws
WxPayException
{
// 由于付款码需要不断轮询,所以需要在较短的时间完成支付
LocalDateTime
expireTime
=
LocalDateTimeUtils
.
addTime
(
AUTH_CODE_EXPIRE
);
if
(
expireTime
.
isAfter
(
reqDTO
.
getExpireTime
()))
{
expireTime
=
reqDTO
.
getExpireTime
();
}
// 构建 WxPayMicropayRequest 对象
WxPayMicropayRequest
request
=
WxPayMicropayRequest
.
newBuilder
()
.
outTradeNo
(
reqDTO
.
getOutTradeNo
())
.
body
(
reqDTO
.
getSubject
())
.
detail
(
reqDTO
.
getBody
())
.
totalFee
(
reqDTO
.
getPrice
())
// 单位分
.
timeExpire
(
formatDateV2
(
expireTime
))
.
spbillCreateIp
(
reqDTO
.
getUserIp
())
.
authCode
(
getAuthCode
(
reqDTO
))
.
build
();
// 执行请求,重试直到失败(过期),或者成功
WxPayException
lastWxPayException
=
null
;
for
(
int
i
=
1
;
i
<
Byte
.
MAX_VALUE
;
i
++)
{
try
{
WxPayMicropayResult
response
=
client
.
micropay
(
request
);
// 支付成功,例如说:1)用户输入了密码;2)用户免密支付
return
PayOrderRespDTO
.
successOf
(
response
.
getTransactionId
(),
response
.
getOpenid
(),
parseDateV2
(
response
.
getTimeEnd
()),
response
.
getOutTradeNo
(),
response
)
.
setDisplayMode
(
PayOrderDisplayModeEnum
.
BAR_CODE
.
getMode
());
}
catch
(
WxPayException
ex
)
{
lastWxPayException
=
ex
;
// 如果不满足这 3 种任一的,则直接抛出 WxPayException 异常,不仅需处理
// 1. SYSTEMERROR:接口返回错误:请立即调用被扫订单结果查询API,查询当前订单状态,并根据订单的状态决定下一步的操作。
// 2. USERPAYING:用户支付中,需要输入密码:等待 5 秒,然后调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。
// 3. BANKERROR:银行系统异常:请立即调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。
if
(!
StrUtil
.
equalsAny
(
ex
.
getErrCode
(),
"SYSTEMERROR"
,
"USERPAYING"
,
"BANKERROR"
))
{
throw
ex
;
}
// 等待 5 秒,继续下一轮重新发起支付
log
.
info
(
"[doUnifiedOrderV2][发起微信 Bar 支付第({})失败,等待下一轮重试,请求({}),响应({})]"
,
i
,
toJsonString
(
request
),
ex
.
getMessage
());
ThreadUtil
.
sleep
(
5
,
TimeUnit
.
SECONDS
);
}
}
throw
lastWxPayException
;
}
@Override
protected
PayOrderRespDTO
doUnifiedOrderV3
(
PayOrderUnifiedReqDTO
reqDTO
)
throws
WxPayException
{
return
doUnifiedOrderV2
(
reqDTO
);
}
// ========== 各种工具方法 ==========
static
String
getAuthCode
(
PayOrderUnifiedReqDTO
reqDTO
)
{
String
authCode
=
MapUtil
.
getStr
(
reqDTO
.
getChannelExtras
(),
"authCode"
);
if
(
StrUtil
.
isEmpty
(
authCode
))
{
throw
invalidParamException
(
"支付请求的 authCode 不能为空!"
);
}
return
authCode
;
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/impl/weixin/WxLitePayClient.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
.
weixin
;
import
lombok.extern.slf4j.Slf4j
;
import
org.dromara.common.pay.core.enums.channel.PayChannelEnum
;
/**
* 微信支付【小程序】的 PayClient 实现类
*
* 由于公众号和小程序的微信支付逻辑一致,所以直接进行继承
*
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml">JSAPI 下单</>
*
* @author zwy
*/
@Slf4j
public
class
WxLitePayClient
extends
WxPubPayClient
{
public
WxLitePayClient
(
Long
channelId
,
WxPayClientConfig
config
)
{
super
(
channelId
,
PayChannelEnum
.
WX_LITE
.
getCode
(),
config
);
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/impl/weixin/WxNativePayClient.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
.
weixin
;
import
com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult
;
import
com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest
;
import
com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request
;
import
com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum
;
import
com.github.binarywang.wxpay.constant.WxPayConstants
;
import
com.github.binarywang.wxpay.exception.WxPayException
;
import
lombok.extern.slf4j.Slf4j
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO
;
import
org.dromara.common.pay.core.enums.channel.PayChannelEnum
;
import
org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum
;
/**
* 微信支付【Native 二维码】的 PayClient 实现类
*
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml">Native 下单</a>
*
* @author zwy
*/
@Slf4j
public
class
WxNativePayClient
extends
AbstractWxPayClient
{
public
WxNativePayClient
(
Long
channelId
,
WxPayClientConfig
config
)
{
super
(
channelId
,
PayChannelEnum
.
WX_NATIVE
.
getCode
(),
config
);
}
@Override
protected
void
doInit
()
{
super
.
doInit
(
WxPayConstants
.
TradeType
.
NATIVE
);
}
@Override
protected
PayOrderRespDTO
doUnifiedOrderV2
(
PayOrderUnifiedReqDTO
reqDTO
)
throws
WxPayException
{
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest
request
=
buildPayUnifiedOrderRequestV2
(
reqDTO
)
.
setProductId
(
reqDTO
.
getOutTradeNo
());
// V2 必须传递 productId,无需在微信配置。该参数在 V3 简化,无需传递!
// 执行请求
WxPayNativeOrderResult
response
=
client
.
createOrder
(
request
);
// 转换结果
return
PayOrderRespDTO
.
waitingOf
(
PayOrderDisplayModeEnum
.
QR_CODE
.
getMode
(),
response
.
getCodeUrl
(),
reqDTO
.
getOutTradeNo
(),
response
);
}
@Override
protected
PayOrderRespDTO
doUnifiedOrderV3
(
PayOrderUnifiedReqDTO
reqDTO
)
throws
WxPayException
{
// 构建 WxPayUnifiedOrderV3Request 对象
WxPayUnifiedOrderV3Request
request
=
buildPayUnifiedOrderRequestV3
(
reqDTO
);
// 执行请求
String
response
=
client
.
createOrderV3
(
TradeTypeEnum
.
NATIVE
,
request
);
// 转换结果
return
PayOrderRespDTO
.
waitingOf
(
PayOrderDisplayModeEnum
.
QR_CODE
.
getMode
(),
response
,
reqDTO
.
getOutTradeNo
(),
response
);
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/impl/weixin/WxPayClientConfig.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
.
weixin
;
import
jakarta.validation.Validator
;
import
jakarta.validation.constraints.NotBlank
;
import
lombok.Data
;
import
org.dromara.common.mall.util.validation.ValidationUtils
;
import
org.dromara.common.pay.core.client.PayClientConfig
;
/**
* 微信支付的 PayClientConfig 实现类
* 属性主要来自 {@link com.github.binarywang.wxpay.config.WxPayConfig} 的必要属性
*
* @author 芋道源码
*/
@Data
public
class
WxPayClientConfig
implements
PayClientConfig
{
/**
* API 版本 - V2
*
* <a href="https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_1">V2 协议说明</a>
*/
public
static
final
String
API_VERSION_V2
=
"v2"
;
/**
* API 版本 - V3
*
* <a href="https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay-1.shtml">V3 协议说明</a>
*/
public
static
final
String
API_VERSION_V3
=
"v3"
;
/**
* 公众号或者小程序的 appid
*
* 只有公众号或小程序需要该字段
*/
@NotBlank
(
message
=
"APPID 不能为空"
,
groups
=
{
V2
.
class
,
V3
.
class
})
private
String
appId
;
/**
* 商户号
*/
@NotBlank
(
message
=
"商户号不能为空"
,
groups
=
{
V2
.
class
,
V3
.
class
})
private
String
mchId
;
/**
* API 版本
*/
@NotBlank
(
message
=
"API 版本不能为空"
,
groups
=
{
V2
.
class
,
V3
.
class
})
private
String
apiVersion
;
// ========== V2 版本的参数 ==========
/**
* 商户密钥
*/
@NotBlank
(
message
=
"商户密钥不能为空"
,
groups
=
V2
.
class
)
private
String
mchKey
;
/**
* apiclient_cert.p12 证书文件的对应字符串【base64 格式】
*
* 为什么采用 base64 格式?因为 p12 读取后是二进制,需要转换成 base64 格式才好传输和存储
*/
@NotBlank
(
message
=
"apiclient_cert.p12 不能为空"
,
groups
=
V2
.
class
)
private
String
keyContent
;
// ========== V3 版本的参数 ==========
/**
* apiclient_key.pem 证书文件的对应字符串
*/
@NotBlank
(
message
=
"apiclient_key 不能为空"
,
groups
=
V3
.
class
)
private
String
privateKeyContent
;
/**
* apiV3 密钥值
*/
@NotBlank
(
message
=
"apiV3 密钥值不能为空"
,
groups
=
V3
.
class
)
private
String
apiV3Key
;
/**
* 证书序列号
*/
@NotBlank
(
message
=
"证书序列号不能为空"
,
groups
=
V3
.
class
)
private
String
certSerialNo
;
@Deprecated
// TODO 芋艿:V2.3.0 进行移除
private
String
privateCertContent
;
/**
* 分组校验 v2版本
*/
public
interface
V2
{
}
/**
* 分组校验 v3版本
*/
public
interface
V3
{
}
@Override
public
void
validate
(
Validator
validator
)
{
ValidationUtils
.
validate
(
validator
,
this
,
API_VERSION_V2
.
equals
(
this
.
getApiVersion
())
?
V2
.
class
:
V3
.
class
);
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/impl/weixin/WxPubPayClient.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
.
weixin
;
import
cn.hutool.core.map.MapUtil
;
import
cn.hutool.core.util.StrUtil
;
import
com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult
;
import
com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest
;
import
com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request
;
import
com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result
;
import
com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum
;
import
com.github.binarywang.wxpay.constant.WxPayConstants
;
import
com.github.binarywang.wxpay.exception.WxPayException
;
import
lombok.extern.slf4j.Slf4j
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO
;
import
org.dromara.common.pay.core.enums.channel.PayChannelEnum
;
import
org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum
;
import
static
org
.
dromara
.
common
.
core
.
utils
.
json
.
JsonUtils
.
toJsonString
;
import
static
org
.
dromara
.
common
.
mall
.
exception
.
util
.
ServiceExceptionUtil
.
invalidParamException
;
/**
* 微信支付(公众号)的 PayClient 实现类
*
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml">JSAPI 下单</>
*
* @author 芋道源码
*/
@Slf4j
public
class
WxPubPayClient
extends
AbstractWxPayClient
{
public
WxPubPayClient
(
Long
channelId
,
WxPayClientConfig
config
)
{
super
(
channelId
,
PayChannelEnum
.
WX_PUB
.
getCode
(),
config
);
}
protected
WxPubPayClient
(
Long
channelId
,
String
channelCode
,
WxPayClientConfig
config
)
{
super
(
channelId
,
channelCode
,
config
);
}
@Override
protected
void
doInit
()
{
super
.
doInit
(
WxPayConstants
.
TradeType
.
JSAPI
);
}
@Override
protected
PayOrderRespDTO
doUnifiedOrderV2
(
PayOrderUnifiedReqDTO
reqDTO
)
throws
WxPayException
{
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest
request
=
buildPayUnifiedOrderRequestV2
(
reqDTO
)
.
setOpenid
(
getOpenid
(
reqDTO
));
// 执行请求
WxPayMpOrderResult
response
=
client
.
createOrder
(
request
);
// 转换结果
return
PayOrderRespDTO
.
waitingOf
(
PayOrderDisplayModeEnum
.
APP
.
getMode
(),
toJsonString
(
response
),
reqDTO
.
getOutTradeNo
(),
response
);
}
@Override
protected
PayOrderRespDTO
doUnifiedOrderV3
(
PayOrderUnifiedReqDTO
reqDTO
)
throws
WxPayException
{
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderV3Request
request
=
buildPayUnifiedOrderRequestV3
(
reqDTO
)
.
setPayer
(
new
WxPayUnifiedOrderV3Request
.
Payer
().
setOpenid
(
getOpenid
(
reqDTO
)));
// 执行请求
WxPayUnifiedOrderV3Result
.
JsapiResult
response
=
client
.
createOrderV3
(
TradeTypeEnum
.
JSAPI
,
request
);
// 转换结果
return
PayOrderRespDTO
.
waitingOf
(
PayOrderDisplayModeEnum
.
APP
.
getMode
(),
toJsonString
(
response
),
reqDTO
.
getOutTradeNo
(),
response
);
}
// ========== 各种工具方法 ==========
static
String
getOpenid
(
PayOrderUnifiedReqDTO
reqDTO
)
{
String
openid
=
MapUtil
.
getStr
(
reqDTO
.
getChannelExtras
(),
"openid"
);
if
(
StrUtil
.
isEmpty
(
openid
))
{
throw
invalidParamException
(
"支付请求的 openid 不能为空!"
);
}
return
openid
;
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/client/impl/weixin/WxWapPayClient.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
client
.
impl
.
weixin
;
import
com.github.binarywang.wxpay.bean.order.WxPayMwebOrderResult
;
import
com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest
;
import
com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request
;
import
com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum
;
import
com.github.binarywang.wxpay.constant.WxPayConstants
;
import
com.github.binarywang.wxpay.exception.WxPayException
;
import
lombok.extern.slf4j.Slf4j
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderRespDTO
;
import
org.dromara.common.pay.core.client.dto.order.PayOrderUnifiedReqDTO
;
import
org.dromara.common.pay.core.enums.channel.PayChannelEnum
;
import
org.dromara.common.pay.core.enums.order.PayOrderDisplayModeEnum
;
/**
* 微信支付(H5 网页)的 PayClient 实现类
*
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_3_1.shtml">H5下单API</>
*
* @author YYQ
*/
@Slf4j
public
class
WxWapPayClient
extends
AbstractWxPayClient
{
public
WxWapPayClient
(
Long
channelId
,
WxPayClientConfig
config
)
{
super
(
channelId
,
PayChannelEnum
.
WX_WAP
.
getCode
(),
config
);
}
protected
WxWapPayClient
(
Long
channelId
,
String
channelCode
,
WxPayClientConfig
config
)
{
super
(
channelId
,
channelCode
,
config
);
}
@Override
protected
void
doInit
()
{
super
.
doInit
(
WxPayConstants
.
TradeType
.
MWEB
);
}
@Override
protected
PayOrderRespDTO
doUnifiedOrderV2
(
PayOrderUnifiedReqDTO
reqDTO
)
throws
WxPayException
{
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest
request
=
buildPayUnifiedOrderRequestV2
(
reqDTO
);
// 执行请求
WxPayMwebOrderResult
response
=
client
.
createOrder
(
request
);
// 转换结果
return
PayOrderRespDTO
.
waitingOf
(
PayOrderDisplayModeEnum
.
URL
.
getMode
(),
response
.
getMwebUrl
(),
reqDTO
.
getOutTradeNo
(),
response
);
}
@Override
protected
PayOrderRespDTO
doUnifiedOrderV3
(
PayOrderUnifiedReqDTO
reqDTO
)
throws
WxPayException
{
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderV3Request
request
=
buildPayUnifiedOrderRequestV3
(
reqDTO
);
// 执行请求
String
response
=
client
.
createOrderV3
(
TradeTypeEnum
.
H5
,
request
);
// 转换结果
return
PayOrderRespDTO
.
waitingOf
(
PayOrderDisplayModeEnum
.
URL
.
getMode
(),
response
,
reqDTO
.
getOutTradeNo
(),
response
);
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/enums/channel/PayChannelEnum.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
enums
.
channel
;
import
cn.hutool.core.util.ArrayUtil
;
import
lombok.AllArgsConstructor
;
import
lombok.Getter
;
import
org.dromara.common.pay.core.client.PayClientConfig
;
import
org.dromara.common.pay.core.client.impl.NonePayClientConfig
;
import
org.dromara.common.pay.core.client.impl.alipay.AlipayPayClientConfig
;
import
org.dromara.common.pay.core.client.impl.weixin.WxPayClientConfig
;
/**
* 支付渠道的编码的枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public
enum
PayChannelEnum
{
WX_PUB
(
"wx_pub"
,
"微信 JSAPI 支付"
,
WxPayClientConfig
.
class
),
// 公众号网页
WX_LITE
(
"wx_lite"
,
"微信小程序支付"
,
WxPayClientConfig
.
class
),
WX_APP
(
"wx_app"
,
"微信 App 支付"
,
WxPayClientConfig
.
class
),
WX_NATIVE
(
"wx_native"
,
"微信 Native 支付"
,
WxPayClientConfig
.
class
),
WX_WAP
(
"wx_wap"
,
"微信 Wap 网站支付"
,
WxPayClientConfig
.
class
),
// H5 网页
WX_BAR
(
"wx_bar"
,
"微信付款码支付"
,
WxPayClientConfig
.
class
),
ALIPAY_PC
(
"alipay_pc"
,
"支付宝 PC 网站支付"
,
AlipayPayClientConfig
.
class
),
ALIPAY_WAP
(
"alipay_wap"
,
"支付宝 Wap 网站支付"
,
AlipayPayClientConfig
.
class
),
ALIPAY_APP
(
"alipay_app"
,
"支付宝App 支付"
,
AlipayPayClientConfig
.
class
),
ALIPAY_QR
(
"alipay_qr"
,
"支付宝扫码支付"
,
AlipayPayClientConfig
.
class
),
ALIPAY_BAR
(
"alipay_bar"
,
"支付宝条码支付"
,
AlipayPayClientConfig
.
class
),
MOCK
(
"mock"
,
"模拟支付"
,
NonePayClientConfig
.
class
),
WALLET
(
"wallet"
,
"钱包支付"
,
NonePayClientConfig
.
class
);
/**
* 编码
*
* 参考 <a href="https://www.pingxx.com/api/支付渠道属性值.html">支付渠道属性值</a>
*/
private
final
String
code
;
/**
* 名字
*/
private
final
String
name
;
/**
* 配置类
*/
private
final
Class
<?
extends
PayClientConfig
>
configClass
;
/**
* 微信支付
*/
public
static
final
String
WECHAT
=
"WECHAT"
;
/**
* 支付宝支付
*/
public
static
final
String
ALIPAY
=
"ALIPAY"
;
public
static
PayChannelEnum
getByCode
(
String
code
)
{
return
ArrayUtil
.
firstMatch
(
o
->
o
.
getCode
().
equals
(
code
),
values
());
}
public
static
boolean
isAlipay
(
String
channelCode
)
{
return
channelCode
!=
null
&&
channelCode
.
startsWith
(
"alipay"
);
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/enums/order/PayOrderDisplayModeEnum.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
enums
.
order
;
import
lombok.AllArgsConstructor
;
import
lombok.Getter
;
/**
* 支付 UI 展示模式
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public
enum
PayOrderDisplayModeEnum
{
URL
(
"url"
),
// Redirect 跳转链接的方式
IFRAME
(
"iframe"
),
// IFrame 内嵌链接的方式【目前暂时用不到】
FORM
(
"form"
),
// HTML 表单提交
QR_CODE
(
"qr_code"
),
// 二维码的文字内容
QR_CODE_URL
(
"qr_code_url"
),
// 二维码的图片链接
BAR_CODE
(
"bar_code"
),
// 条形码
APP
(
"app"
),
// 应用:Android、iOS、微信小程序、微信公众号等,需要做自定义处理的
;
/**
* 展示模式
*/
private
final
String
mode
;
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/enums/order/PayOrderStatusRespEnum.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
enums
.
order
;
import
lombok.AllArgsConstructor
;
import
lombok.Getter
;
import
java.util.Objects
;
/**
* 渠道的支付状态枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public
enum
PayOrderStatusRespEnum
{
WAITING
(
0
,
"未支付"
),
SUCCESS
(
10
,
"支付成功"
),
REFUND
(
20
,
"已退款"
),
CLOSED
(
30
,
"支付关闭"
),
;
private
final
Integer
status
;
private
final
String
name
;
/**
* 判断是否支付成功
*
* @param status 状态
* @return 是否支付成功
*/
public
static
boolean
isSuccess
(
Integer
status
)
{
return
Objects
.
equals
(
status
,
SUCCESS
.
getStatus
());
}
/**
* 判断是否已退款
*
* @param status 状态
* @return 是否支付成功
*/
public
static
boolean
isRefund
(
Integer
status
)
{
return
Objects
.
equals
(
status
,
REFUND
.
getStatus
());
}
/**
* 判断是否支付关闭
*
* @param status 状态
* @return 是否支付关闭
*/
public
static
boolean
isClosed
(
Integer
status
)
{
return
Objects
.
equals
(
status
,
CLOSED
.
getStatus
());
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/enums/refund/PayRefundStatusRespEnum.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
enums
.
refund
;
import
lombok.AllArgsConstructor
;
import
lombok.Getter
;
import
java.util.Objects
;
/**
* 渠道的退款状态枚举
*
* @author jason
*/
@Getter
@AllArgsConstructor
public
enum
PayRefundStatusRespEnum
{
WAITING
(
0
,
"等待退款"
),
SUCCESS
(
10
,
"退款成功"
),
FAILURE
(
20
,
"退款失败"
);
private
final
Integer
status
;
private
final
String
name
;
public
static
boolean
isSuccess
(
Integer
status
)
{
return
Objects
.
equals
(
status
,
SUCCESS
.
getStatus
());
}
public
static
boolean
isFailure
(
Integer
status
)
{
return
Objects
.
equals
(
status
,
FAILURE
.
getStatus
());
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/enums/transfer/PayTransferStatusRespEnum.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
enums
.
transfer
;
import
lombok.AllArgsConstructor
;
import
lombok.Getter
;
import
java.util.Objects
;
/**
* 渠道的转账状态枚举
*
* @author jason
*/
@Getter
@AllArgsConstructor
public
enum
PayTransferStatusRespEnum
{
WAITING
(
0
,
"等待转账"
),
/**
* TODO 转账到银行卡. 会有T+0 T+1 到账的请情况。 还未实现
* TODO @jason:可以看看其它开源项目,针对这个场景,处理策略是怎么样的?例如说,每天主动轮询?这个状态的单子?
*/
IN_PROGRESS
(
10
,
"转账进行中"
),
SUCCESS
(
20
,
"转账成功"
),
/**
* 转账关闭 (失败,或者其它情况)
*/
CLOSED
(
30
,
"转账关闭"
);
private
final
Integer
status
;
private
final
String
name
;
public
static
boolean
isSuccess
(
Integer
status
)
{
return
Objects
.
equals
(
status
,
SUCCESS
.
getStatus
());
}
public
static
boolean
isClosed
(
Integer
status
)
{
return
Objects
.
equals
(
status
,
CLOSED
.
getStatus
());
}
public
static
boolean
isInProgress
(
Integer
status
)
{
return
Objects
.
equals
(
status
,
IN_PROGRESS
.
getStatus
());
}
}
ruoyi-common/ruoyi-common-allPay/src/main/java/org/dromara/common/pay/core/enums/transfer/PayTransferTypeEnum.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
pay
.
core
.
enums
.
transfer
;
import
cn.hutool.core.util.ArrayUtil
;
import
lombok.AllArgsConstructor
;
import
lombok.Getter
;
import
org.dromara.common.mall.core.IntArrayValuable
;
import
java.util.Arrays
;
/**
* 转账类型枚举
*
* @author jason
*/
@AllArgsConstructor
@Getter
public
enum
PayTransferTypeEnum
implements
IntArrayValuable
{
ALIPAY_BALANCE
(
1
,
"支付宝余额"
),
WX_BALANCE
(
2
,
"微信余额"
),
BANK_CARD
(
3
,
"银行卡"
),
WALLET_BALANCE
(
4
,
"钱包余额"
);
public
interface
WxPay
{
}
public
interface
Alipay
{
}
private
final
Integer
type
;
private
final
String
name
;
public
static
final
int
[]
ARRAYS
=
Arrays
.
stream
(
values
()).
mapToInt
(
PayTransferTypeEnum:
:
getType
).
toArray
();
@Override
public
int
[]
array
()
{
return
ARRAYS
;
}
public
static
PayTransferTypeEnum
typeOf
(
Integer
type
)
{
return
ArrayUtil
.
firstMatch
(
item
->
item
.
getType
().
equals
(
type
),
values
());
}
}
ruoyi-common/ruoyi-common-allPay/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
0 → 100644
浏览文件 @
44629dc1
org.dromara.common.pay.config.YudaoPayAutoConfiguration
ruoyi-common/ruoyi-common-bom/pom.xml
浏览文件 @
44629dc1
...
@@ -278,6 +278,27 @@
...
@@ -278,6 +278,27 @@
<version>
${revision}
</version>
<version>
${revision}
</version>
</dependency>
</dependency>
<!-- 聚合支付 -->
<dependency>
<groupId>
org.dromara
</groupId>
<artifactId>
ruoyi-common-allPay
</artifactId>
<version>
${revision}
</version>
</dependency>
<!-- 商城 -->
<dependency>
<groupId>
org.dromara
</groupId>
<artifactId>
ruoyi-common-mall
</artifactId>
<version>
${revision}
</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>
org.dromara
</groupId>
<artifactId>
ruoyi-common-yudao-mybatis
</artifactId>
<version>
${revision}
</version>
</dependency>
</dependencies>
</dependencies>
</dependencyManagement>
</dependencyManagement>
</project>
</project>
ruoyi-common/ruoyi-common-core/pom.xml
浏览文件 @
44629dc1
...
@@ -99,6 +99,21 @@
...
@@ -99,6 +99,21 @@
<artifactId>
ip2region
</artifactId>
<artifactId>
ip2region
</artifactId>
</dependency>
</dependency>
<dependency>
<groupId>
com.fasterxml.jackson.core
</groupId>
<artifactId>
jackson-databind
</artifactId>
</dependency>
<dependency>
<groupId>
com.fasterxml.jackson.datatype
</groupId>
<artifactId>
jackson-datatype-jsr310
</artifactId>
</dependency>
<dependency>
<groupId>
cn.hutool
</groupId>
<artifactId>
hutool-json
</artifactId>
</dependency>
</dependencies>
</dependencies>
</project>
</project>
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/FileUtils.java
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
core
.
utils
.
file
;
package
org
.
dromara
.
common
.
core
.
utils
.
file
;
import
cn.hutool.core.io.FileUtil
;
import
cn.hutool.core.io.FileUtil
;
import
cn.hutool.core.util.IdUtil
;
import
jakarta.servlet.http.HttpServletResponse
;
import
jakarta.servlet.http.HttpServletResponse
;
import
lombok.AccessLevel
;
import
lombok.AccessLevel
;
import
lombok.NoArgsConstructor
;
import
lombok.NoArgsConstructor
;
import
lombok.SneakyThrows
;
import
java.io.File
;
import
java.net.URLEncoder
;
import
java.net.URLEncoder
;
import
java.nio.charset.StandardCharsets
;
import
java.nio.charset.StandardCharsets
;
...
@@ -40,4 +43,49 @@ public class FileUtils extends FileUtil {
...
@@ -40,4 +43,49 @@ public class FileUtils extends FileUtil {
String
encode
=
URLEncoder
.
encode
(
s
,
StandardCharsets
.
UTF_8
);
String
encode
=
URLEncoder
.
encode
(
s
,
StandardCharsets
.
UTF_8
);
return
encode
.
replaceAll
(
"\\+"
,
"%20"
);
return
encode
.
replaceAll
(
"\\+"
,
"%20"
);
}
}
/**
* 创建临时文件
* 该文件会在 JVM 退出时,进行删除
*
* @param data 文件内容
* @return 文件
*/
@SneakyThrows
public
static
File
createTempFile
(
byte
[]
data
)
{
File
file
=
createTempFile
();
// 写入内容
FileUtil
.
writeBytes
(
data
,
file
);
return
file
;
}
/**
* 创建临时文件,无内容
* 该文件会在 JVM 退出时,进行删除
*
* @return 文件
*/
@SneakyThrows
public
static
File
createTempFile
()
{
// 创建文件,通过 UUID 保证唯一
File
file
=
File
.
createTempFile
(
IdUtil
.
simpleUUID
(),
null
);
// 标记 JVM 退出时,自动删除
file
.
deleteOnExit
();
return
file
;
}
/**
* 创建临时文件
* 该文件会在 JVM 退出时,进行删除
*
* @param data 文件内容
* @return 文件
*/
@SneakyThrows
public
static
File
createTempFile
(
String
data
)
{
File
file
=
createTempFile
();
// 写入内容
FileUtil
.
writeUtf8String
(
data
,
file
);
return
file
;
}
}
}
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/json/JsonUtils.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
core
.
utils
.
json
;
import
cn.hutool.core.util.ArrayUtil
;
import
cn.hutool.core.util.StrUtil
;
import
cn.hutool.json.JSONUtil
;
import
com.fasterxml.jackson.annotation.JsonInclude
;
import
com.fasterxml.jackson.core.type.TypeReference
;
import
com.fasterxml.jackson.databind.DeserializationFeature
;
import
com.fasterxml.jackson.databind.JsonNode
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
com.fasterxml.jackson.databind.SerializationFeature
;
import
com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
;
import
lombok.SneakyThrows
;
import
lombok.extern.slf4j.Slf4j
;
import
java.io.IOException
;
import
java.lang.reflect.Type
;
import
java.util.ArrayList
;
import
java.util.List
;
/**
* JSON 工具类
*
* @author 芋道源码
*/
@Slf4j
public
class
JsonUtils
{
private
static
ObjectMapper
objectMapper
=
new
ObjectMapper
();
static
{
objectMapper
.
configure
(
SerializationFeature
.
FAIL_ON_EMPTY_BEANS
,
false
);
objectMapper
.
configure
(
DeserializationFeature
.
FAIL_ON_UNKNOWN_PROPERTIES
,
false
);
objectMapper
.
setSerializationInclusion
(
JsonInclude
.
Include
.
NON_NULL
);
// 忽略 null 值
objectMapper
.
registerModules
(
new
JavaTimeModule
());
// 解决 LocalDateTime 的序列化
}
/**
* 初始化 objectMapper 属性
* <p>
* 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean
*
* @param objectMapper ObjectMapper 对象
*/
public
static
void
init
(
ObjectMapper
objectMapper
)
{
JsonUtils
.
objectMapper
=
objectMapper
;
}
@SneakyThrows
public
static
String
toJsonString
(
Object
object
)
{
return
objectMapper
.
writeValueAsString
(
object
);
}
@SneakyThrows
public
static
byte
[]
toJsonByte
(
Object
object
)
{
return
objectMapper
.
writeValueAsBytes
(
object
);
}
@SneakyThrows
public
static
String
toJsonPrettyString
(
Object
object
)
{
return
objectMapper
.
writerWithDefaultPrettyPrinter
().
writeValueAsString
(
object
);
}
public
static
<
T
>
T
parseObject
(
String
text
,
Class
<
T
>
clazz
)
{
if
(
StrUtil
.
isEmpty
(
text
))
{
return
null
;
}
try
{
return
objectMapper
.
readValue
(
text
,
clazz
);
}
catch
(
IOException
e
)
{
log
.
error
(
"json parse err,json:{}"
,
text
,
e
);
throw
new
RuntimeException
(
e
);
}
}
public
static
<
T
>
T
parseObject
(
String
text
,
String
path
,
Class
<
T
>
clazz
)
{
if
(
StrUtil
.
isEmpty
(
text
))
{
return
null
;
}
try
{
JsonNode
treeNode
=
objectMapper
.
readTree
(
text
);
JsonNode
pathNode
=
treeNode
.
path
(
path
);
return
objectMapper
.
readValue
(
pathNode
.
toString
(),
clazz
);
}
catch
(
IOException
e
)
{
log
.
error
(
"json parse err,json:{}"
,
text
,
e
);
throw
new
RuntimeException
(
e
);
}
}
public
static
<
T
>
T
parseObject
(
String
text
,
Type
type
)
{
if
(
StrUtil
.
isEmpty
(
text
))
{
return
null
;
}
try
{
return
objectMapper
.
readValue
(
text
,
objectMapper
.
getTypeFactory
().
constructType
(
type
));
}
catch
(
IOException
e
)
{
log
.
error
(
"json parse err,json:{}"
,
text
,
e
);
throw
new
RuntimeException
(
e
);
}
}
/**
* 将字符串解析成指定类型的对象
* 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下,
* 如果 text 没有 class 属性,则会报错。此时,使用这个方法,可以解决。
*
* @param text 字符串
* @param clazz 类型
* @return 对象
*/
public
static
<
T
>
T
parseObject2
(
String
text
,
Class
<
T
>
clazz
)
{
if
(
StrUtil
.
isEmpty
(
text
))
{
return
null
;
}
return
JSONUtil
.
toBean
(
text
,
clazz
);
}
public
static
<
T
>
T
parseObject
(
byte
[]
bytes
,
Class
<
T
>
clazz
)
{
if
(
ArrayUtil
.
isEmpty
(
bytes
))
{
return
null
;
}
try
{
return
objectMapper
.
readValue
(
bytes
,
clazz
);
}
catch
(
IOException
e
)
{
log
.
error
(
"json parse err,json:{}"
,
bytes
,
e
);
throw
new
RuntimeException
(
e
);
}
}
public
static
<
T
>
T
parseObject
(
String
text
,
TypeReference
<
T
>
typeReference
)
{
try
{
return
objectMapper
.
readValue
(
text
,
typeReference
);
}
catch
(
IOException
e
)
{
log
.
error
(
"json parse err,json:{}"
,
text
,
e
);
throw
new
RuntimeException
(
e
);
}
}
/**
* 解析 JSON 字符串成指定类型的对象,如果解析失败,则返回 null
*
* @param text 字符串
* @param typeReference 类型引用
* @return 指定类型的对象
*/
public
static
<
T
>
T
parseObjectQuietly
(
String
text
,
TypeReference
<
T
>
typeReference
)
{
try
{
return
objectMapper
.
readValue
(
text
,
typeReference
);
}
catch
(
IOException
e
)
{
return
null
;
}
}
public
static
<
T
>
List
<
T
>
parseArray
(
String
text
,
Class
<
T
>
clazz
)
{
if
(
StrUtil
.
isEmpty
(
text
))
{
return
new
ArrayList
<>();
}
try
{
return
objectMapper
.
readValue
(
text
,
objectMapper
.
getTypeFactory
().
constructCollectionType
(
List
.
class
,
clazz
));
}
catch
(
IOException
e
)
{
log
.
error
(
"json parse err,json:{}"
,
text
,
e
);
throw
new
RuntimeException
(
e
);
}
}
public
static
<
T
>
List
<
T
>
parseArray
(
String
text
,
String
path
,
Class
<
T
>
clazz
)
{
if
(
StrUtil
.
isEmpty
(
text
))
{
return
null
;
}
try
{
JsonNode
treeNode
=
objectMapper
.
readTree
(
text
);
JsonNode
pathNode
=
treeNode
.
path
(
path
);
return
objectMapper
.
readValue
(
pathNode
.
toString
(),
objectMapper
.
getTypeFactory
().
constructCollectionType
(
List
.
class
,
clazz
));
}
catch
(
IOException
e
)
{
log
.
error
(
"json parse err,json:{}"
,
text
,
e
);
throw
new
RuntimeException
(
e
);
}
}
public
static
JsonNode
parseTree
(
String
text
)
{
try
{
return
objectMapper
.
readTree
(
text
);
}
catch
(
IOException
e
)
{
log
.
error
(
"json parse err,json:{}"
,
text
,
e
);
throw
new
RuntimeException
(
e
);
}
}
public
static
JsonNode
parseTree
(
byte
[]
text
)
{
try
{
return
objectMapper
.
readTree
(
text
);
}
catch
(
IOException
e
)
{
log
.
error
(
"json parse err,json:{}"
,
text
,
e
);
throw
new
RuntimeException
(
e
);
}
}
public
static
boolean
isJson
(
String
text
)
{
return
JSONUtil
.
isTypeJSON
(
text
);
}
}
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/json/databind/NumberSerializer.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
core
.
utils
.
json
.
databind
;
import
com.fasterxml.jackson.core.JsonGenerator
;
import
com.fasterxml.jackson.databind.SerializerProvider
;
import
com.fasterxml.jackson.databind.annotation.JacksonStdImpl
;
import
java.io.IOException
;
/**
* Long 序列化规则
*
* 会将超长 long 值转换为 string,解决前端 JavaScript 最大安全整数是 2^53-1 的问题
*
* @author 星语
*/
@JacksonStdImpl
public
class
NumberSerializer
extends
com
.
fasterxml
.
jackson
.
databind
.
ser
.
std
.
NumberSerializer
{
private
static
final
long
MAX_SAFE_INTEGER
=
9007199254740991L
;
private
static
final
long
MIN_SAFE_INTEGER
=
-
9007199254740991L
;
public
static
final
NumberSerializer
INSTANCE
=
new
NumberSerializer
(
Number
.
class
);
public
NumberSerializer
(
Class
<?
extends
Number
>
rawType
)
{
super
(
rawType
);
}
@Override
public
void
serialize
(
Number
value
,
JsonGenerator
gen
,
SerializerProvider
serializers
)
throws
IOException
{
// 超出范围 序列化位字符串
if
(
value
.
longValue
()
>
MIN_SAFE_INTEGER
&&
value
.
longValue
()
<
MAX_SAFE_INTEGER
)
{
super
.
serialize
(
value
,
gen
,
serializers
);
}
else
{
gen
.
writeString
(
value
.
toString
());
}
}
}
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/json/databind/TimestampLocalDateTimeDeserializer.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
core
.
utils
.
json
.
databind
;
import
com.fasterxml.jackson.core.JsonParser
;
import
com.fasterxml.jackson.databind.DeserializationContext
;
import
com.fasterxml.jackson.databind.JsonDeserializer
;
import
java.io.IOException
;
import
java.time.Instant
;
import
java.time.LocalDateTime
;
import
java.time.ZoneId
;
/**
* 基于时间戳的 LocalDateTime 反序列化器
*
* @author 老五
*/
public
class
TimestampLocalDateTimeDeserializer
extends
JsonDeserializer
<
LocalDateTime
>
{
public
static
final
TimestampLocalDateTimeDeserializer
INSTANCE
=
new
TimestampLocalDateTimeDeserializer
();
@Override
public
LocalDateTime
deserialize
(
JsonParser
p
,
DeserializationContext
ctxt
)
throws
IOException
{
// 将 Long 时间戳,转换为 LocalDateTime 对象
return
LocalDateTime
.
ofInstant
(
Instant
.
ofEpochMilli
(
p
.
getValueAsLong
()),
ZoneId
.
systemDefault
());
}
}
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/json/databind/TimestampLocalDateTimeSerializer.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
core
.
utils
.
json
.
databind
;
import
com.fasterxml.jackson.core.JsonGenerator
;
import
com.fasterxml.jackson.databind.JsonSerializer
;
import
com.fasterxml.jackson.databind.SerializerProvider
;
import
java.io.IOException
;
import
java.time.LocalDateTime
;
import
java.time.ZoneId
;
/**
* 基于时间戳的 LocalDateTime 序列化器
*
* @author 老五
*/
public
class
TimestampLocalDateTimeSerializer
extends
JsonSerializer
<
LocalDateTime
>
{
public
static
final
TimestampLocalDateTimeSerializer
INSTANCE
=
new
TimestampLocalDateTimeSerializer
();
@Override
public
void
serialize
(
LocalDateTime
value
,
JsonGenerator
gen
,
SerializerProvider
serializers
)
throws
IOException
{
// 将 LocalDateTime 对象,转换为 Long 时间戳
gen
.
writeNumber
(
value
.
atZone
(
ZoneId
.
systemDefault
()).
toInstant
().
toEpochMilli
());
}
}
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/string/StrUtils.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
core
.
utils
.
string
;
import
cn.hutool.core.text.StrPool
;
import
cn.hutool.core.util.ArrayUtil
;
import
cn.hutool.core.util.StrUtil
;
import
java.util.Arrays
;
import
java.util.Collection
;
import
java.util.List
;
import
java.util.Set
;
import
java.util.stream.Collectors
;
/**
* 字符串工具类
*
* @author 芋道源码
*/
public
class
StrUtils
{
public
static
String
maxLength
(
CharSequence
str
,
int
maxLength
)
{
return
StrUtil
.
maxLength
(
str
,
maxLength
-
3
);
// -3 的原因,是该方法会补充 ... 恰好
}
/**
* 给定字符串是否以任何一个字符串开始
* 给定字符串和数组为空都返回 false
*
* @param str 给定字符串
* @param prefixes 需要检测的开始字符串
* @since 3.0.6
*/
public
static
boolean
startWithAny
(
String
str
,
Collection
<
String
>
prefixes
)
{
if
(
StrUtil
.
isEmpty
(
str
)
||
ArrayUtil
.
isEmpty
(
prefixes
))
{
return
false
;
}
for
(
CharSequence
suffix
:
prefixes
)
{
if
(
StrUtil
.
startWith
(
str
,
suffix
,
false
))
{
return
true
;
}
}
return
false
;
}
public
static
List
<
Long
>
splitToLong
(
String
value
,
CharSequence
separator
)
{
long
[]
longs
=
StrUtil
.
splitToLong
(
value
,
separator
);
return
Arrays
.
stream
(
longs
).
boxed
().
collect
(
Collectors
.
toList
());
}
public
static
Set
<
Long
>
splitToLongSet
(
String
value
)
{
return
splitToLongSet
(
value
,
StrPool
.
COMMA
);
}
public
static
Set
<
Long
>
splitToLongSet
(
String
value
,
CharSequence
separator
)
{
long
[]
longs
=
StrUtil
.
splitToLong
(
value
,
separator
);
return
Arrays
.
stream
(
longs
).
boxed
().
collect
(
Collectors
.
toSet
());
}
public
static
List
<
Integer
>
splitToInteger
(
String
value
,
CharSequence
separator
)
{
int
[]
integers
=
StrUtil
.
splitToInt
(
value
,
separator
);
return
Arrays
.
stream
(
integers
).
boxed
().
collect
(
Collectors
.
toList
());
}
/**
* 移除字符串中,包含指定字符串的行
*
* @param content 字符串
* @param sequence 包含的字符串
* @return 移除后的字符串
*/
public
static
String
removeLineContains
(
String
content
,
String
sequence
)
{
if
(
StrUtil
.
isEmpty
(
content
)
||
StrUtil
.
isEmpty
(
sequence
))
{
return
content
;
}
return
Arrays
.
stream
(
content
.
split
(
"\n"
))
.
filter
(
line
->
!
line
.
contains
(
sequence
))
.
collect
(
Collectors
.
joining
(
"\n"
));
}
}
ruoyi-common/ruoyi-common-dict/src/main/java/org/dromara/common/dict/utils/DictUtils.java
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
dict
.
utils
;
package
org
.
dromara
.
common
.
dict
.
utils
;
import
io.github.linpeilie.utils.CollectionUtils
;
import
org.dromara.common.core.constant.CacheNames
;
import
org.dromara.common.core.constant.CacheNames
;
import
org.dromara.common.core.utils.StringUtils
;
import
org.dromara.common.redis.utils.CacheUtils
;
import
org.dromara.common.redis.utils.CacheUtils
;
import
org.dromara.system.api.domain.vo.RemoteDictDataVo
;
import
org.dromara.system.api.domain.vo.RemoteDictDataVo
;
...
@@ -48,4 +50,32 @@ public class DictUtils {
...
@@ -48,4 +50,32 @@ public class DictUtils {
CacheUtils
.
clear
(
CacheNames
.
SYS_DICT
);
CacheUtils
.
clear
(
CacheNames
.
SYS_DICT
);
}
}
/**
* 获取字典标签
*
* @param dictType 字典类型
* @param value value值
* @return 数据
*/
public
static
String
getDictDataLabel
(
String
dictType
,
String
value
)
{
List
<
RemoteDictDataVo
>
list
=
CacheUtils
.
get
(
CacheNames
.
SYS_DICT
,
dictType
);
return
CollectionUtils
.
isEmpty
(
list
)
?
StringUtils
.
EMPTY
:
list
.
stream
().
filter
(
item
->
StringUtils
.
equals
(
item
.
getDictValue
(),
value
)).
findFirst
().
map
(
RemoteDictDataVo:
:
getDictLabel
).
orElse
(
""
);
}
/**
* 获取字典标签
*
* @param dictType 字典类型
* @param value value值
* @return 数据
*/
public
static
String
getDictDataLabel
(
String
dictType
,
Integer
value
)
{
List
<
RemoteDictDataVo
>
list
=
CacheUtils
.
get
(
CacheNames
.
SYS_DICT
,
dictType
);
return
CollectionUtils
.
isEmpty
(
list
)
?
StringUtils
.
EMPTY
:
list
.
stream
().
filter
(
item
->
StringUtils
.
equals
(
item
.
getDictValue
(),
value
+
""
)).
findFirst
().
map
(
RemoteDictDataVo:
:
getDictLabel
).
orElse
(
""
);
}
}
}
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/convert/MoneyConvert.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
excel
.
convert
;
import
com.alibaba.excel.converters.Converter
;
import
com.alibaba.excel.enums.CellDataTypeEnum
;
import
com.alibaba.excel.metadata.GlobalConfiguration
;
import
com.alibaba.excel.metadata.data.WriteCellData
;
import
com.alibaba.excel.metadata.property.ExcelContentProperty
;
import
java.math.BigDecimal
;
import
java.math.RoundingMode
;
/**
* 金额转换器
*
* 金额单位:分
*
* @author 芋道源码
*/
public
class
MoneyConvert
implements
Converter
<
Integer
>
{
@Override
public
Class
<?>
supportJavaTypeKey
()
{
throw
new
UnsupportedOperationException
(
"暂不支持,也不需要"
);
}
@Override
public
CellDataTypeEnum
supportExcelTypeKey
()
{
throw
new
UnsupportedOperationException
(
"暂不支持,也不需要"
);
}
@Override
public
WriteCellData
<
String
>
convertToExcelData
(
Integer
value
,
ExcelContentProperty
contentProperty
,
GlobalConfiguration
globalConfiguration
)
{
BigDecimal
result
=
BigDecimal
.
valueOf
(
value
)
.
divide
(
new
BigDecimal
(
100
),
2
,
RoundingMode
.
HALF_UP
);
return
new
WriteCellData
<>(
result
.
toString
());
}
}
ruoyi-common/ruoyi-common-mall/pom.xml
0 → 100644
浏览文件 @
44629dc1
<?xml version="1.0" encoding="UTF-8"?>
<project
xmlns=
"http://maven.apache.org/POM/4.0.0"
xmlns:xsi=
"http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=
"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
>
<parent>
<groupId>
org.dromara
</groupId>
<artifactId>
ruoyi-common
</artifactId>
<version>
${revision}
</version>
</parent>
<modelVersion>
4.0.0
</modelVersion>
<artifactId>
ruoyi-common-mall
</artifactId>
<description>
ruoyi-common-mall 商城公共模块
</description>
<dependencies>
<dependency>
<groupId>
org.dromara
</groupId>
<artifactId>
ruoyi-common-core
</artifactId>
</dependency>
<dependency>
<groupId>
org.projectlombok
</groupId>
<artifactId>
lombok
</artifactId>
</dependency>
<dependency>
<groupId>
org.redisson
</groupId>
<artifactId>
redisson
</artifactId>
<version>
3.37.0
</version>
<scope>
compile
</scope>
</dependency>
<dependency>
<groupId>
org.springframework.data
</groupId>
<artifactId>
spring-data-redis
</artifactId>
</dependency>
<dependency>
<groupId>
com.google.guava
</groupId>
<artifactId>
guava
</artifactId>
<version>
33.2.0-jre
</version>
<scope>
compile
</scope>
</dependency>
</dependencies>
</project>
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/core/IntArrayValuable.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
core
;
/**
* 可生成 Int 数组的接口
*
* @author 芋道源码
*/
public
interface
IntArrayValuable
{
/**
* @return int 数组
*/
int
[]
array
();
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/core/KeyValue.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
core
;
import
lombok.AllArgsConstructor
;
import
lombok.Data
;
import
lombok.NoArgsConstructor
;
import
java.io.Serializable
;
/**
* Key Value 的键值对
*
* @author 芋道源码
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public
class
KeyValue
<
K
,
V
>
implements
Serializable
{
private
K
key
;
private
V
value
;
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/domain/Area.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
domain
;
import
com.fasterxml.jackson.annotation.JsonBackReference
;
import
com.fasterxml.jackson.annotation.JsonManagedReference
;
import
lombok.AllArgsConstructor
;
import
lombok.Data
;
import
lombok.NoArgsConstructor
;
import
lombok.ToString
;
import
lombok.experimental.Accessors
;
import
java.util.List
;
/**
* 区域节点,包括国家、省份、城市、地区等信息
*
* 数据可见 resources/area.csv 文件
*
* @author 芋道源码
*/
@Data
@Accessors
(
chain
=
true
)
@AllArgsConstructor
@NoArgsConstructor
@ToString
(
exclude
=
{
"parent"
})
// 参见 https://gitee.com/yudaocode/yudao-cloud-mini/pulls/2 原因
public
class
Area
{
/**
* 编号 - 全球,即根目录
*/
public
static
final
Integer
ID_GLOBAL
=
0
;
/**
* 编号 - 中国
*/
public
static
final
Integer
ID_CHINA
=
1
;
/**
* 编号
*/
private
Integer
id
;
/**
* 名字
*/
private
String
name
;
/**
* 类型
*
* 枚举 {@link AreaTypeEnum}
*/
private
Integer
type
;
/**
* 父节点
*/
@JsonManagedReference
private
Area
parent
;
/**
* 子节点
*/
@JsonBackReference
private
List
<
Area
>
children
;
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/enums/AreaTypeEnum.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
enums
;
import
lombok.AllArgsConstructor
;
import
lombok.Getter
;
import
org.dromara.common.mall.core.IntArrayValuable
;
import
java.util.Arrays
;
/**
* 区域类型枚举
*
* @author 芋道源码
*/
@AllArgsConstructor
@Getter
public
enum
AreaTypeEnum
implements
IntArrayValuable
{
COUNTRY
(
1
,
"国家"
),
PROVINCE
(
2
,
"省份"
),
CITY
(
3
,
"城市"
),
DISTRICT
(
4
,
"地区"
),
// 县、镇、区等
;
public
static
final
int
[]
ARRAYS
=
Arrays
.
stream
(
values
()).
mapToInt
(
AreaTypeEnum:
:
getType
).
toArray
();
/**
* 类型
*/
private
final
Integer
type
;
/**
* 名字
*/
private
final
String
name
;
@Override
public
int
[]
array
()
{
return
ARRAYS
;
}
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/enums/CommonStatusEnum.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
enums
;
import
cn.hutool.core.util.ObjUtil
;
import
lombok.AllArgsConstructor
;
import
lombok.Getter
;
import
org.dromara.common.mall.core.IntArrayValuable
;
import
java.util.Arrays
;
/**
* 通用状态枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public
enum
CommonStatusEnum
implements
IntArrayValuable
{
ENABLE
(
0
,
"开启"
),
DISABLE
(
1
,
"关闭"
);
public
static
final
int
[]
ARRAYS
=
Arrays
.
stream
(
values
()).
mapToInt
(
CommonStatusEnum:
:
getStatus
).
toArray
();
/**
* 状态值
*/
private
final
Integer
status
;
/**
* 状态名
*/
private
final
String
name
;
@Override
public
int
[]
array
()
{
return
ARRAYS
;
}
public
static
boolean
isEnable
(
Integer
status
)
{
return
ObjUtil
.
equal
(
ENABLE
.
status
,
status
);
}
public
static
boolean
isDisable
(
Integer
status
)
{
return
ObjUtil
.
equal
(
DISABLE
.
status
,
status
);
}
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/enums/DateIntervalEnum.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
enums
;
import
cn.hutool.core.util.ArrayUtil
;
import
lombok.AllArgsConstructor
;
import
lombok.Getter
;
import
org.dromara.common.mall.core.IntArrayValuable
;
import
java.util.Arrays
;
/**
* 时间间隔的枚举
*
* @author dhb52
*/
@Getter
@AllArgsConstructor
public
enum
DateIntervalEnum
implements
IntArrayValuable
{
DAY
(
1
,
"天"
),
WEEK
(
2
,
"周"
),
MONTH
(
3
,
"月"
),
QUARTER
(
4
,
"季度"
),
YEAR
(
5
,
"年"
)
;
public
static
final
int
[]
ARRAYS
=
Arrays
.
stream
(
values
()).
mapToInt
(
DateIntervalEnum:
:
getInterval
).
toArray
();
/**
* 类型
*/
private
final
Integer
interval
;
/**
* 名称
*/
private
final
String
name
;
@Override
public
int
[]
array
()
{
return
ARRAYS
;
}
public
static
DateIntervalEnum
valueOf
(
Integer
interval
)
{
return
ArrayUtil
.
firstMatch
(
item
->
item
.
getInterval
().
equals
(
interval
),
DateIntervalEnum
.
values
());
}
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/enums/TerminalEnum.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
enums
;
import
lombok.Getter
;
import
lombok.RequiredArgsConstructor
;
import
org.dromara.common.mall.core.IntArrayValuable
;
import
java.util.Arrays
;
/**
* 终端的枚举
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Getter
public
enum
TerminalEnum
implements
IntArrayValuable
{
UNKNOWN
(
0
,
"未知"
),
// 目的:在无法解析到 terminal 时,使用它
WECHAT_MINI_PROGRAM
(
10
,
"微信小程序"
),
WECHAT_WAP
(
11
,
"微信公众号"
),
H5
(
20
,
"H5 网页"
),
APP
(
31
,
"手机 App"
),
;
public
static
final
int
[]
ARRAYS
=
Arrays
.
stream
(
values
()).
mapToInt
(
TerminalEnum:
:
getTerminal
).
toArray
();
/**
* 终端
*/
private
final
Integer
terminal
;
/**
* 终端名
*/
private
final
String
name
;
@Override
public
int
[]
array
()
{
return
ARRAYS
;
}
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/enums/UserTypeEnum.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
enums
;
import
cn.hutool.core.util.ArrayUtil
;
import
lombok.AllArgsConstructor
;
import
lombok.Getter
;
import
org.dromara.common.mall.core.IntArrayValuable
;
import
java.util.Arrays
;
/**
* 全局用户类型枚举
*/
@AllArgsConstructor
@Getter
public
enum
UserTypeEnum
implements
IntArrayValuable
{
MEMBER
(
1
,
"会员"
),
// 面向 c 端,普通用户
ADMIN
(
2
,
"管理员"
);
// 面向 b 端,管理后台
public
static
final
int
[]
ARRAYS
=
Arrays
.
stream
(
values
()).
mapToInt
(
UserTypeEnum:
:
getValue
).
toArray
();
/**
* 类型
*/
private
final
Integer
value
;
/**
* 类型名
*/
private
final
String
name
;
public
static
UserTypeEnum
valueOf
(
Integer
value
)
{
return
ArrayUtil
.
firstMatch
(
userType
->
userType
.
getValue
().
equals
(
value
),
UserTypeEnum
.
values
());
}
@Override
public
int
[]
array
()
{
return
ARRAYS
;
}
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/exception/ErrorCode.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
exception
;
import
lombok.Data
;
/**
* 错误码对象
*
* 全局错误码,占用 [0, 999], 参见 {@link org.dromara.common.mall.exception.enums.GlobalErrorCodeConstants}
* 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link org.dromara.common.mall.exception.enums.ServiceErrorCodeRange}
*
* TODO 错误码设计成对象的原因,为未来的 i18 国际化做准备
*/
@Data
public
class
ErrorCode
{
/**
* 错误码
*/
private
final
Integer
code
;
/**
* 错误提示
*/
private
final
String
msg
;
public
ErrorCode
(
Integer
code
,
String
message
)
{
this
.
code
=
code
;
this
.
msg
=
message
;
}
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/exception/enums/GlobalErrorCodeConstants.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
exception
.
enums
;
import
org.dromara.common.mall.exception.ErrorCode
;
/**
* 全局错误码枚举
* 0-999 系统异常编码保留
*
* 一般情况下,使用 HTTP 响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status
* 虽然说,HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的
* 比较特殊的是,因为之前一直使用 0 作为成功,就不使用 200 啦。
*
* @author 芋道源码
*/
public
interface
GlobalErrorCodeConstants
{
ErrorCode
SUCCESS
=
new
ErrorCode
(
0
,
"成功"
);
// ========== 客户端错误段 ==========
ErrorCode
BAD_REQUEST
=
new
ErrorCode
(
400
,
"请求参数不正确"
);
ErrorCode
UNAUTHORIZED
=
new
ErrorCode
(
401
,
"账号未登录"
);
ErrorCode
FORBIDDEN
=
new
ErrorCode
(
403
,
"没有该操作权限"
);
ErrorCode
NOT_FOUND
=
new
ErrorCode
(
404
,
"请求未找到"
);
ErrorCode
METHOD_NOT_ALLOWED
=
new
ErrorCode
(
405
,
"请求方法不正确"
);
ErrorCode
LOCKED
=
new
ErrorCode
(
423
,
"请求失败,请稍后重试"
);
// 并发请求,不允许
ErrorCode
TOO_MANY_REQUESTS
=
new
ErrorCode
(
429
,
"请求过于频繁,请稍后重试"
);
// ========== 服务端错误段 ==========
ErrorCode
INTERNAL_SERVER_ERROR
=
new
ErrorCode
(
500
,
"系统异常"
);
ErrorCode
NOT_IMPLEMENTED
=
new
ErrorCode
(
501
,
"功能未实现/未开启"
);
ErrorCode
ERROR_CONFIGURATION
=
new
ErrorCode
(
502
,
"错误的配置项"
);
// ========== 自定义错误段 ==========
ErrorCode
REPEATED_REQUESTS
=
new
ErrorCode
(
900
,
"重复请求,请稍后重试"
);
// 重复请求
ErrorCode
DEMO_DENY
=
new
ErrorCode
(
901
,
"演示模式,禁止写操作"
);
ErrorCode
UNKNOWN
=
new
ErrorCode
(
999
,
"未知错误"
);
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/exception/enums/ServiceErrorCodeRange.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
exception
.
enums
;
/**
* 业务异常的错误码区间,解决:解决各模块错误码定义,避免重复,在此只声明不做实际使用
*
* 一共 10 位,分成四段
*
* 第一段,1 位,类型
* 1 - 业务级别异常
* x - 预留
* 第二段,3 位,系统类型
* 001 - 用户系统
* 002 - 商品系统
* 003 - 订单系统
* 004 - 支付系统
* 005 - 优惠劵系统
* ... - ...
* 第三段,3 位,模块
* 不限制规则。
* 一般建议,每个系统里面,可能有多个模块,可以再去做分段。以用户系统为例子:
* 001 - OAuth2 模块
* 002 - User 模块
* 003 - MobileCode 模块
* 第四段,3 位,错误码
* 不限制规则。
* 一般建议,每个模块自增。
*
* @author 芋道源码
*/
public
class
ServiceErrorCodeRange
{
// 模块 infra 错误码区间 [1-001-000-000 ~ 1-002-000-000)
// 模块 system 错误码区间 [1-002-000-000 ~ 1-003-000-000)
// 模块 report 错误码区间 [1-003-000-000 ~ 1-004-000-000)
// 模块 member 错误码区间 [1-004-000-000 ~ 1-005-000-000)
// 模块 mp 错误码区间 [1-006-000-000 ~ 1-007-000-000)
// 模块 pay 错误码区间 [1-007-000-000 ~ 1-008-000-000)
// 模块 bpm 错误码区间 [1-009-000-000 ~ 1-010-000-000)
// 模块 product 错误码区间 [1-008-000-000 ~ 1-009-000-000)
// 模块 trade 错误码区间 [1-011-000-000 ~ 1-012-000-000)
// 模块 promotion 错误码区间 [1-013-000-000 ~ 1-014-000-000)
// 模块 crm 错误码区间 [1-020-000-000 ~ 1-021-000-000)
// 模块 ai 错误码区间 [1-022-000-000 ~ 1-023-000-000)
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/exception/util/ServiceExceptionUtil.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
exception
.
util
;
import
lombok.extern.slf4j.Slf4j
;
import
org.dromara.common.core.exception.ServiceException
;
import
org.dromara.common.mall.exception.ErrorCode
;
import
org.dromara.common.mall.exception.enums.GlobalErrorCodeConstants
;
/**
* {@link ServiceException} 工具类
* <p>
* 目的在于,格式化异常信息提示。
* 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化
*/
@Slf4j
public
class
ServiceExceptionUtil
{
// ========== 和 ServiceException 的集成 ==========
public
static
ServiceException
exception
(
ErrorCode
errorCode
)
{
return
exception0
(
errorCode
.
getCode
(),
errorCode
.
getMsg
());
}
public
static
ServiceException
exception
(
ErrorCode
errorCode
,
Object
...
params
)
{
return
exception0
(
errorCode
.
getCode
(),
errorCode
.
getMsg
(),
params
);
}
public
static
ServiceException
exception0
(
Integer
code
,
String
messagePattern
,
Object
...
params
)
{
String
message
=
doFormat
(
code
,
messagePattern
,
params
);
return
new
ServiceException
(
message
,
code
);
}
public
static
ServiceException
invalidParamException
(
String
messagePattern
,
Object
...
params
)
{
return
exception0
(
GlobalErrorCodeConstants
.
BAD_REQUEST
.
getCode
(),
messagePattern
,
params
);
}
// ========== 格式化方法 ==========
/**
* 将错误编号对应的消息使用 params 进行格式化。
*
* @param code 错误编号
* @param messagePattern 消息模版
* @param params 参数
* @return 格式化后的提示
*/
public
static
String
doFormat
(
int
code
,
String
messagePattern
,
Object
...
params
)
{
StringBuilder
sbuf
=
new
StringBuilder
(
messagePattern
.
length
()
+
50
);
int
i
=
0
;
int
j
;
int
l
;
for
(
l
=
0
;
l
<
params
.
length
;
l
++)
{
j
=
messagePattern
.
indexOf
(
"{}"
,
i
);
if
(
j
==
-
1
)
{
log
.
error
(
"[doFormat][参数过多:错误码({})|错误内容({})|参数({})"
,
code
,
messagePattern
,
params
);
if
(
i
==
0
)
{
return
messagePattern
;
}
else
{
sbuf
.
append
(
messagePattern
.
substring
(
i
));
return
sbuf
.
toString
();
}
}
else
{
sbuf
.
append
(
messagePattern
,
i
,
j
);
sbuf
.
append
(
params
[
l
]);
i
=
j
+
2
;
}
}
if
(
messagePattern
.
indexOf
(
"{}"
,
i
)
!=
-
1
)
{
log
.
error
(
"[doFormat][参数过少:错误码({})|错误内容({})|参数({})"
,
code
,
messagePattern
,
params
);
}
sbuf
.
append
(
messagePattern
.
substring
(
i
));
return
sbuf
.
toString
();
}
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/util/cache/CacheUtils.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
util
.
cache
;
import
com.google.common.cache.CacheBuilder
;
import
com.google.common.cache.CacheLoader
;
import
com.google.common.cache.LoadingCache
;
import
java.time.Duration
;
import
java.util.concurrent.Executors
;
/**
* Cache 工具类
*
* @author 芋道源码
*/
public
class
CacheUtils
{
/**
* 构建异步刷新的 LoadingCache 对象
*
* 注意:如果你的缓存和 ThreadLocal 有关系,要么自己处理 ThreadLocal 的传递,要么使用 {@link #buildCache(Duration, CacheLoader)} 方法
*
* 或者简单理解:
* 1、和“人”相关的,使用 {@link #buildCache(Duration, CacheLoader)} 方法
* 2、和“全局”、“系统”相关的,使用当前缓存方法
*
* @param duration 过期时间
* @param loader CacheLoader 对象
* @return LoadingCache 对象
*/
public
static
<
K
,
V
>
LoadingCache
<
K
,
V
>
buildAsyncReloadingCache
(
Duration
duration
,
CacheLoader
<
K
,
V
>
loader
)
{
return
CacheBuilder
.
newBuilder
()
// 只阻塞当前数据加载线程,其他线程返回旧值
.
refreshAfterWrite
(
duration
)
// 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程
.
build
(
CacheLoader
.
asyncReloading
(
loader
,
Executors
.
newCachedThreadPool
()));
// TODO 芋艿:可能要思考下,未来要不要做成可配置
}
/**
* 构建同步刷新的 LoadingCache 对象
*
* @param duration 过期时间
* @param loader CacheLoader 对象
* @return LoadingCache 对象
*/
public
static
<
K
,
V
>
LoadingCache
<
K
,
V
>
buildCache
(
Duration
duration
,
CacheLoader
<
K
,
V
>
loader
)
{
return
CacheBuilder
.
newBuilder
().
refreshAfterWrite
(
duration
).
build
(
loader
);
}
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/util/collection/CollectionUtils.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
util
.
collection
;
import
cn.hutool.core.collection.CollUtil
;
import
cn.hutool.core.collection.CollectionUtil
;
import
cn.hutool.core.util.ArrayUtil
;
import
java.util.*
;
import
java.util.function.*
;
import
java.util.stream.Collectors
;
import
java.util.stream.Stream
;
import
static
java
.
util
.
Arrays
.
asList
;
/**
* Collection 工具类
*
* @author 芋道源码
*/
public
class
CollectionUtils
{
public
static
boolean
containsAny
(
Object
source
,
Object
...
targets
)
{
return
asList
(
targets
).
contains
(
source
);
}
public
static
boolean
isAnyEmpty
(
Collection
<?>...
collections
)
{
return
Arrays
.
stream
(
collections
).
anyMatch
(
CollectionUtil:
:
isEmpty
);
}
public
static
<
T
>
boolean
anyMatch
(
Collection
<
T
>
from
,
Predicate
<
T
>
predicate
)
{
return
from
.
stream
().
anyMatch
(
predicate
);
}
public
static
<
T
>
List
<
T
>
filterList
(
Collection
<
T
>
from
,
Predicate
<
T
>
predicate
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
new
ArrayList
<>();
}
return
from
.
stream
().
filter
(
predicate
).
collect
(
Collectors
.
toList
());
}
public
static
<
T
,
R
>
List
<
T
>
distinct
(
Collection
<
T
>
from
,
Function
<
T
,
R
>
keyMapper
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
new
ArrayList
<>();
}
return
distinct
(
from
,
keyMapper
,
(
t1
,
t2
)
->
t1
);
}
public
static
<
T
,
R
>
List
<
T
>
distinct
(
Collection
<
T
>
from
,
Function
<
T
,
R
>
keyMapper
,
BinaryOperator
<
T
>
cover
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
new
ArrayList
<>();
}
return
new
ArrayList
<>(
convertMap
(
from
,
keyMapper
,
Function
.
identity
(),
cover
).
values
());
}
public
static
<
T
,
U
>
List
<
U
>
convertList
(
T
[]
from
,
Function
<
T
,
U
>
func
)
{
if
(
ArrayUtil
.
isEmpty
(
from
))
{
return
new
ArrayList
<>();
}
return
convertList
(
Arrays
.
asList
(
from
),
func
);
}
public
static
<
T
,
U
>
List
<
U
>
convertList
(
Collection
<
T
>
from
,
Function
<
T
,
U
>
func
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
new
ArrayList
<>();
}
return
from
.
stream
().
map
(
func
).
filter
(
Objects:
:
nonNull
).
collect
(
Collectors
.
toList
());
}
public
static
<
T
,
U
>
List
<
U
>
convertList
(
Collection
<
T
>
from
,
Function
<
T
,
U
>
func
,
Predicate
<
T
>
filter
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
new
ArrayList
<>();
}
return
from
.
stream
().
filter
(
filter
).
map
(
func
).
filter
(
Objects:
:
nonNull
).
collect
(
Collectors
.
toList
());
}
public
static
<
T
,
U
>
List
<
U
>
convertListByFlatMap
(
Collection
<
T
>
from
,
Function
<
T
,
?
extends
Stream
<?
extends
U
>>
func
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
new
ArrayList
<>();
}
return
from
.
stream
().
filter
(
Objects:
:
nonNull
).
flatMap
(
func
).
filter
(
Objects:
:
nonNull
).
collect
(
Collectors
.
toList
());
}
public
static
<
T
,
U
,
R
>
List
<
R
>
convertListByFlatMap
(
Collection
<
T
>
from
,
Function
<?
super
T
,
?
extends
U
>
mapper
,
Function
<
U
,
?
extends
Stream
<?
extends
R
>>
func
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
new
ArrayList
<>();
}
return
from
.
stream
().
map
(
mapper
).
filter
(
Objects:
:
nonNull
).
flatMap
(
func
).
filter
(
Objects:
:
nonNull
).
collect
(
Collectors
.
toList
());
}
public
static
<
K
,
V
>
List
<
V
>
mergeValuesFromMap
(
Map
<
K
,
List
<
V
>>
map
)
{
return
map
.
values
()
.
stream
()
.
flatMap
(
List:
:
stream
)
.
collect
(
Collectors
.
toList
());
}
public
static
<
T
>
Set
<
T
>
convertSet
(
Collection
<
T
>
from
)
{
return
convertSet
(
from
,
v
->
v
);
}
public
static
<
T
,
U
>
Set
<
U
>
convertSet
(
Collection
<
T
>
from
,
Function
<
T
,
U
>
func
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
new
HashSet
<>();
}
return
from
.
stream
().
map
(
func
).
filter
(
Objects:
:
nonNull
).
collect
(
Collectors
.
toSet
());
}
public
static
<
T
,
U
>
Set
<
U
>
convertSet
(
Collection
<
T
>
from
,
Function
<
T
,
U
>
func
,
Predicate
<
T
>
filter
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
new
HashSet
<>();
}
return
from
.
stream
().
filter
(
filter
).
map
(
func
).
filter
(
Objects:
:
nonNull
).
collect
(
Collectors
.
toSet
());
}
public
static
<
T
,
K
>
Map
<
K
,
T
>
convertMapByFilter
(
Collection
<
T
>
from
,
Predicate
<
T
>
filter
,
Function
<
T
,
K
>
keyFunc
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
new
HashMap
<>();
}
return
from
.
stream
().
filter
(
filter
).
collect
(
Collectors
.
toMap
(
keyFunc
,
v
->
v
));
}
public
static
<
T
,
U
>
Set
<
U
>
convertSetByFlatMap
(
Collection
<
T
>
from
,
Function
<
T
,
?
extends
Stream
<?
extends
U
>>
func
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
new
HashSet
<>();
}
return
from
.
stream
().
filter
(
Objects:
:
nonNull
).
flatMap
(
func
).
filter
(
Objects:
:
nonNull
).
collect
(
Collectors
.
toSet
());
}
public
static
<
T
,
U
,
R
>
Set
<
R
>
convertSetByFlatMap
(
Collection
<
T
>
from
,
Function
<?
super
T
,
?
extends
U
>
mapper
,
Function
<
U
,
?
extends
Stream
<?
extends
R
>>
func
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
new
HashSet
<>();
}
return
from
.
stream
().
map
(
mapper
).
filter
(
Objects:
:
nonNull
).
flatMap
(
func
).
filter
(
Objects:
:
nonNull
).
collect
(
Collectors
.
toSet
());
}
public
static
<
T
,
K
>
Map
<
K
,
T
>
convertMap
(
Collection
<
T
>
from
,
Function
<
T
,
K
>
keyFunc
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
new
HashMap
<>();
}
return
convertMap
(
from
,
keyFunc
,
Function
.
identity
());
}
public
static
<
T
,
K
>
Map
<
K
,
T
>
convertMap
(
Collection
<
T
>
from
,
Function
<
T
,
K
>
keyFunc
,
Supplier
<?
extends
Map
<
K
,
T
>>
supplier
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
supplier
.
get
();
}
return
convertMap
(
from
,
keyFunc
,
Function
.
identity
(),
supplier
);
}
public
static
<
T
,
K
,
V
>
Map
<
K
,
V
>
convertMap
(
Collection
<
T
>
from
,
Function
<
T
,
K
>
keyFunc
,
Function
<
T
,
V
>
valueFunc
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
new
HashMap
<>();
}
return
convertMap
(
from
,
keyFunc
,
valueFunc
,
(
v1
,
v2
)
->
v1
);
}
public
static
<
T
,
K
,
V
>
Map
<
K
,
V
>
convertMap
(
Collection
<
T
>
from
,
Function
<
T
,
K
>
keyFunc
,
Function
<
T
,
V
>
valueFunc
,
BinaryOperator
<
V
>
mergeFunction
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
new
HashMap
<>();
}
return
convertMap
(
from
,
keyFunc
,
valueFunc
,
mergeFunction
,
HashMap:
:
new
);
}
public
static
<
T
,
K
,
V
>
Map
<
K
,
V
>
convertMap
(
Collection
<
T
>
from
,
Function
<
T
,
K
>
keyFunc
,
Function
<
T
,
V
>
valueFunc
,
Supplier
<?
extends
Map
<
K
,
V
>>
supplier
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
supplier
.
get
();
}
return
convertMap
(
from
,
keyFunc
,
valueFunc
,
(
v1
,
v2
)
->
v1
,
supplier
);
}
public
static
<
T
,
K
,
V
>
Map
<
K
,
V
>
convertMap
(
Collection
<
T
>
from
,
Function
<
T
,
K
>
keyFunc
,
Function
<
T
,
V
>
valueFunc
,
BinaryOperator
<
V
>
mergeFunction
,
Supplier
<?
extends
Map
<
K
,
V
>>
supplier
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
new
HashMap
<>();
}
return
from
.
stream
().
collect
(
Collectors
.
toMap
(
keyFunc
,
valueFunc
,
mergeFunction
,
supplier
));
}
public
static
<
T
,
K
>
Map
<
K
,
List
<
T
>>
convertMultiMap
(
Collection
<
T
>
from
,
Function
<
T
,
K
>
keyFunc
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
new
HashMap
<>();
}
return
from
.
stream
().
collect
(
Collectors
.
groupingBy
(
keyFunc
,
Collectors
.
mapping
(
t
->
t
,
Collectors
.
toList
())));
}
public
static
<
T
,
K
,
V
>
Map
<
K
,
List
<
V
>>
convertMultiMap
(
Collection
<
T
>
from
,
Function
<
T
,
K
>
keyFunc
,
Function
<
T
,
V
>
valueFunc
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
new
HashMap
<>();
}
return
from
.
stream
()
.
collect
(
Collectors
.
groupingBy
(
keyFunc
,
Collectors
.
mapping
(
valueFunc
,
Collectors
.
toList
())));
}
// 暂时没想好名字,先以 2 结尾噶
public
static
<
T
,
K
,
V
>
Map
<
K
,
Set
<
V
>>
convertMultiMap2
(
Collection
<
T
>
from
,
Function
<
T
,
K
>
keyFunc
,
Function
<
T
,
V
>
valueFunc
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
new
HashMap
<>();
}
return
from
.
stream
().
collect
(
Collectors
.
groupingBy
(
keyFunc
,
Collectors
.
mapping
(
valueFunc
,
Collectors
.
toSet
())));
}
/**
* 对比老、新两个列表,找出新增、修改、删除的数据
*
* @param oldList 老列表
* @param newList 新列表
* @param sameFunc 对比函数,返回 true 表示相同,返回 false 表示不同
* 注意,same 是通过每个元素的“标识”,判断它们是不是同一个数据
* @return [新增列表、修改列表、删除列表]
*/
public
static
<
T
>
List
<
List
<
T
>>
diffList
(
Collection
<
T
>
oldList
,
Collection
<
T
>
newList
,
BiFunction
<
T
,
T
,
Boolean
>
sameFunc
)
{
List
<
T
>
createList
=
new
LinkedList
<>(
newList
);
// 默认都认为是新增的,后续会进行移除
List
<
T
>
updateList
=
new
ArrayList
<>();
List
<
T
>
deleteList
=
new
ArrayList
<>();
// 通过以 oldList 为主遍历,找出 updateList 和 deleteList
for
(
T
oldObj
:
oldList
)
{
// 1. 寻找是否有匹配的
T
foundObj
=
null
;
for
(
Iterator
<
T
>
iterator
=
createList
.
iterator
();
iterator
.
hasNext
();
)
{
T
newObj
=
iterator
.
next
();
// 1.1 不匹配,则直接跳过
if
(!
sameFunc
.
apply
(
oldObj
,
newObj
))
{
continue
;
}
// 1.2 匹配,则移除,并结束寻找
iterator
.
remove
();
foundObj
=
newObj
;
break
;
}
// 2. 匹配添加到 updateList;不匹配则添加到 deleteList 中
if
(
foundObj
!=
null
)
{
updateList
.
add
(
foundObj
);
}
else
{
deleteList
.
add
(
oldObj
);
}
}
return
asList
(
createList
,
updateList
,
deleteList
);
}
public
static
boolean
containsAny
(
Collection
<?>
source
,
Collection
<?>
candidates
)
{
return
org
.
springframework
.
util
.
CollectionUtils
.
containsAny
(
source
,
candidates
);
}
public
static
<
T
>
T
getFirst
(
List
<
T
>
from
)
{
return
!
CollectionUtil
.
isEmpty
(
from
)
?
from
.
get
(
0
)
:
null
;
}
public
static
<
T
>
T
findFirst
(
Collection
<
T
>
from
,
Predicate
<
T
>
predicate
)
{
return
findFirst
(
from
,
predicate
,
Function
.
identity
());
}
public
static
<
T
,
U
>
U
findFirst
(
Collection
<
T
>
from
,
Predicate
<
T
>
predicate
,
Function
<
T
,
U
>
func
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
null
;
}
return
from
.
stream
().
filter
(
predicate
).
findFirst
().
map
(
func
).
orElse
(
null
);
}
public
static
<
T
,
V
extends
Comparable
<?
super
V
>>
V
getMaxValue
(
Collection
<
T
>
from
,
Function
<
T
,
V
>
valueFunc
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
null
;
}
assert
!
from
.
isEmpty
();
// 断言,避免告警
T
t
=
from
.
stream
().
max
(
Comparator
.
comparing
(
valueFunc
)).
get
();
return
valueFunc
.
apply
(
t
);
}
public
static
<
T
,
V
extends
Comparable
<?
super
V
>>
V
getMinValue
(
List
<
T
>
from
,
Function
<
T
,
V
>
valueFunc
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
null
;
}
assert
from
.
size
()
>
0
;
// 断言,避免告警
T
t
=
from
.
stream
().
min
(
Comparator
.
comparing
(
valueFunc
)).
get
();
return
valueFunc
.
apply
(
t
);
}
public
static
<
T
,
V
extends
Comparable
<?
super
V
>>
T
getMinObject
(
List
<
T
>
from
,
Function
<
T
,
V
>
valueFunc
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
null
;
}
assert
from
.
size
()
>
0
;
// 断言,避免告警
return
from
.
stream
().
min
(
Comparator
.
comparing
(
valueFunc
)).
get
();
}
public
static
<
T
,
V
extends
Comparable
<?
super
V
>>
V
getSumValue
(
Collection
<
T
>
from
,
Function
<
T
,
V
>
valueFunc
,
BinaryOperator
<
V
>
accumulator
)
{
return
getSumValue
(
from
,
valueFunc
,
accumulator
,
null
);
}
public
static
<
T
,
V
extends
Comparable
<?
super
V
>>
V
getSumValue
(
Collection
<
T
>
from
,
Function
<
T
,
V
>
valueFunc
,
BinaryOperator
<
V
>
accumulator
,
V
defaultValue
)
{
if
(
CollUtil
.
isEmpty
(
from
))
{
return
defaultValue
;
}
assert
!
from
.
isEmpty
();
// 断言,避免告警
return
from
.
stream
().
map
(
valueFunc
).
filter
(
Objects:
:
nonNull
).
reduce
(
accumulator
).
orElse
(
defaultValue
);
}
public
static
<
T
>
void
addIfNotNull
(
Collection
<
T
>
coll
,
T
item
)
{
if
(
item
==
null
)
{
return
;
}
coll
.
add
(
item
);
}
public
static
<
T
>
Collection
<
T
>
singleton
(
T
obj
)
{
return
obj
==
null
?
Collections
.
emptyList
()
:
Collections
.
singleton
(
obj
);
}
public
static
<
T
>
List
<
T
>
newArrayList
(
List
<
List
<
T
>>
list
)
{
return
list
.
stream
().
filter
(
Objects:
:
nonNull
).
flatMap
(
Collection:
:
stream
).
collect
(
Collectors
.
toList
());
}
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/util/collection/MapUtils.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
util
.
collection
;
import
cn.hutool.core.collection.CollUtil
;
import
cn.hutool.core.collection.CollectionUtil
;
import
cn.hutool.core.util.ObjUtil
;
import
com.google.common.collect.Maps
;
import
com.google.common.collect.Multimap
;
import
org.dromara.common.mall.core.KeyValue
;
import
java.util.ArrayList
;
import
java.util.Collection
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.function.Consumer
;
/**
* Map 工具类
*
* @author 芋道源码
*/
public
class
MapUtils
{
/**
* 从哈希表表中,获得 keys 对应的所有 value 数组
*
* @param multimap 哈希表
* @param keys keys
* @return value 数组
*/
public
static
<
K
,
V
>
List
<
V
>
getList
(
Multimap
<
K
,
V
>
multimap
,
Collection
<
K
>
keys
)
{
List
<
V
>
result
=
new
ArrayList
<>();
keys
.
forEach
(
k
->
{
Collection
<
V
>
values
=
multimap
.
get
(
k
);
if
(
CollectionUtil
.
isEmpty
(
values
))
{
return
;
}
result
.
addAll
(
values
);
});
return
result
;
}
/**
* 从哈希表查找到 key 对应的 value,然后进一步处理
* key 为 null 时, 不处理
* 注意,如果查找到的 value 为 null 时,不进行处理
*
* @param map 哈希表
* @param key key
* @param consumer 进一步处理的逻辑
*/
public
static
<
K
,
V
>
void
findAndThen
(
Map
<
K
,
V
>
map
,
K
key
,
Consumer
<
V
>
consumer
)
{
if
(
ObjUtil
.
isNull
(
key
)
||
CollUtil
.
isEmpty
(
map
))
{
return
;
}
V
value
=
map
.
get
(
key
);
if
(
value
==
null
)
{
return
;
}
consumer
.
accept
(
value
);
}
public
static
<
K
,
V
>
Map
<
K
,
V
>
convertMap
(
List
<
KeyValue
<
K
,
V
>>
keyValues
)
{
Map
<
K
,
V
>
map
=
Maps
.
newLinkedHashMapWithExpectedSize
(
keyValues
.
size
());
keyValues
.
forEach
(
keyValue
->
map
.
put
(
keyValue
.
getKey
(),
keyValue
.
getValue
()));
return
map
;
}
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/util/date/DateUtils.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
util
.
date
;
import
cn.hutool.core.date.LocalDateTimeUtil
;
import
java.time.*
;
import
java.util.Calendar
;
import
java.util.Date
;
/**
* 时间工具类
*
* @author 芋道源码
*/
public
class
DateUtils
{
/**
* 时区 - 默认
*/
public
static
final
String
TIME_ZONE_DEFAULT
=
"GMT+8"
;
/**
* 秒转换成毫秒
*/
public
static
final
long
SECOND_MILLIS
=
1000
;
public
static
final
String
FORMAT_YEAR_MONTH_DAY
=
"yyyy-MM-dd"
;
public
static
final
String
FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND
=
"yyyy-MM-dd HH:mm:ss"
;
/**
* 将 LocalDateTime 转换成 Date
*
* @param date LocalDateTime
* @return LocalDateTime
*/
public
static
Date
of
(
LocalDateTime
date
)
{
if
(
date
==
null
)
{
return
null
;
}
// 将此日期时间与时区相结合以创建 ZonedDateTime
ZonedDateTime
zonedDateTime
=
date
.
atZone
(
ZoneId
.
systemDefault
());
// 本地时间线 LocalDateTime 到即时时间线 Instant 时间戳
Instant
instant
=
zonedDateTime
.
toInstant
();
// UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间
return
Date
.
from
(
instant
);
}
/**
* 将 Date 转换成 LocalDateTime
*
* @param date Date
* @return LocalDateTime
*/
public
static
LocalDateTime
of
(
Date
date
)
{
if
(
date
==
null
)
{
return
null
;
}
// 转为时间戳
Instant
instant
=
date
.
toInstant
();
// UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间
return
LocalDateTime
.
ofInstant
(
instant
,
ZoneId
.
systemDefault
());
}
public
static
Date
addTime
(
Duration
duration
)
{
return
new
Date
(
System
.
currentTimeMillis
()
+
duration
.
toMillis
());
}
public
static
boolean
isExpired
(
LocalDateTime
time
)
{
LocalDateTime
now
=
LocalDateTime
.
now
();
return
now
.
isAfter
(
time
);
}
/**
* 创建指定时间
*
* @param year 年
* @param mouth 月
* @param day 日
* @return 指定时间
*/
public
static
Date
buildTime
(
int
year
,
int
mouth
,
int
day
)
{
return
buildTime
(
year
,
mouth
,
day
,
0
,
0
,
0
);
}
/**
* 创建指定时间
*
* @param year 年
* @param mouth 月
* @param day 日
* @param hour 小时
* @param minute 分钟
* @param second 秒
* @return 指定时间
*/
public
static
Date
buildTime
(
int
year
,
int
mouth
,
int
day
,
int
hour
,
int
minute
,
int
second
)
{
Calendar
calendar
=
Calendar
.
getInstance
();
calendar
.
set
(
Calendar
.
YEAR
,
year
);
calendar
.
set
(
Calendar
.
MONTH
,
mouth
-
1
);
calendar
.
set
(
Calendar
.
DAY_OF_MONTH
,
day
);
calendar
.
set
(
Calendar
.
HOUR_OF_DAY
,
hour
);
calendar
.
set
(
Calendar
.
MINUTE
,
minute
);
calendar
.
set
(
Calendar
.
SECOND
,
second
);
calendar
.
set
(
Calendar
.
MILLISECOND
,
0
);
// 一般情况下,都是 0 毫秒
return
calendar
.
getTime
();
}
public
static
Date
max
(
Date
a
,
Date
b
)
{
if
(
a
==
null
)
{
return
b
;
}
if
(
b
==
null
)
{
return
a
;
}
return
a
.
compareTo
(
b
)
>
0
?
a
:
b
;
}
public
static
LocalDateTime
max
(
LocalDateTime
a
,
LocalDateTime
b
)
{
if
(
a
==
null
)
{
return
b
;
}
if
(
b
==
null
)
{
return
a
;
}
return
a
.
isAfter
(
b
)
?
a
:
b
;
}
/**
* 是否今天
*
* @param date 日期
* @return 是否
*/
public
static
boolean
isToday
(
LocalDateTime
date
)
{
return
LocalDateTimeUtil
.
isSameDay
(
date
,
LocalDateTime
.
now
());
}
/**
* 是否昨天
*
* @param date 日期
* @return 是否
*/
public
static
boolean
isYesterday
(
LocalDateTime
date
)
{
return
LocalDateTimeUtil
.
isSameDay
(
date
,
LocalDateTime
.
now
().
minusDays
(
1
));
}
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/util/date/LocalDateTimeUtils.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
util
.
date
;
import
cn.hutool.core.collection.CollUtil
;
import
cn.hutool.core.date.DatePattern
;
import
cn.hutool.core.date.LocalDateTimeUtil
;
import
cn.hutool.core.lang.Assert
;
import
cn.hutool.core.util.StrUtil
;
import
org.dromara.common.mall.enums.DateIntervalEnum
;
import
java.time.*
;
import
java.time.format.DateTimeParseException
;
import
java.time.temporal.ChronoUnit
;
import
java.time.temporal.TemporalAdjusters
;
import
java.util.ArrayList
;
import
java.util.List
;
/**
* 时间工具类,用于 {@link LocalDateTime}
*
* @author 芋道源码
*/
public
class
LocalDateTimeUtils
{
/**
* 空的 LocalDateTime 对象,主要用于 DB 唯一索引的默认值
*/
public
static
LocalDateTime
EMPTY
=
buildTime
(
1970
,
1
,
1
);
/**
* 解析时间
*
* 相比 {@link LocalDateTimeUtil#parse(CharSequence)} 方法来说,会尽量去解析,直到成功
*
* @param time 时间
* @return 时间字符串
*/
public
static
LocalDateTime
parse
(
String
time
)
{
try
{
return
LocalDateTimeUtil
.
parse
(
time
,
DatePattern
.
NORM_DATE_PATTERN
);
}
catch
(
DateTimeParseException
e
)
{
return
LocalDateTimeUtil
.
parse
(
time
);
}
}
public
static
LocalDateTime
addTime
(
Duration
duration
)
{
return
LocalDateTime
.
now
().
plus
(
duration
);
}
public
static
LocalDateTime
minusTime
(
Duration
duration
)
{
return
LocalDateTime
.
now
().
minus
(
duration
);
}
public
static
boolean
beforeNow
(
LocalDateTime
date
)
{
return
date
.
isBefore
(
LocalDateTime
.
now
());
}
public
static
boolean
afterNow
(
LocalDateTime
date
)
{
return
date
.
isAfter
(
LocalDateTime
.
now
());
}
/**
* 创建指定时间
*
* @param year 年
* @param mouth 月
* @param day 日
* @return 指定时间
*/
public
static
LocalDateTime
buildTime
(
int
year
,
int
mouth
,
int
day
)
{
return
LocalDateTime
.
of
(
year
,
mouth
,
day
,
0
,
0
,
0
);
}
public
static
LocalDateTime
[]
buildBetweenTime
(
int
year1
,
int
mouth1
,
int
day1
,
int
year2
,
int
mouth2
,
int
day2
)
{
return
new
LocalDateTime
[]{
buildTime
(
year1
,
mouth1
,
day1
),
buildTime
(
year2
,
mouth2
,
day2
)};
}
/**
* 判指定断时间,是否在该时间范围内
*
* @param startTime 开始时间
* @param endTime 结束时间
* @param time 指定时间
* @return 是否
*/
public
static
boolean
isBetween
(
LocalDateTime
startTime
,
LocalDateTime
endTime
,
String
time
)
{
if
(
startTime
==
null
||
endTime
==
null
||
time
==
null
)
{
return
false
;
}
return
LocalDateTimeUtil
.
isIn
(
parse
(
time
),
startTime
,
endTime
);
}
/**
* 判断当前时间是否在该时间范围内
*
* @param startTime 开始时间
* @param endTime 结束时间
* @return 是否
*/
public
static
boolean
isBetween
(
LocalDateTime
startTime
,
LocalDateTime
endTime
)
{
if
(
startTime
==
null
||
endTime
==
null
)
{
return
false
;
}
return
LocalDateTimeUtil
.
isIn
(
LocalDateTime
.
now
(),
startTime
,
endTime
);
}
/**
* 判断当前时间是否在该时间范围内
*
* @param startTime 开始时间
* @param endTime 结束时间
* @return 是否
*/
public
static
boolean
isBetween
(
String
startTime
,
String
endTime
)
{
if
(
startTime
==
null
||
endTime
==
null
)
{
return
false
;
}
LocalDate
nowDate
=
LocalDate
.
now
();
return
LocalDateTimeUtil
.
isIn
(
LocalDateTime
.
now
(),
LocalDateTime
.
of
(
nowDate
,
LocalTime
.
parse
(
startTime
)),
LocalDateTime
.
of
(
nowDate
,
LocalTime
.
parse
(
endTime
)));
}
/**
* 判断时间段是否重叠
*
* @param startTime1 开始 time1
* @param endTime1 结束 time1
* @param startTime2 开始 time2
* @param endTime2 结束 time2
* @return 重叠:true 不重叠:false
*/
public
static
boolean
isOverlap
(
LocalTime
startTime1
,
LocalTime
endTime1
,
LocalTime
startTime2
,
LocalTime
endTime2
)
{
LocalDate
nowDate
=
LocalDate
.
now
();
return
LocalDateTimeUtil
.
isOverlap
(
LocalDateTime
.
of
(
nowDate
,
startTime1
),
LocalDateTime
.
of
(
nowDate
,
endTime1
),
LocalDateTime
.
of
(
nowDate
,
startTime2
),
LocalDateTime
.
of
(
nowDate
,
endTime2
));
}
/**
* 获取指定日期所在的月份的开始时间
* 例如:2023-09-30 00:00:00,000
*
* @param date 日期
* @return 月份的开始时间
*/
public
static
LocalDateTime
beginOfMonth
(
LocalDateTime
date
)
{
return
date
.
with
(
TemporalAdjusters
.
firstDayOfMonth
()).
with
(
LocalTime
.
MIN
);
}
/**
* 获取指定日期所在的月份的最后时间
* 例如:2023-09-30 23:59:59,999
*
* @param date 日期
* @return 月份的结束时间
*/
public
static
LocalDateTime
endOfMonth
(
LocalDateTime
date
)
{
return
date
.
with
(
TemporalAdjusters
.
lastDayOfMonth
()).
with
(
LocalTime
.
MAX
);
}
/**
* 获得指定日期所在季度
*
* @param date 日期
* @return 所在季度
*/
public
static
int
getQuarterOfYear
(
LocalDateTime
date
)
{
return
(
date
.
getMonthValue
()
-
1
)
/
3
+
1
;
}
/**
* 获取指定日期到现在过了几天,如果指定日期在当前日期之后,获取结果为负
*
* @param dateTime 日期
* @return 相差天数
*/
public
static
Long
between
(
LocalDateTime
dateTime
)
{
return
LocalDateTimeUtil
.
between
(
dateTime
,
LocalDateTime
.
now
(),
ChronoUnit
.
DAYS
);
}
/**
* 获取今天的开始时间
*
* @return 今天
*/
public
static
LocalDateTime
getToday
()
{
return
LocalDateTimeUtil
.
beginOfDay
(
LocalDateTime
.
now
());
}
/**
* 获取昨天的开始时间
*
* @return 昨天
*/
public
static
LocalDateTime
getYesterday
()
{
return
LocalDateTimeUtil
.
beginOfDay
(
LocalDateTime
.
now
().
minusDays
(
1
));
}
/**
* 获取本月的开始时间
*
* @return 本月
*/
public
static
LocalDateTime
getMonth
()
{
return
beginOfMonth
(
LocalDateTime
.
now
());
}
/**
* 获取本年的开始时间
*
* @return 本年
*/
public
static
LocalDateTime
getYear
()
{
return
LocalDateTime
.
now
().
with
(
TemporalAdjusters
.
firstDayOfYear
()).
with
(
LocalTime
.
MIN
);
}
public
static
List
<
LocalDateTime
[]>
getDateRangeList
(
LocalDateTime
startTime
,
LocalDateTime
endTime
,
Integer
interval
)
{
// 1.1 找到枚举
DateIntervalEnum
intervalEnum
=
DateIntervalEnum
.
valueOf
(
interval
);
Assert
.
notNull
(
intervalEnum
,
"interval({}} 找不到对应的枚举"
,
interval
);
// 1.2 将时间对齐
startTime
=
LocalDateTimeUtil
.
beginOfDay
(
startTime
);
endTime
=
LocalDateTimeUtil
.
endOfDay
(
endTime
);
// 2. 循环,生成时间范围
List
<
LocalDateTime
[]>
timeRanges
=
new
ArrayList
<>();
switch
(
intervalEnum
)
{
case
DAY:
while
(
startTime
.
isBefore
(
endTime
))
{
timeRanges
.
add
(
new
LocalDateTime
[]{
startTime
,
startTime
.
plusDays
(
1
).
minusNanos
(
1
)});
startTime
=
startTime
.
plusDays
(
1
);
}
break
;
case
WEEK:
while
(
startTime
.
isBefore
(
endTime
))
{
LocalDateTime
endOfWeek
=
startTime
.
with
(
DayOfWeek
.
SUNDAY
).
plusDays
(
1
).
minusNanos
(
1
);
timeRanges
.
add
(
new
LocalDateTime
[]{
startTime
,
endOfWeek
});
startTime
=
endOfWeek
.
plusNanos
(
1
);
}
break
;
case
MONTH:
while
(
startTime
.
isBefore
(
endTime
))
{
LocalDateTime
endOfMonth
=
startTime
.
with
(
TemporalAdjusters
.
lastDayOfMonth
()).
plusDays
(
1
).
minusNanos
(
1
);
timeRanges
.
add
(
new
LocalDateTime
[]{
startTime
,
endOfMonth
});
startTime
=
endOfMonth
.
plusNanos
(
1
);
}
break
;
case
QUARTER:
while
(
startTime
.
isBefore
(
endTime
))
{
int
quarterOfYear
=
getQuarterOfYear
(
startTime
);
LocalDateTime
quarterEnd
=
quarterOfYear
==
4
?
startTime
.
with
(
TemporalAdjusters
.
lastDayOfYear
()).
plusDays
(
1
).
minusNanos
(
1
)
:
startTime
.
withMonth
(
quarterOfYear
*
3
+
1
).
withDayOfMonth
(
1
).
minusNanos
(
1
);
timeRanges
.
add
(
new
LocalDateTime
[]{
startTime
,
quarterEnd
});
startTime
=
quarterEnd
.
plusNanos
(
1
);
}
break
;
case
YEAR:
while
(
startTime
.
isBefore
(
endTime
))
{
LocalDateTime
endOfYear
=
startTime
.
with
(
TemporalAdjusters
.
lastDayOfYear
()).
plusDays
(
1
).
minusNanos
(
1
);
timeRanges
.
add
(
new
LocalDateTime
[]{
startTime
,
endOfYear
});
startTime
=
endOfYear
.
plusNanos
(
1
);
}
break
;
default
:
throw
new
IllegalArgumentException
(
"Invalid interval: "
+
interval
);
}
// 3. 兜底,最后一个时间,需要保持在 endTime 之前
LocalDateTime
[]
lastTimeRange
=
CollUtil
.
getLast
(
timeRanges
);
if
(
lastTimeRange
!=
null
)
{
lastTimeRange
[
1
]
=
endTime
;
}
return
timeRanges
;
}
/**
* 格式化时间范围
*
* @param startTime 开始时间
* @param endTime 结束时间
* @param interval 时间间隔
* @return 时间范围
*/
public
static
String
formatDateRange
(
LocalDateTime
startTime
,
LocalDateTime
endTime
,
Integer
interval
)
{
// 1. 找到枚举
DateIntervalEnum
intervalEnum
=
DateIntervalEnum
.
valueOf
(
interval
);
Assert
.
notNull
(
intervalEnum
,
"interval({}} 找不到对应的枚举"
,
interval
);
// 2. 循环,生成时间范围
switch
(
intervalEnum
)
{
case
DAY:
return
LocalDateTimeUtil
.
format
(
startTime
,
DatePattern
.
NORM_DATE_PATTERN
);
case
WEEK:
return
LocalDateTimeUtil
.
format
(
startTime
,
DatePattern
.
NORM_DATE_PATTERN
)
+
StrUtil
.
format
(
"(第 {} 周)"
,
LocalDateTimeUtil
.
weekOfYear
(
startTime
));
case
MONTH:
return
LocalDateTimeUtil
.
format
(
startTime
,
DatePattern
.
NORM_MONTH_PATTERN
);
case
QUARTER:
return
StrUtil
.
format
(
"{}-Q{}"
,
startTime
.
getYear
(),
getQuarterOfYear
(
startTime
));
case
YEAR:
return
LocalDateTimeUtil
.
format
(
startTime
,
DatePattern
.
NORM_YEAR_PATTERN
);
default
:
throw
new
IllegalArgumentException
(
"Invalid interval: "
+
interval
);
}
}
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/util/ip/AreaUtils.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
util
.
ip
;
import
cn.hutool.core.io.resource.ResourceUtil
;
import
cn.hutool.core.lang.Assert
;
import
cn.hutool.core.text.csv.CsvRow
;
import
cn.hutool.core.text.csv.CsvUtil
;
import
lombok.NonNull
;
import
lombok.extern.slf4j.Slf4j
;
import
org.dromara.common.mall.domain.Area
;
import
org.dromara.common.mall.enums.AreaTypeEnum
;
import
org.dromara.common.mall.util.object.ObjectUtils
;
import
java.util.ArrayList
;
import
java.util.HashMap
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.function.Function
;
import
static
org
.
dromara
.
common
.
mall
.
util
.
collection
.
CollectionUtils
.
convertList
;
import
static
org
.
dromara
.
common
.
mall
.
util
.
collection
.
CollectionUtils
.
findFirst
;
/**
* 区域工具类
*
* @author 芋道源码
*/
@Slf4j
public
class
AreaUtils
{
/**
* 初始化 SEARCHER
*/
@SuppressWarnings
(
"InstantiationOfUtilityClass"
)
private
final
static
AreaUtils
INSTANCE
=
new
AreaUtils
();
/**
* Area 内存缓存,提升访问速度
*/
private
static
Map
<
Integer
,
Area
>
areas
;
private
AreaUtils
()
{
long
now
=
System
.
currentTimeMillis
();
areas
=
new
HashMap
<>();
areas
.
put
(
Area
.
ID_GLOBAL
,
new
Area
(
Area
.
ID_GLOBAL
,
"全球"
,
0
,
null
,
new
ArrayList
<>()));
// 从 csv 中加载数据
List
<
CsvRow
>
rows
=
CsvUtil
.
getReader
().
read
(
ResourceUtil
.
getUtf8Reader
(
"area.csv"
)).
getRows
();
rows
.
remove
(
0
);
// 删除 header
for
(
CsvRow
row
:
rows
)
{
// 创建 Area 对象
Area
area
=
new
Area
(
Integer
.
valueOf
(
row
.
get
(
0
)),
row
.
get
(
1
),
Integer
.
valueOf
(
row
.
get
(
2
)),
null
,
new
ArrayList
<>());
// 添加到 areas 中
areas
.
put
(
area
.
getId
(),
area
);
}
// 构建父子关系:因为 Area 中没有 parentId 字段,所以需要重复读取
for
(
CsvRow
row
:
rows
)
{
Area
area
=
areas
.
get
(
Integer
.
valueOf
(
row
.
get
(
0
)));
// 自己
Area
parent
=
areas
.
get
(
Integer
.
valueOf
(
row
.
get
(
3
)));
// 父
Assert
.
isTrue
(
area
!=
parent
,
"{}:父子节点相同"
,
area
.
getName
());
area
.
setParent
(
parent
);
parent
.
getChildren
().
add
(
area
);
}
log
.
info
(
"启动加载 AreaUtils 成功,耗时 ({}) 毫秒"
,
System
.
currentTimeMillis
()
-
now
);
}
/**
* 获得指定编号对应的区域
*
* @param id 区域编号
* @return 区域
*/
public
static
Area
getArea
(
Integer
id
)
{
return
areas
.
get
(
id
);
}
/**
* 获得指定区域对应的编号
*
* @param pathStr 区域路径,例如说:河南省/石家庄市/新华区
* @return 区域
*/
public
static
Area
parseArea
(
String
pathStr
)
{
String
[]
paths
=
pathStr
.
split
(
"/"
);
Area
area
=
null
;
for
(
String
path
:
paths
)
{
if
(
area
==
null
)
{
area
=
findFirst
(
areas
.
values
(),
item
->
item
.
getName
().
equals
(
path
));
}
else
{
area
=
findFirst
(
area
.
getChildren
(),
item
->
item
.
getName
().
equals
(
path
));
}
}
return
area
;
}
/**
* 获取所有节点的全路径名称如:河南省/石家庄市/新华区
*
* @param areas 地区树
* @return 所有节点的全路径名称
*/
public
static
List
<
String
>
getAreaNodePathList
(
List
<
Area
>
areas
)
{
List
<
String
>
paths
=
new
ArrayList
<>();
areas
.
forEach
(
area
->
getAreaNodePathList
(
area
,
""
,
paths
));
return
paths
;
}
/**
* 构建一棵树的所有节点的全路径名称,并将其存储为 "祖先/父级/子级" 的形式
*
* @param node 父节点
* @param path 全路径名称
* @param paths 全路径名称列表,省份/城市/地区
*/
private
static
void
getAreaNodePathList
(
Area
node
,
String
path
,
List
<
String
>
paths
)
{
if
(
node
==
null
)
{
return
;
}
// 构建当前节点的路径
String
currentPath
=
path
.
isEmpty
()
?
node
.
getName
()
:
path
+
"/"
+
node
.
getName
();
paths
.
add
(
currentPath
);
// 递归遍历子节点
for
(
Area
child
:
node
.
getChildren
())
{
getAreaNodePathList
(
child
,
currentPath
,
paths
);
}
}
/**
* 格式化区域
*
* @param id 区域编号
* @return 格式化后的区域
*/
public
static
String
format
(
Integer
id
)
{
return
format
(
id
,
" "
);
}
/**
* 格式化区域
* <p>
* 例如说:
* 1. id = “静安区”时:上海 上海市 静安区
* 2. id = “上海市”时:上海 上海市
* 3. id = “上海”时:上海
* 4. id = “美国”时:美国
* 当区域在中国时,默认不显示中国
*
* @param id 区域编号
* @param separator 分隔符
* @return 格式化后的区域
*/
public
static
String
format
(
Integer
id
,
String
separator
)
{
// 获得区域
Area
area
=
areas
.
get
(
id
);
if
(
area
==
null
)
{
return
null
;
}
// 格式化
StringBuilder
sb
=
new
StringBuilder
();
for
(
int
i
=
0
;
i
<
AreaTypeEnum
.
values
().
length
;
i
++)
{
// 避免死循环
sb
.
insert
(
0
,
area
.
getName
());
// “递归”父节点
area
=
area
.
getParent
();
if
(
area
==
null
||
ObjectUtils
.
equalsAny
(
area
.
getId
(),
Area
.
ID_GLOBAL
,
Area
.
ID_CHINA
))
{
// 跳过父节点为中国的情况
break
;
}
sb
.
insert
(
0
,
separator
);
}
return
sb
.
toString
();
}
/**
* 获取指定类型的区域列表
*
* @param type 区域类型
* @param func 转换函数
* @param <T> 结果类型
* @return 区域列表
*/
public
static
<
T
>
List
<
T
>
getByType
(
AreaTypeEnum
type
,
Function
<
Area
,
T
>
func
)
{
return
convertList
(
areas
.
values
(),
func
,
area
->
type
.
getType
().
equals
(
area
.
getType
()));
}
/**
* 根据区域编号、上级区域类型,获取上级区域编号
*
* @param id 区域编号
* @param type 区域类型
* @return 上级区域编号
*/
public
static
Integer
getParentIdByType
(
Integer
id
,
@NonNull
AreaTypeEnum
type
)
{
for
(
int
i
=
0
;
i
<
Byte
.
MAX_VALUE
;
i
++)
{
Area
area
=
AreaUtils
.
getArea
(
id
);
if
(
area
==
null
)
{
return
null
;
}
// 情况一:匹配到,返回它
if
(
type
.
getType
().
equals
(
area
.
getType
()))
{
return
area
.
getId
();
}
// 情况二:找到根节点,返回空
if
(
area
.
getParent
()
==
null
||
area
.
getParent
().
getId
()
==
null
)
{
return
null
;
}
// 其它:继续向上查找
id
=
area
.
getParent
().
getId
();
}
return
null
;
}
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/util/ip/IPUtils.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
util
.
ip
;
import
cn.hutool.core.io.resource.ResourceUtil
;
import
lombok.SneakyThrows
;
import
lombok.extern.slf4j.Slf4j
;
import
org.dromara.common.mall.domain.Area
;
import
org.lionsoul.ip2region.xdb.Searcher
;
import
java.io.IOException
;
/**
* IP 工具类
*
* IP 数据源来自 ip2region.xdb 精简版,基于 <a href="https://gitee.com/zhijiantianya/ip2region"/> 项目
*
* @author wanglhup
*/
@Slf4j
public
class
IPUtils
{
/**
* 初始化 SEARCHER
*/
@SuppressWarnings
(
"InstantiationOfUtilityClass"
)
private
final
static
IPUtils
INSTANCE
=
new
IPUtils
();
/**
* IP 查询器,启动加载到内存中
*/
private
static
Searcher
SEARCHER
;
/**
* 私有化构造
*/
private
IPUtils
()
{
try
{
long
now
=
System
.
currentTimeMillis
();
byte
[]
bytes
=
ResourceUtil
.
readBytes
(
"ip2region.xdb"
);
SEARCHER
=
Searcher
.
newWithBuffer
(
bytes
);
log
.
info
(
"启动加载 IPUtils 成功,耗时 ({}) 毫秒"
,
System
.
currentTimeMillis
()
-
now
);
}
catch
(
IOException
e
)
{
log
.
error
(
"启动加载 IPUtils 失败"
,
e
);
}
}
/**
* 查询 IP 对应的地区编号
*
* @param ip IP 地址,格式为 127.0.0.1
* @return 地区id
*/
@SneakyThrows
public
static
Integer
getAreaId
(
String
ip
)
{
return
Integer
.
parseInt
(
SEARCHER
.
search
(
ip
.
trim
()));
}
/**
* 查询 IP 对应的地区编号
*
* @param ip IP 地址的时间戳,格式参考{@link Searcher#checkIP(String)} 的返回
* @return 地区编号
*/
@SneakyThrows
public
static
Integer
getAreaId
(
long
ip
)
{
return
Integer
.
parseInt
(
SEARCHER
.
search
(
ip
));
}
/**
* 查询 IP 对应的地区
*
* @param ip IP 地址,格式为 127.0.0.1
* @return 地区
*/
public
static
Area
getArea
(
String
ip
)
{
return
AreaUtils
.
getArea
(
getAreaId
(
ip
));
}
/**
* 查询 IP 对应的地区
*
* @param ip IP 地址的时间戳,格式参考{@link Searcher#checkIP(String)} 的返回
* @return 地区
*/
public
static
Area
getArea
(
long
ip
)
{
return
AreaUtils
.
getArea
(
getAreaId
(
ip
));
}
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/util/number/MoneyUtils.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
util
.
number
;
import
cn.hutool.core.math.Money
;
import
cn.hutool.core.util.NumberUtil
;
import
java.math.BigDecimal
;
import
java.math.RoundingMode
;
/**
* 金额工具类
*
* @author 芋道源码
*/
public
class
MoneyUtils
{
/**
* 金额的小数位数
*/
private
static
final
int
PRICE_SCALE
=
2
;
/**
* 百分比对应的 BigDecimal 对象
*/
public
static
final
BigDecimal
PERCENT_100
=
BigDecimal
.
valueOf
(
100
);
/**
* 计算百分比金额,四舍五入
*
* @param price 金额
* @param rate 百分比,例如说 56.77% 则传入 56.77
* @return 百分比金额
*/
public
static
Integer
calculateRatePrice
(
Integer
price
,
Double
rate
)
{
return
calculateRatePrice
(
price
,
rate
,
0
,
RoundingMode
.
HALF_UP
).
intValue
();
}
/**
* 计算百分比金额,向下传入
*
* @param price 金额
* @param rate 百分比,例如说 56.77% 则传入 56.77
* @return 百分比金额
*/
public
static
Integer
calculateRatePriceFloor
(
Integer
price
,
Double
rate
)
{
return
calculateRatePrice
(
price
,
rate
,
0
,
RoundingMode
.
FLOOR
).
intValue
();
}
/**
* 计算百分比金额
*
* @param price 金额(单位分)
* @param count 数量
* @param percent 折扣(单位分),列如 60.2%,则传入 6020
* @return 商品总价
*/
public
static
Integer
calculator
(
Integer
price
,
Integer
count
,
Integer
percent
)
{
price
=
price
*
count
;
if
(
percent
==
null
)
{
return
price
;
}
return
MoneyUtils
.
calculateRatePriceFloor
(
price
,
(
double
)
(
percent
/
100
));
}
/**
* 计算百分比金额
*
* @param price 金额
* @param rate 百分比,例如说 56.77% 则传入 56.77
* @param scale 保留小数位数
* @param roundingMode 舍入模式
*/
public
static
BigDecimal
calculateRatePrice
(
Number
price
,
Number
rate
,
int
scale
,
RoundingMode
roundingMode
)
{
return
NumberUtil
.
toBigDecimal
(
price
).
multiply
(
NumberUtil
.
toBigDecimal
(
rate
))
// 乘以
.
divide
(
BigDecimal
.
valueOf
(
100
),
scale
,
roundingMode
);
// 除以 100
}
/**
* 分转元
*
* @param fen 分
* @return 元
*/
public
static
BigDecimal
fenToYuan
(
int
fen
)
{
return
new
Money
(
0
,
fen
).
getAmount
();
}
/**
* 分转元(字符串)
*
* 例如说 fen 为 1 时,则结果为 0.01
*
* @param fen 分
* @return 元
*/
public
static
String
fenToYuanStr
(
int
fen
)
{
return
new
Money
(
0
,
fen
).
toString
();
}
/**
* 金额相乘,默认进行四舍五入
*
* 位数:{@link #PRICE_SCALE}
*
* @param price 金额
* @param count 数量
* @return 金额相乘结果
*/
public
static
BigDecimal
priceMultiply
(
BigDecimal
price
,
BigDecimal
count
)
{
if
(
price
==
null
||
count
==
null
)
{
return
null
;
}
return
price
.
multiply
(
count
).
setScale
(
PRICE_SCALE
,
RoundingMode
.
HALF_UP
);
}
/**
* 金额相乘(百分比),默认进行四舍五入
*
* 位数:{@link #PRICE_SCALE}
*
* @param price 金额
* @param percent 百分比
* @return 金额相乘结果
*/
public
static
BigDecimal
priceMultiplyPercent
(
BigDecimal
price
,
BigDecimal
percent
)
{
if
(
price
==
null
||
percent
==
null
)
{
return
null
;
}
return
price
.
multiply
(
percent
).
divide
(
PERCENT_100
,
PRICE_SCALE
,
RoundingMode
.
HALF_UP
);
}
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/util/number/NumberUtils.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
util
.
number
;
import
cn.hutool.core.util.NumberUtil
;
import
cn.hutool.core.util.StrUtil
;
import
java.math.BigDecimal
;
/**
* 数字的工具类,补全 {@link NumberUtil} 的功能
*
* @author 芋道源码
*/
public
class
NumberUtils
{
public
static
Long
parseLong
(
String
str
)
{
return
StrUtil
.
isNotEmpty
(
str
)
?
Long
.
valueOf
(
str
)
:
null
;
}
public
static
Integer
parseInt
(
String
str
)
{
return
StrUtil
.
isNotEmpty
(
str
)
?
Integer
.
valueOf
(
str
)
:
null
;
}
/**
* 通过经纬度获取地球上两点之间的距离
*
* 参考 <<a href="https://gitee.com/dromara/hutool/blob/1caabb586b1f95aec66a21d039c5695df5e0f4c1/hutool-core/src/main/java/cn/hutool/core/util/DistanceUtil.java">DistanceUtil</a>> 实现,目前它已经被 hutool 删除
*
* @param lat1 经度1
* @param lng1 纬度1
* @param lat2 经度2
* @param lng2 纬度2
* @return 距离,单位:千米
*/
public
static
double
getDistance
(
double
lat1
,
double
lng1
,
double
lat2
,
double
lng2
)
{
double
radLat1
=
lat1
*
Math
.
PI
/
180.0
;
double
radLat2
=
lat2
*
Math
.
PI
/
180.0
;
double
a
=
radLat1
-
radLat2
;
double
b
=
lng1
*
Math
.
PI
/
180.0
-
lng2
*
Math
.
PI
/
180.0
;
double
distance
=
2
*
Math
.
asin
(
Math
.
sqrt
(
Math
.
pow
(
Math
.
sin
(
a
/
2
),
2
)
+
Math
.
cos
(
radLat1
)
*
Math
.
cos
(
radLat2
)
*
Math
.
pow
(
Math
.
sin
(
b
/
2
),
2
)));
distance
=
distance
*
6378.137
;
distance
=
Math
.
round
(
distance
*
10000
d
)
/
10000
d
;
return
distance
;
}
/**
* 提供精确的乘法运算
*
* 和 hutool {@link NumberUtil#mul(BigDecimal...)} 的差别是,如果存在 null,则返回 null
*
* @param values 多个被乘值
* @return 积
*/
public
static
BigDecimal
mul
(
BigDecimal
...
values
)
{
for
(
BigDecimal
value
:
values
)
{
if
(
value
==
null
)
{
return
null
;
}
}
return
NumberUtil
.
mul
(
values
);
}
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/util/object/BeanUtils.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
util
.
object
;
import
cn.hutool.core.bean.BeanUtil
;
import
org.dromara.common.mall.util.collection.CollectionUtils
;
import
java.util.List
;
import
java.util.function.Consumer
;
/**
* Bean 工具类
*
* 1. 默认使用 {@link BeanUtil} 作为实现类,虽然不同 bean 工具的性能有差别,但是对绝大多数同学的项目,不用在意这点性能
* 2. 针对复杂的对象转换,可以搜参考 AuthConvert 实现,通过 mapstruct + default 配合实现
*
* @author 芋道源码
*/
public
class
BeanUtils
{
public
static
<
T
>
T
toBean
(
Object
source
,
Class
<
T
>
targetClass
)
{
return
BeanUtil
.
toBean
(
source
,
targetClass
);
}
public
static
<
T
>
T
toBean
(
Object
source
,
Class
<
T
>
targetClass
,
Consumer
<
T
>
peek
)
{
T
target
=
toBean
(
source
,
targetClass
);
if
(
target
!=
null
)
{
peek
.
accept
(
target
);
}
return
target
;
}
public
static
<
S
,
T
>
List
<
T
>
toBean
(
List
<
S
>
source
,
Class
<
T
>
targetType
)
{
if
(
source
==
null
)
{
return
null
;
}
return
CollectionUtils
.
convertList
(
source
,
s
->
toBean
(
s
,
targetType
));
}
public
static
<
S
,
T
>
List
<
T
>
toBean
(
List
<
S
>
source
,
Class
<
T
>
targetType
,
Consumer
<
T
>
peek
)
{
List
<
T
>
list
=
toBean
(
source
,
targetType
);
if
(
list
!=
null
)
{
list
.
forEach
(
peek
);
}
return
list
;
}
public
static
void
copyProperties
(
Object
source
,
Object
target
)
{
if
(
source
==
null
||
target
==
null
)
{
return
;
}
BeanUtil
.
copyProperties
(
source
,
target
,
false
);
}
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/util/object/ObjectUtils.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
util
.
object
;
import
cn.hutool.core.util.ObjectUtil
;
import
cn.hutool.core.util.ReflectUtil
;
import
java.lang.reflect.Field
;
import
java.util.Arrays
;
import
java.util.function.Consumer
;
/**
* Object 工具类
*
* @author 芋道源码
*/
public
class
ObjectUtils
{
/**
* 复制对象,并忽略 Id 编号
*
* @param object 被复制对象
* @param consumer 消费者,可以二次编辑被复制对象
* @return 复制后的对象
*/
public
static
<
T
>
T
cloneIgnoreId
(
T
object
,
Consumer
<
T
>
consumer
)
{
T
result
=
ObjectUtil
.
clone
(
object
);
// 忽略 id 编号
Field
field
=
ReflectUtil
.
getField
(
object
.
getClass
(),
"id"
);
if
(
field
!=
null
)
{
ReflectUtil
.
setFieldValue
(
result
,
field
,
null
);
}
// 二次编辑
if
(
result
!=
null
)
{
consumer
.
accept
(
result
);
}
return
result
;
}
public
static
<
T
extends
Comparable
<
T
>>
T
max
(
T
obj1
,
T
obj2
)
{
if
(
obj1
==
null
)
{
return
obj2
;
}
if
(
obj2
==
null
)
{
return
obj1
;
}
return
obj1
.
compareTo
(
obj2
)
>
0
?
obj1
:
obj2
;
}
@SafeVarargs
public
static
<
T
>
T
defaultIfNull
(
T
...
array
)
{
for
(
T
item
:
array
)
{
if
(
item
!=
null
)
{
return
item
;
}
}
return
null
;
}
@SafeVarargs
public
static
<
T
>
boolean
equalsAny
(
T
obj
,
T
...
array
)
{
return
Arrays
.
asList
(
array
).
contains
(
obj
);
}
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/util/validation/ValidationUtils.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
util
.
validation
;
import
cn.hutool.core.collection.CollUtil
;
import
cn.hutool.core.lang.Assert
;
import
jakarta.validation.ConstraintViolation
;
import
jakarta.validation.ConstraintViolationException
;
import
jakarta.validation.Validation
;
import
jakarta.validation.Validator
;
import
org.springframework.util.StringUtils
;
import
java.util.Set
;
import
java.util.regex.Pattern
;
/**
* 校验工具类
*
* @author 芋道源码
*/
public
class
ValidationUtils
{
private
static
final
Pattern
PATTERN_MOBILE
=
Pattern
.
compile
(
"^(?:(?:\\+|00)86)?1(?:(?:3[\\d])|(?:4[0,1,4-9])|(?:5[0-3,5-9])|(?:6[2,5-7])|(?:7[0-8])|(?:8[\\d])|(?:9[0-3,5-9]))\\d{8}$"
);
private
static
final
Pattern
PATTERN_URL
=
Pattern
.
compile
(
"^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"
);
private
static
final
Pattern
PATTERN_XML_NCNAME
=
Pattern
.
compile
(
"[a-zA-Z_][\\-_.0-9_a-zA-Z$]*"
);
public
static
boolean
isMobile
(
String
mobile
)
{
return
StringUtils
.
hasText
(
mobile
)
&&
PATTERN_MOBILE
.
matcher
(
mobile
).
matches
();
}
public
static
boolean
isURL
(
String
url
)
{
return
StringUtils
.
hasText
(
url
)
&&
PATTERN_URL
.
matcher
(
url
).
matches
();
}
public
static
boolean
isXmlNCName
(
String
str
)
{
return
StringUtils
.
hasText
(
str
)
&&
PATTERN_XML_NCNAME
.
matcher
(
str
).
matches
();
}
public
static
void
validate
(
Object
object
,
Class
<?>...
groups
)
{
Validator
validator
=
Validation
.
buildDefaultValidatorFactory
().
getValidator
();
Assert
.
notNull
(
validator
);
validate
(
validator
,
object
,
groups
);
}
public
static
void
validate
(
Validator
validator
,
Object
object
,
Class
<?>...
groups
)
{
Set
<
ConstraintViolation
<
Object
>>
constraintViolations
=
validator
.
validate
(
object
,
groups
);
if
(
CollUtil
.
isNotEmpty
(
constraintViolations
))
{
throw
new
ConstraintViolationException
(
constraintViolations
);
}
}
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/validation/InEnum.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
validation
;
import
jakarta.validation.Constraint
;
import
jakarta.validation.Payload
;
import
org.dromara.common.mall.core.IntArrayValuable
;
import
java.lang.annotation.*
;
@Target
({
ElementType
.
METHOD
,
ElementType
.
FIELD
,
ElementType
.
ANNOTATION_TYPE
,
ElementType
.
CONSTRUCTOR
,
ElementType
.
PARAMETER
,
ElementType
.
TYPE_USE
})
@Retention
(
RetentionPolicy
.
RUNTIME
)
@Documented
@Constraint
(
validatedBy
=
{
InEnumValidator
.
class
,
InEnumCollectionValidator
.
class
}
)
public
@interface
InEnum
{
/**
* @return 实现 EnumValuable 接口的
*/
Class
<?
extends
IntArrayValuable
>
value
();
String
message
()
default
"必须在指定范围 {value}"
;
Class
<?>[]
groups
()
default
{};
Class
<?
extends
Payload
>[]
payload
()
default
{};
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/validation/InEnumCollectionValidator.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
validation
;
import
cn.hutool.core.collection.CollUtil
;
import
jakarta.validation.ConstraintValidator
;
import
jakarta.validation.ConstraintValidatorContext
;
import
org.dromara.common.mall.core.IntArrayValuable
;
import
java.util.Arrays
;
import
java.util.Collection
;
import
java.util.Collections
;
import
java.util.List
;
import
java.util.stream.Collectors
;
public
class
InEnumCollectionValidator
implements
ConstraintValidator
<
InEnum
,
Collection
<
Integer
>>
{
private
List
<
Integer
>
values
;
@Override
public
void
initialize
(
InEnum
annotation
)
{
IntArrayValuable
[]
values
=
annotation
.
value
().
getEnumConstants
();
if
(
values
.
length
==
0
)
{
this
.
values
=
Collections
.
emptyList
();
}
else
{
this
.
values
=
Arrays
.
stream
(
values
[
0
].
array
()).
boxed
().
collect
(
Collectors
.
toList
());
}
}
@Override
public
boolean
isValid
(
Collection
<
Integer
>
list
,
ConstraintValidatorContext
context
)
{
// 校验通过
if
(
CollUtil
.
containsAll
(
values
,
list
))
{
return
true
;
}
// 校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值)
context
.
disableDefaultConstraintViolation
();
// 禁用默认的 message 的值
context
.
buildConstraintViolationWithTemplate
(
context
.
getDefaultConstraintMessageTemplate
()
.
replaceAll
(
"\\{value}"
,
CollUtil
.
join
(
list
,
","
))).
addConstraintViolation
();
// 重新添加错误提示语句
return
false
;
}
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/validation/InEnumValidator.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
validation
;
import
jakarta.validation.ConstraintValidator
;
import
jakarta.validation.ConstraintValidatorContext
;
import
org.dromara.common.mall.core.IntArrayValuable
;
import
java.util.Arrays
;
import
java.util.Collections
;
import
java.util.List
;
import
java.util.stream.Collectors
;
public
class
InEnumValidator
implements
ConstraintValidator
<
InEnum
,
Integer
>
{
private
List
<
Integer
>
values
;
@Override
public
void
initialize
(
InEnum
annotation
)
{
IntArrayValuable
[]
values
=
annotation
.
value
().
getEnumConstants
();
if
(
values
.
length
==
0
)
{
this
.
values
=
Collections
.
emptyList
();
}
else
{
this
.
values
=
Arrays
.
stream
(
values
[
0
].
array
()).
boxed
().
collect
(
Collectors
.
toList
());
}
}
@Override
public
boolean
isValid
(
Integer
value
,
ConstraintValidatorContext
context
)
{
// 为空时,默认不校验,即认为通过
if
(
value
==
null
)
{
return
true
;
}
// 校验通过
if
(
values
.
contains
(
value
))
{
return
true
;
}
// 校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值)
context
.
disableDefaultConstraintViolation
();
// 禁用默认的 message 的值
context
.
buildConstraintViolationWithTemplate
(
context
.
getDefaultConstraintMessageTemplate
()
.
replaceAll
(
"\\{value}"
,
values
.
toString
())).
addConstraintViolation
();
// 重新添加错误提示语句
return
false
;
}
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/validation/Mobile.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
validation
;
import
jakarta.validation.Constraint
;
import
jakarta.validation.Payload
;
import
java.lang.annotation.*
;
@Target
({
ElementType
.
METHOD
,
ElementType
.
FIELD
,
ElementType
.
ANNOTATION_TYPE
,
ElementType
.
CONSTRUCTOR
,
ElementType
.
PARAMETER
,
ElementType
.
TYPE_USE
})
@Retention
(
RetentionPolicy
.
RUNTIME
)
@Documented
@Constraint
(
validatedBy
=
MobileValidator
.
class
)
public
@interface
Mobile
{
String
message
()
default
"手机号格式不正确"
;
Class
<?>[]
groups
()
default
{};
Class
<?
extends
Payload
>[]
payload
()
default
{};
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/validation/MobileValidator.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
validation
;
import
cn.hutool.core.util.StrUtil
;
import
jakarta.validation.ConstraintValidator
;
import
jakarta.validation.ConstraintValidatorContext
;
import
org.dromara.common.mall.util.validation.ValidationUtils
;
public
class
MobileValidator
implements
ConstraintValidator
<
Mobile
,
String
>
{
@Override
public
void
initialize
(
Mobile
annotation
)
{
}
@Override
public
boolean
isValid
(
String
value
,
ConstraintValidatorContext
context
)
{
// 如果手机号为空,默认不校验,即校验通过
if
(
StrUtil
.
isEmpty
(
value
))
{
return
true
;
}
// 校验手机
return
ValidationUtils
.
isMobile
(
value
);
}
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/validation/Telephone.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
validation
;
import
jakarta.validation.Constraint
;
import
jakarta.validation.Payload
;
import
java.lang.annotation.*
;
@Target
({
ElementType
.
METHOD
,
ElementType
.
FIELD
,
ElementType
.
ANNOTATION_TYPE
,
ElementType
.
CONSTRUCTOR
,
ElementType
.
PARAMETER
,
ElementType
.
TYPE_USE
})
@Retention
(
RetentionPolicy
.
RUNTIME
)
@Documented
@Constraint
(
validatedBy
=
TelephoneValidator
.
class
)
public
@interface
Telephone
{
String
message
()
default
"电话格式不正确"
;
Class
<?>[]
groups
()
default
{};
Class
<?
extends
Payload
>[]
payload
()
default
{};
}
ruoyi-common/ruoyi-common-mall/src/main/java/org/dromara/common/mall/validation/TelephoneValidator.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mall
.
validation
;
import
cn.hutool.core.text.CharSequenceUtil
;
import
cn.hutool.core.util.PhoneUtil
;
import
jakarta.validation.ConstraintValidator
;
import
jakarta.validation.ConstraintValidatorContext
;
public
class
TelephoneValidator
implements
ConstraintValidator
<
Telephone
,
String
>
{
@Override
public
void
initialize
(
Telephone
annotation
)
{
}
@Override
public
boolean
isValid
(
String
value
,
ConstraintValidatorContext
context
)
{
// 如果手机号为空,默认不校验,即校验通过
if
(
CharSequenceUtil
.
isEmpty
(
value
))
{
return
true
;
}
// 校验手机
return
PhoneUtil
.
isTel
(
value
)
||
PhoneUtil
.
isPhone
(
value
);
}
}
ruoyi-common/ruoyi-common-mall/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
0 → 100644
浏览文件 @
44629dc1
ruoyi-common/ruoyi-common-mall/src/main/resources/area.csv
0 → 100644
浏览文件 @
44629dc1
This source diff could not be displayed because it is too large. You can
view the blob
instead.
ruoyi-common/ruoyi-common-mall/src/main/resources/ip2region.xdb
0 → 100644
浏览文件 @
44629dc1
File added
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/mapper/BaseMapperPlus.java
浏览文件 @
44629dc1
...
@@ -3,10 +3,12 @@ package org.dromara.common.mybatis.core.mapper;
...
@@ -3,10 +3,12 @@ package org.dromara.common.mybatis.core.mapper;
import
cn.hutool.core.collection.CollUtil
;
import
cn.hutool.core.collection.CollUtil
;
import
cn.hutool.core.util.ObjectUtil
;
import
cn.hutool.core.util.ObjectUtil
;
import
com.baomidou.mybatisplus.core.conditions.Wrapper
;
import
com.baomidou.mybatisplus.core.conditions.Wrapper
;
import
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper
;
import
com.baomidou.mybatisplus.core.conditions.query.QueryWrapper
;
import
com.baomidou.mybatisplus.core.conditions.query.QueryWrapper
;
import
com.baomidou.mybatisplus.core.mapper.BaseMapper
;
import
com.baomidou.mybatisplus.core.mapper.BaseMapper
;
import
com.baomidou.mybatisplus.core.metadata.IPage
;
import
com.baomidou.mybatisplus.core.metadata.IPage
;
import
com.baomidou.mybatisplus.core.toolkit.ReflectionKit
;
import
com.baomidou.mybatisplus.core.toolkit.ReflectionKit
;
import
com.baomidou.mybatisplus.core.toolkit.support.SFunction
;
import
com.baomidou.mybatisplus.extension.plugins.pagination.Page
;
import
com.baomidou.mybatisplus.extension.plugins.pagination.Page
;
import
com.baomidou.mybatisplus.extension.toolkit.Db
;
import
com.baomidou.mybatisplus.extension.toolkit.Db
;
import
org.apache.ibatis.logging.Log
;
import
org.apache.ibatis.logging.Log
;
...
@@ -128,6 +130,12 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
...
@@ -128,6 +130,12 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
return
Db
.
saveOrUpdateBatch
(
entityList
,
batchSize
);
return
Db
.
saveOrUpdateBatch
(
entityList
,
batchSize
);
}
}
default
Boolean
updateBatch
(
Collection
<
T
>
entities
)
{
return
Db
.
updateBatchById
(
entities
);
}
/**
/**
* 根据ID查询单个VO对象
* 根据ID查询单个VO对象
*
*
...
@@ -340,4 +348,161 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
...
@@ -340,4 +348,161 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
return
this
.
selectObjs
(
wrapper
).
stream
().
filter
(
Objects:
:
nonNull
).
map
(
mapper
).
collect
(
Collectors
.
toList
());
return
this
.
selectObjs
(
wrapper
).
stream
().
filter
(
Objects:
:
nonNull
).
map
(
mapper
).
collect
(
Collectors
.
toList
());
}
}
/**
* 根据条件查询符合条件的对象,并将其转换为指定类型的对象列表
*
* @param field 字段表达式
* @param value 字段值
* @return 查询到的符合条件的对象列表,经过转换为指定类型的对象后返回
*/
default
T
selectOne
(
SFunction
<
T
,
?>
field
,
Object
value
)
{
return
selectOne
(
new
LambdaQueryWrapper
<
T
>().
eq
(
field
,
value
));
}
/**
* 根据条件查询符合条件的对象,并将其转换为指定类型的对象列表
*
* @param field1 字段表达式
* @param value1 字段值
* @param field2 字段表达式
* @param value2 字段值
* @return
*/
default
T
selectOne
(
String
field1
,
Object
value1
,
String
field2
,
Object
value2
)
{
return
selectOne
(
new
QueryWrapper
<
T
>().
eq
(
field1
,
value1
).
eq
(
field2
,
value2
));
}
/**
* 根据条件查询符合条件的对象,并将其转换为指定类型的对象列表
*
* @param field1 字段表达式
* @param value1 字段值
* @param field2 字段表达式
* @param value2 字段值
* @return 查询到的符合条件的对象列表,经过转换为指定类型的对象后返回
*/
default
T
selectOne
(
SFunction
<
T
,
?>
field1
,
Object
value1
,
SFunction
<
T
,
?>
field2
,
Object
value2
)
{
return
selectOne
(
new
LambdaQueryWrapper
<
T
>().
eq
(
field1
,
value1
).
eq
(
field2
,
value2
));
}
/**
* 根据条件查询符合条件的对象,并将其转换为指定类型的对象列表
*
* @param field1 字段表达式
* @param value1 字段值
* @param field2 字段表达式
* @param value2 字段值
* @param field3 字段表达式
* @param value3 字段值
* @return 查询到的符合条件的对象列表,经过转换为指定类型的对象后返回
*/
default
T
selectOne
(
SFunction
<
T
,
?>
field1
,
Object
value1
,
SFunction
<
T
,
?>
field2
,
Object
value2
,
SFunction
<
T
,
?>
field3
,
Object
value3
)
{
return
selectOne
(
new
LambdaQueryWrapper
<
T
>().
eq
(
field1
,
value1
).
eq
(
field2
,
value2
)
.
eq
(
field3
,
value3
));
}
/**
* 根据条件查询符合条件的对象,并将其转换为指定类型的对象列表
*
* @param field 字段表达式
* @param value 字段值
* @return 查询到的符合条件的对象列表,经过转换为指定类型的对象后返回
*/
default
List
<
T
>
selectList
(
String
field
,
Object
value
)
{
return
selectList
(
new
QueryWrapper
<
T
>().
eq
(
field
,
value
));
}
/**
* 根据条件查询符合条件的对象,并将其转换为指定类型的对象列表
*
* @param field 字段表达式
* @param value 字段值
* @return 查询到的符合条件的对象列表,经过转换为指定类型的对象后返回
*/
default
List
<
T
>
selectList
(
SFunction
<
T
,
?>
field
,
Object
value
)
{
return
selectList
(
new
LambdaQueryWrapper
<
T
>().
eq
(
field
,
value
));
}
/**
* 根据条件查询符合条件的对象,并将其转换为指定类型的对象列表
*
* @param field 字段表达式
* @param values 字段值集合
* @return 查询到的符合条件的对象列表,经过转换为指定类型的对象后返回
*/
default
List
<
T
>
selectList
(
String
field
,
Collection
<?>
values
)
{
if
(
CollUtil
.
isEmpty
(
values
))
{
return
CollUtil
.
newArrayList
();
}
return
selectList
(
new
QueryWrapper
<
T
>().
in
(
field
,
values
));
}
/**
* 根据条件查询符合条件的对象,并将其转换为指定类型的对象列表
*
* @param field 字段表达式
* @param values 字段值集合
* @return 查询到的符合条件的对象列表,经过转换为指定类型的对象后返回
*/
default
List
<
T
>
selectList
(
SFunction
<
T
,
?>
field
,
Collection
<?>
values
)
{
if
(
CollUtil
.
isEmpty
(
values
))
{
return
CollUtil
.
newArrayList
();
}
return
selectList
(
new
LambdaQueryWrapper
<
T
>().
in
(
field
,
values
));
}
/**
* 根据条件查询符合条件的对象,并将其转换为指定类型的对象列表
*
* @param field1 字段表达式
* @param value1 字段值
* @param field2 字段表达式
* @param value2 字段值
* @return 查询到的符合条件的对象列表,经过转换为指定类型的对象后返回
*/
default
List
<
T
>
selectList
(
SFunction
<
T
,
?>
field1
,
Object
value1
,
SFunction
<
T
,
?>
field2
,
Object
value2
)
{
return
selectList
(
new
LambdaQueryWrapper
<
T
>().
eq
(
field1
,
value1
).
eq
(
field2
,
value2
));
}
/**
* 根据条件查询符合条件的对象数量
*
* @return 查询到的符合条件的对象数量
*/
default
Long
selectCount
()
{
return
selectCount
(
new
QueryWrapper
<>());
}
/**
* 根据条件查询符合条件的对象数量
*
* @param field 字段表达式
* @param value 字段值
* @return 查询到的符合条件的对象数量
*/
default
Long
selectCount
(
String
field
,
Object
value
)
{
return
selectCount
(
new
QueryWrapper
<
T
>().
eq
(
field
,
value
));
}
/**
* 根据条件查询符合条件的对象数量
*
* @param field 字段表达式
* @param value 字段值
* @return 查询到的符合条件的对象数量
*/
default
Long
selectCount
(
SFunction
<
T
,
?>
field
,
Object
value
)
{
return
selectCount
(
new
LambdaQueryWrapper
<
T
>().
eq
(
field
,
value
));
}
default
int
delete
(
String
field
,
String
value
)
{
return
delete
(
new
QueryWrapper
<
T
>().
eq
(
field
,
value
));
}
default
int
delete
(
SFunction
<
T
,
?>
field
,
Object
value
)
{
return
delete
(
new
LambdaQueryWrapper
<
T
>().
eq
(
field
,
value
));
}
}
}
ruoyi-common/ruoyi-common-satoken/pom.xml
浏览文件 @
44629dc1
...
@@ -46,6 +46,11 @@
...
@@ -46,6 +46,11 @@
<artifactId>
caffeine
</artifactId>
<artifactId>
caffeine
</artifactId>
</dependency>
</dependency>
<dependency>
<groupId>
org.dromara
</groupId>
<artifactId>
ruoyi-common-mall
</artifactId>
</dependency>
</dependencies>
</dependencies>
</project>
</project>
ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/utils/LoginHelper.java
浏览文件 @
44629dc1
...
@@ -11,6 +11,7 @@ import lombok.NoArgsConstructor;
...
@@ -11,6 +11,7 @@ import lombok.NoArgsConstructor;
import
org.dromara.common.core.constant.TenantConstants
;
import
org.dromara.common.core.constant.TenantConstants
;
import
org.dromara.common.core.constant.UserConstants
;
import
org.dromara.common.core.constant.UserConstants
;
import
org.dromara.common.core.enums.UserType
;
import
org.dromara.common.core.enums.UserType
;
import
org.dromara.common.mall.enums.TerminalEnum
;
import
org.dromara.system.api.model.LoginUser
;
import
org.dromara.system.api.model.LoginUser
;
import
java.util.Optional
;
import
java.util.Optional
;
...
@@ -41,6 +42,7 @@ public class LoginHelper {
...
@@ -41,6 +42,7 @@ public class LoginHelper {
public
static
final
String
CLIENT_KEY
=
"clientid"
;
public
static
final
String
CLIENT_KEY
=
"clientid"
;
public
static
final
String
YS_USER_ID
=
"ysUserId"
;
public
static
final
String
YS_USER_ID
=
"ysUserId"
;
public
static
final
String
OPEN_ID
=
"openId"
;
public
static
final
String
OPEN_ID
=
"openId"
;
public
static
final
String
MEMBER_ID
=
"memberId"
;
/**
/**
* 登录系统 基于 设备类型
* 登录系统 基于 设备类型
...
@@ -133,6 +135,20 @@ public class LoginHelper {
...
@@ -133,6 +135,20 @@ public class LoginHelper {
return
Convert
.
toStr
(
getExtra
(
DEPT_NAME_KEY
));
return
Convert
.
toStr
(
getExtra
(
DEPT_NAME_KEY
));
}
}
/**
* 获取会员id
*/
public
static
Long
getMemberId
()
{
return
Convert
.
toLong
(
getExtra
(
MEMBER_ID
));
}
/**
* 获取客户端ID
*/
public
static
Integer
getTerminal
()
{
return
getMemberId
()
==
null
?
TerminalEnum
.
H5
.
getTerminal
()
:
TerminalEnum
.
WECHAT_MINI_PROGRAM
.
getTerminal
();
}
/**
/**
* 获取是否是虚拟租户
* 获取是否是虚拟租户
*/
*/
...
...
ruoyi-common/ruoyi-common-yudao-mybatis/pom.xml
0 → 100644
浏览文件 @
44629dc1
<?xml version="1.0" encoding="UTF-8"?>
<project
xmlns=
"http://maven.apache.org/POM/4.0.0"
xmlns:xsi=
"http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=
"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
>
<parent>
<groupId>
org.dromara
</groupId>
<artifactId>
ruoyi-common
</artifactId>
<version>
${revision}
</version>
</parent>
<modelVersion>
4.0.0
</modelVersion>
<artifactId>
ruoyi-common-yudao-mybatis
</artifactId>
<description>
ruoyi-common-yudao-mybatis 数据库服务
</description>
<dependencies>
<dependency>
<groupId>
org.dromara
</groupId>
<artifactId>
ruoyi-common-mybatis
</artifactId>
</dependency>
<dependency>
<groupId>
org.dromara
</groupId>
<artifactId>
ruoyi-common-mall
</artifactId>
</dependency>
<dependency>
<groupId>
com.github.yulichang
</groupId>
<artifactId>
mybatis-plus-join-core
</artifactId>
<version>
1.4.13
</version>
<scope>
compile
</scope>
</dependency>
</dependencies>
</project>
ruoyi-common/ruoyi-common-yudao-mybatis/src/main/java/org/dromara/common/mybatis/core/mapper/BaseMapperPlusPlus.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mybatis
.
core
.
mapper
;
import
com.baomidou.mybatisplus.core.conditions.Wrapper
;
import
com.baomidou.mybatisplus.core.metadata.IPage
;
import
com.github.yulichang.base.MPJBaseMapper
;
import
org.apache.ibatis.annotations.Param
;
import
org.apache.ibatis.logging.Log
;
import
org.apache.ibatis.logging.LogFactory
;
import
org.dromara.common.mybatis.core.page.PageParam
;
import
org.dromara.common.mybatis.core.page.PageResult
;
import
org.dromara.common.mybatis.core.page.SortablePageParam
;
import
org.dromara.common.mybatis.core.page.SortingField
;
import
org.dromara.common.mybatis.util.MyBatisUtils
;
import
java.util.Collection
;
import
java.util.List
;
/**
* 自定义 Mapper 接口, 实现 自定义扩展
*
* @param <T> table 泛型
* @param <V> vo 泛型
* @author Lion Li
* @since 2021-05-13
*/
@SuppressWarnings
(
"unchecked"
)
public
interface
BaseMapperPlusPlus
<
T
,
V
>
extends
BaseMapperPlus
<
T
,
V
>,
MPJBaseMapper
<
T
>
{
Log
log
=
LogFactory
.
getLog
(
BaseMapperPlus
.
class
);
default
PageResult
<
T
>
selectPagePlus
(
SortablePageParam
pageParam
,
@Param
(
"ew"
)
Wrapper
<
T
>
queryWrapper
)
{
return
selectPagePlus
(
pageParam
,
pageParam
.
getSortingFields
(),
queryWrapper
);
}
default
PageResult
<
T
>
selectPagePlus
(
PageParam
pageParam
,
@Param
(
"ew"
)
Wrapper
<
T
>
queryWrapper
)
{
return
selectPagePlus
(
pageParam
,
null
,
queryWrapper
);
}
default
PageResult
<
T
>
selectPagePlus
(
PageParam
pageParam
,
Collection
<
SortingField
>
sortingFields
,
@Param
(
"ew"
)
Wrapper
<
T
>
queryWrapper
)
{
// 特殊:不分页,直接查询全部
if
(
PageParam
.
PAGE_SIZE_NONE
.
equals
(
pageParam
.
getPageSize
()))
{
List
<
T
>
list
=
selectList
(
queryWrapper
);
return
new
PageResult
<>(
list
,
(
long
)
list
.
size
());
}
// MyBatis Plus 查询
IPage
<
T
>
mpPage
=
MyBatisUtils
.
buildPage
(
pageParam
,
sortingFields
);
selectPage
(
mpPage
,
queryWrapper
);
// 转换返回
return
new
PageResult
<>(
mpPage
.
getRecords
(),
mpPage
.
getTotal
());
}
}
ruoyi-common/ruoyi-common-yudao-mybatis/src/main/java/org/dromara/common/mybatis/core/page/PageParam.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mybatis
.
core
.
page
;
import
io.swagger.v3.oas.annotations.media.Schema
;
import
jakarta.validation.constraints.Max
;
import
jakarta.validation.constraints.Min
;
import
jakarta.validation.constraints.NotNull
;
import
lombok.Data
;
import
java.io.Serializable
;
@Schema
(
description
=
"分页参数"
)
@Data
public
class
PageParam
implements
Serializable
{
private
static
final
Integer
PAGE_NO
=
1
;
private
static
final
Integer
PAGE_SIZE
=
10
;
/**
* 每页条数 - 不分页
*
* 例如说,导出接口,可以设置 {@link #pageSize} 为 -1 不分页,查询所有数据。
*/
public
static
final
Integer
PAGE_SIZE_NONE
=
-
1
;
@Schema
(
description
=
"页码,从 1 开始"
,
requiredMode
=
Schema
.
RequiredMode
.
REQUIRED
,
example
=
"1"
)
@NotNull
(
message
=
"页码不能为空"
)
@Min
(
value
=
1
,
message
=
"页码最小值为 1"
)
private
Integer
pageNo
=
PAGE_NO
;
@Schema
(
description
=
"每页条数,最大值为 100"
,
requiredMode
=
Schema
.
RequiredMode
.
REQUIRED
,
example
=
"10"
)
@NotNull
(
message
=
"每页条数不能为空"
)
@Min
(
value
=
1
,
message
=
"每页条数最小值为 1"
)
@Max
(
value
=
100
,
message
=
"每页条数最大值为 100"
)
private
Integer
pageSize
=
PAGE_SIZE
;
}
ruoyi-common/ruoyi-common-yudao-mybatis/src/main/java/org/dromara/common/mybatis/core/page/PageResult.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mybatis
.
core
.
page
;
import
cn.hutool.core.bean.BeanUtil
;
import
io.swagger.v3.oas.annotations.media.Schema
;
import
lombok.Data
;
import
java.io.Serializable
;
import
java.util.ArrayList
;
import
java.util.List
;
@Schema
(
description
=
"分页结果"
)
@Data
public
final
class
PageResult
<
T
>
implements
Serializable
{
@Schema
(
description
=
"数据"
,
requiredMode
=
Schema
.
RequiredMode
.
REQUIRED
)
private
List
<
T
>
list
;
@Schema
(
description
=
"总量"
,
requiredMode
=
Schema
.
RequiredMode
.
REQUIRED
)
private
Long
total
;
public
PageResult
()
{
}
public
PageResult
(
List
<
T
>
list
,
Long
total
)
{
this
.
list
=
list
;
this
.
total
=
total
;
}
public
PageResult
(
Long
total
)
{
this
.
list
=
new
ArrayList
<>();
this
.
total
=
total
;
}
public
static
<
T
>
PageResult
<
T
>
empty
()
{
return
new
PageResult
<>(
0L
);
}
public
static
<
T
>
PageResult
<
T
>
empty
(
Long
total
)
{
return
new
PageResult
<>(
total
);
}
public
static
<
T
>
PageResult
<
T
>
convertPage
(
PageResult
<?>
page
,
Class
<
T
>
clazz
)
{
if
(
page
==
null
)
{
return
null
;
}
PageResult
<
T
>
pageResult
=
new
PageResult
<>();
pageResult
.
setList
(
BeanUtil
.
copyToList
(
page
.
getList
(),
clazz
));
pageResult
.
setTotal
(
page
.
getTotal
());
return
pageResult
;
}
}
ruoyi-common/ruoyi-common-yudao-mybatis/src/main/java/org/dromara/common/mybatis/core/page/SortablePageParam.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mybatis
.
core
.
page
;
import
io.swagger.v3.oas.annotations.media.Schema
;
import
lombok.Data
;
import
lombok.EqualsAndHashCode
;
import
lombok.ToString
;
import
java.util.List
;
@Schema
(
description
=
"可排序的分页参数"
)
@Data
@EqualsAndHashCode
(
callSuper
=
true
)
@ToString
(
callSuper
=
true
)
public
class
SortablePageParam
extends
PageParam
{
@Schema
(
description
=
"排序字段"
)
private
List
<
SortingField
>
sortingFields
;
}
ruoyi-common/ruoyi-common-yudao-mybatis/src/main/java/org/dromara/common/mybatis/core/page/SortingField.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mybatis
.
core
.
page
;
import
lombok.AllArgsConstructor
;
import
lombok.Data
;
import
lombok.NoArgsConstructor
;
import
java.io.Serializable
;
/**
* 排序字段 DTO
*
* 类名加了 ing 的原因是,避免和 ES SortField 重名。
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public
class
SortingField
implements
Serializable
{
/**
* 顺序 - 升序
*/
public
static
final
String
ORDER_ASC
=
"asc"
;
/**
* 顺序 - 降序
*/
public
static
final
String
ORDER_DESC
=
"desc"
;
/**
* 字段
*/
private
String
field
;
/**
* 顺序
*/
private
String
order
;
}
ruoyi-common/ruoyi-common-yudao-mybatis/src/main/java/org/dromara/common/mybatis/core/query/LambdaQueryWrapperX.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mybatis
.
core
.
query
;
import
cn.hutool.core.util.ArrayUtil
;
import
cn.hutool.core.util.ObjectUtil
;
import
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper
;
import
com.baomidou.mybatisplus.core.toolkit.support.SFunction
;
import
org.apache.commons.lang3.ArrayUtils
;
import
org.springframework.util.StringUtils
;
import
java.util.Collection
;
/**
* 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能:
* <p>
* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
*
* @param <T> 数据类型
*/
public
class
LambdaQueryWrapperX
<
T
>
extends
LambdaQueryWrapper
<
T
>
{
public
LambdaQueryWrapperX
<
T
>
likeIfPresent
(
SFunction
<
T
,
?>
column
,
String
val
)
{
if
(
StringUtils
.
hasText
(
val
))
{
return
(
LambdaQueryWrapperX
<
T
>)
super
.
like
(
column
,
val
);
}
return
this
;
}
public
LambdaQueryWrapperX
<
T
>
inIfPresent
(
SFunction
<
T
,
?>
column
,
Collection
<?>
values
)
{
if
(
ObjectUtil
.
isAllNotEmpty
(
values
)
&&
!
ArrayUtil
.
isEmpty
(
values
))
{
return
(
LambdaQueryWrapperX
<
T
>)
super
.
in
(
column
,
values
);
}
return
this
;
}
public
LambdaQueryWrapperX
<
T
>
inIfPresent
(
SFunction
<
T
,
?>
column
,
Object
...
values
)
{
if
(
ObjectUtil
.
isAllNotEmpty
(
values
)
&&
!
ArrayUtil
.
isEmpty
(
values
))
{
return
(
LambdaQueryWrapperX
<
T
>)
super
.
in
(
column
,
values
);
}
return
this
;
}
public
LambdaQueryWrapperX
<
T
>
eqIfPresent
(
SFunction
<
T
,
?>
column
,
Object
val
)
{
if
(
ObjectUtil
.
isNotEmpty
(
val
))
{
return
(
LambdaQueryWrapperX
<
T
>)
super
.
eq
(
column
,
val
);
}
return
this
;
}
public
LambdaQueryWrapperX
<
T
>
neIfPresent
(
SFunction
<
T
,
?>
column
,
Object
val
)
{
if
(
ObjectUtil
.
isNotEmpty
(
val
))
{
return
(
LambdaQueryWrapperX
<
T
>)
super
.
ne
(
column
,
val
);
}
return
this
;
}
public
LambdaQueryWrapperX
<
T
>
gtIfPresent
(
SFunction
<
T
,
?>
column
,
Object
val
)
{
if
(
val
!=
null
)
{
return
(
LambdaQueryWrapperX
<
T
>)
super
.
gt
(
column
,
val
);
}
return
this
;
}
public
LambdaQueryWrapperX
<
T
>
geIfPresent
(
SFunction
<
T
,
?>
column
,
Object
val
)
{
if
(
val
!=
null
)
{
return
(
LambdaQueryWrapperX
<
T
>)
super
.
ge
(
column
,
val
);
}
return
this
;
}
public
LambdaQueryWrapperX
<
T
>
ltIfPresent
(
SFunction
<
T
,
?>
column
,
Object
val
)
{
if
(
val
!=
null
)
{
return
(
LambdaQueryWrapperX
<
T
>)
super
.
lt
(
column
,
val
);
}
return
this
;
}
public
LambdaQueryWrapperX
<
T
>
leIfPresent
(
SFunction
<
T
,
?>
column
,
Object
val
)
{
if
(
val
!=
null
)
{
return
(
LambdaQueryWrapperX
<
T
>)
super
.
le
(
column
,
val
);
}
return
this
;
}
public
LambdaQueryWrapperX
<
T
>
betweenIfPresent
(
SFunction
<
T
,
?>
column
,
Object
val1
,
Object
val2
)
{
if
(
val1
!=
null
&&
val2
!=
null
)
{
return
(
LambdaQueryWrapperX
<
T
>)
super
.
between
(
column
,
val1
,
val2
);
}
if
(
val1
!=
null
)
{
return
(
LambdaQueryWrapperX
<
T
>)
ge
(
column
,
val1
);
}
if
(
val2
!=
null
)
{
return
(
LambdaQueryWrapperX
<
T
>)
le
(
column
,
val2
);
}
return
this
;
}
public
LambdaQueryWrapperX
<
T
>
betweenIfPresent
(
SFunction
<
T
,
?>
column
,
Object
[]
values
)
{
Object
val1
=
ArrayUtils
.
get
(
values
,
0
);
Object
val2
=
ArrayUtils
.
get
(
values
,
1
);
return
betweenIfPresent
(
column
,
val1
,
val2
);
}
// ========== 重写父类方法,方便链式调用 ==========
@Override
public
LambdaQueryWrapperX
<
T
>
eq
(
boolean
condition
,
SFunction
<
T
,
?>
column
,
Object
val
)
{
super
.
eq
(
condition
,
column
,
val
);
return
this
;
}
@Override
public
LambdaQueryWrapperX
<
T
>
eq
(
SFunction
<
T
,
?>
column
,
Object
val
)
{
super
.
eq
(
column
,
val
);
return
this
;
}
@Override
public
LambdaQueryWrapperX
<
T
>
orderByDesc
(
SFunction
<
T
,
?>
column
)
{
super
.
orderByDesc
(
true
,
column
);
return
this
;
}
@Override
public
LambdaQueryWrapperX
<
T
>
last
(
String
lastSql
)
{
super
.
last
(
lastSql
);
return
this
;
}
@Override
public
LambdaQueryWrapperX
<
T
>
in
(
SFunction
<
T
,
?>
column
,
Collection
<?>
coll
)
{
super
.
in
(
column
,
coll
);
return
this
;
}
}
ruoyi-common/ruoyi-common-yudao-mybatis/src/main/java/org/dromara/common/mybatis/core/query/MPJLambdaWrapperX.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mybatis
.
core
.
query
;
import
cn.hutool.core.util.ArrayUtil
;
import
cn.hutool.core.util.ObjectUtil
;
import
com.baomidou.mybatisplus.core.toolkit.support.SFunction
;
import
com.github.yulichang.toolkit.MPJWrappers
;
import
com.github.yulichang.wrapper.MPJLambdaWrapper
;
import
org.apache.commons.lang3.ArrayUtils
;
import
org.springframework.util.StringUtils
;
import
java.util.Collection
;
import
java.util.function.Consumer
;
/**
* 拓展 MyBatis Plus Join QueryWrapper 类,主要增加如下功能:
* <p>
* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
*
* @param <T> 数据类型
*/
public
class
MPJLambdaWrapperX
<
T
>
extends
MPJLambdaWrapper
<
T
>
{
public
MPJLambdaWrapperX
<
T
>
likeIfPresent
(
SFunction
<
T
,
?>
column
,
String
val
)
{
MPJWrappers
.
lambdaJoin
().
like
(
column
,
val
);
if
(
StringUtils
.
hasText
(
val
))
{
return
(
MPJLambdaWrapperX
<
T
>)
super
.
like
(
column
,
val
);
}
return
this
;
}
public
MPJLambdaWrapperX
<
T
>
inIfPresent
(
SFunction
<
T
,
?>
column
,
Collection
<?>
values
)
{
if
(
ObjectUtil
.
isAllNotEmpty
(
values
)
&&
!
ArrayUtil
.
isEmpty
(
values
))
{
return
(
MPJLambdaWrapperX
<
T
>)
super
.
in
(
column
,
values
);
}
return
this
;
}
public
MPJLambdaWrapperX
<
T
>
inIfPresent
(
SFunction
<
T
,
?>
column
,
Object
...
values
)
{
if
(
ObjectUtil
.
isAllNotEmpty
(
values
)
&&
!
ArrayUtil
.
isEmpty
(
values
))
{
return
(
MPJLambdaWrapperX
<
T
>)
super
.
in
(
column
,
values
);
}
return
this
;
}
public
MPJLambdaWrapperX
<
T
>
eqIfPresent
(
SFunction
<
T
,
?>
column
,
Object
val
)
{
if
(
ObjectUtil
.
isNotEmpty
(
val
))
{
return
(
MPJLambdaWrapperX
<
T
>)
super
.
eq
(
column
,
val
);
}
return
this
;
}
public
MPJLambdaWrapperX
<
T
>
neIfPresent
(
SFunction
<
T
,
?>
column
,
Object
val
)
{
if
(
ObjectUtil
.
isNotEmpty
(
val
))
{
return
(
MPJLambdaWrapperX
<
T
>)
super
.
ne
(
column
,
val
);
}
return
this
;
}
public
MPJLambdaWrapperX
<
T
>
gtIfPresent
(
SFunction
<
T
,
?>
column
,
Object
val
)
{
if
(
val
!=
null
)
{
return
(
MPJLambdaWrapperX
<
T
>)
super
.
gt
(
column
,
val
);
}
return
this
;
}
public
MPJLambdaWrapperX
<
T
>
geIfPresent
(
SFunction
<
T
,
?>
column
,
Object
val
)
{
if
(
val
!=
null
)
{
return
(
MPJLambdaWrapperX
<
T
>)
super
.
ge
(
column
,
val
);
}
return
this
;
}
public
MPJLambdaWrapperX
<
T
>
ltIfPresent
(
SFunction
<
T
,
?>
column
,
Object
val
)
{
if
(
val
!=
null
)
{
return
(
MPJLambdaWrapperX
<
T
>)
super
.
lt
(
column
,
val
);
}
return
this
;
}
public
MPJLambdaWrapperX
<
T
>
leIfPresent
(
SFunction
<
T
,
?>
column
,
Object
val
)
{
if
(
val
!=
null
)
{
return
(
MPJLambdaWrapperX
<
T
>)
super
.
le
(
column
,
val
);
}
return
this
;
}
public
MPJLambdaWrapperX
<
T
>
betweenIfPresent
(
SFunction
<
T
,
?>
column
,
Object
val1
,
Object
val2
)
{
if
(
val1
!=
null
&&
val2
!=
null
)
{
return
(
MPJLambdaWrapperX
<
T
>)
super
.
between
(
column
,
val1
,
val2
);
}
if
(
val1
!=
null
)
{
return
(
MPJLambdaWrapperX
<
T
>)
ge
(
column
,
val1
);
}
if
(
val2
!=
null
)
{
return
(
MPJLambdaWrapperX
<
T
>)
le
(
column
,
val2
);
}
return
this
;
}
public
MPJLambdaWrapperX
<
T
>
betweenIfPresent
(
SFunction
<
T
,
?>
column
,
Object
[]
values
)
{
Object
val1
=
ArrayUtils
.
get
(
values
,
0
);
Object
val2
=
ArrayUtils
.
get
(
values
,
1
);
return
betweenIfPresent
(
column
,
val1
,
val2
);
}
// ========== 重写父类方法,方便链式调用 ==========
@Override
public
<
X
>
MPJLambdaWrapperX
<
T
>
eq
(
boolean
condition
,
SFunction
<
X
,
?>
column
,
Object
val
)
{
super
.
eq
(
condition
,
column
,
val
);
return
this
;
}
@Override
public
<
X
>
MPJLambdaWrapperX
<
T
>
eq
(
SFunction
<
X
,
?>
column
,
Object
val
)
{
super
.
eq
(
column
,
val
);
return
this
;
}
@Override
public
<
X
>
MPJLambdaWrapperX
<
T
>
orderByDesc
(
SFunction
<
X
,
?>
column
)
{
//noinspection unchecked
super
.
orderByDesc
(
true
,
column
);
return
this
;
}
@Override
public
MPJLambdaWrapperX
<
T
>
last
(
String
lastSql
)
{
super
.
last
(
lastSql
);
return
this
;
}
@Override
public
<
X
>
MPJLambdaWrapperX
<
T
>
in
(
SFunction
<
X
,
?>
column
,
Collection
<?>
coll
)
{
super
.
in
(
column
,
coll
);
return
this
;
}
@Override
public
MPJLambdaWrapperX
<
T
>
selectAll
(
Class
<?>
clazz
)
{
super
.
selectAll
(
clazz
);
return
this
;
}
@Override
public
MPJLambdaWrapperX
<
T
>
selectAll
(
Class
<?>
clazz
,
String
prefix
)
{
super
.
selectAll
(
clazz
,
prefix
);
return
this
;
}
@Override
public
<
S
>
MPJLambdaWrapperX
<
T
>
selectAs
(
SFunction
<
S
,
?>
column
,
String
alias
)
{
super
.
selectAs
(
column
,
alias
);
return
this
;
}
@Override
public
<
E
>
MPJLambdaWrapperX
<
T
>
selectAs
(
String
column
,
SFunction
<
E
,
?>
alias
)
{
super
.
selectAs
(
column
,
alias
);
return
this
;
}
@Override
public
<
S
,
X
>
MPJLambdaWrapperX
<
T
>
selectAs
(
SFunction
<
S
,
?>
column
,
SFunction
<
X
,
?>
alias
)
{
super
.
selectAs
(
column
,
alias
);
return
this
;
}
@Override
public
<
E
,
X
>
MPJLambdaWrapperX
<
T
>
selectAs
(
String
index
,
SFunction
<
E
,
?>
column
,
SFunction
<
X
,
?>
alias
)
{
super
.
selectAs
(
index
,
column
,
alias
);
return
this
;
}
@Override
public
<
E
>
MPJLambdaWrapperX
<
T
>
selectAsClass
(
Class
<
E
>
source
,
Class
<?>
tag
)
{
super
.
selectAsClass
(
source
,
tag
);
return
this
;
}
@Override
public
<
E
,
F
>
MPJLambdaWrapperX
<
T
>
selectSub
(
Class
<
E
>
clazz
,
Consumer
<
MPJLambdaWrapper
<
E
>>
consumer
,
SFunction
<
F
,
?>
alias
)
{
super
.
selectSub
(
clazz
,
consumer
,
alias
);
return
this
;
}
@Override
public
<
E
,
F
>
MPJLambdaWrapperX
<
T
>
selectSub
(
Class
<
E
>
clazz
,
String
st
,
Consumer
<
MPJLambdaWrapper
<
E
>>
consumer
,
SFunction
<
F
,
?>
alias
)
{
super
.
selectSub
(
clazz
,
st
,
consumer
,
alias
);
return
this
;
}
@Override
public
<
S
>
MPJLambdaWrapperX
<
T
>
selectCount
(
SFunction
<
S
,
?>
column
)
{
super
.
selectCount
(
column
);
return
this
;
}
@Override
public
MPJLambdaWrapperX
<
T
>
selectCount
(
Object
column
,
String
alias
)
{
super
.
selectCount
(
column
,
alias
);
return
this
;
}
@Override
public
<
X
>
MPJLambdaWrapperX
<
T
>
selectCount
(
Object
column
,
SFunction
<
X
,
?>
alias
)
{
super
.
selectCount
(
column
,
alias
);
return
this
;
}
@Override
public
<
S
,
X
>
MPJLambdaWrapperX
<
T
>
selectCount
(
SFunction
<
S
,
?>
column
,
String
alias
)
{
super
.
selectCount
(
column
,
alias
);
return
this
;
}
@Override
public
<
S
,
X
>
MPJLambdaWrapperX
<
T
>
selectCount
(
SFunction
<
S
,
?>
column
,
SFunction
<
X
,
?>
alias
)
{
super
.
selectCount
(
column
,
alias
);
return
this
;
}
@Override
public
<
S
>
MPJLambdaWrapperX
<
T
>
selectSum
(
SFunction
<
S
,
?>
column
)
{
super
.
selectSum
(
column
);
return
this
;
}
@Override
public
<
S
,
X
>
MPJLambdaWrapperX
<
T
>
selectSum
(
SFunction
<
S
,
?>
column
,
String
alias
)
{
super
.
selectSum
(
column
,
alias
);
return
this
;
}
@Override
public
<
S
,
X
>
MPJLambdaWrapperX
<
T
>
selectSum
(
SFunction
<
S
,
?>
column
,
SFunction
<
X
,
?>
alias
)
{
super
.
selectSum
(
column
,
alias
);
return
this
;
}
@Override
public
<
S
>
MPJLambdaWrapperX
<
T
>
selectMax
(
SFunction
<
S
,
?>
column
)
{
super
.
selectMax
(
column
);
return
this
;
}
@Override
public
<
S
,
X
>
MPJLambdaWrapperX
<
T
>
selectMax
(
SFunction
<
S
,
?>
column
,
String
alias
)
{
super
.
selectMax
(
column
,
alias
);
return
this
;
}
@Override
public
<
S
,
X
>
MPJLambdaWrapperX
<
T
>
selectMax
(
SFunction
<
S
,
?>
column
,
SFunction
<
X
,
?>
alias
)
{
super
.
selectMax
(
column
,
alias
);
return
this
;
}
@Override
public
<
S
>
MPJLambdaWrapperX
<
T
>
selectMin
(
SFunction
<
S
,
?>
column
)
{
super
.
selectMin
(
column
);
return
this
;
}
@Override
public
<
S
,
X
>
MPJLambdaWrapperX
<
T
>
selectMin
(
SFunction
<
S
,
?>
column
,
String
alias
)
{
super
.
selectMin
(
column
,
alias
);
return
this
;
}
@Override
public
<
S
,
X
>
MPJLambdaWrapperX
<
T
>
selectMin
(
SFunction
<
S
,
?>
column
,
SFunction
<
X
,
?>
alias
)
{
super
.
selectMin
(
column
,
alias
);
return
this
;
}
@Override
public
<
S
>
MPJLambdaWrapperX
<
T
>
selectAvg
(
SFunction
<
S
,
?>
column
)
{
super
.
selectAvg
(
column
);
return
this
;
}
@Override
public
<
S
,
X
>
MPJLambdaWrapperX
<
T
>
selectAvg
(
SFunction
<
S
,
?>
column
,
String
alias
)
{
super
.
selectAvg
(
column
,
alias
);
return
this
;
}
@Override
public
<
S
,
X
>
MPJLambdaWrapperX
<
T
>
selectAvg
(
SFunction
<
S
,
?>
column
,
SFunction
<
X
,
?>
alias
)
{
super
.
selectAvg
(
column
,
alias
);
return
this
;
}
@Override
public
<
S
>
MPJLambdaWrapperX
<
T
>
selectLen
(
SFunction
<
S
,
?>
column
)
{
super
.
selectLen
(
column
);
return
this
;
}
@Override
public
<
S
,
X
>
MPJLambdaWrapperX
<
T
>
selectLen
(
SFunction
<
S
,
?>
column
,
String
alias
)
{
super
.
selectLen
(
column
,
alias
);
return
this
;
}
@Override
public
<
S
,
X
>
MPJLambdaWrapperX
<
T
>
selectLen
(
SFunction
<
S
,
?>
column
,
SFunction
<
X
,
?>
alias
)
{
super
.
selectLen
(
column
,
alias
);
return
this
;
}
}
ruoyi-common/ruoyi-common-yudao-mybatis/src/main/java/org/dromara/common/mybatis/core/query/QueryWrapperX.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mybatis
.
core
.
query
;
import
com.baomidou.mybatisplus.annotation.DbType
;
import
com.baomidou.mybatisplus.core.conditions.query.QueryWrapper
;
import
com.baomidou.mybatisplus.core.toolkit.ArrayUtils
;
import
com.baomidou.mybatisplus.core.toolkit.CollectionUtils
;
import
org.dromara.common.mybatis.util.JdbcUtils
;
import
org.springframework.util.StringUtils
;
import
java.util.Collection
;
/**
* 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能:
*
* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
*
* @param <T> 数据类型
*/
public
class
QueryWrapperX
<
T
>
extends
QueryWrapper
<
T
>
{
public
QueryWrapperX
<
T
>
likeIfPresent
(
String
column
,
String
val
)
{
if
(
StringUtils
.
hasText
(
val
))
{
return
(
QueryWrapperX
<
T
>)
super
.
like
(
column
,
val
);
}
return
this
;
}
public
QueryWrapperX
<
T
>
inIfPresent
(
String
column
,
Collection
<?>
values
)
{
if
(!
CollectionUtils
.
isEmpty
(
values
))
{
return
(
QueryWrapperX
<
T
>)
super
.
in
(
column
,
values
);
}
return
this
;
}
public
QueryWrapperX
<
T
>
inIfPresent
(
String
column
,
Object
...
values
)
{
if
(!
ArrayUtils
.
isEmpty
(
values
))
{
return
(
QueryWrapperX
<
T
>)
super
.
in
(
column
,
values
);
}
return
this
;
}
public
QueryWrapperX
<
T
>
eqIfPresent
(
String
column
,
Object
val
)
{
if
(
val
!=
null
)
{
return
(
QueryWrapperX
<
T
>)
super
.
eq
(
column
,
val
);
}
return
this
;
}
public
QueryWrapperX
<
T
>
neIfPresent
(
String
column
,
Object
val
)
{
if
(
val
!=
null
)
{
return
(
QueryWrapperX
<
T
>)
super
.
ne
(
column
,
val
);
}
return
this
;
}
public
QueryWrapperX
<
T
>
gtIfPresent
(
String
column
,
Object
val
)
{
if
(
val
!=
null
)
{
return
(
QueryWrapperX
<
T
>)
super
.
gt
(
column
,
val
);
}
return
this
;
}
public
QueryWrapperX
<
T
>
geIfPresent
(
String
column
,
Object
val
)
{
if
(
val
!=
null
)
{
return
(
QueryWrapperX
<
T
>)
super
.
ge
(
column
,
val
);
}
return
this
;
}
public
QueryWrapperX
<
T
>
ltIfPresent
(
String
column
,
Object
val
)
{
if
(
val
!=
null
)
{
return
(
QueryWrapperX
<
T
>)
super
.
lt
(
column
,
val
);
}
return
this
;
}
public
QueryWrapperX
<
T
>
leIfPresent
(
String
column
,
Object
val
)
{
if
(
val
!=
null
)
{
return
(
QueryWrapperX
<
T
>)
super
.
le
(
column
,
val
);
}
return
this
;
}
public
QueryWrapperX
<
T
>
betweenIfPresent
(
String
column
,
Object
val1
,
Object
val2
)
{
if
(
val1
!=
null
&&
val2
!=
null
)
{
return
(
QueryWrapperX
<
T
>)
super
.
between
(
column
,
val1
,
val2
);
}
if
(
val1
!=
null
)
{
return
(
QueryWrapperX
<
T
>)
ge
(
column
,
val1
);
}
if
(
val2
!=
null
)
{
return
(
QueryWrapperX
<
T
>)
le
(
column
,
val2
);
}
return
this
;
}
public
QueryWrapperX
<
T
>
betweenIfPresent
(
String
column
,
Object
[]
values
)
{
if
(
values
!=
null
&&
values
.
length
!=
0
&&
values
[
0
]
!=
null
&&
values
[
1
]
!=
null
)
{
return
(
QueryWrapperX
<
T
>)
super
.
between
(
column
,
values
[
0
],
values
[
1
]);
}
if
(
values
!=
null
&&
values
.
length
!=
0
&&
values
[
0
]
!=
null
)
{
return
(
QueryWrapperX
<
T
>)
ge
(
column
,
values
[
0
]);
}
if
(
values
!=
null
&&
values
.
length
!=
0
&&
values
[
1
]
!=
null
)
{
return
(
QueryWrapperX
<
T
>)
le
(
column
,
values
[
1
]);
}
return
this
;
}
// ========== 重写父类方法,方便链式调用 ==========
@Override
public
QueryWrapperX
<
T
>
eq
(
boolean
condition
,
String
column
,
Object
val
)
{
super
.
eq
(
condition
,
column
,
val
);
return
this
;
}
@Override
public
QueryWrapperX
<
T
>
eq
(
String
column
,
Object
val
)
{
super
.
eq
(
column
,
val
);
return
this
;
}
@Override
public
QueryWrapperX
<
T
>
orderByDesc
(
String
column
)
{
super
.
orderByDesc
(
true
,
column
);
return
this
;
}
@Override
public
QueryWrapperX
<
T
>
last
(
String
lastSql
)
{
super
.
last
(
lastSql
);
return
this
;
}
@Override
public
QueryWrapperX
<
T
>
in
(
String
column
,
Collection
<?>
coll
)
{
super
.
in
(
column
,
coll
);
return
this
;
}
/**
* 设置只返回最后一条
*
* TODO 芋艿:不是完美解,需要在思考下。如果使用多数据源,并且数据源是多种类型时,可能会存在问题:实现之返回一条的语法不同
*
* @return this
*/
public
QueryWrapperX
<
T
>
limitN
(
int
n
)
{
DbType
dbType
=
JdbcUtils
.
getDbType
();
switch
(
dbType
)
{
case
ORACLE:
case
ORACLE_12C:
super
.
le
(
"ROWNUM"
,
n
);
break
;
case
SQL_SERVER:
case
SQL_SERVER2005:
super
.
select
(
"TOP "
+
n
+
" *"
);
// 由于 SQL Server 是通过 SELECT TOP 1 实现限制一条,所以只好使用 * 查询剩余字段
break
;
default
:
// MySQL、PostgreSQL、DM 达梦、KingbaseES 大金都是采用 LIMIT 实现
super
.
last
(
"LIMIT "
+
n
);
}
return
this
;
}
}
ruoyi-common/ruoyi-common-yudao-mybatis/src/main/java/org/dromara/common/mybatis/core/type/EncryptTypeHandler.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mybatis
.
core
.
type
;
import
cn.hutool.core.lang.Assert
;
import
cn.hutool.crypto.SecureUtil
;
import
cn.hutool.crypto.symmetric.AES
;
import
cn.hutool.extra.spring.SpringUtil
;
import
org.apache.ibatis.type.BaseTypeHandler
;
import
org.apache.ibatis.type.JdbcType
;
import
java.sql.CallableStatement
;
import
java.sql.PreparedStatement
;
import
java.sql.ResultSet
;
import
java.sql.SQLException
;
/**
* 字段字段的 TypeHandler 实现类,基于 {@link AES} 实现
* 可通过 jasypt.encryptor.password 配置项,设置密钥
*
* @author 芋道源码
*/
public
class
EncryptTypeHandler
extends
BaseTypeHandler
<
String
>
{
private
static
final
String
ENCRYPTOR_PROPERTY_NAME
=
"mybatis-plus.encryptor.password"
;
private
static
AES
aes
;
@Override
public
void
setNonNullParameter
(
PreparedStatement
ps
,
int
i
,
String
parameter
,
JdbcType
jdbcType
)
throws
SQLException
{
ps
.
setString
(
i
,
encrypt
(
parameter
));
}
@Override
public
String
getNullableResult
(
ResultSet
rs
,
String
columnName
)
throws
SQLException
{
String
value
=
rs
.
getString
(
columnName
);
return
decrypt
(
value
);
}
@Override
public
String
getNullableResult
(
ResultSet
rs
,
int
columnIndex
)
throws
SQLException
{
String
value
=
rs
.
getString
(
columnIndex
);
return
decrypt
(
value
);
}
@Override
public
String
getNullableResult
(
CallableStatement
cs
,
int
columnIndex
)
throws
SQLException
{
String
value
=
cs
.
getString
(
columnIndex
);
return
decrypt
(
value
);
}
private
static
String
decrypt
(
String
value
)
{
if
(
value
==
null
)
{
return
null
;
}
return
getEncryptor
().
decryptStr
(
value
);
}
public
static
String
encrypt
(
String
rawValue
)
{
if
(
rawValue
==
null
)
{
return
null
;
}
return
getEncryptor
().
encryptBase64
(
rawValue
);
}
private
static
AES
getEncryptor
()
{
if
(
aes
!=
null
)
{
return
aes
;
}
// 构建 AES
String
password
=
SpringUtil
.
getProperty
(
ENCRYPTOR_PROPERTY_NAME
);
Assert
.
notEmpty
(
password
,
"配置项({}) 不能为空"
,
ENCRYPTOR_PROPERTY_NAME
);
aes
=
SecureUtil
.
aes
(
password
.
getBytes
());
return
aes
;
}
}
ruoyi-common/ruoyi-common-yudao-mybatis/src/main/java/org/dromara/common/mybatis/core/type/IntegerListTypeHandler.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mybatis
.
core
.
type
;
import
cn.hutool.core.collection.CollUtil
;
import
org.apache.ibatis.type.JdbcType
;
import
org.apache.ibatis.type.MappedJdbcTypes
;
import
org.apache.ibatis.type.MappedTypes
;
import
org.apache.ibatis.type.TypeHandler
;
import
org.dromara.common.core.utils.string.StrUtils
;
import
java.sql.CallableStatement
;
import
java.sql.PreparedStatement
;
import
java.sql.ResultSet
;
import
java.sql.SQLException
;
import
java.util.List
;
/**
* List<Integer> 的类型转换器实现类,对应数据库的 varchar 类型
*
* @author jason
*/
@MappedJdbcTypes
(
JdbcType
.
VARCHAR
)
@MappedTypes
(
List
.
class
)
public
class
IntegerListTypeHandler
implements
TypeHandler
<
List
<
Integer
>>
{
private
static
final
String
COMMA
=
","
;
@Override
public
void
setParameter
(
PreparedStatement
ps
,
int
i
,
List
<
Integer
>
strings
,
JdbcType
jdbcType
)
throws
SQLException
{
ps
.
setString
(
i
,
CollUtil
.
join
(
strings
,
COMMA
));
}
@Override
public
List
<
Integer
>
getResult
(
ResultSet
rs
,
String
columnName
)
throws
SQLException
{
String
value
=
rs
.
getString
(
columnName
);
return
getResult
(
value
);
}
@Override
public
List
<
Integer
>
getResult
(
ResultSet
rs
,
int
columnIndex
)
throws
SQLException
{
String
value
=
rs
.
getString
(
columnIndex
);
return
getResult
(
value
);
}
@Override
public
List
<
Integer
>
getResult
(
CallableStatement
cs
,
int
columnIndex
)
throws
SQLException
{
String
value
=
cs
.
getString
(
columnIndex
);
return
getResult
(
value
);
}
private
List
<
Integer
>
getResult
(
String
value
)
{
if
(
value
==
null
)
{
return
null
;
}
return
StrUtils
.
splitToInteger
(
value
,
COMMA
);
}
}
ruoyi-common/ruoyi-common-yudao-mybatis/src/main/java/org/dromara/common/mybatis/core/type/LongListTypeHandler.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mybatis
.
core
.
type
;
import
cn.hutool.core.collection.CollUtil
;
import
org.apache.ibatis.type.JdbcType
;
import
org.apache.ibatis.type.MappedJdbcTypes
;
import
org.apache.ibatis.type.MappedTypes
;
import
org.apache.ibatis.type.TypeHandler
;
import
org.dromara.common.core.utils.string.StrUtils
;
import
java.sql.CallableStatement
;
import
java.sql.PreparedStatement
;
import
java.sql.ResultSet
;
import
java.sql.SQLException
;
import
java.util.List
;
/**
* List<Long> 的类型转换器实现类,对应数据库的 varchar 类型
*
* @author 芋道源码
*/
@MappedJdbcTypes
(
JdbcType
.
VARCHAR
)
@MappedTypes
(
List
.
class
)
public
class
LongListTypeHandler
implements
TypeHandler
<
List
<
Long
>>
{
private
static
final
String
COMMA
=
","
;
@Override
public
void
setParameter
(
PreparedStatement
ps
,
int
i
,
List
<
Long
>
strings
,
JdbcType
jdbcType
)
throws
SQLException
{
// 设置占位符
ps
.
setString
(
i
,
CollUtil
.
join
(
strings
,
COMMA
));
}
@Override
public
List
<
Long
>
getResult
(
ResultSet
rs
,
String
columnName
)
throws
SQLException
{
String
value
=
rs
.
getString
(
columnName
);
return
getResult
(
value
);
}
@Override
public
List
<
Long
>
getResult
(
ResultSet
rs
,
int
columnIndex
)
throws
SQLException
{
String
value
=
rs
.
getString
(
columnIndex
);
return
getResult
(
value
);
}
@Override
public
List
<
Long
>
getResult
(
CallableStatement
cs
,
int
columnIndex
)
throws
SQLException
{
String
value
=
cs
.
getString
(
columnIndex
);
return
getResult
(
value
);
}
private
List
<
Long
>
getResult
(
String
value
)
{
if
(
value
==
null
)
{
return
null
;
}
return
StrUtils
.
splitToLong
(
value
,
COMMA
);
}
}
ruoyi-common/ruoyi-common-yudao-mybatis/src/main/java/org/dromara/common/mybatis/core/type/StringListTypeHandler.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mybatis
.
core
.
type
;
import
cn.hutool.core.collection.CollUtil
;
import
cn.hutool.core.util.StrUtil
;
import
org.apache.ibatis.type.JdbcType
;
import
org.apache.ibatis.type.MappedJdbcTypes
;
import
org.apache.ibatis.type.MappedTypes
;
import
org.apache.ibatis.type.TypeHandler
;
import
java.sql.CallableStatement
;
import
java.sql.PreparedStatement
;
import
java.sql.ResultSet
;
import
java.sql.SQLException
;
import
java.util.List
;
/**
* List<String> 的类型转换器实现类,对应数据库的 varchar 类型
*
* @author 永不言败
* @since 2022 3/23 12:50:15
*/
@MappedJdbcTypes
(
JdbcType
.
VARCHAR
)
@MappedTypes
(
List
.
class
)
public
class
StringListTypeHandler
implements
TypeHandler
<
List
<
String
>>
{
private
static
final
String
COMMA
=
","
;
@Override
public
void
setParameter
(
PreparedStatement
ps
,
int
i
,
List
<
String
>
strings
,
JdbcType
jdbcType
)
throws
SQLException
{
// 设置占位符
ps
.
setString
(
i
,
CollUtil
.
join
(
strings
,
COMMA
));
}
@Override
public
List
<
String
>
getResult
(
ResultSet
rs
,
String
columnName
)
throws
SQLException
{
String
value
=
rs
.
getString
(
columnName
);
return
getResult
(
value
);
}
@Override
public
List
<
String
>
getResult
(
ResultSet
rs
,
int
columnIndex
)
throws
SQLException
{
String
value
=
rs
.
getString
(
columnIndex
);
return
getResult
(
value
);
}
@Override
public
List
<
String
>
getResult
(
CallableStatement
cs
,
int
columnIndex
)
throws
SQLException
{
String
value
=
cs
.
getString
(
columnIndex
);
return
getResult
(
value
);
}
private
List
<
String
>
getResult
(
String
value
)
{
if
(
value
==
null
)
{
return
null
;
}
return
StrUtil
.
splitTrim
(
value
,
COMMA
);
}
}
ruoyi-common/ruoyi-common-yudao-mybatis/src/main/java/org/dromara/common/mybatis/enums/DbTypeEnum.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mybatis
.
enums
;
import
cn.hutool.core.util.StrUtil
;
import
com.baomidou.mybatisplus.annotation.DbType
;
import
lombok.AllArgsConstructor
;
import
lombok.Getter
;
import
java.util.Arrays
;
import
java.util.Map
;
import
java.util.Optional
;
import
java.util.function.Function
;
import
java.util.stream.Collectors
;
/**
* 针对 MyBatis Plus 的 {@link DbType} 增强,补充更多信息
*/
@Getter
@AllArgsConstructor
public
enum
DbTypeEnum
{
/**
* H2
*
* 注意:H2 不支持 find_in_set 函数
*/
H2
(
DbType
.
H2
,
"H2"
,
""
),
/**
* MySQL
*/
MY_SQL
(
DbType
.
MYSQL
,
"MySQL"
,
"FIND_IN_SET('#{value}', #{column}) <> 0"
),
/**
* Oracle
*/
ORACLE
(
DbType
.
ORACLE
,
"Oracle"
,
"FIND_IN_SET('#{value}', #{column}) <> 0"
),
/**
* PostgreSQL
*
* 华为 openGauss 使用 ProductName 与 PostgreSQL 相同
*/
POSTGRE_SQL
(
DbType
.
POSTGRE_SQL
,
"PostgreSQL"
,
"POSITION('#{value}' IN #{column}) <> 0"
),
/**
* SQL Server
*/
SQL_SERVER
(
DbType
.
SQL_SERVER
,
"Microsoft SQL Server"
,
"CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"
),
/**
* SQL Server 2005
*/
SQL_SERVER2005
(
DbType
.
SQL_SERVER2005
,
"Microsoft SQL Server 2005"
,
"CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"
),
/**
* 达梦
*/
DM
(
DbType
.
DM
,
"DM DBMS"
,
"FIND_IN_SET('#{value}', #{column}) <> 0"
),
/**
* 人大金仓
*/
KINGBASE_ES
(
DbType
.
KINGBASE_ES
,
"KingbaseES"
,
"POSITION('#{value}' IN #{column}) <> 0"
),
;
public
static
final
Map
<
String
,
DbTypeEnum
>
MAP_BY_NAME
=
Arrays
.
stream
(
values
())
.
collect
(
Collectors
.
toMap
(
DbTypeEnum:
:
getProductName
,
Function
.
identity
()));
public
static
final
Map
<
DbType
,
DbTypeEnum
>
MAP_BY_MP
=
Arrays
.
stream
(
values
())
.
collect
(
Collectors
.
toMap
(
DbTypeEnum:
:
getMpDbType
,
Function
.
identity
()));
/**
* MyBatis Plus 类型
*/
private
final
DbType
mpDbType
;
/**
* 数据库产品名
*/
private
final
String
productName
;
/**
* SQL FIND_IN_SET 模板
*/
private
final
String
findInSetTemplate
;
public
static
DbType
find
(
String
databaseProductName
)
{
if
(
StrUtil
.
isBlank
(
databaseProductName
))
{
return
null
;
}
return
MAP_BY_NAME
.
get
(
databaseProductName
).
getMpDbType
();
}
public
static
String
getFindInSetTemplate
(
DbType
dbType
)
{
return
Optional
.
of
(
MAP_BY_MP
.
get
(
dbType
).
getFindInSetTemplate
())
.
orElseThrow
(()
->
new
IllegalArgumentException
(
"FIND_IN_SET not supported"
));
}
}
ruoyi-common/ruoyi-common-yudao-mybatis/src/main/java/org/dromara/common/mybatis/util/BeanUtils.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mybatis
.
util
;
import
cn.hutool.core.bean.BeanUtil
;
import
org.dromara.common.mybatis.core.page.PageResult
;
import
java.util.List
;
import
java.util.function.Consumer
;
/**
* Bean 工具类
* <p>
* 1. 默认使用 {@link BeanUtil} 作为实现类,虽然不同 bean 工具的性能有差别,但是对绝大多数同学的项目,不用在意这点性能
* 2. 针对复杂的对象转换,可以搜参考 AuthConvert 实现,通过 mapstruct + default 配合实现
*
* @author 芋道源码
*/
public
class
BeanUtils
extends
org
.
dromara
.
common
.
mall
.
util
.
object
.
BeanUtils
{
public
static
<
S
,
T
>
PageResult
<
T
>
toBean
(
PageResult
<
S
>
source
,
Class
<
T
>
targetType
)
{
return
toBean
(
source
,
targetType
,
null
);
}
public
static
<
S
,
T
>
PageResult
<
T
>
toBean
(
PageResult
<
S
>
source
,
Class
<
T
>
targetType
,
Consumer
<
T
>
peek
)
{
if
(
source
==
null
)
{
return
null
;
}
List
<
T
>
list
=
toBean
(
source
.
getList
(),
targetType
);
if
(
peek
!=
null
)
{
list
.
forEach
(
peek
);
}
return
new
PageResult
<>(
list
,
source
.
getTotal
());
}
}
ruoyi-common/ruoyi-common-yudao-mybatis/src/main/java/org/dromara/common/mybatis/util/JdbcUtils.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mybatis
.
util
;
import
com.baomidou.dynamic.datasource.DynamicRoutingDataSource
;
import
com.baomidou.mybatisplus.annotation.DbType
;
import
org.dromara.common.core.utils.SpringUtils
;
import
org.dromara.common.mybatis.enums.DbTypeEnum
;
import
org.springframework.beans.factory.NoSuchBeanDefinitionException
;
import
javax.sql.DataSource
;
import
java.sql.Connection
;
import
java.sql.DriverManager
;
import
java.sql.SQLException
;
import
java.util.Arrays
;
/**
* JDBC 工具类
*
* @author 芋道源码
*/
public
class
JdbcUtils
{
/**
* 判断连接是否正确
*
* @param url 数据源连接
* @param username 账号
* @param password 密码
* @return 是否正确
*/
public
static
boolean
isConnectionOK
(
String
url
,
String
username
,
String
password
)
{
try
(
Connection
ignored
=
DriverManager
.
getConnection
(
url
,
username
,
password
))
{
return
true
;
}
catch
(
Exception
ex
)
{
return
false
;
}
}
/**
* 获得 URL 对应的 DB 类型
*
* @param url URL
* @return DB 类型
*/
public
static
DbType
getDbType
(
String
url
)
{
return
com
.
baomidou
.
mybatisplus
.
extension
.
toolkit
.
JdbcUtils
.
getDbType
(
url
);
}
/**
* 通过当前数据库连接获得对应的 DB 类型
*
* @return DB 类型
*/
public
static
DbType
getDbType
()
{
DataSource
dataSource
;
try
{
DynamicRoutingDataSource
dynamicRoutingDataSource
=
SpringUtils
.
getBean
(
DynamicRoutingDataSource
.
class
);
dataSource
=
dynamicRoutingDataSource
.
determineDataSource
();
}
catch
(
NoSuchBeanDefinitionException
e
)
{
dataSource
=
SpringUtils
.
getBean
(
DataSource
.
class
);
}
try
(
Connection
conn
=
dataSource
.
getConnection
())
{
return
DbTypeEnum
.
find
(
conn
.
getMetaData
().
getDatabaseProductName
());
}
catch
(
SQLException
e
)
{
throw
new
IllegalArgumentException
(
e
.
getMessage
());
}
}
/**
* 判断 JDBC 连接是否为 SQLServer 数据库
*
* @param url JDBC 连接
* @return 是否为 SQLServer 数据库
*/
public
static
boolean
isSQLServer
(
String
url
)
{
DbType
dbType
=
getDbType
(
url
);
return
isSQLServer
(
dbType
);
}
/**
* 判断 JDBC 连接是否为 SQLServer 数据库
*
* @param dbType DB 类型
* @return 是否为 SQLServer 数据库
*/
public
static
boolean
isSQLServer
(
DbType
dbType
)
{
return
equalsAny
(
dbType
,
DbType
.
SQL_SERVER
,
DbType
.
SQL_SERVER2005
);
}
@SafeVarargs
public
static
<
T
>
boolean
equalsAny
(
T
obj
,
T
...
array
)
{
return
Arrays
.
asList
(
array
).
contains
(
obj
);
}
}
ruoyi-common/ruoyi-common-yudao-mybatis/src/main/java/org/dromara/common/mybatis/util/MyBatisUtils.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mybatis
.
util
;
import
cn.hutool.core.collection.CollectionUtil
;
import
cn.hutool.core.util.StrUtil
;
import
com.baomidou.mybatisplus.annotation.DbType
;
import
com.baomidou.mybatisplus.core.metadata.OrderItem
;
import
com.baomidou.mybatisplus.core.toolkit.StringPool
;
import
com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor
;
import
com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor
;
import
com.baomidou.mybatisplus.extension.plugins.pagination.Page
;
import
net.sf.jsqlparser.expression.Alias
;
import
net.sf.jsqlparser.schema.Column
;
import
net.sf.jsqlparser.schema.Table
;
import
org.dromara.common.mybatis.core.page.PageParam
;
import
org.dromara.common.mybatis.core.page.SortingField
;
import
org.dromara.common.mybatis.enums.DbTypeEnum
;
import
java.util.ArrayList
;
import
java.util.Collection
;
import
java.util.List
;
import
java.util.stream.Collectors
;
/**
* MyBatis 工具类
*/
public
class
MyBatisUtils
{
private
static
final
String
MYSQL_ESCAPE_CHARACTER
=
"`"
;
public
static
<
T
>
Page
<
T
>
buildPage
(
PageParam
pageParam
)
{
return
buildPage
(
pageParam
,
null
);
}
public
static
<
T
>
Page
<
T
>
buildPage
(
PageParam
pageParam
,
Collection
<
SortingField
>
sortingFields
)
{
// 页码 + 数量
Page
<
T
>
page
=
new
Page
<>(
pageParam
.
getPageNo
(),
pageParam
.
getPageSize
());
// 排序字段
if
(!
CollectionUtil
.
isEmpty
(
sortingFields
))
{
page
.
addOrder
(
sortingFields
.
stream
().
map
(
sortingField
->
SortingField
.
ORDER_ASC
.
equals
(
sortingField
.
getOrder
())
?
OrderItem
.
asc
(
StrUtil
.
toUnderlineCase
(
sortingField
.
getField
()))
:
OrderItem
.
desc
(
StrUtil
.
toUnderlineCase
(
sortingField
.
getField
())))
.
collect
(
Collectors
.
toList
()));
}
return
page
;
}
/**
* 将拦截器添加到链中
* 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置
*
* @param interceptor 链
* @param inner 拦截器
* @param index 位置
*/
public
static
void
addInterceptor
(
MybatisPlusInterceptor
interceptor
,
InnerInterceptor
inner
,
int
index
)
{
List
<
InnerInterceptor
>
inners
=
new
ArrayList
<>(
interceptor
.
getInterceptors
());
inners
.
add
(
index
,
inner
);
interceptor
.
setInterceptors
(
inners
);
}
/**
* 获得 Table 对应的表名
* <p>
* 兼容 MySQL 转义表名 `t_xxx`
*
* @param table 表
* @return 去除转移字符后的表名
*/
public
static
String
getTableName
(
Table
table
)
{
String
tableName
=
table
.
getName
();
if
(
tableName
.
startsWith
(
MYSQL_ESCAPE_CHARACTER
)
&&
tableName
.
endsWith
(
MYSQL_ESCAPE_CHARACTER
))
{
tableName
=
tableName
.
substring
(
1
,
tableName
.
length
()
-
1
);
}
return
tableName
;
}
/**
* 构建 Column 对象
*
* @param tableName 表名
* @param tableAlias 别名
* @param column 字段名
* @return Column 对象
*/
public
static
Column
buildColumn
(
String
tableName
,
Alias
tableAlias
,
String
column
)
{
if
(
tableAlias
!=
null
)
{
tableName
=
tableAlias
.
getName
();
}
return
new
Column
(
tableName
+
StringPool
.
DOT
+
column
);
}
/**
* 跨数据库的 find_in_set 实现
*
* @param column 字段名称
* @param value 查询值(不带单引号)
* @return sql
*/
public
static
String
findInSet
(
String
column
,
Object
value
)
{
DbType
dbType
=
JdbcUtils
.
getDbType
();
return
DbTypeEnum
.
getFindInSetTemplate
(
dbType
)
.
replace
(
"#{column}"
,
column
)
.
replace
(
"#{value}"
,
StrUtil
.
toString
(
value
));
}
}
ruoyi-common/ruoyi-common-yudao-mybatis/src/main/java/org/dromara/common/mybatis/util/PageUtils.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
common
.
mybatis
.
util
;
import
cn.hutool.core.collection.CollUtil
;
import
cn.hutool.core.lang.func.Func1
;
import
cn.hutool.core.lang.func.LambdaUtil
;
import
cn.hutool.core.util.ArrayUtil
;
import
org.dromara.common.mybatis.core.page.PageParam
;
import
org.dromara.common.mybatis.core.page.SortablePageParam
;
import
org.dromara.common.mybatis.core.page.SortingField
;
import
org.springframework.util.Assert
;
import
static
java
.
util
.
Collections
.
singletonList
;
/**
* {@link PageParam} 工具类
*
* @author 芋道源码
*/
public
class
PageUtils
{
private
static
final
Object
[]
ORDER_TYPES
=
new
String
[]{
SortingField
.
ORDER_ASC
,
SortingField
.
ORDER_DESC
};
public
static
int
getStart
(
PageParam
pageParam
)
{
return
(
pageParam
.
getPageNo
()
-
1
)
*
pageParam
.
getPageSize
();
}
/**
* 构建排序字段(默认倒序)
*
* @param func 排序字段的 Lambda 表达式
* @param <T> 排序字段所属的类型
* @return 排序字段
*/
public
static
<
T
>
SortingField
buildSortingField
(
Func1
<
T
,
?>
func
)
{
return
buildSortingField
(
func
,
SortingField
.
ORDER_DESC
);
}
/**
* 构建排序字段
*
* @param func 排序字段的 Lambda 表达式
* @param order 排序类型 {@link SortingField#ORDER_ASC} {@link SortingField#ORDER_DESC}
* @param <T> 排序字段所属的类型
* @return 排序字段
*/
public
static
<
T
>
SortingField
buildSortingField
(
Func1
<
T
,
?>
func
,
String
order
)
{
Assert
.
isTrue
(
ArrayUtil
.
contains
(
ORDER_TYPES
,
order
),
String
.
format
(
"字段的排序类型只能是 %s/%s"
,
ORDER_TYPES
));
String
fieldName
=
LambdaUtil
.
getFieldName
(
func
);
return
new
SortingField
(
fieldName
,
order
);
}
/**
* 构建默认的排序字段
* 如果排序字段为空,则设置排序字段;否则忽略
*
* @param sortablePageParam 排序分页查询参数
* @param func 排序字段的 Lambda 表达式
* @param <T> 排序字段所属的类型
*/
public
static
<
T
>
void
buildDefaultSortingField
(
SortablePageParam
sortablePageParam
,
Func1
<
T
,
?>
func
)
{
if
(
sortablePageParam
!=
null
&&
CollUtil
.
isEmpty
(
sortablePageParam
.
getSortingFields
()))
{
sortablePageParam
.
setSortingFields
(
singletonList
(
buildSortingField
(
func
)));
}
}
}
ruoyi-modules/ruoyi-job/pom.xml
浏览文件 @
44629dc1
...
@@ -87,6 +87,11 @@
...
@@ -87,6 +87,11 @@
<artifactId>
ruoyi-api-order
</artifactId>
<artifactId>
ruoyi-api-order
</artifactId>
</dependency>
</dependency>
<dependency>
<groupId>
org.dromara
</groupId>
<artifactId>
ruoyi-api-mall
</artifactId>
</dependency>
</dependencies>
</dependencies>
...
...
ruoyi-modules/ruoyi-job/src/main/java/org/dromara/job/snailjob/mall/pay/notify/PayNotifyJob.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
job
.
snailjob
.
mall
.
pay
.
notify
;
import
com.aizuda.snailjob.client.job.core.annotation.JobExecutor
;
import
com.aizuda.snailjob.client.job.core.dto.JobArgs
;
import
com.aizuda.snailjob.client.model.ExecuteResult
;
import
lombok.AllArgsConstructor
;
import
org.apache.dubbo.config.annotation.DubboReference
;
import
org.dromara.mall.api.pay.RemotePayNotifyService
;
import
org.springframework.stereotype.Component
;
/**
* 支付通知 Job
* 通过不断扫描待通知的 PayNotifyTaskDO 记录,回调业务线的回调接口
*
* @author 芋道源码
*/
@AllArgsConstructor
@Component
@JobExecutor
(
name
=
"PayNotifyJob"
)
public
class
PayNotifyJob
{
@DubboReference
private
RemotePayNotifyService
payNotifyService
;
public
ExecuteResult
jobExecute
(
JobArgs
jobArgs
)
throws
InterruptedException
{
int
notifyCount
=
payNotifyService
.
executeNotify
();
return
ExecuteResult
.
success
(
String
.
format
(
"执行支付通知 %s 个"
,
notifyCount
));
}
}
ruoyi-modules/ruoyi-job/src/main/java/org/dromara/job/snailjob/mall/pay/order/PayOrderExpireJob.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
job
.
snailjob
.
mall
.
pay
.
order
;
import
cn.hutool.core.util.StrUtil
;
import
com.aizuda.snailjob.client.job.core.annotation.JobExecutor
;
import
com.aizuda.snailjob.client.job.core.dto.JobArgs
;
import
com.aizuda.snailjob.client.model.ExecuteResult
;
import
lombok.AllArgsConstructor
;
import
org.apache.dubbo.config.annotation.DubboReference
;
import
org.dromara.mall.api.pay.RemotePayOrderService
;
import
org.springframework.stereotype.Component
;
/**
* 支付订单的过期 Job
*
* 支付超过过期时间时,支付渠道是不会通知进行过期,所以需要定时进行过期关闭。
*
* @author 芋道源码
*/
@AllArgsConstructor
@Component
@JobExecutor
(
name
=
"PayOrderExpireJob"
)
public
class
PayOrderExpireJob
{
@DubboReference
private
RemotePayOrderService
service
;
public
ExecuteResult
jobExecute
(
JobArgs
jobArgs
)
{
int
count
=
service
.
expireOrder
();
return
ExecuteResult
.
success
(
StrUtil
.
format
(
"支付过期 {} 个"
,
count
));
}
}
ruoyi-modules/ruoyi-job/src/main/java/org/dromara/job/snailjob/mall/pay/order/PayOrderSyncJob.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
job
.
snailjob
.
mall
.
pay
.
order
;
import
cn.hutool.core.util.StrUtil
;
import
com.aizuda.snailjob.client.job.core.annotation.JobExecutor
;
import
com.aizuda.snailjob.client.job.core.dto.JobArgs
;
import
com.aizuda.snailjob.client.model.ExecuteResult
;
import
lombok.AllArgsConstructor
;
import
org.apache.dubbo.config.annotation.DubboReference
;
import
org.dromara.mall.api.pay.RemotePayOrderService
;
import
org.springframework.stereotype.Component
;
import
java.time.Duration
;
import
java.time.LocalDateTime
;
/**
* 支付订单的同步 Job
*
* 由于支付订单的状态,是由支付渠道异步通知进行同步,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。
*
* @author 芋道源码
*/
@AllArgsConstructor
@Component
@JobExecutor
(
name
=
"PayOrderSyncJob"
)
public
class
PayOrderSyncJob
{
/**
* 同步创建时间在 N 分钟之内的订单
*
* 为什么同步 10 分钟之内的订单?
* 因为一个订单发起支付,到支付成功,大多数在 10 分钟内,需要保证轮询到。
* 如果设置为 30、60 或者更大时间范围,会导致轮询的订单太多,影响性能。当然,你也可以根据自己的业务情况来处理。
*/
private
static
final
Duration
CREATE_TIME_DURATION_BEFORE
=
Duration
.
ofMinutes
(
10
);
@DubboReference
private
RemotePayOrderService
orderService
;
public
ExecuteResult
jobExecute
(
JobArgs
jobArgs
)
{
LocalDateTime
minCreateTime
=
LocalDateTime
.
now
().
minus
(
CREATE_TIME_DURATION_BEFORE
);
int
count
=
orderService
.
syncOrder
(
minCreateTime
);
return
ExecuteResult
.
success
(
StrUtil
.
format
(
"同步支付订单 {} 个"
,
count
));
}
}
ruoyi-modules/ruoyi-job/src/main/java/org/dromara/job/snailjob/mall/pay/refund/PayRefundSyncJob.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
job
.
snailjob
.
mall
.
pay
.
refund
;
import
cn.hutool.core.util.StrUtil
;
import
com.aizuda.snailjob.client.job.core.annotation.JobExecutor
;
import
com.aizuda.snailjob.client.job.core.dto.JobArgs
;
import
com.aizuda.snailjob.client.model.ExecuteResult
;
import
lombok.AllArgsConstructor
;
import
org.apache.dubbo.config.annotation.DubboReference
;
import
org.dromara.mall.api.pay.RemotePayRefundService
;
import
org.springframework.stereotype.Component
;
/**
* 退款订单的同步 Job
* <p>
* 由于退款订单的状态,是由支付渠道异步通知进行同步,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。
*
* @author 芋道源码
*/
@AllArgsConstructor
@Component
@JobExecutor
(
name
=
"PayRefundSyncJob"
)
public
class
PayRefundSyncJob
{
@DubboReference
private
RemotePayRefundService
refundService
;
public
ExecuteResult
jobExecute
(
JobArgs
jobArgs
)
{
int
count
=
refundService
.
syncRefund
();
return
ExecuteResult
.
success
(
StrUtil
.
format
(
"同步退款订单 {} 个"
,
count
));
}
}
ruoyi-modules/ruoyi-job/src/main/java/org/dromara/job/snailjob/mall/pay/transfer/PayTransferSyncJob.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
job
.
snailjob
.
mall
.
pay
.
transfer
;
import
cn.hutool.core.util.StrUtil
;
import
com.aizuda.snailjob.client.job.core.annotation.JobExecutor
;
import
com.aizuda.snailjob.client.job.core.dto.JobArgs
;
import
com.aizuda.snailjob.client.model.ExecuteResult
;
import
lombok.AllArgsConstructor
;
import
org.apache.dubbo.config.annotation.DubboReference
;
import
org.dromara.mall.api.pay.RemotePayTransferService
;
import
org.springframework.stereotype.Component
;
/**
* 转账订单的同步 Job
* <p>
* 由于转账订单的转账结果,有些渠道是异步通知进行同步的,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。
*
* @author jason
*/
@AllArgsConstructor
@Component
@JobExecutor
(
name
=
"PayTransferSyncJob"
)
public
class
PayTransferSyncJob
{
@DubboReference
private
RemotePayTransferService
transferService
;
public
ExecuteResult
jobExecute
(
JobArgs
jobArgs
)
{
int
count
=
transferService
.
syncTransfer
();
return
ExecuteResult
.
success
(
StrUtil
.
format
(
"同步转账订单 {} 个"
,
count
));
}
}
ruoyi-modules/ruoyi-job/src/main/java/org/dromara/job/snailjob/mall/promotion/combination/CombinationRecordExpireJob.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
job
.
snailjob
.
mall
.
promotion
.
combination
;
import
cn.hutool.core.util.StrUtil
;
import
com.aizuda.snailjob.client.job.core.annotation.JobExecutor
;
import
com.aizuda.snailjob.client.job.core.dto.JobArgs
;
import
com.aizuda.snailjob.client.model.ExecuteResult
;
import
lombok.AllArgsConstructor
;
import
org.apache.dubbo.config.annotation.DubboReference
;
import
org.dromara.common.mall.core.KeyValue
;
import
org.dromara.mall.api.promotion.RemoteCombinationRecordService
;
import
org.springframework.stereotype.Component
;
/**
* 拼团过期 Job
*
* @author HUIHUI
*/
@AllArgsConstructor
@Component
@JobExecutor
(
name
=
"CombinationRecordExpireJob"
)
public
class
CombinationRecordExpireJob
{
@DubboReference
private
RemoteCombinationRecordService
combinationRecordService
;
public
ExecuteResult
jobExecute
(
JobArgs
jobArgs
)
{
KeyValue
<
Integer
,
Integer
>
keyValue
=
combinationRecordService
.
expireCombinationRecord
();
return
ExecuteResult
.
success
(
StrUtil
.
format
(
"过期拼团 {} 个, 虚拟成团 {} 个"
,
keyValue
.
getKey
(),
keyValue
.
getValue
()));
}
}
ruoyi-modules/ruoyi-job/src/main/java/org/dromara/job/snailjob/mall/promotion/coupon/CouponExpireJob.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
job
.
snailjob
.
mall
.
promotion
.
coupon
;
import
cn.hutool.core.util.StrUtil
;
import
com.aizuda.snailjob.client.job.core.annotation.JobExecutor
;
import
com.aizuda.snailjob.client.job.core.dto.JobArgs
;
import
com.aizuda.snailjob.client.model.ExecuteResult
;
import
lombok.AllArgsConstructor
;
import
org.apache.dubbo.config.annotation.DubboReference
;
import
org.dromara.mall.api.promotion.RemoteCouponService
;
import
org.springframework.stereotype.Component
;
/**
* 优惠券过期 Job
*
* @author owen
*/
@AllArgsConstructor
@Component
@JobExecutor
(
name
=
"CouponExpireJob"
)
public
class
CouponExpireJob
{
@DubboReference
private
RemoteCouponService
couponService
;
public
ExecuteResult
jobExecute
(
JobArgs
jobArgs
)
{
int
count
=
couponService
.
expireCoupon
();
return
ExecuteResult
.
success
(
StrUtil
.
format
(
"过期优惠券 {} 个"
,
count
));
}
}
ruoyi-modules/ruoyi-job/src/main/java/org/dromara/job/snailjob/mall/statistics/product/ProductStatisticsJob.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
job
.
snailjob
.
mall
.
statistics
.
product
;
import
cn.hutool.core.convert.Convert
;
import
cn.hutool.core.util.NumberUtil
;
import
cn.hutool.core.util.ObjUtil
;
import
cn.hutool.core.util.StrUtil
;
import
com.aizuda.snailjob.client.job.core.annotation.JobExecutor
;
import
com.aizuda.snailjob.client.job.core.dto.JobArgs
;
import
com.aizuda.snailjob.client.model.ExecuteResult
;
import
lombok.AllArgsConstructor
;
import
org.apache.dubbo.config.annotation.DubboReference
;
import
org.dromara.mall.api.statistics.RemoteProductStatisticsService
;
import
org.springframework.stereotype.Component
;
/**
* 商品统计 Job
*
* @author owen
*/
@AllArgsConstructor
@Component
@JobExecutor
(
name
=
"ProductStatisticsJob"
)
public
class
ProductStatisticsJob
{
@DubboReference
private
RemoteProductStatisticsService
productStatisticsService
;
/**
* 执行商品统计任务
*
* @param jobArgs 要统计的天数,只能是正整数,1 代表昨日数据
* @return 统计结果
*/
public
ExecuteResult
jobExecute
(
JobArgs
jobArgs
)
{
String
param
=
(
String
)
jobArgs
.
getJobParams
();
// 默认昨日
param
=
ObjUtil
.
defaultIfBlank
(
param
,
"1"
);
// 校验参数的合理性
if
(!
NumberUtil
.
isInteger
(
param
))
{
throw
new
RuntimeException
(
"商品统计任务的参数只能为是正整数"
);
}
Integer
days
=
Convert
.
toInt
(
param
,
0
);
if
(
days
<
1
)
{
throw
new
RuntimeException
(
"商品统计任务的参数只能为是正整数"
);
}
String
result
=
productStatisticsService
.
statisticsProduct
(
days
);
return
ExecuteResult
.
success
(
StrUtil
.
format
(
"商品统计:\n{}"
,
result
));
}
}
ruoyi-modules/ruoyi-job/src/main/java/org/dromara/job/snailjob/mall/statistics/trade/TradeStatisticsJob.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
job
.
snailjob
.
mall
.
statistics
.
trade
;
import
cn.hutool.core.convert.Convert
;
import
cn.hutool.core.util.NumberUtil
;
import
cn.hutool.core.util.ObjUtil
;
import
cn.hutool.core.util.StrUtil
;
import
com.aizuda.snailjob.client.job.core.annotation.JobExecutor
;
import
com.aizuda.snailjob.client.job.core.dto.JobArgs
;
import
com.aizuda.snailjob.client.model.ExecuteResult
;
import
lombok.AllArgsConstructor
;
import
org.apache.dubbo.config.annotation.DubboReference
;
import
org.dromara.mall.api.statistics.RemoteTradeStatisticsService
;
import
org.springframework.stereotype.Component
;
/**
* 交易统计 Job
*
* @author owen
*/
@AllArgsConstructor
@Component
@JobExecutor
(
name
=
"TradeStatisticsJob"
)
public
class
TradeStatisticsJob
{
@DubboReference
private
RemoteTradeStatisticsService
tradeStatisticsService
;
/**
* 执行交易统计任务
*
* @param jobArgs 要统计的天数,只能是正整数,1 代表昨日数据
* @return 统计结果
*/
public
ExecuteResult
jobExecute
(
JobArgs
jobArgs
)
{
String
param
=
(
String
)
jobArgs
.
getJobParams
();
// 默认昨日
param
=
ObjUtil
.
defaultIfBlank
(
param
,
"1"
);
// 校验参数的合理性
if
(!
NumberUtil
.
isInteger
(
param
))
{
throw
new
RuntimeException
(
"交易统计任务的参数只能为是正整数"
);
}
Integer
days
=
Convert
.
toInt
(
param
,
0
);
if
(
days
<
1
)
{
throw
new
RuntimeException
(
"交易统计任务的参数只能为是正整数"
);
}
String
result
=
tradeStatisticsService
.
statisticsTrade
(
days
);
return
ExecuteResult
.
success
(
StrUtil
.
format
(
"交易统计:\n{}"
,
result
));
}
}
ruoyi-modules/ruoyi-job/src/main/java/org/dromara/job/snailjob/mall/trade/brokerage/BrokerageRecordUnfreezeJob.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
job
.
snailjob
.
mall
.
trade
.
brokerage
;
import
cn.hutool.core.util.StrUtil
;
import
com.aizuda.snailjob.client.job.core.annotation.JobExecutor
;
import
com.aizuda.snailjob.client.job.core.dto.JobArgs
;
import
com.aizuda.snailjob.client.model.ExecuteResult
;
import
lombok.AllArgsConstructor
;
import
org.apache.dubbo.config.annotation.DubboReference
;
import
org.dromara.mall.api.trade.RemoteBrokerageRecordService
;
import
org.springframework.stereotype.Component
;
/**
* 佣金解冻 Job
*
* @author owen
*/
@AllArgsConstructor
@Component
@JobExecutor
(
name
=
"BrokerageRecordUnfreezeJob"
)
public
class
BrokerageRecordUnfreezeJob
{
@DubboReference
private
RemoteBrokerageRecordService
brokerageRecordService
;
public
ExecuteResult
jobExecute
(
JobArgs
jobArgs
)
{
int
count
=
brokerageRecordService
.
unfreezeRecord
();
return
ExecuteResult
.
success
(
StrUtil
.
format
(
"解冻佣金 {} 个"
,
count
));
}
}
ruoyi-modules/ruoyi-job/src/main/java/org/dromara/job/snailjob/mall/trade/order/TradeOrderAutoCancelJob.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
job
.
snailjob
.
mall
.
trade
.
order
;
import
com.aizuda.snailjob.client.job.core.annotation.JobExecutor
;
import
com.aizuda.snailjob.client.job.core.dto.JobArgs
;
import
com.aizuda.snailjob.client.model.ExecuteResult
;
import
lombok.AllArgsConstructor
;
import
org.apache.dubbo.config.annotation.DubboReference
;
import
org.dromara.mall.api.trade.RemoteTradeOrderUpdateService
;
import
org.springframework.stereotype.Component
;
/**
* 交易订单的自动过期 Job
*
* @author 芋道源码
*/
@AllArgsConstructor
@Component
@JobExecutor
(
name
=
"TradeOrderAutoCancelJob"
)
public
class
TradeOrderAutoCancelJob
{
@DubboReference
private
RemoteTradeOrderUpdateService
tradeOrderUpdateService
;
public
ExecuteResult
jobExecute
(
JobArgs
jobArgs
)
{
int
count
=
tradeOrderUpdateService
.
cancelOrderBySystem
();
return
ExecuteResult
.
success
(
String
.
format
(
"过期订单 %s 个"
,
count
));
}
}
ruoyi-modules/ruoyi-job/src/main/java/org/dromara/job/snailjob/mall/trade/order/TradeOrderAutoCommentJob.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
job
.
snailjob
.
mall
.
trade
.
order
;
import
com.aizuda.snailjob.client.job.core.annotation.JobExecutor
;
import
com.aizuda.snailjob.client.job.core.dto.JobArgs
;
import
com.aizuda.snailjob.client.model.ExecuteResult
;
import
lombok.AllArgsConstructor
;
import
org.apache.dubbo.config.annotation.DubboReference
;
import
org.dromara.mall.api.trade.RemoteTradeOrderUpdateService
;
import
org.springframework.stereotype.Component
;
/**
* 交易订单的自动评论 Job
*
* @author 芋道源码
*/
@AllArgsConstructor
@Component
@JobExecutor
(
name
=
"TradeOrderAutoCommentJob"
)
public
class
TradeOrderAutoCommentJob
{
@DubboReference
private
RemoteTradeOrderUpdateService
tradeOrderUpdateService
;
public
ExecuteResult
jobExecute
(
JobArgs
jobArgs
)
{
int
count
=
tradeOrderUpdateService
.
createOrderItemCommentBySystem
();
return
ExecuteResult
.
success
(
String
.
format
(
"评论订单 %s 个"
,
count
));
}
}
ruoyi-modules/ruoyi-job/src/main/java/org/dromara/job/snailjob/mall/trade/order/TradeOrderAutoReceiveJob.java
0 → 100644
浏览文件 @
44629dc1
package
org
.
dromara
.
job
.
snailjob
.
mall
.
trade
.
order
;
import
com.aizuda.snailjob.client.job.core.annotation.JobExecutor
;
import
com.aizuda.snailjob.client.job.core.dto.JobArgs
;
import
com.aizuda.snailjob.client.model.ExecuteResult
;
import
lombok.AllArgsConstructor
;
import
org.apache.dubbo.config.annotation.DubboReference
;
import
org.dromara.mall.api.trade.RemoteTradeOrderUpdateService
;
import
org.springframework.stereotype.Component
;
/**
* 交易订单的自动收货 Job
*
* @author 芋道源码
*/
@AllArgsConstructor
@Component
@JobExecutor
(
name
=
"TradeOrderAutoReceiveJob"
)
public
class
TradeOrderAutoReceiveJob
{
@DubboReference
private
RemoteTradeOrderUpdateService
tradeOrderUpdateService
;
public
ExecuteResult
jobExecute
(
JobArgs
jobArgs
)
{
int
count
=
tradeOrderUpdateService
.
receiveOrderBySystem
();
return
ExecuteResult
.
success
(
String
.
format
(
"自动收货 %s 个"
,
count
));
}
}
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论