Compare commits

...

50 Commits

Author SHA1 Message Date
FCL
8ce0a52046 Merge branch 'refs/heads/zt-test' into test 2026-02-04 11:25:07 +08:00
ranke
da77f92523 Merge branch 'dev-klw' into test
* dev-klw:
  清理与ztcloud中重复的代码,改为 jar 包方式引用 ztcloud

# Conflicts:
#	zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/sms/dto/send/SmsSendSingleToUserReqDTO.java
#	zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sms/SmsCallbackController.java
#	zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/mq/message/sms/SmsSendMessage.java
2026-02-03 16:18:37 +08:00
ranke
4d997d9b86 清理与ztcloud中重复的代码,改为 jar 包方式引用 ztcloud 2026-02-03 15:23:43 +08:00
chenbowen
f1bafb98f6 Merge branch 'dev' into test 2026-02-03 09:35:55 +08:00
chenbowen
47e2529b2b Merge remote-tracking branch 'base-version/test' into dev 2026-02-03 09:35:39 +08:00
ranke
5f43006ee2 新增监听器的demo 2026-02-03 09:30:37 +08:00
chenbowen
1a5959a7e1 Merge branch 'dev' into test 2026-02-02 18:40:01 +08:00
chenbowen
351e4ab1ae 新增计划定额服务路由 2026-02-02 18:39:26 +08:00
chenbowen
634a84f8b8 Merge branch 'dev' into test 2026-02-02 18:16:13 +08:00
chenbowen
edb1a0f8ed 新增计划定额服务路由 2026-02-02 18:15:49 +08:00
朝锦 杨
7a8291992c Merge branch 'dev' into 'test'
重写手动针对用户以及组织的单条同步逻辑

See merge request jygk/dsc!29
2026-02-02 08:36:22 +00:00
yangchaojin
b689dd7741 Merge remote-tracking branch 'ztcloud/test' into dev
# Conflicts:
#	zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java
2026-02-02 16:34:03 +08:00
chenbowen
1a9e602ce7 删除用户不再限制 dept 数据权限 2026-02-02 16:20:39 +08:00
yangchaojin
8a27141edc 添加前端加密文件预览支持 2026-02-02 16:00:07 +08:00
chenbowen
c4aff51c36 Merge branch 'dev' into test 2026-02-02 11:57:56 +08:00
chenbowen
34bfe041e8 重写手动针对用户以及组织的单条同步逻辑
登录获取 token 时新增客户端编号标识
支持根据客户端编号以及用户id批量失效 token 方法
2026-02-02 11:57:26 +08:00
ranke
394e6b6bbb 解决Bean冲突 2026-02-02 11:25:01 +08:00
chenbowen
2e47c66fda 修复用户下其他 schema 包含同名 flowable 表无法启动 bpm 服务的错误 2026-01-30 16:02:48 +08:00
wuzongyong
42c01dc0a4 docs(iwork): 添加用印流程集成开发文档
- 新增完整的 iWork 用印流程集成开发文档
- 包含整体架构图和完整流程时序图
- 详细说明数据库设计和状态流转机制
- 提供 API 接口说明和请求参数定义
- 描述 MQ 消息机制和消息格式定义
- 编写业务模块接入指南和消费者实现示例
- 说明重试机制配置和手工重试接口
- 提供本地开发调试和常见问题解决方案
- 列出相关代码位置便于查阅和维护
2026-01-30 10:12:45 +08:00
wuzongyong
399875bc77 feat(iwork): 合并流程创建日志和回调日志(iwork统一用印内容)
- 将原 iwork_workflow_log.sql 重命名为 iwork_workflow_log_20260130.sql
- 在数据库表中新增回调状态、重试次数、错误信息等字段
- 更新表注释为 iWork 流程日志,表明合并了创建日志和回调日志
- 修改 requestId 字段长度从 64 扩展到 128
- 新增回调相关索引配置
- 删除 IWorkCallbackLogService 相关接口及实现类
- 将 IWorkBizCallbackListener 中的日志服务替换为工作流日志服务
- 在控制器层将回调日志查询统一到工作流日志服务
- 合并 IWorkIntegrationServiceImpl 中的流程日志处理逻辑
- 移除独立的用印流程回调日志实体类 IWorkSealLogDO
- 在 IWorkWorkflowLogDO 中增加回调相关字段定义
- 完善工作流日志服务接口和实现类,支持回调状态管理
- 更新流程回调处理逻辑,统一使用工作流日志表进行状态跟踪
2026-01-30 10:07:41 +08:00
wuzongyong
bcdba608c7 feat(iwork): 合并流程创建日志和回调日志(iwork统一用印内容)
- 将原 iwork_workflow_log.sql 重命名为 iwork_workflow_log_20260130.sql
- 在数据库表中新增回调状态、重试次数、错误信息等字段
- 更新表注释为 iWork 流程日志,表明合并了创建日志和回调日志
- 修改 requestId 字段长度从 64 扩展到 128
- 新增回调相关索引配置
- 删除 IWorkCallbackLogService 相关接口及实现类
- 将 IWorkBizCallbackListener 中的日志服务替换为工作流日志服务
- 在控制器层将回调日志查询统一到工作流日志服务
- 合并 IWorkIntegrationServiceImpl 中的流程日志处理逻辑
- 移除独立的用印流程回调日志实体类 IWorkSealLogDO
- 在 IWorkWorkflowLogDO 中增加回调相关字段定义
- 完善工作流日志服务接口和实现类,支持回调状态管理
- 更新流程回调处理逻辑,统一使用工作流日志表进行状态跟踪
2026-01-30 10:06:45 +08:00
ranke
860f13914d bpm加入maven管理 2026-01-29 18:53:43 +08:00
ranke
24352b34db ztcloud-dist 中 bpm 覆盖回来,包含 http://172.16.46.63:31560/index.php?m=task&f=view&taskID=735http://172.16.46.63:31560/index.php?m=task&f=view&taskID=552 中的修复 2026-01-29 18:53:35 +08:00
ranke
df2b0f52e3 no message 2026-01-29 18:53:22 +08:00
ranke
61d85988fc 修复数据总线访问日志无法显示状态码问题: http://172.16.46.63:31560/index.php?m=task&f=view&taskID=703. databus 新增 client 统一出口内容管理审计: http://172.16.46.63:31560/index.php?m=task&f=view&taskID=716 2026-01-29 18:51:48 +08:00
ranke
6b3bc5d18e userId改为 workcode , 修复数据权限的问题. http://172.16.46.63:31560/index.php?m=task&f=view&taskID=715 2026-01-29 18:51:20 +08:00
chenbowen
5f55c90e1b 重写手动针对用户以及组织的单条同步逻辑
登录获取 token 时新增客户端编号标识
支持根据客户端编号以及用户id批量失效 token 方法
2026-01-29 18:28:00 +08:00
ranke
b94fcd17ac bpm加入maven管理 2026-01-29 18:15:57 +08:00
ranke
c8cc9a9cb4 ztcloud-dist 中 bpm 覆盖回来,包含 http://172.16.46.63:31560/index.php?m=task&f=view&taskID=735http://172.16.46.63:31560/index.php?m=task&f=view&taskID=552 中的修复 2026-01-29 18:07:49 +08:00
朝锦 杨
2b2460a143 Merge branch 'dev' into 'test'
Dev

See merge request jygk/dsc!28
2026-01-29 09:39:08 +00:00
yangchaojin
ee4645dce3 Merge remote-tracking branch 'ztcloud/test' into dev 2026-01-29 17:27:14 +08:00
yangchaojin
4da9bd6cc5 修复kkfile预览加密文件问题、修改文件下载验证码发送模板 2026-01-29 17:26:35 +08:00
朝锦 杨
687d392392 Merge branch 'dev' into 'test'
feat(iwork): 添加工作流创建操作中operatorUserId字段验证

See merge request jygk/dsc!27
2026-01-29 07:39:54 +00:00
yangchaojin
c852d9f111 Merge remote-tracking branch 'ztcloud/test' into dev 2026-01-29 15:38:43 +08:00
yangchaojin
cabd6e3297 修复测试环境多级代理路劲拼接问题 2026-01-29 15:38:07 +08:00
wuzongyong
2af0f178dc feat(iwork): 添加工作流创建操作中operatorUserId字段验证
- 在createWorkflow方法中添加operatorUserId必填字段检查
- 在createGenericWorkflow方法中添加operatorUserId必填字段检查
- 当operatorUserId为空时抛出IWORK_SEAL_REQUIRED_FIELD_MISSING异常
- 优化createGenericWorkflow中operatorUserId参数传递逻辑
2026-01-29 15:26:59 +08:00
wuzongyong
f91d2d05e8 feat(iwork): 添加工作流创建操作中operatorUserId字段验证
- 在createWorkflow方法中添加operatorUserId必填字段检查
- 在createGenericWorkflow方法中添加operatorUserId必填字段检查
- 当operatorUserId为空时抛出IWORK_SEAL_REQUIRED_FIELD_MISSING异常
- 优化createGenericWorkflow中operatorUserId参数传递逻辑
2026-01-29 15:26:25 +08:00
朝锦 杨
947e1cc891 Merge branch 'dev' into 'test'
feat(iwork): 扩展iWork集成功能支持通用流程创建和回调处理

See merge request jygk/dsc!26
2026-01-29 07:10:59 +00:00
yangchaojin
e2a3280b70 Merge remote-tracking branch 'ztcloud/test' into dev 2026-01-29 15:09:44 +08:00
yangchaojin
9f622d460f 添加测试环境调试信息 2026-01-29 15:08:08 +08:00
wuzongyong
0fd7756d17 feat(iwork): 扩展iWork集成功能支持通用流程创建和回调处理
- 添加通用流程创建接口支持透传任意业务参数
- 实现流程创建日志记录功能包括requestId、workflowId等关键信息
- 增强文件回调处理记录日志并发送MQ通知业务系统
- 添加iWork业务回调消息测试消费者用于验证回调机制
- 在模板模块合同详情中增加业务附件列表展示功能
- 更新应用配置忽略缓存新增iWork印章日志表配置项
2026-01-29 14:54:59 +08:00
chenbowen
891bfad529 Merge branch 'dev' into test 2026-01-29 14:53:49 +08:00
wuzongyong
958ae5a519 feat(iwork): 扩展iWork集成功能支持通用流程创建和回调处理
- 添加通用流程创建接口支持透传任意业务参数
- 实现流程创建日志记录功能包括requestId、workflowId等关键信息
- 增强文件回调处理记录日志并发送MQ通知业务系统
- 添加iWork业务回调消息测试消费者用于验证回调机制
- 在模板模块合同详情中增加业务附件列表展示功能
- 更新应用配置忽略缓存新增iWork印章日志表配置项
2026-01-29 14:53:18 +08:00
chenbowen
b02ed67fa4 Merge remote-tracking branch 'base-version/test' into dev 2026-01-29 14:52:57 +08:00
chenbowen
9e98fa8c23 重写手动针对用户以及组织的单条同步逻辑
登录获取 token 时新增客户端编号标识
支持根据客户端编号以及用户id批量失效 token 方法
2026-01-29 14:51:32 +08:00
朝锦 杨
d7a6be298d Merge branch 'dev' into 'test'
Dev

See merge request jygk/dsc!25
2026-01-29 02:44:09 +00:00
yangchaojin
b2d29a1423 Merge remote-tracking branch 'ztcloud/test' into dev 2026-01-29 10:43:20 +08:00
yangchaojin
58df702cef 日志输出预览地址 2026-01-29 10:42:37 +08:00
chenbowen
d15dabfeac 1. 修改 iwork 用户同步逻辑,不对非 iwork 来源的关联关系进行覆盖,不覆盖已关联的岗位信息 2026-01-29 09:22:30 +08:00
ranke
396b26225f 新增根据用户id查询是否超级管理员的接口 2026-01-29 09:20:23 +08:00
3407 changed files with 829 additions and 259881 deletions

View 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` |

23
pom.xml
View File

@@ -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>

View 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);

View File

@@ -1,43 +0,0 @@
-- iWork 用印回调日志DM8
-- 表system_iwork_seal_log
-- 序列system_iwork_seal_log_seq
-- 清理旧对象(若存在)
DROP TABLE IF EXISTS system_iwork_seal_log;
CREATE TABLE system_iwork_seal_log (
id BIGINT NOT NULL,
request_id VARCHAR(128) NOT NULL,
business_code VARCHAR(128),
biz_callback_key VARCHAR(255),
status INTEGER,
retry_count INTEGER DEFAULT 0,
max_retry INTEGER,
last_error_message VARCHAR(512),
raw_callback VARCHAR(2000),
last_callback_time DATETIME,
creator VARCHAR(64),
create_time DATETIME DEFAULT SYSDATE,
updater VARCHAR(64),
update_time DATETIME DEFAULT SYSDATE,
deleted SMALLINT DEFAULT 0 NOT NULL,
PRIMARY KEY (id),
UNIQUE (request_id)
);
COMMENT ON TABLE system_iwork_seal_log IS 'iWork 用印回调日志';
COMMENT ON COLUMN system_iwork_seal_log.id IS '主键';
COMMENT ON COLUMN system_iwork_seal_log.request_id IS 'iWork requestId 唯一标识';
COMMENT ON COLUMN system_iwork_seal_log.business_code IS '业务单号';
COMMENT ON COLUMN system_iwork_seal_log.biz_callback_key IS '业务回调标识';
COMMENT ON COLUMN system_iwork_seal_log.status IS '状态枚举';
COMMENT ON COLUMN system_iwork_seal_log.retry_count IS '已重试次数';
COMMENT ON COLUMN system_iwork_seal_log.max_retry IS '最大重试次数快照';
COMMENT ON COLUMN system_iwork_seal_log.last_error_message IS '最后错误信息';
COMMENT ON COLUMN system_iwork_seal_log.raw_callback IS '回调原文截断';
COMMENT ON COLUMN system_iwork_seal_log.last_callback_time IS '最近回调时间';
COMMENT ON COLUMN system_iwork_seal_log.creator IS '创建者';
COMMENT ON COLUMN system_iwork_seal_log.create_time IS '创建时间';
COMMENT ON COLUMN system_iwork_seal_log.updater IS '更新者';
COMMENT ON COLUMN system_iwork_seal_log.update_time IS '最后更新时间';
COMMENT ON COLUMN system_iwork_seal_log.deleted IS '是否删除';

View File

@@ -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.43</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 ZThttps://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> <!-- 接口文档 UIknife4j【网关专属】 -->
<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
View 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>

View File

@@ -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>

View File

@@ -1,7 +1,5 @@
package com.zt.plat.framework.common.util.asyncTask;
import com.alibaba.ttl.TransmittableThreadLocal;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.*;
@@ -11,7 +9,7 @@ import java.util.concurrent.*;
* 多次提交,一次等待
*/
public class AsyncLatchUtils {
private static final TransmittableThreadLocal<List<TaskInfo>> THREAD_LOCAL = TransmittableThreadLocal.withInitial(LinkedList::new);
private static final ThreadLocal<List<TaskInfo>> THREADLOCAL = ThreadLocal.withInitial(LinkedList::new);
/**
* 提交一个异步任务
@@ -19,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));
}
/**
@@ -27,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;
}
@@ -41,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());
@@ -59,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 {

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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 ZTfallbackFactory =
@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();
}
}

View File

@@ -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 ZTfallbackFactory =
@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();
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -1,4 +0,0 @@
/**
* 针对 infra 模块的 api 包
*/
package com.zt.plat.framework.common.biz.infra;

View File

@@ -1,4 +0,0 @@
/**
* 特殊:用于 framework 下starter 需要调用 biz 业务模块的接口定义!
*/
package com.zt.plat.framework.common.biz;

View File

@@ -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 ZTfallbackFactory =
@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);
}

View File

@@ -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 枚举
}

View File

@@ -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 ZTfallbackFactory =
@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();
}
}

View File

@@ -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;
}

View File

@@ -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 ZTfallbackFactory =
@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);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -1,4 +0,0 @@
/**
* 针对 system 模块的 api 包
*/
package com.zt.plat.framework.common.biz.system;

View File

@@ -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 ZTfallbackFactory =
@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);
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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 ZTfallbackFactory =
@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);
}

View File

@@ -1,15 +0,0 @@
package com.zt.plat.framework.common.core;
/**
* 可生成 T 数组的接口
*
* @author HUIHUI
*/
public interface ArrayValuable<T> {
/**
* @return 数组
*/
T[] array();
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}

View File

@@ -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";
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -1,9 +0,0 @@
package com.zt.plat.framework.common.enums;
/**
* 验证码发送方式
*/
public enum VerifyCodeSendType {
SMS, // 短信验证码
E_OFFICE // e办消息推送
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 业务接口标记");
}

View File

@@ -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)
}

View File

@@ -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();
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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];
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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(), "/");
}
}

View File

@@ -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) {
}
}

View File

@@ -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;
}
}

View 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));
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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(), "-", "");
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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)));
}
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -1,123 +0,0 @@
package com.zt.plat.framework.common.util.spring;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Spring EL 表达式的工具类
*
* @author mashu
*/
public class SpringExpressionUtils {
/**
* Spring EL 表达式解析器
*/
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
/**
* 参数名发现器
*/
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
private SpringExpressionUtils() {
}
/**
* 从切面中,单个解析 EL 表达式的结果
*
* @param joinPoint 切面点
* @param expressionString EL 表达式数组
* @return 执行界面
*/
public static Object parseExpression(JoinPoint joinPoint, String expressionString) {
Map<String, Object> result = parseExpressions(joinPoint, Collections.singletonList(expressionString));
return result.get(expressionString);
}
/**
* 从切面中,批量解析 EL 表达式的结果
*
* @param joinPoint 切面点
* @param expressionStrings EL 表达式数组
* @return 结果key 为表达式value 为对应值
*/
public static Map<String, Object> parseExpressions(JoinPoint joinPoint, List<String> expressionStrings) {
// 如果为空,则不进行解析
if (CollUtil.isEmpty(expressionStrings)) {
return MapUtil.newHashMap();
}
// 第一步,构建解析的上下文 EvaluationContext
// 通过 joinPoint 获取被注解方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 使用 spring 的 ParameterNameDiscoverer 获取方法形参名数组
String[] paramNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method);
// Spring 的表达式上下文对象
EvaluationContext context = new StandardEvaluationContext();
// 给上下文赋值
if (ArrayUtil.isNotEmpty(paramNames)) {
Object[] args = joinPoint.getArgs();
for (int i = 0; i < paramNames.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
}
// 第二步,逐个参数解析
Map<String, Object> result = MapUtil.newHashMap(expressionStrings.size(), true);
expressionStrings.forEach(key -> {
Object value = EXPRESSION_PARSER.parseExpression(key).getValue(context);
result.put(key, value);
});
return result;
}
/**
* 从 Bean 工厂,解析 EL 表达式的结果
*
* @param expressionString EL 表达式
* @return 执行界面
*/
public static Object parseExpression(String expressionString) {
return parseExpression(expressionString, null);
}
/**
* 从 Bean 工厂,解析 EL 表达式的结果
*
* @param expressionString EL 表达式
* @param variables 变量
* @return 执行界面
*/
public static Object parseExpression(String expressionString, Map<String, Object> variables) {
if (StrUtil.isBlank(expressionString)) {
return null;
}
Expression expression = EXPRESSION_PARSER.parseExpression(expressionString);
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new BeanFactoryResolver(SpringUtil.getApplicationContext()));
if (MapUtil.isNotEmpty(variables)) {
context.setVariables(variables);
}
return expression.getValue(context);
}
}

View File

@@ -1,24 +0,0 @@
package com.zt.plat.framework.common.util.spring;
import cn.hutool.extra.spring.SpringUtil;
import java.util.Objects;
/**
* Spring 工具类
*
* @author ZT
*/
public class SpringUtils extends SpringUtil {
/**
* 是否为生产环境
*
* @return 是否生产环境
*/
public static boolean isProd() {
String activeProfile = getActiveProfile();
return Objects.equals("prod", activeProfile);
}
}

View File

@@ -1,107 +0,0 @@
package com.zt.plat.framework.common.util.string;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import org.aspectj.lang.JoinPoint;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 字符串工具类
*
* @author ZT
*/
public class StrUtils {
public static String maxLength(CharSequence str, int maxLength) {
return StrUtil.maxLength(str, maxLength - 3); // -3 的原因,是该方法会补充 ... 恰好
}
/**
* 给定字符串是否以任何一个字符串开始
* 给定字符串和数组为空都返回 false
*
* @param str 给定字符串
* @param prefixes 需要检测的开始字符串
* @since 3.0.6
*/
public static boolean startWithAny(String str, Collection<String> prefixes) {
if (StrUtil.isEmpty(str) || ArrayUtil.isEmpty(prefixes)) {
return false;
}
for (CharSequence suffix : prefixes) {
if (StrUtil.startWith(str, suffix, false)) {
return true;
}
}
return false;
}
public static List<Long> splitToLong(String value, CharSequence separator) {
long[] longs = StrUtil.splitToLong(value, separator);
return Arrays.stream(longs).boxed().collect(Collectors.toList());
}
public static Set<Long> splitToLongSet(String value) {
return splitToLongSet(value, StrPool.COMMA);
}
public static Set<Long> splitToLongSet(String value, CharSequence separator) {
long[] longs = StrUtil.splitToLong(value, separator);
return Arrays.stream(longs).boxed().collect(Collectors.toSet());
}
public static List<Integer> splitToInteger(String value, CharSequence separator) {
int[] integers = StrUtil.splitToInt(value, separator);
return Arrays.stream(integers).boxed().collect(Collectors.toList());
}
/**
* 移除字符串中,包含指定字符串的行
*
* @param content 字符串
* @param sequence 包含的字符串
* @return 移除后的字符串
*/
public static String removeLineContains(String content, String sequence) {
if (StrUtil.isEmpty(content) || StrUtil.isEmpty(sequence)) {
return content;
}
return Arrays.stream(content.split("\n"))
.filter(line -> !line.contains(sequence))
.collect(Collectors.joining("\n"));
}
/**
* 拼接方法的参数
*
* 特殊:排除一些无法序列化的参数,如 ServletRequest、ServletResponse、MultipartFile
*
* @param joinPoint 连接点
* @return 拼接后的参数
*/
public static String joinMethodArgs(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
if (ArrayUtil.isEmpty(args)) {
return "";
}
return ArrayUtil.join(args, ",", item -> {
if (item == null) {
return "";
}
// 讨论可见https://t.zsxq.com/XUJVk、https://t.zsxq.com/MnKcL
String clazzName = item.getClass().getName();
if (StrUtil.startWithAny(clazzName, "javax.servlet", "jakarta.servlet", "org.springframework.web")) {
return "";
}
return item;
});
}
}

View File

@@ -1,123 +0,0 @@
package com.zt.plat.framework.common.util.user;
import cn.hutool.core.collection.CollUtil;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 用户名称填充工具类
* 通用的用户信息填充工具,可在各个模块中使用
*
* @author ZT
*/
public class UserNameEnrichUtils {
/**
* 批量设置创建人名称
*
* @param items 需要设置创建人名称的对象列表
* @param creatorExtractor 创建人ID提取器
* @param creatorNameSetter 创建人名称设置器
* @param userNicknameProvider 用户昵称提供者函数
*/
public static <T> void setCreatorNames(List<T> items,
Function<T, String> creatorExtractor,
BiConsumer<T, String> creatorNameSetter,
Function<Collection<Long>, Map<Long, String>> userNicknameProvider) {
if (CollUtil.isEmpty(items) || userNicknameProvider == null) {
return;
}
// 提取所有创建人ID
Set<Long> creatorIds = items.stream()
.map(creatorExtractor)
.filter(Objects::nonNull)
.filter(creator -> !creator.isEmpty())
.map(creatorId -> {
try {
return Long.parseLong(creatorId);
} catch (NumberFormatException e) {
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (creatorIds.isEmpty()) {
return;
}
// 批量获取用户昵称
Map<Long, String> userNicknameMap = userNicknameProvider.apply(creatorIds);
// 设置创建人名称
items.forEach(item -> {
String creatorId = creatorExtractor.apply(item);
if (creatorId != null && !creatorId.isEmpty()) {
try {
Long id = Long.parseLong(creatorId);
String nickname = userNicknameMap.get(id);
if (nickname != null) {
creatorNameSetter.accept(item, nickname);
}
} catch (NumberFormatException e) {
// 忽略无效的ID格式
}
}
});
}
/**
* 设置单个对象的创建人名称
* 直接复用批量设置的逻辑
*
* @param item 需要设置创建人名称的对象
* @param creatorExtractor 创建人ID提取器
* @param creatorNameSetter 创建人名称设置器
* @param userNicknameProvider 用户昵称提供者函数
*/
public static <T> void setCreatorName(T item,
Function<T, String> creatorExtractor,
BiConsumer<T, String> creatorNameSetter,
Function<Collection<Long>, Map<Long, String>> userNicknameProvider) {
if (item == null) {
return;
}
// 直接复用批量设置的逻辑
setCreatorNames(Collections.singletonList(item), creatorExtractor, creatorNameSetter, userNicknameProvider);
}
/**
* 创建通用的用户昵称提供者
* 这是一个通用的实现,可以直接在各个模块中使用
*
* @param userDataProvider 用户数据提供者接收用户ID集合返回用户数据Map
* @param nicknameExtractor 从用户数据中提取昵称的函数
* @return 用户昵称提供者函数
*/
public static <UserData> Function<Collection<Long>, Map<Long, String>> createUserNicknameProvider(
Function<Collection<Long>, Map<Long, UserData>> userDataProvider,
Function<UserData, String> nicknameExtractor) {
return userIds -> {
if (CollUtil.isEmpty(userIds)) {
return Collections.emptyMap();
}
Map<Long, UserData> userDataMap = userDataProvider.apply(userIds);
if (userDataMap == null || userDataMap.isEmpty()) {
return Collections.emptyMap();
}
return userDataMap.entrySet().stream()
.filter(entry -> entry.getValue() != null)
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> nicknameExtractor.apply(entry.getValue()),
(existing, replacement) -> existing // 处理重复key的情况
));
};
}
}

View File

@@ -1,210 +0,0 @@
package com.zt.plat.framework.common.util.validation;
import java.util.regex.Pattern;
/**
* 正则校验工具
*
* @author luzemin
**/
public class MatcherSolveUtils {
/**
* 正则校验正数、负数、和小数
*/
private static final Pattern IS_NUM = Pattern.compile("^(\\-|\\+)?\\d+(\\.\\d+)?$");
/**
* 正则校验大于0的数字
*/
private static final Pattern IS_GT_0_NUM = Pattern.compile("(^[0-9]+\\.\\d+$)|(^[0-9]+$)");
/**
* 正则校验大于等于0的数字
*/
private static final Pattern IS_GE_EQ_0_NUM = Pattern.compile("(^[0-9]+\\.\\d+$)|(^[1-9]+\\d*$)");
/**
* 正则校验字母或者数字组成的字符串
*/
private static final Pattern EN_NUM = Pattern.compile("^[A-Za-z0-9]+$");
/**
* 中文、英文、数字但不包括下划线等符号
*/
private static final Pattern EN_CN_NUM = Pattern.compile("^[\\u4E00-\\u9FA5A-Za-z0-9]+$");
/**
* Email地址
*/
private static final Pattern EMAIL = Pattern.compile("^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$");
/**
* 域名
*/
private static final Pattern WWW = Pattern.compile("[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\\.?");
/**
* InternetURL
*/
private static final Pattern URL = Pattern.compile("(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]");
/**
* 手机号码
*/
private static final Pattern MOBILE_NUM = Pattern.compile("^(13[0-9]|14[5|7]|15[0|1|2|3|4|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\\d{8}$");
/**
* 车牌号
*/
private static final Pattern TRUCK_NO = Pattern.compile("^([京津晋冀蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新][ABCDEFGHJKLMNPQRSTUVWXY][1-9DF][1-9ABCDEFGHJKLMNPQRSTUVWXYZ]\\d{3}[1-9DF]|[京津晋冀蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新][ABCDEFGHJKLMNPQRSTUVWXY][\\dABCDEFGHJKLNMxPQRSTUVWXYZ]{5})$");
/**
* 电话号码正则表达式支持手机号码3-4位区号7-8位直播号码14位分机号
*/
private static final Pattern PHONE_NUM = Pattern.compile("((\\d{11})|^((\\d{7,8})|(\\d{4}|\\d{3})-(\\d{7,8})|(\\d{4}|\\d{3})-(\\d{7,8})-(\\d{4}|\\d{3}|\\d{2}|\\d{1})|(\\d{7,8})-(\\d{4}|\\d{3}|\\d{2}|\\d{1}))$)");
/**
* 身份证
*/
private static final Pattern ID_CARD = Pattern.compile("(^\\d{15}$)|(^\\d{18}$)|(^\\d{17}(\\d|X|x)$)");
/**
* 帐号是否合法(字母开头允许5-16字节允许字母数字下划线)
*/
private static final Pattern ACCOUNT = Pattern.compile("^[a-zA-Z][a-zA-Z0-9_]{4,15}$");
/**
* 密码(以字母开头长度在6~18之间只能包含字母、数字和下划线)
*/
private static final Pattern PASSWORD = Pattern.compile("^[a-zA-Z]\\w{5,17}$");
/**
* 强密码(必须包含大小写字母和数字的组合可以使用特殊字符长度在8-18之间)
*/
private static final Pattern PASSWORD_STRONG = Pattern.compile("^(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,18}$");
/**
* 正则校验正数、负数、和小数
*
* @param str 待校验字符串
*/
public static boolean checkIsNum(String str) {
return IS_NUM.matcher(str).matches();
}
/**
* 正则校验大于0的数字
*
* @param str 待校验字符串t
*/
public static boolean checkIsGt0Num(String str) {
return IS_GT_0_NUM.matcher(str).matches();
}
/**
* 正则校验大于等于0的数字
*
* @param str 待校验字符串
*/
public static boolean checkIsGtEq0Num(String str) {
return IS_GE_EQ_0_NUM.matcher(str).matches();
}
/**
* 正则校验字母或者数字组成的字符串
*
* @param str 待校验字符串
*/
public static boolean checkEnNum(String str) {
return EN_NUM.matcher(str).matches();
}
/**
* 中文、英文、数字但不包括下划线等符号
*
* @param str 待校验字符串
*/
public static boolean checkEnCnNum(String str) {
return EN_CN_NUM.matcher(str).matches();
}
/**
* Email地址
*
* @param str 待校验字符串
*/
public static boolean checkEmail(String str) {
return EMAIL.matcher(str).matches();
}
/**
* 域名
*
* @param str 待校验字符串
*/
public static boolean checkWww(String str) {
return WWW.matcher(str).matches();
}
/**
* InternetURL
*
* @param str 待校验字符串
*/
public static boolean checkInternetURL(String str) {
return URL.matcher(str).matches();
}
/**
* 手机号码
*
* @param str 待校验字符串
*/
public static boolean checkMobileNum(String str) {
return MOBILE_NUM.matcher(str).matches();
}
/**
* 车牌号
*
* @param str 待校验字符串
* @return 校验通过-true反之-false
*/
public static boolean checkTruckNo(String str) {
return TRUCK_NO.matcher(str).matches();
}
/**
* 电话号码正则表达式支持手机号码3-4位区号7-8位直播号码14位分机号
*
* @param str 待校验字符串
*/
public static boolean checkPhoneNum(String str) {
return PHONE_NUM.matcher(str).matches();
}
/**
* 身份证
*
* @param str 待校验字符串
*/
public static boolean checkIdCard(String str) {
return ID_CARD.matcher(str).matches();
}
/**
* 帐号是否合法(字母开头允许5-16字节允许字母数字下划线)
*
* @param str 待校验字符串
*/
public static boolean checkAccount(String str) {
return ACCOUNT.matcher(str).matches();
}
/**
* 密码(以字母开头长度在6~18之间只能包含字母、数字和下划线)
*
* @param str 待校验字符串
*/
public static boolean checkPassword(String str) {
return PASSWORD.matcher(str).matches();
}
/**
* 强密码(必须包含大小写字母和数字的组合可以使用特殊字符长度在8-18之间)
*
* @param str 待校验字符串
*/
public static boolean checkPasswordStrong(String str) {
return PASSWORD_STRONG.matcher(str).matches();
}
}

View File

@@ -1,55 +0,0 @@
package com.zt.plat.framework.common.util.validation;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import org.springframework.util.StringUtils;
import java.util.Set;
import java.util.regex.Pattern;
/**
* 校验工具类
*
* @author ZT
*/
public class ValidationUtils {
private static final Pattern PATTERN_MOBILE = Pattern.compile("^(?:(?:\\+|00)86)?1(?:(?:3[\\d])|(?:4[0,1,4-9])|(?:5[0-3,5-9])|(?:6[2,5-7])|(?:7[0-8])|(?:8[\\d])|(?:9[0-3,5-9]))\\d{8}$");
private static final Pattern PATTERN_URL = Pattern.compile("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]");
private static final Pattern PATTERN_XML_NCNAME = Pattern.compile("[a-zA-Z_][\\-_.0-9_a-zA-Z$]*");
public static boolean isMobile(String mobile) {
return StringUtils.hasText(mobile)
&& PATTERN_MOBILE.matcher(mobile).matches();
}
public static boolean isURL(String url) {
return StringUtils.hasText(url)
&& PATTERN_URL.matcher(url).matches();
}
public static boolean isXmlNCName(String str) {
return StringUtils.hasText(str)
&& PATTERN_XML_NCNAME.matcher(str).matches();
}
public static void validate(Object object, Class<?>... groups) {
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Assert.notNull(validator);
validate(validator, object, groups);
}
public static void validate(Validator validator, Object object, Class<?>... groups) {
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
if (CollUtil.isNotEmpty(constraintViolations)) {
throw new ConstraintViolationException(constraintViolations);
}
}
}

View File

@@ -1,35 +0,0 @@
package com.zt.plat.framework.common.validation;
import com.zt.plat.framework.common.core.ArrayValuable;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Target({
ElementType.METHOD,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR,
ElementType.PARAMETER,
ElementType.TYPE_USE
})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = {InEnumValidator.class, InEnumCollectionValidator.class}
)
public @interface InEnum {
/**
* @return 实现 ArrayValuable 接口的类
*/
Class<? extends ArrayValuable<?>> value();
String message() default "必须在指定范围 {value}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@@ -1,44 +0,0 @@
package com.zt.plat.framework.common.validation;
import cn.hutool.core.collection.CollUtil;
import com.zt.plat.framework.common.core.ArrayValuable;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class InEnumCollectionValidator implements ConstraintValidator<InEnum, Collection<?>> {
private List<?> values;
@Override
public void initialize(InEnum annotation) {
ArrayValuable<?>[] values = annotation.value().getEnumConstants();
if (values.length == 0) {
this.values = Collections.emptyList();
} else {
this.values = Arrays.asList(values[0].array());
}
}
@Override
public boolean isValid(Collection<?> list, ConstraintValidatorContext context) {
if (list == null) {
return true;
}
// 校验通过
if (CollUtil.containsAll(values, list)) {
return true;
}
// 校验不通过,自定义提示语句
context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()
.replaceAll("\\{value}", CollUtil.join(list, ","))).addConstraintViolation(); // 重新添加错误提示语句
return false;
}
}

View File

@@ -1,43 +0,0 @@
package com.zt.plat.framework.common.validation;
import com.zt.plat.framework.common.core.ArrayValuable;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class InEnumValidator implements ConstraintValidator<InEnum, Object> {
private List<?> values;
@Override
public void initialize(InEnum annotation) {
ArrayValuable<?>[] values = annotation.value().getEnumConstants();
if (values.length == 0) {
this.values = Collections.emptyList();
} else {
this.values = Arrays.asList(values[0].array());
}
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
// 为空时,默认不校验,即认为通过
if (value == null) {
return true;
}
// 校验通过
if (values.contains(value)) {
return true;
}
// 校验不通过,自定义提示语句
context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()
.replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句
return false;
}
}

View File

@@ -1,29 +0,0 @@
package com.zt.plat.framework.common.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Target({
ElementType.METHOD,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR,
ElementType.PARAMETER,
ElementType.TYPE_USE
})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = MobileValidator.class
)
public @interface Mobile {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@@ -1,25 +0,0 @@
package com.zt.plat.framework.common.validation;
import cn.hutool.core.util.StrUtil;
import com.zt.plat.framework.common.util.validation.ValidationUtils;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class MobileValidator implements ConstraintValidator<Mobile, String> {
@Override
public void initialize(Mobile annotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 如果手机号为空,默认不校验,即校验通过
if (StrUtil.isEmpty(value)) {
return true;
}
// 校验手机
return ValidationUtils.isMobile(value);
}
}

View File

@@ -1,31 +0,0 @@
package com.zt.plat.framework.common.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
/**
* 密码复杂度校验注解,要求至少包含大小写字母、数字、特殊字符中的三种。
* @author chenbowen
*/
@Target({
ElementType.METHOD,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR,
ElementType.PARAMETER,
ElementType.TYPE_USE
})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = PasswordValidator.class)
public @interface Password {
String message() default "密码必须包含大写字母、小写字母、数字、特殊字符中的至少三种";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@@ -1,38 +0,0 @@
package com.zt.plat.framework.common.validation;
import cn.hutool.core.util.StrUtil;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
/**
* 密码复杂度校验:至少命中以下类别中的三类:大写字母、小写字母、数字、特殊字符。
*/
public class PasswordValidator implements ConstraintValidator<Password, String> {
@Override
public void initialize(Password constraintAnnotation) {
// no-op
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (StrUtil.isBlank(value)) {
// 空值交由 @NotEmpty 等注解处理;在无需修改密码时视为空密码通过
return true;
}
int categories = 0;
if (value.matches(".*[A-Z].*")) {
categories++;
}
if (value.matches(".*[a-z].*")) {
categories++;
}
if (value.matches(".*[0-9].*")) {
categories++;
}
if (value.matches(".*[^A-Za-z0-9].*")) {
categories++;
}
return categories >= 3;
}
}

View File

@@ -1,29 +0,0 @@
package com.zt.plat.framework.common.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Target({
ElementType.METHOD,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR,
ElementType.PARAMETER,
ElementType.TYPE_USE
})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = TelephoneValidator.class
)
public @interface Telephone {
String message() default "电话格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

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