Compare commits
129 Commits
c333395b6c
...
test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ce0a52046 | ||
|
|
da77f92523 | ||
|
|
4d997d9b86 | ||
|
|
f1bafb98f6 | ||
|
|
47e2529b2b | ||
|
|
5f43006ee2 | ||
|
|
1a5959a7e1 | ||
|
|
351e4ab1ae | ||
|
|
634a84f8b8 | ||
|
|
edb1a0f8ed | ||
|
|
7a8291992c | ||
|
|
b689dd7741 | ||
|
|
1a9e602ce7 | ||
|
|
8a27141edc | ||
|
|
c4aff51c36 | ||
|
|
34bfe041e8 | ||
|
|
394e6b6bbb | ||
|
|
2e47c66fda | ||
|
|
42c01dc0a4 | ||
|
|
399875bc77 | ||
|
|
bcdba608c7 | ||
|
|
860f13914d | ||
|
|
24352b34db | ||
|
|
df2b0f52e3 | ||
|
|
61d85988fc | ||
|
|
6b3bc5d18e | ||
|
|
5f55c90e1b | ||
|
|
b94fcd17ac | ||
|
|
c8cc9a9cb4 | ||
|
|
2b2460a143 | ||
|
|
ee4645dce3 | ||
|
|
4da9bd6cc5 | ||
|
|
687d392392 | ||
|
|
c852d9f111 | ||
|
|
cabd6e3297 | ||
|
|
2af0f178dc | ||
|
|
f91d2d05e8 | ||
|
|
947e1cc891 | ||
|
|
e2a3280b70 | ||
|
|
9f622d460f | ||
|
|
0fd7756d17 | ||
|
|
891bfad529 | ||
|
|
958ae5a519 | ||
|
|
b02ed67fa4 | ||
|
|
9e98fa8c23 | ||
|
|
fd2dc0e033 | ||
|
|
d7a6be298d | ||
|
|
b2d29a1423 | ||
|
|
58df702cef | ||
|
|
c0794fb12e | ||
|
|
d15dabfeac | ||
|
|
396b26225f | ||
|
|
9323c6a9f9 | ||
|
|
545a0086e5 | ||
|
|
1b56bc7739 | ||
|
|
7cd9be9833 | ||
|
|
039c143891 | ||
|
|
65ae5e99c7 | ||
|
|
d6eb1962ca | ||
|
|
1fa8296385 | ||
|
|
1d79da5914 | ||
|
|
15461a216f | ||
|
|
e0aa7758dc | ||
|
|
b7293f7897 | ||
|
|
43558b32a9 | ||
|
|
d60d6d514e | ||
|
|
c4ef8701ef | ||
|
|
93f5561d8e | ||
|
|
c980ab67da | ||
|
|
f9b377fc62 | ||
|
|
049fe83cdc | ||
|
|
1f00961c1b | ||
|
|
815958a138 | ||
|
|
17489ae6f0 | ||
|
|
6c94476a8d | ||
|
|
2227271d08 | ||
|
|
b3e4055b55 | ||
|
|
79ddff3769 | ||
|
|
76e2cf0d60 | ||
|
|
01e6384e27 | ||
|
|
6ea653ca43 | ||
| d46b2a3691 | |||
|
|
22b87fdabf | ||
|
|
90b0d7836a | ||
|
|
c63a153b3e | ||
|
|
68deec4d9d | ||
|
|
2dc1202d04 | ||
|
|
48dd08b6d1 | ||
|
|
d6ef89b70d | ||
|
|
00a092a240 | ||
|
|
6ff39a4f83 | ||
|
|
4a2b3f4c4b | ||
|
|
83545f90bd | ||
|
|
3953d1128c | ||
|
|
db7dc1ad33 | ||
|
|
d6569c1b52 | ||
|
|
83bf2aa9fb | ||
|
|
924c27596e | ||
|
|
966e48ffb7 | ||
|
|
83da1e8bdc | ||
|
|
2a15568b36 | ||
|
|
413ce4c1ef | ||
|
|
95df0a4e12 | ||
|
|
bbc1081a47 | ||
|
|
c1ca1d5372 | ||
|
|
b93cc1ec51 | ||
|
|
a4ff83ec50 | ||
|
|
95d156940f | ||
|
|
8d32e5dbbe | ||
|
|
6919fc3e7f | ||
|
|
a5d3afaf9b | ||
|
|
80de697d59 | ||
|
|
ca75d937fc | ||
|
|
20e76ec7ae | ||
|
|
cc91b6bbcb | ||
|
|
2c2b1e2692 | ||
|
|
fdb3536ce3 | ||
|
|
6dbbda6e59 | ||
|
|
6e6e946ec8 | ||
|
|
c2e2de3742 | ||
|
|
6ea48a3b2f | ||
|
|
0f43cbc302 | ||
|
|
b637c619f1 | ||
|
|
4595cef06e | ||
|
|
86ab64a657 | ||
|
|
1e2b89f5fa | ||
|
|
199f953f35 | ||
|
|
688847d1a9 | ||
|
|
291d18bba7 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -61,6 +61,7 @@ package-lock.json
|
||||
# visual studio code
|
||||
.history
|
||||
*.log
|
||||
logs/**
|
||||
|
||||
functions/mock
|
||||
.temp/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
namespace: ns-d6a0e78ebd674c279614498e4c57b133
|
||||
namespace: ns-f16a3067ca7b434aad127d15eac82503
|
||||
name: zt-module-databus
|
||||
labels:
|
||||
app: zt-module-databus
|
||||
@@ -19,7 +19,7 @@ spec:
|
||||
labels:
|
||||
app: zt-module-databus
|
||||
spec:
|
||||
dnsPolicy: None
|
||||
dnsPolicy: ClusterFirst
|
||||
dnsConfig:
|
||||
nameservers:
|
||||
- "172.16.36.16"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
namespace: ns-d6a0e78ebd674c279614498e4c57b133
|
||||
namespace: ns-f16a3067ca7b434aad127d15eac82503
|
||||
name: zt-gateway
|
||||
labels:
|
||||
app: zt-gateway
|
||||
@@ -61,7 +61,7 @@ spec:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
namespace: ns-d6a0e78ebd674c279614498e4c57b133
|
||||
namespace: ns-f16a3067ca7b434aad127d15eac82503
|
||||
name: zt-gateway
|
||||
spec:
|
||||
type: NodePort
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
namespace: ns-d6a0e78ebd674c279614498e4c57b133
|
||||
namespace: ns-f16a3067ca7b434aad127d15eac82503
|
||||
name: zt-module-infra
|
||||
labels:
|
||||
app: zt-module-infra
|
||||
@@ -19,7 +19,7 @@ spec:
|
||||
labels:
|
||||
app: zt-module-infra
|
||||
spec:
|
||||
dnsPolicy: None
|
||||
dnsPolicy: ClusterFirst
|
||||
dnsConfig:
|
||||
nameservers:
|
||||
- "172.16.36.16"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
namespace: ns-d6a0e78ebd674c279614498e4c57b133
|
||||
namespace: ns-f16a3067ca7b434aad127d15eac82503
|
||||
name: zt-module-system
|
||||
labels:
|
||||
app: zt-module-system
|
||||
@@ -28,7 +28,7 @@ spec:
|
||||
operator: In
|
||||
values:
|
||||
- node-3
|
||||
dnsPolicy: None
|
||||
dnsPolicy: ClusterFirst
|
||||
dnsConfig:
|
||||
nameservers:
|
||||
- "172.16.36.16"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#apiVersion: apps/v1
|
||||
#kind: Deployment
|
||||
#metadata:
|
||||
# namespace: ns-d6a0e78ebd674c279614498e4c57b133
|
||||
# namespace: ns-f16a3067ca7b434aad127d15eac82503
|
||||
# name: zt-gateway
|
||||
# labels:
|
||||
# app: zt-gateway
|
||||
@@ -13,6 +13,8 @@
|
||||
# app: zt-gateway
|
||||
# template:
|
||||
# metadata:
|
||||
# annotations:
|
||||
# proxy.istio.io/config: '{"holdApplicationUntilProxyStarts": true}'
|
||||
# labels:
|
||||
# app: zt-gateway
|
||||
# spec:
|
||||
@@ -51,7 +53,7 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
namespace: ns-d6a0e78ebd674c279614498e4c57b133
|
||||
namespace: ns-f16a3067ca7b434aad127d15eac82503
|
||||
name: zt-gateway
|
||||
spec:
|
||||
type: NodePort
|
||||
@@ -67,7 +69,7 @@ spec:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
namespace: ns-d6a0e78ebd674c279614498e4c57b133
|
||||
namespace: ns-f16a3067ca7b434aad127d15eac82503
|
||||
name: zt-module-infra
|
||||
labels:
|
||||
app: zt-module-infra
|
||||
@@ -82,10 +84,12 @@ spec:
|
||||
app: zt-module-infra
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
proxy.istio.io/config: '{"holdApplicationUntilProxyStarts": true}'
|
||||
labels:
|
||||
app: zt-module-infra
|
||||
spec:
|
||||
dnsPolicy: None
|
||||
dnsPolicy: ClusterFirst
|
||||
dnsConfig:
|
||||
nameservers:
|
||||
- "172.16.36.16"
|
||||
@@ -128,7 +132,7 @@ spec:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
namespace: ns-d6a0e78ebd674c279614498e4c57b133
|
||||
namespace: ns-f16a3067ca7b434aad127d15eac82503
|
||||
name: zt-module-infra
|
||||
spec:
|
||||
type: NodePort
|
||||
@@ -144,7 +148,7 @@ spec:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
namespace: ns-d6a0e78ebd674c279614498e4c57b133
|
||||
namespace: ns-f16a3067ca7b434aad127d15eac82503
|
||||
name: zt-module-system
|
||||
labels:
|
||||
app: zt-module-system
|
||||
@@ -161,6 +165,8 @@ spec:
|
||||
metadata:
|
||||
labels:
|
||||
app: zt-module-system
|
||||
annotations:
|
||||
proxy.istio.io/config: '{"holdApplicationUntilProxyStarts": true}'
|
||||
spec:
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
@@ -171,7 +177,7 @@ spec:
|
||||
operator: In
|
||||
values:
|
||||
- node-3
|
||||
dnsPolicy: None
|
||||
dnsPolicy: ClusterFirst
|
||||
dnsConfig:
|
||||
nameservers:
|
||||
- "172.16.36.16"
|
||||
@@ -214,7 +220,7 @@ spec:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
namespace: ns-d6a0e78ebd674c279614498e4c57b133
|
||||
namespace: ns-f16a3067ca7b434aad127d15eac82503
|
||||
name: zt-module-system
|
||||
spec:
|
||||
type: NodePort
|
||||
@@ -230,7 +236,7 @@ spec:
|
||||
#apiVersion: apps/v1
|
||||
#kind: Deployment
|
||||
#metadata:
|
||||
# namespace: ns-d6a0e78ebd674c279614498e4c57b133
|
||||
# namespace: ns-f16a3067ca7b434aad127d15eac82503
|
||||
# name: zt-module-bpm
|
||||
# labels:
|
||||
# app: zt-module-bpm
|
||||
@@ -245,6 +251,8 @@ spec:
|
||||
# app: zt-module-bpm
|
||||
# template:
|
||||
# metadata:
|
||||
# annotations:
|
||||
# proxy.istio.io/config: '{"holdApplicationUntilProxyStarts": true}'
|
||||
# labels:
|
||||
# app: zt-module-bpm
|
||||
# spec:
|
||||
@@ -286,7 +294,7 @@ spec:
|
||||
#apiVersion: v1
|
||||
#kind: Service
|
||||
#metadata:
|
||||
# namespace: ns-d6a0e78ebd674c279614498e4c57b133
|
||||
# namespace: ns-f16a3067ca7b434aad127d15eac82503
|
||||
# name: zt-module-bpm
|
||||
#spec:
|
||||
# type: NodePort
|
||||
@@ -302,7 +310,7 @@ spec:
|
||||
#apiVersion: apps/v1
|
||||
#kind: Deployment
|
||||
#metadata:
|
||||
# namespace: ns-d6a0e78ebd674c279614498e4c57b133
|
||||
# namespace: ns-f16a3067ca7b434aad127d15eac82503
|
||||
# name: zt-module-report
|
||||
# labels:
|
||||
# app: zt-module-report
|
||||
@@ -358,7 +366,7 @@ spec:
|
||||
#apiVersion: v1
|
||||
#kind: Service
|
||||
#metadata:
|
||||
# namespace: ns-d6a0e78ebd674c279614498e4c57b133
|
||||
# namespace: ns-f16a3067ca7b434aad127d15eac82503
|
||||
# name: zt-module-report
|
||||
#spec:
|
||||
# type: NodePort
|
||||
@@ -374,7 +382,7 @@ spec:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
namespace: ns-d6a0e78ebd674c279614498e4c57b133
|
||||
namespace: ns-f16a3067ca7b434aad127d15eac82503
|
||||
name: zt-module-databus
|
||||
labels:
|
||||
app: zt-module-databus
|
||||
@@ -391,6 +399,8 @@ spec:
|
||||
metadata:
|
||||
labels:
|
||||
app: zt-module-databus
|
||||
annotations:
|
||||
proxy.istio.io/config: '{"holdApplicationUntilProxyStarts": true}'
|
||||
spec:
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
@@ -401,7 +411,7 @@ spec:
|
||||
operator: In
|
||||
values:
|
||||
- node-3
|
||||
dnsPolicy: None
|
||||
dnsPolicy: ClusterFirst
|
||||
dnsConfig:
|
||||
nameservers:
|
||||
- "172.16.36.16"
|
||||
@@ -444,7 +454,7 @@ spec:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
namespace: ns-d6a0e78ebd674c279614498e4c57b133
|
||||
namespace: ns-f16a3067ca7b434aad127d15eac82503
|
||||
name: zt-module-databus
|
||||
spec:
|
||||
type: NodePort
|
||||
@@ -462,7 +472,7 @@ spec:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
namespace: ns-d6a0e78ebd674c279614498e4c57b133
|
||||
namespace: ns-f16a3067ca7b434aad127d15eac82503
|
||||
name: zt-module-template
|
||||
labels:
|
||||
app: zt-module-template
|
||||
@@ -477,10 +487,12 @@ spec:
|
||||
app: zt-module-template
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
proxy.istio.io/config: '{"holdApplicationUntilProxyStarts": true}'
|
||||
labels:
|
||||
app: zt-module-template
|
||||
spec:
|
||||
dnsPolicy: None
|
||||
dnsPolicy: ClusterFirst
|
||||
dnsConfig:
|
||||
nameservers:
|
||||
- "172.16.36.16"
|
||||
@@ -523,7 +535,7 @@ spec:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
namespace: ns-d6a0e78ebd674c279614498e4c57b133
|
||||
namespace: ns-f16a3067ca7b434aad127d15eac82503
|
||||
name: zt-module-template
|
||||
spec:
|
||||
type: NodePort
|
||||
|
||||
31
docs/databus-client使用说明.md
Normal file
31
docs/databus-client使用说明.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Databus Client 使用说明
|
||||
|
||||
databus client 最主要用于调用基于http协议的第三方接口时需要记录调用日志到 databus 的情况, 通过databus client 调用第三方接口会将调用日志记录到databus的访问日志中
|
||||
|
||||
# 使用方法
|
||||
1. 添加依赖:
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-module-databus-client</artifactId>
|
||||
<version>3.0.47-SNAPSHOT</version>
|
||||
</dependency>
|
||||
```
|
||||
2. 注入 DatabusClient
|
||||
```java
|
||||
@Resource
|
||||
private DatabusClient databusClient;
|
||||
```
|
||||
3. 方法说明
|
||||
* get(...) : 发送 get 请求
|
||||
* post(...): 发送 post 请求
|
||||
* put(...): 发送 put 请求
|
||||
* delete(...): 发送 delete 请求
|
||||
* doRequest(...): 发送自定义请求
|
||||
4. 方法参数说明(由于所有方法参数都是一样的,所以在此统一说明)
|
||||
* String urlString: 请求的 http 接口地址(get/delete请求不需要带url参数)
|
||||
* Map<String, Object> data: 请求的参数(post/put方法会转换为json提交, get/delete会拼接到url上)
|
||||
* Map<String, String> headers: 请求头信息
|
||||
* String appId: databus 的appid
|
||||
* String authToken: databus 的访问令牌
|
||||
* Method method: doRequest 方法独有,如果要使用 get/post/put/delete 之外的方法,请使用doRequest方法并通过method参数指定
|
||||
514
docs/iWork用印流程集成开发文档.md
Normal file
514
docs/iWork用印流程集成开发文档.md
Normal file
@@ -0,0 +1,514 @@
|
||||
# iWork 用印流程集成开发文档
|
||||
|
||||
## 1. 概述
|
||||
|
||||
本文档描述了 ZT Cloud 平台与 iWork 系统的用印流程集成方案,包括流程发起、回调处理、消息通知及重试机制。
|
||||
|
||||
### 1.1 功能特性
|
||||
|
||||
- **流程发起**:支持用印专用流程和通用流程两种创建方式
|
||||
- **回调处理**:接收 iWork 回调,自动通知业务模块
|
||||
- **消息队列**:基于 RocketMQ 的异步消息通知机制
|
||||
- **自动重试**:失败回调自动重试,支持配置重试次数和间隔
|
||||
- **日志追踪**:完整记录流程创建和回调处理全生命周期
|
||||
|
||||
### 1.2 整体架构
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ 业务系统 │────▶│ System 模块 │────▶│ iWork 系统 │
|
||||
│ (调用方) │ │ (集成层) │ │ (OA 流程) │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
▲ ▲ │
|
||||
│ │ │
|
||||
│ └───────────────────────┘
|
||||
│ iWork 流程完成后回调
|
||||
│ ┌─────────────────┐
|
||||
│ │ RocketMQ │
|
||||
│ │ (消息队列) │
|
||||
└───────────────┴─────────────────┘
|
||||
```
|
||||
|
||||
### 1.3 完整流程时序图
|
||||
|
||||
```
|
||||
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
|
||||
│ 业务系统 │ │ System │ │ iWork │ │RocketMQ│ │业务消费者│
|
||||
└───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘
|
||||
│ │ │ │ │
|
||||
│ 1.发起用印流程 │ │ │ │
|
||||
│──────────────▶│ │ │ │
|
||||
│ │ 2.创建流程 │ │ │
|
||||
│ │──────────────▶│ │ │
|
||||
│ │ 返回requestId│ │ │
|
||||
│ │◀──────────────│ │ │
|
||||
│ 返回结果 │ │ │ │
|
||||
│◀──────────────│ │ │ │
|
||||
│ │ │ │ │
|
||||
│ │ │ 3.OA流程审批 │ │
|
||||
│ │ │ (异步进行) │ │
|
||||
│ │ │ │ │
|
||||
│ │ 4.流程完成回调 │ │ │
|
||||
│ │◀──────────────│ │ │
|
||||
│ │ │ │ │
|
||||
│ │ 5.发送MQ消息 │ │ │
|
||||
│ │──────────────────────────────▶│ │
|
||||
│ │ │ │ 6.投递消息 │
|
||||
│ │ │ │──────────────▶│
|
||||
│ │ │ │ │
|
||||
│ │ │ │ 7.返回处理结果 │
|
||||
│ │ │ │◀──────────────│
|
||||
│ │ 8.接收结果 │ │ │
|
||||
│ │◀──────────────────────────────│ │
|
||||
│ │ │ │ │
|
||||
│ │ 9.更新日志状态 │ │ │
|
||||
│ │ (成功/重试) │ │ │
|
||||
└───────────────┴───────────────┴───────────────┴───────────────┘
|
||||
```
|
||||
|
||||
**流程说明**:
|
||||
|
||||
1. **发起流程**:业务系统调用 System 模块的流程创建接口
|
||||
2. **创建流程**:System 模块调用 iWork API 创建 OA 流程,获取 `requestId`
|
||||
3. **OA 审批**:流程在 iWork 系统中流转(审批、签章等),此过程异步进行
|
||||
4. **iWork 回调**:流程完成后,iWork 系统主动回调 System 模块的回调接口
|
||||
5. **MQ 通知**:System 模块将回调数据通过 RocketMQ 发送给业务消费者
|
||||
6. **业务处理**:业务消费者接收消息并处理(如保存签章文件、更新业务状态)
|
||||
7. **返回结果**:业务消费者处理完成后,发送处理结果消息
|
||||
8. **接收结果**:System 模块接收处理结果
|
||||
9. **状态更新**:根据结果更新日志状态,失败则触发重试机制
|
||||
|
||||
## 2. 数据库设计
|
||||
|
||||
### 2.1 流程日志表 (system_iwork_workflow_log)
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| id | BIGINT | 主键 |
|
||||
| request_id | VARCHAR(128) | iWork 请求编号(唯一) |
|
||||
| workflow_id | BIGINT | 流程模板 ID |
|
||||
| business_code | VARCHAR(128) | 业务编码 |
|
||||
| biz_callback_key | VARCHAR(255) | 业务回调标识(MQ tag) |
|
||||
| raw_request | VARCHAR(2000) | 创建请求原文 |
|
||||
| status | VARCHAR(32) | 流程状态 |
|
||||
| callback_status | INTEGER | 回调处理状态 |
|
||||
| retry_count | INTEGER | 已重试次数 |
|
||||
| max_retry | INTEGER | 最大重试次数 |
|
||||
| last_error_message | VARCHAR(512) | 最后错误信息 |
|
||||
| raw_callback | VARCHAR(2000) | 回调原文 |
|
||||
| last_callback_time | TIMESTAMP | 最近回调时间 |
|
||||
| tenant_id | BIGINT | 租户编号 |
|
||||
|
||||
### 2.2 回调状态枚举 (callback_status)
|
||||
|
||||
| 值 | 状态 | 说明 |
|
||||
|----|------|------|
|
||||
| 0 | CREATE_PENDING | 创建中 |
|
||||
| 1 | CREATE_SUCCESS | 创建成功 |
|
||||
| 2 | CREATE_FAILED | 创建失败 |
|
||||
| 3 | CALLBACK_PENDING | 回调待处理 |
|
||||
| 4 | CALLBACK_SUCCESS | 回调处理成功 |
|
||||
| 5 | CALLBACK_FAILED | 回调处理失败 |
|
||||
| 6 | CALLBACK_RETRYING | 回调重试中 |
|
||||
| 7 | CALLBACK_RETRY_FAILED | 回调重试失败 |
|
||||
|
||||
### 2.3 状态流转图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 流程创建阶段 │
|
||||
│ ┌──────────┐ 成功 ┌──────────┐ │
|
||||
│ │ PENDING │ ─────────▶ │ SUCCESS │ │
|
||||
│ │ (0) │ │ (1) │ │
|
||||
│ └──────────┘ └──────────┘ │
|
||||
│ │ │
|
||||
│ │ 失败 │
|
||||
│ ▼ │
|
||||
│ ┌──────────┐ │
|
||||
│ │ FAILED │ │
|
||||
│ │ (2) │ │
|
||||
│ └──────────┘ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ iWork 回调
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 回调处理阶段 │
|
||||
│ ┌──────────┐ 成功 ┌──────────┐ │
|
||||
│ │ CALLBACK │ ─────────▶ │ CALLBACK │ │
|
||||
│ │ PENDING │ │ SUCCESS │ │
|
||||
│ │ (3) │ │ (4) │ │
|
||||
│ └──────────┘ └──────────┘ │
|
||||
│ │ │
|
||||
│ │ 失败 │
|
||||
│ ▼ │
|
||||
│ ┌──────────┐ 重试中 ┌──────────┐ │
|
||||
│ │ CALLBACK │ ◀───────▶ │ CALLBACK │ │
|
||||
│ │ FAILED │ │ RETRYING │ │
|
||||
│ │ (5) │ │ (6) │ │
|
||||
│ └──────────┘ └──────────┘ │
|
||||
│ │ │
|
||||
│ │ 重试次数耗尽 │
|
||||
│ ▼ │
|
||||
│ ┌──────────┐ │
|
||||
│ │ RETRY │ │
|
||||
│ │ FAILED(7)│ │
|
||||
│ └──────────┘ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 3. API 接口说明
|
||||
|
||||
### 3.1 用印流程创建
|
||||
|
||||
**接口地址**:`POST /admin-api/system/integration/iwork/workflow/create`
|
||||
|
||||
**请求参数**:
|
||||
|
||||
```json
|
||||
{
|
||||
"operatorUserId": "1001",
|
||||
"jbr": "1001",
|
||||
"yybm": "2001",
|
||||
"fb": "3001",
|
||||
"sqsj": "2025-01-30",
|
||||
"yyqx": "内部使用",
|
||||
"yyfkUrl": "https://example.com/attachment.pdf",
|
||||
"yysy": "合同盖章",
|
||||
"xyywjUrl": "https://example.com/contract.pdf",
|
||||
"yysx": "公章",
|
||||
"ywxtdjbh": "DJ-2025-0001",
|
||||
"bizCallbackKey": "seal-callback"
|
||||
}
|
||||
```
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| operatorUserId | 是 | 操作人 iWork 用户 ID |
|
||||
| jbr | 是 | 用印申请人 |
|
||||
| yybm | 是 | 用印部门 ID |
|
||||
| fb | 是 | 用印单位(分部 ID) |
|
||||
| sqsj | 是 | 申请时间 (yyyy-MM-dd) |
|
||||
| yyqx | 是 | 用印去向 |
|
||||
| xyywjUrl | 是 | 用印材料附件 URL |
|
||||
| yysx | 是 | 用印事项 |
|
||||
| ywxtdjbh | 是 | 业务系统单据编号 |
|
||||
| bizCallbackKey | 否 | 业务回调标识 |
|
||||
| yyfkUrl | 否 | 用印依据附件 URL |
|
||||
| yysy | 否 | 用印事由 |
|
||||
|
||||
### 3.2 通用流程创建
|
||||
|
||||
**接口地址**:`POST /admin-api/system/integration/iwork/workflow/create-generic`
|
||||
|
||||
**请求参数**:
|
||||
|
||||
```json
|
||||
{
|
||||
"operatorUserId": "1001",
|
||||
"workflowId": 54,
|
||||
"payload": {
|
||||
"requestName": "用印-DJ-2025-0001",
|
||||
"mainData": [
|
||||
{"fieldName": "jbr", "fieldValue": "1001"},
|
||||
{"fieldName": "yybm", "fieldValue": "2001"}
|
||||
]
|
||||
},
|
||||
"ywxtdjbh": "DJ-2025-0001",
|
||||
"bizCallbackKey": "seal-callback"
|
||||
}
|
||||
```
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| operatorUserId | 是 | 操作人 iWork 用户 ID |
|
||||
| workflowId | 是 | 流程模板 ID |
|
||||
| payload | 是 | 透传给 iWork 的业务参数 |
|
||||
| ywxtdjbh | 否 | 业务编码 |
|
||||
| bizCallbackKey | 否 | 业务回调标识 |
|
||||
|
||||
### 3.3 iWork 回调接口
|
||||
|
||||
**接口地址**:`POST /admin-api/system/integration/iwork/callback/file`
|
||||
|
||||
**说明**:此接口供 iWork 系统回调,无需认证(@PermitAll, @TenantIgnore)
|
||||
|
||||
**iWork 侧配置**:需要在 iWork 系统中配置回调地址,当流程完成时自动调用此接口。
|
||||
|
||||
**请求参数**:
|
||||
|
||||
```json
|
||||
{
|
||||
"requestId": "3603649",
|
||||
"businessCode": "DJ-2025-0001",
|
||||
"fileUrl": "https://iwork.example.com/signed-file.pdf",
|
||||
"fileName": "已签章合同.pdf",
|
||||
"status": "COMPLETED"
|
||||
}
|
||||
```
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| requestId | 是 | iWork 请求编号(与创建流程时返回的一致) |
|
||||
| businessCode | 是 | 业务编码(与创建流程时传入的 ywxtdjbh 一致) |
|
||||
| fileUrl | 是 | 签章后文件 URL |
|
||||
| fileName | 否 | 文件名称 |
|
||||
| status | 否 | 业务状态 |
|
||||
|
||||
**回调处理逻辑**:
|
||||
|
||||
1. 根据 `requestId` 查询流程创建日志,获取 `bizCallbackKey`
|
||||
2. 更新日志状态为 `CALLBACK_PENDING`
|
||||
3. 发送 MQ 消息通知业务模块(仅当 `bizCallbackKey` 存在时)
|
||||
4. 返回处理结果
|
||||
|
||||
## 4. MQ 消息机制
|
||||
|
||||
### 4.1 消息流程图
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ iWork 回调 │───▶│ System 模块 │───▶│ RocketMQ │───▶│ 业务消费者 │
|
||||
│ │ │ (Producer) │ │ │ │ │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ 更新日志状态 │◀───│ System 模块 │◀───│ RocketMQ │◀───│ 返回处理结果 │
|
||||
│ │ │ (Listener) │ │ │ │ │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
### 4.2 Topic 定义
|
||||
|
||||
| Topic | 说明 |
|
||||
|-------|------|
|
||||
| SYSTEM_IWORK_BIZ_CALLBACK | 回调通知消息(System → 业务模块) |
|
||||
| SYSTEM_IWORK_BIZ_CALLBACK_RESULT | 处理结果消息(业务模块 → System) |
|
||||
|
||||
### 4.3 回调通知消息 (IWorkBizCallbackMessage)
|
||||
|
||||
```java
|
||||
{
|
||||
"requestId": "3603649",
|
||||
"bizCallbackKey": "seal-callback",
|
||||
"payload": { /* 回调原始数据 */ },
|
||||
"attempt": 0,
|
||||
"maxAttempts": 3
|
||||
}
|
||||
```
|
||||
|
||||
**Tag 规则**:消息 tag = `bizCallbackKey`,业务模块按 tag 订阅
|
||||
|
||||
### 4.4 处理结果消息 (IWorkBizCallbackResultMessage)
|
||||
|
||||
```java
|
||||
{
|
||||
"requestId": "3603649",
|
||||
"bizCallbackKey": "seal-callback",
|
||||
"success": true,
|
||||
"errorMessage": null,
|
||||
"attempt": 0,
|
||||
"maxAttempts": 3,
|
||||
"payload": { /* 原始数据,用于重试 */ }
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 业务模块接入指南
|
||||
|
||||
### 5.1 添加依赖
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-module-system-api</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### 5.2 实现消费者
|
||||
|
||||
```java
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@RocketMQMessageListener(
|
||||
topic = IWorkBizCallbackMessage.TOPIC,
|
||||
consumerGroup = IWorkBizCallbackMessage.TOPIC + "_YOUR_BIZ_KEY",
|
||||
selectorExpression = "your-biz-callback-key" // 与 bizCallbackKey 一致
|
||||
)
|
||||
public class YourBizCallbackConsumer implements RocketMQListener<IWorkBizCallbackMessage> {
|
||||
|
||||
private final RocketMQTemplate rocketMQTemplate;
|
||||
|
||||
@Override
|
||||
public void onMessage(IWorkBizCallbackMessage message) {
|
||||
log.info("收到 iWork 回调: requestId={}", message.getRequestId());
|
||||
|
||||
IWorkBizCallbackResultMessage result;
|
||||
try {
|
||||
// 处理业务逻辑
|
||||
processCallback(message);
|
||||
|
||||
result = IWorkBizCallbackResultMessage.builder()
|
||||
.requestId(message.getRequestId())
|
||||
.bizCallbackKey(message.getBizCallbackKey())
|
||||
.success(true)
|
||||
.attempt(message.getAttempt())
|
||||
.maxAttempts(message.getMaxAttempts())
|
||||
.payload(message.getPayload())
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("处理回调失败", e);
|
||||
result = IWorkBizCallbackResultMessage.builder()
|
||||
.requestId(message.getRequestId())
|
||||
.bizCallbackKey(message.getBizCallbackKey())
|
||||
.success(false)
|
||||
.errorMessage(e.getMessage())
|
||||
.attempt(message.getAttempt())
|
||||
.maxAttempts(message.getMaxAttempts())
|
||||
.payload(message.getPayload())
|
||||
.build();
|
||||
}
|
||||
|
||||
// 发送处理结果
|
||||
rocketMQTemplate.syncSend(IWorkBizCallbackResultMessage.TOPIC, result);
|
||||
}
|
||||
|
||||
private void processCallback(IWorkBizCallbackMessage message) {
|
||||
// 业务处理逻辑
|
||||
// 1. 解析 payload 获取回调数据
|
||||
// 2. 更新业务状态
|
||||
// 3. 保存签章文件等
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 关键配置项
|
||||
|
||||
| 配置项 | 说明 |
|
||||
|--------|------|
|
||||
| consumerGroup | 消费者组,建议格式:`TOPIC + "_" + bizCallbackKey` |
|
||||
| selectorExpression | Tag 过滤,必须与发起流程时的 `bizCallbackKey` 一致 |
|
||||
|
||||
### 5.4 注意事项
|
||||
|
||||
1. **bizCallbackKey 唯一性**:每个业务场景使用独立的 bizCallbackKey
|
||||
2. **幂等处理**:消费者需实现幂等,同一 requestId 可能重复投递
|
||||
3. **必须返回结果**:处理完成后必须发送 `IWorkBizCallbackResultMessage`
|
||||
4. **错误信息**:失败时填写 errorMessage,便于问题排查
|
||||
|
||||
## 6. 重试机制
|
||||
|
||||
### 6.1 重试流程
|
||||
|
||||
```
|
||||
业务处理失败 → 返回 success=false → System Listener 接收
|
||||
↓
|
||||
检查 attempt < maxAttempts?
|
||||
↓ ↓
|
||||
是 否
|
||||
↓ ↓
|
||||
延迟后重新投递 标记最终失败
|
||||
```
|
||||
|
||||
### 6.2 配置参数
|
||||
|
||||
```yaml
|
||||
iwork:
|
||||
callback:
|
||||
retry:
|
||||
max-attempts: 3 # 最大重试次数
|
||||
delay-seconds: 5 # 重试间隔(秒)
|
||||
```
|
||||
|
||||
### 6.3 手工重试
|
||||
|
||||
**接口地址**:`POST /admin-api/system/integration/iwork/log/retry`
|
||||
|
||||
```json
|
||||
{
|
||||
"requestId": "3603649"
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 日志查询
|
||||
|
||||
### 7.1 分页查询接口
|
||||
|
||||
**接口地址**:`POST /admin-api/system/integration/iwork/log/page`
|
||||
|
||||
**请求参数**:
|
||||
|
||||
```json
|
||||
{
|
||||
"requestId": "3603649",
|
||||
"businessCode": "DJ-2025-0001",
|
||||
"bizCallbackKey": "seal-callback",
|
||||
"status": 4,
|
||||
"pageNo": 1,
|
||||
"pageSize": 10
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 本地开发调试
|
||||
|
||||
### 8.1 隔离测试环境
|
||||
|
||||
为避免与测试环境消息冲突,本地开发时需修改:
|
||||
|
||||
1. **Listener 消费者组**:添加本地标识后缀
|
||||
```java
|
||||
consumerGroup = IWorkBizCallbackResultMessage.TOPIC + "_CONSUMER_local"
|
||||
```
|
||||
|
||||
2. **Listener Tag 过滤**:使用本地专用 tag
|
||||
```java
|
||||
selectorExpression = "local_test"
|
||||
```
|
||||
|
||||
3. **业务消费者**:同样使用本地专用 bizCallbackKey
|
||||
```java
|
||||
selectorExpression = "your-biz-key_local"
|
||||
```
|
||||
|
||||
4. **数据库记录**:将 `biz_callback_key` 设为本地专用值
|
||||
|
||||
### 8.2 调试建议
|
||||
|
||||
- 使用独立的 `bizCallbackKey` 避免消息串扰
|
||||
- 检查 RocketMQ 控制台确认消息投递情况
|
||||
- 关注日志中的 `requestId` 进行链路追踪
|
||||
|
||||
## 9. 常见问题
|
||||
|
||||
### Q1: 业务消费者收不到消息?
|
||||
|
||||
检查项:
|
||||
- `selectorExpression` 是否与 `bizCallbackKey` 一致
|
||||
- 消费者组名是否正确
|
||||
- RocketMQ 连接是否正常
|
||||
|
||||
### Q2: 收到重复消息?
|
||||
|
||||
可能原因:
|
||||
- 多个环境的 Listener 都在消费同一 topic
|
||||
- 解决:使用独立的消费者组和 tag 过滤
|
||||
|
||||
### Q3: 重试不生效?
|
||||
|
||||
检查项:
|
||||
- 是否正确返回了 `IWorkBizCallbackResultMessage`
|
||||
- `success` 字段是否为 `false`
|
||||
- 配置的 `max-attempts` 是否大于当前 `attempt`
|
||||
|
||||
## 10. 相关代码位置
|
||||
|
||||
| 组件 | 路径 |
|
||||
|------|------|
|
||||
| Controller | `zt-module-system-server/.../controller/admin/integration/iwork/IWorkIntegrationController.java` |
|
||||
| Service | `zt-module-system-server/.../service/integration/iwork/impl/IWorkIntegrationServiceImpl.java` |
|
||||
| 日志 Service | `zt-module-system-server/.../service/integration/iwork/impl/IWorkWorkflowLogServiceImpl.java` |
|
||||
| MQ Producer | `zt-module-system-server/.../mq/iwork/IWorkBizCallbackProducer.java` |
|
||||
| MQ Listener | `zt-module-system-server/.../mq/iwork/IWorkBizCallbackListener.java` |
|
||||
| 消息定义 | `zt-module-system-api/.../mq/iwork/IWorkBizCallbackMessage.java` |
|
||||
| 配置类 | `zt-module-system-server/.../framework/integration/iwork/config/IWorkProperties.java` |
|
||||
@@ -1,6 +1,6 @@
|
||||
# iWork 统一集成使用说明
|
||||
|
||||
本文档介绍如何在 System 模块中使用项目已实现的统一 iWork 流程发起能力(controller + service + properties)。内容包含:配置项、调用方式(内部 Java 调用 & 外部 HTTP 调用)、请求/响应示例、错误处理、缓存与 Token 生命周期、典型问题与排查步骤。
|
||||
本文档介绍如何在 System 模块中使用项目已实现的统一 iWork 流程发起能力(controller + service + properties)。内容包含:配置项、调用方式(内部 Java 调用 & 外部 HTTP 调用)、请求/响应示例、错误处理、缓存与 Token 生命周期、业务回调分发与重试、流程日志查询,以及典型问题与排查步骤。
|
||||
|
||||
---
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
- 提供 Service 层 `IWorkIntegrationService`,供其它模块以 Spring Bean 注入方式直接调用。
|
||||
- 使用 `IWorkProperties` 绑定 `application.yml` 中 `iwork` 的配置项。
|
||||
- Token / 会话采用本地 Caffeine 缓存缓存(按 appId + operatorUserId 缓存 session),并在到期前按配置提前刷新。
|
||||
- 使用统一配置的 appId、公钥以及默认流程编号,无需再维护多套凭证。
|
||||
- 使用统一配置的 appId、公钥以及默认流程编号,无需再维护多套凭证,所有调用强制使用配置的 appId,不再接受请求覆盖。
|
||||
- 全链路以 requestId 作为唯一业务标识(发起返回、作废入参、日志查询、回调与重试均基于 requestId),workflowId 仅用于指定 iWork 模板。
|
||||
- 支持业务回调标识 bizCallbackKey(字符串,≤255),发起时提交,回调时按标识分发业务回调并带自动重试与手工重试入口。
|
||||
- 回调与日志记录会保存截断后的原始回调请求/响应文本(无需脱敏),无业务附件或处理异常时标记失败。
|
||||
|
||||
---
|
||||
|
||||
@@ -47,6 +50,10 @@ iwork:
|
||||
token:
|
||||
ttl-seconds: 3600 # token 有效期(秒)
|
||||
refresh-ahead-seconds: 60 # 在到期前多少秒认为需要刷新
|
||||
callback:
|
||||
retry:
|
||||
max-attempts: 3 # 业务回调自动重试次数(默认 3 次,可调整)
|
||||
delay-seconds: 5 # 业务回调自动重试间隔秒数(默认 5 秒,可调整)
|
||||
client:
|
||||
connect-timeout: 5s
|
||||
response-timeout: 30s
|
||||
@@ -55,10 +62,11 @@ iwork:
|
||||
说明:
|
||||
|
||||
- `base-url` 为 iWork 网关的基础地址,不能留空。
|
||||
- `app-id` 与 `client-public-key` 共同构成注册/申请 token 所需的凭据信息,由配置统一提供,不再支持多套切换。
|
||||
- `workflow-id` 提供全局默认流程编号,单次调用也可通过 `workflowId` 覆盖。
|
||||
- `app-id` 与 `client-public-key` 共同构成注册/申请 token 所需的凭据信息,由配置统一提供,不再支持多套切换,系统强制使用配置的 appId,忽略请求中的 appId。
|
||||
- `workflow-id` 提供全局默认流程编号,仅用于向 iWork 指定流程模板;流程标识、查询、补偿、回调与重试一律使用 requestId,不再以 workflowId 标识业务。
|
||||
- 请求头键名固定为 `app-id`、`client-public-key`、`secret`、`token`、`time`、`user-id`,无需在配置中重复声明。
|
||||
- `org.*` 配置负责 iWork 人力组织 REST 代理:`token-seed` 为与 iWork 约定的标识,系统会自动将其与毫秒时间戳拼接并计算 MD5 生成 `key`,无需额外传递 token。
|
||||
- `callback.retry.*` 控制业务回调的自动重试次数与间隔,默认为 3 次、5 秒,可按需调整。
|
||||
|
||||
---
|
||||
|
||||
@@ -102,7 +110,7 @@ Controller 暴露的 REST 接口:
|
||||
## 请求 VO 说明(重要字段)
|
||||
|
||||
- IWorkBaseReqVO(公用字段)
|
||||
- `appId` (String):为兼容历史接口保留,系统始终使用配置项 `iwork.app-id`。
|
||||
- `appId` (String):仅保留字段,系统强制使用配置项 `iwork.app-id`,忽略请求值。
|
||||
- `operatorUserId` (String):在 iWork 内部代表操作人的用户编号(可为空,框架会使用 `properties.userId`)。
|
||||
- `forceRefreshToken` (Boolean):是否强制刷新 token(例如遇到 token 错误时强制刷新)。
|
||||
|
||||
@@ -117,7 +125,7 @@ Controller 暴露的 REST 接口:
|
||||
- `success` / `message`:调用成功标志与提示信息。
|
||||
|
||||
- IWorkWorkflowCreateReqVO(统一用印流程发起)
|
||||
- `workflowId` (String):流程模板 ID,必填,不再回退到配置。
|
||||
- `workflowId` (String):流程模板 ID,必填,通常由配置 `iwork.workflow-id` 提供;仅用于向 iWork 指定模板,不再作为业务标识。
|
||||
- `jbr`:用印申请人(iWork 人员 ID,必填)。
|
||||
- `yybm`:用印部门 ID(必填)。
|
||||
- `fb`:用印单位/分部 ID(必填)。
|
||||
@@ -128,10 +136,11 @@ Controller 暴露的 REST 接口:
|
||||
- `xyywjUrl`:用印材料附件 URL(必填)。
|
||||
- `yysx`:用印事项(必填)。
|
||||
- `ywxtdjbh`:业务系统单据编号(必填,同时用于生成流程标题“用印-{ywxtdjbh}”)。
|
||||
- `bizCallbackKey` (String):业务回调标识,≤255 字符,回调时按该标识分发到对应业务回调入口(可选但推荐)。
|
||||
- 额外字段不再支持,Service 会根据以上字段自动补齐固定流程类型 (`lclx=2979600781334966993`) 与签署动作 (`qsdz=CORPORATE`)。
|
||||
|
||||
- IWorkWorkflowVoidReqVO(作废)
|
||||
- `requestId` (String):流程请求编号(必填)。
|
||||
- `requestId` (String):流程请求编号(必填,唯一标识)。
|
||||
- `reason`、`extraParams`、`formExtras` 等用于传递作废原因或额外字段。
|
||||
|
||||
- IWorkFormFieldVO(表单字段)
|
||||
@@ -188,8 +197,9 @@ public class MyService {
|
||||
|
||||
说明:
|
||||
|
||||
- 若需使用特定凭证,可设置 `req.setAppId("my-iwork-app")`。
|
||||
- 若需覆盖默认流程模板,可调用 `req.setWorkflowId(123L)` 指定。
|
||||
- 无需设置 appId,系统强制使用配置项 `iwork.app-id`。
|
||||
- `workflowId` 通常来自配置 `iwork.workflow-id`,也可在请求中指定模板编号,但流程标识一律以返回的 requestId 为准。
|
||||
- 可设置 `bizCallbackKey` 以便回调时分发到对应业务处理;建议在发起后立即记录响应中的 requestId 及 bizCallbackKey。
|
||||
- 若希望以特定 iWork 操作人发起,可设置 `req.setOperatorUserId("1001")`。
|
||||
|
||||
---
|
||||
@@ -202,7 +212,6 @@ public class MyService {
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"appId":"my-iwork-app",
|
||||
"identifierKey":"loginid",
|
||||
"identifierValue":"zhangsan"
|
||||
}' \
|
||||
@@ -238,19 +247,20 @@ curl -X POST -H "Content-Type: application/json" -d '{
|
||||
"yysy":"与客户合同用印",
|
||||
"xyywjUrl":"https://files.example.com/contract.pdf",
|
||||
"yysx":"合同用印",
|
||||
"ywxtdjbh":"DJ-2025-0001"
|
||||
"ywxtdjbh":"DJ-2025-0001",
|
||||
"bizCallbackKey":"seal-flow-callback"
|
||||
}' https://your-zt-server/admin-api/system/integration/iwork/workflow/create
|
||||
```
|
||||
|
||||
> 说明:外部仍以 JSON 请求调用本服务,系统在向 iWork 转发时会自动将负载转换为 `application/x-www-form-urlencoded` 表单(含 `requestName`、`workflowId`、`mainData` 等字段)。
|
||||
> 响应的 `data.requestId` 为唯一业务标识,请在业务侧保存;appId 始终取配置;`xyywjFileName` 不存储。
|
||||
|
||||
1. Void workflow
|
||||
|
||||
```bash
|
||||
curl -X POST -H "Content-Type: application/json" -d '{
|
||||
"requestId":"REQ-001",
|
||||
"reason":"作废原因",
|
||||
"appId":"my-iwork-app"
|
||||
"reason":"作废原因"
|
||||
}' https://your-zt-server/admin-api/system/integration/iwork/workflow/void
|
||||
```
|
||||
|
||||
@@ -259,34 +269,98 @@ curl -X POST -H "Content-Type: application/json" -d '{
|
||||
## 核心逻辑与细节
|
||||
|
||||
1. 基础参数解析
|
||||
appId:始终取配置 `iwork.app-id`,忽略请求值。
|
||||
主标识:全链路仅使用 `requestId`(发起返回、作废入参、日志查询、回调分发、重试都基于 requestId)。
|
||||
模板:`workflowId` 仅用于选择 iWork 模板,不作为业务标识。
|
||||
业务回调标识:`bizCallbackKey`(字符串,≤255),发起时提交,回调按该标识分发到具体业务回调入口。
|
||||
附件文件名:`xyywjFileName` 不存储也不参与处理;仅使用 `xyywjUrl`。
|
||||
|
||||
系统始终使用 `application.yml` 中配置的 `app-id` 与 `client-public-key` 与 iWork 通信。
|
||||
请求体中的 `appId` 字段仅为兼容历史调用而保留,框架内部不会使用该值做切换。
|
||||
|
||||
1. Workflow 模板解析
|
||||
|
||||
调用时优先使用请求体中的 `workflowId`。
|
||||
若未显式传入,则回退到全局 `iwork.workflow-id`,若仍为空则抛出 `IWORK_WORKFLOW_ID_MISSING`。
|
||||
|
||||
1. 注册 + RSA + Token
|
||||
|
||||
- 在首次或 token 过期时,会按以下步骤获取 session:
|
||||
调用时可在请求体中指定 `workflowId` 以选择模板;若未显式传入,则回退到全局 `iwork.workflow-id`,若仍为空则抛出 `IWORK_WORKFLOW_ID_MISSING`。
|
||||
POST `/system/integration/iwork/workflow/create`:发起用印流程,返回 `requestId`。
|
||||
POST `/system/integration/iwork/workflow/void`:作废流程,请求需携带 `requestId`。
|
||||
POST `/system/integration/iwork/callback/file`(上游回调):接收 iWork 回调,校验 `requestId`,若缺 `bizCallbackKey` 则记录失败但不中断附件保存;下载并落库附件,记录回调日志(截断原文),并在提供 `bizCallbackKey` 时通过 RocketMQ 分发业务回调(自动/手工重试)。
|
||||
POST `/system/integration/iwork/log/page`:分页查询用印回调日志(requestId / 业务单号 / bizCallbackKey / 状态 / 时间段)。
|
||||
POST `/system/integration/iwork/log/retry`:手工重试业务回调,需权限,入参 `requestId`(状态为失败/超重试时可用)。
|
||||
1. 向 iWork 的 `register` 接口发起请求(Headers 包含 appId 与 clientPublicKey)。
|
||||
2. 从注册响应中获取 `secret` 与 `spk`(服务端公钥),使用本地的 client 公钥做 RSA 加密(`spk` 用于加密),得到加密后的 secret 与 encryptedUserId。
|
||||
3. 使用注册返回的密钥申请 token(apply-token),token 会被按 `ttl-seconds` 缓存。
|
||||
|
||||
- `IWorkIntegrationServiceImpl` 中维护一个 Caffeine `sessionCache`,缓存 key 为 `appId::operatorUserId`。
|
||||
发起:`workflowId`、`jbr`、`yybm`、`fb`、`sqsj`、`yyqx`、`xyywjUrl`、`yysx`、`ywxtdjbh` 必填;`bizCallbackKey` 可选但建议提供;appId 忽略。
|
||||
作废:`requestId` 必填;`reason` 可选。
|
||||
回调:若无法找到业务附件(或不存在),标记用印失败并记日志,不抛出未捕获异常;记录原始回调文本(截断)。
|
||||
- 当 token 接近到期(`refresh-ahead-seconds`)时会在下一次请求触发刷新。
|
||||
|
||||
1. 请求构造
|
||||
|
||||
- 用户解析、作废等场景继续以 `application/json` 调用 iWork。
|
||||
- 用印流程发起在转发至 iWork 时改为 `application/x-www-form-urlencoded` 表单,请求正文包含 `requestName`、`workflowId` 及字符串化后的 `mainData`,与 iWork 网关当前要求保持一致。
|
||||
- 认证 Header:由 `IWorkProperties.Headers` 中的常量控制,固定键名为 `app-id`、`client-public-key`、`secret`、`token`、`time`、`user-id`。
|
||||
1) 发起:校验必填 → 使用配置 appId 与模板 workflowId → 申请/缓存 token → 以 form-urlencoded 调 iWork → 返回 `requestId`,记录发起日志状态。
|
||||
2) 回调(/callback/file):校验 requestId + bizCallbackKey → 校验租户/必备字段 → 下载/保存附件(若缺业务附件则直接标记失败并记日志)→ 写入回调日志,保存原始回调请求/响应(截断)→ 通过 RocketMQ 发送业务回调消息(topic=`SYSTEM_IWORK_BIZ_CALLBACK`,tag=`bizCallbackKey`)。
|
||||
3) 业务处理 & 结果上报(跨模块/跨进程):业务模块订阅 `SYSTEM_IWORK_BIZ_CALLBACK` 对应 tag 处理后,将结果发布到 `SYSTEM_IWORK_BIZ_CALLBACK_RESULT`(tag 同 bizCallbackKey,携带 requestId / bizCallbackKey / success / errorMessage / payload / attempt / maxAttempts),system 模块消费结果并更新日志,失败且未超限时由 system 端按配置延迟重试再次投递回调消息。
|
||||
4) 重试:默认 3 次、间隔 5 秒,可配置(`iwork.callback.retry.*`),手工重试仍使用 `/log/retry`,由 system 模块重新投递 MQ 消息。
|
||||
5) 手工重试:需具备 iWork 模块权限;根据 requestId 将任务重新投递至回调分发器,不增加自动重试计数,可在日志详情中发起。
|
||||
|
||||
1. 响应解析
|
||||
`CREATE_PENDING` / `CREATE_SUCCESS` / `CREATE_FAILED`
|
||||
`CALLBACK_PENDING` / `CALLBACK_SUCCESS` / `CALLBACK_FAILED`
|
||||
`CALLBACK_RETRYING` / `CALLBACK_RETRY_FAILED`
|
||||
|
||||
- 实现里对响应成功的判定比较宽松:检查 `code`、`status`、`success`、`errno` 等字段(支持布尔、字符串 ‘0’/‘1’/‘success’)以判断是否成功,并解析常见的 message 字段 `msg|message|errmsg`。
|
||||
---
|
||||
|
||||
## 用印流程日志与业务回调
|
||||
|
||||
### 范围与标识
|
||||
|
||||
- appId:始终取配置 `iwork.app-id`,忽略请求值。
|
||||
- 主标识:全链路仅使用 `requestId`(发起返回、作废入参、日志查询、回调分发、重试都基于 requestId)。
|
||||
- 模板:`workflowId` 仅用于选择 iWork 模板,不作为业务标识。
|
||||
- 业务回调标识:`bizCallbackKey`(字符串,≤255),发起时提交,回调按该标识分发到具体业务回调入口。
|
||||
- 附件文件名:`xyywjFileName` 不存储也不参与处理;仅使用 `xyywjUrl`。
|
||||
|
||||
### 入口(REST)
|
||||
|
||||
- POST `/system/integration/iwork/workflow/create`:发起用印流程,返回 `requestId`。
|
||||
- POST `/system/integration/iwork/workflow/void`:作废流程,请求需携带 `requestId`。
|
||||
- POST `/system/integration/iwork/callback/file`(上游回调):接收 iWork 回调,校验 `requestId`,若缺 `bizCallbackKey` 则记录失败但不中断附件保存;下载并落库附件,记录回调日志(截断原文),并在提供 `bizCallbackKey` 时通过 RocketMQ 分发业务回调(自动/手工重试)。
|
||||
- POST `/system/integration/iwork/log/page`:分页查询用印回调日志(requestId / 业务单号 / bizCallbackKey / 状态 / 时间段)。
|
||||
- POST `/system/integration/iwork/log/retry`:手工重试业务回调,需权限,入参 `requestId`(状态为失败/超重试时可用)。
|
||||
|
||||
### 必填/校验要点
|
||||
|
||||
- 发起:`workflowId`、`jbr`、`yybm`、`fb`、`sqsj`、`yyqx`、`xyywjUrl`、`yysx`、`ywxtdjbh` 必填;`bizCallbackKey` 可选但建议提供;appId 忽略。
|
||||
- 作废:`requestId` 必填;`reason` 可选。
|
||||
- 回调:若无法找到业务附件(或不存在),标记用印失败并记日志,不抛出未捕获异常;记录原始回调文本(截断)。
|
||||
|
||||
### 处理流程(摘要)
|
||||
|
||||
1) 发起:校验必填 → 使用配置 appId 与模板 workflowId → 申请/缓存 token → 以 form-urlencoded 调 iWork → 返回 `requestId`,记录发起日志状态。
|
||||
2) 回调(/callback/file):校验 requestId + bizCallbackKey → 校验租户/必备字段 → 下载/保存附件(若缺业务附件则直接标记失败并记日志)→ 写入回调日志,保存原始回调请求/响应(截断)→ 通过 RocketMQ 发送业务回调消息(topic=`SYSTEM_IWORK_BIZ_CALLBACK`,tag=`bizCallbackKey`)。
|
||||
3) 业务处理 & 结果上报(跨模块/跨进程):业务模块订阅 `SYSTEM_IWORK_BIZ_CALLBACK` 对应 tag 处理后,将结果发布到 `SYSTEM_IWORK_BIZ_CALLBACK_RESULT`(tag 同 bizCallbackKey,携带 requestId / bizCallbackKey / success / errorMessage / payload / attempt / maxAttempts),system 模块消费结果并更新日志,失败且未超限时由 system 端按配置延迟重试再次投递回调消息。
|
||||
4) 重试:默认 3 次、间隔 5 秒,可配置(`iwork.callback.retry.*`),手工重试仍使用 `/log/retry`,由 system 模块重新投递 MQ 消息。
|
||||
5) 手工重试:需具备 iWork 模块权限;根据 requestId 将任务重新投递至回调分发器,不增加自动重试计数,可在日志详情中发起。
|
||||
|
||||
### 状态字面量(示例)
|
||||
|
||||
- `CREATE_PENDING` / `CREATE_SUCCESS` / `CREATE_FAILED`
|
||||
- `CALLBACK_PENDING` / `CALLBACK_SUCCESS` / `CALLBACK_FAILED`
|
||||
- `CALLBACK_RETRYING` / `CALLBACK_RETRY_FAILED`
|
||||
|
||||
> 实际实现可根据需要细化,唯一标识均为 requestId。
|
||||
|
||||
### 重试与配置
|
||||
|
||||
- 自动重试:默认 `maxAttempts=3`、`delaySeconds=5`,可配置(示例键:`iwork.callback.retry.max-attempts`、`iwork.callback.retry.delay-seconds`)。
|
||||
- 手工重试:权限校验(沿用 iWork 模块权限前缀);仅在失败/超重试状态下开放。
|
||||
- 幂等:按 requestId + bizCallbackKey 进行幂等检查,成功后不再重复分发,除非手工强制重试。
|
||||
|
||||
### 日志存储
|
||||
|
||||
- 表:`system_iwork_seal_callback_log`(示例字段)
|
||||
- `requestId`(PK)、`businessCode`(ywxtdjbh)、`bizCallbackKey`、`status`、`retryCount`、`lastErrorMessage`、`fileUrl`、`fileId`、`businessFileId`、`requestBody`、`responseBody`、`rawCallback`(截断保存原文)、`lastCallbackTime`、`creator`、`createTime`、`updateTime`。
|
||||
- 字段要点:
|
||||
- `rawCallback`:保存回调原始文本,截断存储(无需脱敏,不注明上限)。
|
||||
- 无业务附件或保存失败:写 `status=CREATE_FAILED` / `CALLBACK_FAILED` 并记录错误原因。
|
||||
|
||||
### 查询与展示
|
||||
|
||||
- 分页:沿用 `PageParam` 约束,`pageNo` 默认 1、`pageSize` 默认 10、上限 10000,仅分页浏览不支持导出。
|
||||
- 查询条件:`requestId`、`businessCode`(ywxtdjbh)、`bizCallbackKey`、`status`、时间范围(createTime / lastCallbackTime)。
|
||||
- 列表字段:requestId、业务单号、bizCallbackKey、状态、重试次数、最后错误、更新时间。
|
||||
- 详情:展示截断的回调原文(请求/响应)、错误原因、附件信息;提供“手工重试”按钮(需权限)。
|
||||
|
||||
---
|
||||
|
||||
@@ -326,8 +400,8 @@ curl -X POST -H "Content-Type: application/json" -d '{
|
||||
|
||||
## 小结与建议
|
||||
|
||||
- 在配置中补齐 `iwork.app-id`、`iwork.client-public-key`、`iwork.user-id`、`iwork.workflow-id` 等关键字段。
|
||||
- 优先在本地通过 `IWorkIntegrationService` Java API 调试,成功后再通过 Controller 的 REST 接口对外暴露。
|
||||
- 若遇到请求失败,查看应用日志(`[iWork]` 前缀的日志)与 iWork 网关返回 body,定位是注册、申请 token,还是业务接口(user-info/create/void)失败。
|
||||
- 在配置中补齐 `iwork.app-id`、`iwork.client-public-key`、`iwork.user-id`、`iwork.workflow-id` 等关键字段,并按需配置回调重试参数(默认 3 次,5 秒间隔)。
|
||||
- 优先在本地通过 `IWorkIntegrationService` Java API 调试,成功后再通过 Controller 的 REST 接口对外暴露;发起后请记录返回的 requestId(唯一标识)与 bizCallbackKey。
|
||||
- 若遇到请求失败,查看应用日志(`[iWork]` 前缀的日志)与 iWork 网关返回 body;若业务回调失败,可在日志页面查看截断原文并按权限发起手工重试。
|
||||
|
||||
文档已生成并保存到:`docs/iWork集成说明.md`。
|
||||
|
||||
BIN
docs/iwork 人力资源组织机构RESTFUL接口说明.pdf
Normal file
BIN
docs/iwork 人力资源组织机构RESTFUL接口说明.pdf
Normal file
Binary file not shown.
202
docs/数据总线用户使用指南.md
Normal file
202
docs/数据总线用户使用指南.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# 数据总线(Databus)用户使用指南
|
||||
|
||||
> **适用范围**:`zt-module-databus-server`(管理与运行时)、`zt-module-databus-api`(对外协议),面向已部署 `ztcloud` 后端 + `zt-vue-element` 前端的环境。文档区分 **业务管理员** 与 **第三方开发者** 两类读者,帮助您从零到一完成配置、发布与调用。
|
||||
|
||||
## 1. 角色视角与总体流程
|
||||
|
||||
| 角色 | 核心目标 | 关键系统入口 |
|
||||
| --- | --- | --- |
|
||||
| 业务管理员 | 在后台可视化界面中定义/编排 API,配置凭证、限流、访问日志,并确保版本可控 | 后台路径:`系统管理 → 数据总线`(或直接访问 `/#/databus`)|
|
||||
| 第三方开发者 | 按照后台下发的 `apiCode + version + appId`,满足安全约束后调用统一网关,解析响应并接入自身系统 | 对外入口:`{protocol}://{host}{basePath}/{apiCode}/{version}`(默认 Base Path `/admin-api/databus/api/portal`)|
|
||||
|
||||
完整流程示意:
|
||||
1. **管理员初始化**(执行 DM8 脚本、开通菜单与权限)。
|
||||
2. **管理员在“API 定义”中建模**(步骤、变换、策略、版本发布)。
|
||||
3. **管理员为业务方创建凭证**(`ZT-App-Id`、密钥、IP 白名单、匿名用户等)。
|
||||
4. **第三方拿到凭证后发起调用**(签名 → 加密 → 发送 → 解密响应)。
|
||||
5. **管理员通过限流/日志功能持续运维**,必要时回滚版本或刷新缓存。
|
||||
|
||||
## 2. 通用准备事项
|
||||
|
||||
### 2.1 服务与配置
|
||||
|
||||
- **运行中服务**:`zt-server` 网关、`zt-module-databus-*` 相关微服务、Redis(用于 nonce、防重放、限流计数)。
|
||||
- **关键配置**(摘自 `application.yml`):
|
||||
- `databus.api-portal.base-path`:默认 `/admin-api/databus/api/portal`,如需兼容旧系统可保留 `/databus/api/portal` 别名。
|
||||
- `allowed-ips/denied-ips`:无白名单时留空即放行全部;建议第三方出口 IP 全量登记。
|
||||
- `security`:`signature-type(MD5/SHA256)`、`encryption-type(AES/DES)`、`nonce-ttl`、`allowed-clock-skew-seconds`、是否强制请求/响应加密。
|
||||
- `enable-tenant-header`:如开启多租,会自动从 `ZT-Tenant-Id`(可自定义)透传。
|
||||
|
||||
### 2.2 数据库与菜单初始化(DM8)
|
||||
|
||||
| 场景 | 脚本(目录 `sql/dm/`) | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 初始化菜单与按钮权限 | `统一外部网关菜单_20251010.sql` | 建立“数据总线”一级菜单及 API/凭证/策略/访问日志页面的所有操作权限,支持重复执行。 |
|
||||
| 访问日志菜单补充 | `数据总线API访问日志菜单权限_20251028.sql` | 若已执行主菜单脚本仍找不到访问日志入口,可追加该脚本。 |
|
||||
| API 版本历史表 | `数据总线API版本历史表结构_备忘录模式_20251030.sql` | DM8 兼容语法,包含索引与字段注释,确保支持版本快照/回滚。 |
|
||||
| 访问日志表 | `数据总线API访问日志表结构_20251028.sql` | 记录追踪 ID、请求/响应、步骤结果等,供后台“访问日志”页面检索。 |
|
||||
| 示例/演示数据 | `../mysql/databus_sample_data.sql`(MySQL) | 可作为建模参考,字段一致,迁移到 DM 时仅需替换数据类型即可。 |
|
||||
|
||||
> **执行顺序建议**:先运行菜单脚本 → 赋予管理员角色对应按钮权限 → 执行表结构脚本 → 根据需要导入示例数据。
|
||||
|
||||
## 3. 业务管理员操作路线
|
||||
|
||||
### 3.1 角色与权限校验
|
||||
|
||||
1. 通过 DM8 脚本写入菜单/按钮后,将 `数据总线` 相关权限分配给目标角色(如 `system_admin`)。
|
||||
2. 确认角色拥有以下最少权限:`databus:gateway:*`、`databus:credential:*`、`databus:policy:*`、`databus:gateway:access-log:query`。
|
||||
|
||||
### 3.2 创建 API 定义
|
||||
|
||||
进入 **数据总线 → API 定义**,按以下步骤操作:
|
||||
|
||||
1. 点击“新建”填写基础信息:`API 编码 (apiCode)`、`版本 (version)`、`描述`、`HTTP Method`、`URI Pattern`(运行期仅用于文档,可自由描述)。
|
||||
2. 选择 **认证策略** 与 **限流策略**:默认策略可复用,也可在对应菜单先新建。
|
||||
3. 选择 **状态 = 草稿**,避免未完成配置就被调度。
|
||||
|
||||
> **提示**:Start/End 步骤系统会自动校验唯一性,必须至少存在一条 Start 与 End 步骤。
|
||||
|
||||
### 3.3 配置编排步骤与变换
|
||||
|
||||
1. 在 API 详情页新增步骤:
|
||||
- **类型**:HTTP / RPC / Script / Start / End。
|
||||
- **目标地址**:HTTP 需写 `METHOD http://host/path`,并按需填写 Header/Body 映射表达式。
|
||||
- **映射表达式**:支持 JSON 表达式,常用写法示例同 `databus_sample_data.sql` 中 `JSON::(...)`。
|
||||
- **重试/超时**:以 JSON 形式配置 `maxAttempts`、`delayMs`。
|
||||
2. 配置步骤级或 API 级 **Transform**:
|
||||
- 场景示例:请求前补链路追踪 ID(`REQUEST_PRE`)、响应前组装统一结构(`RESPONSE_PRE`)。
|
||||
- 校验规则:同一 `phase` 仅允许一条记录。
|
||||
|
||||
### 3.4 版本与发布
|
||||
|
||||
1. **保存即可生成版本快照**:系统会在 `databus_api_version` 中写入 JSON 快照并标记 `is_current=1`。
|
||||
2. **调试**:在 API 列表点击“调试”,通过 `POST /databus/gateway/invoke` 模拟真实请求,可自带 Header/Body。
|
||||
3. **上线**:修改 `状态=ONLINE` 后保存,系统自动刷新运行时 Flow。若需全量刷新,可在列表右上点击“刷新缓存”。
|
||||
4. **回滚**:在“版本历史”侧栏选择目标版本 → 点击“回滚”,系统将历史快照恢复为现有配置并重新发布。
|
||||
|
||||
### 3.5 凭证与策略运营
|
||||
|
||||
1. **客户端凭证**:
|
||||
- 进入“客户端凭证” → 新建 → 生成 `appId`、密钥、签名/加密算法。
|
||||
- 可绑定匿名用户(后台会在调用时自动模拟该用户登录)。
|
||||
- 支持 IP 白名单、描述及状态维护。
|
||||
2. **限流策略**:
|
||||
- 在“限流策略”菜单中新增 `FIXED_WINDOW` 类型,配置 `limit`、`windowSeconds`、`keyTemplate`。
|
||||
- 常见做法:`${apiCode}:${header.X-Client-Id}`,以不同客户端维度做计数。
|
||||
|
||||
### 3.6 监控与排障
|
||||
|
||||
1. **访问日志**:菜单路径“访问日志”,支持按 `traceId / apiCode / 时间` 检索详情,字段对应 `databus_api_access_log`。
|
||||
2. **常见问题定位**:
|
||||
- 401 → 校验凭证/签名/时间戳。
|
||||
- 429 → 限流策略触发,可在日志中观察 `status=1`/`errorCode=API_RATE_LIMIT_EXCEEDED`。
|
||||
- 5xx → 检查步骤 `stepResults` 中的 `error` 描述、Integration Flow 日志。
|
||||
|
||||
## 4. 第三方开发者操作路线
|
||||
|
||||
### 4.1 凭证申请
|
||||
|
||||
向业务管理员提供以下信息:调用系统名称、出口 IP 列表、是否需要匿名账号、预期调用 API 列表。管理员创建凭证后会得到:
|
||||
|
||||
- `ZT-App-Id`
|
||||
- `encryptionKey` + `encryptionType`(AES/DES)
|
||||
- `signatureType`(MD5/SHA256)
|
||||
- 可选:匿名内部用户、租户 ID
|
||||
|
||||
### 4.2 网络与安全要求
|
||||
|
||||
- 请求必须来自允许的 IP,且使用 HTTPS(推荐)或可信的内网段。
|
||||
- 时间戳与服务器偏差 ≤ `allowed-clock-skew-seconds`(默认 300 秒)。
|
||||
- `nonce` 在 `nonce-ttl-seconds` 内不得重复(默认 600 秒,服务器使用 Redis 去重)。
|
||||
- 若开启 `require-body-encryption`,请求体必须是 **加密后再 Base64 编码** 的字符串。
|
||||
|
||||
### 4.3 构建请求的 7 个步骤
|
||||
|
||||
| 步骤 | 动作 | 关键点 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 生成 `timestamp` | `long` 类型毫秒值;建议每次现取。 |
|
||||
| 2 | 生成 `nonce` | ≥8 位随机字符串,推荐 `UUID` 去掉 `-`。 |
|
||||
| 3 | 准备明文 Body | JSON 文本,记为 `plainBody`。GET 请求仍建议传空 JSON `{}`,以便签名。 |
|
||||
| 4 | 计算签名 | 将 `appId`、`timestamp`、`nonce`、`plainBody`(或 Query)按 key 排序拼接 `key=value`,使用约定算法得出 `signature`。 |
|
||||
| 5 | 加密 Body | 以凭证密钥对 `plainBody` 执行 AES/DES 加密,再 Base64(需与服务端配置一致)。 |
|
||||
| 6 | 组装请求头 | 至少包含 `ZT-App-Id`、`ZT-Timestamp`、`ZT-Nonce`、`ZT-Signature`、`Content-Type`,可附带 `ZT-Tenant-Id`、`X-Client-Id`。 |
|
||||
| 7 | 发送请求 | URL = `{basePath}/{apiCode}/{version}`,HTTP 方法与后台配置一致;响应若被加密需反向解密。 |
|
||||
|
||||
#### 请求示例(伪代码)
|
||||
|
||||
```text
|
||||
POST https://gw.example.com/admin-api/databus/api/portal/order.create/v1
|
||||
Headers:
|
||||
ZT-App-Id: demo-app
|
||||
ZT-Timestamp: 1732070400000
|
||||
ZT-Nonce: 0c5e2df9a1
|
||||
ZT-Signature: 8e377...
|
||||
X-Client-Id: mall
|
||||
Body (Base64):Q2hhcnNldGV4dC1CYXNlNjQgZW5jcnlwdGVkIGJvZHk=
|
||||
```
|
||||
|
||||
#### 响应处理
|
||||
|
||||
1. 读取 HTTP 状态与 `code/message/traceId` 字段。
|
||||
2. 若后台开启响应加密,需使用同一密钥解密得到真正的 `response` JSON。
|
||||
3. 保留 `traceId` 以便与管理员对齐日志。
|
||||
|
||||
### 4.4 常见故障排查
|
||||
|
||||
| 症状 | 常见原因 | 自助检查 |
|
||||
| --- | --- | --- |
|
||||
| 401 + `签名校验失败` | 拼接顺序错误 / 对象未序列化一致 | 确认按字典序拼接 `key=value`,body 使用明文。 |
|
||||
| 401 + `请求到达时间超出` | 客户端时间漂移 | 同步 NTP,或在发送前刷新服务器时间。 |
|
||||
| 401 + `重复请求` | `nonce` 重复或被重放 | 确保每次生成唯一随机串。 |
|
||||
| 403 | IP/应用被禁用 | 让管理员检查凭证状态、白名单。 |
|
||||
| 429 | 限流策略触发 | 调整并发或与管理员协商提升 `limit`。 |
|
||||
| 5xx + `步骤执行失败` | 后端步骤调用异常 | 提供 `traceId`,管理员可在访问日志中查看 `stepResults` 详细报错。 |
|
||||
|
||||
## 5. 策略、监控与日志
|
||||
|
||||
### 5.1 限流与审计
|
||||
|
||||
- 策略以 JSON 存储在 `databus_policy_rate_limit`,字段:`limit`(次数)、`windowSeconds`(窗口秒)及可选 `keyTemplate`。
|
||||
- 当前默认策略实现为 **Redis 固定窗口**:Key 格式 `databus:api:rl:{apiCode}:{version}:{X-Client-Id}`。
|
||||
- 触发限流时返回 `HTTP 429`,日志中 `status=1` 且 `errorCode=API_RATE_LIMIT_EXCEEDED`。
|
||||
- 如需关闭限流,管理员可在 `application.yml` 中设置 `databus.api-portal.enable-rate-limit=false` 或在 API 上解除策略绑定。
|
||||
- 审计日志由 `ApiGatewayAccessLogger` 写入 `databus_api_access_log`,可结合 ELK/BI 做分析。
|
||||
|
||||
### 5.2 访问日志使用
|
||||
|
||||
1. **建表**:执行 `sql/dm/数据总线API访问日志表结构_20251028.sql`(已含主键、索引、字段注释)。
|
||||
2. **后台入口**:`数据总线 → 访问日志`,需 `databus:gateway:access-log:query` 权限。
|
||||
3. **常见字段解释**:
|
||||
- `REQUEST_*`:存储原始请求;如启用请求加密,会记录解密后的明文。
|
||||
- `STEP_RESULTS`:JSON 数组,展示每个步骤的 `elapsed`、`request/response`、`error`,用于定位链路问题。
|
||||
- `TRACE_ID`:可与链路追踪或日志平台联动。
|
||||
4. **TODO**:如目标环境尚未执行该表脚本,请尽快在 DM 数据库运行,以免访问日志页面无法查询。
|
||||
|
||||
## 6. 演示数据与调试建议
|
||||
|
||||
- 使用 `sql/mysql/databus_sample_data.sql` 可一次性生成:
|
||||
- 3 个示例 API(聚合查询 / 用户画像 / 快速登录)。
|
||||
- 对应的步骤、变换、限流策略、发布记录。
|
||||
- 若目标环境为 DM,可参考该脚本结构,将 `AUTO_INCREMENT` 改为手动 ID、`datetime` 改 `timestamp`,即可导入。
|
||||
- 通过示例数据,管理员可演练以下操作:
|
||||
1. 打开 API 列表验证导入内容;
|
||||
2. 使用“调试”通过管理端发起调用,确认步骤链路;
|
||||
3. 让第三方按照真实流程调用,观察访问日志、限流表现。
|
||||
|
||||
## 7. 附录:常用 REST 接口速查
|
||||
|
||||
| 模块 | 方法 | 路径 | 用途 |
|
||||
| --- | --- | --- | --- |
|
||||
| API 定义 | GET | `/databus/gateway/definition/page` | 列表检索 |
|
||||
| | POST | `/databus/gateway/definition` | 新建/更新 API(PUT 更新) |
|
||||
| | DELETE | `/databus/gateway/definition/{id}` | 删除并注销 Flow |
|
||||
| API 版本 | GET | `/databus/gateway/version/list?apiId=` | 历史版本 |
|
||||
| | PUT | `/databus/gateway/version/rollback` | 回滚 |
|
||||
| 客户端凭证 | POST | `/databus/gateway/credential/create` | 创建凭证 |
|
||||
| 限流策略 | POST | `/databus/gateway/policy/rate-limit` | 创建/更新策略 |
|
||||
| 调试/刷新 | POST | `/databus/gateway/invoke` | 管理端调试 |
|
||||
| | POST | `/databus/gateway/cache/refresh` | 全量刷新 Flow |
|
||||
| 日志 | GET | `/databus/gateway/access-log/page` | 查看访问日志 |
|
||||
|
||||
---
|
||||
如仍有未覆盖的业务场景,可在 `docs/数据总线模块大致功能与调用介绍.md` 中查阅更底层的架构说明,再结合本文步骤完成落地。祝使用顺利!
|
||||
617
docs/菜单数据权限使用文档.md
Normal file
617
docs/菜单数据权限使用文档.md
Normal file
@@ -0,0 +1,617 @@
|
||||
# 菜单数据权限使用文档
|
||||
|
||||
## 📖 目录
|
||||
|
||||
1. [功能介绍](#功能介绍)
|
||||
2. [架构说明](#架构说明)
|
||||
3. [开发指南](#开发指南)
|
||||
4. [配置指南](#配置指南)
|
||||
5. [注意事项](#注意事项)
|
||||
6. [完整示例](#完整示例)
|
||||
7. [常见问题](#常见问题)
|
||||
|
||||
---
|
||||
|
||||
## 功能介绍
|
||||
|
||||
### 什么是菜单数据权限?
|
||||
|
||||
菜单数据权限是一种**基于菜单的动态数据过滤机制**,允许管理员为不同的角色配置不同的数据查询规则,实现细粒度的数据权限控制。
|
||||
|
||||
### 核心特性
|
||||
|
||||
- ✅ **动态配置**:无需修改代码,通过页面配置即可实现数据过滤
|
||||
- ✅ **基于角色**:不同角色可以看到不同的数据
|
||||
- ✅ **灵活规则**:支持多种条件(等于、大于、IN、LIKE等)
|
||||
- ✅ **变量支持**:支持动态变量(如当前用户ID、部门ID等)
|
||||
- ✅ **SQL级别**:在SQL层面过滤,性能高效
|
||||
|
||||
### 使用场景
|
||||
|
||||
1. **部门数据隔离**:用户只能查看自己部门的数据
|
||||
2. **创建人过滤**:用户只能查看自己创建的数据
|
||||
3. **状态过滤**:某些角色只能查看特定状态的数据
|
||||
4. **自定义规则**:根据业务需求配置任意过滤条件
|
||||
|
||||
---
|
||||
|
||||
## 架构说明
|
||||
|
||||
### 模块结构
|
||||
|
||||
```
|
||||
zt-framework/
|
||||
└── zt-spring-boot-starter-biz-data-permission/ # 框架模块
|
||||
└── menudatapermission/
|
||||
├── annotation/ # @PermissionData 注解
|
||||
├── aop/ # AOP 切面
|
||||
├── context/ # ThreadLocal 上下文
|
||||
├── handler/ # MyBatis 拦截器
|
||||
├── model/ # DTO 模型
|
||||
└── util/ # 工具类
|
||||
|
||||
zt-module-system/
|
||||
└── zt-module-system-server/ # 业务模块
|
||||
└── framework/permission/
|
||||
└── MenuDataRuleLoaderImpl.java # 规则加载器实现
|
||||
```
|
||||
|
||||
### 工作流程
|
||||
|
||||
```
|
||||
1. 用户访问页面(如:角色管理)
|
||||
↓
|
||||
2. Controller 方法上有 @PermissionData 注解
|
||||
↓
|
||||
3. AOP 切面拦截,根据 pageComponent 查询菜单ID
|
||||
↓
|
||||
4. 加载该菜单下用户角色关联的数据规则
|
||||
↓
|
||||
5. 将规则存入 ThreadLocal
|
||||
↓
|
||||
6. MyBatis 执行查询时,拦截器读取规则
|
||||
↓
|
||||
7. 构建 SQL WHERE 条件并添加到查询中
|
||||
↓
|
||||
8. 返回过滤后的数据
|
||||
↓
|
||||
9. finally 块清理 ThreadLocal
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 前置条件
|
||||
|
||||
### ✅ 检查 Maven 依赖
|
||||
|
||||
在使用菜单数据权限功能前,**必须确保业务模块已引入框架依赖**。
|
||||
|
||||
#### 1. 检查依赖是否存在
|
||||
|
||||
打开业务模块的 `pom.xml` 文件(如 `zt-module-xxx-server/pom.xml`),检查是否包含以下依赖:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-spring-boot-starter-biz-data-permission</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
#### 2. 如何检查?
|
||||
|
||||
**方法1:查看 pom.xml**
|
||||
```bash
|
||||
# 在项目根目录执行
|
||||
grep -r "zt-spring-boot-starter-biz-data-permission" zt-module-xxx/zt-module-xxx-server/pom.xml
|
||||
```
|
||||
|
||||
**方法2:在 IDE 中查看**
|
||||
- IDEA:打开 `pom.xml`,搜索 `zt-spring-boot-starter-biz-data-permission`
|
||||
- 或者查看 Maven 依赖树
|
||||
|
||||
#### 3. 如果没有依赖,如何添加?
|
||||
|
||||
在业务模块的 `pom.xml` 中添加:
|
||||
|
||||
```xml
|
||||
<dependencies>
|
||||
<!-- 其他依赖 -->
|
||||
|
||||
<!-- 菜单数据权限框架 -->
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-spring-boot-starter-biz-data-permission</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
#### 4. 已包含该依赖的模块
|
||||
|
||||
以下模块已默认包含该依赖,可直接使用:
|
||||
- ✅ `zt-module-system-server`
|
||||
- ⚠️ 其他业务模块需自行检查
|
||||
|
||||
---
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 步骤1:在 Controller 方法上添加注解
|
||||
|
||||
在需要数据过滤的查询方法上添加 `@PermissionData` 注解:
|
||||
|
||||
```java
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.annotation.PermissionData;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/system/role")
|
||||
public class RoleController {
|
||||
|
||||
@GetMapping("/page")
|
||||
@PermissionData(pageComponent = "system/role/index") // 指定页面组件路径
|
||||
public CommonResult<PageResult<RoleRespVO>> getRolePage(RolePageReqVO pageReqVO) {
|
||||
PageResult<RoleDO> pageResult = roleService.getRolePage(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, RoleRespVO.class));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤2:确定 pageComponent 值
|
||||
|
||||
`pageComponent` 是前端页面组件的路径,用于关联菜单:
|
||||
|
||||
- **格式**:`模块名/功能名/页面名`
|
||||
- **示例**:
|
||||
- `system/role/index` - 角色管理页面
|
||||
- `system/user/index` - 用户管理页面
|
||||
- `system/dept/index` - 部门管理页面
|
||||
|
||||
**如何查找 pageComponent?**
|
||||
|
||||
1. 打开前端项目,找到对应的 Vue 文件路径
|
||||
2. 例如:`src/views/system/role/index.vue`
|
||||
3. pageComponent 就是:`system/role/index`
|
||||
|
||||
### 步骤3:在菜单表中配置 component 字段
|
||||
|
||||
确保数据库 `system_menu` 表中,该菜单的 `component` 字段值与 `pageComponent` 一致:
|
||||
|
||||
```sql
|
||||
-- 示例:角色管理菜单
|
||||
UPDATE system_menu
|
||||
SET component = 'system/role/index'
|
||||
WHERE id = 角色管理菜单ID;
|
||||
```
|
||||
|
||||
### 注解参数说明
|
||||
|
||||
```java
|
||||
@PermissionData(
|
||||
pageComponent = "system/role/index", // 必填:页面组件路径
|
||||
enable = true // 可选:是否启用,默认 true
|
||||
)
|
||||
```
|
||||
|
||||
**何时设置 `enable = false`?**
|
||||
|
||||
当某个方法不需要数据权限过滤时(如管理员查看所有数据):
|
||||
|
||||
```java
|
||||
@GetMapping("/all")
|
||||
@PermissionData(pageComponent = "system/role/index", enable = false)
|
||||
public CommonResult<List<RoleRespVO>> getAllRoles() {
|
||||
// 返回所有角色,不受数据权限限制
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置指南
|
||||
|
||||
### 步骤1:配置菜单
|
||||
|
||||
1. 登录系统,进入 **系统管理 > 菜单管理**
|
||||
2. 找到需要配置数据权限的菜单(如:角色管理)
|
||||
3. 确认该菜单的 **组件路径** 字段与代码中的 `pageComponent` 一致
|
||||
|
||||
### 步骤2:配置数据规则
|
||||
|
||||
1. 在菜单列表中,点击对应菜单的 **"数据规则"** 按钮
|
||||
2. 点击 **"新增规则"** 按钮
|
||||
3. 填写规则信息:
|
||||
|
||||
#### 规则字段说明
|
||||
|
||||
| 字段 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| **规则名称** | 规则的描述性名称 | `只看自己创建的角色` |
|
||||
| **规则字段** | 数据库表的字段名 | `creator`、`dept_id`、`status` |
|
||||
| **规则条件** | 比较条件 | `=`、`IN`、`LIKE` 等 |
|
||||
| **规则值** | 比较的值,支持变量 | `#{userId}`、`#{deptId}` |
|
||||
| **状态** | 启用/禁用 | 启用 |
|
||||
| **排序** | 规则执行顺序 | 0 |
|
||||
|
||||
#### 支持的规则条件
|
||||
|
||||
| 条件 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `=` | 等于 | `creator = #{userId}` |
|
||||
| `!=` | 不等于 | `status != 0` |
|
||||
| `>` | 大于 | `create_time > '2024-01-01'` |
|
||||
| `<` | 小于 | `sort < 100` |
|
||||
| `>=` | 大于等于 | `age >= 18` |
|
||||
| `<=` | 小于等于 | `price <= 1000` |
|
||||
| `IN` | 包含 | `dept_id IN (#{deptIds})` |
|
||||
| `NOT_IN` | 不包含 | `status NOT_IN (0,1)` |
|
||||
| `LIKE` | 模糊匹配 | `name LIKE '张'` |
|
||||
| `NOT_LIKE` | 不匹配 | `name NOT_LIKE '测试'` |
|
||||
| `IS_NULL` | 为空 | `deleted_time IS_NULL` |
|
||||
| `IS_NOT_NULL` | 不为空 | `phone IS_NOT_NULL` |
|
||||
| `BETWEEN` | 区间 | `age BETWEEN 18,60` |
|
||||
| `NOT_BETWEEN` | 不在区间 | `score NOT_BETWEEN 0,60` |
|
||||
| `SQL_RULE` | 自定义SQL | `(dept_id = 1 OR creator = 2)` |
|
||||
|
||||
#### 支持的变量
|
||||
|
||||
| 变量 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `#{userId}` | 当前用户ID | `creator = #{userId}` |
|
||||
| `#{username}` | 当前用户名 | `create_by = #{username}` |
|
||||
| `#{deptId}` | 当前部门ID | `dept_id = #{deptId}` |
|
||||
| `#{companyId}` | 当前公司ID | `company_id = #{companyId}` |
|
||||
| `#{tenantId}` | 当前租户ID | `tenant_id = #{tenantId}` |
|
||||
| `#{deptIds}` | 用户所有部门ID | `dept_id IN (#{deptIds})` |
|
||||
| `#{companyIds}` | 用户所有公司ID | `company_id IN (#{companyIds})` |
|
||||
| `#{postIds}` | 用户所有岗位ID | `post_id IN (#{postIds})` |
|
||||
|
||||
#### SQL_RULE 类型详解
|
||||
|
||||
`SQL_RULE` 是一种特殊的规则类型,允许你直接编写自定义 SQL 表达式,实现更复杂的数据过滤逻辑。
|
||||
|
||||
**什么时候使用 SQL_RULE?**
|
||||
|
||||
- ✅ 需要 `OR` 逻辑连接多个条件
|
||||
- ✅ 需要组合多个字段的复杂判断
|
||||
- ✅ 需要嵌套条件或括号分组
|
||||
- ✅ 标准规则条件无法满足业务需求
|
||||
|
||||
**配置方法**
|
||||
|
||||
| 字段 | 配置值 |
|
||||
|------|--------|
|
||||
| 规则条件 | 选择 `SQL_RULE` |
|
||||
| 规则值 | 直接填写 SQL WHERE 条件表达式 |
|
||||
| 规则字段 | 可以留空(不使用) |
|
||||
|
||||
**配置示例**
|
||||
|
||||
1. **OR 逻辑 - 查看自己创建的或自己部门的数据**
|
||||
|
||||
```
|
||||
规则条件:SQL_RULE
|
||||
规则值:(creator = #{userId} OR dept_id = #{deptId})
|
||||
```
|
||||
|
||||
生成的 SQL:
|
||||
```sql
|
||||
WHERE (creator = '123' OR dept_id = '456')
|
||||
```
|
||||
|
||||
2. **多字段组合 - 特定部门的已启用数据**
|
||||
|
||||
```
|
||||
规则条件:SQL_RULE
|
||||
规则值:(dept_id IN (#{deptIds}) AND status = 1)
|
||||
```
|
||||
|
||||
生成的 SQL:
|
||||
```sql
|
||||
WHERE (dept_id IN ('100', '101', '102') AND status = 1)
|
||||
```
|
||||
|
||||
3. **复杂嵌套条件 - 管理员或本部门负责人**
|
||||
|
||||
```
|
||||
规则条件:SQL_RULE
|
||||
规则值:(role_type = 'admin' OR (dept_id = #{deptId} AND is_leader = 1))
|
||||
```
|
||||
|
||||
生成的 SQL:
|
||||
```sql
|
||||
WHERE (role_type = 'admin' OR (dept_id = '456' AND is_leader = 1))
|
||||
```
|
||||
|
||||
4. **时间范围 + 状态过滤**
|
||||
|
||||
```
|
||||
规则条件:SQL_RULE
|
||||
规则值:(create_time >= '2024-01-01' AND status IN (1, 2))
|
||||
```
|
||||
|
||||
生成的 SQL:
|
||||
```sql
|
||||
WHERE (create_time >= '2024-01-01' AND status IN (1, 2))
|
||||
```
|
||||
|
||||
**工作原理**
|
||||
|
||||
当规则条件为 `SQL_RULE` 时:
|
||||
|
||||
1. 系统会忽略"规则字段"和"规则条件"
|
||||
2. 直接使用"规则值"中的 SQL 表达式
|
||||
3. 先替换表达式中的变量(如 `#{userId}`)
|
||||
4. 将替换后的表达式直接添加到 SQL WHERE 子句中
|
||||
|
||||
代码实现(MenuDataPermissionRule.java:64-67):
|
||||
```java
|
||||
// 处理 SQL_RULE 类型(自定义 SQL)
|
||||
if ("SQL_RULE".equals(ruleConditions)) {
|
||||
return actualValue; // 直接返回替换变量后的 SQL 表达式
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ 重要警告**
|
||||
|
||||
1. **SQL 注入风险**
|
||||
- SQL_RULE 直接拼接到 SQL 中,存在注入风险
|
||||
- ✅ **安全做法**:只使用预定义变量(`#{userId}` 等)
|
||||
- ❌ **危险做法**:不要在规则值中拼接用户输入的内容
|
||||
|
||||
2. **字段名必须正确**
|
||||
- SQL_RULE 中的字段名必须与数据库表字段完全一致
|
||||
- 错误的字段名会导致 SQL 查询报错
|
||||
- 建议先在数据库中测试 SQL 语句
|
||||
|
||||
3. **括号很重要**
|
||||
- 建议始终用括号包裹整个表达式:`(condition1 OR condition2)`
|
||||
- 避免与其他规则或系统条件产生优先级问题
|
||||
|
||||
4. **变量替换**
|
||||
- 变量会被替换为带引号的字符串值
|
||||
- 例如:`#{userId}` → `'123'`
|
||||
- 数据库会自动处理类型转换
|
||||
|
||||
**SQL_RULE vs 普通规则对比**
|
||||
|
||||
| 特性 | 普通规则 | SQL_RULE |
|
||||
|------|---------|----------|
|
||||
| 配置难度 | 简单,选择即可 | 需要 SQL 知识 |
|
||||
| 灵活性 | 有限,单一条件 | 非常灵活,任意表达式 |
|
||||
| 安全性 | 高,系统控制 | 需要注意 SQL 注入 |
|
||||
| OR 逻辑 | ❌ 不支持 | ✅ 支持 |
|
||||
| 嵌套条件 | ❌ 不支持 | ✅ 支持 |
|
||||
| 多规则组合 | AND 连接 | 单个规则内实现 |
|
||||
| 错误提示 | 友好 | SQL 错误信息 |
|
||||
|
||||
**最佳实践**
|
||||
|
||||
1. **优先使用普通规则**:能用普通规则解决的,不要用 SQL_RULE
|
||||
2. **测试后再上线**:在测试环境验证 SQL 语句正确性
|
||||
3. **添加注释**:在规则名称中说明 SQL_RULE 的用途
|
||||
4. **定期审查**:定期检查 SQL_RULE 规则,删除不再使用的
|
||||
5. **权限控制**:限制能配置 SQL_RULE 的管理员权限
|
||||
|
||||
### 步骤3:关联角色
|
||||
|
||||
1. 配置完规则后,进入 **系统管理 > 角色管理**
|
||||
2. 编辑需要应用规则的角色
|
||||
3. 在 **"数据权限"** 标签页中,勾选对应的菜单数据规则
|
||||
4. 保存角色配置
|
||||
|
||||
### 步骤4:测试验证
|
||||
|
||||
1. 使用该角色的用户登录系统
|
||||
2. 访问配置了数据规则的页面
|
||||
3. 验证数据是否按规则过滤
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
### ⚠️ 重要提醒
|
||||
|
||||
#### 1. 规则字段必须与数据库表字段一致
|
||||
|
||||
**错误示例**:
|
||||
```
|
||||
规则字段:dept_id_xxx
|
||||
数据库字段:dept_id
|
||||
结果:SQL 查询报错!
|
||||
```
|
||||
|
||||
**正确做法**:
|
||||
- 在配置规则前,先确认数据库表结构
|
||||
- 字段名必须完全一致(包括大小写)
|
||||
- 前端已添加警告提示,请仔细阅读
|
||||
|
||||
#### 2. 只在菜单/页面级别配置规则
|
||||
|
||||
- ✅ **菜单/页面**(type=2):需要配置数据规则
|
||||
- ❌ **目录**(type=1):不需要配置
|
||||
- ❌ **按钮**(type=3):不需要配置
|
||||
|
||||
前端已自动隐藏目录和按钮的"数据规则"按钮。
|
||||
|
||||
#### 3. 多个规则使用 AND 连接
|
||||
|
||||
如果为同一个菜单配置了多个规则,它们会用 `AND` 连接:
|
||||
|
||||
```sql
|
||||
-- 规则1:creator = #{userId}
|
||||
-- 规则2:status = 1
|
||||
-- 最终SQL:
|
||||
WHERE creator = '当前用户ID' AND status = 1
|
||||
```
|
||||
|
||||
#### 4. 变量不存在时的处理
|
||||
|
||||
如果配置了不存在的变量(如 `#{unknownVar}`),系统会:
|
||||
- 记录警告日志
|
||||
- 将变量替换为空字符串
|
||||
- 可能导致查询结果为空
|
||||
|
||||
**建议**:使用前端下拉框选择变量,避免手动输入错误。
|
||||
|
||||
#### 5. 性能考虑
|
||||
|
||||
- 数据规则在 SQL 层面过滤,性能较好
|
||||
- 但过多的规则会增加 SQL 复杂度
|
||||
- 建议每个菜单不超过 5 条规则
|
||||
|
||||
#### 6. 禁用数据权限的场景
|
||||
|
||||
某些查询不应该受数据权限限制,需要添加 `@DataPermission(enable = false)`:
|
||||
|
||||
```java
|
||||
// 示例:查询所有根级部门(不受数据权限限制)
|
||||
@Override
|
||||
@DataPermission(enable = false)
|
||||
public List<DeptDO> getTopLevelDeptList() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 完整示例
|
||||
|
||||
### 场景:角色管理 - 只看自己创建的角色
|
||||
|
||||
#### 1. 后端代码
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/system/role")
|
||||
public class RoleController {
|
||||
|
||||
@Resource
|
||||
private RoleService roleService;
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得角色分页")
|
||||
@PreAuthorize("@ss.hasPermission('system:role:query')")
|
||||
@PermissionData(pageComponent = "system/role/index") // 添加数据权限注解
|
||||
public CommonResult<PageResult<RoleRespVO>> getRolePage(RolePageReqVO pageReqVO) {
|
||||
PageResult<RoleDO> pageResult = roleService.getRolePage(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, RoleRespVO.class));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 菜单配置
|
||||
|
||||
确保 `system_menu` 表中角色管理菜单的配置:
|
||||
|
||||
```sql
|
||||
SELECT id, name, component
|
||||
FROM system_menu
|
||||
WHERE name = '角色管理';
|
||||
|
||||
-- 结果:
|
||||
-- id: 101
|
||||
-- name: 角色管理
|
||||
-- component: system/role/index ✅ 与代码中的 pageComponent 一致
|
||||
```
|
||||
|
||||
#### 3. 数据规则配置
|
||||
|
||||
在页面上配置规则:
|
||||
|
||||
| 字段 | 值 |
|
||||
|------|------|
|
||||
| 规则名称 | 只看自己创建的角色 |
|
||||
| 规则字段 | `creator` |
|
||||
| 规则条件 | `=` |
|
||||
| 规则值 | `#{userId}` |
|
||||
| 状态 | 启用 |
|
||||
| 排序 | 0 |
|
||||
|
||||
#### 4. 角色关联
|
||||
|
||||
1. 进入角色管理,编辑"普通用户"角色
|
||||
2. 在"数据权限"标签页,勾选"只看自己创建的角色"规则
|
||||
3. 保存
|
||||
|
||||
#### 5. 生成的 SQL
|
||||
|
||||
当普通用户(ID=123)查询角色列表时,实际执行的 SQL:
|
||||
|
||||
```sql
|
||||
SELECT * FROM system_role
|
||||
WHERE deleted = 0
|
||||
AND tenant_id = 1
|
||||
AND creator = '123' -- 自动添加的数据权限条件
|
||||
ORDER BY sort ASC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 为什么配置了规则但不生效?
|
||||
|
||||
**可能原因**:
|
||||
|
||||
1. ✅ 检查 Controller 方法是否添加了 `@PermissionData` 注解
|
||||
2. ✅ 检查 `pageComponent` 是否与菜单的 `component` 字段一致
|
||||
3. ✅ 检查规则是否启用(状态=启用)
|
||||
4. ✅ 检查角色是否关联了该规则
|
||||
5. ✅ 检查用户是否拥有该角色
|
||||
|
||||
### Q2: 如何查看实际执行的 SQL?
|
||||
|
||||
在 `application.yaml` 中开启 SQL 日志:
|
||||
|
||||
```yaml
|
||||
# 方式1:MyBatis Plus SQL 日志
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
|
||||
# 方式2:Logback 日志
|
||||
logging:
|
||||
level:
|
||||
com.zt.plat.module.system.dal.mysql: debug
|
||||
```
|
||||
|
||||
### Q3: 多个规则如何组合?
|
||||
|
||||
多个规则使用 `AND` 连接,如果需要 `OR` 逻辑,使用 `SQL_RULE` 类型:
|
||||
|
||||
```
|
||||
规则条件:SQL_RULE
|
||||
规则值:(dept_id = #{deptId} OR creator = #{userId})
|
||||
```
|
||||
|
||||
### Q4: 如何配置"查看本部门及下级部门"的规则?
|
||||
|
||||
这需要在业务层实现,菜单数据权限只支持简单的字段过滤。建议使用原有的部门数据权限功能。
|
||||
|
||||
### Q5: 规则字段配置错误会怎样?
|
||||
|
||||
会导致 SQL 查询报错,因为数据库找不到该字段。前端已添加警告提示,请仔细检查。
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
菜单数据权限提供了一种灵活、高效的数据过滤机制:
|
||||
|
||||
✅ **开发简单**:只需添加一个注解
|
||||
✅ **配置灵活**:通过页面配置,无需修改代码
|
||||
✅ **性能高效**:SQL 层面过滤,不影响性能
|
||||
✅ **易于维护**:规则集中管理,便于调整
|
||||
|
||||
**最佳实践**:
|
||||
1. 优先使用预定义变量,避免手动输入
|
||||
2. 规则字段必须与数据库表字段一致
|
||||
3. 合理使用规则,避免过度复杂
|
||||
4. 定期review规则配置,删除无用规则
|
||||
|
||||
---
|
||||
|
||||
## 技术支持
|
||||
|
||||
如有问题,请联系:
|
||||
- 开发团队:ZT
|
||||
- 文档版本:v1.0
|
||||
- 更新日期:2026-01-27
|
||||
152
docs/附件上传注解使用说明.md
Normal file
152
docs/附件上传注解使用说明.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# @FileUploadController 注解使用说明
|
||||
|
||||
本文档说明 `@FileUploadController` 的使用方式、调用流程、以及报文结构示例。适用范围为业务创建接口在请求体中携带附件列表的场景,并在业务创建成功后自动建立“业务-附件”的关联关系。
|
||||
|
||||
## 1. 适用前提
|
||||
|
||||
- **附件列表在业务创建请求体中传入**。
|
||||
- **业务创建返回结构统一为**:`{ code, data: { id, code } }`。
|
||||
- 不涉及二次请求与补充请求头流程(文档不包含相关内容)。
|
||||
|
||||
## 2. 注解字段说明
|
||||
|
||||
位置:`com.zt.plat.framework.business.annotation.FileUploadController`
|
||||
|
||||
| 字段 | 说明 | 默认值 | 备注 |
|
||||
|---|---|---|---|
|
||||
| `filesKey` | 请求体中附件数组的路径 | `files` | 支持多层级路径,如 `data.files` |
|
||||
| `fileNameKey` | 附件名称字段 | `name` | 解析附件名称使用 |
|
||||
| `fileIdKey` | 附件ID字段 | `id` | 解析附件ID使用 |
|
||||
| `source` | 业务来源标识 | `default` | 例如 `bpm` / `oa` / `hr` 等 |
|
||||
| `primaryKey` | 响应中业务主键路径 | `data.id` | 支持多层级路径 |
|
||||
| `codeKey` | 响应中业务编码路径 | `data.code` | 支持多层级路径 |
|
||||
|
||||
## 3. 关联处理机制(简述)
|
||||
|
||||
当 Controller 标注 `@FileUploadController` 后:
|
||||
|
||||
1. 请求进入时会读取注解配置并写入 request attribute。
|
||||
2. 请求处理完成后,过滤器会读取:
|
||||
- 请求体中的附件数组
|
||||
- 响应体中的业务主键与业务编码
|
||||
3. 当响应 `code == 0` 时,自动调用 `BusinessFileApi.batchCreateBusinessFile(...)` 建立附件关联。
|
||||
|
||||
> 注意:如果请求体中未包含附件数组,或响应中未返回业务主键,则不会产生关联。
|
||||
|
||||
## 4. 标准调用流程
|
||||
|
||||
### 步骤 1:业务 Controller 标注注解
|
||||
|
||||
```java
|
||||
@FileUploadController(source = "template.contract")
|
||||
public class DemoContractController extends AbstractFileUploadController implements BusinessControllerMarker {
|
||||
static {
|
||||
FileUploadController annotation = DemoContractController.class.getAnnotation(FileUploadController.class);
|
||||
if (annotation != null) {
|
||||
setFileUploadInfo(annotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 2:可选获取 source 信息
|
||||
|
||||
请求:
|
||||
|
||||
```
|
||||
GET /template/demo-contract/upload-info
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": { "source": "template.contract" },
|
||||
"msg": "成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 3:业务创建请求携带附件列表
|
||||
|
||||
请求体示例(默认字段):
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "测试合同",
|
||||
"amount": 2000,
|
||||
"files": [
|
||||
{ "id": 10125, "name": "合同附件.pdf" },
|
||||
{ "id": 10126, "name": "补充材料.docx" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 4:业务创建成功返回
|
||||
|
||||
响应体示例(统一结构):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": 90001,
|
||||
"code": "HT-2026-0001"
|
||||
},
|
||||
"msg": "成功"
|
||||
}
|
||||
```
|
||||
|
||||
系统会自动读取:
|
||||
|
||||
- `files` 中的 `id/name`
|
||||
- `data.id` 和 `data.code`
|
||||
|
||||
并建立业务附件关联。
|
||||
|
||||
## 5. 多层级字段示例(可选配置)
|
||||
|
||||
如果业务请求体与响应体存在嵌套结构,可通过注解自定义路径:
|
||||
|
||||
```java
|
||||
@FileUploadController(
|
||||
source = "template.contract",
|
||||
filesKey = "data.files",
|
||||
fileIdKey = "fileId",
|
||||
fileNameKey = "fileName",
|
||||
primaryKey = "data.businessId",
|
||||
codeKey = "data.businessCode"
|
||||
)
|
||||
```
|
||||
|
||||
对应请求体示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"files": [
|
||||
{ "fileId": 1001, "fileName": "合同.pdf" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
对应响应体示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"businessId": 90001,
|
||||
"businessCode": "HT-2026-0001"
|
||||
},
|
||||
"msg": "成功"
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 常见注意事项
|
||||
|
||||
- 响应 `code != 0` 时不会执行附件关联。
|
||||
- `files` 必须是数组,否则会被忽略。
|
||||
- 若业务返回未包含 `primaryKey` 对应的字段,附件不会关联。
|
||||
- `source` 建议以业务模块唯一标识命名,便于后续查询与归档。
|
||||
23
pom.xml
23
pom.xml
@@ -4,28 +4,19 @@
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt</artifactId>
|
||||
<artifactId>zt-dsc</artifactId>
|
||||
<version>${revision}</version>
|
||||
<packaging>pom</packaging>
|
||||
<modules>
|
||||
<module>zt-dependencies</module>
|
||||
<module>zt-gateway</module>
|
||||
<module>zt-framework</module>
|
||||
<!-- Server 主项目 -->
|
||||
<!-- <module>zt-server</module>-->
|
||||
<!-- 各种 module 拓展 -->
|
||||
<module>zt-framework-dsc</module>
|
||||
<module>zt-module-system</module>
|
||||
<module>zt-module-infra</module>
|
||||
<!-- <module>zt-module-bpm</module>-->
|
||||
<module>zt-module-bpm</module>
|
||||
<module>zt-module-report</module>
|
||||
<!--<module>zt-module-mp</module>-->
|
||||
<!-- <module>zt-module-ai</module>-->
|
||||
<module>zt-module-template</module>
|
||||
<!-- <module>zt-module-iot</module>-->
|
||||
<module>zt-module-databus</module>
|
||||
<!-- <module>zt-module-rule</module>-->
|
||||
<!-- <module>zt-module-html2pdf</module>-->
|
||||
<!-- <module>zt-server</module>-->
|
||||
<module>zt-server</module>
|
||||
</modules>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
@@ -57,6 +48,12 @@
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-common-dsc</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
73
sql/dm/20260126菜单数据规则表.sql
Normal file
73
sql/dm/20260126菜单数据规则表.sql
Normal file
@@ -0,0 +1,73 @@
|
||||
-- ----------------------------
|
||||
-- Table structure for system_menu_data_rule
|
||||
-- ----------------------------
|
||||
CREATE TABLE system_menu_data_rule (
|
||||
id bigint NOT NULL PRIMARY KEY,
|
||||
menu_id bigint NOT NULL,
|
||||
rule_name varchar(100) NOT NULL,
|
||||
rule_column varchar(100) DEFAULT NULL NULL,
|
||||
rule_conditions varchar(20) NOT NULL,
|
||||
rule_value varchar(500) NOT NULL,
|
||||
status smallint DEFAULT 1 NOT NULL,
|
||||
sort int DEFAULT 0 NOT NULL,
|
||||
remark varchar(500) DEFAULT NULL NULL,
|
||||
creator varchar(64) DEFAULT '' NULL,
|
||||
create_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updater varchar(64) DEFAULT '' NULL,
|
||||
update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
deleted bit DEFAULT '0' NOT NULL,
|
||||
tenant_id bigint DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
-- CREATE INDEX idx_menu_data_rule_menu ON system_menu_data_rule (menu_id);
|
||||
-- CREATE INDEX idx_menu_data_rule_tenant ON system_menu_data_rule (tenant_id);
|
||||
|
||||
COMMENT ON COLUMN system_menu_data_rule.id IS '规则ID';
|
||||
COMMENT ON COLUMN system_menu_data_rule.menu_id IS '菜单ID';
|
||||
COMMENT ON COLUMN system_menu_data_rule.rule_name IS '规则名称';
|
||||
COMMENT ON COLUMN system_menu_data_rule.rule_column IS '规则字段(数据库列名)';
|
||||
COMMENT ON COLUMN system_menu_data_rule.rule_conditions IS '规则条件(=、>、<、IN、LIKE等)';
|
||||
COMMENT ON COLUMN system_menu_data_rule.rule_value IS '规则值(支持变量如#{userId}、#{deptId})';
|
||||
COMMENT ON COLUMN system_menu_data_rule.status IS '状态(0=禁用 1=启用)';
|
||||
COMMENT ON COLUMN system_menu_data_rule.sort IS '排序';
|
||||
COMMENT ON COLUMN system_menu_data_rule.remark IS '备注';
|
||||
COMMENT ON COLUMN system_menu_data_rule.creator IS '创建者';
|
||||
COMMENT ON COLUMN system_menu_data_rule.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN system_menu_data_rule.updater IS '更新者';
|
||||
COMMENT ON COLUMN system_menu_data_rule.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN system_menu_data_rule.deleted IS '是否删除';
|
||||
COMMENT ON COLUMN system_menu_data_rule.tenant_id IS '租户编号';
|
||||
COMMENT ON TABLE system_menu_data_rule IS '菜单数据规则表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for system_role_menu_data_rule
|
||||
-- ----------------------------
|
||||
CREATE TABLE system_role_menu_data_rule (
|
||||
id bigint NOT NULL PRIMARY KEY,
|
||||
role_id bigint NOT NULL,
|
||||
menu_id bigint NOT NULL,
|
||||
data_rule_id bigint NOT NULL,
|
||||
creator varchar(64) DEFAULT '' NULL,
|
||||
create_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updater varchar(64) DEFAULT '' NULL,
|
||||
update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
deleted bit DEFAULT '0' NOT NULL,
|
||||
tenant_id bigint DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
-- CREATE INDEX idx_rmdr_role ON system_role_menu_data_rule (role_id);
|
||||
-- CREATE INDEX idx_rmdr_menu ON system_role_menu_data_rule (menu_id);
|
||||
-- CREATE INDEX idx_rmdr_tenant ON system_role_menu_data_rule (tenant_id);
|
||||
-- CREATE INDEX idx_rmdr_role_menu_rule ON system_role_menu_data_rule (role_id, menu_id, data_rule_id);
|
||||
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.id IS '自增主键';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.role_id IS '角色ID';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.menu_id IS '菜单ID';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.data_rule_id IS '数据规则ID';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.creator IS '创建者';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.updater IS '更新者';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.deleted IS '是否删除';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.tenant_id IS '租户编号';
|
||||
COMMENT ON TABLE system_role_menu_data_rule IS '角色菜单数据规则关联表';
|
||||
53
sql/dm/iwork_workflow_log_20260130.sql
Normal file
53
sql/dm/iwork_workflow_log_20260130.sql
Normal file
@@ -0,0 +1,53 @@
|
||||
-- iWork 流程日志表(达梦数据库)
|
||||
-- 合并了流程创建日志和回调日志
|
||||
CREATE TABLE system_iwork_workflow_log (
|
||||
id BIGINT NOT NULL,
|
||||
request_id VARCHAR(128) NOT NULL,
|
||||
workflow_id BIGINT,
|
||||
business_code VARCHAR(128),
|
||||
biz_callback_key VARCHAR(255),
|
||||
raw_request VARCHAR(2000),
|
||||
status VARCHAR(32),
|
||||
callback_status INTEGER,
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
max_retry INTEGER,
|
||||
last_error_message VARCHAR(512),
|
||||
raw_callback VARCHAR(2000),
|
||||
last_callback_time TIMESTAMP,
|
||||
creator VARCHAR(64) DEFAULT '',
|
||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updater VARCHAR(64) DEFAULT '',
|
||||
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted SMALLINT NOT NULL DEFAULT 0,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
-- 添加注释
|
||||
COMMENT ON TABLE system_iwork_workflow_log IS 'iWork 流程日志';
|
||||
COMMENT ON COLUMN system_iwork_workflow_log.id IS '主键';
|
||||
COMMENT ON COLUMN system_iwork_workflow_log.request_id IS 'iWork 请求编号';
|
||||
COMMENT ON COLUMN system_iwork_workflow_log.workflow_id IS '流程模板 ID';
|
||||
COMMENT ON COLUMN system_iwork_workflow_log.business_code IS '业务编码';
|
||||
COMMENT ON COLUMN system_iwork_workflow_log.biz_callback_key IS '业务回调标识';
|
||||
COMMENT ON COLUMN system_iwork_workflow_log.raw_request IS '创建请求原文';
|
||||
COMMENT ON COLUMN system_iwork_workflow_log.status IS '流程状态';
|
||||
COMMENT ON COLUMN system_iwork_workflow_log.callback_status IS '回调处理状态';
|
||||
COMMENT ON COLUMN system_iwork_workflow_log.retry_count IS '已重试次数';
|
||||
COMMENT ON COLUMN system_iwork_workflow_log.max_retry IS '最大重试次数';
|
||||
COMMENT ON COLUMN system_iwork_workflow_log.last_error_message IS '最后错误信息';
|
||||
COMMENT ON COLUMN system_iwork_workflow_log.raw_callback IS '回调原文';
|
||||
COMMENT ON COLUMN system_iwork_workflow_log.last_callback_time IS '最近回调时间';
|
||||
COMMENT ON COLUMN system_iwork_workflow_log.creator IS '创建者';
|
||||
COMMENT ON COLUMN system_iwork_workflow_log.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN system_iwork_workflow_log.updater IS '更新者';
|
||||
COMMENT ON COLUMN system_iwork_workflow_log.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN system_iwork_workflow_log.deleted IS '是否删除';
|
||||
COMMENT ON COLUMN system_iwork_workflow_log.tenant_id IS '租户编号';
|
||||
|
||||
-- 创建唯一索引
|
||||
CREATE UNIQUE INDEX uk_iwork_workflow_log_request_id ON system_iwork_workflow_log(request_id);
|
||||
|
||||
-- 创建普通索引
|
||||
-- CREATE INDEX idx_iwork_workflow_log_business_code ON system_iwork_workflow_log(business_code);
|
||||
-- CREATE INDEX idx_iwork_workflow_log_biz_callback_key ON system_iwork_workflow_log(biz_callback_key);
|
||||
@@ -340,7 +340,8 @@ CREATE TABLE infra_file (
|
||||
updater varchar(64) DEFAULT '' NULL,
|
||||
update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
deleted bit DEFAULT '0' NOT NULL,
|
||||
DOWNLOAD_COUNT INT DEFAULT 0 NOT NULL
|
||||
DOWNLOAD_COUNT INT DEFAULT 0 NOT NULL,
|
||||
DOWNLOADABLE SMALLINT DEFAULT 1 NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN infra_file.id IS '文件编号';
|
||||
@@ -358,6 +359,7 @@ COMMENT ON COLUMN infra_file.updater IS '更新者';
|
||||
COMMENT ON COLUMN infra_file.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN infra_file.deleted IS '是否删除';
|
||||
COMMENT ON COLUMN INFRA_FILE.DOWNLOAD_COUNT IS '下载次数';
|
||||
COMMENT ON COLUMN INFRA_FILE.DOWNLOADABLE IS '是否可下载(1是,0否)';
|
||||
COMMENT ON TABLE infra_file IS '文件表';
|
||||
|
||||
CREATE INDEX idx_infra_file_hash ON infra_file(hash);
|
||||
@@ -4313,3 +4315,74 @@ VALUES
|
||||
(5022, 2, '日期格式', 'DATE', 'system_sequence_detail_rule_type', 0, 'success', '', '日期格式规则', 'admin', SYSDATE, 'admin', SYSDATE, 0),
|
||||
(5023, 3, '数字格式', 'NUMBER', 'system_sequence_detail_rule_type', 0, 'info', '', '数字格式规则', 'admin', SYSDATE, 'admin', SYSDATE, 0),
|
||||
(5024, 4, '自定义格式', 'CUSTOM', 'system_sequence_detail_rule_type', 0, 'warning', '', '自定义格式规则', 'admin', SYSDATE, 'admin', SYSDATE, 0);
|
||||
|
||||
/*
|
||||
增加菜单规则(system_menu_data_rule)和规则角色关联表(system_role_menu_data_rule),同增量脚本:sql/dm/20260126菜单数据规则表.sql
|
||||
*/
|
||||
-- ----------------------------
|
||||
-- Table structure for system_menu_data_rule
|
||||
-- ----------------------------
|
||||
CREATE TABLE system_menu_data_rule (
|
||||
id bigint NOT NULL PRIMARY KEY,
|
||||
menu_id bigint NOT NULL,
|
||||
rule_name varchar(100) NOT NULL,
|
||||
rule_column varchar(100) DEFAULT NULL NULL,
|
||||
rule_conditions varchar(20) NOT NULL,
|
||||
rule_value varchar(500) NOT NULL,
|
||||
status smallint DEFAULT 1 NOT NULL,
|
||||
sort int DEFAULT 0 NOT NULL,
|
||||
remark varchar(500) DEFAULT NULL NULL,
|
||||
creator varchar(64) DEFAULT '' NULL,
|
||||
create_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updater varchar(64) DEFAULT '' NULL,
|
||||
update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
deleted bit DEFAULT '0' NOT NULL,
|
||||
tenant_id bigint DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
|
||||
COMMENT ON COLUMN system_menu_data_rule.id IS '规则ID';
|
||||
COMMENT ON COLUMN system_menu_data_rule.menu_id IS '菜单ID';
|
||||
COMMENT ON COLUMN system_menu_data_rule.rule_name IS '规则名称';
|
||||
COMMENT ON COLUMN system_menu_data_rule.rule_column IS '规则字段(数据库列名)';
|
||||
COMMENT ON COLUMN system_menu_data_rule.rule_conditions IS '规则条件(=、>、<、IN、LIKE等)';
|
||||
COMMENT ON COLUMN system_menu_data_rule.rule_value IS '规则值(支持变量如#{userId}、#{deptId})';
|
||||
COMMENT ON COLUMN system_menu_data_rule.status IS '状态(0=禁用 1=启用)';
|
||||
COMMENT ON COLUMN system_menu_data_rule.sort IS '排序';
|
||||
COMMENT ON COLUMN system_menu_data_rule.remark IS '备注';
|
||||
COMMENT ON COLUMN system_menu_data_rule.creator IS '创建者';
|
||||
COMMENT ON COLUMN system_menu_data_rule.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN system_menu_data_rule.updater IS '更新者';
|
||||
COMMENT ON COLUMN system_menu_data_rule.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN system_menu_data_rule.deleted IS '是否删除';
|
||||
COMMENT ON COLUMN system_menu_data_rule.tenant_id IS '租户编号';
|
||||
COMMENT ON TABLE system_menu_data_rule IS '菜单数据规则表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for system_role_menu_data_rule
|
||||
-- ----------------------------
|
||||
CREATE TABLE system_role_menu_data_rule (
|
||||
id bigint NOT NULL PRIMARY KEY,
|
||||
role_id bigint NOT NULL,
|
||||
menu_id bigint NOT NULL,
|
||||
data_rule_id bigint NOT NULL,
|
||||
creator varchar(64) DEFAULT '' NULL,
|
||||
create_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updater varchar(64) DEFAULT '' NULL,
|
||||
update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
deleted bit DEFAULT '0' NOT NULL,
|
||||
tenant_id bigint DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.id IS '自增主键';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.role_id IS '角色ID';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.menu_id IS '菜单ID';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.data_rule_id IS '数据规则ID';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.creator IS '创建者';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.updater IS '更新者';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.deleted IS '是否删除';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.tenant_id IS '租户编号';
|
||||
COMMENT ON TABLE system_role_menu_data_rule IS '角色菜单数据规则关联表';
|
||||
|
||||
132
sql/dm/外部系统推送配置初始化_DM8_20260120.sql
Normal file
132
sql/dm/外部系统推送配置初始化_DM8_20260120.sql
Normal file
@@ -0,0 +1,132 @@
|
||||
-- DM8 外部系统推送配置初始化脚本
|
||||
-- 用于配置不同公司/部门/业务类型下的外部系统推送开关
|
||||
-- 创建日期:2026-01-20
|
||||
|
||||
-- 重复执行时请先备份数据
|
||||
-- DROP TABLE IF EXISTS system_external_push_config;
|
||||
|
||||
-- 创建表
|
||||
CREATE TABLE system_external_push_config (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
company_id BIGINT DEFAULT NULL NULL,
|
||||
dept_id BIGINT DEFAULT NULL NULL,
|
||||
business_type VARCHAR(32) DEFAULT NULL NULL,
|
||||
external_system VARCHAR(64) DEFAULT NULL NULL,
|
||||
enable_push BIT DEFAULT '1' NOT NULL,
|
||||
remark VARCHAR(512) DEFAULT NULL NULL,
|
||||
tenant_id BIGINT DEFAULT 0 NOT NULL,
|
||||
creator VARCHAR(64) DEFAULT '' NULL,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updater VARCHAR(64) DEFAULT '' NULL,
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
deleted TINYINT DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
-- 表和字段注释
|
||||
COMMENT ON TABLE system_external_push_config IS '外部系统推送配置';
|
||||
COMMENT ON COLUMN system_external_push_config.id IS '主键编号';
|
||||
COMMENT ON COLUMN system_external_push_config.company_id IS '公司编号(关联 system_dept 表的 is_company=1 的记录,为空表示不限制公司)';
|
||||
COMMENT ON COLUMN system_external_push_config.dept_id IS '部门编号(关联 system_dept 表的 is_company=0 的记录,为空表示公司级配置)';
|
||||
COMMENT ON COLUMN system_external_push_config.business_type IS '业务类型(PURCHASE/SALE/PRODUCTION,为空表示所有业务类型)';
|
||||
COMMENT ON COLUMN system_external_push_config.external_system IS '外部系统标识(ERP/IWORK/等,为空表示所有外部系统)';
|
||||
COMMENT ON COLUMN system_external_push_config.enable_push IS '是否启用推送(1启用 0停用)';
|
||||
COMMENT ON COLUMN system_external_push_config.remark IS '备注';
|
||||
COMMENT ON COLUMN system_external_push_config.tenant_id IS '租户编号';
|
||||
COMMENT ON COLUMN system_external_push_config.creator IS '创建者';
|
||||
COMMENT ON COLUMN system_external_push_config.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN system_external_push_config.updater IS '更新者';
|
||||
COMMENT ON COLUMN system_external_push_config.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN system_external_push_config.deleted IS '删除标记(0未删除 1已删除)';
|
||||
|
||||
/*-- 唯一索引:租户+公司+部门+业务类型+外部系统的组合唯一
|
||||
-- 注意:因为 dept_id 和 external_system 可以为 NULL,使用 COALESCE 处理
|
||||
CREATE UNIQUE INDEX uk_external_push_config_unique
|
||||
ON system_external_push_config (tenant_id, company_id, COALESCE(dept_id, 0), business_type, COALESCE(external_system, ''));
|
||||
|
||||
-- 辅助索引:按公司查询
|
||||
CREATE INDEX idx_external_push_config_company
|
||||
ON system_external_push_config (tenant_id, company_id);
|
||||
|
||||
-- 辅助索引:按业务类型查询
|
||||
CREATE INDEX idx_external_push_config_biz_type
|
||||
ON system_external_push_config (tenant_id, business_type);
|
||||
|
||||
|
||||
|
||||
-- 初始化菜单权限数据
|
||||
-- 主菜单
|
||||
INSERT INTO system_menu (
|
||||
id, name, permission, type, sort, parent_id,
|
||||
path, icon, component, component_name, status,
|
||||
visible, keep_alive, always_show, creator, create_time,
|
||||
updater, update_time, deleted
|
||||
)
|
||||
SELECT
|
||||
20060, '外部系统推送配置', '', 2, 60, 1,
|
||||
'external-push-config', 'setting', 'system/push/config/index', 'SystemExternalPushConfig', 0,
|
||||
'1', '1', '1', 'admin', SYSDATE,
|
||||
'admin', SYSDATE, 0
|
||||
FROM dual
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM system_menu WHERE id = 20060
|
||||
);
|
||||
|
||||
-- 查询权限
|
||||
INSERT INTO system_menu (
|
||||
id, name, permission, type, sort, parent_id,
|
||||
path, icon, component, status, visible, keep_alive,
|
||||
creator, create_time, updater, update_time, deleted
|
||||
)
|
||||
SELECT
|
||||
2006001, '推送配置查询', 'system:external-push-config:query', 3, 1, 20060,
|
||||
'', '', '', 0, '1', '1',
|
||||
'admin', SYSDATE, 'admin', SYSDATE, 0
|
||||
FROM dual
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM system_menu WHERE id = 2006001
|
||||
);
|
||||
|
||||
-- 创建权限
|
||||
INSERT INTO system_menu (
|
||||
id, name, permission, type, sort, parent_id,
|
||||
path, icon, component, status, visible, keep_alive,
|
||||
creator, create_time, updater, update_time, deleted
|
||||
)
|
||||
SELECT
|
||||
2006002, '推送配置创建', 'system:external-push-config:create', 3, 2, 20060,
|
||||
'', '', '', 0, '1', '1',
|
||||
'admin', SYSDATE, 'admin', SYSDATE, 0
|
||||
FROM dual
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM system_menu WHERE id = 2006002
|
||||
);
|
||||
|
||||
-- 修改权限
|
||||
INSERT INTO system_menu (
|
||||
id, name, permission, type, sort, parent_id,
|
||||
path, icon, component, status, visible, keep_alive,
|
||||
creator, create_time, updater, update_time, deleted
|
||||
)
|
||||
SELECT
|
||||
2006003, '推送配置修改', 'system:external-push-config:update', 3, 3, 20060,
|
||||
'', '', '', 0, '1', '1',
|
||||
'admin', SYSDATE, 'admin', SYSDATE, 0
|
||||
FROM dual
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM system_menu WHERE id = 2006003
|
||||
);
|
||||
|
||||
-- 删除权限
|
||||
INSERT INTO system_menu (
|
||||
id, name, permission, type, sort, parent_id,
|
||||
path, icon, component, status, visible, keep_alive,
|
||||
creator, create_time, updater, update_time, deleted
|
||||
)
|
||||
SELECT
|
||||
2006004, '推送配置删除', 'system:external-push-config:delete', 3, 4, 20060,
|
||||
'', '', '', 0, '1', '1',
|
||||
'admin', SYSDATE, 'admin', SYSDATE, 0
|
||||
FROM dual
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM system_menu WHERE id = 2006004
|
||||
);*/
|
||||
@@ -218,6 +218,7 @@ CREATE TABLE databus_api_client_credential (
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updater VARCHAR(64) DEFAULT '' NOT NULL,
|
||||
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
enable_encryption BIT DEFAULT '1' NOT NULL,
|
||||
deleted BIT DEFAULT '0' NOT NULL
|
||||
);
|
||||
|
||||
|
||||
@@ -1,770 +0,0 @@
|
||||
<?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">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>ZT</id>
|
||||
<name>中铜 ZStack 私服</name>
|
||||
<url>http://172.16.46.63:30708/repository/test/</url>
|
||||
</repository>
|
||||
<snapshotRepository>
|
||||
<id>ZT-snap</id>
|
||||
<name>中铜 ZStack 私服</name>
|
||||
<url>http://172.16.46.63:30708/repository/test-snap/</url>
|
||||
</snapshotRepository>
|
||||
</distributionManagement>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-dependencies</artifactId>
|
||||
<version>${revision}</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>基础 bom 文件,管理整个项目的依赖版本</description>
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<properties>
|
||||
<revision>3.0.47-SNAPSHOT</revision>
|
||||
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
|
||||
<!-- 统一依赖管理 -->
|
||||
<spring.boot.version>3.4.5</spring.boot.version>
|
||||
<spring.cloud.version>2024.0.1</spring.cloud.version>
|
||||
<spring.cloud.alibaba.version>2023.0.3.2</spring.cloud.alibaba.version>
|
||||
<seata.version>2.4.0</seata.version>
|
||||
<!-- Web 相关 -->
|
||||
<springdoc.version>2.8.3</springdoc.version>
|
||||
<knife4j.version>4.6.0</knife4j.version>
|
||||
<!-- DB 相关 -->
|
||||
<druid.version>1.2.24</druid.version>
|
||||
<mybatis.version>3.5.19</mybatis.version>
|
||||
<mybatis-plus.version>3.5.10.1</mybatis-plus.version>
|
||||
<dynamic-datasource.version>4.3.1</dynamic-datasource.version>
|
||||
<mybatis-plus-join.version>1.4.13</mybatis-plus-join.version>
|
||||
<easy-trans.version>3.0.6</easy-trans.version>
|
||||
<redisson.version>3.41.0</redisson.version>
|
||||
<dm8.jdbc.version>8.1.3.140</dm8.jdbc.version>
|
||||
<kingbase.jdbc.version>8.6.0</kingbase.jdbc.version>
|
||||
<opengauss.jdbc.version>5.1.0</opengauss.jdbc.version>
|
||||
<taos.version>3.3.3</taos.version>
|
||||
<!-- 消息队列 -->
|
||||
<rocketmq-spring.version>2.3.2</rocketmq-spring.version>
|
||||
<!-- RPC 相关 -->
|
||||
<!-- Config 配置中心相关 -->
|
||||
<!-- Job 定时任务相关 -->
|
||||
<xxl-job.version>2.4.0</xxl-job.version>
|
||||
<!-- 服务保障相关 -->
|
||||
<lock4j.version>2.2.7</lock4j.version>
|
||||
<!-- 监控相关 -->
|
||||
<skywalking.version>9.5.0</skywalking.version>
|
||||
<spring-boot-admin.version>3.4.5</spring-boot-admin.version>
|
||||
<opentracing.version>0.33.0</opentracing.version>
|
||||
<!-- Test 测试相关 -->
|
||||
<podam.version>8.0.2.RELEASE</podam.version>
|
||||
<jedis-mock.version>1.1.4</jedis-mock.version>
|
||||
<mockito-inline.version>5.2.0</mockito-inline.version>
|
||||
<okhttp3.version>4.12.0</okhttp3.version>
|
||||
<!-- Bpm 工作流相关 -->
|
||||
<flowable.version>7.0.1</flowable.version>
|
||||
<!-- 工具类相关 -->
|
||||
<anji-plus-captcha.version>1.4.0</anji-plus-captcha.version>
|
||||
<jsoup.version>1.18.1</jsoup.version>
|
||||
<lombok.version>1.18.36</lombok.version>
|
||||
<mapstruct.version>1.6.3</mapstruct.version>
|
||||
<hutool-5.version>5.8.35</hutool-5.version>
|
||||
<hutool-6.version>6.0.0-M19</hutool-6.version>
|
||||
<easyexcel.version>4.0.3</easyexcel.version>
|
||||
<velocity.version>2.4.1</velocity.version>
|
||||
<fastjson.version>1.2.83</fastjson.version>
|
||||
<guava.version>33.4.8-jre</guava.version>
|
||||
<transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
|
||||
<commons-net.version>3.11.1</commons-net.version>
|
||||
<jsch.version>0.1.55</jsch.version>
|
||||
<tika-core.version>3.1.0</tika-core.version>
|
||||
<ip2region.version>2.7.0</ip2region.version>
|
||||
<bizlog-sdk.version>3.0.6</bizlog-sdk.version>
|
||||
<reflections.version>0.10.2</reflections.version>
|
||||
<netty.version>4.1.116.Final</netty.version>
|
||||
<mqtt.version>1.2.5</mqtt.version>
|
||||
<pf4j-spring.version>0.9.0</pf4j-spring.version>
|
||||
<okhttp3.version>4.12.0</okhttp3.version>
|
||||
<!-- 规则引擎 -->
|
||||
<liteflow.version>2.15.1</liteflow.version>
|
||||
<vertx.version>4.5.13</vertx.version>
|
||||
<!-- 三方云服务相关 -->
|
||||
<commons-io.version>2.17.0</commons-io.version>
|
||||
<commons-compress.version>1.27.1</commons-compress.version>
|
||||
<awssdk.version>2.30.14</awssdk.version>
|
||||
<justauth.version>1.16.7</justauth.version>
|
||||
<justauth-starter.version>1.4.0</justauth-starter.version>
|
||||
<jimureport.version>1.9.4</jimureport.version>
|
||||
<weixin-java.version>4.7.5.B</weixin-java.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<!-- 统一依赖管理 -->
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-bom</artifactId>
|
||||
<version>${netty.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-dependencies</artifactId>
|
||||
<version>${spring.cloud.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
|
||||
<version>${spring.cloud.alibaba.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 分布式事务 Seata (覆盖 Spring Cloud Alibaba 中的 2.1.0 版本) -->
|
||||
<dependency>
|
||||
<groupId>org.apache.seata</groupId>
|
||||
<artifactId>seata-all</artifactId>
|
||||
<version>${seata.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.seata</groupId>
|
||||
<artifactId>seata-spring-boot-starter</artifactId>
|
||||
<version>${seata.version}</version>
|
||||
</dependency>
|
||||
<!-- Seata 达梦数据库补丁 -->
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-spring-boot-starter-seata-dm</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 业务组件 -->
|
||||
<dependency>
|
||||
<groupId>io.github.mouzt</groupId>
|
||||
<artifactId>bizlog-sdk</artifactId>
|
||||
<version>${bizlog-sdk.version}</version>
|
||||
<exclusions>
|
||||
<exclusion> <!-- 排除掉springboot依赖使用项目的 -->
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-spring-boot-starter-biz-tenant</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-spring-boot-starter-biz-data-permission</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-spring-boot-starter-biz-ip</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring 核心 -->
|
||||
<dependency>
|
||||
<!-- 用于生成自定义的 Spring @ConfigurationProperties 配置类的说明文件 -->
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-spring-boot-starter-env</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-spring-boot-starter-web</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-spring-boot-starter-security</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-spring-boot-starter-websocket</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.xingfudeshi</groupId> <!-- TODO ZT:https://github.com/xiaoymin/knife4j/issues/874 -->
|
||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||
<version>${knife4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
|
||||
<version>${springdoc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId> <!-- 接口文档 UI:knife4j【网关专属】 -->
|
||||
<artifactId>knife4j-gateway-spring-boot-starter</artifactId>
|
||||
<version>4.5.0</version> <!-- TODO ZT:等 4.5.0 => 4.6.0 -->
|
||||
</dependency>
|
||||
|
||||
<!-- DB 相关 -->
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-spring-boot-starter-mybatis</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid-spring-boot-3-starter</artifactId>
|
||||
<version>${druid.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<!-- 注意:必须声明,避免 flowable 和 mybatis-plus 引入的 mybatis 版本不一致!!! -->
|
||||
<groupId>org.mybatis</groupId>
|
||||
<artifactId>mybatis</artifactId>
|
||||
<version>${mybatis.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-jsqlparser</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-generator</artifactId> <!-- 代码生成器,使用它解析表结构 -->
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId> <!-- 多数据源 -->
|
||||
<version>${dynamic-datasource.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.yulichang</groupId>
|
||||
<artifactId>mybatis-plus-join-boot-starter</artifactId> <!-- MyBatis 联表查询 -->
|
||||
<version>${mybatis-plus-join.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fhs-opensource</groupId> <!-- VO 数据翻译 -->
|
||||
<artifactId>easy-trans-spring-boot-starter</artifactId>
|
||||
<version>${easy-trans.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-commons</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fhs-opensource</groupId>
|
||||
<artifactId>easy-trans-mybatis-plus-extend</artifactId>
|
||||
<version>${easy-trans.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fhs-opensource</groupId>
|
||||
<artifactId>easy-trans-anno</artifactId>
|
||||
<version>${easy-trans.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-spring-boot-starter-redis</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson-spring-boot-starter</artifactId>
|
||||
<version>${redisson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.dameng</groupId>
|
||||
<artifactId>DmJdbcDriver18</artifactId>
|
||||
<version>${dm8.jdbc.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.opengauss</groupId>
|
||||
<artifactId>opengauss-jdbc</artifactId>
|
||||
<version>${opengauss.jdbc.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.com.kingbase</groupId>
|
||||
<artifactId>kingbase8</artifactId>
|
||||
<version>${kingbase.jdbc.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.taosdata.jdbc</groupId>
|
||||
<artifactId>taos-jdbcdriver</artifactId>
|
||||
<version>${taos.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- RPC 远程调用相关 -->
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-spring-boot-starter-rpc</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Registry 注册中心相关 -->
|
||||
|
||||
<!-- Config 配置中心相关 -->
|
||||
|
||||
<!-- Job 定时任务相关 -->
|
||||
<dependency>
|
||||
<groupId>com.xuxueli</groupId>
|
||||
<artifactId>xxl-job-core</artifactId>
|
||||
<version>${xxl-job.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-spring-boot-starter-job</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 消息队列相关 -->
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-spring-boot-starter-mq</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.rocketmq</groupId>
|
||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||
<version>${rocketmq-spring.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 服务保障相关 -->
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-spring-boot-starter-protection</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>lock4j-redisson-spring-boot-starter</artifactId>
|
||||
<version>${lock4j.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>redisson-spring-boot-starter</artifactId>
|
||||
<groupId>org.redisson</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- 监控相关 -->
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-spring-boot-starter-monitor</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.skywalking</groupId>
|
||||
<artifactId>apm-toolkit-trace</artifactId>
|
||||
<version>${skywalking.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.skywalking</groupId>
|
||||
<artifactId>apm-toolkit-logback-1.x</artifactId>
|
||||
<version>${skywalking.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.skywalking</groupId>
|
||||
<artifactId>apm-toolkit-opentracing</artifactId>
|
||||
<version>${skywalking.version}</version>
|
||||
<!-- <exclusions>-->
|
||||
<!-- <exclusion>-->
|
||||
<!-- <artifactId>opentracing-api</artifactId>-->
|
||||
<!-- <groupId>io.opentracing</groupId>-->
|
||||
<!-- </exclusion>-->
|
||||
<!-- <exclusion>-->
|
||||
<!-- <artifactId>opentracing-util</artifactId>-->
|
||||
<!-- <groupId>io.opentracing</groupId>-->
|
||||
<!-- </exclusion>-->
|
||||
<!-- </exclusions>-->
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.opentracing</groupId>
|
||||
<artifactId>opentracing-api</artifactId>
|
||||
<version>${opentracing.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.opentracing</groupId>
|
||||
<artifactId>opentracing-util</artifactId>
|
||||
<version>${opentracing.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.opentracing</groupId>
|
||||
<artifactId>opentracing-noop</artifactId>
|
||||
<version>${opentracing.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>de.codecentric</groupId>
|
||||
<artifactId>spring-boot-admin-starter-server</artifactId> <!-- 实现 Spring Boot Admin Server 服务端 -->
|
||||
<version>${spring-boot-admin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.codecentric</groupId>
|
||||
<artifactId>spring-boot-admin-starter-client</artifactId> <!-- 实现 Spring Boot Admin Server 服务端 -->
|
||||
<version>${spring-boot-admin.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Test 测试相关 -->
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-spring-boot-starter-test</artifactId>
|
||||
<version>${revision}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-inline</artifactId>
|
||||
<version>${mockito-inline.version}</version> <!-- 支持 Mockito 的 final 类与 static 方法的 mock -->
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>asm</artifactId>
|
||||
<groupId>org.ow2.asm</groupId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.fppt</groupId> <!-- 单元测试,我们采用内嵌的 Redis 数据库 -->
|
||||
<artifactId>jedis-mock</artifactId>
|
||||
<version>${jedis-mock.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>uk.co.jemos.podam</groupId> <!-- 单元测试,随机生成 POJO 类 -->
|
||||
<artifactId>podam</artifactId>
|
||||
<version>${podam.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<version>${okhttp3.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 工作流相关 -->
|
||||
<dependency>
|
||||
<groupId>org.flowable</groupId>
|
||||
<artifactId>flowable-spring-boot-starter-process</artifactId>
|
||||
<version>${flowable.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flowable</groupId>
|
||||
<artifactId>flowable-spring-boot-starter-actuator</artifactId>
|
||||
<version>${flowable.version}</version>
|
||||
</dependency>
|
||||
<!-- 工作流相关结束 -->
|
||||
|
||||
<!-- 工具类相关 -->
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-common</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-spring-boot-starter-excel</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct</artifactId> <!-- use mapstruct-jdk8 for Java 8 or higher -->
|
||||
<version>${mapstruct.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-jdk8</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-processor</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>${hutool-5.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.dromara.hutool</groupId>
|
||||
<artifactId>hutool-extra</artifactId>
|
||||
<version>${hutool-6.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>easyexcel</artifactId>
|
||||
<version>${easyexcel.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>${commons-io.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-compress</artifactId>
|
||||
<version>${commons-compress.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.tika</groupId>
|
||||
<artifactId>tika-core</artifactId> <!-- 文件类型的识别 -->
|
||||
<version>${tika-core.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.velocity</groupId>
|
||||
<artifactId>velocity-engine-core</artifactId>
|
||||
<version>${velocity.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<version>${fastjson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>${guava.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>transmittable-thread-local</artifactId> <!-- 解决 ThreadLocal 父子线程的传值问题 -->
|
||||
<version>${transmittable-thread-local.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>commons-net</groupId>
|
||||
<artifactId>commons-net</artifactId> <!-- 解决 ftp 连接 -->
|
||||
<version>${commons-net.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.jcraft</groupId>
|
||||
<artifactId>jsch</artifactId> <!-- 解决 sftp 连接 -->
|
||||
<version>${jsch.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.anji-plus</groupId>
|
||||
<artifactId>captcha-spring-boot-starter</artifactId> <!-- 验证码,一般用于登录使用 -->
|
||||
<version>${anji-plus-captcha.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.lionsoul</groupId>
|
||||
<artifactId>ip2region</artifactId>
|
||||
<version>${ip2region.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
<version>${jsoup.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.reflections</groupId>
|
||||
<artifactId>reflections</artifactId>
|
||||
<version>${reflections.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 三方云服务相关 -->
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>s3</artifactId>
|
||||
<version>${awssdk.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>weixin-java-pay</artifactId>
|
||||
<version>${weixin-java.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
|
||||
<version>${weixin-java.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
|
||||
<version>${weixin-java.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>me.zhyd.oauth</groupId>
|
||||
<artifactId>JustAuth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
|
||||
<version>${justauth.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xkcoding.justauth</groupId>
|
||||
<artifactId>justauth-spring-boot-starter</artifactId>
|
||||
<version>${justauth-starter.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 积木报表-->
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.jimureport</groupId>
|
||||
<artifactId>jimureport-spring-boot3-starter-fastjson2</artifactId>
|
||||
<version>${jimureport.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.jimureport</groupId>
|
||||
<artifactId>jimubi-spring-boot3-starter</artifactId>
|
||||
<version>${jimureport.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.github.jsqlparser</groupId>
|
||||
<artifactId>jsqlparser</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- 规则引擎 -->
|
||||
<dependency>
|
||||
<groupId>com.yomahub</groupId>
|
||||
<artifactId>liteflow-spring-boot-starter</artifactId>
|
||||
<version>${liteflow.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- PF4J -->
|
||||
<dependency>
|
||||
<groupId>org.pf4j</groupId>
|
||||
<artifactId>pf4j-spring</artifactId>
|
||||
<version>${pf4j-spring.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-log4j12</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Vert.x -->
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-core</artifactId>
|
||||
<version>${vertx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-web</artifactId>
|
||||
<version>${vertx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-mqtt</artifactId>
|
||||
<version>${vertx.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MQTT -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.paho</groupId>
|
||||
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
|
||||
<version>${mqtt.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- 统一 revision 版本 -->
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>flatten-maven-plugin</artifactId>
|
||||
<version>${flatten-maven-plugin.version}</version>
|
||||
<configuration>
|
||||
<flattenMode>bom</flattenMode>
|
||||
<updatePomFile>true</updatePomFile>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>flatten</goal>
|
||||
</goals>
|
||||
<id>flatten</id>
|
||||
<phase>process-resources</phase>
|
||||
</execution>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>clean</goal>
|
||||
</goals>
|
||||
<id>flatten.clean</id>
|
||||
<phase>clean</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
29
zt-framework-dsc/pom.xml
Normal file
29
zt-framework-dsc/pom.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?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">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<artifactId>zt-dsc</artifactId>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<packaging>pom</packaging>
|
||||
<modules>
|
||||
<module>zt-common-dsc</module>
|
||||
</modules>
|
||||
|
||||
<artifactId>zt-framework-dsc</artifactId>
|
||||
<description>
|
||||
该包是技术组件,每个子包,代表一个组件。每个组件包括两部分:
|
||||
1. core 包:是该组件的核心封装
|
||||
2. config 包:是该组件基于 Spring 的配置
|
||||
|
||||
技术组件,也分成两类:
|
||||
1. 框架组件:和我们熟悉的 MyBatis、Redis 等等的拓展
|
||||
2. 业务组件:和业务相关的组件的封装,例如说数据字典、操作日志等等。
|
||||
如果是业务组件,Maven 名字会包含 biz
|
||||
</description>
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
</project>
|
||||
@@ -4,11 +4,11 @@
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-framework</artifactId>
|
||||
<artifactId>zt-framework-dsc</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>zt-common</artifactId>
|
||||
<artifactId>zt-common-dsc</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
@@ -16,6 +16,10 @@
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-common</artifactId>
|
||||
</dependency>
|
||||
<!-- Spring 核心 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
@@ -9,7 +9,7 @@ import java.util.concurrent.*;
|
||||
* 多次提交,一次等待
|
||||
*/
|
||||
public class AsyncLatchUtils {
|
||||
private static final ThreadLocal<List<TaskInfo>> THREAD_LOCAL = ThreadLocal.withInitial(LinkedList::new);
|
||||
private static final ThreadLocal<List<TaskInfo>> THREADLOCAL = ThreadLocal.withInitial(LinkedList::new);
|
||||
|
||||
/**
|
||||
* 提交一个异步任务
|
||||
@@ -17,7 +17,7 @@ public class AsyncLatchUtils {
|
||||
* @param runnable 需要异步执行的具体业务逻辑
|
||||
*/
|
||||
public static void submitTask(Executor executor, Runnable runnable) {
|
||||
THREAD_LOCAL.get().add(new TaskInfo(executor, runnable));
|
||||
THREADLOCAL.get().add(new TaskInfo(executor, runnable));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,8 +25,8 @@ public class AsyncLatchUtils {
|
||||
* @return
|
||||
*/
|
||||
private static List<TaskInfo> popTask() {
|
||||
List<TaskInfo> taskInfos = THREAD_LOCAL.get();
|
||||
THREAD_LOCAL.remove();
|
||||
List<TaskInfo> taskInfos = THREADLOCAL.get();
|
||||
THREADLOCAL.remove();
|
||||
return taskInfos;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ public class AsyncLatchUtils {
|
||||
*/
|
||||
public static boolean waitFor(long timeout, TimeUnit timeUnit) {
|
||||
List<TaskInfo> taskInfos = popTask();
|
||||
if (taskInfos.isEmpty()) {
|
||||
if (taskInfos.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
CountDownLatch latch = new CountDownLatch(taskInfos.size());
|
||||
@@ -57,11 +57,8 @@ public class AsyncLatchUtils {
|
||||
boolean await = false;
|
||||
try {
|
||||
await = latch.await(timeout, timeUnit);
|
||||
} catch (Exception ignored) {
|
||||
// 恢复中断状态
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return await;
|
||||
} catch (Exception ignored) {}
|
||||
return await;
|
||||
}
|
||||
|
||||
private static final class TaskInfo {
|
||||
@@ -1,51 +0,0 @@
|
||||
<?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">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<artifactId>zt</artifactId>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<packaging>pom</packaging>
|
||||
<modules>
|
||||
<module>zt-common</module>
|
||||
<module>zt-spring-boot-starter-env</module>
|
||||
<module>zt-spring-boot-starter-mybatis</module>
|
||||
<module>zt-spring-boot-starter-redis</module>
|
||||
<module>zt-spring-boot-starter-web</module>
|
||||
<module>zt-spring-boot-starter-security</module>
|
||||
<module>zt-spring-boot-starter-websocket</module>
|
||||
<module>zt-spring-boot-starter-databus-server</module>
|
||||
<module>zt-spring-boot-starter-databus-client</module>
|
||||
<module>zt-spring-boot-starter-monitor</module>
|
||||
<module>zt-spring-boot-starter-protection</module>
|
||||
<!-- <module>zt-spring-boot-starter-config</module>-->
|
||||
<module>zt-spring-boot-starter-job</module>
|
||||
<module>zt-spring-boot-starter-mq</module>
|
||||
<module>zt-spring-boot-starter-rpc</module>
|
||||
<module>zt-spring-boot-starter-seata-dm</module>
|
||||
<module>zt-spring-boot-starter-excel</module>
|
||||
<module>zt-spring-boot-starter-test</module>
|
||||
|
||||
<module>zt-spring-boot-starter-biz-tenant</module>
|
||||
<module>zt-spring-boot-starter-biz-data-permission</module>
|
||||
<module>zt-spring-boot-starter-biz-ip</module>
|
||||
<module>zt-spring-boot-starter-biz-business</module>
|
||||
</modules>
|
||||
|
||||
<artifactId>zt-framework</artifactId>
|
||||
<description>
|
||||
该包是技术组件,每个子包,代表一个组件。每个组件包括两部分:
|
||||
1. core 包:是该组件的核心封装
|
||||
2. config 包:是该组件基于 Spring 的配置
|
||||
|
||||
技术组件,也分成两类:
|
||||
1. 框架组件:和我们熟悉的 MyBatis、Redis 等等的拓展
|
||||
2. 业务组件:和业务相关的组件的封装,例如说数据字典、操作日志等等。
|
||||
如果是业务组件,Maven 名字会包含 biz
|
||||
</description>
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
</project>
|
||||
@@ -1,59 +0,0 @@
|
||||
package com.fhs.trans.service;
|
||||
|
||||
import com.fhs.core.trans.vo.VO;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 只有实现了这个接口的才能自动翻译
|
||||
*
|
||||
* 为什么要赋值粘贴到 zt-common 包下?
|
||||
* 因为 AutoTransable 属于 easy-trans-service 下,无法方便的在 zt-module-xxx-api 模块下使用
|
||||
*
|
||||
* @author jackwang
|
||||
* @since 2020-05-19 10:26:15
|
||||
*/
|
||||
public interface AutoTransable<V extends VO> {
|
||||
|
||||
/**
|
||||
* 根据 ids 查询数据列表
|
||||
*
|
||||
* 改方法已过期啦,请使用 selectByIds
|
||||
*
|
||||
* @param ids 编号数组
|
||||
* @return 数据列表
|
||||
*/
|
||||
@Deprecated
|
||||
default List<V> findByIds(List<? extends Object> ids){
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ids 查询
|
||||
*
|
||||
* @param ids 编号数组
|
||||
* @return 数据列表
|
||||
*/
|
||||
default List<V> selectByIds(List<? extends Object> ids){
|
||||
return this.findByIds(ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 db 中所有的数据
|
||||
*
|
||||
* @return db 中所有的数据
|
||||
*/
|
||||
default List<V> select(){
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 id 获取 vo
|
||||
*
|
||||
* @param primaryValue id
|
||||
* @return vo
|
||||
*/
|
||||
V selectById(Object primaryValue);
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.zt.plat.framework.common.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 标记分页结果中需要求和的字段。
|
||||
* <p>
|
||||
* 未显式指定列名时,会默认使用实体字段对应的数据库列。
|
||||
* <p>
|
||||
* {@link #exist()} 可以用于声明该字段并不存在于表结构中,相当于为字段添加
|
||||
* {@code @TableField(exist = false)},方便在 DO 中声明专用于汇总结果的临时字段。
|
||||
*/
|
||||
@Documented
|
||||
@Target(ElementType.FIELD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface PageSum {
|
||||
|
||||
/**
|
||||
* 自定义求和的数据库列名或表达式,未设置时默认使用实体字段对应的列。
|
||||
*/
|
||||
String column() default "";
|
||||
|
||||
/**
|
||||
* 是否在实体字段上声明真实存在的数据库列。
|
||||
* <p>
|
||||
* 设为 {@code false} 时,框架会自动为该字段提供 {@code @TableField(exist = false)} 的能力,
|
||||
* 适用于只在分页响应中返回的临时统计字段。
|
||||
*/
|
||||
boolean exist() default false;
|
||||
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package com.zt.plat.framework.common.biz.infra.logger;
|
||||
|
||||
import com.zt.plat.framework.common.biz.infra.logger.dto.ApiAccessLogCreateReqDTO;
|
||||
import com.zt.plat.framework.common.enums.RpcConstants;
|
||||
import com.zt.plat.framework.common.pojo.CommonResult;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
@FeignClient(name = RpcConstants.INFRA_NAME) // TODO ZT:fallbackFactory =
|
||||
@Tag(name = "RPC 服务 - API 访问日志")
|
||||
public interface ApiAccessLogCommonApi {
|
||||
|
||||
String PREFIX = RpcConstants.INFRA_PREFIX + "/api-access-log";
|
||||
|
||||
@PostMapping(PREFIX + "/create")
|
||||
@Operation(summary = "创建 API 访问日志")
|
||||
CommonResult<Boolean> createApiAccessLog(@Valid @RequestBody ApiAccessLogCreateReqDTO createDTO);
|
||||
|
||||
/**
|
||||
* 【异步】创建 API 访问日志
|
||||
*
|
||||
* @param createDTO 访问日志 DTO
|
||||
*/
|
||||
@Async
|
||||
default void createApiAccessLogAsync(ApiAccessLogCreateReqDTO createDTO) {
|
||||
createApiAccessLog(createDTO).checkError();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package com.zt.plat.framework.common.biz.infra.logger;
|
||||
|
||||
import com.zt.plat.framework.common.biz.infra.logger.dto.ApiErrorLogCreateReqDTO;
|
||||
import com.zt.plat.framework.common.enums.RpcConstants;
|
||||
import com.zt.plat.framework.common.pojo.CommonResult;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
@FeignClient(name = RpcConstants.INFRA_NAME) // TODO ZT:fallbackFactory =
|
||||
@Tag(name = "RPC 服务 - API 异常日志")
|
||||
public interface ApiErrorLogCommonApi {
|
||||
|
||||
String PREFIX = RpcConstants.INFRA_PREFIX + "/api-error-log";
|
||||
|
||||
@PostMapping(PREFIX + "/create")
|
||||
@Operation(summary = "创建 API 异常日志")
|
||||
CommonResult<Boolean> createApiErrorLog(@Valid @RequestBody ApiErrorLogCreateReqDTO createDTO);
|
||||
|
||||
/**
|
||||
* 【异步】创建 API 异常日志
|
||||
*
|
||||
* @param createDTO 异常日志 DTO
|
||||
*/
|
||||
@Async
|
||||
default void createApiErrorLogAsync(ApiErrorLogCreateReqDTO createDTO) {
|
||||
createApiErrorLog(createDTO).checkError();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package com.zt.plat.framework.common.biz.infra.logger.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* API 访问日志
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Data
|
||||
public class ApiAccessLogCreateReqDTO {
|
||||
|
||||
/**
|
||||
* 链路追踪编号
|
||||
*/
|
||||
private String traceId;
|
||||
/**
|
||||
* 用户编号
|
||||
*/
|
||||
private Long userId;
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private Integer userType;
|
||||
/**
|
||||
* 应用名
|
||||
*/
|
||||
@NotNull(message = "应用名不能为空")
|
||||
private String applicationName;
|
||||
|
||||
/**
|
||||
* 请求方法名
|
||||
*/
|
||||
@NotNull(message = "http 请求方法不能为空")
|
||||
private String requestMethod;
|
||||
/**
|
||||
* 访问地址
|
||||
*/
|
||||
@NotNull(message = "访问地址不能为空")
|
||||
private String requestUrl;
|
||||
/**
|
||||
* 请求参数
|
||||
*/
|
||||
private String requestParams;
|
||||
/**
|
||||
* 响应结果
|
||||
*/
|
||||
private String responseBody;
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
@NotNull(message = "ip 不能为空")
|
||||
private String userIp;
|
||||
/**
|
||||
* 浏览器 UA
|
||||
*/
|
||||
@NotNull(message = "User-Agent 不能为空")
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 操作模块
|
||||
*/
|
||||
private String operateModule;
|
||||
/**
|
||||
* 操作名
|
||||
*/
|
||||
private String operateName;
|
||||
/**
|
||||
* 操作分类
|
||||
*
|
||||
* 枚举,参见 OperateTypeEnum 类
|
||||
*/
|
||||
private Integer operateType;
|
||||
|
||||
/**
|
||||
* 开始请求时间
|
||||
*/
|
||||
@NotNull(message = "开始请求时间不能为空")
|
||||
private LocalDateTime beginTime;
|
||||
/**
|
||||
* 结束请求时间
|
||||
*/
|
||||
@NotNull(message = "结束请求时间不能为空")
|
||||
private LocalDateTime endTime;
|
||||
/**
|
||||
* 执行时长,单位:毫秒
|
||||
*/
|
||||
@NotNull(message = "执行时长不能为空")
|
||||
private Integer duration;
|
||||
/**
|
||||
* 结果码
|
||||
*/
|
||||
@NotNull(message = "错误码不能为空")
|
||||
private Integer resultCode;
|
||||
/**
|
||||
* 结果提示
|
||||
*/
|
||||
private String resultMsg;
|
||||
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package com.zt.plat.framework.common.biz.infra.logger.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "RPC 服务 - API 错误日志创建 Request DTO")
|
||||
@Data
|
||||
public class ApiErrorLogCreateReqDTO {
|
||||
|
||||
@Schema(description = "链路追踪编号", example = "89aca178-a370-411c-ae02-3f0d672be4ab")
|
||||
private String traceId;
|
||||
|
||||
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long userId;
|
||||
@Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Integer userType;
|
||||
@Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "system-server")
|
||||
@NotNull(message = "应用名不能为空")
|
||||
private String applicationName;
|
||||
|
||||
@Schema(description = "请求方法名", requiredMode = Schema.RequiredMode.REQUIRED, example = "GET")
|
||||
@NotNull(message = "http 请求方法不能为空")
|
||||
private String requestMethod;
|
||||
@Schema(description = "请求地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "/xxx/yyy")
|
||||
@NotNull(message = "访问地址不能为空")
|
||||
private String requestUrl;
|
||||
@Schema(description = "请求参数", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "请求参数不能为空")
|
||||
private String requestParams;
|
||||
@Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1")
|
||||
@NotNull(message = "ip 不能为空")
|
||||
private String userIp;
|
||||
@Schema(description = "浏览器 UserAgent", requiredMode = Schema.RequiredMode.REQUIRED, example = "Mozilla/5.0")
|
||||
@NotNull(message = "User-Agent 不能为空")
|
||||
private String userAgent;
|
||||
|
||||
@Schema(description = "异常时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "异常时间不能为空")
|
||||
private LocalDateTime exceptionTime;
|
||||
@Schema(description = "异常名", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "异常名不能为空")
|
||||
private String exceptionName;
|
||||
@Schema(description = "异常发生的类全名", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "异常发生的类全名不能为空")
|
||||
private String exceptionClassName;
|
||||
@Schema(description = "异常发生的类文件", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "异常发生的类文件不能为空")
|
||||
private String exceptionFileName;
|
||||
@Schema(description = "异常发生的方法名", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "异常发生的方法名不能为空")
|
||||
private String exceptionMethodName;
|
||||
@Schema(description = "异常发生的方法所在行", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "异常发生的方法所在行不能为空")
|
||||
private Integer exceptionLineNumber;
|
||||
@Schema(description = "异常的栈轨迹异常的栈轨迹", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "异常的栈轨迹不能为空")
|
||||
private String exceptionStackTrace;
|
||||
@Schema(description = "异常导致的根消息", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "异常导致的根消息不能为空")
|
||||
private String exceptionRootCauseMessage;
|
||||
@Schema(description = "异常导致的消息", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "异常导致的消息不能为空")
|
||||
private String exceptionMessage;
|
||||
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
/**
|
||||
* 针对 infra 模块的 api 包
|
||||
*/
|
||||
package com.zt.plat.framework.common.biz.infra;
|
||||
@@ -1,4 +0,0 @@
|
||||
/**
|
||||
* 特殊:用于 framework 下,starter 需要调用 biz 业务模块的接口定义!
|
||||
*/
|
||||
package com.zt.plat.framework.common.biz;
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.zt.plat.framework.common.biz.system.dict;
|
||||
|
||||
import com.zt.plat.framework.common.biz.system.dict.dto.DictDataRespDTO;
|
||||
import com.zt.plat.framework.common.enums.RpcConstants;
|
||||
import com.zt.plat.framework.common.pojo.CommonResult;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@FeignClient(name = RpcConstants.SYSTEM_NAME, primary = false) // TODO ZT:fallbackFactory =
|
||||
@Tag(name = "RPC 服务 - 字典数据")
|
||||
public interface DictDataCommonApi {
|
||||
|
||||
String PREFIX = RpcConstants.SYSTEM_PREFIX + "/dict-data";
|
||||
|
||||
@GetMapping(PREFIX + "/list")
|
||||
@Operation(summary = "获得指定字典类型的字典数据列表")
|
||||
@Parameter(name = "dictType", description = "字典类型", example = "SEX", required = true)
|
||||
CommonResult<List<DictDataRespDTO>> getDictDataList(@RequestParam("dictType") String dictType);
|
||||
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.zt.plat.framework.common.biz.system.dict.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "RPC 服务 - 字典数据 Response DTO")
|
||||
@Data
|
||||
public class DictDataRespDTO {
|
||||
|
||||
@Schema(description = "字典标签", requiredMode = Schema.RequiredMode.REQUIRED, example = "ZT")
|
||||
private String label;
|
||||
|
||||
@Schema(description = "字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "iocoder")
|
||||
private String value;
|
||||
|
||||
@Schema(description = "字典类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "sys_common_sex")
|
||||
private String dictType;
|
||||
|
||||
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Integer status; // 参见 CommonStatusEnum 枚举
|
||||
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package com.zt.plat.framework.common.biz.system.logger;
|
||||
|
||||
import com.zt.plat.framework.common.biz.system.logger.dto.OperateLogCreateReqDTO;
|
||||
import com.zt.plat.framework.common.enums.RpcConstants;
|
||||
import com.zt.plat.framework.common.pojo.CommonResult;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
@FeignClient(name = RpcConstants.SYSTEM_NAME, primary = false) // TODO ZT:fallbackFactory =
|
||||
@Tag(name = "RPC 服务 - 操作日志")
|
||||
public interface OperateLogCommonApi {
|
||||
|
||||
String PREFIX = RpcConstants.SYSTEM_PREFIX + "/operate-log";
|
||||
|
||||
@PostMapping(PREFIX + "/create")
|
||||
@Operation(summary = "创建操作日志")
|
||||
CommonResult<Boolean> createOperateLog(@Valid @RequestBody OperateLogCreateReqDTO createReqDTO);
|
||||
|
||||
/**
|
||||
* 【异步】创建操作日志
|
||||
*
|
||||
* @param createReqDTO 请求
|
||||
*/
|
||||
@Async
|
||||
default void createOperateLogAsync(OperateLogCreateReqDTO createReqDTO) {
|
||||
createOperateLog(createReqDTO).checkError();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package com.zt.plat.framework.common.biz.system.logger.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(name = "RPC 服务 - 系统操作日志 Create Request DTO")
|
||||
@Data
|
||||
public class OperateLogCreateReqDTO {
|
||||
|
||||
@Schema(description = "链路追踪编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "89aca178-a370-411c-ae02-3f0d672be4ab")
|
||||
private String traceId;
|
||||
|
||||
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "666")
|
||||
@NotNull(message = "用户编号不能为空")
|
||||
private Long userId;
|
||||
@Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "2" )
|
||||
@NotNull(message = "用户类型不能为空")
|
||||
private Integer userType;
|
||||
@Schema(description = "操作模块类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "订单")
|
||||
@NotEmpty(message = "操作模块类型不能为空")
|
||||
private String type;
|
||||
@Schema(description = "操作名", requiredMode = Schema.RequiredMode.REQUIRED, example = "创建订单")
|
||||
@NotEmpty(message = "操作名不能为空")
|
||||
private String subType;
|
||||
@Schema(description = "操作模块业务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "188")
|
||||
@NotNull(message = "操作模块业务编号不能为空")
|
||||
private Long bizId;
|
||||
@Schema(description = "操作内容", requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
example = "修改编号为 1 的用户信息,将性别从男改成女,将姓名从ZT改成源码")
|
||||
@NotEmpty(message = "操作内容不能为空")
|
||||
private String action;
|
||||
@Schema(description = "拓展字段", example = "{\"orderId\": \"1\"}")
|
||||
private String extra;
|
||||
|
||||
@Schema(description = "请求方法名", requiredMode = Schema.RequiredMode.REQUIRED, example = "GET")
|
||||
@NotEmpty(message = "请求方法名不能为空")
|
||||
private String requestMethod;
|
||||
@Schema(description = "请求地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "/order/get")
|
||||
@NotEmpty(message = "请求地址不能为空")
|
||||
private String requestUrl;
|
||||
@Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1")
|
||||
@NotEmpty(message = "用户 IP 不能为空")
|
||||
private String userIp;
|
||||
@Schema(description = "浏览器 UserAgent", requiredMode = Schema.RequiredMode.REQUIRED, example = "Mozilla/5.0")
|
||||
@NotEmpty(message = "浏览器 UA 不能为空")
|
||||
private String userAgent;
|
||||
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package com.zt.plat.framework.common.biz.system.oauth2;
|
||||
|
||||
import com.zt.plat.framework.common.enums.RpcConstants;
|
||||
import com.zt.plat.framework.common.pojo.CommonResult;
|
||||
import com.zt.plat.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
|
||||
import com.zt.plat.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCreateReqDTO;
|
||||
import com.zt.plat.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenRespDTO;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Parameters;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
@FeignClient(name = RpcConstants.SYSTEM_NAME) // TODO ZT:fallbackFactory =
|
||||
@Tag(name = "RPC 服务 - OAuth2.0 令牌")
|
||||
public interface OAuth2TokenCommonApi {
|
||||
|
||||
String PREFIX = RpcConstants.SYSTEM_PREFIX + "/oauth2/token";
|
||||
|
||||
/**
|
||||
* 校验 Token 的 URL 地址,主要是提供给 Gateway 使用
|
||||
*/
|
||||
@SuppressWarnings("HttpUrlsUsage")
|
||||
String URL_CHECK = "http://" + RpcConstants.SYSTEM_NAME + PREFIX + "/check";
|
||||
|
||||
@PostMapping(PREFIX + "/create")
|
||||
@Operation(summary = "创建访问令牌")
|
||||
CommonResult<OAuth2AccessTokenRespDTO> createAccessToken(@Valid @RequestBody OAuth2AccessTokenCreateReqDTO reqDTO);
|
||||
|
||||
@GetMapping(PREFIX + "/check")
|
||||
@Operation(summary = "校验访问令牌")
|
||||
@Parameter(name = "accessToken", description = "访问令牌", required = true, example = "tudou")
|
||||
CommonResult<OAuth2AccessTokenCheckRespDTO> checkAccessToken(@RequestParam("accessToken") String accessToken);
|
||||
|
||||
@DeleteMapping(PREFIX + "/remove")
|
||||
@Operation(summary = "移除访问令牌")
|
||||
@Parameter(name = "accessToken", description = "访问令牌", required = true, example = "tudou")
|
||||
CommonResult<OAuth2AccessTokenRespDTO> removeAccessToken(@RequestParam("accessToken") String accessToken);
|
||||
|
||||
@PutMapping(PREFIX + "/refresh")
|
||||
@Operation(summary = "刷新访问令牌")
|
||||
@Parameters({
|
||||
@Parameter(name = "refreshToken", description = "刷新令牌", required = true, example = "haha"),
|
||||
@Parameter(name = "clientId", description = "客户端编号", required = true, example = "ztyuanma")
|
||||
})
|
||||
CommonResult<OAuth2AccessTokenRespDTO> refreshAccessToken(@RequestParam("refreshToken") String refreshToken,
|
||||
@RequestParam("clientId") String clientId);
|
||||
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.zt.plat.framework.common.biz.system.oauth2.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Schema(description = "RPC 服务 - OAuth2 访问令牌的校验 Response DTO")
|
||||
@Data
|
||||
public class OAuth2AccessTokenCheckRespDTO implements Serializable {
|
||||
|
||||
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Integer userType;
|
||||
|
||||
@Schema(description = "用户信息", example = "{\"nickname\": \"ZT\"}")
|
||||
private Map<String, String> userInfo;
|
||||
|
||||
@Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "授权范围的数组", example = "user_info")
|
||||
private List<String> scopes;
|
||||
|
||||
@Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime expiresTime;
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.zt.plat.framework.common.biz.system.oauth2.dto;
|
||||
|
||||
import com.zt.plat.framework.common.enums.UserTypeEnum;
|
||||
import com.zt.plat.framework.common.validation.InEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "RPC 服务 - OAuth2 访问令牌创建 Request DTO")
|
||||
@Data
|
||||
public class OAuth2AccessTokenCreateReqDTO implements Serializable {
|
||||
|
||||
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
|
||||
@NotNull(message = "用户编号不能为空")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "用户类型不能为空")
|
||||
@InEnum(value = UserTypeEnum.class, message = "用户类型必须是 {value}")
|
||||
private Integer userType;
|
||||
|
||||
@Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "ztyuanma")
|
||||
@NotNull(message = "客户端编号不能为空")
|
||||
private String clientId;
|
||||
|
||||
@Schema(description = "授权范围的数组", example = "user_info")
|
||||
private List<String> scopes;
|
||||
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.zt.plat.framework.common.biz.system.oauth2.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "RPC 服务 - OAuth2 访问令牌的信息 Response DTO")
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class OAuth2AccessTokenRespDTO implements Serializable {
|
||||
|
||||
@Schema(description = "访问令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "tudou")
|
||||
private String accessToken;
|
||||
|
||||
@Schema(description = "刷新令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "haha")
|
||||
private String refreshToken;
|
||||
|
||||
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1" )
|
||||
private Integer userType;
|
||||
|
||||
@Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime expiresTime;
|
||||
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
/**
|
||||
* 针对 system 模块的 api 包
|
||||
*/
|
||||
package com.zt.plat.framework.common.biz.system;
|
||||
@@ -1,50 +0,0 @@
|
||||
package com.zt.plat.framework.common.biz.system.permission;
|
||||
|
||||
import com.zt.plat.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO;
|
||||
import com.zt.plat.framework.common.enums.RpcConstants;
|
||||
import com.zt.plat.framework.common.pojo.CommonResult;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Parameters;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@FeignClient(name = RpcConstants.SYSTEM_NAME, primary = false) // TODO ZT:fallbackFactory =
|
||||
@Tag(name = "RPC 服务 - 权限")
|
||||
public interface PermissionCommonApi {
|
||||
|
||||
String PREFIX = RpcConstants.SYSTEM_PREFIX + "/permission";
|
||||
|
||||
@GetMapping(PREFIX + "/has-any-permissions")
|
||||
@Operation(summary = "判断是否有权限,任一一个即可")
|
||||
@Parameters({
|
||||
@Parameter(name = "userId", description = "用户编号", example = "1", required = true),
|
||||
@Parameter(name = "permissions", description = "权限", example = "read,write", required = true)
|
||||
})
|
||||
CommonResult<Boolean> hasAnyPermissions(@RequestParam("userId") Long userId,
|
||||
@RequestParam("permissions") String... permissions);
|
||||
|
||||
@GetMapping(PREFIX + "/has-any-roles")
|
||||
@Operation(summary = "判断是否有角色,任一一个即可")
|
||||
@Parameters({
|
||||
@Parameter(name = "userId", description = "用户编号", example = "1", required = true),
|
||||
@Parameter(name = "roles", description = "角色数组", example = "2", required = true)
|
||||
})
|
||||
CommonResult<Boolean> hasAnyRoles(@RequestParam("userId") Long userId,
|
||||
@RequestParam("roles") String... roles);
|
||||
|
||||
@GetMapping(PREFIX + "/get-dept-data-permission")
|
||||
@Operation(summary = "获得登陆用户的部门数据权限")
|
||||
@Parameter(name = "userId", description = "用户编号", example = "2", required = true)
|
||||
CommonResult<DeptDataPermissionRespDTO> getDeptDataPermission(@RequestParam("userId") Long userId);
|
||||
|
||||
@GetMapping(PREFIX + "/get-dept-data-permission-with-roleCodes")
|
||||
@Operation(summary = "获得登陆用户的部门数据权限")
|
||||
@Parameters({
|
||||
@Parameter(name = "userId", description = "用户编号", example = "2", required = true),
|
||||
@Parameter(name = "roleCodes", description = "角色编码", example = "2", required = true)
|
||||
})
|
||||
CommonResult<DeptDataPermissionRespDTO> getDeptDataPermissionWithRoleCodes(@RequestParam("userId") Long userId, @RequestParam("roleCodes") String roleCodes);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.zt.plat.framework.common.biz.system.permission.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Schema(description = "RPC 服务 - 部门的数据权限 Response DTO")
|
||||
@Data
|
||||
public class DeptDataPermissionRespDTO {
|
||||
|
||||
@Schema(description = "是否可查看全部数据", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
|
||||
private Boolean all;
|
||||
|
||||
@Schema(description = "是否可查看自己的数据", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
|
||||
private Boolean self;
|
||||
|
||||
@Schema(description = "可查看的部门编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 3]")
|
||||
private Set<Long> deptIds;
|
||||
|
||||
@Schema(description = "可查看的公司编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 3]")
|
||||
private Long companyId;
|
||||
|
||||
public DeptDataPermissionRespDTO() {
|
||||
this.all = false;
|
||||
this.self = false;
|
||||
this.deptIds = new HashSet<>();
|
||||
this.companyId = 0L;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.zt.plat.framework.common.biz.system.sequence;
|
||||
|
||||
import com.zt.plat.framework.common.enums.RpcConstants;
|
||||
import com.zt.plat.framework.common.pojo.CommonResult;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Parameters;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author chenbowen
|
||||
*/
|
||||
@FeignClient(name = RpcConstants.SYSTEM_NAME)
|
||||
@Tag(name = "序列管理 Api")
|
||||
public interface SequenceCommonApi {
|
||||
|
||||
String PREFIX = RpcConstants.SYSTEM_PREFIX + "/sequence";
|
||||
|
||||
@PostMapping(PREFIX + "/next-sequence")
|
||||
@Operation(summary = "获取下一个序列号")
|
||||
@Parameters({
|
||||
@Parameter(name = "sequenceCode", description = "序列编码", example = "ORDER_NO", required = true),
|
||||
@Parameter(name = "circulationValue", description = "循环值", example = "20250811"),
|
||||
@Parameter(name = "inputStrs", description = "输入参数", example = "[\"A\",\"B\"]")
|
||||
})
|
||||
CommonResult<String> getNextSequence(@RequestParam("sequenceCode") String sequenceCode,
|
||||
@RequestParam(value = "circulationValue", required = false) String circulationValue,
|
||||
@RequestParam(value = "inputStrs", required = false) List<String> inputStrs);
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.zt.plat.framework.common.biz.system.tenant;
|
||||
|
||||
import com.zt.plat.framework.common.enums.RpcConstants;
|
||||
import com.zt.plat.framework.common.pojo.CommonResult;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@FeignClient(name = RpcConstants.SYSTEM_NAME) // TODO ZT:fallbackFactory =
|
||||
@Tag(name = "RPC 服务 - 多租户")
|
||||
public interface TenantCommonApi {
|
||||
|
||||
String PREFIX = RpcConstants.SYSTEM_PREFIX + "/tenant";
|
||||
|
||||
@GetMapping(PREFIX + "/id-list")
|
||||
@Operation(summary = "获得所有租户编号")
|
||||
CommonResult<List<Long>> getTenantIdList();
|
||||
|
||||
@GetMapping(PREFIX + "/valid")
|
||||
@Operation(summary = "校验租户是否合法")
|
||||
@Parameter(name = "id", description = "租户编号", required = true, example = "1024")
|
||||
CommonResult<Boolean> validTenant(@RequestParam("id") Long id);
|
||||
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.zt.plat.framework.common.core;
|
||||
|
||||
/**
|
||||
* 可生成 T 数组的接口
|
||||
*
|
||||
* @author HUIHUI
|
||||
*/
|
||||
public interface ArrayValuable<T> {
|
||||
|
||||
/**
|
||||
* @return 数组
|
||||
*/
|
||||
T[] array();
|
||||
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.zt.plat.framework.common.core;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Key Value 的键值对
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class KeyValue<K, V> implements Serializable {
|
||||
|
||||
private K key;
|
||||
private V value;
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package com.zt.plat.framework.common.enums;
|
||||
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import com.zt.plat.framework.common.core.ArrayValuable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 通用状态枚举
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum CommonStatusEnum implements ArrayValuable<Integer> {
|
||||
|
||||
ENABLE(0, "开启"),
|
||||
DISABLE(1, "关闭");
|
||||
|
||||
public static final Integer[] ARRAYS = Arrays.stream(values()).map(CommonStatusEnum::getStatus).toArray(Integer[]::new);
|
||||
|
||||
/**
|
||||
* 状态值
|
||||
*/
|
||||
private final Integer status;
|
||||
/**
|
||||
* 状态名
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
@Override
|
||||
public Integer[] 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package com.zt.plat.framework.common.enums;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.zt.plat.framework.common.core.ArrayValuable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 时间间隔的枚举
|
||||
*
|
||||
* @author dhb52
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum DateIntervalEnum implements ArrayValuable<Integer> {
|
||||
|
||||
DAY(1, "天"),
|
||||
WEEK(2, "周"),
|
||||
MONTH(3, "月"),
|
||||
QUARTER(4, "季度"),
|
||||
YEAR(5, "年")
|
||||
;
|
||||
|
||||
public static final Integer[] ARRAYS = Arrays.stream(values()).map(DateIntervalEnum::getInterval).toArray(Integer[]::new);
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
private final Integer interval;
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
@Override
|
||||
public Integer[] array() {
|
||||
return ARRAYS;
|
||||
}
|
||||
|
||||
public static DateIntervalEnum valueOf(Integer interval) {
|
||||
return ArrayUtil.firstMatch(item -> item.getInterval().equals(interval), DateIntervalEnum.values());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.zt.plat.framework.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 文档地址
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum DocumentEnum {
|
||||
|
||||
REDIS_INSTALL("https://gitee.com/zhijiantianya/ruoyi-vue-pro/issues/I4VCSJ", "Redis 安装文档"),
|
||||
TENANT("http://172.16.46.63:30888", "SaaS 多租户文档");
|
||||
|
||||
private final String url;
|
||||
private final String memo;
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package com.zt.plat.framework.common.enums;
|
||||
|
||||
/**
|
||||
* RPC 相关的枚举
|
||||
*
|
||||
* 虽然放在 zt-spring-boot-starter-rpc 会相对合适,但是每个 API 模块需要使用到,所以暂时只好放在此处
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
public interface RpcConstants {
|
||||
|
||||
/**
|
||||
* RPC API 的前缀
|
||||
*/
|
||||
String RPC_API_PREFIX = "/rpc-api";
|
||||
|
||||
/**
|
||||
* system 服务名
|
||||
*
|
||||
* 注意,需要保证和 spring.application.name 保持一致
|
||||
*/
|
||||
String SYSTEM_NAME = "system-server";
|
||||
|
||||
/**
|
||||
* system 服务的前缀
|
||||
*/
|
||||
String SYSTEM_PREFIX = RPC_API_PREFIX + "/system";
|
||||
|
||||
/**
|
||||
* infra 服务名
|
||||
*
|
||||
* 注意,需要保证和 spring.application.name 保持一致
|
||||
*/
|
||||
String INFRA_NAME = "infra-server";
|
||||
/**
|
||||
* infra 服务的前缀
|
||||
*/
|
||||
String INFRA_PREFIX = RPC_API_PREFIX + "/infra";
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package com.zt.plat.framework.common.enums;
|
||||
|
||||
import com.zt.plat.framework.common.core.ArrayValuable;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 终端的枚举
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
public enum TerminalEnum implements ArrayValuable<Integer> {
|
||||
|
||||
UNKNOWN(0, "未知"), // 目的:在无法解析到 terminal 时,使用它
|
||||
WECHAT_MINI_PROGRAM(10, "微信小程序"),
|
||||
WECHAT_WAP(11, "微信公众号"),
|
||||
H5(20, "H5 网页"),
|
||||
APP(31, "手机 App"),
|
||||
;
|
||||
|
||||
public static final Integer[] ARRAYS = Arrays.stream(values()).map(TerminalEnum::getTerminal).toArray(Integer[]::new);
|
||||
|
||||
/**
|
||||
* 终端
|
||||
*/
|
||||
private final Integer terminal;
|
||||
/**
|
||||
* 终端名
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
@Override
|
||||
public Integer[] array() {
|
||||
return ARRAYS;
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package com.zt.plat.framework.common.enums;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.zt.plat.framework.common.core.ArrayValuable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 全局用户类型枚举
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum UserTypeEnum implements ArrayValuable<Integer> {
|
||||
|
||||
MEMBER(1, "会员"), // 面向 c 端,普通用户
|
||||
ADMIN(2, "管理员"); // 面向 b 端,管理后台
|
||||
|
||||
public static final Integer[] ARRAYS = Arrays.stream(values()).map(UserTypeEnum::getValue).toArray(Integer[]::new);
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
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 Integer[] array() {
|
||||
return ARRAYS;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.zt.plat.framework.common.enums;
|
||||
|
||||
/**
|
||||
* Web 过滤器顺序的枚举类,保证过滤器按照符合我们的预期
|
||||
*
|
||||
* 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 enum 包下
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
public interface WebFilterOrderEnum {
|
||||
|
||||
int CORS_FILTER = Integer.MIN_VALUE;
|
||||
|
||||
int TRACE_FILTER = CORS_FILTER + 1;
|
||||
|
||||
int ENV_TAG_FILTER = TRACE_FILTER + 1;
|
||||
|
||||
int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500;
|
||||
|
||||
// OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等
|
||||
|
||||
int TENANT_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面
|
||||
|
||||
int API_ACCESS_LOG_FILTER = -103; // 需要保证在 RequestBodyCacheFilter 后面
|
||||
|
||||
int XSS_FILTER = -102; // 需要保证在 RequestBodyCacheFilter 后面
|
||||
|
||||
// Spring Security Filter 默认为 -100,可见 org.springframework.boot.autoconfigure.security.SecurityProperties 配置属性类
|
||||
|
||||
int TENANT_SECURITY_FILTER = -99; // 需要保证在 Spring Security 过滤器后面
|
||||
|
||||
int FLOWABLE_FILTER = -98; // 需要保证在 Spring Security 过滤后面
|
||||
|
||||
int DEMO_FILTER = Integer.MAX_VALUE;
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.zt.plat.framework.common.exception;
|
||||
|
||||
import com.zt.plat.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import com.zt.plat.framework.common.exception.enums.ServiceErrorCodeRange;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 错误码对象
|
||||
*
|
||||
* 全局错误码,占用 [0, 999], 参见 {@link GlobalErrorCodeConstants}
|
||||
* 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package com.zt.plat.framework.common.exception;
|
||||
|
||||
import com.zt.plat.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 服务器异常 Exception
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public final class ServerException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* 全局错误码
|
||||
*
|
||||
* @see GlobalErrorCodeConstants
|
||||
*/
|
||||
private Integer code;
|
||||
/**
|
||||
* 错误提示
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 空构造方法,避免反序列化问题
|
||||
*/
|
||||
public ServerException() {
|
||||
}
|
||||
|
||||
public ServerException(ErrorCode errorCode) {
|
||||
this.code = errorCode.getCode();
|
||||
this.message = errorCode.getMsg();
|
||||
}
|
||||
|
||||
public ServerException(Integer code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Integer getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public ServerException setCode(Integer code) {
|
||||
this.code = code;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public ServerException setMessage(String message) {
|
||||
this.message = message;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package com.zt.plat.framework.common.exception;
|
||||
|
||||
import com.zt.plat.framework.common.exception.enums.ServiceErrorCodeRange;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 业务逻辑异常 Exception
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public final class ServiceException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* 业务错误码
|
||||
*
|
||||
* @see ServiceErrorCodeRange
|
||||
*/
|
||||
private Integer code;
|
||||
/**
|
||||
* 错误提示
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 空构造方法,避免反序列化问题
|
||||
*/
|
||||
public ServiceException() {
|
||||
}
|
||||
|
||||
public ServiceException(ErrorCode errorCode) {
|
||||
this.code = errorCode.getCode();
|
||||
this.message = errorCode.getMsg();
|
||||
}
|
||||
|
||||
public ServiceException(Integer code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Integer getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public ServiceException setCode(Integer code) {
|
||||
this.code = code;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public ServiceException setMessage(String message) {
|
||||
this.message = message;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package com.zt.plat.framework.common.exception.enums;
|
||||
|
||||
import com.zt.plat.framework.common.exception.ErrorCode;
|
||||
|
||||
/**
|
||||
* 全局错误码枚举
|
||||
* 0-999 系统异常编码保留
|
||||
*
|
||||
* 一般情况下,使用 HTTP 响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status
|
||||
* 虽然说,HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的
|
||||
* 比较特殊的是,因为之前一直使用 0 作为成功,就不使用 200 啦。
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
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 NOT_NULL_REQUEST_ERROR = new ErrorCode(430, "请求参数不能为空");
|
||||
|
||||
// ========== 服务端错误段 ==========
|
||||
|
||||
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, "未知错误");
|
||||
|
||||
// ========== 业务错误段 ==========
|
||||
// 用户未设置公司信息,无法办理当前业务
|
||||
ErrorCode USER_NOT_SET_DEPT = new ErrorCode(1000, "用户未设置有效的公司或部门信息,无法办理当前业务,请确认已实现 BusinessControllerMarker 业务接口标记");
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package com.zt.plat.framework.common.exception.enums;
|
||||
|
||||
/**
|
||||
* 业务异常的错误码区间,解决:解决各模块错误码定义,避免重复,在此只声明不做实际使用
|
||||
*
|
||||
* 一共 10 位,分成四段
|
||||
*
|
||||
* 第一段,1 位,类型
|
||||
* 1 - 业务级别异常
|
||||
* x - 预留
|
||||
* 第二段,3 位,系统类型
|
||||
* 001 - 用户系统
|
||||
* 002 - 商品系统
|
||||
* 003 - 订单系统
|
||||
* 004 - 支付系统
|
||||
* 005 - 优惠劵系统
|
||||
* ... - ...
|
||||
* 第三段,3 位,模块
|
||||
* 不限制规则。
|
||||
* 一般建议,每个系统里面,可能有多个模块,可以再去做分段。以用户系统为例子:
|
||||
* 001 - OAuth2 模块
|
||||
* 002 - User 模块
|
||||
* 003 - MobileCode 模块
|
||||
* 第四段,3 位,错误码
|
||||
* 不限制规则。
|
||||
* 一般建议,每个模块自增。
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
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)
|
||||
|
||||
// 模块 databus 错误码区间 [1-023-000-000 ~ 1-024-000-000)
|
||||
// 模块 rule 错误码区间 [1-024-000-000 ~ 1-025-000-000)
|
||||
// 模块 gateway 错误码区间 [1-025-000-000 ~ 1-026-000-000)
|
||||
// 模块 convert 错误码区间 [1-026-000-000 ~ 1-027-000-000)
|
||||
// 模块 base 错误码区间 [1-027-000-000 ~ 1-028-000-000)
|
||||
// 模块 manage 错误码区间 [1-028-000-000 ~ 1-029-000-000)
|
||||
// 模块 supply 错误码区间 [1-029-000-000 ~ 1-030-000-000)
|
||||
// 模块 mes 错误码区间 [1-030-000-000 ~ 1-031-000-000)
|
||||
// 模块 logistics 错误码区间 [1-031-000-000 ~ 1-032-000-000)
|
||||
// 模块 qms 错误码区间 [1-032-000-000 ~ 1-033-000-000)
|
||||
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package com.zt.plat.framework.common.exception.util;
|
||||
|
||||
import com.zt.plat.framework.common.exception.ErrorCode;
|
||||
import com.zt.plat.framework.common.exception.ServiceException;
|
||||
import com.zt.plat.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* {@link ServiceException} 工具类
|
||||
*
|
||||
* 目的在于,格式化异常信息提示。
|
||||
* 考虑到 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(code, message);
|
||||
}
|
||||
|
||||
public static ServiceException invalidParamException(String messagePattern, Object... params) {
|
||||
return exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), messagePattern, params);
|
||||
}
|
||||
|
||||
// ========== 格式化方法 ==========
|
||||
|
||||
/**
|
||||
* 将错误编号对应的消息使用 params 进行格式化。
|
||||
*
|
||||
* @param code 错误编号
|
||||
* @param messagePattern 消息模版
|
||||
* @param params 参数
|
||||
* @return 格式化后的提示
|
||||
*/
|
||||
@VisibleForTesting
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
package com.zt.plat.framework.common.pojo;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.zt.plat.framework.common.exception.ErrorCode;
|
||||
import com.zt.plat.framework.common.exception.ServiceException;
|
||||
import com.zt.plat.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 通用返回
|
||||
*
|
||||
* @param <T> 数据泛型
|
||||
*/
|
||||
@Data
|
||||
public class CommonResult<T> implements Serializable {
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*
|
||||
* @see ErrorCode#getCode()
|
||||
*/
|
||||
private Integer code;
|
||||
/**
|
||||
* 返回数据
|
||||
*/
|
||||
private T data;
|
||||
/**
|
||||
* 错误提示,用户可阅读
|
||||
*
|
||||
* @see ErrorCode#getMsg() ()
|
||||
*/
|
||||
private String msg;
|
||||
|
||||
/**
|
||||
* 将传入的 result 对象,转换成另外一个泛型结果的对象
|
||||
*
|
||||
* 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。
|
||||
*
|
||||
* @param result 传入的 result 对象
|
||||
* @param <T> 返回的泛型
|
||||
* @return 新的 CommonResult 对象
|
||||
*/
|
||||
public static <T> CommonResult<T> error(CommonResult<?> result) {
|
||||
return error(result.getCode(), result.getMsg());
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义的中间返回
|
||||
* @param data
|
||||
* @param code
|
||||
* @return
|
||||
* @param <T>
|
||||
*/
|
||||
public static <T> CommonResult<T> customize(T data, int code, String msg) {
|
||||
CommonResult<T> result = new CommonResult<>();
|
||||
result.code = code;
|
||||
result.data = data;
|
||||
result.msg = msg;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义的中间返回
|
||||
*/
|
||||
public static <T> CommonResult<T> customize(T data, CommonResultCodeEnum commonResultCodeEnum) {
|
||||
CommonResult<T> result = new CommonResult<>();
|
||||
result.code = commonResultCodeEnum.getCode();
|
||||
result.data = data;
|
||||
result.msg = commonResultCodeEnum.getMessage();
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public static <T> CommonResult<T> error(Integer code, String message) {
|
||||
Assert.notEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), code, "code 必须是错误的!");
|
||||
CommonResult<T> result = new CommonResult<>();
|
||||
result.code = code;
|
||||
result.msg = message;
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T> CommonResult<T> error(ErrorCode errorCode, Object... params) {
|
||||
Assert.notEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), errorCode.getCode(), "code 必须是错误的!");
|
||||
CommonResult<T> result = new CommonResult<>();
|
||||
result.code = errorCode.getCode();
|
||||
result.msg = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMsg(), params);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T> CommonResult<T> error(ErrorCode errorCode) {
|
||||
return error(errorCode.getCode(), errorCode.getMsg());
|
||||
}
|
||||
|
||||
public static <T> CommonResult<T> success(T data) {
|
||||
CommonResult<T> result = new CommonResult<>();
|
||||
result.code = GlobalErrorCodeConstants.SUCCESS.getCode();
|
||||
result.data = data;
|
||||
result.msg = "";
|
||||
return result;
|
||||
}
|
||||
|
||||
public static boolean isSuccess(Integer code) {
|
||||
return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode());
|
||||
}
|
||||
|
||||
@JsonIgnore // 避免 jackson 序列化
|
||||
public boolean isSuccess() {
|
||||
return isSuccess(code);
|
||||
}
|
||||
|
||||
@JsonIgnore // 避免 jackson 序列化
|
||||
public boolean isError() {
|
||||
return !isSuccess();
|
||||
}
|
||||
|
||||
// ========= 和 Exception 异常体系集成 =========
|
||||
|
||||
/**
|
||||
* 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常
|
||||
*/
|
||||
public void checkError() throws ServiceException {
|
||||
if (isSuccess()) {
|
||||
return;
|
||||
}
|
||||
// 业务异常
|
||||
throw new ServiceException(code, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常
|
||||
* 如果没有,则返回 {@link #data} 数据
|
||||
*/
|
||||
@JsonIgnore // 避免 jackson 序列化
|
||||
public T getCheckedData() {
|
||||
checkError();
|
||||
return data;
|
||||
}
|
||||
|
||||
public static <T> CommonResult<T> error(ServiceException serviceException) {
|
||||
return error(serviceException.getCode(), serviceException.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.zt.plat.framework.common.pojo;
|
||||
|
||||
/**
|
||||
* @author chenbowen
|
||||
*/
|
||||
|
||||
public enum CommonResultCodeEnum {
|
||||
// 成功
|
||||
SUCCESS(0, "成功"),
|
||||
|
||||
// 根据返回重试
|
||||
NEED_ADJUST(400, "需要根据返回二次重新请求"),
|
||||
|
||||
//
|
||||
ERROR(500, "错误");
|
||||
|
||||
private final int code;
|
||||
private final String message;
|
||||
|
||||
CommonResultCodeEnum(int code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.zt.plat.framework.common.pojo;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 登录用户信息
|
||||
*
|
||||
* @author chenbowen
|
||||
*/
|
||||
@Data
|
||||
public class CompanyDeptInfo {
|
||||
/**
|
||||
* 公司Id
|
||||
*/
|
||||
private String companyId;
|
||||
/**
|
||||
* 公司名称
|
||||
*/
|
||||
private String companyName;
|
||||
/**
|
||||
* 部门Id
|
||||
*/
|
||||
private String deptId;
|
||||
/**
|
||||
* 部门名称
|
||||
*/
|
||||
private String deptName;
|
||||
|
||||
private String companyCode;
|
||||
|
||||
private String deptCode;
|
||||
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.zt.plat.framework.common.pojo;
|
||||
|
||||
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 = 10000, message = "每页条数最大值为 10000")
|
||||
private Integer pageSize = PAGE_SIZE;
|
||||
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package com.zt.plat.framework.common.pojo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Schema(description = "分页结果")
|
||||
@Data //TODO 分页结果参考这个
|
||||
public final class PageResult<T> implements Serializable {
|
||||
|
||||
@Schema(description = "数据", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<T> list;
|
||||
|
||||
@Schema(description = "总量", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@JsonProperty("total")
|
||||
@JsonAlias({"totalCount"})
|
||||
private Long total;
|
||||
|
||||
@Schema(description = "汇总信息(字段需使用 @PageSum 标注)")
|
||||
@JsonProperty("summary")
|
||||
private Map<String, BigDecimal> summary;
|
||||
|
||||
public PageResult() {
|
||||
this.list = new ArrayList<>();
|
||||
this.summary = Collections.emptyMap();
|
||||
}
|
||||
|
||||
public PageResult(List<T> list, Long total) {
|
||||
this(list, total, null);
|
||||
}
|
||||
|
||||
public PageResult(List<T> list, Long total, Map<String, BigDecimal> summary) {
|
||||
this.list = list;
|
||||
this.total = total;
|
||||
setSummaryInternal(summary);
|
||||
}
|
||||
|
||||
public PageResult(Long total) {
|
||||
this(new ArrayList<>(), total, null);
|
||||
}
|
||||
|
||||
public static <T> PageResult<T> empty() {
|
||||
return new PageResult<>(0L);
|
||||
}
|
||||
|
||||
public static <T> PageResult<T> empty(Long total) {
|
||||
return new PageResult<>(total);
|
||||
}
|
||||
|
||||
public void setSummary(Map<String, BigDecimal> summary) {
|
||||
setSummaryInternal(summary);
|
||||
}
|
||||
|
||||
private void setSummaryInternal(Map<String, BigDecimal> summary) {
|
||||
if (summary == null || summary.isEmpty()) {
|
||||
this.summary = Collections.emptyMap();
|
||||
return;
|
||||
}
|
||||
this.summary = new LinkedHashMap<>(summary);
|
||||
}
|
||||
|
||||
public <R> PageResult<R> convert(List<R> newList) {
|
||||
return new PageResult<>(newList, total, summary);
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public Long getTotalCount() {
|
||||
return total;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public void setTotalCount(Long totalCount) {
|
||||
this.total = totalCount;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.zt.plat.framework.common.pojo;
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package com.zt.plat.framework.common.pojo;
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.zt.plat.framework.common.pojo.vo;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author chenbowen
|
||||
*/
|
||||
@Data
|
||||
public class BatchDeleteReqVO {
|
||||
@NotEmpty(message = "批量删除 ids 不能为空")
|
||||
private List<Long> ids;
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package com.zt.plat.framework.common.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 ZT
|
||||
*/
|
||||
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 ZT:可能要思考下,未来要不要做成可配置
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建同步刷新的 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.collection;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.collection.IterUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static com.zt.plat.framework.common.util.collection.CollectionUtils.convertList;
|
||||
|
||||
/**
|
||||
* Array 工具类
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
public class ArrayUtils {
|
||||
|
||||
/**
|
||||
* 将 object 和 newElements 合并成一个数组
|
||||
*
|
||||
* @param object 对象
|
||||
* @param newElements 数组
|
||||
* @param <T> 泛型
|
||||
* @return 结果数组
|
||||
*/
|
||||
@SafeVarargs
|
||||
public static <T> Consumer<T>[] append(Consumer<T> object, Consumer<T>... newElements) {
|
||||
if (object == null) {
|
||||
return newElements;
|
||||
}
|
||||
Consumer<T>[] result = ArrayUtil.newArray(Consumer.class, 1 + newElements.length);
|
||||
result[0] = object;
|
||||
System.arraycopy(newElements, 0, result, 1, newElements.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T, V> V[] toArray(Collection<T> from, Function<T, V> mapper) {
|
||||
return toArray(convertList(from, mapper));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T[] toArray(Collection<T> from) {
|
||||
if (CollectionUtil.isEmpty(from)) {
|
||||
return (T[]) (new Object[0]);
|
||||
}
|
||||
return ArrayUtil.toArray(from, (Class<T>) IterUtil.getElementType(from.iterator()));
|
||||
}
|
||||
|
||||
public static <T> T get(T[] array, int index) {
|
||||
if (null == array || index >= array.length) {
|
||||
return null;
|
||||
}
|
||||
return array[index];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,352 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.collection;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.zt.plat.framework.common.pojo.PageResult;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static cn.hutool.core.convert.Convert.toCollection;
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
/**
|
||||
* Collection 工具类
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
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> PageResult<U> convertPage(PageResult<T> from, Function<T, U> func) {
|
||||
if (ArrayUtil.isEmpty(from)) {
|
||||
return new PageResult<>(from.getTotal());
|
||||
}
|
||||
return new PageResult<>(convertList(from.getList(), func), from.getTotal());
|
||||
}
|
||||
|
||||
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())));
|
||||
}
|
||||
|
||||
public static <T, K> Map<K, T> convertImmutableMap(Collection<T> from, Function<T, K> keyFunc) {
|
||||
if (CollUtil.isEmpty(from)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
ImmutableMap.Builder<K, T> builder = ImmutableMap.builder();
|
||||
from.forEach(item -> builder.put(keyFunc.apply(item), item));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 对比老、新两个列表,找出新增、修改、删除的数据
|
||||
*
|
||||
* @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());
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 LinkedHashSet
|
||||
*
|
||||
* @param <T> 元素类型
|
||||
* @param elementType 集合中元素类型
|
||||
* @param value 被转换的值
|
||||
* @return {@link LinkedHashSet}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> LinkedHashSet<T> toLinkedHashSet(Class<T> elementType, Object value) {
|
||||
return (LinkedHashSet<T>) toCollection(LinkedHashSet.class, elementType, value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.collection;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import com.zt.plat.framework.common.core.KeyValue;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Multimap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Map 工具类
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.collection;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Set 工具类
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
public class SetUtils {
|
||||
|
||||
@SafeVarargs
|
||||
public static <T> Set<T> asSet(T... objs) {
|
||||
return CollUtil.newHashSet(objs);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.date;
|
||||
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
|
||||
import java.time.*;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 时间工具类
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,315 +0,0 @@
|
||||
package com.zt.plat.framework.common.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 com.zt.plat.framework.common.enums.DateIntervalEnum;
|
||||
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.time.temporal.TemporalAdjusters;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.hutool.core.date.DatePattern.UTC_MS_WITH_XXX_OFFSET_PATTERN;
|
||||
import static cn.hutool.core.date.DatePattern.createFormatter;
|
||||
|
||||
/**
|
||||
* 时间工具类,用于 {@link java.time.LocalDateTime}
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
public class LocalDateTimeUtils {
|
||||
|
||||
/**
|
||||
* 空的 LocalDateTime 对象,主要用于 DB 唯一索引的默认值
|
||||
*/
|
||||
public static LocalDateTime EMPTY = buildTime(1970, 1, 1);
|
||||
|
||||
public static DateTimeFormatter UTC_MS_WITH_XXX_OFFSET_FORMATTER = createFormatter(UTC_MS_WITH_XXX_OFFSET_PATTERN);
|
||||
|
||||
/**
|
||||
* 解析时间
|
||||
*
|
||||
* 相比 {@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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,382 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.http;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.entity.mime.HttpMultipartMode;
|
||||
import org.apache.http.entity.mime.MultipartEntityBuilder;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* HttpClient工具类
|
||||
*
|
||||
* @author luzemin
|
||||
*/
|
||||
@Slf4j
|
||||
public class HttpClientUtils {
|
||||
/**
|
||||
* 请求配置对象
|
||||
*/
|
||||
private static final RequestConfig REQUEST_CONFIG;
|
||||
|
||||
static {
|
||||
/* 设置请求和传输超时时间 */
|
||||
REQUEST_CONFIG = RequestConfig.custom().setSocketTimeout(60000).setConnectTimeout(60000).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* post请求传输json参数
|
||||
*
|
||||
* @param url url地址
|
||||
* @param jsonParam 参数
|
||||
* @return JSONObject 请求结果对象
|
||||
*/
|
||||
public static JSONObject httpPost(String url, JSONObject jsonParam) {
|
||||
/* 请求返回结果 */
|
||||
JSONObject jsonResult = null;
|
||||
|
||||
/* 构建连接对象 */
|
||||
CloseableHttpClient httpClient = HttpClients.createDefault();
|
||||
HttpPost httpPost = new HttpPost(url);
|
||||
httpPost.setConfig(REQUEST_CONFIG);
|
||||
|
||||
try {
|
||||
/* 构建请求体 */
|
||||
if (StringUtils.isNotBlank(jsonParam.toJSONString())) {
|
||||
StringEntity entity = new StringEntity(jsonParam.toJSONString(), StandardCharsets.UTF_8);
|
||||
entity.setContentEncoding(StandardCharsets.UTF_8.name());
|
||||
entity.setContentType(ContentType.APPLICATION_JSON.getMimeType());
|
||||
httpPost.setEntity(entity);
|
||||
}
|
||||
|
||||
/* 提交请求,构建响应对象 */
|
||||
CloseableHttpResponse response = httpClient.execute(httpPost);
|
||||
|
||||
/* 处理请求结果 */
|
||||
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
|
||||
HttpEntity responseEntity = response.getEntity();
|
||||
String result = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8);
|
||||
jsonResult = JSONObject.parseObject(result);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
httpPost.releaseConnection();
|
||||
}
|
||||
return jsonResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* post请求传输String参数 例如:name=Jack&sex=1&type=2
|
||||
* Content-type:application/x-www-form-urlencoded
|
||||
*
|
||||
* @param url url地址
|
||||
* @param strParam 参数
|
||||
* @return JSONObject 请求结果对象
|
||||
*/
|
||||
public static JSONObject httpPost(String url, String strParam) {
|
||||
/* 请求返回结果 */
|
||||
JSONObject jsonResult = null;
|
||||
|
||||
/* 构建连接对象 */
|
||||
CloseableHttpClient httpClient = HttpClients.createDefault();
|
||||
HttpPost httpPost = new HttpPost(url);
|
||||
httpPost.setConfig(REQUEST_CONFIG);
|
||||
|
||||
try {
|
||||
/* 构建请求体 */
|
||||
if (StringUtils.isNotBlank(strParam)) {
|
||||
StringEntity entity = new StringEntity(strParam, StandardCharsets.UTF_8);
|
||||
entity.setContentEncoding(StandardCharsets.UTF_8.name());
|
||||
entity.setContentType(ContentType.APPLICATION_JSON.getMimeType());
|
||||
httpPost.setEntity(entity);
|
||||
}
|
||||
|
||||
/* 提交请求,构建响应对象 */
|
||||
CloseableHttpResponse response = httpClient.execute(httpPost);
|
||||
|
||||
/* 处理请求结果 */
|
||||
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
|
||||
HttpEntity responseEntity = response.getEntity();
|
||||
String result = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8);
|
||||
jsonResult = JSONObject.parseObject(result);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
httpPost.releaseConnection();
|
||||
}
|
||||
return jsonResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* post请求传输String参数 例如:name=Jack&sex=1&type=2
|
||||
* Content-type:application/x-www-form-urlencoded
|
||||
*
|
||||
* @param url url地址
|
||||
* @param strParam 参数
|
||||
* @param token 身份认证令牌
|
||||
* @return JSONObject 请求结果对象
|
||||
*/
|
||||
public static JSONObject httpPostByToken(String url, String strParam, String token) {
|
||||
/* 请求返回结果 */
|
||||
JSONObject jsonResult = null;
|
||||
|
||||
/* 构建连接对象 */
|
||||
CloseableHttpClient httpClient = HttpClients.createDefault();
|
||||
HttpPost httpPost = new HttpPost(url);
|
||||
httpPost.setConfig(REQUEST_CONFIG);
|
||||
|
||||
try {
|
||||
/* 构建请求体 */
|
||||
if (StringUtils.isNotBlank(strParam)) {
|
||||
StringEntity entity = new StringEntity(strParam, StandardCharsets.UTF_8);
|
||||
entity.setContentEncoding(StandardCharsets.UTF_8.name());
|
||||
entity.setContentType(ContentType.APPLICATION_JSON.getMimeType());
|
||||
httpPost.setEntity(entity);
|
||||
}
|
||||
if (StringUtils.isNotBlank(token)) {
|
||||
httpPost.setHeader("token", token);
|
||||
}
|
||||
|
||||
|
||||
/* 提交请求,构建响应对象 */
|
||||
CloseableHttpResponse response = httpClient.execute(httpPost);
|
||||
|
||||
/* 处理请求结果 */
|
||||
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
|
||||
HttpEntity responseEntity = response.getEntity();
|
||||
String result = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8);
|
||||
jsonResult = JSONObject.parseObject(result);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
httpPost.releaseConnection();
|
||||
}
|
||||
return jsonResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* post请求传输String参数 例如:name=Jack&sex=1&type=2
|
||||
* Content-type:application/x-www-form-urlencoded
|
||||
*
|
||||
* @param url url地址
|
||||
* @param strParam 参数
|
||||
* @return String 请求结果对象
|
||||
*/
|
||||
public static String httpPostStr(String url, String strParam) {
|
||||
/* 请求返回结果 */
|
||||
String jsonResult = null;
|
||||
|
||||
/* 构建连接对象 */
|
||||
CloseableHttpClient httpClient = HttpClients.createDefault();
|
||||
HttpPost httpPost = new HttpPost(url);
|
||||
httpPost.setConfig(REQUEST_CONFIG);
|
||||
|
||||
try {
|
||||
/* 构建请求体 */
|
||||
if (StringUtils.isNotBlank(strParam)) {
|
||||
StringEntity entity = new StringEntity(strParam, StandardCharsets.UTF_8);
|
||||
entity.setContentEncoding(StandardCharsets.UTF_8.name());
|
||||
entity.setContentType(ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
|
||||
httpPost.setEntity(entity);
|
||||
}
|
||||
|
||||
/* 提交请求,构建响应对象 */
|
||||
CloseableHttpResponse response = httpClient.execute(httpPost);
|
||||
|
||||
/* 处理请求结果 */
|
||||
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
|
||||
HttpEntity responseEntity = response.getEntity();
|
||||
jsonResult = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
httpPost.releaseConnection();
|
||||
}
|
||||
return jsonResult;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* post请求传输String参数 例如:name=Jack&sex=1&type=2
|
||||
* Content-type:application/x-www-form-urlencoded
|
||||
*
|
||||
* @param url url地址
|
||||
* @param strParam 参数
|
||||
* @param token 身份认证字符串
|
||||
* @return String 请求结果对象
|
||||
*/
|
||||
public static String httpPostStrByToken(String url, String strParam, String token) {
|
||||
/* 请求返回结果 */
|
||||
String jsonResult = null;
|
||||
|
||||
/* 构建连接对象 */
|
||||
CloseableHttpClient httpClient = HttpClients.createDefault();
|
||||
HttpPost httpPost = new HttpPost(url);
|
||||
httpPost.setConfig(REQUEST_CONFIG);
|
||||
|
||||
try {
|
||||
/* 构建请求体 */
|
||||
if (StringUtils.isNotBlank(strParam)) {
|
||||
StringEntity entity = new StringEntity(strParam, StandardCharsets.UTF_8);
|
||||
entity.setContentEncoding(StandardCharsets.UTF_8.name());
|
||||
entity.setContentType(ContentType.APPLICATION_JSON.getMimeType());
|
||||
httpPost.setEntity(entity);
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(token)) {
|
||||
httpPost.setHeader("token", token);
|
||||
}
|
||||
|
||||
/* 提交请求,构建响应对象 */
|
||||
CloseableHttpResponse response = httpClient.execute(httpPost);
|
||||
|
||||
/* 处理请求结果 */
|
||||
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
|
||||
HttpEntity responseEntity = response.getEntity();
|
||||
jsonResult = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
httpPost.releaseConnection();
|
||||
}
|
||||
return jsonResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据指定的url地址上传文件
|
||||
*
|
||||
* @param mediaName 服务器定义的读取文件流的name
|
||||
* @param url 文件上传url
|
||||
* @param file 文件对象
|
||||
* @return JSONObject 文件上传结果
|
||||
*/
|
||||
public static JSONObject httpFileUpload(String mediaName, String url, MultipartFile file) {
|
||||
/* 请求返回结果 */
|
||||
JSONObject jsonResult = null;
|
||||
|
||||
/* 构建连接对象 */
|
||||
CloseableHttpClient httpClient = HttpClients.createDefault();
|
||||
HttpPost httpPost = new HttpPost(url);
|
||||
httpPost.setConfig(REQUEST_CONFIG);
|
||||
|
||||
/* 构建请求头 */
|
||||
String boundaryStr = UUID.randomUUID().toString();
|
||||
httpPost.setHeader(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.name());
|
||||
httpPost.setHeader(HttpHeaders.CONNECTION, "Keep-Alive");
|
||||
httpPost.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.MULTIPART_FORM_DATA.getMimeType() + ";boundary=" + boundaryStr);
|
||||
|
||||
try {
|
||||
/* 构建文件对象 */
|
||||
MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
|
||||
multipartEntityBuilder.setBoundary(boundaryStr)
|
||||
.setContentType(ContentType.APPLICATION_OCTET_STREAM)
|
||||
.setCharset(StandardCharsets.UTF_8)
|
||||
.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
|
||||
multipartEntityBuilder.addBinaryBody(mediaName, file.getInputStream(), ContentType.APPLICATION_OCTET_STREAM, file.getOriginalFilename());
|
||||
HttpEntity entity = multipartEntityBuilder.build();
|
||||
httpPost.setEntity(entity);
|
||||
|
||||
/* 提交请求,构建响应对象 */
|
||||
CloseableHttpResponse response = httpClient.execute(httpPost);
|
||||
|
||||
/* 处理请求结果 */
|
||||
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
|
||||
HttpEntity responseEntity = response.getEntity();
|
||||
String result = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8);
|
||||
jsonResult = JSONObject.parseObject(result);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
httpPost.releaseConnection();
|
||||
}
|
||||
return jsonResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送get请求
|
||||
*
|
||||
* @param url 路径
|
||||
* @return JSONObject 请求结果对象
|
||||
*/
|
||||
public static JSONObject httpGet(String url) {
|
||||
/* 请求返回结果 */
|
||||
JSONObject jsonResult = null;
|
||||
|
||||
/* 构建连接对象 */
|
||||
CloseableHttpClient httpClient = HttpClients.createDefault();
|
||||
HttpGet httpGet = new HttpGet(url);
|
||||
httpGet.setConfig(REQUEST_CONFIG);
|
||||
|
||||
try {
|
||||
/* 提交请求,构建响应对象 */
|
||||
CloseableHttpResponse response = httpClient.execute(httpGet);
|
||||
|
||||
/* 处理请求结果 */
|
||||
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
|
||||
HttpEntity responseEntity = response.getEntity();
|
||||
String result = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8);
|
||||
jsonResult = JSONObject.parseObject(result);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
httpGet.releaseConnection();
|
||||
}
|
||||
return jsonResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送get请求
|
||||
*
|
||||
* @param url 路径
|
||||
* @return JSONObject 请求结果对象
|
||||
*/
|
||||
public static String httpGetStr(String url) {
|
||||
/* 请求返回结果 */
|
||||
String jsonResult = null;
|
||||
|
||||
/* 构建连接对象 */
|
||||
CloseableHttpClient httpClient = HttpClients.createDefault();
|
||||
HttpGet httpGet = new HttpGet(url);
|
||||
httpGet.setConfig(REQUEST_CONFIG);
|
||||
|
||||
try {
|
||||
/* 提交请求,构建响应对象 */
|
||||
CloseableHttpResponse response = httpClient.execute(httpGet);
|
||||
|
||||
/* 处理请求结果 */
|
||||
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
|
||||
HttpEntity responseEntity = response.getEntity();
|
||||
jsonResult = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
httpGet.releaseConnection();
|
||||
}
|
||||
return jsonResult;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.http;
|
||||
|
||||
import cn.hutool.core.codec.Base64;
|
||||
import cn.hutool.core.map.TableMap;
|
||||
import cn.hutool.core.net.url.UrlBuilder;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* HTTP 工具类
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
public class HttpUtils {
|
||||
|
||||
/**
|
||||
* 编码 URL 参数
|
||||
*
|
||||
* @param value 参数
|
||||
* @return 编码后的参数
|
||||
*/
|
||||
public static String encodeUtf8(String value) {
|
||||
return URLEncoder.encode(value, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static String replaceUrlQuery(String url, String key, String value) {
|
||||
UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());
|
||||
// 先移除
|
||||
TableMap<CharSequence, CharSequence> query = (TableMap<CharSequence, CharSequence>)
|
||||
ReflectUtil.getFieldValue(builder.getQuery(), "query");
|
||||
query.remove(key);
|
||||
// 后添加
|
||||
builder.addQuery(key, value);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private String append(String base, Map<String, ?> query, boolean fragment) {
|
||||
return append(base, query, null, fragment);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接 URL
|
||||
*
|
||||
* copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 append 方法
|
||||
*
|
||||
* @param base 基础 URL
|
||||
* @param query 查询参数
|
||||
* @param keys query 的 key,对应的原本的 key 的映射。例如说 query 里有个 key 是 xx,实际它的 key 是 extra_xx,则通过 keys 里添加这个映射
|
||||
* @param fragment URL 的 fragment,即拼接到 # 中
|
||||
* @return 拼接后的 URL
|
||||
*/
|
||||
public static String append(String base, Map<String, ?> query, Map<String, String> keys, boolean fragment) {
|
||||
UriComponentsBuilder template = UriComponentsBuilder.newInstance();
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(base);
|
||||
URI redirectUri;
|
||||
try {
|
||||
// assume it's encoded to start with (if it came in over the wire)
|
||||
redirectUri = builder.build(true).toUri();
|
||||
} catch (Exception e) {
|
||||
// ... but allow client registrations to contain hard-coded non-encoded values
|
||||
redirectUri = builder.build().toUri();
|
||||
builder = UriComponentsBuilder.fromUri(redirectUri);
|
||||
}
|
||||
template.scheme(redirectUri.getScheme()).port(redirectUri.getPort()).host(redirectUri.getHost())
|
||||
.userInfo(redirectUri.getUserInfo()).path(redirectUri.getPath());
|
||||
|
||||
if (fragment) {
|
||||
StringBuilder values = new StringBuilder();
|
||||
if (redirectUri.getFragment() != null) {
|
||||
String append = redirectUri.getFragment();
|
||||
values.append(append);
|
||||
}
|
||||
for (String key : query.keySet()) {
|
||||
if (values.length() > 0) {
|
||||
values.append("&");
|
||||
}
|
||||
String name = key;
|
||||
if (keys != null && keys.containsKey(key)) {
|
||||
name = keys.get(key);
|
||||
}
|
||||
values.append(name).append("={").append(key).append("}");
|
||||
}
|
||||
if (values.length() > 0) {
|
||||
template.fragment(values.toString());
|
||||
}
|
||||
UriComponents encoded = template.build().expand(query).encode();
|
||||
builder.fragment(encoded.getFragment());
|
||||
} else {
|
||||
for (String key : query.keySet()) {
|
||||
String name = key;
|
||||
if (keys != null && keys.containsKey(key)) {
|
||||
name = keys.get(key);
|
||||
}
|
||||
template.queryParam(name, "{" + key + "}");
|
||||
}
|
||||
template.fragment(redirectUri.getFragment());
|
||||
UriComponents encoded = template.build().expand(query).encode();
|
||||
builder.query(encoded.getQuery());
|
||||
}
|
||||
return builder.build().toUriString();
|
||||
}
|
||||
|
||||
public static String[] obtainBasicAuthorization(HttpServletRequest request) {
|
||||
String clientId;
|
||||
String clientSecret;
|
||||
// 先从 Header 中获取
|
||||
String authorization = request.getHeader("Authorization");
|
||||
authorization = StrUtil.subAfter(authorization, "Basic ", true);
|
||||
if (StringUtils.hasText(authorization)) {
|
||||
authorization = Base64.decodeStr(authorization);
|
||||
clientId = StrUtil.subBefore(authorization, ":", false);
|
||||
clientSecret = StrUtil.subAfter(authorization, ":", false);
|
||||
// 再从 Param 中获取
|
||||
} else {
|
||||
clientId = request.getParameter("client_id");
|
||||
clientSecret = request.getParameter("client_secret");
|
||||
}
|
||||
|
||||
// 如果两者非空,则返回
|
||||
if (StrUtil.isNotEmpty(clientId) && StrUtil.isNotEmpty(clientSecret)) {
|
||||
return new String[]{clientId, clientSecret};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP post 请求,基于 {@link cn.hutool.http.HttpUtil} 实现
|
||||
*
|
||||
* 为什么要封装该方法,因为 HttpUtil 默认封装的方法,没有允许传递 headers 参数
|
||||
*
|
||||
* @param url URL
|
||||
* @param headers 请求头
|
||||
* @param requestBody 请求体
|
||||
* @return 请求结果
|
||||
*/
|
||||
public static String post(String url, Map<String, String> headers, String requestBody) {
|
||||
try (HttpResponse response = HttpRequest.post(url)
|
||||
.addHeaders(headers)
|
||||
.body(requestBody)
|
||||
.execute()) {
|
||||
return response.body();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP get 请求,基于 {@link cn.hutool.http.HttpUtil} 实现
|
||||
*
|
||||
* 为什么要封装该方法,因为 HttpUtil 默认封装的方法,没有允许传递 headers 参数
|
||||
*
|
||||
* @param url URL
|
||||
* @param headers 请求头
|
||||
* @return 请求结果
|
||||
*/
|
||||
public static String get(String url, Map<String, String> headers) {
|
||||
try (HttpResponse response = HttpRequest.get(url)
|
||||
.addHeaders(headers)
|
||||
.execute()) {
|
||||
return response.body();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.integration;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* 配置参数,控制对接 ePlat 共享服务的请求行为。
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "eplat.share")
|
||||
public class ShareServiceProperties {
|
||||
|
||||
private static final String DEFAULT_TOKEN_ENDPOINT_PATH = "/eplat/oauth/token";
|
||||
|
||||
/**
|
||||
* 共享服务基础地址,例如:https://example.com/share。
|
||||
*/
|
||||
private String urlPrefix;
|
||||
|
||||
/**
|
||||
* OAuth 客户端标识。
|
||||
*/
|
||||
private String clientId;
|
||||
|
||||
/**
|
||||
* OAuth 客户端密钥。
|
||||
*/
|
||||
private String clientSecret;
|
||||
|
||||
/**
|
||||
* OAuth scope,默认 read。
|
||||
*/
|
||||
private String scope = "read";
|
||||
|
||||
/**
|
||||
* 访问 token 在 Redis 中的缓存 key。
|
||||
*/
|
||||
private String tokenCacheKey = "eplat:cache:shareToken";
|
||||
|
||||
/**
|
||||
* 刷新 token 在 Redis 中的缓存 key。
|
||||
*/
|
||||
private String refreshTokenCacheKey = "eplat:cache:shareRefreshToken";
|
||||
|
||||
/**
|
||||
* 调用共享服务时携带 token 的请求头名称。
|
||||
*/
|
||||
private String tokenHeaderName = "Xplat-Token";
|
||||
|
||||
/**
|
||||
* 获取 token 的接口路径,默认 /eplat/oauth/token。
|
||||
*/
|
||||
private String tokenEndpointPath = DEFAULT_TOKEN_ENDPOINT_PATH;
|
||||
|
||||
/**
|
||||
* 访问 token 默认有效期,默认 5000 秒,建议略小于服务端实际过期时间。
|
||||
*/
|
||||
private Duration tokenTtl = Duration.ofSeconds(5000);
|
||||
|
||||
/**
|
||||
* 刷新 token 默认有效期,若未设置则取访问 token 的 2 倍。
|
||||
*/
|
||||
private Duration refreshTokenTtl;
|
||||
|
||||
/**
|
||||
* 构造具体服务的请求地址。
|
||||
*
|
||||
* @param serviceNo 服务号
|
||||
* @return 完整请求地址
|
||||
*/
|
||||
public String buildServiceUrl(String serviceNo) {
|
||||
return normalizeBaseUrl(urlPrefix) + "/service/" + serviceNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造获取 token 的请求地址。
|
||||
*
|
||||
* @return token 请求地址
|
||||
*/
|
||||
public String buildTokenUrl() {
|
||||
String base = normalizeBaseUrl(urlPrefix);
|
||||
String path = StrUtil.prependIfMissing(tokenEndpointPath, "/");
|
||||
return base + path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新 token 的缓存有效期。
|
||||
*
|
||||
* @return 刷新 token 有效期
|
||||
*/
|
||||
public Duration getRefreshTokenTtl() {
|
||||
if (refreshTokenTtl != null) {
|
||||
return refreshTokenTtl;
|
||||
}
|
||||
return tokenTtl.multipliedBy(2);
|
||||
}
|
||||
|
||||
private static String normalizeBaseUrl(String url) {
|
||||
if (StrUtil.isBlank(url)) {
|
||||
throw new IllegalArgumentException("共享服务地址不能为空");
|
||||
}
|
||||
return StrUtil.removeSuffix(url.trim(), "/");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.integration;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.zt.plat.framework.common.util.json.JsonUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.core.ValueOperations;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* ePlat 共享服务调用工具,负责发送请求与自动刷新访问 token。
|
||||
*/
|
||||
@Slf4j
|
||||
public final class ShareServiceUtils {
|
||||
|
||||
private static final Duration MIN_CACHE_TTL = Duration.ofSeconds(1);
|
||||
private static final ConcurrentMap<String, Lock> TOKEN_REFRESH_LOCKS = new ConcurrentHashMap<>();
|
||||
|
||||
private ShareServiceUtils() {
|
||||
}
|
||||
|
||||
public static String callShareService(RestTemplate restTemplate,
|
||||
StringRedisTemplate redisTemplate,
|
||||
ShareServiceProperties properties,
|
||||
String serviceNo,
|
||||
String requestBody) {
|
||||
return callShareService(restTemplate, redisTemplate, properties, serviceNo, (Object) requestBody);
|
||||
}
|
||||
|
||||
public static String callShareService(RestTemplate restTemplate,
|
||||
StringRedisTemplate redisTemplate,
|
||||
ShareServiceProperties properties,
|
||||
String serviceNo,
|
||||
Object requestBody) {
|
||||
Assert.notNull(restTemplate, "RestTemplate 不能为空");
|
||||
Assert.notNull(redisTemplate, "StringRedisTemplate 不能为空");
|
||||
Assert.notNull(properties, "ShareServiceProperties 不能为空");
|
||||
Assert.hasText(serviceNo, "服务号不能为空");
|
||||
|
||||
String url = properties.buildServiceUrl(serviceNo);
|
||||
String payload = convertRequestBody(requestBody);
|
||||
log.info("共享服务调用地址:[{}],请求体:[{}]", url, payload);
|
||||
|
||||
String token = obtainAccessToken(restTemplate, redisTemplate, properties);
|
||||
log.debug("共享服务服务号 [{}] 使用的 token 已获取", serviceNo);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set(properties.getTokenHeaderName(), token);
|
||||
|
||||
HttpEntity<String> entity = new HttpEntity<>(payload, headers);
|
||||
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
|
||||
return Objects.requireNonNullElse(response.getBody(), "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取共享服务的访问 token,可复用于自定义调用场景。
|
||||
*
|
||||
* @param restTemplate 用于请求共享服务的 {@link RestTemplate}
|
||||
* @param redisTemplate 缓存 token 的 {@link StringRedisTemplate}
|
||||
* @param properties 共享服务配置
|
||||
* @return 访问共享服务的 token
|
||||
*/
|
||||
public static String getAccessToken(RestTemplate restTemplate,
|
||||
StringRedisTemplate redisTemplate,
|
||||
ShareServiceProperties properties) {
|
||||
Assert.notNull(restTemplate, "RestTemplate 不能为空");
|
||||
Assert.notNull(redisTemplate, "StringRedisTemplate 不能为空");
|
||||
Assert.notNull(properties, "ShareServiceProperties 不能为空");
|
||||
return obtainAccessToken(restTemplate, redisTemplate, properties);
|
||||
}
|
||||
|
||||
private static String convertRequestBody(Object requestBody) {
|
||||
if (requestBody == null) {
|
||||
return "";
|
||||
}
|
||||
if (requestBody instanceof String str) {
|
||||
return str;
|
||||
}
|
||||
if (requestBody instanceof byte[] bytes) {
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
return JsonUtils.toJsonString(requestBody);
|
||||
}
|
||||
|
||||
private static String obtainAccessToken(RestTemplate restTemplate,
|
||||
StringRedisTemplate redisTemplate,
|
||||
ShareServiceProperties properties) {
|
||||
// 直接从 Redis 读取可复用的 token
|
||||
ValueOperations<String, String> valueOps = redisTemplate.opsForValue();
|
||||
String token = valueOps.get(properties.getTokenCacheKey());
|
||||
if (StrUtil.isNotBlank(token)) {
|
||||
return token;
|
||||
}
|
||||
// 针对同一个缓存 key 做细粒度加锁,避免并发刷新问题
|
||||
Lock lock = TOKEN_REFRESH_LOCKS.computeIfAbsent(properties.getTokenCacheKey(), key -> new ReentrantLock());
|
||||
lock.lock();
|
||||
try {
|
||||
token = valueOps.get(properties.getTokenCacheKey());
|
||||
if (StrUtil.isNotBlank(token)) {
|
||||
return token;
|
||||
}
|
||||
return refreshAccessToken(restTemplate, redisTemplate, properties, valueOps);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private static String refreshAccessToken(RestTemplate restTemplate,
|
||||
StringRedisTemplate redisTemplate,
|
||||
ShareServiceProperties properties,
|
||||
ValueOperations<String, String> valueOps) {
|
||||
String refreshToken = valueOps.get(properties.getRefreshTokenCacheKey());
|
||||
if (StrUtil.isNotBlank(refreshToken)) {
|
||||
try {
|
||||
return requestToken(restTemplate, redisTemplate, properties,
|
||||
buildRefreshTokenParams(properties, refreshToken));
|
||||
} catch (RuntimeException ex) {
|
||||
log.warn("刷新共享服务 token 失败,准备回退为 client_credentials 模式", ex);
|
||||
redisTemplate.delete(properties.getRefreshTokenCacheKey());
|
||||
}
|
||||
}
|
||||
return requestToken(restTemplate, redisTemplate, properties,
|
||||
buildClientCredentialsParams(properties));
|
||||
}
|
||||
|
||||
private static MultiValueMap<String, String> buildClientCredentialsParams(ShareServiceProperties properties) {
|
||||
MultiValueMap<String, String> params = baseTokenParams(properties);
|
||||
params.add("grant_type", "client_credentials");
|
||||
if (StrUtil.isNotBlank(properties.getScope())) {
|
||||
params.add("scope", properties.getScope());
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
private static MultiValueMap<String, String> buildRefreshTokenParams(ShareServiceProperties properties,
|
||||
String refreshToken) {
|
||||
MultiValueMap<String, String> params = baseTokenParams(properties);
|
||||
params.add("grant_type", "refresh_token");
|
||||
params.add("refresh_token", refreshToken);
|
||||
return params;
|
||||
}
|
||||
|
||||
private static MultiValueMap<String, String> baseTokenParams(ShareServiceProperties properties) {
|
||||
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
|
||||
Assert.hasText(properties.getClientId(), "clientId 不能为空");
|
||||
Assert.hasText(properties.getClientSecret(), "clientSecret 不能为空");
|
||||
params.add("client_id", properties.getClientId());
|
||||
params.add("client_secret", properties.getClientSecret());
|
||||
return params;
|
||||
}
|
||||
|
||||
private static String requestToken(RestTemplate restTemplate,
|
||||
StringRedisTemplate redisTemplate,
|
||||
ShareServiceProperties properties,
|
||||
MultiValueMap<String, String> body) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(body, headers);
|
||||
String tokenUrl = properties.buildTokenUrl();
|
||||
log.info("共享服务获取 token 地址:[{}],授权方式:[{}]", tokenUrl, body.getFirst("grant_type"));
|
||||
ResponseEntity<String> response;
|
||||
try {
|
||||
response = restTemplate.postForEntity(tokenUrl, entity, String.class);
|
||||
} catch (RestClientException ex) {
|
||||
throw new IllegalStateException("请求共享服务 token 失败", ex);
|
||||
}
|
||||
String responseBody = response.getBody();
|
||||
if (StrUtil.isBlank(responseBody)) {
|
||||
throw new IllegalStateException("共享服务返回的 token 内容为空");
|
||||
}
|
||||
TokenResponse tokenResponse = parseTokenResponse(responseBody);
|
||||
cacheTokens(redisTemplate, properties, tokenResponse);
|
||||
return tokenResponse.accessToken();
|
||||
}
|
||||
|
||||
private static TokenResponse parseTokenResponse(String body) {
|
||||
var node = JsonUtils.parseTree(body);
|
||||
String accessToken = node.path("access_token").asText(null);
|
||||
if (StrUtil.isBlank(accessToken)) {
|
||||
throw new IllegalStateException("共享服务返回结果缺少 access_token 字段");
|
||||
}
|
||||
String refreshToken = node.path("refresh_token").asText(null);
|
||||
long expiresIn = node.path("expires_in").asLong(-1);
|
||||
long refreshExpiresIn = node.path("refresh_expires_in").asLong(-1);
|
||||
return new TokenResponse(accessToken, refreshToken, expiresIn, refreshExpiresIn);
|
||||
}
|
||||
|
||||
private static void cacheTokens(StringRedisTemplate redisTemplate,
|
||||
ShareServiceProperties properties,
|
||||
TokenResponse tokenResponse) {
|
||||
// 将最新的 token 与刷新 token 写回缓存
|
||||
ValueOperations<String, String> valueOps = redisTemplate.opsForValue();
|
||||
Duration tokenTtl = resolveTtl(tokenResponse.expiresIn(), properties.getTokenTtl());
|
||||
valueOps.set(properties.getTokenCacheKey(), tokenResponse.accessToken(), tokenTtl);
|
||||
if (StrUtil.isNotBlank(tokenResponse.refreshToken())) {
|
||||
Duration refreshTtl = resolveTtl(tokenResponse.refreshExpiresIn(), properties.getRefreshTokenTtl());
|
||||
valueOps.set(properties.getRefreshTokenCacheKey(), tokenResponse.refreshToken(), refreshTtl);
|
||||
}
|
||||
}
|
||||
|
||||
private static Duration resolveTtl(long expiresInSeconds, Duration fallback) {
|
||||
Duration effectiveFallback = fallback;
|
||||
if (effectiveFallback == null || effectiveFallback.compareTo(MIN_CACHE_TTL) < 0) {
|
||||
effectiveFallback = Duration.ofMinutes(5);
|
||||
}
|
||||
if (expiresInSeconds > 0) {
|
||||
Duration candidate = Duration.ofSeconds(expiresInSeconds);
|
||||
if (candidate.compareTo(MIN_CACHE_TTL) < 0) {
|
||||
candidate = MIN_CACHE_TTL;
|
||||
}
|
||||
return candidate.compareTo(effectiveFallback) < 0 ? candidate : effectiveFallback;
|
||||
}
|
||||
return effectiveFallback;
|
||||
}
|
||||
|
||||
private record TokenResponse(String accessToken, String refreshToken, long expiresIn, long refreshExpiresIn) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.io;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 文件工具类
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
public class FileUtils {
|
||||
|
||||
/**
|
||||
* 创建临时文件
|
||||
* 该文件会在 JVM 退出时,进行删除
|
||||
*
|
||||
* @param data 文件内容
|
||||
* @return 文件
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static File createTempFile(String data) {
|
||||
File file = createTempFile();
|
||||
// 写入内容
|
||||
FileUtil.writeUtf8String(data, file);
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建临时文件
|
||||
* 该文件会在 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.io;
|
||||
|
||||
import cn.hutool.core.io.IORuntimeException;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* IO 工具类,用于 {@link cn.hutool.core.io.IoUtil} 缺失的方法
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
public class IoUtils {
|
||||
|
||||
/**
|
||||
* 从流中读取 UTF8 编码的内容
|
||||
*
|
||||
* @param in 输入流
|
||||
* @param isClose 是否关闭
|
||||
* @return 内容
|
||||
* @throws IORuntimeException IO 异常
|
||||
*/
|
||||
public static String readUtf8(InputStream in, boolean isClose) throws IORuntimeException {
|
||||
return StrUtil.utf8Str(IoUtil.read(in, isClose));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.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 ZT
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断字符串是否为 JSON 类型的字符串
|
||||
* @param str 字符串
|
||||
*/
|
||||
public static boolean isJsonObject(String str) {
|
||||
return JSONUtil.isTypeJSONObject(str);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.json.databind;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Long / long 数组序列化器,统一按字符串输出以规避 JS 精度问题。
|
||||
*/
|
||||
public class LongArraySerializer extends StdSerializer<Object> {
|
||||
|
||||
public static final LongArraySerializer INSTANCE = new LongArraySerializer();
|
||||
|
||||
private LongArraySerializer() {
|
||||
super(Object.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||
gen.writeStartArray();
|
||||
if (value instanceof long[]) {
|
||||
long[] array = (long[]) value;
|
||||
for (long element : array) {
|
||||
// 原生 long 必定有值,直接走 NumberSerializer
|
||||
NumberSerializer.INSTANCE.serialize(element, gen, provider);
|
||||
}
|
||||
gen.writeEndArray();
|
||||
return;
|
||||
}
|
||||
|
||||
Long[] array = (Long[]) value;
|
||||
for (Long element : array) {
|
||||
if (element == null) {
|
||||
provider.defaultSerializeNull(gen);
|
||||
continue;
|
||||
}
|
||||
NumberSerializer.INSTANCE.serialize(element, gen, provider);
|
||||
}
|
||||
gen.writeEndArray();
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.json.databind;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 将 {@link Collection} 中的 Long 元素序列化成字符串,避免 JavaScript 精度问题。
|
||||
*/
|
||||
public class LongCollectionSerializer extends StdSerializer<Collection<?>> {
|
||||
|
||||
public static final LongCollectionSerializer INSTANCE = new LongCollectionSerializer();
|
||||
|
||||
private LongCollectionSerializer() {
|
||||
super(TypeFactory.defaultInstance().constructCollectionType(Collection.class, Object.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(Collection<?> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||
// 传入集合本身与元素数量,方便 Jackson 合理推测数组边界
|
||||
gen.writeStartArray(value, value.size());
|
||||
for (Object element : value) {
|
||||
if (element == null) {
|
||||
// 允许集合中存在 null,保持 Jackson 默认的 null 序列化行为
|
||||
provider.defaultSerializeNull(gen);
|
||||
continue;
|
||||
}
|
||||
// 所有 Long/long 元素统一走 NumberSerializer,保证前端精度
|
||||
NumberSerializer.INSTANCE.serialize((Number) element, gen, provider);
|
||||
}
|
||||
gen.writeEndArray();
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.json.databind;
|
||||
|
||||
import com.fasterxml.jackson.databind.BeanDescription;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializationConfig;
|
||||
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
|
||||
import com.fasterxml.jackson.databind.type.ArrayType;
|
||||
import com.fasterxml.jackson.databind.type.CollectionType;
|
||||
import com.fasterxml.jackson.databind.type.CollectionLikeType;
|
||||
import com.fasterxml.jackson.databind.JavaType;
|
||||
|
||||
/**
|
||||
* 针对 Long 相关集合、数组的序列化增强,确保统一走 Long 的自定义序列化逻辑。
|
||||
*/
|
||||
public class LongTypeSerializerModifier extends BeanSerializerModifier {
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> modifyCollectionSerializer(SerializationConfig config, CollectionType valueType,
|
||||
BeanDescription beanDesc, JsonSerializer<?> serializer) {
|
||||
// List、Set 等容器若包含 Long,则切换到 LongCollectionSerializer
|
||||
return needsLongCollectionSerializer(valueType.getContentType()) ? LongCollectionSerializer.INSTANCE : serializer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> modifyCollectionLikeSerializer(SerializationConfig config, CollectionLikeType valueType,
|
||||
BeanDescription beanDesc, JsonSerializer<?> serializer) {
|
||||
// 处理 CollectionLike(如 Page、Optional 等)中的 Long 元素
|
||||
return needsLongCollectionSerializer(valueType.getContentType()) ? LongCollectionSerializer.INSTANCE : serializer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> modifyArraySerializer(SerializationConfig config, ArrayType valueType,
|
||||
BeanDescription beanDesc, JsonSerializer<?> serializer) {
|
||||
// 针对 long[]、Long[] 两种数组使用统一的数组序列化器
|
||||
Class<?> rawClass = valueType.getRawClass();
|
||||
if (long[].class.equals(rawClass)) {
|
||||
return LongArraySerializer.INSTANCE;
|
||||
}
|
||||
if (Long[].class.equals(rawClass)) {
|
||||
return LongArraySerializer.INSTANCE;
|
||||
}
|
||||
return serializer;
|
||||
}
|
||||
|
||||
private boolean needsLongCollectionSerializer(JavaType contentType) {
|
||||
if (contentType == null) {
|
||||
return false;
|
||||
}
|
||||
Class<?> rawClass = contentType.getRawClass();
|
||||
return Long.class.equals(rawClass) || Long.TYPE.equals(rawClass);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.monitor;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.skywalking.apm.toolkit.trace.TraceContext;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 链路追踪工具类
|
||||
*
|
||||
* 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 util 包下
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
public final class TracerUtils {
|
||||
|
||||
/**
|
||||
* SkyWalking 在未接入 Agent 时返回的默认占位值
|
||||
*/
|
||||
private static final String SKY_WALKING_PLACEHOLDER = "N/A";
|
||||
|
||||
/**
|
||||
* SkyWalking 在忽略追踪时返回的占位值
|
||||
*/
|
||||
private static final String SKY_WALKING_IGNORED = "Ignored_Trace";
|
||||
|
||||
private static final String MDC_TRACE_ID_KEY = "traceId";
|
||||
private static final String REQUEST_ATTRIBUTE_KEY = TracerUtils.class.getName() + ".TRACE_ID";
|
||||
private static final String[] HEADER_CANDIDATES = {
|
||||
"trace-id",
|
||||
"Trace-Id",
|
||||
"x-trace-id",
|
||||
"X-Trace-Id",
|
||||
"x-request-id",
|
||||
"X-Request-Id"
|
||||
};
|
||||
|
||||
/**
|
||||
* 兜底的 traceId,保证在未接入链路追踪时依旧具备追踪能力
|
||||
*/
|
||||
private static final InheritableThreadLocal<String> FALLBACK_TRACE_ID = new InheritableThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 私有化构造方法
|
||||
*/
|
||||
private TracerUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得链路追踪编号。
|
||||
* <p>
|
||||
* 优先返回 SkyWalking 的 TraceId;在缺少链路上下文或者未接入 SkyWalking 时,会优先复用来自请求上下文的 TraceId,
|
||||
* 否则生成一个新的兜底 TraceId,并在当前线程、请求上下文与日志 MDC 中缓存,确保后续组件能够复用。
|
||||
*
|
||||
* @return 链路追踪编号
|
||||
*/
|
||||
public static String getTraceId() {
|
||||
String traceId = TraceContext.traceId();
|
||||
if (isValidTraceId(traceId)) {
|
||||
cacheTraceId(traceId);
|
||||
return traceId;
|
||||
}
|
||||
String cached = resolveCachedTraceId();
|
||||
if (StringUtils.isNotBlank(cached)) {
|
||||
return cached;
|
||||
}
|
||||
String generated = generateFallbackTraceId();
|
||||
cacheTraceId(generated);
|
||||
return generated;
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动绑定外部传入的 TraceId,例如消费消息、处理异步任务时。
|
||||
*
|
||||
* @param traceId 链路编号
|
||||
*/
|
||||
public static void bindTraceId(String traceId) {
|
||||
if (StringUtils.isBlank(traceId)) {
|
||||
return;
|
||||
}
|
||||
cacheTraceId(traceId.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理当前线程关联的兜底 traceId,避免线程复用导致污染。
|
||||
*/
|
||||
public static void clear() {
|
||||
FALLBACK_TRACE_ID.remove();
|
||||
MDC.remove(MDC_TRACE_ID_KEY);
|
||||
HttpServletRequest request = currentRequest();
|
||||
if (request != null) {
|
||||
request.removeAttribute(REQUEST_ATTRIBUTE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isValidTraceId(String traceId) {
|
||||
if (StringUtils.isBlank(traceId)) {
|
||||
return false;
|
||||
}
|
||||
if (StringUtils.equalsIgnoreCase(traceId, SKY_WALKING_PLACEHOLDER)) {
|
||||
return false;
|
||||
}
|
||||
return !StringUtils.equalsIgnoreCase(traceId, SKY_WALKING_IGNORED);
|
||||
}
|
||||
|
||||
private static String resolveCachedTraceId() {
|
||||
String cached = FALLBACK_TRACE_ID.get();
|
||||
if (StringUtils.isNotBlank(cached)) {
|
||||
return cached;
|
||||
}
|
||||
HttpServletRequest request = currentRequest();
|
||||
if (request != null) {
|
||||
Object attribute = request.getAttribute(REQUEST_ATTRIBUTE_KEY);
|
||||
if (attribute instanceof String attrValue && StringUtils.isNotBlank(attrValue)) {
|
||||
cacheTraceId(attrValue);
|
||||
return attrValue;
|
||||
}
|
||||
String headerValue = resolveTraceIdFromHeader(request);
|
||||
if (StringUtils.isNotBlank(headerValue)) {
|
||||
cacheTraceId(headerValue);
|
||||
return headerValue;
|
||||
}
|
||||
}
|
||||
String mdcTraceId = MDC.get(MDC_TRACE_ID_KEY);
|
||||
if (StringUtils.isNotBlank(mdcTraceId)) {
|
||||
cacheTraceId(mdcTraceId);
|
||||
return mdcTraceId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void cacheTraceId(String traceId) {
|
||||
if (StringUtils.isBlank(traceId)) {
|
||||
return;
|
||||
}
|
||||
String trimmed = traceId.trim();
|
||||
FALLBACK_TRACE_ID.set(trimmed);
|
||||
MDC.put(MDC_TRACE_ID_KEY, trimmed);
|
||||
HttpServletRequest request = currentRequest();
|
||||
if (request != null) {
|
||||
request.setAttribute(REQUEST_ATTRIBUTE_KEY, trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpServletRequest currentRequest() {
|
||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
if (requestAttributes instanceof ServletRequestAttributes servletRequestAttributes) {
|
||||
return servletRequestAttributes.getRequest();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String resolveTraceIdFromHeader(HttpServletRequest request) {
|
||||
for (String header : HEADER_CANDIDATES) {
|
||||
String value = request.getHeader(header);
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String generateFallbackTraceId() {
|
||||
return StringUtils.replace(UUID.randomUUID().toString(), "-", "");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.number;
|
||||
|
||||
import cn.hutool.core.math.Money;
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
/**
|
||||
* 金额工具类
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.number;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 数字的工具类,补全 {@link cn.hutool.core.util.NumberUtil} 的功能
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
public static boolean isAllNumber(List<String> values) {
|
||||
if (CollUtil.isEmpty(values)) {
|
||||
return false;
|
||||
}
|
||||
for (String value : values) {
|
||||
if (!NumberUtil.isNumber(value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过经纬度获取地球上两点之间的距离
|
||||
*
|
||||
* 参考 <<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 * 10000d) / 10000d;
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.object;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.zt.plat.framework.common.pojo.PageResult;
|
||||
import com.zt.plat.framework.common.util.collection.CollectionUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Bean 工具类
|
||||
*
|
||||
* 1. 默认使用 {@link cn.hutool.core.bean.BeanUtil} 作为实现类,虽然不同 bean 工具的性能有差别,但是对绝大多数同学的项目,不用在意这点性能
|
||||
* 2. 针对复杂的对象转换,可以搜参考 AuthConvert 实现,通过 mapstruct + default 配合实现
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
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 <S, T> Set<T> toBean(Set<S> source, Class<T> targetType) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
return CollectionUtils.convertSet(source, s -> toBean(s, targetType));
|
||||
}
|
||||
|
||||
public static <S, T> Set<T> toBean(Set<S> source, Class<T> targetType, Consumer<T> peek) {
|
||||
Set<T> set = toBean(source, targetType);
|
||||
if (set != null) {
|
||||
set.forEach(peek);
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
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 (list == null) {
|
||||
list = Collections.emptyList();
|
||||
}
|
||||
if (peek != null) {
|
||||
list.forEach(peek);
|
||||
}
|
||||
return new PageResult<>(list, source.getTotal(), source.getSummary());
|
||||
}
|
||||
|
||||
public static void copyProperties(Object source, Object target) {
|
||||
if (source == null || target == null) {
|
||||
return;
|
||||
}
|
||||
BeanUtil.copyProperties(source, target, false);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package com.zt.plat.framework.common.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 ZT
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.object;
|
||||
|
||||
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 com.zt.plat.framework.common.pojo.PageParam;
|
||||
import com.zt.plat.framework.common.pojo.SortablePageParam;
|
||||
import com.zt.plat.framework.common.pojo.SortingField;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
|
||||
/**
|
||||
* {@link com.zt.plat.framework.common.pojo.PageParam} 工具类
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.security;
|
||||
|
||||
import cn.hutool.crypto.SecureUtil;
|
||||
import cn.hutool.crypto.symmetric.SM4;
|
||||
import com.zt.plat.framework.common.util.json.JsonUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 通用的签名、加解密工具类
|
||||
*/
|
||||
@Slf4j
|
||||
public final class CryptoSignatureUtils {
|
||||
|
||||
public static final String ENCRYPT_TYPE_AES = "AES";
|
||||
public static final String ENCRYPT_TYPE_DES = "DES";
|
||||
public static final String SIGNATURE_TYPE_MD5 = "MD5";
|
||||
public static final String SIGNATURE_TYPE_SHA256 = "SHA256";
|
||||
|
||||
private static final String AES_TRANSFORMATION = "AES/ECB/PKCS5Padding";
|
||||
public static final String SIGNATURE_FIELD = "signature";
|
||||
|
||||
private static final String CHARSET = "UTF-8";
|
||||
|
||||
//@Value("${sa.encrypt.sm4.key}")
|
||||
private static String SM4_KEY = "1234567890123456";
|
||||
|
||||
static {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
private CryptoSignatureUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 AES 密钥(SecretKeySpec)
|
||||
*
|
||||
* @param password 密钥字符串
|
||||
* @return SecretKeySpec
|
||||
*/
|
||||
public static SecretKeySpec getSecretKey(String password) {
|
||||
try {
|
||||
KeyGenerator kg = KeyGenerator.getInstance("AES");
|
||||
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
|
||||
random.setSeed(password.getBytes(StandardCharsets.UTF_8));
|
||||
kg.init(128, random);
|
||||
SecretKey secretKey = kg.generateKey();
|
||||
return new SecretKeySpec(secretKey.getEncoded(), "AES");
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
throw new IllegalStateException("Failed to generate AES secret key", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对称加密(Base64 格式输出)
|
||||
*
|
||||
* @param plaintext 明文内容
|
||||
* @param key 密钥
|
||||
* @param type 加密类型,支持 AES、DES
|
||||
* @return 密文(Base64 格式)
|
||||
*/
|
||||
public static String encrypt(String plaintext, String key, String type) {
|
||||
if (ENCRYPT_TYPE_AES.equalsIgnoreCase(type)) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(key));
|
||||
byte[] result = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(result);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("Failed to encrypt using AES", ex);
|
||||
}
|
||||
} else if (ENCRYPT_TYPE_DES.equalsIgnoreCase(type)) {
|
||||
byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] desKey = new byte[8];
|
||||
System.arraycopy(keyBytes, 0, desKey, 0, Math.min(keyBytes.length, desKey.length));
|
||||
byte[] encrypted = SecureUtil.des(desKey).encrypt(plaintext.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(encrypted);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported encryption type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对称解密(输入为 Base64 格式密文)
|
||||
*
|
||||
* @param ciphertext 密文内容(Base64 格式)
|
||||
* @param key 密钥
|
||||
* @param type 加密类型,支持 AES、DES
|
||||
* @return 明文内容
|
||||
*/
|
||||
public static String decrypt(String ciphertext, String key, String type) {
|
||||
if (ciphertext == null) {
|
||||
return null;
|
||||
}
|
||||
if (ENCRYPT_TYPE_AES.equalsIgnoreCase(type)) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION);
|
||||
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(key));
|
||||
byte[] decoded = decodeBase64Ciphertext(ciphertext);
|
||||
byte[] result = cipher.doFinal(decoded);
|
||||
return new String(result, StandardCharsets.UTF_8);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("Failed to decrypt using AES", ex);
|
||||
}
|
||||
} else if (ENCRYPT_TYPE_DES.equalsIgnoreCase(type)) {
|
||||
byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] desKey = new byte[8];
|
||||
System.arraycopy(keyBytes, 0, desKey, 0, Math.min(keyBytes.length, desKey.length));
|
||||
byte[] decoded = decodeBase64Ciphertext(ciphertext);
|
||||
byte[] decrypted = SecureUtil.des(desKey).decrypt(decoded);
|
||||
return new String(decrypted, StandardCharsets.UTF_8);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported encryption type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证请求签名
|
||||
*
|
||||
* @param reqMap 请求参数 Map
|
||||
* @param type 签名算法类型,支持 MD5、SHA256
|
||||
* @return 签名是否有效
|
||||
*/
|
||||
public static boolean verifySignature(Map<String, Object> reqMap, String type) {
|
||||
Map<String, Object> sortedMap = new TreeMap<>(reqMap);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Map.Entry<String, Object> entry : sortedMap.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
if (SIGNATURE_FIELD.equals(key) || value == null) {
|
||||
continue;
|
||||
}
|
||||
sb.append(key).append('=');
|
||||
if (value instanceof String || value instanceof Number || value instanceof Boolean) {
|
||||
sb.append(value);
|
||||
} else {
|
||||
sb.append(JsonUtils.toJsonString(value));
|
||||
}
|
||||
sb.append('&');
|
||||
}
|
||||
if (sb.length() > 0) {
|
||||
sb.deleteCharAt(sb.length() - 1);
|
||||
}
|
||||
String provided = (String) reqMap.get(SIGNATURE_FIELD);
|
||||
if (provided == null) {
|
||||
return false;
|
||||
}
|
||||
String computed;
|
||||
log.info("原始签名串:{}", sb);
|
||||
if (SIGNATURE_TYPE_MD5.equalsIgnoreCase(type)) {
|
||||
computed = SecureUtil.md5(sb.toString());
|
||||
} else if (SIGNATURE_TYPE_SHA256.equalsIgnoreCase(type)) {
|
||||
computed = SecureUtil.sha256(sb.toString());
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported signature type: " + type);
|
||||
}
|
||||
log.info("原始签名:{}", computed);
|
||||
log.info("请求签名:{}", provided);
|
||||
return provided.equalsIgnoreCase(computed);
|
||||
}
|
||||
|
||||
private static byte[] decodeBase64Ciphertext(String ciphertext) {
|
||||
IllegalArgumentException last = null;
|
||||
for (String candidate : buildBase64Candidates(ciphertext)) {
|
||||
if (candidate == null || candidate.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
return Base64.getDecoder().decode(candidate);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
last = ex;
|
||||
}
|
||||
}
|
||||
throw last != null ? last : new IllegalArgumentException("Invalid Base64 content");
|
||||
}
|
||||
|
||||
private static Set<String> buildBase64Candidates(String ciphertext) {
|
||||
Set<String> candidates = new LinkedHashSet<>();
|
||||
if (ciphertext == null) {
|
||||
return candidates;
|
||||
}
|
||||
String trimmed = ciphertext.trim();
|
||||
candidates.add(trimmed);
|
||||
|
||||
String withoutWhitespace = stripWhitespace(trimmed);
|
||||
candidates.add(withoutWhitespace);
|
||||
|
||||
if (trimmed.indexOf(' ') >= 0) {
|
||||
String restoredPlus = trimmed.replace(' ', '+');
|
||||
candidates.add(restoredPlus);
|
||||
candidates.add(stripWhitespace(restoredPlus));
|
||||
}
|
||||
|
||||
String urlNormalised = withoutWhitespace
|
||||
.replace('-', '+')
|
||||
.replace('_', '/');
|
||||
candidates.add(urlNormalised);
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private static String stripWhitespace(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
boolean hasWhitespace = false;
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
if (Character.isWhitespace(value.charAt(i))) {
|
||||
hasWhitespace = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasWhitespace) {
|
||||
return value;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder(value.length());
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
char ch = value.charAt(i);
|
||||
if (!Character.isWhitespace(ch)) {
|
||||
sb.append(ch);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
//-------------------------------- 国密方式2 -------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 加密
|
||||
*/
|
||||
public static String enCode(String data) {
|
||||
try {
|
||||
// 第一步: SM4 加密
|
||||
SM4 sm4 = new SM4(hexToBytes(stringToHex(SM4_KEY)));
|
||||
String encryptHex = sm4.encryptHex(data);
|
||||
|
||||
// 第二步: Base64 编码
|
||||
return new String(Base64.getEncoder().encode(encryptHex.getBytes(CHARSET)), CHARSET);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("国密加密失败{}",e.getMessage(),e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密
|
||||
*/
|
||||
public static String deCode(String data) {
|
||||
try {
|
||||
|
||||
// 第一步: Base64 解码
|
||||
byte[] base64Decode = Base64.getDecoder().decode(data);
|
||||
|
||||
// 第二步: SM4 解密
|
||||
SM4 sm4 = new SM4(hexToBytes(stringToHex(SM4_KEY)));
|
||||
return sm4.decryptStr(new String(base64Decode));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("国密解密失败{}",e.getMessage(),e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static String stringToHex(String input) {
|
||||
char[] chars = input.toCharArray();
|
||||
StringBuilder hex = new StringBuilder();
|
||||
for (char c : chars) {
|
||||
hex.append(Integer.toHexString((int) c));
|
||||
}
|
||||
return hex.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 16 进制串转字节数组
|
||||
*
|
||||
* @param hex 16进制字符串
|
||||
* @return byte数组
|
||||
*/
|
||||
public static byte[] hexToBytes(String hex) {
|
||||
int length = hex.length();
|
||||
byte[] result;
|
||||
if (length % 2 == 1) {
|
||||
length++;
|
||||
result = new byte[(length / 2)];
|
||||
hex = "0" + hex;
|
||||
} else {
|
||||
result = new byte[(length / 2)];
|
||||
}
|
||||
int j = 0;
|
||||
for (int i = 0; i < length; i += 2) {
|
||||
result[j] = hexToByte(hex.substring(i, i + 2));
|
||||
j++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 16 进制字符转字节
|
||||
*
|
||||
* @param hex 16进制字符 0x00到0xFF
|
||||
* @return byte
|
||||
*/
|
||||
private static byte hexToByte(String hex) {
|
||||
return (byte) Integer.parseInt(hex, 16);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.security;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Key;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* 3DES加密解密工具类
|
||||
* 注意:如果websphere下报错:Could not find class 'com.sun.crypto.provider.SunJCE'
|
||||
* 解决方法如下: 下载sunjce_provider.jar放到jdk目录\jre\lib\ext里即可解决
|
||||
*
|
||||
* @author apple
|
||||
*/
|
||||
|
||||
public class DESUtil {
|
||||
|
||||
/**
|
||||
* 默认的密钥
|
||||
*/
|
||||
private static final String strDefaultKey = "cymco-20160329000000000000000ABCGHDYFYSSPPOEWWWDDDSSXX-cymco";
|
||||
|
||||
private Cipher encryptCipher = null;
|
||||
|
||||
private Cipher decryptCipher = null;
|
||||
|
||||
/**
|
||||
* 将byte数组转换为表示16进制值的字符串, 如:byte[]{8,18}转换为:0813, 和public static byte[]
|
||||
* hexStr2ByteArr(String strIn) 互为可逆的转换过程
|
||||
* @param arrB 需要转换的byte数组
|
||||
* @return 转换后的字符串
|
||||
* @throws Exception 本方法不处理任何异常,所有异常全部抛出
|
||||
*/
|
||||
public static String byteArr2HexStr(byte[] arrB) throws Exception {
|
||||
int iLen = arrB.length;
|
||||
// 每个byte用两个字符才能表示,所以字符串的长度是数组长度的两倍
|
||||
StringBuffer sb = new StringBuffer(iLen * 2);
|
||||
for (int i = 0; i < iLen; i++) {
|
||||
int intTmp = arrB[i];
|
||||
// 把负数转换为正数
|
||||
while (intTmp < 0) {
|
||||
intTmp = intTmp + 256;
|
||||
}
|
||||
// 小于0F的数需要在前面补0
|
||||
if (intTmp < 16) {
|
||||
sb.append("0");
|
||||
}
|
||||
sb.append(Integer.toString(intTmp, 16));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将表示16进制值的字符串转换为byte数组, 和public static String byteArr2HexStr(byte[] arrB)
|
||||
* 互为可逆的转换过程
|
||||
* @param strIn 需要转换的字符串
|
||||
* @return 转换后的byte数组
|
||||
* @throws Exception 本方法不处理任何异常,所有异常全部抛出
|
||||
*/
|
||||
public static byte[] hexStr2ByteArr(String strIn) throws Exception {
|
||||
byte[] arrB = strIn.getBytes(StandardCharsets.UTF_8);
|
||||
int iLen = arrB.length;
|
||||
|
||||
// 两个字符表示一个字节,所以字节数组长度是字符串长度除以2
|
||||
byte[] arrOut = new byte[iLen / 2];
|
||||
for (int i = 0; i < iLen; i = i + 2) {
|
||||
String strTmp = new String(arrB, i, 2);
|
||||
arrOut[i / 2] = (byte) Integer.parseInt(strTmp, 16);
|
||||
}
|
||||
return arrOut;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认构造方法,使用默认密钥
|
||||
* @throws Exception
|
||||
*/
|
||||
public DESUtil() throws Exception {
|
||||
this(strDefaultKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定密钥构造方法
|
||||
* @param strKey 指定的密钥
|
||||
* @throws Exception
|
||||
*/
|
||||
public DESUtil(String strKey) throws Exception {
|
||||
//Security.addProvider(new com.sun.crypto.provider.SunJCE());
|
||||
Key key = getKey(strKey.getBytes());
|
||||
|
||||
encryptCipher = Cipher.getInstance("DES");
|
||||
encryptCipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
|
||||
decryptCipher = Cipher.getInstance("DES");
|
||||
decryptCipher.init(Cipher.DECRYPT_MODE, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密字节数组
|
||||
* @param arrB 需加密的字节数组
|
||||
* @return 加密后的字节数组
|
||||
* @throws Exception
|
||||
*/
|
||||
public byte[] encrypt(byte[] arrB) throws Exception {
|
||||
return encryptCipher.doFinal(arrB);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密字符串
|
||||
* @param strIn 需加密的字符串
|
||||
* @return 加密后的字符串
|
||||
* @throws Exception
|
||||
*/
|
||||
public String encrypt(String strIn) throws Exception {
|
||||
return byteArr2HexStr(encrypt(strIn.getBytes()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密字节数组
|
||||
* @param arrB 需解密的字节数组
|
||||
* @return 解密后的字节数组
|
||||
* @throws Exception
|
||||
*/
|
||||
public byte[] decrypt(byte[] arrB) throws Exception {
|
||||
return decryptCipher.doFinal(arrB);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密字符串
|
||||
* @param strIn 需解密的字符串
|
||||
* @return 解密后的字符串
|
||||
* @throws Exception
|
||||
*/
|
||||
public String decrypt(String strIn) throws Exception {
|
||||
return new String(decrypt(hexStr2ByteArr(strIn)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从指定字符串生成密钥,密钥所需的字节数组长度为8位 不足8位时后面补0,超出8位只取前8位
|
||||
* @param arrBTmp 构成该字符串的字节数组
|
||||
* @return 生成的密钥
|
||||
* @throws Exception
|
||||
*/
|
||||
private Key getKey(byte[] arrBTmp) throws Exception {
|
||||
// 创建一个空的8位字节数组(默认值为0)
|
||||
byte[] arrB = new byte[8];
|
||||
|
||||
// 将原始字节数组转换为8位
|
||||
for (int i = 0; i < arrBTmp.length && i < arrB.length; i++) {
|
||||
arrB[i] = arrBTmp[i];
|
||||
}
|
||||
|
||||
// 生成密钥
|
||||
Key key = new javax.crypto.spec.SecretKeySpec(arrB, "DES");
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
public static String SHA1(String decript) throws NoSuchAlgorithmException {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
digest.update(decript.getBytes());
|
||||
byte messageDigest[] = digest.digest();
|
||||
// Create Hex String
|
||||
StringBuffer hexString = new StringBuffer();
|
||||
// 字节数组转换为 十六进制 数
|
||||
for (int i = 0; i < messageDigest.length; i++) {
|
||||
String shaHex = Integer.toHexString(messageDigest[i] & 0xFF);
|
||||
if (shaHex.length() < 2) {
|
||||
hexString.append(0);
|
||||
}
|
||||
hexString.append(shaHex);
|
||||
}
|
||||
return hexString.toString();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static void main(String [] args){
|
||||
DESUtil des;
|
||||
try {
|
||||
des = new DESUtil();
|
||||
String en= des.encrypt("fls123,12121");
|
||||
System.out.println(en);
|
||||
String de = des.decrypt(en);
|
||||
System.out.println(de);
|
||||
} catch (Exception e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package com.zt.plat.framework.common.util.servlet;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.servlet.JakartaServletUtil;
|
||||
import com.zt.plat.framework.common.util.json.JsonUtils;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 客户端工具类
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
public class ServletUtils {
|
||||
|
||||
/**
|
||||
* 返回 JSON 字符串
|
||||
*
|
||||
* @param response 响应
|
||||
* @param object 对象,会序列化成 JSON 字符串
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码
|
||||
public static void writeJSON(HttpServletResponse response, Object object) {
|
||||
String content = JsonUtils.toJsonString(object);
|
||||
JakartaServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param request 请求
|
||||
* @return ua
|
||||
*/
|
||||
public static String getUserAgent(HttpServletRequest request) {
|
||||
String ua = request.getHeader("User-Agent");
|
||||
return ua != null ? ua : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得请求
|
||||
*
|
||||
* @return HttpServletRequest
|
||||
*/
|
||||
public static HttpServletRequest getRequest() {
|
||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
if (!(requestAttributes instanceof ServletRequestAttributes)) {
|
||||
return null;
|
||||
}
|
||||
return ((ServletRequestAttributes) requestAttributes).getRequest();
|
||||
}
|
||||
|
||||
public static String getUserAgent() {
|
||||
HttpServletRequest request = getRequest();
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
return getUserAgent(request);
|
||||
}
|
||||
|
||||
public static String getClientIP() {
|
||||
HttpServletRequest request = getRequest();
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
return JakartaServletUtil.getClientIP(request);
|
||||
}
|
||||
|
||||
public static boolean isJsonRequest(ServletRequest request) {
|
||||
return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE);
|
||||
}
|
||||
|
||||
public static String getBody(HttpServletRequest request) {
|
||||
// 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取
|
||||
if (isJsonRequest(request)) {
|
||||
return JakartaServletUtil.getBody(request);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static byte[] getBodyBytes(HttpServletRequest request) {
|
||||
// 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取
|
||||
if (isJsonRequest(request)) {
|
||||
return JakartaServletUtil.getBodyBytes(request);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String getClientIP(HttpServletRequest request) {
|
||||
return JakartaServletUtil.getClientIP(request);
|
||||
}
|
||||
|
||||
public static Map<String, String> getParamMap(HttpServletRequest request) {
|
||||
return JakartaServletUtil.getParamMap(request);
|
||||
}
|
||||
|
||||
public static Map<String, String> getHeaderMap(HttpServletRequest request) {
|
||||
return JakartaServletUtil.getHeaderMap(request);
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user