commit 5708f800912d5b73776ecb562cbd4bd2a829ef1b
Author: hongawen <83944980@qq.com>
Date: Wed Mar 11 19:32:37 2026 +0800
初始化
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e55eb64
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,75 @@
+
+# 查看更多 .gitignore 配置 -> https://help.github.com/articles/ignoring-files/
+
+target/
+!.mvn/wrapper/maven-wrapper.jar
+
+.flattened-pom.xml
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+*.class
+target/*
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+/build/
+
+
+
+### admin-web ###
+
+# dependencies
+**/node_modules
+
+# roadhog-api-doc ignore
+/src/utils/request-temp.js
+_roadhog-api-doc
+
+# production
+/dist
+/.vscode
+
+# misc
+.DS_Store
+npm-debug.log*
+yarn-error.log
+
+/coverage
+.idea
+yarn.lock
+package-lock.json
+*bak
+.vscode
+
+# visual studio code
+.history
+*.log
+
+functions/mock
+.temp/**
+
+# umi
+.umi
+.umi-production
+
+# screenshot
+screenshot
+.firebase
+sessionStore
diff --git a/lombok.config b/lombok.config
new file mode 100644
index 0000000..a8e8ce6
--- /dev/null
+++ b/lombok.config
@@ -0,0 +1,4 @@
+config.stopBubbling = true
+lombok.tostring.callsuper=CALL
+lombok.equalsandhashcode.callsuper=CALL
+lombok.accessors.chain=true
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..1d69fe6
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,238 @@
+
+
+ 4.0.0
+ com.njcn
+ cn-rdms
+ ${revision}
+ pom
+
+ rdms-system
+ rdms-framework
+ rdms-gateway
+
+ ${project.artifactId}
+ 灿能研发管理系统
+
+ V-0.5
+
+ 17
+ ${java.version}
+ ${java.version}
+ 3.5.3
+ 3.14.0
+ 1.7.2
+
+ 1.18.42
+ 3.5.9
+ 1.6.3
+ UTF-8
+
+
+
+
+
+ com.njcn
+ njcn-dependencies-bom-jdk17
+ 1.0.0
+ pom
+ import
+
+
+
+
+ com.njcn
+ rdms-common
+ ${revision}
+
+
+
+ com.njcn
+ rdms-spring-boot-starter-biz-ip
+ ${revision}
+
+
+
+ com.njcn
+ rdms-spring-boot-starter-env
+ ${revision}
+
+
+
+ com.njcn
+ rdms-spring-boot-starter-excel
+ ${revision}
+
+
+
+ com.njcn
+ rdms-spring-boot-starter-mq
+ ${revision}
+
+
+
+ com.njcn
+ rdms-spring-boot-starter-mybatis
+ ${revision}
+
+
+
+ com.njcn
+ rdms-spring-boot-starter-protection
+ ${revision}
+
+
+
+ com.njcn
+ rdms-spring-boot-starter-redis
+ ${revision}
+
+
+
+ com.njcn
+ rdms-spring-boot-starter-rpc
+ ${revision}
+
+
+
+ com.njcn
+ rdms-spring-boot-starter-security
+ ${revision}
+
+
+
+ com.njcn
+ rdms-spring-boot-starter-test
+ ${revision}
+
+
+
+ com.njcn
+ rdms-spring-boot-starter-web
+ ${revision}
+
+
+
+ com.njcn
+ rdms-spring-boot-starter-websocket
+ ${revision}
+
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ ${maven-surefire-plugin.version}
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ ${maven-compiler-plugin.version}
+
+ 17
+ 17
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ ${spring.boot.version}
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+
+ org.projectlombok
+ lombok-mapstruct-binding
+ 0.2.0
+
+
+ org.mapstruct
+ mapstruct-processor
+ ${mapstruct.version}
+
+
+
+ false
+
+ -parameters
+
+
+
+
+
+
+
+
+
+ org.codehaus.mojo
+ flatten-maven-plugin
+ ${flatten-maven-plugin.version}
+
+ oss
+ true
+
+
+
+
+ flatten
+
+ flatten
+ process-resources
+
+
+
+ clean
+
+ flatten.clean
+ clean
+
+
+
+
+
+
+
+
+
+ huaweicloud
+ huawei
+ https://mirrors.huaweicloud.com/repository/maven/
+
+
+ aliyunmaven
+ aliyun
+ https://maven.aliyun.com/repository/public
+
+
+
+ spring-milestones
+ Spring Milestones
+ https://repo.spring.io/milestone
+
+ false
+
+
+
+ spring-snapshots
+ Spring Snapshots
+ https://repo.spring.io/snapshot
+
+ false
+
+
+
+
+
diff --git a/rdms-framework/pom.xml b/rdms-framework/pom.xml
new file mode 100644
index 0000000..211dd80
--- /dev/null
+++ b/rdms-framework/pom.xml
@@ -0,0 +1,45 @@
+
+
+ 4.0.0
+
+ com.njcn
+ cn-rdms
+ ${revision}
+
+ pom
+
+
+ rdms-common
+ rdms-spring-boot-starter-env
+ rdms-spring-boot-starter-web
+ rdms-spring-boot-starter-rpc
+ rdms-spring-boot-starter-biz-ip
+ rdms-spring-boot-starter-test
+ rdms-spring-boot-starter-excel
+ rdms-spring-boot-starter-mybatis
+ rdms-spring-boot-starter-protection
+ rdms-spring-boot-starter-redis
+ rdms-spring-boot-starter-security
+ rdms-spring-boot-starter-websocket
+ rdms-spring-boot-starter-mq
+
+ rdms-framework
+
+ RDMS 框架功能模块,每一个子模块都代表一个功能组件。每个组件包括两部分:
+ 1. core 包:是该组件的核心封装
+ 2. config 包:是该组件基于 Spring 的配置
+
+ 技术组件,也分成两类:
+ 1. 框架组件:和我们熟悉的 MyBatis、Redis 等等的拓展
+ 2. 业务组件:和业务相关的组件的封装,例如说数据字典、操作日志等等。
+ 如果是业务组件,Maven 名字会包含 biz
+
+
+ 17
+ 17
+ UTF-8
+
+
+
diff --git a/rdms-framework/rdms-common/pom.xml b/rdms-framework/rdms-common/pom.xml
new file mode 100644
index 0000000..364e23f
--- /dev/null
+++ b/rdms-framework/rdms-common/pom.xml
@@ -0,0 +1,183 @@
+
+
+ 4.0.0
+
+ com.njcn
+ rdms-framework
+ ${revision}
+
+
+ rdms-common
+ jar
+ ${project.artifactId}
+ 定义基础 pojo 类、枚举、工具类等等
+
+ 17
+ 17
+ UTF-8
+
+
+
+
+
+
+
+ org.springframework
+ spring-core
+ provided
+
+
+
+ org.springframework
+ spring-expression
+ provided
+
+
+
+ org.springframework
+ spring-aop
+ provided
+
+
+
+ org.aspectj
+ aspectjweaver
+ provided
+
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+
+ org.springframework
+ spring-web
+ provided
+
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+
+
+ io.swagger.core.v3
+ swagger-annotations
+
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-openfeign-core
+ provided
+
+
+
+
+
+ org.apache.skywalking
+ apm-toolkit-trace
+
+
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+ org.mapstruct
+ mapstruct
+
+
+
+ org.mapstruct
+ mapstruct-jdk8
+
+
+
+ org.mapstruct
+ mapstruct-processor
+
+
+
+
+ com.google.guava
+ guava
+ provided
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ provided
+
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ provided
+
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+ provided
+
+
+
+
+ org.slf4j
+ slf4j-api
+ provided
+
+
+
+
+ jakarta.validation
+ jakarta.validation-api
+ provided
+
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+
+ com.alibaba
+ transmittable-thread-local
+
+
+
+
+ com.fhs-opensource
+ easy-trans-anno
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
\ No newline at end of file
diff --git a/rdms-framework/rdms-common/src/main/java/com/fhs/trans/service/AutoTransable.java b/rdms-framework/rdms-common/src/main/java/com/fhs/trans/service/AutoTransable.java
new file mode 100644
index 0000000..224164e
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/fhs/trans/service/AutoTransable.java
@@ -0,0 +1,57 @@
+package com.fhs.trans.service;
+
+import com.fhs.core.trans.vo.VO;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 只有实现了这个接口的才能自动翻译
+ *
+ * 为什么要赋值粘贴到 rdms-common 包下?
+ * 因为 AutoTransable 属于 easy-trans-service 下,无法方便的在 rdms-module-xxx-api 模块下使用
+ *
+ */
+public interface AutoTransable {
+
+ /**
+ * 根据 ids 查询数据列表
+ *
+ * 改方法已过期啦,请使用 selectByIds
+ *
+ * @param ids 编号数组
+ * @return 数据列表
+ */
+ @Deprecated
+ default List findByIds(List extends Object> ids){
+ return new ArrayList<>();
+ }
+
+ /**
+ * 根据 ids 查询
+ *
+ * @param ids 编号数组
+ * @return 数据列表
+ */
+ default List selectByIds(List extends Object> ids){
+ return this.findByIds(ids);
+ }
+
+ /**
+ * 获取 db 中所有的数据
+ *
+ * @return db 中所有的数据
+ */
+ default List select(){
+ return new ArrayList<>();
+ }
+
+ /**
+ * 根据 id 获取 vo
+ *
+ * @param primaryValue id
+ * @return vo
+ */
+ V selectById(Object primaryValue);
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/infra/logger/ApiAccessLogCommonApi.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/infra/logger/ApiAccessLogCommonApi.java
new file mode 100644
index 0000000..3bb37a6
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/infra/logger/ApiAccessLogCommonApi.java
@@ -0,0 +1,34 @@
+package com.njcn.rdms.framework.common.biz.infra.logger;
+
+import com.njcn.rdms.framework.common.biz.infra.logger.dto.ApiAccessLogCreateReqDTO;
+import com.njcn.rdms.framework.common.enums.RpcConstants;
+import com.njcn.rdms.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)
+@Tag(name = "RPC 服务 - API 访问日志")
+public interface ApiAccessLogCommonApi {
+
+ String PREFIX = RpcConstants.INFRA_PREFIX + "/api-access-log";
+
+ @PostMapping(PREFIX + "/create")
+ @Operation(summary = "创建 API 访问日志")
+ CommonResult createApiAccessLog(@Valid @RequestBody ApiAccessLogCreateReqDTO createDTO);
+
+ /**
+ * 【异步】创建 API 访问日志
+ *
+ * @param createDTO 访问日志 DTO
+ */
+ @Async
+ default void createApiAccessLogAsync(ApiAccessLogCreateReqDTO createDTO) {
+ createApiAccessLog(createDTO).checkError();
+ }
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/infra/logger/ApiErrorLogCommonApi.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/infra/logger/ApiErrorLogCommonApi.java
new file mode 100644
index 0000000..84b5599
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/infra/logger/ApiErrorLogCommonApi.java
@@ -0,0 +1,34 @@
+package com.njcn.rdms.framework.common.biz.infra.logger;
+
+import com.njcn.rdms.framework.common.biz.infra.logger.dto.ApiErrorLogCreateReqDTO;
+import com.njcn.rdms.framework.common.enums.RpcConstants;
+import com.njcn.rdms.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)
+@Tag(name = "RPC 服务 - API 异常日志")
+public interface ApiErrorLogCommonApi {
+
+ String PREFIX = RpcConstants.INFRA_PREFIX + "/api-error-log";
+
+ @PostMapping(PREFIX + "/create")
+ @Operation(summary = "创建 API 异常日志")
+ CommonResult createApiErrorLog(@Valid @RequestBody ApiErrorLogCreateReqDTO createDTO);
+
+ /**
+ * 【异步】创建 API 异常日志
+ *
+ * @param createDTO 异常日志 DTO
+ */
+ @Async
+ default void createApiErrorLogAsync(ApiErrorLogCreateReqDTO createDTO) {
+ createApiErrorLog(createDTO).checkError();
+ }
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/infra/logger/dto/ApiAccessLogCreateReqDTO.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/infra/logger/dto/ApiAccessLogCreateReqDTO.java
new file mode 100644
index 0000000..132ca43
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/infra/logger/dto/ApiAccessLogCreateReqDTO.java
@@ -0,0 +1,104 @@
+package com.njcn.rdms.framework.common.biz.infra.logger.dto;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.time.LocalDateTime;
+
+/**
+ * API 访问日志
+ *
+ * @author hongawen
+ */
+@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;
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/infra/logger/dto/ApiErrorLogCreateReqDTO.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/infra/logger/dto/ApiErrorLogCreateReqDTO.java
new file mode 100644
index 0000000..f16251a
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/infra/logger/dto/ApiErrorLogCreateReqDTO.java
@@ -0,0 +1,68 @@
+package com.njcn.rdms.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;
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/infra/package-info.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/infra/package-info.java
new file mode 100644
index 0000000..7da5f5f
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/infra/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 针对 infra 模块的 api 包
+ */
+package com.njcn.rdms.framework.common.biz.infra;
\ No newline at end of file
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/package-info.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/package-info.java
new file mode 100644
index 0000000..9f4bca7
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 特殊:用于 framework 下,starter 需要调用 biz 业务模块的接口定义!
+ */
+package com.njcn.rdms.framework.common.biz;
\ No newline at end of file
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/dict/DictDataCommonApi.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/dict/DictDataCommonApi.java
new file mode 100644
index 0000000..b92a29c
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/dict/DictDataCommonApi.java
@@ -0,0 +1,26 @@
+package com.njcn.rdms.framework.common.biz.system.dict;
+
+import com.njcn.rdms.framework.common.biz.system.dict.dto.DictDataRespDTO;
+import com.njcn.rdms.framework.common.enums.RpcConstants;
+import com.njcn.rdms.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)
+@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> getDictDataList(@RequestParam("dictType") String dictType);
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/dict/dto/DictDataRespDTO.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/dict/dto/DictDataRespDTO.java
new file mode 100644
index 0000000..179dcfb
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/dict/dto/DictDataRespDTO.java
@@ -0,0 +1,22 @@
+package com.njcn.rdms.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 = "灿能")
+ private String label;
+
+ @Schema(description = "字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "njcn")
+ 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 枚举
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/logger/OperateLogCommonApi.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/logger/OperateLogCommonApi.java
new file mode 100644
index 0000000..0c513af
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/logger/OperateLogCommonApi.java
@@ -0,0 +1,34 @@
+package com.njcn.rdms.framework.common.biz.system.logger;
+
+import com.njcn.rdms.framework.common.biz.system.logger.dto.OperateLogCreateReqDTO;
+import com.njcn.rdms.framework.common.enums.RpcConstants;
+import com.njcn.rdms.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)
+@Tag(name = "RPC 服务 - 操作日志")
+public interface OperateLogCommonApi {
+
+ String PREFIX = RpcConstants.SYSTEM_PREFIX + "/operate-log";
+
+ @PostMapping(PREFIX + "/create")
+ @Operation(summary = "创建操作日志")
+ CommonResult createOperateLog(@Valid @RequestBody OperateLogCreateReqDTO createReqDTO);
+
+ /**
+ * 【异步】创建操作日志
+ *
+ * @param createReqDTO 请求
+ */
+ @Async
+ default void createOperateLogAsync(OperateLogCreateReqDTO createReqDTO) {
+ createOperateLog(createReqDTO).checkError();
+ }
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/logger/dto/OperateLogCreateReqDTO.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/logger/dto/OperateLogCreateReqDTO.java
new file mode 100644
index 0000000..8bafd37
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/logger/dto/OperateLogCreateReqDTO.java
@@ -0,0 +1,50 @@
+package com.njcn.rdms.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 的用户信息,将性别从男改成女,将姓名从灿能改成源码")
+ @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;
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/oauth2/OAuth2TokenCommonApi.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/oauth2/OAuth2TokenCommonApi.java
new file mode 100644
index 0000000..ee5cc34
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/oauth2/OAuth2TokenCommonApi.java
@@ -0,0 +1,51 @@
+package com.njcn.rdms.framework.common.biz.system.oauth2;
+
+import com.njcn.rdms.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
+import com.njcn.rdms.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCreateReqDTO;
+import com.njcn.rdms.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenRespDTO;
+import com.njcn.rdms.framework.common.enums.RpcConstants;
+import com.njcn.rdms.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 jakarta.validation.Valid;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.*;
+
+@FeignClient(name = RpcConstants.SYSTEM_NAME)
+@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 createAccessToken(@Valid @RequestBody OAuth2AccessTokenCreateReqDTO reqDTO);
+
+ @GetMapping(PREFIX + "/check")
+ @Operation(summary = "校验访问令牌")
+ @Parameter(name = "accessToken", description = "访问令牌", required = true, example = "tudou")
+ CommonResult checkAccessToken(@RequestParam("accessToken") String accessToken);
+
+ @DeleteMapping(PREFIX + "/remove")
+ @Operation(summary = "移除访问令牌")
+ @Parameter(name = "accessToken", description = "访问令牌", required = true, example = "tudou")
+ CommonResult 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 = "rdmsyuanma")
+ })
+ CommonResult refreshAccessToken(@RequestParam("refreshToken") String refreshToken,
+ @RequestParam("clientId") String clientId);
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenCheckRespDTO.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenCheckRespDTO.java
new file mode 100644
index 0000000..cb6fe8c
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenCheckRespDTO.java
@@ -0,0 +1,33 @@
+package com.njcn.rdms.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\": \"灿能\"}")
+ private Map userInfo;
+
+ @Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long tenantId;
+
+ @Schema(description = "授权范围的数组", example = "user_info")
+ private List scopes;
+
+ @Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime expiresTime;
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenCreateReqDTO.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenCreateReqDTO.java
new file mode 100644
index 0000000..d100d4d
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenCreateReqDTO.java
@@ -0,0 +1,32 @@
+package com.njcn.rdms.framework.common.biz.system.oauth2.dto;
+
+import com.njcn.rdms.framework.common.enums.UserTypeEnum;
+import com.njcn.rdms.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 = "rdmsyuanma")
+ @NotNull(message = "客户端编号不能为空")
+ private String clientId;
+
+ @Schema(description = "授权范围的数组", example = "user_info")
+ private List scopes;
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenRespDTO.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenRespDTO.java
new file mode 100644
index 0000000..5c5cd4f
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenRespDTO.java
@@ -0,0 +1,28 @@
+package com.njcn.rdms.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;
+
+@Schema(description = "RPC 服务 - OAuth2 访问令牌的信息 Response DTO")
+@Data
+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;
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/package-info.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/package-info.java
new file mode 100644
index 0000000..2261d60
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 针对 system 模块的 api 包
+ */
+package com.njcn.rdms.framework.common.biz.system;
\ No newline at end of file
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/permission/PermissionCommonApi.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/permission/PermissionCommonApi.java
new file mode 100644
index 0000000..f8cccd1
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/permission/PermissionCommonApi.java
@@ -0,0 +1,37 @@
+package com.njcn.rdms.framework.common.biz.system.permission;
+
+import com.njcn.rdms.framework.common.enums.RpcConstants;
+import com.njcn.rdms.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)
+@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 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 hasAnyRoles(@RequestParam("userId") Long userId,
+ @RequestParam("roles") String... roles);
+
+}
\ No newline at end of file
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/core/ArrayValuable.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/core/ArrayValuable.java
new file mode 100644
index 0000000..cee0c93
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/core/ArrayValuable.java
@@ -0,0 +1,15 @@
+package com.njcn.rdms.framework.common.core;
+
+/**
+ * 可生成 T 数组的接口
+ *
+ * @author hongawen
+ */
+public interface ArrayValuable {
+
+ /**
+ * @return 数组
+ */
+ T[] array();
+
+}
\ No newline at end of file
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/core/KeyValue.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/core/KeyValue.java
new file mode 100644
index 0000000..f50a72d
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/core/KeyValue.java
@@ -0,0 +1,22 @@
+package com.njcn.rdms.framework.common.core;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * Key Value 的键值对
+ *
+ * @author hongawen
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class KeyValue implements Serializable {
+
+ private K key;
+ private V value;
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/CommonStatusEnum.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/CommonStatusEnum.java
new file mode 100644
index 0000000..73d4946
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/CommonStatusEnum.java
@@ -0,0 +1,46 @@
+package com.njcn.rdms.framework.common.enums;
+
+import cn.hutool.core.util.ObjUtil;
+import com.njcn.rdms.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 通用状态枚举
+ *
+ * @author hongawen
+ */
+@Getter
+@AllArgsConstructor
+public enum CommonStatusEnum implements ArrayValuable {
+
+ 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);
+ }
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/DateIntervalEnum.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/DateIntervalEnum.java
new file mode 100644
index 0000000..7decd70
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/DateIntervalEnum.java
@@ -0,0 +1,47 @@
+package com.njcn.rdms.framework.common.enums;
+
+import cn.hutool.core.util.ArrayUtil;
+import com.njcn.rdms.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 时间间隔的枚举
+ *
+ * @author dhb52
+ */
+@Getter
+@AllArgsConstructor
+public enum DateIntervalEnum implements ArrayValuable {
+
+ HOUR(0, "小时"), // 特殊:字典里,暂时不会有这个枚举!!!因为大多数情况下,用不到这个间隔
+ 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());
+ }
+
+}
\ No newline at end of file
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/DocumentEnum.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/DocumentEnum.java
new file mode 100644
index 0000000..0b96d3e
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/DocumentEnum.java
@@ -0,0 +1,21 @@
+package com.njcn.rdms.framework.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 文档地址
+ *
+ * @author hongawen
+ */
+@Getter
+@AllArgsConstructor
+public enum DocumentEnum {
+
+ REDIS_INSTALL("https://gitee.com/zhijiantianya/ruoyi-vue-pro/issues/I4VCSJ", "Redis 安装文档"),
+ TENANT("https://doc.iocoder.cn", "SaaS 多租户文档");
+
+ private final String url;
+ private final String memo;
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/RpcConstants.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/RpcConstants.java
new file mode 100644
index 0000000..f3c74f8
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/RpcConstants.java
@@ -0,0 +1,40 @@
+package com.njcn.rdms.framework.common.enums;
+
+/**
+ * RPC 相关的枚举
+ *
+ * 虽然放在 rdms-spring-boot-starter-rpc 会相对合适,但是每个 API 模块需要使用到,所以暂时只好放在此处
+ *
+ * @author hongawen
+ */
+public interface RpcConstants {
+
+ /**
+ * RPC API 的前缀
+ */
+ String RPC_API_PREFIX = "/rpc-api";
+
+ /**
+ * system 服务名
+ *
+ * 注意,需要保证和 spring.application.name 保持一致
+ */
+ String SYSTEM_NAME = "rdms-system-server";
+
+ /**
+ * system 服务的前缀
+ */
+ String SYSTEM_PREFIX = RPC_API_PREFIX + "/system";
+
+ /**
+ * infra 服务名
+ *
+ * 注意,需要保证和 spring.application.name 保持一致
+ */
+ String INFRA_NAME = "rdms-infra-server";
+ /**
+ * infra 服务的前缀
+ */
+ String INFRA_PREFIX = RPC_API_PREFIX + "/infra";
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/TerminalEnum.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/TerminalEnum.java
new file mode 100644
index 0000000..93deb32
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/TerminalEnum.java
@@ -0,0 +1,40 @@
+package com.njcn.rdms.framework.common.enums;
+
+import com.njcn.rdms.framework.common.core.ArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * 终端的枚举
+ *
+ * @author hongawen
+ */
+@RequiredArgsConstructor
+@Getter
+public enum TerminalEnum implements ArrayValuable {
+
+ 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;
+ }
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/UserTypeEnum.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/UserTypeEnum.java
new file mode 100644
index 0000000..b3ec482
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/UserTypeEnum.java
@@ -0,0 +1,43 @@
+package com.njcn.rdms.framework.common.enums;
+
+import cn.hutool.core.util.ArrayUtil;
+import com.njcn.rdms.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 全局用户类型枚举
+ *
+ * 用于区分不同访问端的用户类型:
+ * - MEMBER: 对应 /app-api/** 路径,通常用于移动端或 C 端用户
+ * - ADMIN: 对应 /admin-api/** 路径,通常用于 Web 管理端或 B 端用户
+ */
+@AllArgsConstructor
+@Getter
+public enum UserTypeEnum implements ArrayValuable {
+
+ MEMBER(1, "App端用户"), // 对应 /app-api,移动端或 C 端
+ ADMIN(2, "Web端用户"); // 对应 /admin-api,Web 管理端或 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;
+ }
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/WebFilterOrderEnum.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/WebFilterOrderEnum.java
new file mode 100644
index 0000000..3b139fb
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/WebFilterOrderEnum.java
@@ -0,0 +1,34 @@
+package com.njcn.rdms.framework.common.enums;
+
+/**
+ * Web 过滤器顺序的枚举类,保证过滤器按照符合我们的预期
+ *
+ * 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 enum 包下
+ *
+ * @author hongawen
+ */
+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;
+
+ int API_ENCRYPT_FILTER = REQUEST_BODY_CACHE_FILTER + 1;
+
+ // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等
+
+ int API_ACCESS_LOG_FILTER = -103; // 需要保证在 RequestBodyCacheFilter 后面
+
+ int XSS_FILTER = -102; // 需要保证在 RequestBodyCacheFilter 后面
+
+ // Spring Security Filter 默认为 -100,可见 org.springframework.boot.autoconfigure.security.SecurityProperties 配置属性类
+
+ int FLOWABLE_FILTER = -98; // 需要保证在 Spring Security 过滤后面
+
+ int DEMO_FILTER = Integer.MAX_VALUE;
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/exception/ErrorCode.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/exception/ErrorCode.java
new file mode 100644
index 0000000..98ec364
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/exception/ErrorCode.java
@@ -0,0 +1,32 @@
+package com.njcn.rdms.framework.common.exception;
+
+import com.njcn.rdms.framework.common.exception.enums.GlobalErrorCodeConstants;
+import com.njcn.rdms.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;
+ }
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/exception/ServerException.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/exception/ServerException.java
new file mode 100644
index 0000000..eec1798
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/exception/ServerException.java
@@ -0,0 +1,60 @@
+package com.njcn.rdms.framework.common.exception;
+
+import com.njcn.rdms.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;
+ }
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/exception/ServiceException.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/exception/ServiceException.java
new file mode 100644
index 0000000..80269e6
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/exception/ServiceException.java
@@ -0,0 +1,60 @@
+package com.njcn.rdms.framework.common.exception;
+
+import com.njcn.rdms.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;
+ }
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/exception/enums/GlobalErrorCodeConstants.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/exception/enums/GlobalErrorCodeConstants.java
new file mode 100644
index 0000000..12a7339
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/exception/enums/GlobalErrorCodeConstants.java
@@ -0,0 +1,41 @@
+package com.njcn.rdms.framework.common.exception.enums;
+
+import com.njcn.rdms.framework.common.exception.ErrorCode;
+
+/**
+ * 全局错误码枚举
+ * 0-999 系统异常编码保留
+ *
+ * 一般情况下,使用 HTTP 响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status
+ * 虽然说,HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的
+ * 比较特殊的是,因为之前一直使用 0 作为成功,就不使用 200 啦。
+ *
+ * @author hongawen
+ */
+public interface GlobalErrorCodeConstants {
+
+ ErrorCode SUCCESS = new ErrorCode(0, "成功");
+
+ // ========== 客户端错误段 ==========
+
+ ErrorCode BAD_REQUEST = new ErrorCode(400, "请求参数不正确");
+ ErrorCode UNAUTHORIZED = new ErrorCode(401, "账号未登录");
+ ErrorCode FORBIDDEN = new ErrorCode(403, "没有该操作权限");
+ ErrorCode NOT_FOUND = new ErrorCode(404, "请求未找到");
+ ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "请求方法不正确");
+ ErrorCode LOCKED = new ErrorCode(423, "请求失败,请稍后重试"); // 并发请求,不允许
+ ErrorCode TOO_MANY_REQUESTS = new ErrorCode(429, "请求过于频繁,请稍后重试");
+
+ // ========== 服务端错误段 ==========
+
+ ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常");
+ ErrorCode TABLE_NOT_EXISTS = 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, "未知错误");
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/exception/enums/ServiceErrorCodeRange.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/exception/enums/ServiceErrorCodeRange.java
new file mode 100644
index 0000000..a360e0f
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/exception/enums/ServiceErrorCodeRange.java
@@ -0,0 +1,48 @@
+package com.njcn.rdms.framework.common.exception.enums;
+
+/**
+ * 业务异常的错误码区间,解决:解决各模块错误码定义,避免重复,在此只声明不做实际使用
+ *
+ * 一共 10 位,分成四段
+ *
+ * 第一段,1 位,类型
+ * 1 - 业务级别异常
+ * x - 预留
+ * 第二段,3 位,系统类型
+ * 001 - 用户系统
+ * 002 - 商品系统
+ * 003 - 订单系统
+ * 004 - 支付系统
+ * 005 - 优惠劵系统
+ * ... - ...
+ * 第三段,3 位,模块
+ * 不限制规则。
+ * 一般建议,每个系统里面,可能有多个模块,可以再去做分段。以用户系统为例子:
+ * 001 - OAuth2 模块
+ * 002 - User 模块
+ * 003 - MobileCode 模块
+ * 第四段,3 位,错误码
+ * 不限制规则。
+ * 一般建议,每个模块自增。
+ *
+ * @author hongawen
+ */
+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)
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/exception/util/ServiceExceptionUtil.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/exception/util/ServiceExceptionUtil.java
new file mode 100644
index 0000000..1c3c032
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/exception/util/ServiceExceptionUtil.java
@@ -0,0 +1,77 @@
+package com.njcn.rdms.framework.common.exception.util;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.njcn.rdms.framework.common.exception.ErrorCode;
+import com.njcn.rdms.framework.common.exception.ServiceException;
+import com.njcn.rdms.framework.common.exception.enums.GlobalErrorCodeConstants;
+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();
+ }
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/pojo/CommonResult.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/pojo/CommonResult.java
new file mode 100644
index 0000000..9ada0b6
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/pojo/CommonResult.java
@@ -0,0 +1,121 @@
+package com.njcn.rdms.framework.common.pojo;
+
+import cn.hutool.core.lang.Assert;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.njcn.rdms.framework.common.exception.ErrorCode;
+import com.njcn.rdms.framework.common.exception.ServiceException;
+import com.njcn.rdms.framework.common.exception.enums.GlobalErrorCodeConstants;
+import com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * 通用返回
+ *
+ * @param 数据泛型
+ */
+@Data
+public class CommonResult implements Serializable {
+
+ /**
+ * 错误码
+ *
+ * @see ErrorCode#getCode()
+ */
+ private Integer code;
+ /**
+ * 错误提示,用户可阅读
+ *
+ * @see ErrorCode#getMsg() ()
+ */
+ private String msg;
+ /**
+ * 返回数据
+ */
+ private T data;
+
+ /**
+ * 将传入的 result 对象,转换成另外一个泛型结果的对象
+ *
+ * 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。
+ *
+ * @param result 传入的 result 对象
+ * @param 返回的泛型
+ * @return 新的 CommonResult 对象
+ */
+ public static CommonResult error(CommonResult> result) {
+ return error(result.getCode(), result.getMsg());
+ }
+
+ public static CommonResult error(Integer code, String message) {
+ Assert.notEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), code, "code 必须是错误的!");
+ CommonResult result = new CommonResult<>();
+ result.code = code;
+ result.msg = message;
+ return result;
+ }
+
+ public static CommonResult error(ErrorCode errorCode, Object... params) {
+ Assert.notEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), errorCode.getCode(), "code 必须是错误的!");
+ CommonResult result = new CommonResult<>();
+ result.code = errorCode.getCode();
+ result.msg = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMsg(), params);
+ return result;
+ }
+
+ public static CommonResult error(ErrorCode errorCode) {
+ return error(errorCode.getCode(), errorCode.getMsg());
+ }
+
+ public static CommonResult success(T data) {
+ CommonResult 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 CommonResult error(ServiceException serviceException) {
+ return error(serviceException.getCode(), serviceException.getMessage());
+ }
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/pojo/PageParam.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/pojo/PageParam.java
new file mode 100644
index 0000000..c5ee13a
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/pojo/PageParam.java
@@ -0,0 +1,36 @@
+package com.njcn.rdms.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 = "每页条数,最大值为 200", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+ @NotNull(message = "每页条数不能为空")
+ @Min(value = 1, message = "每页条数最小值为 1")
+ @Max(value = 200, message = "每页条数最大值为 200")
+ private Integer pageSize = PAGE_SIZE;
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/pojo/PageResult.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/pojo/PageResult.java
new file mode 100644
index 0000000..091fb92
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/pojo/PageResult.java
@@ -0,0 +1,41 @@
+package com.njcn.rdms.framework.common.pojo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+@Schema(description = "分页结果")
+@Data
+public final class PageResult implements Serializable {
+
+ @Schema(description = "总量", requiredMode = Schema.RequiredMode.REQUIRED)
+ private Long total;
+
+ @Schema(description = "数据", requiredMode = Schema.RequiredMode.REQUIRED)
+ private List list;
+
+ public PageResult() {
+ }
+
+ public PageResult(List list, Long total) {
+ this.list = list;
+ this.total = total;
+ }
+
+ public PageResult(Long total) {
+ this.list = new ArrayList<>();
+ this.total = total;
+ }
+
+ public static PageResult empty() {
+ return new PageResult<>(0L);
+ }
+
+ public static PageResult empty(Long total) {
+ return new PageResult<>(total);
+ }
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/pojo/SortablePageParam.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/pojo/SortablePageParam.java
new file mode 100644
index 0000000..b0d5686
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/pojo/SortablePageParam.java
@@ -0,0 +1,19 @@
+package com.njcn.rdms.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 sortingFields;
+
+}
\ No newline at end of file
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/pojo/SortingField.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/pojo/SortingField.java
new file mode 100644
index 0000000..f58d30e
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/pojo/SortingField.java
@@ -0,0 +1,37 @@
+package com.njcn.rdms.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;
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/cache/CacheUtils.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/cache/CacheUtils.java
new file mode 100644
index 0000000..4daa145
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/cache/CacheUtils.java
@@ -0,0 +1,61 @@
+package com.njcn.rdms.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 hongawen
+ */
+public class CacheUtils {
+
+ /**
+ * 异步刷新的 LoadingCache 最大缓存数量
+ *
+ * @see 本地缓存 CacheUtils 工具类建议
+ */
+ private static final Integer CACHE_MAX_SIZE = 10000;
+
+ /**
+ * 构建异步刷新的 LoadingCache 对象
+ *
+ * 注意:如果你的缓存和 ThreadLocal 有关系,要么自己处理 ThreadLocal 的传递,要么使用 {@link #buildCache(Duration, CacheLoader)} 方法
+ *
+ * 或者简单理解:
+ * 1、和“人”相关的,使用 {@link #buildCache(Duration, CacheLoader)} 方法
+ * 2、和“全局”、“系统”相关的,使用当前缓存方法
+ *
+ * @param duration 过期时间
+ * @param loader CacheLoader 对象
+ * @return LoadingCache 对象
+ */
+ public static LoadingCache buildAsyncReloadingCache(Duration duration, CacheLoader loader) {
+ return CacheBuilder.newBuilder()
+ .maximumSize(CACHE_MAX_SIZE)
+ // 只阻塞当前数据加载线程,其他线程返回旧值
+ .refreshAfterWrite(duration)
+ // 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程
+ .build(CacheLoader.asyncReloading(loader, Executors.newCachedThreadPool())); // TODO 可能要思考下,未来要不要做成可配置
+ }
+
+ /**
+ * 构建同步刷新的 LoadingCache 对象
+ *
+ * @param duration 过期时间
+ * @param loader CacheLoader 对象
+ * @return LoadingCache 对象
+ */
+ public static LoadingCache buildCache(Duration duration, CacheLoader loader) {
+ return CacheBuilder.newBuilder()
+ .maximumSize(CACHE_MAX_SIZE)
+ // 只阻塞当前数据加载线程,其他线程返回旧值
+ .refreshAfterWrite(duration)
+ .build(loader);
+ }
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/collection/ArrayUtils.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/collection/ArrayUtils.java
new file mode 100644
index 0000000..b1f4dea
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/collection/ArrayUtils.java
@@ -0,0 +1,58 @@
+package com.njcn.rdms.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.njcn.rdms.framework.common.util.collection.CollectionUtils.convertList;
+
+/**
+ * Array 工具类
+ *
+ * @author hongawen
+ */
+public class ArrayUtils {
+
+ /**
+ * 将 object 和 newElements 合并成一个数组
+ *
+ * @param object 对象
+ * @param newElements 数组
+ * @param 泛型
+ * @return 结果数组
+ */
+ @SafeVarargs
+ public static Consumer[] append(Consumer object, Consumer... newElements) {
+ if (object == null) {
+ return newElements;
+ }
+ Consumer[] result = ArrayUtil.newArray(Consumer.class, 1 + newElements.length);
+ result[0] = object;
+ System.arraycopy(newElements, 0, result, 1, newElements.length);
+ return result;
+ }
+
+ public static V[] toArray(Collection from, Function mapper) {
+ return toArray(convertList(from, mapper));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static T[] toArray(Collection from) {
+ if (CollectionUtil.isEmpty(from)) {
+ return (T[]) (new Object[0]);
+ }
+ return ArrayUtil.toArray(from, (Class) IterUtil.getElementType(from.iterator()));
+ }
+
+ public static T get(T[] array, int index) {
+ if (null == array || index >= array.length) {
+ return null;
+ }
+ return array[index];
+ }
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/collection/CollectionUtils.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/collection/CollectionUtils.java
new file mode 100644
index 0000000..4d19e6c
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/collection/CollectionUtils.java
@@ -0,0 +1,352 @@
+package com.njcn.rdms.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.njcn.rdms.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 hongawen
+ */
+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 boolean anyMatch(Collection from, Predicate predicate) {
+ return from.stream().anyMatch(predicate);
+ }
+
+ public static List filterList(Collection from, Predicate predicate) {
+ if (CollUtil.isEmpty(from)) {
+ return new ArrayList<>();
+ }
+ return from.stream().filter(predicate).collect(Collectors.toList());
+ }
+
+ public static List distinct(Collection from, Function keyMapper) {
+ if (CollUtil.isEmpty(from)) {
+ return new ArrayList<>();
+ }
+ return distinct(from, keyMapper, (t1, t2) -> t1);
+ }
+
+ public static List distinct(Collection from, Function keyMapper, BinaryOperator cover) {
+ if (CollUtil.isEmpty(from)) {
+ return new ArrayList<>();
+ }
+ return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values());
+ }
+
+ public static List convertList(T[] from, Function func) {
+ if (ArrayUtil.isEmpty(from)) {
+ return new ArrayList<>();
+ }
+ return convertList(Arrays.asList(from), func);
+ }
+
+ public static List convertList(Collection from, Function func) {
+ if (CollUtil.isEmpty(from)) {
+ return new ArrayList<>();
+ }
+ return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toList());
+ }
+
+ public static List convertList(Collection from, Function func, Predicate filter) {
+ if (CollUtil.isEmpty(from)) {
+ return new ArrayList<>();
+ }
+ return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList());
+ }
+
+ public static PageResult convertPage(PageResult from, Function func) {
+ if (ArrayUtil.isEmpty(from)) {
+ return new PageResult<>(from.getTotal());
+ }
+ return new PageResult<>(convertList(from.getList(), func), from.getTotal());
+ }
+
+ public static List convertListByFlatMap(Collection from,
+ Function> func) {
+ if (CollUtil.isEmpty(from)) {
+ return new ArrayList<>();
+ }
+ return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList());
+ }
+
+ public static List convertListByFlatMap(Collection from,
+ Function super T, ? extends U> mapper,
+ Function> 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 List mergeValuesFromMap(Map> map) {
+ return map.values()
+ .stream()
+ .flatMap(List::stream)
+ .collect(Collectors.toList());
+ }
+
+ public static Set convertSet(Collection from) {
+ return convertSet(from, v -> v);
+ }
+
+ public static Set convertSet(Collection from, Function func) {
+ if (CollUtil.isEmpty(from)) {
+ return new HashSet<>();
+ }
+ return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toSet());
+ }
+
+ public static Set convertSet(Collection from, Function func, Predicate filter) {
+ if (CollUtil.isEmpty(from)) {
+ return new HashSet<>();
+ }
+ return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet());
+ }
+
+ public static Map convertMapByFilter(Collection from, Predicate filter, Function keyFunc) {
+ if (CollUtil.isEmpty(from)) {
+ return new HashMap<>();
+ }
+ return from.stream().filter(filter).collect(Collectors.toMap(keyFunc, v -> v));
+ }
+
+ public static Set convertSetByFlatMap(Collection from,
+ Function> func) {
+ if (CollUtil.isEmpty(from)) {
+ return new HashSet<>();
+ }
+ return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet());
+ }
+
+ public static Set convertSetByFlatMap(Collection from,
+ Function super T, ? extends U> mapper,
+ Function> 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 Map convertMap(Collection from, Function keyFunc) {
+ if (CollUtil.isEmpty(from)) {
+ return new HashMap<>();
+ }
+ return convertMap(from, keyFunc, Function.identity());
+ }
+
+ public static Map convertMap(Collection from, Function keyFunc, Supplier extends Map> supplier) {
+ if (CollUtil.isEmpty(from)) {
+ return supplier.get();
+ }
+ return convertMap(from, keyFunc, Function.identity(), supplier);
+ }
+
+ public static Map convertMap(Collection from, Function keyFunc, Function valueFunc) {
+ if (CollUtil.isEmpty(from)) {
+ return new HashMap<>();
+ }
+ return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1);
+ }
+
+ public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, BinaryOperator mergeFunction) {
+ if (CollUtil.isEmpty(from)) {
+ return new HashMap<>();
+ }
+ return convertMap(from, keyFunc, valueFunc, mergeFunction, HashMap::new);
+ }
+
+ public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, Supplier extends Map> supplier) {
+ if (CollUtil.isEmpty(from)) {
+ return supplier.get();
+ }
+ return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1, supplier);
+ }
+
+ public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, BinaryOperator mergeFunction, Supplier extends Map> supplier) {
+ if (CollUtil.isEmpty(from)) {
+ return new HashMap<>();
+ }
+ return from.stream().collect(Collectors.toMap(keyFunc, valueFunc, mergeFunction, supplier));
+ }
+
+ public static Map> convertMultiMap(Collection from, Function keyFunc) {
+ if (CollUtil.isEmpty(from)) {
+ return new HashMap<>();
+ }
+ return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(t -> t, Collectors.toList())));
+ }
+
+ public static Map> convertMultiMap(Collection from, Function keyFunc, Function valueFunc) {
+ if (CollUtil.isEmpty(from)) {
+ return new HashMap<>();
+ }
+ return from.stream()
+ .collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList())));
+ }
+
+ // 暂时没想好名字,先以 2 结尾噶
+ public static Map> convertMultiMap2(Collection from, Function keyFunc, Function valueFunc) {
+ if (CollUtil.isEmpty(from)) {
+ return new HashMap<>();
+ }
+ return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet())));
+ }
+
+ public static Map convertImmutableMap(Collection from, Function keyFunc) {
+ if (CollUtil.isEmpty(from)) {
+ return Collections.emptyMap();
+ }
+ ImmutableMap.Builder 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 List> diffList(Collection oldList, Collection newList,
+ BiFunction sameFunc) {
+ List createList = new LinkedList<>(newList); // 默认都认为是新增的,后续会进行移除
+ List updateList = new ArrayList<>();
+ List deleteList = new ArrayList<>();
+
+ // 通过以 oldList 为主遍历,找出 updateList 和 deleteList
+ for (T oldObj : oldList) {
+ // 1. 寻找是否有匹配的
+ T foundObj = null;
+ for (Iterator 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 getFirst(List from) {
+ return !CollectionUtil.isEmpty(from) ? from.get(0) : null;
+ }
+
+ public static T findFirst(Collection from, Predicate predicate) {
+ return findFirst(from, predicate, Function.identity());
+ }
+
+ public static U findFirst(Collection from, Predicate predicate, Function func) {
+ if (CollUtil.isEmpty(from)) {
+ return null;
+ }
+ return from.stream().filter(predicate).findFirst().map(func).orElse(null);
+ }
+
+ public static > V getMaxValue(Collection from, Function 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 > V getMinValue(List from, Function 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 getMinObject(List from, Function valueFunc) {
+ if (CollUtil.isEmpty(from)) {
+ return null;
+ }
+ assert from.size() > 0; // 断言,避免告警
+ return from.stream().min(Comparator.comparing(valueFunc)).get();
+ }
+
+ public static > V getSumValue(Collection from, Function valueFunc,
+ BinaryOperator accumulator) {
+ return getSumValue(from, valueFunc, accumulator, null);
+ }
+
+ public static > V getSumValue(Collection from, Function valueFunc,
+ BinaryOperator 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 void addIfNotNull(Collection coll, T item) {
+ if (item == null) {
+ return;
+ }
+ coll.add(item);
+ }
+
+ public static Collection singleton(T obj) {
+ return obj == null ? Collections.emptyList() : Collections.singleton(obj);
+ }
+
+ public static List newArrayList(List> list) {
+ return list.stream().filter(Objects::nonNull).flatMap(Collection::stream).collect(Collectors.toList());
+ }
+
+ /**
+ * 转换为 LinkedHashSet
+ *
+ * @param 元素类型
+ * @param elementType 集合中元素类型
+ * @param value 被转换的值
+ * @return {@link LinkedHashSet}
+ */
+ @SuppressWarnings("unchecked")
+ public static LinkedHashSet toLinkedHashSet(Class elementType, Object value) {
+ return (LinkedHashSet) toCollection(LinkedHashSet.class, elementType, value);
+ }
+
+}
\ No newline at end of file
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/collection/MapUtils.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/collection/MapUtils.java
new file mode 100644
index 0000000..dd12603
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/collection/MapUtils.java
@@ -0,0 +1,112 @@
+package com.njcn.rdms.framework.common.util.collection;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.ObjUtil;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.njcn.rdms.framework.common.core.KeyValue;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+/**
+ * Map 工具类
+ *
+ * @author hongawen
+ */
+public class MapUtils {
+
+ /**
+ * 从哈希表表中,获得 keys 对应的所有 value 数组
+ *
+ * @param multimap 哈希表
+ * @param keys keys
+ * @return value 数组
+ */
+ public static List getList(Multimap multimap, Collection keys) {
+ List result = new ArrayList<>();
+ keys.forEach(k -> {
+ Collection 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 void findAndThen(Map map, K key, Consumer consumer) {
+ if (ObjUtil.isNull(key) || CollUtil.isEmpty(map)) {
+ return;
+ }
+ V value = map.get(key);
+ if (value == null) {
+ return;
+ }
+ consumer.accept(value);
+ }
+
+ public static Map convertMap(List> keyValues) {
+ Map map = Maps.newLinkedHashMapWithExpectedSize(keyValues.size());
+ keyValues.forEach(keyValue -> map.put(keyValue.getKey(), keyValue.getValue()));
+ return map;
+ }
+
+ /**
+ * 从 Map 中获取 BigDecimal 值
+ *
+ * @param map Map 数据源
+ * @param key 键名
+ * @return BigDecimal 值,解析失败或值为 null 时返回 null
+ */
+ public static BigDecimal getBigDecimal(Map map, String key) {
+ return getBigDecimal(map, key, null);
+ }
+
+ /**
+ * 从 Map 中获取 BigDecimal 值
+ *
+ * @param map Map 数据源
+ * @param key 键名
+ * @param defaultValue 默认值
+ * @return BigDecimal 值,解析失败或值为 null 时返回默认值
+ */
+ public static BigDecimal getBigDecimal(Map map, String key, BigDecimal defaultValue) {
+ if (map == null) {
+ return defaultValue;
+ }
+ Object value = map.get(key);
+ if (value == null) {
+ return defaultValue;
+ }
+ if (value instanceof BigDecimal) {
+ return (BigDecimal) value;
+ }
+ if (value instanceof Number) {
+ return BigDecimal.valueOf(((Number) value).doubleValue());
+ }
+ if (value instanceof String) {
+ try {
+ return new BigDecimal((String) value);
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+ return defaultValue;
+ }
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/collection/SetUtils.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/collection/SetUtils.java
new file mode 100644
index 0000000..1303aef
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/collection/SetUtils.java
@@ -0,0 +1,19 @@
+package com.njcn.rdms.framework.common.util.collection;
+
+import cn.hutool.core.collection.CollUtil;
+
+import java.util.Set;
+
+/**
+ * Set 工具类
+ *
+ * @author hongawen
+ */
+public class SetUtils {
+
+ @SafeVarargs
+ public static Set asSet(T... objs) {
+ return CollUtil.newHashSet(objs);
+ }
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/date/DateUtils.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/date/DateUtils.java
new file mode 100644
index 0000000..f3f34cb
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/date/DateUtils.java
@@ -0,0 +1,149 @@
+package com.njcn.rdms.framework.common.util.date;
+
+import cn.hutool.core.date.LocalDateTimeUtil;
+
+import java.time.*;
+import java.util.Calendar;
+import java.util.Date;
+
+/**
+ * 时间工具类
+ *
+ * @author hongawen
+ */
+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 month 月
+ * @param day 日
+ * @return 指定时间
+ */
+ public static Date buildTime(int year, int month, int day) {
+ return buildTime(year, month, day, 0, 0, 0);
+ }
+
+ /**
+ * 创建指定时间
+ *
+ * @param year 年
+ * @param month 月
+ * @param day 日
+ * @param hour 小时
+ * @param minute 分钟
+ * @param second 秒
+ * @return 指定时间
+ */
+ public static Date buildTime(int year, int month, int day,
+ int hour, int minute, int second) {
+ Calendar calendar = Calendar.getInstance();
+ calendar.set(Calendar.YEAR, year);
+ calendar.set(Calendar.MONTH, month - 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));
+ }
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/date/LocalDateTimeUtils.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/date/LocalDateTimeUtils.java
new file mode 100644
index 0000000..4122b22
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/date/LocalDateTimeUtils.java
@@ -0,0 +1,351 @@
+package com.njcn.rdms.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.date.TemporalAccessorUtil;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.StrUtil;
+import com.njcn.rdms.framework.common.enums.DateIntervalEnum;
+
+import java.sql.Timestamp;
+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 LocalDateTime}
+ *
+ * @author hongawen
+ */
+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 month 月
+ * @param day 日
+ * @return 指定时间
+ */
+ public static LocalDateTime buildTime(int year, int month, int day) {
+ return LocalDateTime.of(year, month, day, 0, 0, 0);
+ }
+
+ public static LocalDateTime[] buildBetweenTime(int year1, int month1, int day1,
+ int year2, int month2, int day2) {
+ return new LocalDateTime[]{buildTime(year1, month1, day1), buildTime(year2, month2, day2)};
+ }
+
+ /**
+ * 判指定断时间,是否在该时间范围内
+ *
+ * @param startTime 开始时间
+ * @param endTime 结束时间
+ * @param time 指定时间
+ * @return 是否
+ */
+ public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime, Timestamp time) {
+ if (startTime == null || endTime == null || time == null) {
+ return false;
+ }
+ return LocalDateTimeUtil.isIn(LocalDateTimeUtil.of(time), startTime, endTime);
+ }
+
+ /**
+ * 判指定断时间,是否在该时间范围内
+ *
+ * @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 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 timeRanges = new ArrayList<>();
+ switch (intervalEnum) {
+ case HOUR:
+ while (startTime.isBefore(endTime)) {
+ timeRanges.add(new LocalDateTime[]{startTime, startTime.plusHours(1).minusNanos(1)});
+ startTime = startTime.plusHours(1);
+ }
+ 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 HOUR:
+ return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATETIME_MINUTE_PATTERN);
+ 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);
+ }
+ }
+
+ /**
+ * 将给定的 {@link LocalDateTime} 转换为自 Unix 纪元时间(1970-01-01T00:00:00Z)以来的秒数。
+ *
+ * @param sourceDateTime 需要转换的本地日期时间,不能为空
+ * @return 自 1970-01-01T00:00:00Z 起的秒数(epoch second)
+ * @throws NullPointerException 如果 {@code sourceDateTime} 为 {@code null}
+ * @throws DateTimeException 如果转换过程中发生时间超出范围或其他时间处理异常
+ */
+ public static Long toEpochSecond(LocalDateTime sourceDateTime) {
+ return TemporalAccessorUtil.toInstant(sourceDateTime).getEpochSecond();
+ }
+
+}
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/http/HttpUtils.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/http/HttpUtils.java
new file mode 100644
index 0000000..d3a30c3
--- /dev/null
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/util/http/HttpUtils.java
@@ -0,0 +1,193 @@
+package com.njcn.rdms.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.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+/**
+ * HTTP 工具类
+ *
+ * @author hongawen
+ */
+public class HttpUtils {
+
+ /**
+ * 编码 URL 参数
+ *
+ * @param value 参数
+ * @return 编码后的参数
+ */
+ public static String encodeUtf8(String value) {
+ return URLEncoder.encode(value, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * 解码 URL 参数
+ *
+ * @param value 参数
+ * @return 解码后的参数
+ */
+ public static String decodeUtf8(String value) {
+ return URLDecoder.decode(value, StandardCharsets.UTF_8);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static String replaceUrlQuery(String url, String key, String value) {
+ UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());
+ // 先移除
+ TableMap