微调
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -73,3 +73,6 @@ functions/mock
|
|||||||
screenshot
|
screenshot
|
||||||
.firebase
|
.firebase
|
||||||
sessionStore
|
sessionStore
|
||||||
|
|
||||||
|
# local docs
|
||||||
|
/docs/
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
# rdms-spring-boot-starter-biz-ip
|
|
||||||
|
|
||||||
## 模块定位
|
|
||||||
这是一个本地静态工具模块,用于:
|
|
||||||
|
|
||||||
1. IP -> 地区编码查询
|
|
||||||
2. 地区树查询与地区名称格式化
|
|
||||||
|
|
||||||
不包含远程调用,不包含自动配置,不包含智能识别能力。
|
|
||||||
|
|
||||||
## 它做什么
|
|
||||||
|
|
||||||
1. 基于 `ip2region.xdb` 做 IP 归属地编码查询(离线、本地内存查询)
|
|
||||||
2. 基于 `area.csv` 提供地区树、地区路径、父级区域定位等工具方法
|
|
||||||
|
|
||||||
主要入口类:
|
|
||||||
|
|
||||||
1. `com.njcn.rdms.framework.ip.core.utils.IPUtils`
|
|
||||||
2. `com.njcn.rdms.framework.ip.core.utils.AreaUtils`
|
|
||||||
|
|
||||||
## 它不做什么
|
|
||||||
|
|
||||||
1. 不访问外部 IP 服务
|
|
||||||
2. 不保证行政区数据实时更新(数据随资源文件版本)
|
|
||||||
3. 不负责业务策略(如风控、推荐、画像)
|
|
||||||
|
|
||||||
## 资源与代价
|
|
||||||
|
|
||||||
1. 内置资源文件:
|
|
||||||
2. `src/main/resources/ip2region.xdb`(约 4MB)
|
|
||||||
3. `src/main/resources/area.csv`
|
|
||||||
4. 类加载时会预加载资源到内存,换取查询速度
|
|
||||||
|
|
||||||
## 前端地区树返回(重点)
|
|
||||||
|
|
||||||
本模块非常适合做省市区级联选择器的后端数据源。
|
|
||||||
|
|
||||||
典型接口:
|
|
||||||
|
|
||||||
1. 管理端:`GET /system/area/tree`
|
|
||||||
2. App 端:`GET /system/area/tree`
|
|
||||||
|
|
||||||
节点字段:
|
|
||||||
|
|
||||||
1. `id`:区域编码
|
|
||||||
2. `name`:区域名称
|
|
||||||
3. `children`:子节点列表
|
|
||||||
|
|
||||||
## 后端示例(获取“江苏区域树”)
|
|
||||||
|
|
||||||
### 1. 获取江苏节点
|
|
||||||
|
|
||||||
```java
|
|
||||||
import com.njcn.rdms.framework.ip.core.Area;
|
|
||||||
import com.njcn.rdms.framework.ip.core.utils.AreaUtils;
|
|
||||||
|
|
||||||
Area jiangsu = AreaUtils.parseArea("中国/江苏省");
|
|
||||||
if (jiangsu == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 直接返回江苏及其下级区域树
|
|
||||||
|
|
||||||
`Area` 本身就是树节点(包含 `children`),拿到江苏节点后即可把它作为一棵子树返回。
|
|
||||||
|
|
||||||
```java
|
|
||||||
import com.njcn.rdms.framework.ip.core.Area;
|
|
||||||
import com.njcn.rdms.framework.ip.core.utils.AreaUtils;
|
|
||||||
|
|
||||||
Area jiangsu = AreaUtils.parseArea("中国/江苏省");
|
|
||||||
return jiangsu; // children 中包含南京、苏州等下级节点
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 转成前端常用结构(id/name/children)
|
|
||||||
|
|
||||||
```java
|
|
||||||
import com.njcn.rdms.framework.ip.core.Area;
|
|
||||||
import com.njcn.rdms.framework.ip.core.utils.AreaUtils;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
record AreaNode(Integer id, String name, List<AreaNode> children) {}
|
|
||||||
|
|
||||||
private static AreaNode toNode(Area a) {
|
|
||||||
List<AreaNode> children = a.getChildren() == null
|
|
||||||
? Collections.emptyList()
|
|
||||||
: a.getChildren().stream().map(child -> toNode(child)).collect(Collectors.toList());
|
|
||||||
return new AreaNode(a.getId(), a.getName(), children);
|
|
||||||
}
|
|
||||||
|
|
||||||
Area jiangsu = AreaUtils.parseArea("中国/江苏省");
|
|
||||||
AreaNode jiangsuTree = jiangsu == null ? null : toNode(jiangsu);
|
|
||||||
```
|
|
||||||
|
|
||||||
返回示例:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": 320000,
|
|
||||||
"name": "江苏省",
|
|
||||||
"children": [
|
|
||||||
{ "id": 320100, "name": "南京市", "children": [] },
|
|
||||||
{ "id": 320500, "name": "苏州市", "children": [] }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 适用场景
|
|
||||||
|
|
||||||
1. 后端需要快速把 IP 转成地区信息
|
|
||||||
2. 需要返回地区树给前端做省市区联动
|
|
||||||
|
|
||||||
## 不需要它的场景
|
|
||||||
|
|
||||||
1. 项目没有 IP 归属地需求
|
|
||||||
2. 项目不需要地区树能力
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
这个模块的核心作用是“环境标签(tag)透传 + 按标签路由”。
|
|
||||||
|
|
||||||
它具体做了什么
|
|
||||||
|
|
||||||
1. 启动早期把 rdms.env.tag 映射到 Nacos 实例 metadata (spring.cloud.nacos.discovery.metadata.tag)。
|
|
||||||
文件: EnvEnvironmentPostProcessor.java
|
|
||||||
2. Web 请求进来时读取请求头 tag,放到线程上下文。
|
|
||||||
文件: EnvWebFilter.java
|
|
||||||
3. Feign 调用时把上下文里的 tag 继续写到下游请求头。
|
|
||||||
文件: EnvLoadBalancerClient.java
|
|
||||||
|
|
||||||
rdms.env.tag 在这个项目里是可选的,不配也能跑。
|
|
||||||
代码里明确是“有就生效、没有就跳过”:
|
|
||||||
- 读取 rdms.env.tag,为空就直接返回,不改路由标签
|
|
||||||
EnvEnvironmentPostProcessor.java
|
|
||||||
- application-local.yaml 里配了 rdms.env.tag: ${HOSTNAME},而 dev 没配
|
|
||||||
application-local.yaml
|
|
||||||
它的目的就是你说的这种场景之一:
|
|
||||||
多人共用一个 Nacos namespace/集群时,用 tag 做流量隔离(比如“我发起的请求尽量打到我这套实例”)。
|
|
||||||
再具体点,作用有三层:
|
|
||||||
1. 给本实例打 Nacos metadata tag。
|
|
||||||
2. 接口收到 tag 请求头后放到上下文。
|
|
||||||
3. Feign/LB 优先选同 tag 实例。
|
|
||||||
所以 dev 不配 tag,通常代表该环境不需要“按人/按版本隔离流量”,走默认路由即可。
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
# rdms-spring-boot-starter-excel
|
|
||||||
|
|
||||||
## 模块定位
|
|
||||||
这是一个面向业务的 Excel 能力模块,核心目标是:
|
|
||||||
|
|
||||||
1. 降低导入导出开发成本(统一工具类)
|
|
||||||
2. 让业务字段和 Excel 展示语义解耦(注解 + Converter)
|
|
||||||
3. 把字典能力接入 Excel 场景(值与标签互转、校验、下拉选项)
|
|
||||||
|
|
||||||
## 设计思路
|
|
||||||
|
|
||||||
1. 统一入口:通过 `ExcelUtils` 封装读写,Controller 只关心 VO 和数据。
|
|
||||||
2. 注解驱动:通过 `@DictFormat`、`@ExcelColumnSelect` 把“字段语义”放在 VO 上,而不是散落在业务代码里。
|
|
||||||
3. 转换器分层:`DictConvert`、`AreaConvert`、`JsonConvert`、`MoneyConvert` 分别处理不同类型转换。
|
|
||||||
4. 字典缓存:`DictFrameworkUtils` 基于缓存读取字典,减少重复远程调用。
|
|
||||||
5. 导出体验:自动列宽、下拉选项、Long 防精度丢失等细节统一在模块内处理。
|
|
||||||
|
|
||||||
## 核心类
|
|
||||||
|
|
||||||
1. `com.njcn.rdms.framework.excel.core.util.ExcelUtils`
|
|
||||||
2. `com.njcn.rdms.framework.excel.core.convert.DictConvert`
|
|
||||||
3. `com.njcn.rdms.framework.excel.core.handler.SelectSheetWriteHandler`
|
|
||||||
4. `com.njcn.rdms.framework.dict.core.DictFrameworkUtils`
|
|
||||||
5. `com.njcn.rdms.framework.dict.validation.InDict`
|
|
||||||
|
|
||||||
## 示例:用户导入导出(含字典转换)
|
|
||||||
|
|
||||||
### 1. 定义 Excel VO
|
|
||||||
|
|
||||||
```java
|
|
||||||
import cn.idev.excel.annotation.ExcelProperty;
|
|
||||||
import com.njcn.rdms.framework.excel.core.annotations.DictFormat;
|
|
||||||
import com.njcn.rdms.framework.excel.core.convert.DictConvert;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class UserImportExcelVO {
|
|
||||||
|
|
||||||
@ExcelProperty("登录名称")
|
|
||||||
private String username;
|
|
||||||
|
|
||||||
@ExcelProperty(value = "用户性别", converter = DictConvert.class)
|
|
||||||
@DictFormat("USER_SEX")
|
|
||||||
private Integer sex;
|
|
||||||
|
|
||||||
@ExcelProperty(value = "账号状态", converter = DictConvert.class)
|
|
||||||
@DictFormat("COMMON_STATUS")
|
|
||||||
private Integer status;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 导出模板
|
|
||||||
|
|
||||||
```java
|
|
||||||
import com.njcn.rdms.framework.excel.core.util.ExcelUtils;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
public void exportTemplate(HttpServletResponse response) throws Exception {
|
|
||||||
ExcelUtils.write(response, "用户导入模板.xls", "用户列表", UserImportExcelVO.class, List.of());
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 导入解析
|
|
||||||
|
|
||||||
```java
|
|
||||||
import com.njcn.rdms.framework.excel.core.util.ExcelUtils;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
public List<UserImportExcelVO> importExcel(MultipartFile file) throws Exception {
|
|
||||||
return ExcelUtils.read(file, UserImportExcelVO.class);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
说明:
|
|
||||||
|
|
||||||
1. 导入时 `DictConvert` 会把 Excel 标签值转回字典 value(如“男”->`1`)。
|
|
||||||
2. 导出时 `DictConvert` 会把字典 value 转成可读标签(如 `1`->“男”)。
|
|
||||||
|
|
||||||
## 适用场景
|
|
||||||
|
|
||||||
1. 后台管理常规 Excel 导入导出
|
|
||||||
2. 字典字段较多、希望自动做值/标签转换
|
|
||||||
3. 需要生成带下拉选项的导入模板
|
|
||||||
|
|
||||||
## 不适用场景
|
|
||||||
|
|
||||||
1. 超大规模离线数据处理(建议走专用批处理链路)
|
|
||||||
2. 复杂流式处理或多工作簿复杂编排(建议单独实现)
|
|
||||||
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
# MQ 改造方案(评审稿)
|
|
||||||
|
|
||||||
## 1. 背景与目标
|
|
||||||
你当前的业务策略是:
|
|
||||||
1. 单体部署优先使用 Redis 作为 MQ。
|
|
||||||
2. 微服务部署优先使用 RocketMQ 作为 MQ。
|
|
||||||
3. RabbitMQ、Kafka 暂时不作为主路径,仅保留包结构隔离,不参与核心链路。
|
|
||||||
|
|
||||||
本方案目标是:
|
|
||||||
1. 在不破坏现有系统的前提下,建立可切换的 MQ 入口。
|
|
||||||
2. 把“切换成本”收敛到配置层,而不是业务代码层。
|
|
||||||
3. 采用分阶段改造,先低风险,后统一抽象。
|
|
||||||
|
|
||||||
## 1.1 本期非目标(避免范围蔓延)
|
|
||||||
本期改造明确不包含:
|
|
||||||
1. 严格顺序语义保障(全链路有序消费)。
|
|
||||||
2. 统一死信队列(DLQ)治理体系。
|
|
||||||
3. 事务消息与最终一致性框架化封装。
|
|
||||||
4. 多机房容灾级别的 MQ 治理。
|
|
||||||
|
|
||||||
说明:
|
|
||||||
1. 本期先完成“可切换、可回滚、可观测”的基础能力。
|
|
||||||
2. 复杂语义能力后续在 RocketMQ 路线下单独立项。
|
|
||||||
|
|
||||||
## 2. 当前实现现状(简版)
|
|
||||||
当前 `rdms-spring-boot-starter-mq` 的真实情况:
|
|
||||||
1. Redis 能力最完整,包含模板、监听器抽象、Stream 补偿与清理任务。
|
|
||||||
2. RabbitMQ 只有 `MessageConverter` 级别自动配置,未形成统一收发抽象。
|
|
||||||
|
|
||||||
当前 `rdms-system` 默认配置:
|
|
||||||
|
|
||||||
## 3. 改造原则
|
|
||||||
1. 先配置统一,再接口统一,最后再清理非主路径。
|
|
||||||
2. 任何阶段都必须可回滚,且回滚只改配置不改代码。
|
|
||||||
3. 保持 Rabbit/Kafka 包路径存在,避免一次性大删导致历史分支合并困难。
|
|
||||||
|
|
||||||
## 4. 目标架构(落地后)
|
|
||||||
统一引入配置:
|
|
||||||
1. `rdms.mq.type=redis|rocketmq`
|
|
||||||
|
|
||||||
统一行为:
|
|
||||||
1. 单体环境配 `redis`。
|
|
||||||
2. 微服务环境配 `rocketmq`。
|
|
||||||
|
|
||||||
建议方式:
|
|
||||||
|
|
||||||
## 4.1 配置矩阵(最小可运行)
|
|
||||||
|
|
||||||
### 单体(Redis)
|
|
||||||
```yaml
|
|
||||||
rdms:
|
|
||||||
mq:
|
|
||||||
type: redis
|
|
||||||
|
|
||||||
spring:
|
|
||||||
data:
|
|
||||||
redis:
|
|
||||||
host: 127.0.0.1
|
|
||||||
port: 6379
|
|
||||||
```
|
|
||||||
|
|
||||||
### 微服务(RocketMQ)
|
|
||||||
```yaml
|
|
||||||
rdms:
|
|
||||||
mq:
|
|
||||||
type: rocketmq
|
|
||||||
|
|
||||||
rocketmq:
|
|
||||||
name-server: 127.0.0.1:9876
|
|
||||||
producer:
|
|
||||||
group: rdms-producer-group
|
|
||||||
```
|
|
||||||
|
|
||||||
备注:
|
|
||||||
1. 微服务切 Rocket 前,必须先补齐 topic、consumer-group 等业务配置。
|
|
||||||
2. 建议在 dev/local 保留 Redis 配置,便于快速回滚。
|
|
||||||
|
|
||||||
## 4.2 命名规范(Topic / Channel / StreamKey)
|
|
||||||
建议统一命名规则,避免后续混乱:
|
|
||||||
1. Redis Channel / StreamKey:`{env}:{domain}:{event}`
|
|
||||||
2. Rocket Topic:`{env}_{domain}_{event}`
|
|
||||||
3. ConsumerGroup:`{app}-{domain}-cg`
|
|
||||||
|
|
||||||
示例:
|
|
||||||
1. `dev:system:notify`
|
|
||||||
2. `prod_system_notify`
|
|
||||||
3. `rdms-system-notify-cg`
|
|
||||||
|
|
||||||
## 5. 分阶段实施计划
|
|
||||||
|
|
||||||
## 阶段 A(低风险,建议先做)
|
|
||||||
目标:只做配置层统一,不动大规模代码。
|
|
||||||
|
|
||||||
实施项:
|
|
||||||
1. 新增配置项 `rdms.mq.type`,默认值 `redis`。
|
|
||||||
3. 本阶段不删除 Rabbit/Kafka 代码,不变更包结构。
|
|
||||||
|
|
||||||
收益:
|
|
||||||
1. 切换 Redis/Rocket 只改配置。
|
|
||||||
2. 不触碰核心业务逻辑,风险最低。
|
|
||||||
|
|
||||||
回滚:
|
|
||||||
|
|
||||||
## 阶段 B(中风险,接口统一)
|
|
||||||
目标:抽象统一 MQ 发送接口,减少业务对具体中间件的耦合。
|
|
||||||
|
|
||||||
实施项:
|
|
||||||
1. 新增统一接口,例如 `UnifiedMqSender`。
|
|
||||||
2. 提供 `RedisMqSender` 与 `RocketMqSender` 两个实现。
|
|
||||||
3. 用 `@ConditionalOnProperty` 根据 `rdms.mq.type` 注入唯一实现。
|
|
||||||
5. 统一消息契约,至少包含标准消息头:
|
|
||||||
`msgId`、`bizKey`、`timestamp`、`producer`、`traceId`、`version`。
|
|
||||||
|
|
||||||
收益:
|
|
||||||
1. 业务层不再直接依赖 `RedisMQTemplate` 或 `RocketMQTemplate`。
|
|
||||||
2. 后续切换中间件时改动面可控。
|
|
||||||
3. 消息协议可演进,减少多团队并行开发冲突。
|
|
||||||
|
|
||||||
回滚:
|
|
||||||
1. 切回原 sender Bean 注入方案,保留统一接口代码但不启用。
|
|
||||||
|
|
||||||
## 阶段 C(可选,结构收敛)
|
|
||||||
目标:让非主路径代码“隔离可见但不激活”。
|
|
||||||
|
|
||||||
实施项:
|
|
||||||
1. Rabbit/Kafka 相关自动配置入口默认关闭。
|
|
||||||
2. 保留目录与类,增加注释标识 `reserved` 或 `deprecated`。
|
|
||||||
3. 文档明确“当前生产主路径仅 Redis/Rocket”。
|
|
||||||
|
|
||||||
收益:
|
|
||||||
1. 新成员不会误判项目“全量支持四种 MQ”。
|
|
||||||
2. 后续若要恢复 Rabbit/Kafka,可低成本重新开启。
|
|
||||||
|
|
||||||
回滚:
|
|
||||||
1. 打开对应配置开关即可恢复。
|
|
||||||
|
|
||||||
## 6. 影响面评估
|
|
||||||
主要影响模块:
|
|
||||||
1. `rdms-framework/rdms-spring-boot-starter-mq`
|
|
||||||
3. `rdms-system/rdms-system-boot` 配置文件
|
|
||||||
|
|
||||||
主要风险点:
|
|
||||||
1. Redis 与 Rocket 的消费语义不完全一致,幂等要在业务侧兜底。
|
|
||||||
2. 广播模型下,多实例重复消费属于预期,业务处理要避免副作用。
|
|
||||||
3. 配置切换后,缺失 Rocket 连接配置会导致启动失败。
|
|
||||||
4. 现有代码中 Kafka 消费类注解存在明显可疑项,改造时应单独复核。
|
|
||||||
5. 缺少统一幂等策略时,Rocket 与 Redis 切换后可能放大重复消费副作用。
|
|
||||||
6. 配置缺失时若做了自动降级,可能导致“误以为切到 Rocket,实际走本地/Redis”。
|
|
||||||
|
|
||||||
## 6.1 幂等策略(建议作为阶段 B 配套)
|
|
||||||
最小策略建议:
|
|
||||||
1. 每条消息必须带 `msgId` 与 `bizKey`。
|
|
||||||
2. 以 `bizKey` 作为业务幂等键(例如订单号、任务号)。
|
|
||||||
3. 消费前先做幂等检查,消费成功后写入幂等记录。
|
|
||||||
4. 幂等记录建议放 Redis,设置合理 TTL(如 3~7 天,按业务回放窗口)。
|
|
||||||
|
|
||||||
说明:
|
|
||||||
1. `msgId` 解决“传输级去重定位”,`bizKey` 解决“业务级幂等”。
|
|
||||||
2. 幂等是切换 MQ 前置条件,不应后补。
|
|
||||||
|
|
||||||
## 6.2 故障切换策略(明确 fail-fast)
|
|
||||||
建议默认策略:
|
|
||||||
1. 目标 MQ 不可用时 `fail-fast`,启动或发送直接失败并告警。
|
|
||||||
2. 禁止隐式降级到 local,避免行为不透明。
|
|
||||||
|
|
||||||
可选策略:
|
|
||||||
1. 在开发环境允许显式降级(需开关控制,并打印告警日志)。
|
|
||||||
|
|
||||||
## 7. 验收清单(每阶段都要过)
|
|
||||||
1. 单体环境 `rdms.mq.type=redis` 可启动、可发送、可消费。
|
|
||||||
2. 微服务环境 `rdms.mq.type=rocketmq` 可启动、可发送、可消费。
|
|
||||||
4. 关键链路日志可定位发送端与消费端。
|
|
||||||
5. 切换只改配置,不改代码。
|
|
||||||
6. 回滚路径已验证。
|
|
||||||
7. 幂等校验通过(重复消息不会造成业务副作用)。
|
|
||||||
8. 关键指标可观测并有告警(发送失败、消费失败、积压、重试)。
|
|
||||||
|
|
||||||
## 7.1 观测与告警建议
|
|
||||||
建议统一接入以下指标:
|
|
||||||
1. `mq_send_total` / `mq_send_fail_total`
|
|
||||||
2. `mq_consume_total` / `mq_consume_fail_total`
|
|
||||||
3. `mq_backlog_size`
|
|
||||||
4. `mq_retry_total`
|
|
||||||
|
|
||||||
建议阈值(示例):
|
|
||||||
1. 5 分钟发送失败率 > 1% 告警。
|
|
||||||
2. 连续 10 分钟消费失败率 > 0.5% 告警。
|
|
||||||
3. 积压超过基线 3 倍且持续 10 分钟告警。
|
|
||||||
|
|
||||||
## 8. 推荐实施顺序与工时
|
|
||||||
推荐顺序:
|
|
||||||
1. 先做阶段 A。
|
|
||||||
2. 阶段 A 稳定后再做阶段 B。
|
|
||||||
3. 阶段 C 按团队节奏处理。
|
|
||||||
|
|
||||||
粗略工时(含联调):
|
|
||||||
1. 阶段 A:0.5 天。
|
|
||||||
2. 阶段 B:1 天到 1.5 天。
|
|
||||||
3. 阶段 C:0.5 天。
|
|
||||||
|
|
||||||
## 8.1 上线与回滚步骤(建议)
|
|
||||||
上线顺序:
|
|
||||||
1. 开发环境完成阶段 A,验证双配置切换。
|
|
||||||
2. 预发环境灰度 1 个实例切换目标 MQ。
|
|
||||||
3. 小流量观察 1 天,确认无异常后全量。
|
|
||||||
|
|
||||||
回滚触发条件(任一满足即回滚):
|
|
||||||
1. 消费失败率持续超过阈值。
|
|
||||||
2. 积压持续增长且无法在观察窗口内回落。
|
|
||||||
3. 关键业务出现重复消费副作用。
|
|
||||||
|
|
||||||
回滚动作:
|
|
||||||
2. 不回滚代码,确保恢复路径最短。
|
|
||||||
|
|
||||||
## 9. 当前建议
|
|
||||||
你现在对项目结构还在熟悉期,建议先做阶段 A 的评审与验证,不直接进入阶段 B。
|
|
||||||
这样能先拿到“可切换能力”,同时把改造风险控制在最低范围。
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
# rdms-spring-boot-starter-mq
|
|
||||||
|
|
||||||
## 模块定位
|
|
||||||
`rdms-spring-boot-starter-mq` 是项目里的消息队列基础封装,目标是:
|
|
||||||
1. 对 Redis 消息模型做统一抽象,降低业务接入复杂度
|
|
||||||
2. 通过自动配置按需启用消费者容器,不强制业务使用全部能力
|
|
||||||
3. 为后续多 MQ 方案(Redis / RabbitMQ / RocketMQ / Kafka)提供统一入口
|
|
||||||
|
|
||||||
当前实现里,**核心能力在 Redis**,RabbitMQ 是轻量补充(消息转换器)。
|
|
||||||
|
|
||||||
## 设计思路
|
|
||||||
|
|
||||||
### 1. Redis 统一模型
|
|
||||||
Redis 相关能力分成两类:
|
|
||||||
1. `Pub/Sub` 广播模型
|
|
||||||
2. `Stream` 分组消费模型
|
|
||||||
|
|
||||||
两者的消息基类分别是:
|
|
||||||
1. `AbstractRedisChannelMessage`
|
|
||||||
2. `AbstractRedisStreamMessage`
|
|
||||||
|
|
||||||
共同父类是 `AbstractRedisMessage`,内置 `headers`,方便扩展链路信息。
|
|
||||||
|
|
||||||
### 2. 统一发送模板
|
|
||||||
发送端统一走 `RedisMQTemplate`:
|
|
||||||
1. `send(AbstractRedisChannelMessage)` -> Redis `convertAndSend`
|
|
||||||
2. `send(AbstractRedisStreamMessage)` -> Redis Stream `XADD`
|
|
||||||
|
|
||||||
这样业务代码不需要关心底层命令细节。
|
|
||||||
|
|
||||||
### 3. 监听器抽象 + 惰性自动装配
|
|
||||||
消费者由两个抽象监听器承接:
|
|
||||||
1. `AbstractRedisChannelMessageListener<T>`
|
|
||||||
2. `AbstractRedisStreamMessageListener<T>`
|
|
||||||
|
|
||||||
自动配置采用 `@ConditionalOnBean(...)`:
|
|
||||||
1. 只有你定义了对应监听器 Bean,容器才会注册消费者
|
|
||||||
2. 没有监听器时不会额外启动 MQ 消费组件
|
|
||||||
|
|
||||||
### 4. Stream 运维补偿
|
|
||||||
针对 Redis Stream,框架额外提供两个定时任务:
|
|
||||||
1. `RedisPendingMessageResendJob`
|
|
||||||
扫描 pending 超时消息并重投(默认超时 5 分钟)
|
|
||||||
2. `RedisStreamMessageCleanupJob`
|
|
||||||
定时 `XTRIM`,默认仅保留最近 10000 条,防止内存膨胀
|
|
||||||
|
|
||||||
两者都使用 Redisson 分布式锁,避免多实例重复执行。
|
|
||||||
|
|
||||||
### 5. RabbitMQ 轻封装
|
|
||||||
`RdmsRabbitMQAutoConfiguration` 只提供 `Jackson2JsonMessageConverter`,让 RabbitTemplate / @RabbitListener 默认按 JSON 处理消息。
|
|
||||||
|
|
||||||
### 6. 多 MQ 的真实边界
|
|
||||||
虽然模块描述写了支持 Redis / RocketMQ / RabbitMQ / Kafka,
|
|
||||||
但本 starter 的自动配置主要是 Redis + Rabbit。
|
|
||||||
|
|
||||||
## 自动配置入口
|
|
||||||
`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`:
|
|
||||||
1. `RdmsRedisMQProducerAutoConfiguration`
|
|
||||||
2. `RdmsRedisMQConsumerAutoConfiguration`
|
|
||||||
3. `RdmsRabbitMQAutoConfiguration`
|
|
||||||
|
|
||||||
## 如何使用
|
|
||||||
|
|
||||||
## 1. 引入依赖
|
|
||||||
一般由业务模块直接依赖:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.njcn</groupId>
|
|
||||||
<artifactId>rdms-spring-boot-starter-mq</artifactId>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 1.1 项目现有 Redis 作为 MQ 的常规用法(推荐)
|
|
||||||
推荐按这个顺序接入:
|
|
||||||
1. 配好 `spring.data.redis.*`。
|
|
||||||
2. 先按第 2 节使用 `Pub/Sub`(这是项目里最常见路径)。
|
|
||||||
3. 需要可恢复消费时,再按第 3 节接入 `Stream`。
|
|
||||||
|
|
||||||
## 2. 使用 Redis Pub/Sub(广播)
|
|
||||||
适用场景: 通知广播、在线会话广播、对可靠性要求不高但强调实时性。
|
|
||||||
|
|
||||||
### 2.1 定义消息
|
|
||||||
```java
|
|
||||||
import com.njcn.rdms.framework.mq.redis.core.pubsub.AbstractRedisChannelMessage;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class DemoNotifyMessage extends AbstractRedisChannelMessage {
|
|
||||||
|
|
||||||
private Long userId;
|
|
||||||
private String content;
|
|
||||||
|
|
||||||
// 可选: 自定义 channel;不重写时默认是类名
|
|
||||||
@Override
|
|
||||||
public String getChannel() {
|
|
||||||
return "demo:notify";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 定义消费者
|
|
||||||
```java
|
|
||||||
import com.njcn.rdms.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
public class DemoNotifyListener extends AbstractRedisChannelMessageListener<DemoNotifyMessage> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMessage(DemoNotifyMessage message) {
|
|
||||||
log.info("收到广播消息 userId={}, content={}", message.getUserId(), message.getContent());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.3 发送消息
|
|
||||||
```java
|
|
||||||
import com.njcn.rdms.framework.mq.redis.core.RedisMQTemplate;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class DemoNotifyProducer {
|
|
||||||
|
|
||||||
private final RedisMQTemplate redisMQTemplate;
|
|
||||||
|
|
||||||
public void send(Long userId, String content) {
|
|
||||||
redisMQTemplate.send(new DemoNotifyMessage()
|
|
||||||
.setUserId(userId)
|
|
||||||
.setContent(content));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. 使用 Redis Stream(分组消费 + ACK)
|
|
||||||
适用场景: 异步任务、可恢复消费、希望具备重投和清理机制。
|
|
||||||
|
|
||||||
### 3.1 定义消息
|
|
||||||
```java
|
|
||||||
import com.njcn.rdms.framework.mq.redis.core.stream.AbstractRedisStreamMessage;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class DemoTaskMessage extends AbstractRedisStreamMessage {
|
|
||||||
|
|
||||||
private Long taskId;
|
|
||||||
private String bizType;
|
|
||||||
|
|
||||||
// 可选: 自定义 stream key;不重写时默认是类名
|
|
||||||
@Override
|
|
||||||
public String getStreamKey() {
|
|
||||||
return "demo:task:stream";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 定义消费者
|
|
||||||
```java
|
|
||||||
import com.njcn.rdms.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
public class DemoTaskListener extends AbstractRedisStreamMessageListener<DemoTaskMessage> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMessage(DemoTaskMessage message) {
|
|
||||||
log.info("消费任务 taskId={}, bizType={}", message.getTaskId(), message.getBizType());
|
|
||||||
// 这里抛异常会导致本次消费失败,不会执行 ACK
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.3 发送消息
|
|
||||||
```java
|
|
||||||
import com.njcn.rdms.framework.mq.redis.core.RedisMQTemplate;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.data.redis.connection.stream.RecordId;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class DemoTaskProducer {
|
|
||||||
|
|
||||||
private final RedisMQTemplate redisMQTemplate;
|
|
||||||
|
|
||||||
public RecordId send(Long taskId, String bizType) {
|
|
||||||
return redisMQTemplate.send(new DemoTaskMessage()
|
|
||||||
.setTaskId(taskId)
|
|
||||||
.setBizType(bizType));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. 消息拦截器扩展(可选)
|
|
||||||
可通过实现 `RedisMessageInterceptor`,对发送/消费前后做统一处理(比如租户透传、审计埋点)。
|
|
||||||
|
|
||||||
```java
|
|
||||||
import com.njcn.rdms.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
|
|
||||||
import com.njcn.rdms.framework.mq.redis.core.message.AbstractRedisMessage;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class DemoRedisMessageInterceptor implements RedisMessageInterceptor {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendMessageBefore(AbstractRedisMessage message) {
|
|
||||||
message.addHeader("source", "rdms-system");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 配置说明
|
|
||||||
本模块本身几乎没有 `rdms.mq.*` 配置项,主要依赖:
|
|
||||||
1. `spring.data.redis.*`(Redis 连接)
|
|
||||||
2. `spring.application.name`(Stream consumer group 默认值)
|
|
||||||
|
|
||||||
另外:
|
|
||||||
1. Redis Stream 要求 Redis 版本 >= 5.0
|
|
||||||
2. 只有存在 Stream 监听器时,重投/清理定时任务才会生效
|
|
||||||
|
|
||||||
## 当前已知注意点
|
|
||||||
1. `AbstractRedisStreamMessageListener` 有一个 `(streamKey, group)` 构造器,内部把 `messageType` 置空;而消费反序列化仍依赖 `messageType`。
|
|
||||||
建议优先使用无参构造路径(即泛型推断消息类型的默认方式)。
|
|
||||||
2. Stream 的重投机制本质是“至少一次投递”语义,业务侧应自行保证幂等。
|
|
||||||
|
|
||||||
## 适用与不适用
|
|
||||||
|
|
||||||
适用:
|
|
||||||
1. 需要快速接入 Redis 广播/异步任务队列
|
|
||||||
2. 希望保留基础的失败补偿和消息清理能力
|
|
||||||
3. 希望通过抽象基类统一消息定义风格
|
|
||||||
|
|
||||||
不适用:
|
|
||||||
1. 对事务一致性、延迟、吞吐有强约束且需要完整 MQ 运维体系
|
|
||||||
2. 需要复杂顺序语义、死信重试拓扑、跨机房高可用治理
|
|
||||||
@@ -1,349 +0,0 @@
|
|||||||
# rdms-spring-boot-starter-mybatis
|
|
||||||
|
|
||||||
## 模块定位
|
|
||||||
`rdms-spring-boot-starter-mybatis` 不是一个“只把 MyBatis-Plus 引进来”的轻量 starter,而是项目里的数据库访问基础设施模块。它把下面几类能力统一收口到一个地方:
|
|
||||||
|
|
||||||
1. 数据源与事务管理
|
|
||||||
2. MyBatis-Plus 与 MyBatis-Plus-Join 的统一装配
|
|
||||||
3. 多数据库兼容策略
|
|
||||||
4. DO 基类、审计字段自动填充、逻辑删除
|
|
||||||
5. Mapper / Wrapper 的统一编码风格
|
|
||||||
6. 常用 TypeHandler
|
|
||||||
7. Easy-Trans 的 VO 翻译接入
|
|
||||||
|
|
||||||
从仓库里的实际用法看,这个模块倾向于让业务模块在 DAL 层遵循一套固定约定,而不是每个模块自己重复配置 MyBatis、分页、排序、主键策略和字段填充。
|
|
||||||
|
|
||||||
## 设计思路
|
|
||||||
|
|
||||||
## 1. 用一个 starter 统一数据库层约定
|
|
||||||
这个模块直接依赖了:
|
|
||||||
|
|
||||||
1. `druid-spring-boot-3-starter`
|
|
||||||
2. `mybatis-plus-spring-boot3-starter`
|
|
||||||
3. `dynamic-datasource-spring-boot3-starter`
|
|
||||||
4. `mybatis-plus-join-boot-starter`
|
|
||||||
5. `easy-trans-spring-boot-starter`
|
|
||||||
|
|
||||||
这说明模块不是“最小封装”路线,而是把项目默认采用的数据库栈整体打包,业务模块只需要依赖这一个 starter,就能得到一致的运行时行为。
|
|
||||||
|
|
||||||
## 2. 优先解决“约定不一致”问题,而不是只提供工具类
|
|
||||||
模块里最核心的不是某一个工具类,而是整套约定:
|
|
||||||
|
|
||||||
1. `@MapperScan` 统一扫描 `${rdms.info.base-package}`
|
|
||||||
2. `BaseDO` 统一审计字段和逻辑删除字段
|
|
||||||
3. `BaseMapperX` 统一分页、批量操作、便捷查询
|
|
||||||
4. `LambdaQueryWrapperX` / `QueryWrapperX` 统一动态拼条件写法
|
|
||||||
5. `IdTypeEnvironmentPostProcessor` 统一不同数据库下的主键策略
|
|
||||||
|
|
||||||
这套设计可以让代码生成器、业务模块和框架模块都围绕同一套 DAL 模型工作。
|
|
||||||
|
|
||||||
## 3. 用“自动推断”减少多数据库切换成本
|
|
||||||
模块针对不同数据库运行场景做了几层兼容:
|
|
||||||
|
|
||||||
1. `pom.xml` 直接预留了 MySQL、Oracle、PostgreSQL、SQL Server、达梦、人大金仓、openGauss 等驱动
|
|
||||||
2. `IdTypeEnvironmentPostProcessor` 会根据主数据源 URL 自动判断 `DbType`
|
|
||||||
3. 如果 `mybatis-plus.global-config.db-config.id-type=NONE`,则自动改写为更适合当前数据库的 `AUTO` 或 `INPUT`
|
|
||||||
4. 同一个后处理器顺手补齐 Quartz 的 `driverDelegateClass`
|
|
||||||
5. `QueryWrapperX.limitN` 和 `MyBatisUtils.findInSet` 也在做跨数据库差异适配
|
|
||||||
|
|
||||||
也就是说,这个模块尽量不让业务代码关心“当前连接的是哪一种数据库”,而是把差异尽量前移到框架层。
|
|
||||||
|
|
||||||
## 4. 把“业务里高频重复的 Mapper 写法”沉到基类里
|
|
||||||
`BaseMapperX<T>` 是这个模块最重要的抽象之一。它在 `BaseMapper` 之上继续补了几类默认能力:
|
|
||||||
|
|
||||||
1. `selectPage(...)`:直接返回项目统一的 `PageResult`
|
|
||||||
2. `selectJoinPage(...)`:把 Join 查询也纳入同一分页模型
|
|
||||||
3. `selectOne` / `selectList` / `selectCount` 的便捷重载
|
|
||||||
4. `insertBatch` / `updateBatch` 等批量操作
|
|
||||||
5. 针对 SQL Server 的批量插入特殊处理
|
|
||||||
6. `PAGE_SIZE_NONE` 时的“不分页查询”约定
|
|
||||||
|
|
||||||
这背后的设计取向很明确:让 Mapper 接口本身承载一部分“轻业务语义”的默认实现,减少 XML 和 Service 层重复代码。
|
|
||||||
|
|
||||||
## 5. 动态查询要“少写 if”,而不是把 if 搬到 Service 层
|
|
||||||
`LambdaQueryWrapperX`、`QueryWrapperX`、`MPJLambdaWrapperX` 都提供了 `xxxIfPresent` 系列方法,例如:
|
|
||||||
|
|
||||||
1. `likeIfPresent`
|
|
||||||
2. `eqIfPresent`
|
|
||||||
3. `inIfPresent`
|
|
||||||
4. `betweenIfPresent`
|
|
||||||
|
|
||||||
这样 Service 或 Mapper 默认方法可以直接链式拼接条件,空值自动跳过。这样可以把“查询条件存在才拼 SQL”这件事收口到 Wrapper,而不是散落在业务代码里做大量 `if (param != null)`。
|
|
||||||
|
|
||||||
## 6. 基础字段治理优先于业务字段治理
|
|
||||||
`BaseDO` 和 `DefaultDBFieldHandler` 体现了这个模块在数据治理上的几个固定要求:
|
|
||||||
|
|
||||||
1. 所有 DO 默认带 `createTime`、`updateTime`、`creator`、`updater`、`deleted`
|
|
||||||
2. 插入和更新时自动补时间
|
|
||||||
3. 已登录用户存在时自动补创建人和更新人
|
|
||||||
4. 逻辑删除统一使用 `deleted`,其未删除/已删除值由 yaml 中的 `logic-not-delete-value: 0` 和 `logic-delete-value: 1` 配置
|
|
||||||
5. `BaseDO.clean()` 用于清空前端可能误传回来的审计字段
|
|
||||||
|
|
||||||
这说明“审计字段一致性”被视为框架职责,而不是每个表、每个 Service 自己维护。
|
|
||||||
|
|
||||||
## 7. 常见字段存储形式做成透明 TypeHandler
|
|
||||||
模块内置了几类 `TypeHandler`:
|
|
||||||
|
|
||||||
1. `EncryptTypeHandler`:字符串字段透明加解密
|
|
||||||
2. `StringListTypeHandler`
|
|
||||||
3. `LongListTypeHandler`
|
|
||||||
4. `LongSetTypeHandler`
|
|
||||||
5. `IntegerListTypeHandler`
|
|
||||||
6. `JacksonTypeHandler` 的 `ObjectMapper` 统一注入
|
|
||||||
|
|
||||||
这里的取向是:数据库里允许保留“逗号分隔字符串”“JSON”“密文”等存储形式,但业务对象层尽量继续使用自然的数据结构。
|
|
||||||
|
|
||||||
例如:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@TableName(value = "infra_mail_account", autoResultMap = true)
|
|
||||||
public class MailAccountDO extends BaseDO {
|
|
||||||
|
|
||||||
@TableField(typeHandler = EncryptTypeHandler.class)
|
|
||||||
private String password;
|
|
||||||
|
|
||||||
@TableField(typeHandler = StringListTypeHandler.class)
|
|
||||||
private List<String> toMails;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
如果数据库里的 `password` 存的是密文、`to_mails` 存的是 `a@xx.com,b@xx.com` 这样的逗号分隔字符串,
|
|
||||||
那么业务代码里仍然可以分别按普通 `String` 和 `List<String>` 来使用它们,字段转换交给 `TypeHandler` 处理。
|
|
||||||
|
|
||||||
## 8. 把“查询之后的展示翻译”也并入数据库 starter
|
|
||||||
`RdmsTranslateAutoConfiguration` 和 `TranslateUtils` 说明这个模块并不只关心“查出来”,还关心“查出来后如何转成面向前端的 VO”。
|
|
||||||
这是一种比较明显的项目式封装方式:把 DAL 和 VO 翻译放在同一个 starter,方便后台管理类系统直接复用。
|
|
||||||
|
|
||||||
如果开发中需要这种能力,可以按下面的方式使用:
|
|
||||||
|
|
||||||
1. 在 VO 里同时定义“原始值字段”和“展示字段”,例如 `userId` 和 `userName`
|
|
||||||
2. 在原始值字段上增加 `@Trans`,声明要根据哪个对象、取哪个字段、回填到哪个展示字段
|
|
||||||
3. 查询完成后,先把 DO 转成 VO
|
|
||||||
4. 在返回前触发翻译。适合注解方式的接口可使用 `@TransMethodResult`,不适合注解方式的场景可手动调用 `TranslateUtils.translate(...)`
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class OperateLogRespVO implements VO {
|
|
||||||
|
|
||||||
@Trans(type = TransType.SIMPLE, target = AdminUserDO.class, fields = "nickname", ref = "userName")
|
|
||||||
private Long userId;
|
|
||||||
|
|
||||||
private String userName;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```java
|
|
||||||
@TransMethodResult
|
|
||||||
public CommonResult<PageResult<OperateLogRespVO>> pageOperateLog(...) {
|
|
||||||
PageResult<OperateLogDO> pageResult = operateLogService.getOperateLogPage(pageReqVO);
|
|
||||||
return success(BeanUtils.toBean(pageResult, OperateLogRespVO.class));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
上面的含义是:返回结果里先保留 `userId`,然后在翻译阶段根据 `userId` 查到对应用户的 `nickname`,再回填到 `userName`。
|
|
||||||
如果不是常规接口返回场景,例如导出 Excel,可以在转换为 VO 后手动调用:
|
|
||||||
|
|
||||||
```java
|
|
||||||
TranslateUtils.translate(BeanUtils.toBean(list, OperateLogRespVO.class));
|
|
||||||
```
|
|
||||||
|
|
||||||
## 自动装配链路
|
|
||||||
|
|
||||||
## 1. Spring Boot 自动配置入口
|
|
||||||
`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`
|
|
||||||
|
|
||||||
1. `com.njcn.rdms.framework.datasource.config.RdmsDataSourceAutoConfiguration`
|
|
||||||
2. `com.njcn.rdms.framework.mybatis.config.RdmsMybatisAutoConfiguration`
|
|
||||||
3. `com.njcn.rdms.framework.translate.config.RdmsTranslateAutoConfiguration`
|
|
||||||
|
|
||||||
## 2. EnvironmentPostProcessor 入口
|
|
||||||
`META-INF/spring.factories`
|
|
||||||
|
|
||||||
1. `com.njcn.rdms.framework.mybatis.config.IdTypeEnvironmentPostProcessor`
|
|
||||||
|
|
||||||
这意味着模块启动时会先做环境预处理,再进入正常的自动配置流程。
|
|
||||||
|
|
||||||
## 核心组件
|
|
||||||
|
|
||||||
| 组件 | 作用 |
|
|
||||||
| --- | --- |
|
|
||||||
| `RdmsDataSourceAutoConfiguration` | 开启事务管理,并在启用 Druid 监控页时注册过滤器去掉广告脚本 |
|
|
||||||
| `RdmsMybatisAutoConfiguration` | 提前于 MyBatis-Plus 完成装配,统一 `@MapperScan`、分页插件、字段填充、主键生成器、`JacksonTypeHandler` |
|
|
||||||
| `IdTypeEnvironmentPostProcessor` | 根据主数据源 URL 推断数据库类型,自动设置 `id-type` 和 Quartz Delegate |
|
|
||||||
| `BaseDO` | 统一 DO 基类,内置审计字段、逻辑删除和 Easy-Trans 兼容处理 |
|
|
||||||
| `DefaultDBFieldHandler` | 自动填充创建人、更新人、创建时间、更新时间 |
|
|
||||||
| `BaseMapperX` | 统一分页、Join 分页、批量操作、便捷查询 |
|
|
||||||
| `LambdaQueryWrapperX` / `QueryWrapperX` / `MPJLambdaWrapperX` | 提供 `IfPresent` 风格的条件拼装能力 |
|
|
||||||
| `JdbcUtils` / `MyBatisUtils` | 统一数据库类型探测、分页构造、排序拼装、跨库 SQL 片段 |
|
|
||||||
| `EncryptTypeHandler` 等 | 处理密文、列表、集合等特殊字段映射 |
|
|
||||||
| `TranslateUtils` | 在不能用注解自动翻译时,手动触发 Easy-Trans 翻译 |
|
|
||||||
|
|
||||||
## 模块特征
|
|
||||||
从实现细节和业务模块的用法看,这个 starter 有几个比较明确的特征:
|
|
||||||
|
|
||||||
1. 它承担“项目默认 DAL 规范”的角色,而不是一个可随意裁剪的通用组件。
|
|
||||||
2. 它偏向“约定优于配置”,例如主键策略、Mapper 扫描、分页结果、审计字段都由框架先定好。
|
|
||||||
3. 它存在一定程度的强绑定,例如绑定动态数据源、Druid、Easy-Trans、Security 上下文,以换取更少的样板代码。
|
|
||||||
4. 它在多数据库兼容上投入较多,适合数据库切换、国产库适配或私有化部署场景。
|
|
||||||
5. 它优先抽象后台管理系统里高频出现的分页、筛选、列表页、字典翻译、逻辑删除、审计字段等能力。
|
|
||||||
|
|
||||||
## 如何使用
|
|
||||||
|
|
||||||
## 1. 引入依赖
|
|
||||||
通常业务模块直接依赖:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.njcn</groupId>
|
|
||||||
<artifactId>rdms-spring-boot-starter-mybatis</artifactId>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
如果项目需要自动填充 `creator`、`updater`,运行时还应提供安全模块里的登录上下文能力。
|
|
||||||
|
|
||||||
## 2. 基础配置
|
|
||||||
最小可工作的配置重点有四个:
|
|
||||||
|
|
||||||
1. `rdms.info.base-package`
|
|
||||||
2. `spring.datasource.dynamic.primary`
|
|
||||||
3. `spring.datasource.dynamic.datasource.<primary>.url`
|
|
||||||
4. `mybatis-plus.global-config.db-config.id-type`
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
rdms:
|
|
||||||
info:
|
|
||||||
base-package: com.njcn.rdms.module.system
|
|
||||||
|
|
||||||
spring:
|
|
||||||
datasource:
|
|
||||||
dynamic:
|
|
||||||
primary: master
|
|
||||||
datasource:
|
|
||||||
master:
|
|
||||||
url: jdbc:mysql://127.0.0.1:13306/rdms?useSSL=false&serverTimezone=Asia/Shanghai
|
|
||||||
username: root
|
|
||||||
password: 123456
|
|
||||||
|
|
||||||
mybatis-plus:
|
|
||||||
global-config:
|
|
||||||
db-config:
|
|
||||||
id-type: NONE
|
|
||||||
logic-delete-value: 1
|
|
||||||
logic-not-delete-value: 0
|
|
||||||
type-aliases-package: ${rdms.info.base-package}.dal.dataobject
|
|
||||||
encryptor:
|
|
||||||
password: your-16-bytes-key
|
|
||||||
|
|
||||||
mybatis-plus-join:
|
|
||||||
banner: false
|
|
||||||
|
|
||||||
easy-trans:
|
|
||||||
is-enable-global: false
|
|
||||||
```
|
|
||||||
|
|
||||||
说明:
|
|
||||||
|
|
||||||
1. `id-type: NONE` 是这里推荐的“自动判断模式”
|
|
||||||
2. 如果没有配置主数据源 URL,`IdTypeEnvironmentPostProcessor` 就无法自动判断数据库类型
|
|
||||||
3. `encryptor.password` 只在使用 `EncryptTypeHandler` 时需要
|
|
||||||
|
|
||||||
## 3. DO 写法
|
|
||||||
推荐让 DO 继承 `BaseDO`,并在需要时启用 `autoResultMap = true`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@TableName(value = "infra_data_source_config", autoResultMap = true)
|
|
||||||
@KeySequence("infra_data_source_config_seq")
|
|
||||||
public class DataSourceConfigDO extends BaseDO {
|
|
||||||
|
|
||||||
private Long id;
|
|
||||||
private String name;
|
|
||||||
private String url;
|
|
||||||
private String username;
|
|
||||||
|
|
||||||
@TableField(typeHandler = EncryptTypeHandler.class)
|
|
||||||
private String password;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
如果字段在库里按逗号分隔字符串保存,也可以直接挂列表型 `TypeHandler`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@TableField(typeHandler = StringListTypeHandler.class)
|
|
||||||
private List<String> toMails;
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. Mapper 写法
|
|
||||||
推荐所有 Mapper 继承 `BaseMapperX<T>`,把分页和条件拼装直接写在默认方法里:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Mapper
|
|
||||||
public interface AdminUserMapper extends BaseMapperX<AdminUserDO> {
|
|
||||||
|
|
||||||
default PageResult<AdminUserDO> selectPage(UserPageReqVO reqVO,
|
|
||||||
Collection<Long> deptIds,
|
|
||||||
Collection<Long> userIds) {
|
|
||||||
return selectPage(reqVO, new LambdaQueryWrapperX<AdminUserDO>()
|
|
||||||
.likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername())
|
|
||||||
.likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile())
|
|
||||||
.eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus())
|
|
||||||
.betweenIfPresent(AdminUserDO::getCreateTime, reqVO.getCreateTime())
|
|
||||||
.inIfPresent(AdminUserDO::getDeptId, deptIds)
|
|
||||||
.inIfPresent(AdminUserDO::getId, userIds)
|
|
||||||
.orderByDesc(AdminUserDO::getId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
如果需要跨库兼容的 `limit n`,当前实现建议使用 `QueryWrapperX`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
return selectList(new QueryWrapperX<NotifyMessageDO>()
|
|
||||||
.eq("user_id", userId)
|
|
||||||
.eq("user_type", userType)
|
|
||||||
.eq("read_status", false)
|
|
||||||
.orderByDesc("id")
|
|
||||||
.limitN(size));
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. 手动触发 VO 翻译
|
|
||||||
当场景不适合用注解自动翻译时,可以手动调用:
|
|
||||||
|
|
||||||
```java
|
|
||||||
List<OperateLogRespVO> result = TranslateUtils.translate(list);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 典型开发流程
|
|
||||||
在这个 starter 的设计下,比较顺畅的开发流程基本是:
|
|
||||||
|
|
||||||
1. 配置好多数据源和 `rdms.info.base-package`
|
|
||||||
2. DO 继承 `BaseDO`
|
|
||||||
3. Mapper 继承 `BaseMapperX`
|
|
||||||
4. 查询条件优先使用 `WrapperX` 系列拼装
|
|
||||||
5. 分页统一返回 `PageResult`
|
|
||||||
6. 特殊字段优先通过 `TypeHandler` 做透明映射
|
|
||||||
7. VO 展示转换需要字典或名称翻译时再接 Easy-Trans
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. `IdTypeEnvironmentPostProcessor` 依赖 `spring.datasource.dynamic.primary` 和对应数据源的 `url`,否则无法自动识别数据库类型。
|
|
||||||
2. 当 `id-type` 被识别为 `INPUT` 时,自动配置还会注册对应数据库的 `IKeyGenerator`。这类场景下实体通常还需要正确声明 `@KeySequence`。
|
|
||||||
3. `DefaultDBFieldHandler` 会调用安全上下文工具获取登录用户。如果运行时没有对应安全能力,创建人和更新人自动填充就不能按预期工作。
|
|
||||||
4. `EncryptTypeHandler` 依赖 `mybatis-plus.encryptor.password`。密钥一旦更换,历史密文可能无法解密,生产环境不应硬编码。
|
|
||||||
5. `easy-trans.is-enable-global` 在示例配置里默认关闭,说明默认没有把全局响应翻译作为高优先级能力。
|
|
||||||
6. `QueryWrapperX.limitN` 是跨数据库兼容封装,但它仍然是基于数据库方言分支处理,不适合无限扩展到所有数据库。
|
|
||||||
7. 这个模块的职责边界偏“大而全”,适合本项目统一使用;如果要抽成通用开源组件,需要先拆掉对动态数据源、安全上下文、Easy-Trans 的强耦合。
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
这个模块的核心价值,不是简单提供几个 MyBatis 工具类,而是把数据库层的共性问题一次性沉到框架层:
|
|
||||||
|
|
||||||
1. 启动时怎么自动装配
|
|
||||||
2. 多数据库怎么少改代码
|
|
||||||
3. 分页、排序、条件查询怎么统一写
|
|
||||||
4. 审计字段和逻辑删除怎么保持一致
|
|
||||||
5. 特殊字段怎么透明映射
|
|
||||||
6. 查询结果怎么更顺滑地进入 VO 展示层
|
|
||||||
|
|
||||||
如果把它当成一个“带强约束的项目级 DAL 基座”去理解,这个模块的设计就会非常清晰。
|
|
||||||
@@ -1,373 +0,0 @@
|
|||||||
# rdms-spring-boot-starter-protection
|
|
||||||
|
|
||||||
## 模块定位
|
|
||||||
`rdms-spring-boot-starter-protection` 用于提供一组服务保护能力,减少业务代码里重复编写“防重复提交、限流、分布式锁、接口签名校验”这类横切逻辑。
|
|
||||||
|
|
||||||
当前模块实际包含四块能力:
|
|
||||||
|
|
||||||
1. 幂等:防止同一请求在短时间内被重复执行,例如重复提交、重复点击
|
|
||||||
2. 分布式锁:在并发场景下保证同一业务动作同一时间只有一个线程或节点执行
|
|
||||||
3. 限流:限制单位时间内的访问次数,避免接口被刷爆或高频调用
|
|
||||||
4. HTTP API 签名:校验调用方身份、请求时效和随机数,防止伪造请求和重放攻击
|
|
||||||
|
|
||||||
它的目标不是提供复杂的治理平台,而是把常见保护能力做成可声明、可复用、可分布式生效的基础设施。
|
|
||||||
|
|
||||||
## 设计思路
|
|
||||||
|
|
||||||
## 1. 用注解声明保护规则,用 AOP 统一执行
|
|
||||||
模块里的幂等、限流、签名校验都不是要求业务手写 Redis 判断逻辑,而是通过注解声明规则,再由 AOP 统一拦截执行:
|
|
||||||
|
|
||||||
1. `@Idempotent`
|
|
||||||
2. `@RateLimiter`
|
|
||||||
3. `@ApiSignature`
|
|
||||||
|
|
||||||
这样业务代码只负责说明“这个方法需要什么保护”,具体的 Redis Key 生成、重复请求判断、限流判断、签名验证由框架层统一处理。
|
|
||||||
|
|
||||||
## 2. 把状态统一放到 Redis,面向分布式部署
|
|
||||||
这个模块的保护能力不是单机内存级别,而是默认面向多实例部署场景:
|
|
||||||
|
|
||||||
1. 幂等使用 `StringRedisTemplate`
|
|
||||||
2. API 签名防重放使用 `StringRedisTemplate`
|
|
||||||
3. 限流使用 `Redisson` 的 `RRateLimiter`
|
|
||||||
4. 分布式锁接入 `lock4j-redisson-spring-boot-starter`
|
|
||||||
|
|
||||||
这意味着它的核心价值不是“本机防抖”,而是“多个节点共享保护状态”。
|
|
||||||
|
|
||||||
## 3. 单一能力单独建模,避免职责混杂
|
|
||||||
从实现上看,模块有一个比较明确的边界划分:
|
|
||||||
|
|
||||||
1. 幂等用于“同一请求短时间内只允许执行一次”
|
|
||||||
2. 分布式锁用于“并发竞争下只允许一个线程/节点进入”
|
|
||||||
3. 限流用于“单位时间窗口内限制请求次数”
|
|
||||||
4. API 签名用于“校验调用方身份、请求时效和防重放”
|
|
||||||
|
|
||||||
例如幂等组件没有扩展成“成功后立即删 Key”的锁语义,而是把这类能力交给 Lock4j 处理。
|
|
||||||
|
|
||||||
区分这两类能力时,可以抓住一个核心点:
|
|
||||||
|
|
||||||
1. 幂等控制的是“短时间内不要重复执行同一类请求”
|
|
||||||
2. 分布式锁控制的是“同一时间谁有资格进入临界区执行”
|
|
||||||
|
|
||||||
例如:
|
|
||||||
|
|
||||||
1. 用户连续点击两次“提交订单”,更适合用幂等
|
|
||||||
2. 两个线程同时刷新同一份缓存,只允许一个线程进入执行,更适合用分布式锁
|
|
||||||
|
|
||||||
一个常见误区是把长耗时任务也交给幂等处理。假设幂等窗口配置为 3 秒,但某个任务实际执行了 20 秒:
|
|
||||||
|
|
||||||
1. 第一个请求在第 0 秒进入
|
|
||||||
2. 幂等 Key 在第 3 秒过期
|
|
||||||
3. 第二个请求在第 5 秒再次进入
|
|
||||||
4. 此时第一个任务还没执行完,但第二个请求已经有机会再次执行
|
|
||||||
|
|
||||||
所以幂等更偏“防重复请求”,而不是“在整个执行期间控制线程执行权”。如果真正关心的是执行过程中的互斥,应优先使用分布式锁。
|
|
||||||
|
|
||||||
## 4. 用 KeyResolver 抽象不同粒度的保护范围
|
|
||||||
幂等和限流都没有把 Key 生成规则写死,而是抽成了 `KeyResolver`:
|
|
||||||
|
|
||||||
1. 默认级别:方法名 + 参数
|
|
||||||
2. 用户级别:方法名 + 参数 + 当前用户
|
|
||||||
3. IP 级别:方法名 + 参数 + 客户端 IP
|
|
||||||
4. 节点级别:方法名 + 参数 + 当前服务节点
|
|
||||||
5. 表达式级别:通过 SpEL 自定义 Key
|
|
||||||
|
|
||||||
这样模块能在“全局、按用户、按 IP、按节点、按业务字段”之间切换,而不是只能支持一种固定粒度。
|
|
||||||
|
|
||||||
## 5. 自动装配按功能块拆分
|
|
||||||
自动配置入口见:
|
|
||||||
`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`
|
|
||||||
|
|
||||||
当前注册了四个自动配置类:
|
|
||||||
|
|
||||||
1. `RdmsIdempotentConfiguration`
|
|
||||||
2. `RdmsLock4jConfiguration`
|
|
||||||
3. `RdmsRateLimiterConfiguration`
|
|
||||||
4. `RdmsApiSignatureAutoConfiguration`
|
|
||||||
|
|
||||||
这说明模块是按能力块拆分装配的,而不是堆在一个总配置类里。
|
|
||||||
|
|
||||||
## 功能说明
|
|
||||||
|
|
||||||
## 1. 幂等
|
|
||||||
核心类:
|
|
||||||
|
|
||||||
1. `Idempotent`
|
|
||||||
2. `IdempotentAspect`
|
|
||||||
3. `IdempotentRedisDAO`
|
|
||||||
|
|
||||||
实现方式:
|
|
||||||
|
|
||||||
1. AOP 拦截带 `@Idempotent` 的方法
|
|
||||||
2. 通过 `IdempotentKeyResolver` 解析 Redis Key
|
|
||||||
3. 调用 `SETNX + EXPIRE` 语义的 `setIfAbsent`
|
|
||||||
4. 锁定失败时抛出“重复请求”异常
|
|
||||||
5. 业务异常时可按配置删除 Key,允许后续重试
|
|
||||||
|
|
||||||
适合场景:
|
|
||||||
|
|
||||||
1. 防止用户双击按钮
|
|
||||||
2. 防止表单重复提交
|
|
||||||
3. 防止短时间内重复触发同一个业务动作
|
|
||||||
|
|
||||||
典型案例:
|
|
||||||
|
|
||||||
1. 创建工单时,前端连续点击两次“提交”,只允许创建一次
|
|
||||||
2. 导入任务提交后,用户刷新页面再次点击“开始导入”,短时间内不允许重复发起
|
|
||||||
3. 审批流提交节点时,浏览器因为网络抖动自动重发请求,后端只执行一次
|
|
||||||
|
|
||||||
边界说明:
|
|
||||||
|
|
||||||
1. 这里的幂等更偏“短时间窗口防重复请求”,不适合作为支付、退款这类长期业务幂等的唯一方案
|
|
||||||
2. 对于支付回调、订单状态推进这类场景,更可靠的做法仍然是基于业务唯一号和持久化状态做幂等判断,必要时再配合分布式锁
|
|
||||||
|
|
||||||
可选 KeyResolver:
|
|
||||||
|
|
||||||
1. `DefaultIdempotentKeyResolver`
|
|
||||||
2. `UserIdempotentKeyResolver`
|
|
||||||
3. `ExpressionIdempotentKeyResolver`
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Idempotent(timeout = 3, message = "请勿重复提交")
|
|
||||||
public Long createOrder(OrderCreateReqVO reqVO) {
|
|
||||||
return orderService.createOrder(reqVO);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
如果需要按某个参数做幂等,可以使用表达式:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Idempotent(
|
|
||||||
keyResolver = ExpressionIdempotentKeyResolver.class,
|
|
||||||
keyArg = "#reqVO.no",
|
|
||||||
timeout = 10
|
|
||||||
)
|
|
||||||
public void submit(OrderReqVO reqVO) {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
如果接口已经完成登录鉴权,也可以按“当前登录用户 + 接口参数”做幂等:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Idempotent(
|
|
||||||
keyResolver = UserIdempotentKeyResolver.class,
|
|
||||||
timeout = 3,
|
|
||||||
message = "请勿重复提交"
|
|
||||||
)
|
|
||||||
public CommonResult<Long> createOrder(OrderCreateReqVO reqVO) {
|
|
||||||
return success(orderService.createOrder(reqVO));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
这种方式不要求把 `userId` 显式放在请求参数里,而是从当前登录上下文中获取用户信息,适合“同一个用户短时间内不能重复提交同一类请求”的场景。
|
|
||||||
|
|
||||||
## 2. 限流
|
|
||||||
核心类:
|
|
||||||
|
|
||||||
1. `RateLimiter`
|
|
||||||
2. `RateLimiterAspect`
|
|
||||||
3. `RateLimiterRedisDAO`
|
|
||||||
|
|
||||||
实现方式:
|
|
||||||
|
|
||||||
1. AOP 在方法执行前拦截
|
|
||||||
2. 通过 `RateLimiterKeyResolver` 解析限流 Key
|
|
||||||
3. 使用 Redisson 的 `RRateLimiter` 设置速率
|
|
||||||
4. 超过限制时抛出“请求过于频繁”异常
|
|
||||||
|
|
||||||
适合场景:
|
|
||||||
|
|
||||||
1. 接口防刷
|
|
||||||
2. 高频查询保护
|
|
||||||
3. 短时间内限制短信、验证码、导出等高成本操作
|
|
||||||
4. 对开放接口或公网接口做基础流量保护
|
|
||||||
|
|
||||||
典型案例:
|
|
||||||
|
|
||||||
1. 短信验证码接口限制“1 分钟内最多发送 1 次”
|
|
||||||
2. 导出 Excel 接口限制“10 分钟内最多导出 3 次”
|
|
||||||
3. 登录接口按 IP 限制访问频率,防止暴力尝试
|
|
||||||
4. 某个开放查询接口按调用方或用户维度限制 QPS,避免被刷爆
|
|
||||||
|
|
||||||
可选 KeyResolver:
|
|
||||||
|
|
||||||
1. `DefaultRateLimiterKeyResolver`
|
|
||||||
2. `UserRateLimiterKeyResolver`
|
|
||||||
3. `ClientIpRateLimiterKeyResolver`
|
|
||||||
4. `ServerNodeRateLimiterKeyResolver`
|
|
||||||
5. `ExpressionRateLimiterKeyResolver`
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@RateLimiter(count = 5, time = 1, timeUnit = TimeUnit.MINUTES, message = "请求过于频繁")
|
|
||||||
public CommonResult<Boolean> sendSmsCode(String mobile) {
|
|
||||||
return success(true);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
如果希望按用户限流:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@RateLimiter(
|
|
||||||
count = 10,
|
|
||||||
time = 1,
|
|
||||||
timeUnit = TimeUnit.MINUTES,
|
|
||||||
keyResolver = UserRateLimiterKeyResolver.class
|
|
||||||
)
|
|
||||||
public PageResult<OrderRespVO> pageMyOrders(OrderPageReqVO reqVO) {
|
|
||||||
return orderService.pageMyOrders(reqVO);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. HTTP API 签名
|
|
||||||
核心类:
|
|
||||||
|
|
||||||
1. `ApiSignature`
|
|
||||||
2. `ApiSignatureAspect`
|
|
||||||
3. `ApiSignatureRedisDAO`
|
|
||||||
|
|
||||||
实现方式:
|
|
||||||
|
|
||||||
1. 从请求头中读取 `appId`、`timestamp`、`nonce`、`sign`
|
|
||||||
2. 校验时间戳是否过期
|
|
||||||
3. 校验 `nonce` 是否已使用,防止重放
|
|
||||||
4. 根据 `appId` 从 Redis 中获取 `appSecret`
|
|
||||||
5. 按“请求参数 + 请求体 + 请求头 + 密钥”构建签名字符串
|
|
||||||
6. 使用 `SHA-256` 计算服务端签名并比对
|
|
||||||
|
|
||||||
适合场景:
|
|
||||||
|
|
||||||
1. 开放接口
|
|
||||||
2. 系统间调用
|
|
||||||
3. 对请求来源和重放攻击有要求的 HTTP 接口
|
|
||||||
|
|
||||||
典型案例:
|
|
||||||
|
|
||||||
1. 第三方系统调用内部开放 API 时,使用 `appId + appSecret` 做签名校验
|
|
||||||
2. 支付平台、供应商平台、合作方系统调用回调接口时,先校验签名再进入业务处理
|
|
||||||
3. 对外提供的 B2B 接口要求请求在 60 秒内有效,并且 `nonce` 只能使用一次
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@ApiSignature(timeout = 60)
|
|
||||||
public CommonResult<String> callback() {
|
|
||||||
return success("ok");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
当前切面是按 `@annotation(signature)` 拦截的,因此实操上应优先加在方法上。
|
|
||||||
|
|
||||||
## 4. 分布式锁
|
|
||||||
核心类:
|
|
||||||
|
|
||||||
1. `RdmsLock4jConfiguration`
|
|
||||||
2. `DefaultLockFailureStrategy`
|
|
||||||
|
|
||||||
这部分没有重复实现分布式锁,而是接入 Lock4j,并补了一层默认失败策略:
|
|
||||||
|
|
||||||
1. 如果类路径存在 `Lock4j`,则启用配置
|
|
||||||
2. 在获取锁失败时,统一抛出项目内的 `ServiceException`
|
|
||||||
3. 保持锁失败时的错误处理风格一致
|
|
||||||
|
|
||||||
适合场景:
|
|
||||||
|
|
||||||
1. 防止同一业务动作并发执行
|
|
||||||
2. 串行化关键资源操作
|
|
||||||
3. 抢占式处理任务
|
|
||||||
|
|
||||||
典型案例:
|
|
||||||
|
|
||||||
1. 同一个用户同时触发两次“刷新缓存”,只允许一个线程进入
|
|
||||||
2. 定时任务集群部署时,同一时间只允许一个节点执行清理任务
|
|
||||||
3. 库存扣减、状态迁移、批量结算这类关键动作,在并发场景下需要串行处理
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Lock4j(keys = "#userId", expire = 30000, acquireTimeout = 1000)
|
|
||||||
public void refreshUserCache(Long userId) {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 自动装配链路
|
|
||||||
|
|
||||||
## 1. Spring Boot 自动配置入口
|
|
||||||
`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`
|
|
||||||
|
|
||||||
1. `com.njcn.rdms.framework.idempotent.config.RdmsIdempotentConfiguration`
|
|
||||||
2. `com.njcn.rdms.framework.lock4j.config.RdmsLock4jConfiguration`
|
|
||||||
3. `com.njcn.rdms.framework.ratelimiter.config.RdmsRateLimiterConfiguration`
|
|
||||||
4. `com.njcn.rdms.framework.signature.config.RdmsApiSignatureAutoConfiguration`
|
|
||||||
|
|
||||||
## 2. 各配置类负责的事情
|
|
||||||
|
|
||||||
| 配置类 | 作用 |
|
|
||||||
| --- | --- |
|
|
||||||
| `RdmsIdempotentConfiguration` | 注册幂等切面、幂等 Redis DAO、幂等 KeyResolver |
|
|
||||||
| `RdmsLock4jConfiguration` | 在 Lock4j 存在时注册默认加锁失败策略 |
|
|
||||||
| `RdmsRateLimiterConfiguration` | 注册限流切面、限流 Redis DAO、限流 KeyResolver |
|
|
||||||
| `RdmsApiSignatureAutoConfiguration` | 注册 API 签名切面、签名 Redis DAO |
|
|
||||||
|
|
||||||
## 如何使用
|
|
||||||
|
|
||||||
## 1. 引入依赖
|
|
||||||
业务模块通常直接依赖:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.njcn</groupId>
|
|
||||||
<artifactId>rdms-spring-boot-starter-protection</artifactId>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. 基础前提
|
|
||||||
这个模块依赖 Redis,因此至少需要可用的 Redis 配置。
|
|
||||||
|
|
||||||
其中:
|
|
||||||
|
|
||||||
1. 幂等依赖 `StringRedisTemplate`
|
|
||||||
2. API 签名依赖 `StringRedisTemplate`
|
|
||||||
3. 限流依赖 `RedissonClient`
|
|
||||||
4. 分布式锁依赖 Lock4j + Redisson
|
|
||||||
|
|
||||||
## 3. 选择合适的保护手段
|
|
||||||
|
|
||||||
1. 防重复提交,优先使用 `@Idempotent`
|
|
||||||
2. 控制访问频率,优先使用 `@RateLimiter`
|
|
||||||
3. 控制并发进入,优先使用 Lock4j
|
|
||||||
4. 保护开放接口,优先使用 `@ApiSignature`
|
|
||||||
|
|
||||||
不要把它们混成一类能力:
|
|
||||||
|
|
||||||
1. 幂等不等于分布式锁
|
|
||||||
2. 限流不等于幂等
|
|
||||||
3. API 签名不等于权限校验
|
|
||||||
|
|
||||||
## 4. KeyResolver 的选择建议
|
|
||||||
|
|
||||||
1. 默认对全局请求做保护,使用默认 Resolver
|
|
||||||
2. 需要按登录用户隔离保护,使用 User Resolver
|
|
||||||
3. 需要按 IP 控制访问,使用 ClientIp Resolver
|
|
||||||
4. 需要按业务字段控制粒度,使用 Expression Resolver
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. 幂等的 `timeout` 只是保护窗口,不是业务执行超时时间。如果方法执行时间长于这个窗口,后续请求仍可能进入。
|
|
||||||
2. 幂等在异常时默认删除 Key,这是为了允许业务失败后重新提交;如果追求的是“执行期间只允许一个线程进入”,更适合使用分布式锁。
|
|
||||||
3. 限流底层使用 Redisson 的 `RRateLimiter`,运行环境需要有 `RedissonClient`。
|
|
||||||
4. API 签名依赖 Redis 中预先存在 `appId -> appSecret` 的映射,否则签名校验无法通过。
|
|
||||||
5. API 签名当前实操上应加在方法上,避免把类级注解误认为一定会生效。
|
|
||||||
6. 这个模块的保护能力都是方法级横切逻辑,适合放在 Controller 或 Service 的明确边界上使用,不适合到处滥加。
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
这个模块的核心价值,是把服务保护相关的高频横切问题统一沉到框架层:
|
|
||||||
|
|
||||||
1. 如何防止重复提交
|
|
||||||
2. 如何限制单位时间的访问频率
|
|
||||||
3. 如何在分布式环境下串行化关键操作
|
|
||||||
4. 如何校验开放接口的请求签名和防重放
|
|
||||||
|
|
||||||
如果把它理解成一个“请求保护与并发保护能力集合”的 starter,这个模块的设计会比较清晰。
|
|
||||||
@@ -1,389 +0,0 @@
|
|||||||
# rdms-spring-boot-starter-redis
|
|
||||||
|
|
||||||
## 模块定位
|
|
||||||
`rdms-spring-boot-starter-redis` 不是单纯把 Redis 依赖引进来,而是把项目里常用的 Redis 使用约定统一收口。
|
|
||||||
|
|
||||||
当前模块主要承担三类职责:
|
|
||||||
|
|
||||||
1. 统一 `RedisTemplate` 的序列化策略
|
|
||||||
2. 统一 Spring Cache 基于 Redis 的实现方式
|
|
||||||
3. 引入 Redisson,为分布式锁、限流等上层模块提供基础能力
|
|
||||||
|
|
||||||
它的重点不是封装大量 Redis 工具类,而是先把“Redis 怎么接入、怎么序列化、缓存怎么配置”这些基础设施统一好。
|
|
||||||
|
|
||||||
## 设计思路
|
|
||||||
|
|
||||||
## 1. 先统一 RedisTemplate,再让上层模块复用
|
|
||||||
模块通过 `RdmsRedisAutoConfiguration` 注册了统一的 `RedisTemplate<String, Object>`,核心约定是:
|
|
||||||
|
|
||||||
1. Key 使用字符串序列化
|
|
||||||
2. Hash Key 使用字符串序列化
|
|
||||||
3. Value 使用 JSON 序列化
|
|
||||||
4. Hash Value 使用 JSON 序列化
|
|
||||||
|
|
||||||
这样可以避免各模块各自定义一套 `RedisTemplate`,导致同一个 Redis 里出现不同的序列化格式。
|
|
||||||
|
|
||||||
## 2. 优先解决“可读性和兼容性”问题
|
|
||||||
这个模块没有采用 JDK 默认序列化,而是直接使用 JSON 序列化。这样做的直接收益是:
|
|
||||||
|
|
||||||
1. Redis 中的数据结构更容易观察和排查
|
|
||||||
2. 避免 JDK 序列化带来的可读性差和兼容性问题
|
|
||||||
3. 方便不同模块围绕同一种序列化格式工作
|
|
||||||
|
|
||||||
同时,它还额外处理了 `LocalDateTime` 的序列化问题,避免 Java 时间类型在 Redis 中读写异常。
|
|
||||||
|
|
||||||
## 3. 把 Cache 也纳入统一约定
|
|
||||||
模块不只提供 `RedisTemplate`,还把 Spring Cache 一起接入 Redis:
|
|
||||||
|
|
||||||
1. 开启 `@EnableCaching`
|
|
||||||
2. 统一 `RedisCacheConfiguration`
|
|
||||||
3. 统一 `RedisCacheManager`
|
|
||||||
4. 扩展支持按缓存名声明过期时间
|
|
||||||
|
|
||||||
这说明模块的目标不是“给你 Redis 客户端自己玩”,而是先把项目里高频使用的缓存场景标准化。
|
|
||||||
|
|
||||||
## 4. 通过小扩展解决 Spring Cache 默认能力不够用的问题
|
|
||||||
模块里的 `TimeoutRedisCacheManager` 做了一个很实用的扩展:
|
|
||||||
|
|
||||||
1. 如果缓存名是普通格式,例如 `user`
|
|
||||||
2. 则按全局默认 TTL 工作
|
|
||||||
3. 如果缓存名写成 `user#10m`
|
|
||||||
4. 则这个缓存自动按 10 分钟过期
|
|
||||||
|
|
||||||
这相当于把“单个缓存项的过期时间”从配置文件和自定义代码里,收口到了缓存名约定里。
|
|
||||||
|
|
||||||
## 5. 给上层能力模块提供 Redis 和 Redisson 基座
|
|
||||||
这个模块本身不直接实现分布式锁、限流、幂等,但它提供了这些上层能力所依赖的基础能力:
|
|
||||||
|
|
||||||
1. `StringRedisTemplate`
|
|
||||||
2. `RedisTemplate<String, Object>`
|
|
||||||
3. `RedissonClient`
|
|
||||||
4. 基于 Redis 的 Spring Cache
|
|
||||||
|
|
||||||
因此像 `rdms-spring-boot-starter-protection`、`rdms-spring-boot-starter-mq` 这样的模块,都可以把 Redis 能力建立在这个 starter 之上。
|
|
||||||
|
|
||||||
## 自动装配链路
|
|
||||||
|
|
||||||
## 1. Spring Boot 自动配置入口
|
|
||||||
`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`
|
|
||||||
|
|
||||||
1. `com.njcn.rdms.framework.redis.config.RdmsRedisAutoConfiguration`
|
|
||||||
2. `com.njcn.rdms.framework.redis.config.RdmsCacheAutoConfiguration`
|
|
||||||
|
|
||||||
## 2. 各配置类负责的事情
|
|
||||||
|
|
||||||
| 配置类 | 作用 |
|
|
||||||
| --- | --- |
|
|
||||||
| `RdmsRedisAutoConfiguration` | 注册统一序列化规则的 `RedisTemplate<String, Object>` |
|
|
||||||
| `RdmsCacheAutoConfiguration` | 开启 Spring Cache,注册 `RedisCacheConfiguration` 和自定义 `RedisCacheManager` |
|
|
||||||
|
|
||||||
## 核心组件
|
|
||||||
|
|
||||||
| 组件 | 作用 |
|
|
||||||
| --- | --- |
|
|
||||||
| `RdmsRedisAutoConfiguration` | 提供统一 JSON 序列化的 RedisTemplate,并处理 `LocalDateTime` 序列化 |
|
|
||||||
| `RdmsCacheAutoConfiguration` | 统一 Redis Cache 前缀、TTL、空值缓存策略和 CacheManager |
|
|
||||||
| `RdmsCacheProperties` | 提供 `rdms.cache.*` 配置项,目前包含 `redis-scan-batch-size` |
|
|
||||||
| `TimeoutRedisCacheManager` | 支持通过 `cacheName#ttl` 的形式为单个缓存声明过期时间 |
|
|
||||||
|
|
||||||
## 功能说明
|
|
||||||
|
|
||||||
## 1. 统一 RedisTemplate 序列化
|
|
||||||
核心类:
|
|
||||||
|
|
||||||
1. `RdmsRedisAutoConfiguration`
|
|
||||||
|
|
||||||
实现方式:
|
|
||||||
|
|
||||||
1. `RedisTemplate<String, Object>` 统一由框架注册
|
|
||||||
2. Key/Hash Key 使用字符串序列化
|
|
||||||
3. Value/Hash Value 使用 JSON 序列化
|
|
||||||
4. 通过 `JavaTimeModule` 兼容 `LocalDateTime`
|
|
||||||
|
|
||||||
适合场景:
|
|
||||||
|
|
||||||
1. 业务模块直接注入 `RedisTemplate<String, Object>` 使用
|
|
||||||
2. 需要缓存对象、列表、Map 等结构
|
|
||||||
3. 希望 Redis 中的数据具有一定可读性
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Resource
|
|
||||||
private RedisTemplate<String, Object> redisTemplate;
|
|
||||||
|
|
||||||
public void saveUser(Long userId, UserRespVO user) {
|
|
||||||
redisTemplate.opsForValue().set("user:" + userId, user);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. 统一 Spring Cache 基于 Redis 的实现
|
|
||||||
核心类:
|
|
||||||
|
|
||||||
1. `RdmsCacheAutoConfiguration`
|
|
||||||
2. `RdmsCacheProperties`
|
|
||||||
|
|
||||||
实现方式:
|
|
||||||
|
|
||||||
1. 开启 `@EnableCaching`
|
|
||||||
2. 创建统一的 `RedisCacheConfiguration`
|
|
||||||
3. 按 `spring.cache.redis.*` 读取默认 TTL、前缀、空值缓存等配置
|
|
||||||
4. 使用 JSON 序列化缓存值
|
|
||||||
5. 使用自定义 `RedisCacheManager`
|
|
||||||
|
|
||||||
这个模块还额外做了一个约定:
|
|
||||||
|
|
||||||
1. Cache Key 前缀使用单个 `:`,而不是默认的 `::`
|
|
||||||
2. 这样 Redis 中的 Key 更紧凑,也更方便在可视化工具中查看
|
|
||||||
|
|
||||||
Spring Cache 在这个模块里的作用,可以理解为:
|
|
||||||
|
|
||||||
1. 让“方法查询结果缓存”变成注解式能力,而不是每次都手写 `RedisTemplate`
|
|
||||||
2. 让缓存的写入、读取、失效围绕业务方法本身声明
|
|
||||||
3. 让“查库 -> 放缓存 -> 下次命中缓存”这类标准流程交给框架处理
|
|
||||||
|
|
||||||
它更适合下面这类场景:
|
|
||||||
|
|
||||||
1. 根据主键查询角色、菜单、模板、配置
|
|
||||||
2. 根据某个唯一业务字段查询对象,例如 `clientId`、`code`
|
|
||||||
3. 根据某个参数查询相对稳定的数据,例如部门子节点列表、权限对应菜单列表
|
|
||||||
|
|
||||||
这类场景通常有几个共同点:
|
|
||||||
|
|
||||||
1. 输入参数比较明确
|
|
||||||
2. 返回结果可以直接缓存
|
|
||||||
3. 数据更新时可以找到明确的缓存失效点
|
|
||||||
|
|
||||||
典型流程是:
|
|
||||||
|
|
||||||
1. 在查询方法上加 `@Cacheable`
|
|
||||||
2. 第一次执行时,方法正常查数据库或远程接口
|
|
||||||
3. 返回结果自动写入 Redis
|
|
||||||
4. 后续相同参数调用时,直接命中缓存,不再进入方法体
|
|
||||||
5. 在更新、删除方法上通过 `@CacheEvict` 清理对应缓存
|
|
||||||
|
|
||||||
和直接使用 `RedisTemplate` 相比,Spring Cache 的优势主要是:
|
|
||||||
|
|
||||||
1. 不需要手写 `get -> 判空 -> set -> expire` 这类模板代码
|
|
||||||
2. 缓存逻辑和业务方法绑定更紧,读代码时更容易看出哪里有缓存
|
|
||||||
3. 适合“查询结果缓存”这种标准模式
|
|
||||||
|
|
||||||
当前项目里,权限、角色、菜单、模板这类数据就大量采用这种方式;而像 Token、验证码这类更像“直接把业务对象存到 Redis”的场景,则更适合单独写 RedisDAO。
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Cacheable(cacheNames = "user", key = "#id")
|
|
||||||
public UserRespVO getUser(Long id) {
|
|
||||||
return userApi.getUser(id);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
如果某个写操作会影响缓存,需要配套清理缓存:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@CacheEvict(cacheNames = "user", key = "#id")
|
|
||||||
public void deleteUser(Long id) {
|
|
||||||
userMapper.deleteById(id);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
在当前项目语境下,更贴近实际的使用案例包括:
|
|
||||||
|
|
||||||
1. 根据角色 ID 查询角色信息,并在角色更新或删除时清理缓存
|
|
||||||
2. 根据权限标识查询菜单 ID 列表,并在菜单新增、修改、删除时清理缓存
|
|
||||||
3. 根据模板编码查询通知模板、邮件模板,并在模板变更时清理缓存
|
|
||||||
|
|
||||||
## 3. 支持按缓存名自定义过期时间
|
|
||||||
核心类:
|
|
||||||
|
|
||||||
1. `TimeoutRedisCacheManager`
|
|
||||||
|
|
||||||
实现方式:
|
|
||||||
|
|
||||||
1. 解析 `@Cacheable(cacheNames = "...")` 中的缓存名
|
|
||||||
2. 如果命中 `cacheName#ttl` 格式,则为该缓存单独设置 TTL
|
|
||||||
3. 支持的单位有:
|
|
||||||
- `d` 天
|
|
||||||
- `h` 小时
|
|
||||||
- `m` 分钟
|
|
||||||
- `s` 秒
|
|
||||||
4. 如果不带单位,默认按秒处理
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Cacheable(cacheNames = "user#10m", key = "#id")
|
|
||||||
public UserRespVO getUser(Long id) {
|
|
||||||
return userApi.getUser(id);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
说明:
|
|
||||||
|
|
||||||
1. `#10m` 只用于声明这个缓存的过期时间是 10 分钟
|
|
||||||
2. 它不会作为最终 Redis key 的一部分长期保留
|
|
||||||
3. 如果 `id = 1`
|
|
||||||
4. 且 `spring.cache.redis.key-prefix = rdms`
|
|
||||||
5. 那么实际 Redis key 一般形如 `rdms:user:1`
|
|
||||||
6. 其中 `user` 是缓存名,`1` 是 `key = "#id"` 计算出的缓存 Key
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Cacheable(cacheNames = "config#30s", key = "#key")
|
|
||||||
public ConfigRespVO getConfig(String key) {
|
|
||||||
return configService.getConfig(key);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
说明:
|
|
||||||
|
|
||||||
1. 如果 `key = "sms"`
|
|
||||||
2. 且 `spring.cache.redis.key-prefix = rdms`
|
|
||||||
3. 那么实际 Redis key 一般形如 `rdms:config:sms`
|
|
||||||
4. `#30s` 同样只影响 TTL,不直接体现在最终 key 名上
|
|
||||||
|
|
||||||
这种方式适合“绝大多数缓存共用默认 TTL,少数缓存按场景单独缩短或延长过期时间”的场景。
|
|
||||||
|
|
||||||
补充说明:
|
|
||||||
|
|
||||||
1. `cacheNames = "user#10m"` 中的 `user` 是缓存名,`10m` 是 TTL 描述
|
|
||||||
2. `key = "#id"` 或 `key = "#key"` 才决定具体缓存项的业务 Key
|
|
||||||
3. 如果配置了 `spring.cache.redis.key-prefix = rdms`,最终 Redis key 通常是 `rdms:user:1`、`rdms:config:sms` 这种形式
|
|
||||||
4. `#10m`、`#30s` 这类 TTL 后缀用于创建缓存时解析过期时间,不应理解成最终 key 名的一部分
|
|
||||||
|
|
||||||
## 4. 提供 Redisson 基础能力
|
|
||||||
从依赖上看,模块直接引入了 `redisson-spring-boot-starter`,因此除了 Spring Data Redis 以外,也会把 `RedissonClient` 这类能力带入项目。
|
|
||||||
|
|
||||||
这部分的直接收益是:
|
|
||||||
|
|
||||||
1. 分布式锁能力可被上层模块直接复用
|
|
||||||
2. 限流等基于 Redisson 的能力可直接建立在此模块之上
|
|
||||||
3. 项目不需要在多个模块里重复引入 Redisson
|
|
||||||
|
|
||||||
## 如何使用
|
|
||||||
|
|
||||||
## 1. 引入依赖
|
|
||||||
业务模块通常直接依赖:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.njcn</groupId>
|
|
||||||
<artifactId>rdms-spring-boot-starter-redis</artifactId>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. 基础配置
|
|
||||||
最常见的配置重点包括:
|
|
||||||
|
|
||||||
1. `spring.data.redis.*`
|
|
||||||
2. `spring.cache.redis.*`
|
|
||||||
3. `rdms.cache.redis-scan-batch-size`
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
spring:
|
|
||||||
data:
|
|
||||||
redis:
|
|
||||||
host: 127.0.0.1 # Redis 地址
|
|
||||||
port: 6379 # Redis 端口
|
|
||||||
database: 0 # 使用的 Redis 库编号
|
|
||||||
|
|
||||||
cache:
|
|
||||||
type: REDIS # Spring Cache 底层使用 Redis
|
|
||||||
redis:
|
|
||||||
time-to-live: 1h # 全局默认缓存过期时间
|
|
||||||
cache-null-values: false # 是否缓存空值,通常建议关闭
|
|
||||||
use-key-prefix: true # 是否启用缓存 Key 前缀
|
|
||||||
key-prefix: rdms # 缓存 Key 的统一前缀
|
|
||||||
|
|
||||||
rdms:
|
|
||||||
cache:
|
|
||||||
redis-scan-batch-size: 100 # Redis CacheWriter 使用 SCAN 批量处理时的单次扫描数量
|
|
||||||
```
|
|
||||||
|
|
||||||
说明:
|
|
||||||
|
|
||||||
1. `spring.cache.redis.time-to-live` 是全局默认缓存过期时间
|
|
||||||
2. `cacheName#ttl` 可以覆盖单个缓存的 TTL
|
|
||||||
3. `rdms.cache.redis-scan-batch-size` 用于控制 Redis CacheWriter 使用 `SCAN` 批量处理时的单次返回数量
|
|
||||||
|
|
||||||
## 3. 直接使用 RedisTemplate
|
|
||||||
如果场景不适合 Spring Cache,例如:
|
|
||||||
|
|
||||||
1. 需要操作复杂数据结构
|
|
||||||
2. 需要精细控制过期时间
|
|
||||||
3. 需要手动删除、递增、集合操作
|
|
||||||
|
|
||||||
可以直接注入 `RedisTemplate<String, Object>` 或 `StringRedisTemplate` 使用。
|
|
||||||
|
|
||||||
## 4. 优先使用 Spring Cache 的场景
|
|
||||||
如果场景是:
|
|
||||||
|
|
||||||
1. 查询结果缓存
|
|
||||||
2. 参数到结果的简单映射
|
|
||||||
3. 希望通过注解快速声明缓存
|
|
||||||
|
|
||||||
则优先使用 `@Cacheable`、`@CacheEvict`、`@CachePut`,并利用统一的 Redis Cache 约定。
|
|
||||||
|
|
||||||
这三个注解的典型使用场景可以这样区分:
|
|
||||||
|
|
||||||
1. `@Cacheable`
|
|
||||||
用于“先查缓存,缓存没有再执行方法,并把结果写入缓存”。
|
|
||||||
适合读操作,例如“根据 ID 查询角色”“根据编码查询模板”。
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Cacheable(cacheNames = "role", key = "#id", unless = "#result == null")
|
|
||||||
public RoleDO getRoleFromCache(Long id) {
|
|
||||||
return roleMapper.selectById(id);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. `@CacheEvict`
|
|
||||||
用于“执行方法后删除缓存”,避免更新或删除数据后缓存还是旧值。
|
|
||||||
适合写操作,例如“更新角色后清理角色缓存”“删除模板后清理模板缓存”。
|
|
||||||
|
|
||||||
```java
|
|
||||||
@CacheEvict(cacheNames = "role", key = "#updateReqVO.id")
|
|
||||||
public void updateRole(RoleSaveReqVO updateReqVO) {
|
|
||||||
roleMapper.updateById(BeanUtils.toBean(updateReqVO, RoleDO.class));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. `@CachePut`
|
|
||||||
用于“执行方法后,把方法返回值直接写回缓存”。
|
|
||||||
适合更新后希望立即刷新缓存,而不是简单删除缓存的场景。
|
|
||||||
|
|
||||||
```java
|
|
||||||
@CachePut(cacheNames = "role", key = "#result.id")
|
|
||||||
public RoleDO updateRoleAndReturn(RoleSaveReqVO updateReqVO) {
|
|
||||||
RoleDO role = BeanUtils.toBean(updateReqVO, RoleDO.class);
|
|
||||||
roleMapper.updateById(role);
|
|
||||||
return role;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
可以把它们简单记成:
|
|
||||||
|
|
||||||
1. `@Cacheable`:查的时候用,没有缓存才执行方法
|
|
||||||
2. `@CacheEvict`:改或删的时候用,执行后清缓存
|
|
||||||
3. `@CachePut`:改的时候用,执行后直接刷新缓存
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. `RedisTemplate<String, Object>` 使用 JSON 序列化,适合对象缓存,但跨版本变更字段时仍应关注兼容性。
|
|
||||||
2. `LocalDateTime` 已做序列化兼容处理,但复杂对象结构仍应关注 Jackson 序列化结果。
|
|
||||||
3. `cacheName#ttl` 是这个模块扩展出来的约定,不是 Spring Cache 默认语法。
|
|
||||||
4. 通过 `cacheName#ttl` 设置过期时间时,TTL 是针对这个缓存名生效的,不是针对单条 Key 动态计算。
|
|
||||||
5. `RdmsCacheAutoConfiguration` 将 Cache Key 前缀改成了单冒号 `:` 风格,如果已有依赖默认 `::` 的脚本或习惯,需要注意。
|
|
||||||
6. 这个模块本身主要解决“接入和约定”问题,不提供大量 Redis 业务工具方法;复杂 Redis 场景通常还是由业务模块或上层 starter 自己封装。
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
这个模块的核心价值,不是简单引入 Redis 依赖,而是统一项目里的 Redis 使用方式:
|
|
||||||
|
|
||||||
1. 统一 `RedisTemplate` 的序列化规则
|
|
||||||
2. 统一 Spring Cache 的 Redis 落地方式
|
|
||||||
3. 提供按缓存名声明 TTL 的扩展能力
|
|
||||||
4. 为 Redisson 相关的上层能力提供基础环境
|
|
||||||
|
|
||||||
如果把它理解成一个“Redis 接入基座 + Cache 约定基座”的 starter,这个模块的设计会比较清晰。
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
# rdms-spring-boot-starter-rpc
|
|
||||||
|
|
||||||
## 1. 模块现状
|
|
||||||
|
|
||||||
当前模块本体基本是空的。
|
|
||||||
|
|
||||||
从代码来看,这个模块目前只有:
|
|
||||||
|
|
||||||
- `pom.xml` 中的依赖声明
|
|
||||||
- `config`、`core` 两个包下的占位 `package-info.java`
|
|
||||||
|
|
||||||
当前没有实际落地的 RPC 代码。
|
|
||||||
|
|
||||||
## 2. 当前实际作用
|
|
||||||
|
|
||||||
这个模块目前的作用,主要是统一引入 RPC 相关基础依赖,包括:
|
|
||||||
|
|
||||||
- `spring-cloud-starter-openfeign`
|
|
||||||
- `spring-cloud-starter-loadbalancer`
|
|
||||||
- `feign-okhttp`
|
|
||||||
- `jakarta.validation-api`
|
|
||||||
|
|
||||||
所以更准确地说,它现在是一个“RPC 依赖聚合模块”,而不是一个已经实现完整功能的 RPC starter。
|
|
||||||
|
|
||||||
## 3. 当前不包含的能力
|
|
||||||
|
|
||||||
这个模块当前没有提供以下能力:
|
|
||||||
|
|
||||||
- 没有自动配置类
|
|
||||||
- 没有 `@EnableFeignClients` 封装
|
|
||||||
- 没有请求拦截器
|
|
||||||
- 没有统一异常处理
|
|
||||||
- 没有统一重试策略
|
|
||||||
- 没有统一降级处理
|
|
||||||
- 没有自定义编码器、解码器
|
|
||||||
- 没有任何业务侧 RPC 工具类
|
|
||||||
|
|
||||||
也就是说,单看这个模块本身,当前还谈不上“功能实现”。
|
|
||||||
|
|
||||||
## 4. 如何理解这个模块
|
|
||||||
|
|
||||||
如果结合当前仓库结构来理解,这个模块更适合看作:
|
|
||||||
|
|
||||||
- 先把 RPC 相关依赖收进来
|
|
||||||
- 给后续扩展预留模块位置
|
|
||||||
- 暂时还没有继续往里面建设实际能力
|
|
||||||
|
|
||||||
因此,这个模块当前的关键词不是“功能”,而是“占位”和“依赖聚合”。
|
|
||||||
|
|
||||||
## 5. 这个模块后续适合承接的内容
|
|
||||||
|
|
||||||
如果后续要继续建设,这个模块比较适合承接“各业务模块都可能共用的 RPC 通用能力”。
|
|
||||||
|
|
||||||
### 5.1 Feign 统一配置
|
|
||||||
|
|
||||||
适合放在这里的内容包括:
|
|
||||||
|
|
||||||
- 统一开启 Feign 所需的基础配置
|
|
||||||
- 统一配置超时、连接池、日志级别
|
|
||||||
- 统一切换底层 HTTP 客户端
|
|
||||||
|
|
||||||
这类内容属于基础设施层,通常不应该散落在各业务模块中。
|
|
||||||
|
|
||||||
### 5.2 请求拦截
|
|
||||||
|
|
||||||
这部分正是比较典型、也比较适合放在本模块中的能力。
|
|
||||||
|
|
||||||
例如可以统一实现:
|
|
||||||
|
|
||||||
- 请求头透传
|
|
||||||
- Token 透传
|
|
||||||
- 用户信息透传
|
|
||||||
- TraceId / 请求链路标识透传
|
|
||||||
|
|
||||||
常见做法是基于 `RequestInterceptor`,在所有 Feign 请求发出前统一补充请求头。
|
|
||||||
|
|
||||||
### 5.3 返回结果统一处理
|
|
||||||
|
|
||||||
如果希望对所有 OpenFeign 调用结果做统一拦截和异常处理,也适合放在这个模块。
|
|
||||||
|
|
||||||
常见做法包括:
|
|
||||||
|
|
||||||
- 通过 `ErrorDecoder` 统一处理非 2xx 响应
|
|
||||||
- 通过自定义 `Decoder` 统一解析返回体
|
|
||||||
- 对返回结果做统一业务异常转换
|
|
||||||
- 对远程服务错误码做统一包装
|
|
||||||
|
|
||||||
例如:
|
|
||||||
|
|
||||||
- 对方服务返回 404、500 时,统一转成项目内部异常
|
|
||||||
- 对方服务返回统一响应结构时,统一判断是否成功
|
|
||||||
- 对远程调用超时、连接失败做统一异常封装
|
|
||||||
|
|
||||||
### 5.4 调用日志与问题排查
|
|
||||||
|
|
||||||
这类通用能力也适合放在这里:
|
|
||||||
|
|
||||||
- 统一打印 RPC 请求日志
|
|
||||||
- 统一打印 RPC 响应日志
|
|
||||||
- 记录调用耗时
|
|
||||||
- 记录失败原因
|
|
||||||
|
|
||||||
这样做的好处是,排查跨服务问题时不需要每个模块各写一套。
|
|
||||||
|
|
||||||
### 5.5 重试、降级与容错
|
|
||||||
|
|
||||||
如果项目后续需要增强 RPC 稳定性,也可以在这里统一建设:
|
|
||||||
|
|
||||||
- 重试策略
|
|
||||||
- 熔断策略
|
|
||||||
- 降级处理
|
|
||||||
- 超时兜底
|
|
||||||
|
|
||||||
不过这部分是否要做,需要结合项目实际复杂度决定;如果当前系统调用链不复杂,也可以先不引入。
|
|
||||||
|
|
||||||
### 5.6 统一 RPC 规范
|
|
||||||
|
|
||||||
这个模块也适合沉淀一些统一约定,例如:
|
|
||||||
|
|
||||||
- 统一返回结构
|
|
||||||
- 统一异常码映射
|
|
||||||
- 统一 Header 命名
|
|
||||||
- 统一调用方与被调用方的接口规范
|
|
||||||
|
|
||||||
这样可以避免不同模块各自定义一套 RPC 风格。
|
|
||||||
|
|
||||||
## 6. 是否适合把拦截器和异常处理放在这里
|
|
||||||
|
|
||||||
如果这些能力是“针对所有或大多数 OpenFeign 调用都生效”的公共能力,那么放在这个模块是合适的。
|
|
||||||
|
|
||||||
原因很直接:
|
|
||||||
|
|
||||||
- 它属于 RPC 基础设施,不属于某个单独业务模块
|
|
||||||
- 放在这里可以统一维护,避免重复实现
|
|
||||||
- 后续任何模块接入 Feign 时,都可以复用同一套规则
|
|
||||||
|
|
||||||
例如你提到的这类需求,就很适合落在这里:
|
|
||||||
|
|
||||||
- 所有 Feign 请求统一加请求头
|
|
||||||
- 所有 Feign 返回统一做异常判断
|
|
||||||
- 所有远程调用失败统一转换成项目内部异常
|
|
||||||
|
|
||||||
如果某个拦截逻辑只服务于单一业务模块,那更适合放在该业务模块自己的 RPC 配置中;如果是全局通用规则,则更适合放在本模块。
|
|
||||||
|
|
||||||
## 7. 结论
|
|
||||||
|
|
||||||
`rdms-spring-boot-starter-rpc` 当前没有真正的功能代码。
|
|
||||||
|
|
||||||
如果后续要把它做成一个真正的 RPC starter,通常才会逐步补这些内容:
|
|
||||||
|
|
||||||
- 自动装配
|
|
||||||
- Feign 统一启用
|
|
||||||
- 请求头透传
|
|
||||||
- 调用日志
|
|
||||||
- 异常处理
|
|
||||||
- 超时与重试配置
|
|
||||||
- 统一 RPC 规范
|
|
||||||
|
|
||||||
但以当前仓库代码为准,这些能力都还没有在本模块中实现。
|
|
||||||
@@ -1,646 +0,0 @@
|
|||||||
# rdms-spring-boot-starter-security
|
|
||||||
|
|
||||||
## 1. 模块定位
|
|
||||||
|
|
||||||
`rdms-spring-boot-starter-security` 是项目中的安全基础模块,当前实际包含两块能力:
|
|
||||||
|
|
||||||
- 安全认证与权限校验
|
|
||||||
- 操作日志记录
|
|
||||||
|
|
||||||
这个模块不是单纯引入 Spring Security 依赖,而是已经在项目里落了完整的自动装配链路,包括:
|
|
||||||
|
|
||||||
- 基于 Token 的无状态认证
|
|
||||||
- 登录用户上下文维护
|
|
||||||
- URL 与方法级权限控制
|
|
||||||
- 跨服务 `LoginUser` 透传
|
|
||||||
- 401 / 403 统一返回
|
|
||||||
- 操作日志采集与异步上报
|
|
||||||
|
|
||||||
## 2. 设计思路
|
|
||||||
|
|
||||||
### 2.1 认证与权限分层
|
|
||||||
|
|
||||||
这个模块把安全相关能力拆成了几层:
|
|
||||||
|
|
||||||
1. 配置层
|
|
||||||
通过 `SecurityProperties` 统一管理 token 请求头、token 参数名、白名单、mock 登录、密码加密强度等配置。
|
|
||||||
|
|
||||||
2. 过滤器层
|
|
||||||
通过 `TokenAuthenticationFilter` 解析请求中的登录信息,并把 `LoginUser` 放入 Spring Security 上下文。
|
|
||||||
|
|
||||||
3. Spring Security 配置层
|
|
||||||
通过 `SecurityFilterChain` 统一配置无状态认证、放行规则、异常处理和过滤器顺序。
|
|
||||||
|
|
||||||
4. 权限服务层
|
|
||||||
通过 `SecurityFrameworkService` 对外提供权限、角色、scope 判断能力,供 `@PreAuthorize` 等表达式直接使用。
|
|
||||||
|
|
||||||
5. RPC 透传层
|
|
||||||
通过 Feign `RequestInterceptor` 把当前 `LoginUser` 继续透传给下游服务。
|
|
||||||
|
|
||||||
### 2.2 当前认证链路
|
|
||||||
|
|
||||||
当前项目的认证链路可以理解为:
|
|
||||||
|
|
||||||
1. 请求进入服务
|
|
||||||
2. 优先尝试从 `login-user` 请求头恢复登录用户
|
|
||||||
适用于网关或上游服务已经完成认证并透传用户信息的场景
|
|
||||||
3. 如果没有 `login-user`,再尝试从 `Authorization` 或请求参数中获取 token
|
|
||||||
4. 通过 `OAuth2TokenCommonApi` 校验 token
|
|
||||||
5. 构造 `LoginUser`
|
|
||||||
6. 放入 Spring Security 上下文,供后续权限判断、业务代码、日志记录使用
|
|
||||||
|
|
||||||
这意味着当前项目跨服务调用时,主要透传的是 `LoginUser`,而不是原始 `Authorization`。
|
|
||||||
|
|
||||||
### 2.3 权限判断思路
|
|
||||||
|
|
||||||
权限判断没有把权限数据直接塞进本地配置,而是通过远程接口获取:
|
|
||||||
|
|
||||||
- 权限校验走 `PermissionCommonApi`
|
|
||||||
- token 校验走 `OAuth2TokenCommonApi`
|
|
||||||
|
|
||||||
也就是说,这个模块负责“接入和执行安全规则”,权限与 token 数据本身仍然由系统服务提供。
|
|
||||||
|
|
||||||
### 2.4 操作日志思路
|
|
||||||
|
|
||||||
操作日志能力不是自己从零实现注解解析,而是集成 `bizlog-sdk`:
|
|
||||||
|
|
||||||
- 业务代码通过 `@LogRecord` 声明日志
|
|
||||||
- 本模块提供 `ILogRecordService` 实现
|
|
||||||
- 最终通过 `OperateLogCommonApi` 异步上报操作日志
|
|
||||||
|
|
||||||
这样做的特点是:
|
|
||||||
|
|
||||||
- 业务代码侧书写简单
|
|
||||||
- 日志记录逻辑统一
|
|
||||||
- 日志存储仍集中在系统服务
|
|
||||||
|
|
||||||
## 3. 自动装配入口
|
|
||||||
|
|
||||||
当前自动装配入口在:
|
|
||||||
|
|
||||||
- `RdmsSecurityRpcAutoConfiguration`
|
|
||||||
- `RdmsSecurityAutoConfiguration`
|
|
||||||
- `RdmsWebSecurityConfigurerAdapter`
|
|
||||||
- `RdmsOperateLogConfiguration`
|
|
||||||
- `RdmsOperateLogRpcAutoConfiguration`
|
|
||||||
|
|
||||||
说明这个 starter 当前已经不是占位模块,而是有完整自动配置入口的基础模块。
|
|
||||||
|
|
||||||
## 4. 核心功能点
|
|
||||||
|
|
||||||
### 4.1 基于 Token 的无状态认证
|
|
||||||
|
|
||||||
通过 `SecurityFilterChain` 配置为无状态模式:
|
|
||||||
|
|
||||||
- 禁用 Session
|
|
||||||
- 禁用表单登录
|
|
||||||
- 禁用 httpBasic
|
|
||||||
- 通过自定义 `TokenAuthenticationFilter` 完成登录态识别
|
|
||||||
|
|
||||||
这意味着服务本身不维护 Session,认证主要依赖 token 或上游透传的 `login-user`。
|
|
||||||
|
|
||||||
### 4.2 登录用户上下文维护
|
|
||||||
|
|
||||||
模块内定义了 `LoginUser`,当前主要包含这些信息:
|
|
||||||
|
|
||||||
- `id`
|
|
||||||
- `userType`
|
|
||||||
- `info`
|
|
||||||
- `scopes`
|
|
||||||
- `expiresTime`
|
|
||||||
|
|
||||||
认证成功后,`LoginUser` 会进入 Spring Security 上下文,后续可以通过 `SecurityFrameworkUtils` 获取,例如:
|
|
||||||
|
|
||||||
- 当前用户 ID
|
|
||||||
- 当前用户昵称
|
|
||||||
- 当前用户部门 ID
|
|
||||||
|
|
||||||
### 4.3 支持两种登录信息来源
|
|
||||||
|
|
||||||
`TokenAuthenticationFilter` 当前支持两种方式恢复登录用户:
|
|
||||||
|
|
||||||
1. 从 `login-user` 请求头恢复
|
|
||||||
适合网关转发、服务间调用、Feign 透传场景
|
|
||||||
|
|
||||||
2. 从 token 恢复
|
|
||||||
适合直接请求服务、没有上游透传 `login-user` 的场景
|
|
||||||
|
|
||||||
这也是为什么当前服务间调用时,即使不继续透传原始 token,下游仍然可以识别当前登录用户。
|
|
||||||
|
|
||||||
### 4.4 跨服务 LoginUser 透传
|
|
||||||
|
|
||||||
模块内提供了 `LoginUserRequestInterceptor`。
|
|
||||||
|
|
||||||
作用是:
|
|
||||||
|
|
||||||
- 发起 Feign 请求时,读取当前线程中的 `LoginUser`
|
|
||||||
- 将其序列化后放到 `login-user` 请求头
|
|
||||||
- 下游服务再通过 `TokenAuthenticationFilter` 还原为登录用户上下文
|
|
||||||
|
|
||||||
当前项目跨服务透传的重点是“登录用户信息透传”,不是“原始 token 透传”。
|
|
||||||
|
|
||||||
### 4.5 URL 级访问控制
|
|
||||||
|
|
||||||
`RdmsWebSecurityConfigurerAdapter` 里统一配置了请求访问规则,当前支持:
|
|
||||||
|
|
||||||
- 静态资源默认放行
|
|
||||||
- `@PermitAll` 标注的方法或类自动放行
|
|
||||||
- `rdms.security.permit-all-urls` 配置的 URL 放行
|
|
||||||
- 各业务模块通过 `AuthorizeRequestsCustomizer` 继续追加自定义规则
|
|
||||||
- 其余请求默认需要认证
|
|
||||||
|
|
||||||
其中 `AuthorizeRequestsCustomizer` 是一个扩展点,适合各模块自己补充 URL 安全规则。
|
|
||||||
|
|
||||||
例如 WebSocket 模块就是通过继承这个类,把自己的连接地址加入放行规则。
|
|
||||||
|
|
||||||
### 4.6 方法级权限校验
|
|
||||||
|
|
||||||
模块开启了 `@EnableMethodSecurity`,并注册了名为 `ss` 的权限服务 Bean。
|
|
||||||
|
|
||||||
因此业务代码可以直接写:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@PreAuthorize("@ss.hasPermission('system:user:create')")
|
|
||||||
public Long createUser(UserSaveReqVO reqVO) {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
当前支持的能力包括:
|
|
||||||
|
|
||||||
- `hasPermission`
|
|
||||||
- `hasAnyPermissions`
|
|
||||||
- `hasRole`
|
|
||||||
- `hasAnyRoles`
|
|
||||||
- `hasScope`
|
|
||||||
- `hasAnyScopes`
|
|
||||||
|
|
||||||
### 4.7 权限与角色远程校验
|
|
||||||
|
|
||||||
权限、角色判断不是本地硬编码,而是通过 `PermissionCommonApi` 远程校验。
|
|
||||||
|
|
||||||
为了减少频繁 RPC 调用,当前还做了本地 Guava 缓存:
|
|
||||||
|
|
||||||
- 权限判断缓存 1 分钟
|
|
||||||
- 角色判断缓存 1 分钟
|
|
||||||
|
|
||||||
`scope` 判断则直接基于当前 `LoginUser.scopes` 完成。
|
|
||||||
|
|
||||||
### 4.8 401 / 403 统一处理
|
|
||||||
|
|
||||||
模块内提供了统一异常处理器:
|
|
||||||
|
|
||||||
- `AuthenticationEntryPointImpl`:未登录访问需要认证的资源时返回 401
|
|
||||||
- `AccessDeniedHandlerImpl`:已登录但权限不足时返回 403
|
|
||||||
|
|
||||||
这样前端收到的是统一 JSON 结构,而不是默认的 Spring Security 页面或跳转行为。
|
|
||||||
|
|
||||||
### 4.9 SecurityContext 跨线程传递
|
|
||||||
|
|
||||||
模块把 `SecurityContextHolder` 的策略切换成了 `TransmittableThreadLocalSecurityContextHolderStrategy`。
|
|
||||||
|
|
||||||
作用是:
|
|
||||||
|
|
||||||
- 在 `@Async` 等异步线程场景下,尽量保留当前安全上下文
|
|
||||||
- 减少使用普通 `ThreadLocal` 时上下文丢失的问题
|
|
||||||
|
|
||||||
### 4.10 支持开发期 mock 登录
|
|
||||||
|
|
||||||
当配置开启后,可以使用 mock token 构造登录用户,便于本地调试。
|
|
||||||
|
|
||||||
这块能力由 `SecurityProperties` 中的以下配置控制:
|
|
||||||
|
|
||||||
- `mock-enable`
|
|
||||||
- `mock-secret`
|
|
||||||
|
|
||||||
这类能力只适合开发调试环境,不适合生产环境开启。
|
|
||||||
|
|
||||||
### 4.11 操作日志能力
|
|
||||||
|
|
||||||
这个模块还同时集成了操作日志能力。
|
|
||||||
|
|
||||||
当前实现方式是:
|
|
||||||
|
|
||||||
1. 通过 `@EnableLogRecord` 开启日志能力
|
|
||||||
2. 业务方法上使用 `@LogRecord`
|
|
||||||
3. 本模块的 `LogRecordServiceImpl` 收集日志内容
|
|
||||||
4. 自动补充用户信息、请求信息、链路追踪信息
|
|
||||||
5. 通过 `OperateLogCommonApi` 异步上报
|
|
||||||
|
|
||||||
日志中会补充的典型信息包括:
|
|
||||||
|
|
||||||
- 用户 ID
|
|
||||||
- 用户类型
|
|
||||||
- 请求方法
|
|
||||||
- 请求 URL
|
|
||||||
- 请求 IP
|
|
||||||
- User-Agent
|
|
||||||
- TraceId
|
|
||||||
|
|
||||||
## 5. 配置项
|
|
||||||
|
|
||||||
当前可见的核心配置在 `rdms.security` 下:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
rdms:
|
|
||||||
security:
|
|
||||||
token-header: Authorization
|
|
||||||
token-parameter: token
|
|
||||||
mock-enable: false
|
|
||||||
mock-secret: test
|
|
||||||
permit-all-urls:
|
|
||||||
- /admin-api/system/auth/login
|
|
||||||
password-encoder-length: 4
|
|
||||||
```
|
|
||||||
|
|
||||||
说明:
|
|
||||||
|
|
||||||
- `token-header`:请求头里 token 的字段名,默认是 `Authorization`
|
|
||||||
- `token-parameter`:请求参数里 token 的字段名,主要兼容不能方便传 header 的场景
|
|
||||||
- `mock-enable` / `mock-secret`:开发期 mock 登录控制
|
|
||||||
- `permit-all-urls`:额外白名单 URL
|
|
||||||
- `password-encoder-length`:BCrypt 加密强度
|
|
||||||
|
|
||||||
## 6. 快速上手
|
|
||||||
|
|
||||||
如果从“一个开发者接手后该怎么用”来理解,这个模块最常见的上手路径如下。
|
|
||||||
|
|
||||||
### 6.1 引入依赖后,会自动得到什么
|
|
||||||
|
|
||||||
业务模块引入这个 starter 后,默认会自动得到这些能力:
|
|
||||||
|
|
||||||
- Spring Security 过滤链
|
|
||||||
- Token 认证过滤器
|
|
||||||
- `@EnableMethodSecurity`
|
|
||||||
- `@ss` 权限表达式 Bean
|
|
||||||
- 401 / 403 统一返回
|
|
||||||
- Feign 的 `login-user` 透传
|
|
||||||
- `@LogRecord` 操作日志能力
|
|
||||||
|
|
||||||
也就是说,通常不需要你再手写一套 Security 配置类,除非业务模块有额外的特殊规则。
|
|
||||||
|
|
||||||
### 6.2 最小配置怎么写
|
|
||||||
|
|
||||||
最常见的基础配置如下:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
rdms:
|
|
||||||
security:
|
|
||||||
token-header: Authorization
|
|
||||||
token-parameter: token
|
|
||||||
mock-enable: false
|
|
||||||
permit-all-urls:
|
|
||||||
- /admin-api/system/auth/login
|
|
||||||
- /admin-api/system/auth/refresh-token
|
|
||||||
```
|
|
||||||
|
|
||||||
如果没有特殊需求,通常只需要确认:
|
|
||||||
|
|
||||||
- 哪些接口要放行
|
|
||||||
- token 从哪个 header 取
|
|
||||||
|
|
||||||
### 6.3 一个新接口,默认是什么状态
|
|
||||||
|
|
||||||
默认情况下:
|
|
||||||
|
|
||||||
- 没有显式放行的接口,都会要求登录
|
|
||||||
- 已登录后,如果方法上写了 `@PreAuthorize`,还会继续做权限校验
|
|
||||||
|
|
||||||
也就是说,一个普通接口最常见的开发方式是:
|
|
||||||
|
|
||||||
1. 默认需要登录
|
|
||||||
2. 再按需要加权限注解
|
|
||||||
|
|
||||||
例如:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@PreAuthorize("@ss.hasPermission('system:user:query')")
|
|
||||||
@GetMapping("/page")
|
|
||||||
public CommonResult<PageResult<UserRespVO>> getUserPage(UserPageReqVO reqVO) {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.4 哪些接口不需要登录,怎么做
|
|
||||||
|
|
||||||
有两种常用方式。
|
|
||||||
|
|
||||||
方式一:在接口或类上标 `@PermitAll`
|
|
||||||
|
|
||||||
```java
|
|
||||||
@PermitAll
|
|
||||||
@GetMapping("/public-info")
|
|
||||||
public CommonResult<String> getPublicInfo() {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
方式二:通过配置白名单放行
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
rdms:
|
|
||||||
security:
|
|
||||||
permit-all-urls:
|
|
||||||
- /admin-api/system/auth/login
|
|
||||||
- /admin-api/system/auth/refresh-token
|
|
||||||
```
|
|
||||||
|
|
||||||
实践建议:
|
|
||||||
|
|
||||||
- 固定的公共接口、登录接口、刷新 token 接口,更适合放配置白名单
|
|
||||||
- 个别无需登录的业务接口,更适合直接写 `@PermitAll`
|
|
||||||
|
|
||||||
### 6.5 需要权限控制,怎么做
|
|
||||||
|
|
||||||
最常用的是在 Controller 或 Service 方法上写 `@PreAuthorize`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@PreAuthorize("@ss.hasPermission('system:user:create')")
|
|
||||||
@PostMapping("/create")
|
|
||||||
public CommonResult<Long> createUser(@Valid @RequestBody UserSaveReqVO reqVO) {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
当前可直接使用的表达式包括:
|
|
||||||
|
|
||||||
- `@ss.hasPermission('xxx')`
|
|
||||||
- `@ss.hasAnyPermissions('a', 'b')`
|
|
||||||
- `@ss.hasRole('admin')`
|
|
||||||
- `@ss.hasAnyRoles('admin', 'manager')`
|
|
||||||
- `@ss.hasScope('user.read')`
|
|
||||||
- `@ss.hasAnyScopes('a', 'b')`
|
|
||||||
|
|
||||||
最常见的是 `hasPermission`。
|
|
||||||
|
|
||||||
### 6.5.1 接口可以按三类理解
|
|
||||||
|
|
||||||
从实际开发角度,可以把接口简单分成三类:
|
|
||||||
|
|
||||||
1. 匿名可访问
|
|
||||||
这类接口不要求登录。
|
|
||||||
常见做法:
|
|
||||||
- 使用 `@PermitAll`
|
|
||||||
- 或加入 `rdms.security.permit-all-urls` 白名单
|
|
||||||
|
|
||||||
2. 登录即可访问
|
|
||||||
这类接口要求登录,但不校验具体 permission。
|
|
||||||
常见做法:
|
|
||||||
- 不加 `@PermitAll`
|
|
||||||
- 不加白名单
|
|
||||||
- 也不加 `@PreAuthorize`
|
|
||||||
|
|
||||||
在当前配置下,这类接口会被默认的 `authenticated()` 规则保护:
|
|
||||||
- 未登录无法访问
|
|
||||||
- 已登录可以访问
|
|
||||||
|
|
||||||
3. 登录后还要校验具体权限
|
|
||||||
这类接口除了要求登录,还要求具备具体权限点。
|
|
||||||
常见做法:
|
|
||||||
- 使用 `@PreAuthorize("@ss.hasPermission('xxx')")`
|
|
||||||
|
|
||||||
一个比较实用的开发策略是:
|
|
||||||
|
|
||||||
- 大部分普通接口只要求登录
|
|
||||||
- 删除、导出、审批、分配等敏感动作再加具体权限校验
|
|
||||||
|
|
||||||
### 6.6 权限校验是怎么触发的
|
|
||||||
|
|
||||||
很多时候容易疑惑:`@PreAuthorize("@ss.hasPermission('system:dept:update')")` 这行代码本身没有手动调用 `PermissionCommonApi`,那远程权限校验是什么时候发生的?
|
|
||||||
|
|
||||||
当前链路可以直接理解成下面这几步:
|
|
||||||
|
|
||||||
```text
|
|
||||||
请求进入服务
|
|
||||||
-> TokenAuthenticationFilter 完成登录态恢复
|
|
||||||
-> 请求到达 Controller / Service 方法前
|
|
||||||
-> Spring Security 拦截 @PreAuthorize
|
|
||||||
-> 解析表达式 @ss.hasPermission('system:dept:update')
|
|
||||||
-> 调用 Spring 容器里的 ss Bean
|
|
||||||
-> 进入 SecurityFrameworkServiceImpl.hasPermission(...)
|
|
||||||
-> 转到 hasAnyPermissions(...)
|
|
||||||
-> 先查本地 1 分钟缓存
|
|
||||||
-> 缓存未命中时,调用 PermissionCommonApi 远程判断权限
|
|
||||||
-> 返回 true:允许进入方法
|
|
||||||
-> 返回 false:拒绝访问,返回 403
|
|
||||||
```
|
|
||||||
|
|
||||||
可以把它理解为:
|
|
||||||
|
|
||||||
- `@PreAuthorize` 负责“触发权限判断”
|
|
||||||
- `@ss` 负责“找到权限判断 Bean”
|
|
||||||
- `SecurityFrameworkServiceImpl` 负责“执行权限判断逻辑”
|
|
||||||
- `PermissionCommonApi` 负责“向权限服务查询结果”
|
|
||||||
|
|
||||||
所以真正触发 `PermissionCommonApi` 的,不是 Controller 代码手动调用,而是 Spring Security 在执行 `@PreAuthorize` 表达式时自动触发。
|
|
||||||
|
|
||||||
### 6.7 业务代码里怎么拿当前登录用户
|
|
||||||
|
|
||||||
最常用的是通过 `SecurityFrameworkUtils` 获取:
|
|
||||||
|
|
||||||
```java
|
|
||||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
|
||||||
String nickname = SecurityFrameworkUtils.getLoginUserNickname();
|
|
||||||
Long deptId = SecurityFrameworkUtils.getLoginUserDeptId();
|
|
||||||
```
|
|
||||||
|
|
||||||
如果需要更完整的信息:
|
|
||||||
|
|
||||||
```java
|
|
||||||
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
|
|
||||||
```
|
|
||||||
|
|
||||||
适用场景:
|
|
||||||
|
|
||||||
- 新增、修改数据时记录操作人
|
|
||||||
- 查询当前用户自己的数据
|
|
||||||
- 记录审计字段
|
|
||||||
|
|
||||||
### 6.8 为什么跨服务还能拿到当前用户
|
|
||||||
|
|
||||||
当前项目的链路是这样的:
|
|
||||||
|
|
||||||
1. 前端把 token 传给网关
|
|
||||||
2. 网关校验 token 后,把用户信息转成 `login-user` 请求头
|
|
||||||
3. 下游服务读取 `login-user`,恢复 `LoginUser`
|
|
||||||
4. 如果下游服务再通过 Feign 调别的服务,`LoginUserRequestInterceptor` 会继续透传 `login-user`
|
|
||||||
|
|
||||||
所以开发时可以这样理解:
|
|
||||||
|
|
||||||
- Web 入口主要靠 token 建立登录态
|
|
||||||
- 服务间调用主要靠 `login-user` 继续传递登录态
|
|
||||||
|
|
||||||
### 6.9 什么时候需要自己补 URL 规则
|
|
||||||
|
|
||||||
如果某个模块有特殊放行需求,可以继承 `AuthorizeRequestsCustomizer`。
|
|
||||||
|
|
||||||
例如:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class XxxAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
|
|
||||||
registry.requestMatchers("/xxx/**").permitAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
常见场景:
|
|
||||||
|
|
||||||
- WebSocket 握手地址
|
|
||||||
- 第三方回调接口
|
|
||||||
- 某些模块自己的公共入口
|
|
||||||
|
|
||||||
### 6.10 需要记录操作日志,怎么做
|
|
||||||
|
|
||||||
在业务方法上直接加 `@LogRecord`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@LogRecord(type = "SYSTEM_USER", subType = "CREATE", bizNo = "{{#user.id}}",
|
|
||||||
success = "创建用户成功")
|
|
||||||
public Long createUser(UserSaveReqVO reqVO) {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
这个注解更适合加在“有明确业务动作”的方法上,例如:
|
|
||||||
|
|
||||||
- 新增用户
|
|
||||||
- 修改角色
|
|
||||||
- 删除字典
|
|
||||||
- 分配权限
|
|
||||||
|
|
||||||
### 6.11 开发时最容易混淆的几个点
|
|
||||||
|
|
||||||
1. 这个模块负责“认证接入和权限执行”,不负责 token 签发
|
|
||||||
token 的签发和存储仍在系统服务等其他模块。
|
|
||||||
|
|
||||||
2. 当前跨服务透传的是 `login-user`,不是原始 token
|
|
||||||
所以下游服务能识别当前用户,不等于它一直拿着原始 `Authorization`。
|
|
||||||
3. 单体部署(不经过网关)建议在入口层剥离 `login-user` 请求头,避免外部伪造登录态
|
|
||||||
- Nginx 示例:
|
|
||||||
```nginx
|
|
||||||
# 去除客户端伪造的 login-user 头
|
|
||||||
proxy_set_header login-user "";
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 不加 `@PreAuthorize` 不代表匿名可访问
|
|
||||||
默认仍然需要登录,只是不会进一步做权限点校验。
|
|
||||||
|
|
||||||
4. `@PermitAll` 和 `permit-all-urls` 都是放行登录校验
|
|
||||||
放行后通常也不会再走权限控制。
|
|
||||||
|
|
||||||
## 7. 常见使用方式
|
|
||||||
|
|
||||||
### 7.1 方法权限控制
|
|
||||||
|
|
||||||
```java
|
|
||||||
@PreAuthorize("@ss.hasPermission('system:user:query')")
|
|
||||||
@GetMapping("/page")
|
|
||||||
public CommonResult<PageResult<UserRespVO>> getUserPage(UserPageReqVO reqVO) {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
适用场景:
|
|
||||||
|
|
||||||
- 控制某个接口必须具备某个权限点
|
|
||||||
- 细粒度控制新增、修改、删除、导出等操作
|
|
||||||
|
|
||||||
### 7.2 放行无需登录的接口
|
|
||||||
|
|
||||||
方式一:直接标注 `@PermitAll`
|
|
||||||
|
|
||||||
```java
|
|
||||||
@PermitAll
|
|
||||||
@GetMapping("/public-info")
|
|
||||||
public CommonResult<String> getPublicInfo() {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
方式二:通过配置加入白名单
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
rdms:
|
|
||||||
security:
|
|
||||||
permit-all-urls:
|
|
||||||
- /admin-api/system/auth/login
|
|
||||||
- /admin-api/system/auth/refresh-token
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.3 为某个模块补充 URL 安全规则
|
|
||||||
|
|
||||||
如果某个模块有自己的特殊放行需求,可以继承 `AuthorizeRequestsCustomizer`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class XxxAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
|
|
||||||
registry.requestMatchers("/xxx/**").permitAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
适用场景:
|
|
||||||
|
|
||||||
- WebSocket 握手地址放行
|
|
||||||
- 回调接口放行
|
|
||||||
- 某些特殊模块追加自己的 URL 规则
|
|
||||||
|
|
||||||
### 7.4 记录操作日志
|
|
||||||
|
|
||||||
```java
|
|
||||||
@LogRecord(type = "SYSTEM_USER", subType = "CREATE", bizNo = "{{#user.id}}",
|
|
||||||
success = "创建用户成功")
|
|
||||||
public Long createUser(UserSaveReqVO reqVO) {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
适用场景:
|
|
||||||
|
|
||||||
- 新增、修改、删除等需要审计的核心业务动作
|
|
||||||
- 希望把操作人、请求来源、业务对象编号一起记录下来
|
|
||||||
|
|
||||||
## 8. 模块边界
|
|
||||||
|
|
||||||
阅读这个模块时,需要把“它负责什么”和“它不负责什么”分开看。
|
|
||||||
|
|
||||||
当前它负责:
|
|
||||||
|
|
||||||
- 接入 Spring Security
|
|
||||||
- 解析 token 或 `login-user`
|
|
||||||
- 构建登录用户上下文
|
|
||||||
- 提供权限判断能力
|
|
||||||
- 统一未登录 / 未授权响应
|
|
||||||
- 透传 `LoginUser`
|
|
||||||
- 集成操作日志记录
|
|
||||||
|
|
||||||
当前它不负责:
|
|
||||||
|
|
||||||
- token 的签发
|
|
||||||
- 权限数据本身的维护
|
|
||||||
- 用户、角色、菜单、权限模型的存储
|
|
||||||
- 原始 `Authorization` 的 Feign 透传
|
|
||||||
|
|
||||||
这些数据来源和业务模型,当前仍由系统服务等其他模块提供。
|
|
||||||
|
|
||||||
## 9. 总结
|
|
||||||
|
|
||||||
`rdms-spring-boot-starter-security` 当前已经是一个实际落地的基础模块,不是占位模块。
|
|
||||||
|
|
||||||
从当前代码看,它的核心价值在于:
|
|
||||||
|
|
||||||
- 统一接入认证与权限体系
|
|
||||||
- 把登录用户上下文在 Web、Spring Security、Feign 调用之间串起来
|
|
||||||
- 给业务层提供简单直接的权限表达式入口
|
|
||||||
- 把操作日志能力收进统一基础设施
|
|
||||||
|
|
||||||
如果后续继续扩展,这个模块仍然适合承接更多安全通用能力,例如:
|
|
||||||
|
|
||||||
- 原始 token 透传
|
|
||||||
- 更细的权限表达式
|
|
||||||
- 更完整的审计日志字段
|
|
||||||
@@ -1,373 +0,0 @@
|
|||||||
# rdms-spring-boot-starter-test
|
|
||||||
|
|
||||||
## 1. 模块定位
|
|
||||||
|
|
||||||
`rdms-spring-boot-starter-test` 是项目中的测试基础模块,作用不是提供业务测试用例,而是为测试提供统一的基础设施和基类。
|
|
||||||
|
|
||||||
当前模块主要解决的是:
|
|
||||||
|
|
||||||
- 纯 Mockito 单元测试怎么写
|
|
||||||
- 依赖数据库的单元测试怎么快速启动
|
|
||||||
- 依赖 Redis 的单元测试怎么快速启动
|
|
||||||
- 同时依赖 DB 和 Redis 的测试怎么快速启动
|
|
||||||
- 测试里怎么更方便地造随机数据和做断言
|
|
||||||
|
|
||||||
所以这个模块更适合理解为“测试脚手架模块”。
|
|
||||||
|
|
||||||
## 2. 设计思路
|
|
||||||
|
|
||||||
当前模块的设计思路比较明确:把常见测试场景抽成几种固定模板,让开发人员通过继承基类快速开始写测试,而不是每个模块都自己从零搭测试环境。
|
|
||||||
|
|
||||||
整体上分成三层:
|
|
||||||
|
|
||||||
1. 依赖层
|
|
||||||
统一引入测试常用依赖,例如 Mockito、Spring Boot Test、H2、内存 Redis、随机对象生成工具。
|
|
||||||
|
|
||||||
2. 配置层
|
|
||||||
提供针对测试场景的补充配置,例如:
|
|
||||||
- 内存 Redis 启动配置
|
|
||||||
- SQL 初始化配置
|
|
||||||
|
|
||||||
3. 基类层
|
|
||||||
把测试场景收口成几类基类,开发时按场景继承即可。
|
|
||||||
|
|
||||||
这种设计的重点不是“测试能力多复杂”,而是“让测试环境标准化”。
|
|
||||||
|
|
||||||
## 3. 当前提供的能力
|
|
||||||
|
|
||||||
### 3.1 纯 Mockito 单元测试基类
|
|
||||||
|
|
||||||
基类:
|
|
||||||
|
|
||||||
- `BaseMockitoUnitTest`
|
|
||||||
|
|
||||||
作用:
|
|
||||||
|
|
||||||
- 适合不需要 Spring 容器、不需要 DB、不需要 Redis 的纯单元测试
|
|
||||||
- 基于 `MockitoExtension`
|
|
||||||
|
|
||||||
适用场景:
|
|
||||||
|
|
||||||
- 测试工具类
|
|
||||||
- 测试纯业务逻辑类
|
|
||||||
- 依赖全部可以通过 Mock 替代的 Service
|
|
||||||
|
|
||||||
### 3.2 内存 DB 单元测试基类
|
|
||||||
|
|
||||||
基类:
|
|
||||||
|
|
||||||
- `BaseDbUnitTest`
|
|
||||||
|
|
||||||
作用:
|
|
||||||
|
|
||||||
- 启动 Spring 测试上下文
|
|
||||||
- 引入数据源、事务、MyBatis 相关配置
|
|
||||||
- 使用 H2 作为内存数据库
|
|
||||||
- 支持 SQL 初始化
|
|
||||||
- 每个测试方法结束后自动执行 `/sql/clean.sql` 清理数据库
|
|
||||||
|
|
||||||
适用场景:
|
|
||||||
|
|
||||||
- Mapper 测试
|
|
||||||
- 依赖本模块数据库访问的 Service 测试
|
|
||||||
- 需要真实执行 MyBatis SQL 的测试
|
|
||||||
|
|
||||||
### 3.3 内存 Redis 单元测试基类
|
|
||||||
|
|
||||||
基类:
|
|
||||||
|
|
||||||
- `BaseRedisUnitTest`
|
|
||||||
|
|
||||||
作用:
|
|
||||||
|
|
||||||
- 启动 Spring 测试上下文
|
|
||||||
- 启动内存 Redis
|
|
||||||
- 引入 Redis 与 Redisson 相关配置
|
|
||||||
|
|
||||||
适用场景:
|
|
||||||
|
|
||||||
- Redis DAO 测试
|
|
||||||
- 缓存逻辑测试
|
|
||||||
- 分布式锁、限流、缓存等依赖 Redis 的逻辑测试
|
|
||||||
|
|
||||||
### 3.4 内存 DB + Redis 组合测试基类
|
|
||||||
|
|
||||||
基类:
|
|
||||||
|
|
||||||
- `BaseDbAndRedisUnitTest`
|
|
||||||
|
|
||||||
作用:
|
|
||||||
|
|
||||||
- 同时启动内存数据库和内存 Redis
|
|
||||||
- 一次性引入 DB、MyBatis、Redis、Redisson 所需配置
|
|
||||||
- 每个测试方法结束后自动清理数据库
|
|
||||||
|
|
||||||
适用场景:
|
|
||||||
|
|
||||||
- 同时依赖数据库和缓存的 Service 测试
|
|
||||||
- 需要验证“写库 + 删缓存 / 刷缓存”这类联动逻辑的测试
|
|
||||||
|
|
||||||
### 3.5 内存 Redis 启动配置
|
|
||||||
|
|
||||||
配置类:
|
|
||||||
|
|
||||||
- `RedisTestConfiguration`
|
|
||||||
|
|
||||||
作用:
|
|
||||||
|
|
||||||
- 基于 `jedis-mock` 启动一个内存 Redis Server
|
|
||||||
- 供 Redis 相关测试基类复用
|
|
||||||
|
|
||||||
这个配置的重点是:让测试不依赖外部真实 Redis 服务。
|
|
||||||
|
|
||||||
### 3.6 SQL 初始化配置
|
|
||||||
|
|
||||||
配置类:
|
|
||||||
|
|
||||||
- `SqlInitializationTestConfiguration`
|
|
||||||
|
|
||||||
作用:
|
|
||||||
|
|
||||||
- 在测试场景下补充 DataSource SQL 初始化能力
|
|
||||||
- 解决延迟加载场景下默认 SQL 初始化配置不生效的问题
|
|
||||||
|
|
||||||
这个配置的核心价值是:保证 H2 测试数据库能按测试配置正常初始化 schema 和 data。
|
|
||||||
|
|
||||||
### 3.7 随机测试数据工具
|
|
||||||
|
|
||||||
工具类:
|
|
||||||
|
|
||||||
- `RandomUtils`
|
|
||||||
|
|
||||||
作用:
|
|
||||||
|
|
||||||
- 随机生成字符串、数字、时间、邮箱、手机号
|
|
||||||
- 随机生成 POJO
|
|
||||||
- 随机生成对象列表、对象集合
|
|
||||||
|
|
||||||
它内部基于 `podam` 做随机对象填充,并对部分字段做了定制处理,例如:
|
|
||||||
|
|
||||||
- `status` 字段优先生成常见状态值
|
|
||||||
- `deleted` 字段默认生成 `false`
|
|
||||||
- `LocalDateTime` 的纳秒位归零,避免 MySQL / H2 时间精度差异
|
|
||||||
|
|
||||||
### 3.8 测试断言工具
|
|
||||||
|
|
||||||
工具类:
|
|
||||||
|
|
||||||
- `AssertUtils`
|
|
||||||
|
|
||||||
作用:
|
|
||||||
|
|
||||||
- 对比两个 POJO 的字段是否一致
|
|
||||||
- 断言是否抛出指定 `ServiceException`
|
|
||||||
|
|
||||||
适用场景:
|
|
||||||
|
|
||||||
- 校验 DTO / DO / VO 转换结果
|
|
||||||
- 校验业务异常是否符合预期
|
|
||||||
|
|
||||||
## 4. 开发人员怎么上手
|
|
||||||
|
|
||||||
这个模块最重要的不是“看懂代码”,而是先判断当前测试属于哪一类,然后继承对应基类。
|
|
||||||
|
|
||||||
### 4.1 如果只是纯逻辑测试
|
|
||||||
|
|
||||||
直接继承:
|
|
||||||
|
|
||||||
```java
|
|
||||||
class XxxServiceTest extends BaseMockitoUnitTest {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
适合:
|
|
||||||
|
|
||||||
- 不需要 Spring 容器
|
|
||||||
- 不访问数据库
|
|
||||||
- 不访问 Redis
|
|
||||||
- 依赖都可以 Mock
|
|
||||||
|
|
||||||
这是成本最低、执行最快的一类测试。
|
|
||||||
|
|
||||||
### 4.2 如果要测 Mapper 或真实 SQL
|
|
||||||
|
|
||||||
直接继承:
|
|
||||||
|
|
||||||
```java
|
|
||||||
class XxxMapperTest extends BaseDbUnitTest {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
适合:
|
|
||||||
|
|
||||||
- Mapper 层测试
|
|
||||||
- 依赖 H2 跑 SQL
|
|
||||||
- Service 里要真实查库、写库
|
|
||||||
|
|
||||||
开发时通常要准备:
|
|
||||||
|
|
||||||
- `application-unit-test.yaml`
|
|
||||||
- 初始化 SQL
|
|
||||||
- `clean.sql`
|
|
||||||
|
|
||||||
### 4.3 如果要测 Redis 逻辑
|
|
||||||
|
|
||||||
直接继承:
|
|
||||||
|
|
||||||
```java
|
|
||||||
class XxxRedisDaoTest extends BaseRedisUnitTest {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
适合:
|
|
||||||
|
|
||||||
- Redis Key 操作测试
|
|
||||||
- 缓存逻辑测试
|
|
||||||
- 基于 Redis 的基础设施测试
|
|
||||||
|
|
||||||
这样就不需要自己额外准备 Redis 环境。
|
|
||||||
|
|
||||||
### 4.4 如果业务同时依赖 DB 和 Redis
|
|
||||||
|
|
||||||
直接继承:
|
|
||||||
|
|
||||||
```java
|
|
||||||
class XxxServiceTest extends BaseDbAndRedisUnitTest {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
适合:
|
|
||||||
|
|
||||||
- 先写库再删缓存
|
|
||||||
- 先查库再写缓存
|
|
||||||
- 依赖数据库和 Redis 共同完成的业务逻辑
|
|
||||||
|
|
||||||
### 4.5 如果想快速造测试对象
|
|
||||||
|
|
||||||
可以直接使用 `RandomUtils`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
UserSaveReqVO reqVO = RandomUtils.randomPojo(UserSaveReqVO.class);
|
|
||||||
List<UserSaveReqVO> list = RandomUtils.randomPojoList(UserSaveReqVO.class, 3);
|
|
||||||
String email = RandomUtils.randomEmail();
|
|
||||||
String mobile = RandomUtils.randomMobile();
|
|
||||||
```
|
|
||||||
|
|
||||||
适合:
|
|
||||||
|
|
||||||
- 减少手写测试数据
|
|
||||||
- 快速生成大量测试输入
|
|
||||||
- 配合局部字段覆盖提高测试效率
|
|
||||||
|
|
||||||
### 4.6 如果要断言业务异常
|
|
||||||
|
|
||||||
可以直接使用 `AssertUtils.assertServiceException(...)`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
AssertUtils.assertServiceException(
|
|
||||||
() -> userService.createUser(reqVO),
|
|
||||||
USER_USERNAME_EXISTS
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
适合:
|
|
||||||
|
|
||||||
- 校验业务校验逻辑
|
|
||||||
- 校验异常码和异常信息
|
|
||||||
|
|
||||||
### 4.7 如果要断言对象字段一致
|
|
||||||
|
|
||||||
可以使用:
|
|
||||||
|
|
||||||
```java
|
|
||||||
AssertUtils.assertPojoEquals(expected, actual, "createTime", "updateTime");
|
|
||||||
```
|
|
||||||
|
|
||||||
适合:
|
|
||||||
|
|
||||||
- 校验转换逻辑
|
|
||||||
- 校验查询结果
|
|
||||||
- 忽略少数字段差异后做整体对比
|
|
||||||
|
|
||||||
## 5. 常见使用建议
|
|
||||||
|
|
||||||
### 5.1 优先选最轻的测试基类
|
|
||||||
|
|
||||||
建议顺序是:
|
|
||||||
|
|
||||||
1. 能用 `BaseMockitoUnitTest` 就不要上 Spring 容器
|
|
||||||
2. 需要真实 DB 再用 `BaseDbUnitTest`
|
|
||||||
3. 需要真实 Redis 再用 `BaseRedisUnitTest`
|
|
||||||
4. 两者都需要时再用 `BaseDbAndRedisUnitTest`
|
|
||||||
|
|
||||||
原因很简单:
|
|
||||||
|
|
||||||
- 测试越轻,执行越快
|
|
||||||
- 环境越少,问题越少
|
|
||||||
|
|
||||||
### 5.2 本模块更偏“单元测试基础设施”
|
|
||||||
|
|
||||||
虽然这里用到了 Spring Boot Test、H2、内存 Redis,但当前命名和设计仍然偏“单元测试 / 轻量集成测试”。
|
|
||||||
|
|
||||||
也就是说,它更适合:
|
|
||||||
|
|
||||||
- 在模块内部验证逻辑正确性
|
|
||||||
- 快速验证 Mapper / Redis / Service 行为
|
|
||||||
|
|
||||||
而不是:
|
|
||||||
|
|
||||||
- 完整端到端测试
|
|
||||||
- 完整微服务联调测试
|
|
||||||
- 依赖真实外部中间件的系统测试
|
|
||||||
|
|
||||||
### 5.3 自己模块真实依赖走内存实现,外部模块依赖走 Mock
|
|
||||||
|
|
||||||
从 `BaseDbUnitTest` 的注释就能看出当前设计思路:
|
|
||||||
|
|
||||||
- 自己模块的 Mapper 走 H2
|
|
||||||
- 别的模块的 Service 走 Mock
|
|
||||||
|
|
||||||
这是一种比较务实的测试策略:
|
|
||||||
|
|
||||||
- 保留自己模块的真实数据访问能力
|
|
||||||
- 避免跨模块依赖把测试拖重
|
|
||||||
|
|
||||||
## 6. 模块边界
|
|
||||||
|
|
||||||
当前模块负责的是:
|
|
||||||
|
|
||||||
- 提供测试基类
|
|
||||||
- 提供测试配置
|
|
||||||
- 提供随机数据和断言工具
|
|
||||||
|
|
||||||
当前模块不负责的是:
|
|
||||||
|
|
||||||
- 自动生成业务测试用例
|
|
||||||
- 自动替你写 Mock 行为
|
|
||||||
- 真实中间件联调
|
|
||||||
- 完整集成测试平台
|
|
||||||
|
|
||||||
所以它的职责很明确:让测试环境更容易搭,不是替业务写测试。
|
|
||||||
|
|
||||||
## 7. 总结
|
|
||||||
|
|
||||||
`rdms-spring-boot-starter-test` 当前已经把项目里最常见的测试场景做成了统一模板。
|
|
||||||
|
|
||||||
对于开发人员来说,真正重要的上手方式只有两步:
|
|
||||||
|
|
||||||
1. 先判断当前测试依赖什么环境
|
|
||||||
2. 再继承对应的基类
|
|
||||||
|
|
||||||
如果只是想快速理解这个模块,可以直接记住下面这张映射:
|
|
||||||
|
|
||||||
- 纯逻辑测试:`BaseMockitoUnitTest`
|
|
||||||
- 测 DB:`BaseDbUnitTest`
|
|
||||||
- 测 Redis:`BaseRedisUnitTest`
|
|
||||||
- DB + Redis 一起测:`BaseDbAndRedisUnitTest`
|
|
||||||
|
|
||||||
再配合:
|
|
||||||
|
|
||||||
- `RandomUtils` 造数据
|
|
||||||
- `AssertUtils` 做断言
|
|
||||||
|
|
||||||
基本就能覆盖大部分日常测试开发场景。
|
|
||||||
@@ -1,606 +0,0 @@
|
|||||||
# rdms-spring-boot-starter-web
|
|
||||||
|
|
||||||
## 1. 模块定位
|
|
||||||
|
|
||||||
`rdms-spring-boot-starter-web` 是 Web 层基础设施模块,用来统一处理 HTTP 接口开发中的通用问题,而不是承载具体业务逻辑。
|
|
||||||
|
|
||||||
模块当前聚合了以下能力:
|
|
||||||
|
|
||||||
- Web 基础自动配置
|
|
||||||
- 全局异常处理
|
|
||||||
- 接口返回结果暂存
|
|
||||||
- 管理端 / 应用端接口前缀约定
|
|
||||||
- Swagger / Knife4j 文档
|
|
||||||
- API 访问日志
|
|
||||||
- XSS 防护
|
|
||||||
- API 加解密
|
|
||||||
- 返回字段脱敏
|
|
||||||
- Jackson JSON 定制
|
|
||||||
- Banner 输出
|
|
||||||
|
|
||||||
## 2. 设计思路
|
|
||||||
|
|
||||||
这个模块的设计重点,是把 Web 层横切能力收敛到 starter 中,业务模块只关注 Controller、参数对象和业务逻辑本身。
|
|
||||||
|
|
||||||
核心思路如下:
|
|
||||||
|
|
||||||
- 通过自动装配统一注册 `Filter`、`ControllerAdvice`、`RestTemplate`、Swagger 等基础组件。
|
|
||||||
- 通过包路径约定,自动给 Controller 增加统一前缀,减少每个模块重复写公共路径。
|
|
||||||
- 通过全局异常处理,把常见异常统一翻译成 `CommonResult`。
|
|
||||||
- 通过过滤器和拦截器处理访问日志、XSS、防重复读取请求体、演示环境保护等横切逻辑。
|
|
||||||
- 通过注解方式扩展局部能力,例如 API 访问日志增强、接口加解密、字段脱敏。
|
|
||||||
|
|
||||||
自动装配入口如下:
|
|
||||||
|
|
||||||
- `com.njcn.rdms.framework.apilog.config.RdmsApiLogAutoConfiguration`
|
|
||||||
- `com.njcn.rdms.framework.jackson.config.RdmsJacksonAutoConfiguration`
|
|
||||||
- `com.njcn.rdms.framework.swagger.config.RdmsSwaggerAutoConfiguration`
|
|
||||||
- `com.njcn.rdms.framework.web.config.RdmsWebAutoConfiguration`
|
|
||||||
- `com.njcn.rdms.framework.apilog.config.RdmsApiLogRpcAutoConfiguration`
|
|
||||||
- `com.njcn.rdms.framework.xss.config.RdmsXssAutoConfiguration`
|
|
||||||
- `com.njcn.rdms.framework.banner.config.RdmsBannerAutoConfiguration`
|
|
||||||
- `com.njcn.rdms.framework.encrypt.config.RdmsApiEncryptAutoConfiguration`
|
|
||||||
|
|
||||||
## 3. 功能模块
|
|
||||||
|
|
||||||
### 3.1 Web 基础自动配置
|
|
||||||
|
|
||||||
`RdmsWebAutoConfiguration` 是本模块的核心入口,主要提供以下能力:
|
|
||||||
|
|
||||||
- 注册全局异常处理器 `GlobalExceptionHandler`
|
|
||||||
- 注册返回结果处理器 `GlobalResponseBodyHandler`
|
|
||||||
- 注册 `WebFrameworkUtils`
|
|
||||||
- 注册跨域过滤器 `CorsFilter`
|
|
||||||
- 注册请求体缓存过滤器 `CacheRequestBodyFilter`
|
|
||||||
- 提供普通 `RestTemplate`
|
|
||||||
- 提供带 `@LoadBalanced` 的 `RestTemplate`
|
|
||||||
|
|
||||||
其中有两个基础能力需要重点关注:
|
|
||||||
|
|
||||||
#### 3.1.1 按包路径自动增加接口前缀
|
|
||||||
|
|
||||||
模块会根据 Controller 所在包路径,自动追加统一前缀:
|
|
||||||
|
|
||||||
- `**.controller.admin.**` 默认追加 `/admin-api`
|
|
||||||
- `**.controller.app.**` 默认追加 `/app-api`
|
|
||||||
|
|
||||||
默认配置来自 `rdms.web`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
rdms:
|
|
||||||
web:
|
|
||||||
admin-api:
|
|
||||||
prefix: /admin-api
|
|
||||||
controller: "**.controller.admin.**"
|
|
||||||
app-api:
|
|
||||||
prefix: /app-api
|
|
||||||
controller: "**.controller.app.**"
|
|
||||||
admin-ui:
|
|
||||||
url: http://127.0.0.1:80
|
|
||||||
|
|
||||||
# 说明:
|
|
||||||
# - admin-api/app-api 已有默认值(如上所示),不改可省略
|
|
||||||
# - admin-ui.url 无默认值,需要显式配置
|
|
||||||
```
|
|
||||||
|
|
||||||
例如:
|
|
||||||
|
|
||||||
```java
|
|
||||||
package com.njcn.rdms.module.system.controller.admin.user;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/user")
|
|
||||||
public class UserController {
|
|
||||||
|
|
||||||
@GetMapping("/get")
|
|
||||||
public CommonResult<String> get() {
|
|
||||||
return CommonResult.success("ok");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
实际访问路径是:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/admin-api/user/get
|
|
||||||
```
|
|
||||||
|
|
||||||
如果 Controller 放在:
|
|
||||||
|
|
||||||
```text
|
|
||||||
com.xxx.xxx.controller.app.xxx
|
|
||||||
```
|
|
||||||
|
|
||||||
则会自动挂到 `/app-api` 下。
|
|
||||||
|
|
||||||
#### 3.1.2 `CommonResult` 不是自动包装
|
|
||||||
|
|
||||||
这个模块不会把任意 Controller 返回值自动包成 `CommonResult`。
|
|
||||||
|
|
||||||
`GlobalResponseBodyHandler` 的作用,是在返回前记录已经构造好的 `CommonResult`,供访问日志等能力读取,而不是替开发者自动改写返回结构。
|
|
||||||
|
|
||||||
因此 Controller 仍然需要显式返回:
|
|
||||||
|
|
||||||
```java
|
|
||||||
return CommonResult.success(data);
|
|
||||||
```
|
|
||||||
|
|
||||||
而不是依赖框架自动包装。
|
|
||||||
|
|
||||||
### 3.2 全局异常处理
|
|
||||||
|
|
||||||
`GlobalExceptionHandler` 会把常见异常统一转换成 `CommonResult`,包括:
|
|
||||||
|
|
||||||
- 参数缺失
|
|
||||||
- 参数类型错误
|
|
||||||
- `@Validated` / `@Valid` 校验失败
|
|
||||||
- 请求方式不匹配
|
|
||||||
- Content-Type 不匹配
|
|
||||||
- 无权限访问
|
|
||||||
- 业务异常 `ServiceException`
|
|
||||||
- 系统异常
|
|
||||||
|
|
||||||
对于系统异常,除了统一返回错误结果外,还会通过 `ApiErrorLogCommonApi` 异步记录异常日志。
|
|
||||||
|
|
||||||
这意味着业务代码中通常只需要:
|
|
||||||
|
|
||||||
- 正常场景返回 `CommonResult.success(...)`
|
|
||||||
- 业务错误抛出 `ServiceException`
|
|
||||||
|
|
||||||
其余异常由框架统一兜底。
|
|
||||||
|
|
||||||
### 3.3 接口返回结果暂存
|
|
||||||
|
|
||||||
这里的“接口返回结果记录”,更准确地说是“接口返回结果暂存”。
|
|
||||||
|
|
||||||
对应实现是 `GlobalResponseBodyHandler`,它只会在 Controller 返回值为 `CommonResult` 时,把该结果放到当前请求上下文中,供后续组件读取。
|
|
||||||
|
|
||||||
它本身不负责:
|
|
||||||
|
|
||||||
- 打印日志
|
|
||||||
- 持久化日志
|
|
||||||
- 改写返回结构
|
|
||||||
|
|
||||||
它主要服务于访问日志能力。因为 `ApiAccessLogFilter` 执行时,需要拿到本次请求最终返回的 `CommonResult`,才能把返回码、返回消息、响应体等内容写入访问日志。
|
|
||||||
|
|
||||||
可以这样理解两者关系:
|
|
||||||
|
|
||||||
- 接口返回结果暂存:把本次返回结果放到 request 上,供后续读取
|
|
||||||
- API 访问日志:读取请求信息和返回结果,组装后异步上报日志
|
|
||||||
|
|
||||||
因此,这两个能力不是并列重复关系,而是前者为后者提供支撑。
|
|
||||||
|
|
||||||
### 3.4 请求体缓存
|
|
||||||
|
|
||||||
`CacheRequestBodyFilter` 会把请求体包装成可重复读取的形式。
|
|
||||||
|
|
||||||
这个能力主要解决以下问题:
|
|
||||||
|
|
||||||
- 过滤器里读过一次请求体后,后续代码还能继续读取
|
|
||||||
- 访问日志、XSS、防护类过滤器可以提前读取请求内容
|
|
||||||
- 异常日志记录时可以再次拿到请求参数
|
|
||||||
|
|
||||||
这类能力对 `application/json` 请求尤其重要。
|
|
||||||
|
|
||||||
### 3.5 跨域处理
|
|
||||||
|
|
||||||
模块默认注册了全局 `CorsFilter`,允许:
|
|
||||||
|
|
||||||
- 任意来源
|
|
||||||
- 任意请求头
|
|
||||||
- 任意请求方法
|
|
||||||
- 携带凭证
|
|
||||||
|
|
||||||
适合前后端分离场景的统一跨域处理。
|
|
||||||
|
|
||||||
如果项目已经有更严格的网关级跨域策略,需要注意是否与这里的配置重复。
|
|
||||||
|
|
||||||
### 3.6 RestTemplate 支持
|
|
||||||
|
|
||||||
模块提供两个 `RestTemplate` Bean:
|
|
||||||
|
|
||||||
- 普通 `RestTemplate`
|
|
||||||
- 支持服务名负载均衡的 `loadBalancedRestTemplate`
|
|
||||||
|
|
||||||
使用示例:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Resource
|
|
||||||
private RestTemplate restTemplate;
|
|
||||||
|
|
||||||
@Resource(name = "loadBalancedRestTemplate")
|
|
||||||
private RestTemplate loadBalancedRestTemplate;
|
|
||||||
```
|
|
||||||
|
|
||||||
适用场景:
|
|
||||||
|
|
||||||
- `restTemplate`:直接调用固定地址
|
|
||||||
- `loadBalancedRestTemplate`:通过服务名访问注册中心中的其他服务
|
|
||||||
|
|
||||||
### 3.7 Swagger / Knife4j 文档
|
|
||||||
|
|
||||||
模块会自动装配 OpenAPI,并按管理端、应用端拆分分组:
|
|
||||||
|
|
||||||
- `/admin-api/**`
|
|
||||||
- `/app-api/**`
|
|
||||||
|
|
||||||
同时会在文档中预置常见请求头,例如认证头,便于在 Swagger 页面直接调试接口。
|
|
||||||
|
|
||||||
Swagger 配置示例:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
rdms:
|
|
||||||
swagger:
|
|
||||||
title: RDMS API # 文档标题
|
|
||||||
description: RDMS 接口文档 # 文档描述
|
|
||||||
author: RDMS # 作者/团队
|
|
||||||
version: 1.0.0 # 版本号
|
|
||||||
url: https://example.com # 项目或团队主页
|
|
||||||
email: dev@example.com # 联系邮箱
|
|
||||||
license: Apache 2.0 # 协议名称
|
|
||||||
license-url: https://www.apache.org/licenses/LICENSE-2.0.html # 协议地址
|
|
||||||
```
|
|
||||||
|
|
||||||
开发时通常只需要:
|
|
||||||
|
|
||||||
- 引入本模块
|
|
||||||
- 补齐 `rdms.swagger` 配置
|
|
||||||
- 在 Controller 上使用 `@Tag`
|
|
||||||
- 在接口上使用 `@Operation`
|
|
||||||
|
|
||||||
这样访问日志模块还能直接复用这些注解信息,自动推断操作模块、操作名称。
|
|
||||||
|
|
||||||
### 3.8 API 访问日志
|
|
||||||
|
|
||||||
模块通过 `ApiAccessLogFilter + ApiAccessLogInterceptor` 组合处理接口访问日志。
|
|
||||||
|
|
||||||
这里的访问日志,才是真正意义上的“日志记录能力”。
|
|
||||||
|
|
||||||
处理逻辑分成两层:
|
|
||||||
|
|
||||||
- 拦截器层:把 `HandlerMethod` 放到 request 中,供过滤器层读取
|
|
||||||
- 过滤器层:在请求结束后读取请求信息、异常信息、返回结果,并异步上报访问日志
|
|
||||||
|
|
||||||
访问日志内容包括:
|
|
||||||
|
|
||||||
- 用户信息
|
|
||||||
- 请求路径、方法、IP、User-Agent
|
|
||||||
- 请求参数
|
|
||||||
- 返回码、返回信息
|
|
||||||
- 执行耗时
|
|
||||||
- 操作模块、操作名称、操作类型
|
|
||||||
|
|
||||||
其中“返回码、返回信息、响应体”这部分数据,正是从前面的 `GlobalResponseBodyHandler` 暂存结果中读取出来的。
|
|
||||||
|
|
||||||
默认会记录访问日志;如需关闭,可配置:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
rdms:
|
|
||||||
access-log:
|
|
||||||
enable: false
|
|
||||||
```
|
|
||||||
|
|
||||||
如果需要对某个接口单独调整日志行为,可使用 `@ApiAccessLog`。
|
|
||||||
|
|
||||||
常见用法示例:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Tag(name = "用户管理")
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/user")
|
|
||||||
public class UserController {
|
|
||||||
|
|
||||||
@GetMapping("/page")
|
|
||||||
@Operation(summary = "查询用户分页")
|
|
||||||
@ApiAccessLog(responseEnable = true, sanitizeKeys = {"mobile"})
|
|
||||||
public CommonResult<PageResult<UserRespVO>> page(UserPageReqVO reqVO) {
|
|
||||||
return CommonResult.success(userService.page(reqVO));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
说明:
|
|
||||||
|
|
||||||
- `responseEnable = true`:额外记录响应体
|
|
||||||
- `sanitizeKeys`:从日志中移除敏感字段
|
|
||||||
- 如果没有显式指定操作名称,默认优先从 `@Operation` 中获取
|
|
||||||
|
|
||||||
访问日志的异步落库依赖 `ApiAccessLogCommonApi`,因此它本质上是“记录请求轨迹”,不是直接把日志写在本模块内部。
|
|
||||||
|
|
||||||
### 3.9 XSS 防护
|
|
||||||
|
|
||||||
模块内提供了 XSS 过滤能力,核心组成包括:
|
|
||||||
|
|
||||||
- `XssFilter`
|
|
||||||
- `XssRequestWrapper`
|
|
||||||
- `XssStringJsonDeserializer`
|
|
||||||
- `JsoupXssCleaner`
|
|
||||||
|
|
||||||
设计方式是:
|
|
||||||
|
|
||||||
- 对进入系统的请求内容做统一清洗
|
|
||||||
- 对 JSON 中的字符串字段做清洗
|
|
||||||
- 底层使用 `jsoup` 处理潜在的恶意脚本片段
|
|
||||||
|
|
||||||
适用场景:
|
|
||||||
|
|
||||||
- 富文本之外的大多数普通文本输入
|
|
||||||
- 表单提交
|
|
||||||
- JSON 请求体中的字符串字段
|
|
||||||
|
|
||||||
如果某些接口需要保留原始 HTML 内容,通常需要结合该模块的 XSS 配置做排除,而不是在业务层手工规避。
|
|
||||||
|
|
||||||
URL 白名单说明:
|
|
||||||
|
|
||||||
- 支持通过 `rdms.xss.exclude-urls` 配置 URL 白名单,命中的 URL 会跳过 XSS 过滤(包括 Filter 与 JSON 字符串反序列化)。
|
|
||||||
- 适合富文本、HTML 片段等需要保留原始内容的接口。
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
rdms:
|
|
||||||
xss:
|
|
||||||
enable: true
|
|
||||||
exclude-urls:
|
|
||||||
- /admin-api/rich-text/**
|
|
||||||
- /app-api/article/save
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.10 API 加解密
|
|
||||||
|
|
||||||
模块提供了接口级加解密能力,核心组成包括:
|
|
||||||
|
|
||||||
- `@ApiEncrypt`
|
|
||||||
- `ApiEncryptFilter`
|
|
||||||
- `ApiDecryptRequestWrapper`
|
|
||||||
- `ApiEncryptResponseWrapper`
|
|
||||||
|
|
||||||
适合理解为:
|
|
||||||
|
|
||||||
- 请求进入时先解密
|
|
||||||
- Controller / Service 内部仍然处理明文
|
|
||||||
- 响应返回前再加密
|
|
||||||
|
|
||||||
开发使用上,一般是在需要加解密的接口上增加 `@ApiEncrypt`,其余请求不受影响。
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/secure/demo")
|
|
||||||
public class SecureDemoController {
|
|
||||||
|
|
||||||
@PostMapping("/submit")
|
|
||||||
@ApiEncrypt
|
|
||||||
public CommonResult<String> submit(@RequestBody DemoReqVO reqVO) {
|
|
||||||
return CommonResult.success(reqVO.getContent());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
这类能力适合:
|
|
||||||
|
|
||||||
- 对外开放接口
|
|
||||||
- 对传输内容有额外保护要求的场景
|
|
||||||
|
|
||||||
不适合把它当成通用权限控制手段。它解决的是“传输内容保护”,不是“访问授权”。
|
|
||||||
|
|
||||||
### 3.11 返回字段脱敏
|
|
||||||
|
|
||||||
模块内置了一组字段脱敏注解和序列化器,常见注解包括:
|
|
||||||
|
|
||||||
- `@MobileDesensitize`
|
|
||||||
- `@EmailDesensitize`
|
|
||||||
- `@IdCardDesensitize`
|
|
||||||
- `@BankCardDesensitize`
|
|
||||||
- `@ChineseNameDesensitize`
|
|
||||||
- `@PasswordDesensitize`
|
|
||||||
|
|
||||||
适用方式是:在返回对象字段上直接加注解,序列化为 JSON 时自动脱敏。
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Data
|
|
||||||
public class UserRespVO {
|
|
||||||
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@MobileDesensitize
|
|
||||||
private String mobile;
|
|
||||||
|
|
||||||
@EmailDesensitize
|
|
||||||
private String email;
|
|
||||||
|
|
||||||
@ChineseNameDesensitize
|
|
||||||
private String nickname;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
这样接口内部仍可使用原始值,真正对外输出时才变成脱敏后的内容。
|
|
||||||
|
|
||||||
### 3.12 Jackson JSON 定制
|
|
||||||
|
|
||||||
模块包含 `RdmsJacksonAutoConfiguration`,用于统一注册 JSON 序列化 / 反序列化相关定制能力。
|
|
||||||
|
|
||||||
从模块结构上看,它主要承担两类职责:
|
|
||||||
|
|
||||||
- 承接本模块的 XSS、脱敏等 JSON 处理能力
|
|
||||||
- 把 JSON 相关行为统一收口到 starter 中,避免各业务模块重复配置 `ObjectMapper`
|
|
||||||
|
|
||||||
因此业务模块通常不需要自己再手动声明一套新的全局 Jackson 配置,除非有明确的覆盖需求。
|
|
||||||
|
|
||||||
### 3.13 Banner
|
|
||||||
|
|
||||||
模块还包含 `RdmsBannerAutoConfiguration` 和 `BannerApplicationRunner`,用于在应用启动时输出框架 Banner 信息。
|
|
||||||
|
|
||||||
这部分属于展示型能力,不影响具体业务逻辑。
|
|
||||||
|
|
||||||
## 4. 开发人员上手
|
|
||||||
|
|
||||||
### 4.1 引入依赖
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.njcn</groupId>
|
|
||||||
<artifactId>rdms-spring-boot-starter-web</artifactId>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
本模块已经聚合:
|
|
||||||
|
|
||||||
- `spring-boot-starter-web`
|
|
||||||
- `spring-boot-starter-validation`
|
|
||||||
- `knife4j-openapi3-jakarta-spring-boot-starter`
|
|
||||||
- `springdoc-openapi-starter-webmvc-ui`
|
|
||||||
|
|
||||||
如果项目已经单独引入了这些依赖,需要留意是否存在重复配置。
|
|
||||||
|
|
||||||
### 4.2 准备基础配置
|
|
||||||
|
|
||||||
建议至少配置以下内容:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
spring:
|
|
||||||
application:
|
|
||||||
name: rdms-system-server
|
|
||||||
|
|
||||||
rdms:
|
|
||||||
web:
|
|
||||||
admin-api:
|
|
||||||
prefix: /admin-api
|
|
||||||
controller: "**.controller.admin.**"
|
|
||||||
app-api:
|
|
||||||
prefix: /app-api
|
|
||||||
controller: "**.controller.app.**"
|
|
||||||
admin-ui:
|
|
||||||
url: http://127.0.0.1:80
|
|
||||||
|
|
||||||
swagger:
|
|
||||||
title: RDMS API # 文档标题
|
|
||||||
description: RDMS 接口文档 # 文档描述
|
|
||||||
author: RDMS # 作者/团队
|
|
||||||
version: 1.0.0 # 版本号
|
|
||||||
url: https://example.com # 项目或团队主页
|
|
||||||
email: dev@example.com # 联系邮箱
|
|
||||||
license: Apache 2.0 # 协议名称
|
|
||||||
license-url: https://www.apache.org/licenses/LICENSE-2.0.html # 协议地址
|
|
||||||
|
|
||||||
access-log:
|
|
||||||
enable: true
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.3 按约定放置 Controller
|
|
||||||
|
|
||||||
如果希望自动带上前缀,需要把 Controller 放到约定包下:
|
|
||||||
|
|
||||||
- 管理端:`xx.controller.admin.xx`
|
|
||||||
- 应用端:`xx.controller.app.xx`
|
|
||||||
|
|
||||||
不符合这个包路径规则的 Controller,不会自动追加 `/admin-api` 或 `/app-api`。
|
|
||||||
|
|
||||||
### 4.4 统一返回 `CommonResult`
|
|
||||||
|
|
||||||
Controller 写法建议保持统一:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Tag(name = "部门管理")
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/dept")
|
|
||||||
public class DeptController {
|
|
||||||
|
|
||||||
@GetMapping("/get")
|
|
||||||
@Operation(summary = "获得部门详情")
|
|
||||||
public CommonResult<DeptRespVO> get(@RequestParam("id") Long id) {
|
|
||||||
return CommonResult.success(deptService.get(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
建议遵循:
|
|
||||||
|
|
||||||
- 成功场景返回 `CommonResult.success(...)`
|
|
||||||
- 业务异常抛出 `ServiceException`
|
|
||||||
- 参数对象使用 `@Validated` / `@Valid`
|
|
||||||
|
|
||||||
### 4.5 使用访问日志
|
|
||||||
|
|
||||||
多数情况下,不需要额外写代码,访问日志会自动生效。
|
|
||||||
|
|
||||||
如果需要补充操作名、操作类型、响应体记录等信息,可以加:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@ApiAccessLog(responseEnable = true)
|
|
||||||
```
|
|
||||||
|
|
||||||
建议同时补齐:
|
|
||||||
|
|
||||||
- `@Tag`
|
|
||||||
- `@Operation`
|
|
||||||
|
|
||||||
这样日志中的操作模块和操作名称会更完整;建议将 `@Tag` / `@Operation` 作为接口开发的必填规范。
|
|
||||||
|
|
||||||
### 4.6 使用加解密
|
|
||||||
|
|
||||||
只在需要的接口上加:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@ApiEncrypt
|
|
||||||
```
|
|
||||||
|
|
||||||
然后让调用方按约定传输密文即可。接口内部仍按明文对象开发,不需要在 Controller 内手工做解密逻辑。
|
|
||||||
|
|
||||||
### 4.7 使用字段脱敏
|
|
||||||
|
|
||||||
在返回对象字段上直接加注解即可:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@MobileDesensitize
|
|
||||||
private String mobile;
|
|
||||||
```
|
|
||||||
|
|
||||||
适合用户信息、联系方式、证件信息等对外展示场景。
|
|
||||||
|
|
||||||
### 4.8 使用 RestTemplate
|
|
||||||
|
|
||||||
固定地址调用:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Resource
|
|
||||||
private RestTemplate restTemplate;
|
|
||||||
```
|
|
||||||
|
|
||||||
服务名调用:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Resource(name = "loadBalancedRestTemplate")
|
|
||||||
private RestTemplate loadBalancedRestTemplate;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.9 开发时需要注意的几个点
|
|
||||||
|
|
||||||
- 这个模块不会自动把任意返回值包装成 `CommonResult`,需要显式返回。
|
|
||||||
- 接口前缀是按包路径匹配出来的,不是看 Controller 名称。
|
|
||||||
- 访问日志默认开启,但是否真正异步落库,还取决于日志相关 RPC 接口是否可用。
|
|
||||||
- XSS、防脱敏、加解密都属于横切能力,原则上应该通过统一配置或注解使用,不建议在业务代码里重复造轮子。
|
|
||||||
- 如果项目已经在网关层处理跨域、日志、安全头等逻辑,需要评估是否与本模块职责重叠。
|
|
||||||
|
|
||||||
## 5. 适合承载什么,不适合承载什么
|
|
||||||
|
|
||||||
适合放在这个模块里的能力:
|
|
||||||
|
|
||||||
- Web 层公共配置
|
|
||||||
- HTTP 请求/响应横切处理
|
|
||||||
- 接口文档
|
|
||||||
- 访问日志
|
|
||||||
- 安全性输入输出处理
|
|
||||||
- Web 基础工具注册
|
|
||||||
|
|
||||||
不适合放在这个模块里的能力:
|
|
||||||
|
|
||||||
- 具体业务规则
|
|
||||||
- 菜单、权限、数据权限等业务权限判断
|
|
||||||
- 某个业务模块专属的 Controller 逻辑
|
|
||||||
- 与单一业务强耦合的转换规则
|
|
||||||
|
|
||||||
从职责上看,这个模块更接近“Web 基础设施层”,而不是“业务功能层”。
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
# rdms-spring-boot-starter-websocket
|
|
||||||
|
|
||||||
## 1. 模块定位
|
|
||||||
|
|
||||||
`rdms-spring-boot-starter-websocket` 是 WebSocket 基础设施模块,用于统一处理连接、会话管理、消息分发与消息发送。
|
|
||||||
|
|
||||||
模块聚合的核心能力:
|
|
||||||
|
|
||||||
- WebSocket 自动装配与路径注册
|
|
||||||
- 登录用户绑定与会话管理
|
|
||||||
- JSON 消息协议与监听器分发
|
|
||||||
- 消息发送
|
|
||||||
|
|
||||||
## 2. 设计思路
|
|
||||||
|
|
||||||
- 统一 JSON 消息协议(`type` + `content`),通过 `type` 分发到对应监听器,降低业务耦合。
|
|
||||||
- 通过 `WebSocketSessionManager` 统一管理会话,支持按用户类型/用户编号/会话 ID 进行推送。
|
|
||||||
- 通过本地发送器支持单机消息推送。
|
|
||||||
- 与安全体系结合,在握手阶段写入 `LoginUser`,便于后续鉴权与定向发送。
|
|
||||||
|
|
||||||
## 3. 功能模块
|
|
||||||
|
|
||||||
### 3.1 自动装配
|
|
||||||
|
|
||||||
`RdmsWebSocketAutoConfiguration` 会完成:
|
|
||||||
|
|
||||||
- 注册 WebSocket 路径与处理器
|
|
||||||
- 注册握手拦截器(默认 `LoginUserHandshakeInterceptor`)
|
|
||||||
- 注册 `WebSocketSessionManager`
|
|
||||||
- 放行 WebSocket 路径的安全校验
|
|
||||||
- 注册消息发送器
|
|
||||||
|
|
||||||
### 3.2 JSON 消息协议
|
|
||||||
|
|
||||||
内置统一消息结构:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "notice",
|
|
||||||
"content": "{...}"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
处理流程:
|
|
||||||
|
|
||||||
- `JsonWebSocketMessageHandler` 解析 JSON
|
|
||||||
- 根据 `type` 选择对应 `WebSocketMessageListener`
|
|
||||||
- 将 `content` 反序列化为监听器泛型类型并交给业务处理
|
|
||||||
|
|
||||||
内置 `ping/pong`:收到 `ping` 会直接返回 `pong`。
|
|
||||||
|
|
||||||
### 3.3 会话管理
|
|
||||||
|
|
||||||
`WebSocketSessionManager` 支持:
|
|
||||||
|
|
||||||
- 按 `sessionId` 获取会话
|
|
||||||
- 按 `userType` 获取会话列表
|
|
||||||
- 按 `userType + userId` 获取会话列表
|
|
||||||
|
|
||||||
用于定向推送和广播推送。
|
|
||||||
|
|
||||||
### 3.4 消息发送
|
|
||||||
|
|
||||||
统一使用 `WebSocketMessageSender` 发送消息:
|
|
||||||
|
|
||||||
- 按用户推送
|
|
||||||
- 按用户类型广播
|
|
||||||
- 按会话 ID 推送
|
|
||||||
|
|
||||||
消息会被封装为统一的 `JsonWebSocketMessage` 后发送。
|
|
||||||
|
|
||||||
### 3.5 登录用户绑定
|
|
||||||
|
|
||||||
握手阶段会读取当前登录用户并写入 WebSocket Session:
|
|
||||||
|
|
||||||
- 需要前端通过 `?token={token}` 形式携带令牌
|
|
||||||
- `LoginUserHandshakeInterceptor` 会将 `LoginUser` 写入 Session
|
|
||||||
- `WebSocketFrameworkUtils` 可获取 `userId/userType`
|
|
||||||
|
|
||||||
## 4. 开发人员上手
|
|
||||||
|
|
||||||
### 4.1 引入依赖
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.njcn</groupId>
|
|
||||||
<artifactId>rdms-spring-boot-starter-websocket</artifactId>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 基础配置
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
rdms:
|
|
||||||
websocket:
|
|
||||||
enable: true
|
|
||||||
path: /ws
|
|
||||||
```
|
|
||||||
|
|
||||||
说明:
|
|
||||||
|
|
||||||
- `path` 默认 `/ws`
|
|
||||||
- `enable` 默认 `true`
|
|
||||||
|
|
||||||
### 4.3 连接方式
|
|
||||||
|
|
||||||
前端连接示例:
|
|
||||||
|
|
||||||
```
|
|
||||||
ws://{host}:{port}/ws?token={token}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.4 编写消息监听器
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Component
|
|
||||||
public class NoticeMessageListener implements WebSocketMessageListener<NoticeDTO> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMessage(WebSocketSession session, NoticeDTO message) {
|
|
||||||
// 处理消息
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getType() {
|
|
||||||
return "notice";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.5 服务端推送
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Resource
|
|
||||||
private WebSocketMessageSender webSocketMessageSender;
|
|
||||||
|
|
||||||
public void push(Long userId, NoticeDTO notice) {
|
|
||||||
webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), userId, "notice", notice);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
# rdms-gateway
|
|
||||||
|
|
||||||
## 1. 模块定位
|
|
||||||
|
|
||||||
`rdms-gateway` 是 API 服务网关模块,基于 Spring Cloud Gateway(WebFlux)实现,负责统一入口的路由转发、鉴权透传、跨域处理、访问日志、灰度/标签负载与全局异常处理。
|
|
||||||
|
|
||||||
## 2. 设计思路
|
|
||||||
|
|
||||||
- 通过网关统一处理横切能力,业务服务只关注业务逻辑。
|
|
||||||
- 鉴权只做“解析 + 透传”,是否需要登录交给后端服务判定。
|
|
||||||
- 灰度/标签负载通过自定义 `grayLb://` 方案实现。
|
|
||||||
- 访问日志默认打印到日志,后续可扩展为落库。
|
|
||||||
|
|
||||||
## 3. 功能模块
|
|
||||||
|
|
||||||
### 3.1 路由与文档聚合
|
|
||||||
|
|
||||||
- 路由配置集中在 `application.yaml` 的 `spring.cloud.gateway.server.webflux.routes`。
|
|
||||||
- Knife4j Gateway 负责聚合各服务的 OpenAPI 文档。
|
|
||||||
|
|
||||||
### 3.2 Token 鉴权透传
|
|
||||||
|
|
||||||
`TokenAuthenticationFilter` 负责:
|
|
||||||
|
|
||||||
- 从 `Authorization` 解析 Token
|
|
||||||
- 远程校验 Token 有效性
|
|
||||||
- 校验通过后将 `login-user` 头透传给后端
|
|
||||||
- 不阻断请求,是否需要登录由后端服务决定
|
|
||||||
|
|
||||||
### 3.3 跨域处理
|
|
||||||
|
|
||||||
`CorsFilter` 统一放行跨域请求,并处理 OPTIONS 预检请求。
|
|
||||||
|
|
||||||
### 3.4 访问日志
|
|
||||||
|
|
||||||
`AccessLogFilter` 记录:
|
|
||||||
|
|
||||||
- 请求路径、方法、参数、Body、Headers
|
|
||||||
- 响应体、响应码
|
|
||||||
- 耗时、用户信息
|
|
||||||
|
|
||||||
当前实现是打印到日志(控制台/文件),可扩展为落库。
|
|
||||||
|
|
||||||
### 3.5 灰度/标签负载
|
|
||||||
|
|
||||||
灰度发布(Gray/Canary)是指新旧版本并行运行,只让一小部分请求先进入新版本验证,稳定后再逐步扩大流量,最终全量切换。
|
|
||||||
|
|
||||||
在本项目中,灰度不是按“百分比随机分流”,而是**按请求头定向路由**:
|
|
||||||
|
|
||||||
- 请求头 `version` 用于匹配实例 `metadata.version`
|
|
||||||
- 请求头 `tag` 用于匹配实例 `metadata.tag`
|
|
||||||
- 如果匹配不到目标实例,会回退到全量实例
|
|
||||||
- 最终按 Nacos 权重随机选择实例
|
|
||||||
|
|
||||||
`grayLb://` + `GrayLoadBalancer` 规则:
|
|
||||||
|
|
||||||
- header `version` 匹配实例 metadata `version`
|
|
||||||
- header `tag` 匹配实例 metadata `tag`
|
|
||||||
- 无匹配时回落到全量实例
|
|
||||||
|
|
||||||
注意:
|
|
||||||
|
|
||||||
- 不带 `tag` 的请求会优先过滤掉带 `tag` 的实例,避免测试/灰度实例被默认流量命中
|
|
||||||
- 灰度发布依赖请求头控制路由范围,需要由网关或内部调用方注入 `version/tag`,避免外部随意绕路
|
|
||||||
|
|
||||||
### 3.6 全局异常
|
|
||||||
|
|
||||||
`GlobalExceptionHandler` 将异常统一翻译为 `CommonResult` 返回。
|
|
||||||
|
|
||||||
### 3.7 Jackson 一致性
|
|
||||||
|
|
||||||
`GatewayJacksonAutoConfiguration` 统一 JSON 序列化策略(时间戳、Long -> Number 等)。
|
|
||||||
|
|
||||||
## 4. 开发人员上手
|
|
||||||
|
|
||||||
### 4.1 路由配置示例
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
spring:
|
|
||||||
cloud:
|
|
||||||
gateway:
|
|
||||||
server:
|
|
||||||
webflux:
|
|
||||||
routes:
|
|
||||||
- id: system-admin-api
|
|
||||||
uri: grayLb://rdms-system-server
|
|
||||||
predicates:
|
|
||||||
- Path=/admin-api/system/**
|
|
||||||
filters:
|
|
||||||
- RewritePath=/admin-api/system/v3/api-docs, /v3/api-docs
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 鉴权透传
|
|
||||||
|
|
||||||
前端携带:
|
|
||||||
|
|
||||||
```
|
|
||||||
Authorization: Bearer <token>
|
|
||||||
```
|
|
||||||
|
|
||||||
网关校验成功后,会透传 `login-user` 给后端服务。
|
|
||||||
|
|
||||||
### 4.3 灰度/标签路由
|
|
||||||
|
|
||||||
请求头示例:
|
|
||||||
|
|
||||||
```
|
|
||||||
version: 1.0.0
|
|
||||||
tag: dev
|
|
||||||
```
|
|
||||||
|
|
||||||
服务实例需要在注册中心 metadata 中配置对应 `version/tag`。
|
|
||||||
|
|
||||||
### 4.4 WebSocket 路由
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- id: system-websocket
|
|
||||||
uri: grayLb://rdms-system-server
|
|
||||||
predicates:
|
|
||||||
- Path=/system/ws/**
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.5 文档聚合示例
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
knife4j:
|
|
||||||
gateway:
|
|
||||||
enabled: true
|
|
||||||
routes:
|
|
||||||
- name: system-server
|
|
||||||
service-name: rdms-system-server
|
|
||||||
url: /admin-api/system/v3/api-docs
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. 关键类索引
|
|
||||||
|
|
||||||
- 入口:`src/main/java/com/njcn/rdms/gateway/GatewayServerApplication.java`
|
|
||||||
- 路由配置:`src/main/resources/application.yaml`
|
|
||||||
- 鉴权:`src/main/java/com/njcn/rdms/gateway/filter/security/TokenAuthenticationFilter.java`
|
|
||||||
- 灰度:`src/main/java/com/njcn/rdms/gateway/filter/grey/GrayLoadBalancer.java`
|
|
||||||
- 访问日志:`src/main/java/com/njcn/rdms/gateway/filter/logging/AccessLogFilter.java`
|
|
||||||
- 跨域:`src/main/java/com/njcn/rdms/gateway/filter/cors/CorsFilter.java`
|
|
||||||
- 全局异常:`src/main/java/com/njcn/rdms/gateway/handler/GlobalExceptionHandler.java`
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
# rdms-system-api
|
|
||||||
|
|
||||||
**概述**
|
|
||||||
本模块定义了 System(系统)模块对其他模块开放的公共 RPC API 面。它是一个纯 API 的 jar 包:包含 Feign 客户端接口、请求/响应 DTO,以及枚举/常量。不包含 controller 或 service 的实现。
|
|
||||||
|
|
||||||
**这里包含什么**
|
|
||||||
- 位于 `com.njcn.rdms.module.system.api.*` 下的 Feign 客户端接口
|
|
||||||
- 位于 `com.njcn.rdms.module.system.api.*.dto` 下的 DTO
|
|
||||||
- 位于 `com.njcn.rdms.module.system.enums.*` 下的系统枚举和常量
|
|
||||||
|
|
||||||
**API 分组**
|
|
||||||
- `config`:`ConfigApi`,用于根据 key 读取配置值
|
|
||||||
- `dept`:`DeptApi` 和 `PostApi`,用于部门与岗位数据及校验
|
|
||||||
- `dict`:`DictDataApi`,用于字典数据校验(继承 `DictDataCommonApi`)
|
|
||||||
- `file`:`FileApi`,用于文件创建与预签名 URL
|
|
||||||
- `logger`:`LoginLogApi` 和 `OperateLogApi`(继承 `OperateLogCommonApi`)
|
|
||||||
- `notify`:`NotifyMessageSendApi`,用于站内消息发送
|
|
||||||
- `permission`:`PermissionApi`(继承 `PermissionCommonApi`)以及 `RoleApi`
|
|
||||||
- `user`:`AdminUserApi`,用于管理员用户查询与校验
|
|
||||||
- `websocket`:`WebSocketSenderApi`,用于推送 WebSocket 消息
|
|
||||||
|
|
||||||
**关键常量**
|
|
||||||
- `ApiConstants.NAME = "system-server"`(必须与 system 服务的 `spring.application.name` 保持一致)
|
|
||||||
- `ApiConstants.PREFIX = "/rpc-api/system"`
|
|
||||||
- `ApiConstants.VERSION = "1.0.0"`
|
|
||||||
|
|
||||||
**用法**
|
|
||||||
1. 在调用方模块中添加依赖。
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.njcn</groupId>
|
|
||||||
<artifactId>rdms-system-api</artifactId>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 在调用方服务中启用 Feign 客户端扫描。
|
|
||||||
|
|
||||||
```java
|
|
||||||
@EnableFeignClients(basePackages = "com.njcn.rdms.module.system.api")
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 注入并调用 API 接口。
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Resource
|
|
||||||
private AdminUserApi adminUserApi;
|
|
||||||
|
|
||||||
public AdminUserRespDTO loadUser(Long id) {
|
|
||||||
return adminUserApi.getUser(id).getCheckedData();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**备注**
|
|
||||||
- 所有方法都返回 `CommonResult<T>`。按需使用 `getCheckedData()` 或 `checkError()`。
|
|
||||||
- 有些 API 提供了默认的辅助方法(例如 `FileApi.createFile(...)`、`WebSocketSenderApi.send(...)`)。
|
|
||||||
- `DictDataApi`、`OperateLogApi`、`PermissionApi` 继承了来自 `rdms-framework` 的通用 RPC 接口。
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
### 请求 /login 接口 => 成功
|
|
||||||
POST {{baseUrl}}/system/auth/login
|
|
||||||
Content-Type: application/json
|
|
||||||
tag: Yunai.local
|
|
||||||
|
|
||||||
{
|
|
||||||
"username": "admin",
|
|
||||||
"password": "admin123",
|
|
||||||
"uuid": "3acd87a09a4f48fb9118333780e94883",
|
|
||||||
"code": "1024"
|
|
||||||
}
|
|
||||||
|
|
||||||
### 请求 /login 接口【加密 AES】 => 成功
|
|
||||||
POST {{baseUrl}}/system/auth/login
|
|
||||||
Content-Type: application/json
|
|
||||||
tag: Yunai.local
|
|
||||||
X-API-ENCRYPT: true
|
|
||||||
|
|
||||||
WvSX9MOrenyGfBhEM0g1/hHgq8ocktMZ9OwAJ6MOG5FUrzYF/rG5JF1eMptQM1wT73VgDS05l/37WeRtad+JrqChAul/sR/SdOsUKqjBhvvQx1JVhzxr6s8uUP67aKTSZ6Psv7O32ELxXrzSaQvG5CInzz3w6sLtbNNLd1kXe6Q=
|
|
||||||
|
|
||||||
### 请求 /login 接口【加密 RSA】 => 成功
|
|
||||||
POST {{baseUrl}}/system/auth/login
|
|
||||||
Content-Type: application/json
|
|
||||||
tag: Yunai.local
|
|
||||||
X-API-ENCRYPT: true
|
|
||||||
|
|
||||||
e7QZTork9ZV5CmgZvSd+cHZk3xdUxKtowLM02kOha+gxHK2H/daU8nVBYS3+bwuDRy5abf+Pz1QJJGVAEd27wwrXBmupOOA/bhpuzzDwcRuJRD+z+YgiNoEXFDRHERxPYlPqAe9zAHtihD0ceub1AjybQsEsROew4C3Q602XYW0=
|
|
||||||
|
|
||||||
### 请求 /login 接口 => 成功(无验证码)
|
|
||||||
POST {{baseUrl}}/system/auth/login
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"username": "admin",
|
|
||||||
"password": "admin123"
|
|
||||||
}
|
|
||||||
|
|
||||||
### 请求 /get-permission-info 接口 => 成功
|
|
||||||
GET {{baseUrl}}/system/auth/get-permission-info
|
|
||||||
Authorization: Bearer {{token}}
|
|
||||||
|
|
||||||
### 请求 /list-menus 接口 => 成功
|
|
||||||
GET {{baseUrl}}/system/list-menus
|
|
||||||
Authorization: Bearer {{token}}
|
|
||||||
#Authorization: Bearer a6aa7714a2e44c95aaa8a2c5adc2a67a
|
|
||||||
@@ -19,7 +19,7 @@ public class AuthRegisterReqVO extends CaptchaVerificationReqVO {
|
|||||||
@Size(min = 4, max = 30, message = "用户账号长度为 4-30 个字符")
|
@Size(min = 4, max = 30, message = "用户账号长度为 4-30 个字符")
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
|
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "awen")
|
||||||
@NotBlank(message = "用户昵称不能为空")
|
@NotBlank(message = "用户昵称不能为空")
|
||||||
@Size(max = 30, message = "用户昵称长度不能超过 30 个字符")
|
@Size(max = 30, message = "用户昵称长度不能超过 30 个字符")
|
||||||
private String nickname;
|
private String nickname;
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
### 请求 /menu/list 接口 => 成功
|
|
||||||
GET {{baseUrl}}/system/dict-data/list-all-simple
|
|
||||||
Authorization: Bearer {{token}}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
### 请求 /system/file-config/create 接口 => 成功
|
|
||||||
POST {{baseUrl}}/system/file-config/create
|
|
||||||
Content-Type: application/json
|
|
||||||
Authorization: Bearer {{token}}
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "S3 - 七牛云",
|
|
||||||
"remark": "",
|
|
||||||
"storage": 20,
|
|
||||||
"config": {
|
|
||||||
"accessKey": "b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8",
|
|
||||||
"accessSecret": "kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP",
|
|
||||||
"bucket": "ruoyi-vue-pro",
|
|
||||||
"endpoint": "s3-cn-south-1.qiniucs.com",
|
|
||||||
"domain": "http://test.rdms.iocoder.cn",
|
|
||||||
"region": "oss-cn-beijing"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
### 请求 /system/file-config/update 接口 => 成功
|
|
||||||
PUT {{baseUrl}}/system/file-config/update
|
|
||||||
Content-Type: application/json
|
|
||||||
Authorization: Bearer {{token}}
|
|
||||||
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"name": "S3 - 七牛云",
|
|
||||||
"remark": "",
|
|
||||||
"config": {
|
|
||||||
"accessKey": "b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8",
|
|
||||||
"accessSecret": "kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP",
|
|
||||||
"bucket": "ruoyi-vue-pro",
|
|
||||||
"endpoint": "s3-cn-south-1.qiniucs.com",
|
|
||||||
"domain": "http://test.rdms.iocoder.cn",
|
|
||||||
"region": "oss-cn-beijing"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
### 请求 /system/file-config/test 接口 => 成功
|
|
||||||
GET {{baseUrl}}/system/file-config/test?id=2
|
|
||||||
Content-Type: application/json
|
|
||||||
Authorization: Bearer {{token}}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
### 获得地区树
|
|
||||||
GET {{baseUrl}}/system/area/tree
|
|
||||||
Authorization: Bearer {{token}}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
### 请求 /system/operate-log/page 接口 => 成功
|
|
||||||
GET {{baseUrl}}/system/operate-log/page
|
|
||||||
Authorization: Bearer {{token}}
|
|
||||||
@@ -29,7 +29,7 @@ public class OperateLogRespVO implements VO {
|
|||||||
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||||
@Trans(type = TransType.SIMPLE, target = AdminUserDO.class, fields = "nickname", ref = "userName")
|
@Trans(type = TransType.SIMPLE, target = AdminUserDO.class, fields = "nickname", ref = "userName")
|
||||||
private Long userId;
|
private Long userId;
|
||||||
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
|
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "awen")
|
||||||
@ExcelProperty("操作人")
|
@ExcelProperty("操作人")
|
||||||
private String userName;
|
private String userName;
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public class NotifyMessageRespVO {
|
|||||||
@Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01")
|
@Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01")
|
||||||
private String templateCode;
|
private String templateCode;
|
||||||
|
|
||||||
@Schema(description = "模版发送人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
|
@Schema(description = "模版发送人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "awen")
|
||||||
private String templateNickname;
|
private String templateNickname;
|
||||||
|
|
||||||
@Schema(description = "模版内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试内容")
|
@Schema(description = "模版内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试内容")
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
### 请求 /login 接口 => 成功
|
|
||||||
POST {{baseUrl}}/system/oauth2-client/create
|
|
||||||
Content-Type: application/json
|
|
||||||
Authorization: Bearer {{token}}
|
|
||||||
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"secret": "admin123",
|
|
||||||
"name": "灿能源码",
|
|
||||||
"logo": "https://www.iocoder.cn/images/favicon.ico",
|
|
||||||
"description": "我是描述",
|
|
||||||
"status": 0,
|
|
||||||
"accessTokenValiditySeconds": 180,
|
|
||||||
"refreshTokenValiditySeconds": 8640,
|
|
||||||
"redirectUris": ["https://www.iocoder.cn"],
|
|
||||||
"autoApprove": true,
|
|
||||||
"authorizedGrantTypes": ["password"],
|
|
||||||
"scopes": ["user_info"],
|
|
||||||
"authorities": ["system:user:query"],
|
|
||||||
"resource_ids": ["1024"],
|
|
||||||
"additionalInformation": "{}"
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
### 请求 /system/oauth2/authorize 接口 => 成功
|
|
||||||
GET {{baseUrl}}/system/oauth2/authorize?clientId=default
|
|
||||||
Authorization: Bearer {{token}}
|
|
||||||
|
|
||||||
### 请求 /system/oauth2/authorize + token 接口 => 成功
|
|
||||||
POST {{baseUrl}}/system/oauth2/authorize
|
|
||||||
Content-Type: application/x-www-form-urlencoded
|
|
||||||
Authorization: Bearer {{token}}
|
|
||||||
|
|
||||||
response_type=token&client_id=default&scope={"user.read": true}&redirect_uri=https://www.iocoder.cn&auto_approve=true
|
|
||||||
|
|
||||||
### 请求 /system/oauth2/authorize + code 接口 => 成功
|
|
||||||
POST {{baseUrl}}/system/oauth2/authorize
|
|
||||||
Content-Type: application/x-www-form-urlencoded
|
|
||||||
Authorization: Bearer {{token}}
|
|
||||||
|
|
||||||
response_type=code&client_id=default&scope={"user.read": true}&redirect_uri=https://www.iocoder.cn&auto_approve=false
|
|
||||||
|
|
||||||
### 请求 /system/oauth2/token + code 接口 => 成功
|
|
||||||
POST {{baseUrl}}/system/oauth2/token
|
|
||||||
Content-Type: application/x-www-form-urlencoded
|
|
||||||
Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
|
|
||||||
|
|
||||||
grant_type=authorization_code&redirect_uri=https://www.iocoder.cn&code=189956c07a174588a97157eabef2f93a
|
|
||||||
|
|
||||||
### 请求 /system/oauth2/token + password 接口 => 成功
|
|
||||||
POST {{baseUrl}}/system/oauth2/token
|
|
||||||
Content-Type: application/x-www-form-urlencoded
|
|
||||||
Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
|
|
||||||
|
|
||||||
grant_type=password&username=admin&password=admin123&scope=user.read
|
|
||||||
|
|
||||||
### 请求 /system/oauth2/token + client_credentials 接口 => 成功
|
|
||||||
POST {{baseUrl}}/system/oauth2/token
|
|
||||||
Content-Type: application/x-www-form-urlencoded
|
|
||||||
Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
|
|
||||||
|
|
||||||
grant_type=client_credentials&scope=user.read
|
|
||||||
|
|
||||||
### 请求 /system/oauth2/token + refresh_token 接口 => 成功
|
|
||||||
POST {{baseUrl}}/system/oauth2/token
|
|
||||||
Content-Type: application/x-www-form-urlencoded
|
|
||||||
Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
|
|
||||||
|
|
||||||
grant_type=refresh_token&refresh_token=00895465d6994f72a9d926ceeed0f588
|
|
||||||
|
|
||||||
### 请求 /system/oauth2/token + DELETE 接口 => 成功
|
|
||||||
DELETE {{baseUrl}}/system/oauth2/token?token=ca8a188f464441d6949c51493a2b7596
|
|
||||||
Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
|
|
||||||
|
|
||||||
### 请求 /system/oauth2/check-token 接口 => 成功
|
|
||||||
POST {{baseUrl}}/system/oauth2/check-token?token=620d307c5b4148df8a98dd6c6c547106
|
|
||||||
Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
### 请求 /system/oauth2/user/get 接口 => 成功
|
|
||||||
GET {{baseUrl}}/system/oauth2/user/get
|
|
||||||
Authorization: Bearer 47f9c74ec11041f193b777ebb95c3b0d
|
|
||||||
|
|
||||||
### 请求 /system/oauth2/user/update 接口 => 成功
|
|
||||||
PUT {{baseUrl}}/system/oauth2/user/update
|
|
||||||
Content-Type: application/json
|
|
||||||
Authorization: Bearer 47f9c74ec11041f193b777ebb95c3b0d
|
|
||||||
|
|
||||||
{
|
|
||||||
"nickname": "灿能源码"
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,7 @@ public class OAuth2UserInfoRespVO {
|
|||||||
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
|
@Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "awen")
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "灿能")
|
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "灿能")
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import jakarta.validation.constraints.Size;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class OAuth2UserUpdateReqVO {
|
public class OAuth2UserUpdateReqVO {
|
||||||
|
|
||||||
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
|
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "awen")
|
||||||
@Size(max = 30, message = "用户昵称长度不能超过 30 个字符")
|
@Size(max = 30, message = "用户昵称长度不能超过 30 个字符")
|
||||||
private String nickname;
|
private String nickname;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
### 请求 /menu/list 接口 => 成功
|
|
||||||
GET {{baseUrl}}/system/menu/list
|
|
||||||
Authorization: Bearer {{token}}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
### /role/create 成功
|
|
||||||
POST {{baseUrl}}/system/role/create
|
|
||||||
Authorization: Bearer {{token}}
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "测试角色",
|
|
||||||
"code": "test",
|
|
||||||
"sort": 0
|
|
||||||
}
|
|
||||||
|
|
||||||
### /role/update 成功
|
|
||||||
POST {{baseUrl}}/system/role/update
|
|
||||||
Authorization: Bearer {{token}}
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"id": 100,
|
|
||||||
"name": "测试角色",
|
|
||||||
"code": "test",
|
|
||||||
"sort": 10
|
|
||||||
}
|
|
||||||
### /resource/delete 成功
|
|
||||||
POST {{baseUrl}}/system/role/delete
|
|
||||||
Content-Type: application/x-www-form-urlencoded
|
|
||||||
Authorization: Bearer {{token}}
|
|
||||||
|
|
||||||
roleId=14
|
|
||||||
|
|
||||||
### /role/get 成功
|
|
||||||
GET {{baseUrl}}/system/role/get?id=100
|
|
||||||
Content-Type: application/x-www-form-urlencoded
|
|
||||||
Authorization: Bearer {{token}}
|
|
||||||
|
|
||||||
### /role/page 成功
|
|
||||||
GET {{baseUrl}}/system/role/page?pageNo=1&pageSize=10
|
|
||||||
Authorization: Bearer {{token}}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
### 请求 /system/redis/get-monitor-info 接口 => 成功
|
|
||||||
GET {{baseUrl}}/system/redis/get-monitor-info
|
|
||||||
Authorization: Bearer {{token}}
|
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
### 请求 /system/user/page 接口 => 没有权限
|
|
||||||
GET {{baseUrl}}/system/user/page?pageNo=1&pageSize=10
|
|
||||||
Authorization: Bearer {{token}}
|
|
||||||
#Authorization: Bearer test100
|
|
||||||
|
|
||||||
### 请求 /system/user/page 接口(测试访问别的租户)
|
|
||||||
GET {{baseUrl}}/system/user/page?pageNo=1&pageSize=10
|
|
||||||
Authorization: Bearer {{token}}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
### 请求 /system/user/profile/get 接口 => 没有权限
|
|
||||||
GET {{baseUrl}}/system/user/profile/get
|
|
||||||
Authorization: Bearer {{token}}
|
|
||||||
@@ -19,7 +19,7 @@ public class UserProfileRespVO {
|
|||||||
@Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "rdms")
|
@Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "rdms")
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
|
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "awen")
|
||||||
private String nickname;
|
private String nickname;
|
||||||
|
|
||||||
@Schema(description = "用户邮箱", example = "rdms@iocoder.cn")
|
@Schema(description = "用户邮箱", example = "rdms@iocoder.cn")
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import org.hibernate.validator.constraints.URL;
|
|||||||
@Data
|
@Data
|
||||||
public class UserProfileUpdateReqVO {
|
public class UserProfileUpdateReqVO {
|
||||||
|
|
||||||
@Schema(description = "用户昵称", example = "芋艿")
|
@Schema(description = "用户昵称", example = "awen")
|
||||||
@Size(max = 30, message = "用户昵称长度不能超过 30 个字符")
|
@Size(max = 30, message = "用户昵称长度不能超过 30 个字符")
|
||||||
private String nickname;
|
private String nickname;
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public class UserRespVO{
|
|||||||
@ExcelProperty("用户名称")
|
@ExcelProperty("用户名称")
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
|
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "awen")
|
||||||
@ExcelProperty("用户昵称")
|
@ExcelProperty("用户昵称")
|
||||||
private String nickname;
|
private String nickname;
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public class UserSaveReqVO {
|
|||||||
@DiffLogField(name = "用户账号")
|
@DiffLogField(name = "用户账号")
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
|
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "awen")
|
||||||
@Size(max = 30, message = "用户昵称长度不能超过30个字符")
|
@Size(max = 30, message = "用户昵称长度不能超过30个字符")
|
||||||
@DiffLogField(name = "用户昵称")
|
@DiffLogField(name = "用户昵称")
|
||||||
private String nickname;
|
private String nickname;
|
||||||
|
|||||||
Reference in New Issue
Block a user