ai-engineering-init 1.4.3 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/skills/bug-detective/SKILL.md +19 -19
- package/.cursor/skills/project-navigator/SKILL.md +164 -258
- package/README.md +20 -236
- package/bin/index.js +437 -7
- package/package.json +7 -1
- package/scripts/build-skills.js +180 -0
- package/src/platform-map.json +56 -0
- package/src/skills/add-skill/SKILL.md +488 -0
- package/src/skills/add-todo/SKILL.md +269 -0
- package/src/skills/api-development/SKILL.md +266 -0
- package/src/skills/architecture-design/SKILL.md +262 -0
- package/src/skills/backend-annotations/SKILL.md +302 -0
- package/src/skills/banana-image/CHANGELOG.md +37 -0
- package/src/skills/banana-image/README.md +146 -0
- package/src/skills/banana-image/SKILL.md +171 -0
- package/src/skills/banana-image/assets/logo.png +0 -0
- package/src/skills/banana-image/references/advanced-usage.md +189 -0
- package/src/skills/banana-image/scripts/apply_template.py +125 -0
- package/src/skills/banana-image/scripts/banana_image_exec.ts +412 -0
- package/src/skills/banana-image/scripts/batch_prep.py +82 -0
- package/src/skills/banana-image/scripts/package-lock.json +1437 -0
- package/src/skills/banana-image/scripts/package.json +18 -0
- package/src/skills/banana-image/scripts/requirements.txt +10 -0
- package/src/skills/banana-image/templates/poster.json +22 -0
- package/src/skills/banana-image/templates/product.json +17 -0
- package/src/skills/banana-image/templates/social.json +22 -0
- package/src/skills/banana-image/templates/thumbnail.json +17 -0
- package/src/skills/brainstorm/SKILL.md +216 -0
- package/src/skills/bug-detective/SKILL.md +256 -0
- package/src/skills/bug-detective/references/error-patterns.md +242 -0
- package/src/skills/check/SKILL.md +367 -0
- package/src/skills/code-patterns/SKILL.md +280 -0
- package/src/skills/code-patterns/references/leniu-code-patterns.md +87 -0
- package/src/skills/codex-code-review/SKILL.md +135 -0
- package/src/skills/collaborating-with-codex/SKILL.md +174 -0
- package/src/skills/collaborating-with-codex/scripts/codex_bridge.py +275 -0
- package/src/skills/collaborating-with-gemini/SKILL.md +194 -0
- package/src/skills/collaborating-with-gemini/scripts/gemini_bridge.py +275 -0
- package/src/skills/crud/SKILL.md +265 -0
- package/src/skills/crud-development/SKILL.md +409 -0
- package/src/skills/data-permission/SKILL.md +292 -0
- package/src/skills/data-permission/references/custom-data-scope.md +90 -0
- package/src/skills/database-ops/SKILL.md +407 -0
- package/src/skills/dev/SKILL.md +187 -0
- package/src/skills/error-handler/SKILL.md +371 -0
- package/src/skills/file-oss-management/SKILL.md +255 -0
- package/src/skills/file-oss-management/references/entities.md +105 -0
- package/src/skills/file-oss-management/references/service-impl.md +104 -0
- package/src/skills/git-workflow/SKILL.md +397 -0
- package/src/skills/init-docs/SKILL.md +194 -0
- package/src/skills/json-serialization/SKILL.md +357 -0
- package/src/skills/leniu-api-development/SKILL.md +319 -0
- package/src/skills/leniu-api-development/references/real-examples.md +273 -0
- package/src/skills/leniu-architecture-design/SKILL.md +383 -0
- package/src/skills/leniu-backend-annotations/SKILL.md +277 -0
- package/src/skills/leniu-brainstorm/SKILL.md +242 -0
- package/src/skills/leniu-brainstorm/references/business-scenarios.md +162 -0
- package/src/skills/leniu-code-patterns/SKILL.md +411 -0
- package/src/skills/leniu-crud-development/SKILL.md +404 -0
- package/src/skills/leniu-crud-development/references/templates.md +597 -0
- package/src/skills/leniu-customization-location/SKILL.md +410 -0
- package/src/skills/leniu-data-permission/SKILL.md +341 -0
- package/src/skills/leniu-database-ops/SKILL.md +426 -0
- package/src/skills/leniu-error-handler/SKILL.md +462 -0
- package/src/skills/leniu-java-amount-handling/SKILL.md +461 -0
- package/src/skills/leniu-java-code-style/SKILL.md +510 -0
- package/src/skills/leniu-java-concurrent/SKILL.md +400 -0
- package/src/skills/leniu-java-entity/SKILL.md +237 -0
- package/src/skills/leniu-java-entity/references/templates.md +237 -0
- package/src/skills/leniu-java-export/SKILL.md +570 -0
- package/src/skills/leniu-java-logging/SKILL.md +229 -0
- package/src/skills/leniu-java-logging/references/data-mask.md +46 -0
- package/src/skills/leniu-java-logging/references/logging-scenarios.md +113 -0
- package/src/skills/leniu-java-mq/SKILL.md +338 -0
- package/src/skills/leniu-java-mybatis/SKILL.md +267 -0
- package/src/skills/leniu-java-mybatis/references/report-mapper.md +88 -0
- package/src/skills/leniu-java-report-query-param/SKILL.md +291 -0
- package/src/skills/leniu-java-task/SKILL.md +367 -0
- package/src/skills/leniu-java-total-line/SKILL.md +196 -0
- package/src/skills/leniu-marketing-price-rule-customizer/SKILL.md +301 -0
- package/src/skills/leniu-marketing-recharge-rule-customizer/SKILL.md +285 -0
- package/src/skills/leniu-mealtime/SKILL.md +215 -0
- package/src/skills/leniu-redis-cache/SKILL.md +331 -0
- package/src/skills/leniu-report-customization/SKILL.md +335 -0
- package/src/skills/leniu-report-customization/references/table-fields.md +93 -0
- package/src/skills/leniu-report-standard-customization/SKILL.md +328 -0
- package/src/skills/leniu-report-standard-customization/references/analysis-module.md +64 -0
- package/src/skills/leniu-report-standard-customization/references/table-fields.md +113 -0
- package/src/skills/leniu-security-guard/SKILL.md +306 -0
- package/src/skills/leniu-utils-toolkit/SKILL.md +380 -0
- package/src/skills/mysql-debug/SKILL.md +364 -0
- package/src/skills/next/SKILL.md +137 -0
- package/src/skills/openspec-apply-change/SKILL.md +165 -0
- package/src/skills/openspec-archive-change/SKILL.md +122 -0
- package/src/skills/openspec-bulk-archive-change/SKILL.md +254 -0
- package/src/skills/openspec-continue-change/SKILL.md +126 -0
- package/src/skills/openspec-explore/SKILL.md +299 -0
- package/src/skills/openspec-ff-change/SKILL.md +109 -0
- package/src/skills/openspec-new-change/SKILL.md +82 -0
- package/src/skills/openspec-onboard/SKILL.md +414 -0
- package/src/skills/openspec-sync-specs/SKILL.md +146 -0
- package/src/skills/openspec-verify-change/SKILL.md +176 -0
- package/src/skills/performance-doctor/SKILL.md +303 -0
- package/src/skills/progress/SKILL.md +193 -0
- package/src/skills/project-navigator/SKILL.md +211 -0
- package/src/skills/redis-cache/SKILL.md +333 -0
- package/src/skills/redis-cache/references/listeners.md +23 -0
- package/src/skills/scheduled-jobs/SKILL.md +314 -0
- package/src/skills/security-guard/SKILL.md +353 -0
- package/src/skills/security-guard/references/encrypt-config.md +103 -0
- package/src/skills/security-guard/references/sensitive-strategies.md +42 -0
- package/src/skills/sms-mail/SKILL.md +308 -0
- package/src/skills/sms-mail/references/mail-config.md +88 -0
- package/src/skills/sms-mail/references/sms-config.md +74 -0
- package/src/skills/social-login/SKILL.md +266 -0
- package/src/skills/social-login/references/provider-configs.md +118 -0
- package/src/skills/start/SKILL.md +154 -0
- package/src/skills/store-pc/SKILL.md +366 -0
- package/src/skills/sync/SKILL.md +149 -0
- package/src/skills/task-tracker/SKILL.md +307 -0
- package/src/skills/tech-decision/SKILL.md +393 -0
- package/src/skills/tenant-management/SKILL.md +288 -0
- package/src/skills/tenant-management/references/tenant-scenarios.md +91 -0
- package/src/skills/test-development/SKILL.md +301 -0
- package/src/skills/test-development/references/parameterized-examples.md +119 -0
- package/src/skills/ui-pc/SKILL.md +438 -0
- package/src/skills/update-status/SKILL.md +159 -0
- package/src/skills/utils-toolkit/SKILL.md +362 -0
- package/src/skills/utils-toolkit/references/redis-utils-api.md +56 -0
- package/src/skills/websocket-sse/SKILL.md +271 -0
- package/src/skills/workflow-engine/SKILL.md +321 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: utils-toolkit
|
|
3
|
+
description: |
|
|
4
|
+
后端工具类使用指南。包含 MapstructUtils、StringUtils、StreamUtils、TreeBuildUtils、DateUtils、RedisUtils 等核心工具类。
|
|
5
|
+
|
|
6
|
+
触发场景:
|
|
7
|
+
- 对象转换(BO/VO/Entity)
|
|
8
|
+
- 字符串处理、集合流操作
|
|
9
|
+
- 树结构构建
|
|
10
|
+
- Redis 缓存操作
|
|
11
|
+
- Excel 导入导出
|
|
12
|
+
|
|
13
|
+
触发词:工具类、MapstructUtils、StringUtils、StreamUtils、TreeBuildUtils、DateUtils、RedisUtils、ExcelUtil、JsonUtils、LoginHelper、convert、对象转换、集合操作、树结构、缓存
|
|
14
|
+
|
|
15
|
+
注意:
|
|
16
|
+
- 对象转换必须使用 MapstructUtils.convert(),禁止使用 BeanUtils。
|
|
17
|
+
- 本项目是纯后端项目,无前端工具类。
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# 后端工具类大全
|
|
21
|
+
|
|
22
|
+
## 快速索引
|
|
23
|
+
|
|
24
|
+
| 功能 | 工具类 | 包路径 | 常用方法 |
|
|
25
|
+
|------|--------|--------|---------|
|
|
26
|
+
| **对象转换** | `MapstructUtils` | `o.d.common.core.utils` | `convert()` |
|
|
27
|
+
| 字符串 | `StringUtils` | `o.d.common.core.utils` | `isBlank()`, `format()` |
|
|
28
|
+
| 集合/流 | `StreamUtils` | `o.d.common.core.utils` | `filter()`, `toList()`, `toMap()` |
|
|
29
|
+
| 树结构 | `TreeBuildUtils` | `o.d.common.core.utils` | `build()` |
|
|
30
|
+
| 日期时间 | `DateUtils` | `o.d.common.core.utils` | `getTime()`, `formatDateTime()` |
|
|
31
|
+
| 参数校验 | `ValidatorUtils` | `o.d.common.core.utils` | `validate()` |
|
|
32
|
+
| Redis缓存 | `RedisUtils` | `o.d.common.redis.utils` | `setCacheObject()`, `getCacheObject()` |
|
|
33
|
+
| Excel导出 | `ExcelUtil` | `o.d.common.excel.utils` | `exportExcel()` |
|
|
34
|
+
| JSON | `JsonUtils` | `o.d.common.json.utils` | `toJsonString()`, `parseObject()` |
|
|
35
|
+
| 登录用户 | `LoginHelper` | `o.d.common.satoken.utils` | `getUserId()`, `getUsername()` |
|
|
36
|
+
| 业务异常 | `ServiceException` | `o.d.common.core.exception` | `throw new ServiceException()` |
|
|
37
|
+
| Spring容器 | `SpringUtils` | `o.d.common.core.utils` | `getBean()` |
|
|
38
|
+
|
|
39
|
+
> **包路径说明**: `o.d` = `org.dromara`
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 1. 对象转换 - MapstructUtils(必须使用!)
|
|
44
|
+
|
|
45
|
+
> **强制规范**: 禁止使用 `BeanUtils.copyProperties()`,必须使用 `MapstructUtils.convert()`
|
|
46
|
+
|
|
47
|
+
```java
|
|
48
|
+
import org.dromara.common.core.utils.MapstructUtils;
|
|
49
|
+
|
|
50
|
+
// 单个对象转换
|
|
51
|
+
XxxVo vo = MapstructUtils.convert(entity, XxxVo.class);
|
|
52
|
+
|
|
53
|
+
// 集合转换
|
|
54
|
+
List<XxxVo> voList = MapstructUtils.convert(entityList, XxxVo.class);
|
|
55
|
+
|
|
56
|
+
// 转换到已有对象(合并属性)
|
|
57
|
+
XxxVo vo = MapstructUtils.convert(source, existingVo);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**配合 @AutoMapper 注解**:
|
|
61
|
+
|
|
62
|
+
```java
|
|
63
|
+
@AutoMapper(target = Xxx.class, reverseConvertGenerate = false)
|
|
64
|
+
public class XxxBo extends BaseEntity { }
|
|
65
|
+
|
|
66
|
+
@AutoMapper(target = Xxx.class)
|
|
67
|
+
public class XxxVo implements Serializable { }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 2. 字符串操作 - StringUtils
|
|
73
|
+
|
|
74
|
+
```java
|
|
75
|
+
import org.dromara.common.core.utils.StringUtils;
|
|
76
|
+
|
|
77
|
+
// 格式化(占位符 {})—— 项目特有
|
|
78
|
+
StringUtils.format("用户 {} 不存在", userId);
|
|
79
|
+
|
|
80
|
+
// 字符串分割/拼接 —— 项目特有
|
|
81
|
+
List<String> list = StringUtils.splitList("1,2,3");
|
|
82
|
+
List<Long> ids = StringUtils.splitTo("1,2,3", Convert::toLong);
|
|
83
|
+
String str = StringUtils.joinComma(list);
|
|
84
|
+
|
|
85
|
+
// 驼峰转换
|
|
86
|
+
StringUtils.toCamelCase("user_name"); // "userName"
|
|
87
|
+
StringUtils.toUnderScoreCase("userName"); // "user_name"
|
|
88
|
+
|
|
89
|
+
// Ant 风格路径匹配
|
|
90
|
+
StringUtils.isMatch("/api/**", "/api/user/list");
|
|
91
|
+
StringUtils.matches("/api/user", Arrays.asList("/api/**", "/admin/**"));
|
|
92
|
+
|
|
93
|
+
// 左补零
|
|
94
|
+
StringUtils.padl(5, 3); // "005"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 3. 集合与流操作 - StreamUtils
|
|
100
|
+
|
|
101
|
+
```java
|
|
102
|
+
import org.dromara.common.core.utils.StreamUtils;
|
|
103
|
+
|
|
104
|
+
// 过滤
|
|
105
|
+
List<User> active = StreamUtils.filter(users, u -> "1".equals(u.getStatus()));
|
|
106
|
+
|
|
107
|
+
// 查找
|
|
108
|
+
User user = StreamUtils.findFirstValue(users, u -> u.getId().equals(id));
|
|
109
|
+
|
|
110
|
+
// 提取属性
|
|
111
|
+
List<Long> ids = StreamUtils.toList(users, User::getId);
|
|
112
|
+
Set<Long> deptIds = StreamUtils.toSet(users, User::getDeptId);
|
|
113
|
+
|
|
114
|
+
// 转 Map
|
|
115
|
+
Map<Long, User> userMap = StreamUtils.toIdentityMap(users, User::getId);
|
|
116
|
+
Map<Long, String> nameMap = StreamUtils.toMap(users, User::getId, User::getName);
|
|
117
|
+
|
|
118
|
+
// 分组
|
|
119
|
+
Map<Long, List<User>> grouped = StreamUtils.groupByKey(users, User::getDeptId);
|
|
120
|
+
|
|
121
|
+
// 拼接字符串
|
|
122
|
+
String names = StreamUtils.join(users, User::getName); // "张三,李四,王五"
|
|
123
|
+
String names = StreamUtils.join(users, User::getName, "|"); // 自定义分隔符
|
|
124
|
+
|
|
125
|
+
// 排序 / 合并 Map
|
|
126
|
+
List<User> sorted = StreamUtils.sorted(users, Comparator.comparing(User::getCreateTime));
|
|
127
|
+
Map<Long, String> merged = StreamUtils.merge(map1, map2, (v1, v2) -> v1 + v2);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 4. 树结构构建 - TreeBuildUtils
|
|
133
|
+
|
|
134
|
+
```java
|
|
135
|
+
import org.dromara.common.core.utils.TreeBuildUtils;
|
|
136
|
+
import cn.hutool.core.lang.tree.Tree;
|
|
137
|
+
|
|
138
|
+
List<Tree<Long>> tree = TreeBuildUtils.build(list, (node, item) -> {
|
|
139
|
+
node.setId(item.getId());
|
|
140
|
+
node.setParentId(item.getParentId());
|
|
141
|
+
node.setName(item.getName());
|
|
142
|
+
node.setWeight(item.getOrderNum());
|
|
143
|
+
node.putExtra("icon", item.getIcon());
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// 指定根节点 ID
|
|
147
|
+
List<Tree<Long>> tree = TreeBuildUtils.build(list, 0L, (node, item) -> { ... });
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 5. 日期时间 - DateUtils
|
|
153
|
+
|
|
154
|
+
```java
|
|
155
|
+
import org.dromara.common.core.utils.DateUtils;
|
|
156
|
+
import org.dromara.common.core.enums.FormatsType;
|
|
157
|
+
|
|
158
|
+
// 获取当前时间
|
|
159
|
+
String date = DateUtils.getDate(); // "2026-01-24"
|
|
160
|
+
String time = DateUtils.getTime(); // "2026-01-24 15:30:00"
|
|
161
|
+
|
|
162
|
+
// 格式化 / 解析
|
|
163
|
+
DateUtils.formatDateTime(date);
|
|
164
|
+
DateUtils.parseDateToStr(FormatsType.YYYY_MM_DD_HH_MM, date);
|
|
165
|
+
Date date = DateUtils.parseDateTime(FormatsType.YYYY_MM_DD_HH_MM_SS, "2026-01-24 15:30:00");
|
|
166
|
+
|
|
167
|
+
// 时间差
|
|
168
|
+
long days = DateUtils.difference(start, end, TimeUnit.DAYS);
|
|
169
|
+
String diff = DateUtils.getDatePoor(endDate, startDate); // "3天 2小时 30分钟"
|
|
170
|
+
|
|
171
|
+
// 日期范围校验
|
|
172
|
+
DateUtils.validateDateRange(startDate, endDate, 30, TimeUnit.DAYS);
|
|
173
|
+
|
|
174
|
+
// 友好时间(仿微信)
|
|
175
|
+
DateUtils.formatFriendlyTime(date); // "刚刚" / "5分钟前" / "昨天 14:30"
|
|
176
|
+
|
|
177
|
+
// 类型转换
|
|
178
|
+
Date date = DateUtils.toDate(localDateTime);
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**FormatsType 枚举**: `YYYY_MM_DD` | `YYYY_MM_DD_HH_MM_SS` | `YYYY_MM_DD_HH_MM` | `YYYYMMDD` | `YYYYMMDDHHMMSS` | `HH_MM_SS`
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## 6. 参数校验 - ValidatorUtils
|
|
186
|
+
|
|
187
|
+
```java
|
|
188
|
+
import org.dromara.common.core.utils.ValidatorUtils;
|
|
189
|
+
import org.dromara.common.core.validate.AddGroup;
|
|
190
|
+
import org.dromara.common.core.validate.EditGroup;
|
|
191
|
+
|
|
192
|
+
// Service 层手动校验
|
|
193
|
+
ValidatorUtils.validate(bo, AddGroup.class);
|
|
194
|
+
|
|
195
|
+
// Controller 层(推荐)
|
|
196
|
+
@PostMapping
|
|
197
|
+
public R<Void> add(@Validated(AddGroup.class) @RequestBody XxxBo bo) { }
|
|
198
|
+
|
|
199
|
+
@PutMapping
|
|
200
|
+
public R<Void> edit(@Validated(EditGroup.class) @RequestBody XxxBo bo) { }
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**BO 类校验注解**:
|
|
204
|
+
|
|
205
|
+
```java
|
|
206
|
+
public class XxxBo extends BaseEntity {
|
|
207
|
+
@NotNull(message = "ID不能为空", groups = { EditGroup.class })
|
|
208
|
+
private Long id;
|
|
209
|
+
|
|
210
|
+
@NotBlank(message = "名称不能为空", groups = { AddGroup.class, EditGroup.class })
|
|
211
|
+
@Size(max = 100, message = "名称长度不能超过100个字符")
|
|
212
|
+
private String name;
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 7. Redis 缓存 - RedisUtils
|
|
219
|
+
|
|
220
|
+
> 详细 API 见 `references/redis-utils-api.md`
|
|
221
|
+
|
|
222
|
+
```java
|
|
223
|
+
import org.dromara.common.redis.utils.RedisUtils;
|
|
224
|
+
|
|
225
|
+
// 基本操作
|
|
226
|
+
RedisUtils.setCacheObject("key", value);
|
|
227
|
+
RedisUtils.setCacheObject("key", value, Duration.ofMinutes(30));
|
|
228
|
+
RedisUtils.getCacheObject("key");
|
|
229
|
+
RedisUtils.deleteObject("key");
|
|
230
|
+
RedisUtils.hasKey("key");
|
|
231
|
+
|
|
232
|
+
// 条件设置
|
|
233
|
+
RedisUtils.setObjectIfAbsent("key", value, Duration.ofMinutes(5));
|
|
234
|
+
|
|
235
|
+
// List / Set / Map 操作
|
|
236
|
+
RedisUtils.setCacheList("listKey", dataList);
|
|
237
|
+
RedisUtils.setCacheMap("mapKey", dataMap);
|
|
238
|
+
RedisUtils.setCacheMapValue("mapKey", "field", value);
|
|
239
|
+
|
|
240
|
+
// 原子操作
|
|
241
|
+
long val = RedisUtils.incrAtomicValue("counter");
|
|
242
|
+
|
|
243
|
+
// 发布订阅
|
|
244
|
+
RedisUtils.publish("channel", message);
|
|
245
|
+
RedisUtils.subscribe("channel", Message.class, msg -> { });
|
|
246
|
+
|
|
247
|
+
// 限流
|
|
248
|
+
long remaining = RedisUtils.rateLimiter("api:user:list", RateType.OVERALL, 100, 60);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 8. 登录用户 - LoginHelper
|
|
254
|
+
|
|
255
|
+
```java
|
|
256
|
+
import org.dromara.common.satoken.utils.LoginHelper;
|
|
257
|
+
|
|
258
|
+
Long userId = LoginHelper.getUserId();
|
|
259
|
+
String username = LoginHelper.getUsername();
|
|
260
|
+
Long deptId = LoginHelper.getDeptId();
|
|
261
|
+
String tenantId = LoginHelper.getTenantId();
|
|
262
|
+
LoginUser loginUser = LoginHelper.getLoginUser();
|
|
263
|
+
boolean isLogin = LoginHelper.isLogin();
|
|
264
|
+
boolean isSuperAdmin = LoginHelper.isSuperAdmin();
|
|
265
|
+
boolean isTenantAdmin = LoginHelper.isTenantAdmin();
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## 9. 业务异常 - ServiceException
|
|
271
|
+
|
|
272
|
+
```java
|
|
273
|
+
import org.dromara.common.core.exception.ServiceException;
|
|
274
|
+
|
|
275
|
+
throw new ServiceException("用户不存在");
|
|
276
|
+
throw new ServiceException("用户 {} 不存在", userId);
|
|
277
|
+
throw new ServiceException("用户不存在", 500); // 带错误码
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## 10. Excel 导出 - ExcelUtil
|
|
283
|
+
|
|
284
|
+
```java
|
|
285
|
+
import org.dromara.common.excel.utils.ExcelUtil;
|
|
286
|
+
|
|
287
|
+
// 导出
|
|
288
|
+
@PostMapping("/export")
|
|
289
|
+
public void export(XxxBo bo, HttpServletResponse response) {
|
|
290
|
+
List<XxxVo> list = xxxService.queryList(bo);
|
|
291
|
+
ExcelUtil.exportExcel(list, "数据导出", XxxVo.class, response);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 导入
|
|
295
|
+
@PostMapping("/import")
|
|
296
|
+
public R<Void> importData(@RequestPart("file") MultipartFile file) throws Exception {
|
|
297
|
+
List<XxxVo> list = ExcelUtil.importExcel(file.getInputStream(), XxxVo.class);
|
|
298
|
+
return R.ok();
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**VO 类 Excel 注解**:
|
|
303
|
+
|
|
304
|
+
```java
|
|
305
|
+
public class XxxVo implements Serializable {
|
|
306
|
+
@ExcelProperty(value = "ID")
|
|
307
|
+
private Long id;
|
|
308
|
+
|
|
309
|
+
@ExcelProperty(value = "状态", converter = ExcelDictConvert.class)
|
|
310
|
+
@ExcelDictFormat(dictType = "sys_normal_disable")
|
|
311
|
+
private String status;
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## 11. JSON 操作 - JsonUtils
|
|
318
|
+
|
|
319
|
+
```java
|
|
320
|
+
import org.dromara.common.json.utils.JsonUtils;
|
|
321
|
+
|
|
322
|
+
String json = JsonUtils.toJsonString(obj);
|
|
323
|
+
User user = JsonUtils.parseObject(json, User.class);
|
|
324
|
+
List<User> list = JsonUtils.parseArray(json, User.class);
|
|
325
|
+
Map<String, Object> map = JsonUtils.parseMap(json);
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## 工具类选择速查
|
|
331
|
+
|
|
332
|
+
| 需求 | 推荐工具 | 说明 |
|
|
333
|
+
|------|---------|------|
|
|
334
|
+
| BO/Entity/VO 转换 | `MapstructUtils.convert()` | **必须使用** |
|
|
335
|
+
| 字符串判空 | `StringUtils.isBlank()` | 项目工具类 |
|
|
336
|
+
| 集合判空 | `CollUtil.isEmpty()` | Hutool |
|
|
337
|
+
| 对象判空 | `ObjectUtil.isNull()` | Hutool |
|
|
338
|
+
| 提取集合属性 | `StreamUtils.toList()` | 项目工具类 |
|
|
339
|
+
| 集合转 Map | `StreamUtils.toIdentityMap()` | 项目工具类 |
|
|
340
|
+
| 构建树 | `TreeBuildUtils.build()` | 项目工具类 |
|
|
341
|
+
| 日期格式化 | `DateUtils.formatDateTime()` | 项目工具类 |
|
|
342
|
+
| 抛业务异常 | `throw new ServiceException()` | 项目异常类 |
|
|
343
|
+
| 获取登录用户 | `LoginHelper.getUserId()` | 项目工具类 |
|
|
344
|
+
| Redis 缓存 | `RedisUtils.setCacheObject()` | 项目工具类 |
|
|
345
|
+
| Excel 导出 | `ExcelUtil.exportExcel()` | 项目工具类 |
|
|
346
|
+
| 生成 ID | `IdUtil.getSnowflakeNextId()` | Hutool |
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## 禁止事项
|
|
351
|
+
|
|
352
|
+
```java
|
|
353
|
+
// ❌ 禁止使用 BeanUtils
|
|
354
|
+
BeanUtils.copyProperties(source, target);
|
|
355
|
+
|
|
356
|
+
// ❌ 禁止使用 Map 传递业务数据
|
|
357
|
+
public Map<String, Object> getXxx() { ... }
|
|
358
|
+
|
|
359
|
+
// ❌ 禁止手写 stream 转换(应使用 StreamUtils)
|
|
360
|
+
list.stream().map(User::getId).collect(Collectors.toList()); // 不推荐
|
|
361
|
+
StreamUtils.toList(list, User::getId); // ✅ 推荐
|
|
362
|
+
```
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# RedisUtils 完整 API
|
|
2
|
+
|
|
3
|
+
```java
|
|
4
|
+
import org.dromara.common.redis.utils.RedisUtils;
|
|
5
|
+
import java.time.Duration;
|
|
6
|
+
|
|
7
|
+
// ========== 基本操作 ==========
|
|
8
|
+
RedisUtils.setCacheObject("key", value); // 永久
|
|
9
|
+
RedisUtils.setCacheObject("key", value, Duration.ofMinutes(30)); // 30分钟过期
|
|
10
|
+
RedisUtils.getCacheObject("key");
|
|
11
|
+
RedisUtils.deleteObject("key");
|
|
12
|
+
RedisUtils.hasKey("key");
|
|
13
|
+
RedisUtils.expire("key", 3600); // 设置过期时间(秒)
|
|
14
|
+
|
|
15
|
+
// ========== 条件设置 ==========
|
|
16
|
+
RedisUtils.setObjectIfAbsent("key", value, Duration.ofMinutes(5)); // 不存在才设置
|
|
17
|
+
RedisUtils.setObjectIfExists("key", value, Duration.ofMinutes(5)); // 存在才设置
|
|
18
|
+
|
|
19
|
+
// ========== List 操作 ==========
|
|
20
|
+
RedisUtils.setCacheList("listKey", dataList);
|
|
21
|
+
RedisUtils.addCacheList("listKey", item);
|
|
22
|
+
List<T> list = RedisUtils.getCacheList("listKey");
|
|
23
|
+
List<T> range = RedisUtils.getCacheListRange("listKey", 0, 10);
|
|
24
|
+
|
|
25
|
+
// ========== Set 操作 ==========
|
|
26
|
+
RedisUtils.setCacheSet("setKey", dataSet);
|
|
27
|
+
RedisUtils.addCacheSet("setKey", item);
|
|
28
|
+
Set<T> set = RedisUtils.getCacheSet("setKey");
|
|
29
|
+
|
|
30
|
+
// ========== Map/Hash 操作 ==========
|
|
31
|
+
RedisUtils.setCacheMap("mapKey", dataMap);
|
|
32
|
+
RedisUtils.setCacheMapValue("mapKey", "field", value);
|
|
33
|
+
T value = RedisUtils.getCacheMapValue("mapKey", "field");
|
|
34
|
+
Map<String, T> map = RedisUtils.getCacheMap("mapKey");
|
|
35
|
+
RedisUtils.delCacheMapValue("mapKey", "field");
|
|
36
|
+
|
|
37
|
+
// ========== 原子操作 ==========
|
|
38
|
+
RedisUtils.setAtomicValue("counter", 0);
|
|
39
|
+
long val = RedisUtils.incrAtomicValue("counter"); // +1
|
|
40
|
+
long val = RedisUtils.decrAtomicValue("counter"); // -1
|
|
41
|
+
long val = RedisUtils.getAtomicValue("counter");
|
|
42
|
+
|
|
43
|
+
// ========== 批量操作 ==========
|
|
44
|
+
Collection<String> keys = RedisUtils.keys("user:*");
|
|
45
|
+
RedisUtils.deleteKeys("temp:*");
|
|
46
|
+
|
|
47
|
+
// ========== 发布订阅 ==========
|
|
48
|
+
RedisUtils.publish("channel", message);
|
|
49
|
+
RedisUtils.subscribe("channel", Message.class, msg -> {
|
|
50
|
+
// 处理消息
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ========== 限流 ==========
|
|
54
|
+
long remaining = RedisUtils.rateLimiter("api:user:list", RateType.OVERALL, 100, 60);
|
|
55
|
+
// 每60秒最多100次,返回剩余次数,-1表示被限流
|
|
56
|
+
```
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: websocket-sse
|
|
3
|
+
description: |
|
|
4
|
+
当需要实现实时通信、消息推送、在线状态管理时自动使用此 Skill。
|
|
5
|
+
|
|
6
|
+
触发场景:
|
|
7
|
+
- 需要实现服务端向客户端推送消息
|
|
8
|
+
- 需要实现双向实时通信(聊天、协作)
|
|
9
|
+
- 需要管理用户在线状态
|
|
10
|
+
- 需要实现系统通知、订单状态变更等实时推送
|
|
11
|
+
- 需要在多实例部署环境下同步消息
|
|
12
|
+
|
|
13
|
+
触发词:WebSocket、SSE、实时推送、消息通知、在线状态、双向通信、Server-Sent Events、实时通信、消息推送、SseEmitter、WebSocketUtils、SseMessageUtils
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# 实时通信开发指南(WebSocket & SSE)
|
|
17
|
+
|
|
18
|
+
> **适用模块**:`ruoyi-common-websocket`、`ruoyi-common-sse`
|
|
19
|
+
|
|
20
|
+
## 方案选型
|
|
21
|
+
|
|
22
|
+
| 方案 | 模块 | 方向 | 场景 |
|
|
23
|
+
|------|------|------|------|
|
|
24
|
+
| **WebSocket** | `ruoyi-common-websocket` | 双向 | 聊天、协作、低延迟交互 |
|
|
25
|
+
| **SSE** | `ruoyi-common-sse` | 服务端→客户端 | 通知推送、状态更新、AI流式响应 |
|
|
26
|
+
|
|
27
|
+
**共同特性**:Sa-Token 认证集成、Redis 发布订阅(多实例消息同步)、配置启用即可。
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 一、WebSocket
|
|
32
|
+
|
|
33
|
+
### 1.1 配置
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
websocket:
|
|
37
|
+
enabled: true
|
|
38
|
+
path: /resource/websocket
|
|
39
|
+
allowedOrigins: "*" # 生产环境应限制
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 1.2 WebSocketUtils API
|
|
43
|
+
|
|
44
|
+
**位置**:`org.dromara.common.websocket.utils.WebSocketUtils`
|
|
45
|
+
|
|
46
|
+
```java
|
|
47
|
+
import org.dromara.common.websocket.utils.WebSocketUtils;
|
|
48
|
+
import org.dromara.common.websocket.dto.WebSocketMessageDto;
|
|
49
|
+
|
|
50
|
+
// 单实例:向指定用户发送
|
|
51
|
+
WebSocketUtils.sendMessage(userId, "消息内容");
|
|
52
|
+
|
|
53
|
+
// 多实例:通过 Redis Pub/Sub 广播(推荐)
|
|
54
|
+
WebSocketMessageDto dto = new WebSocketMessageDto();
|
|
55
|
+
dto.setSessionKeys(List.of(userId1, userId2));
|
|
56
|
+
dto.setMessage("消息内容");
|
|
57
|
+
WebSocketUtils.publishMessage(dto);
|
|
58
|
+
|
|
59
|
+
// 群发所有在线用户
|
|
60
|
+
WebSocketUtils.publishAll("系统广播消息");
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 1.3 WebSocketMessageDto
|
|
64
|
+
|
|
65
|
+
```java
|
|
66
|
+
@Data
|
|
67
|
+
public class WebSocketMessageDto implements Serializable {
|
|
68
|
+
private List<Long> sessionKeys; // 目标用户ID(空则群发)
|
|
69
|
+
private String message;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 1.4 WebSocketSessionHolder(会话管理)
|
|
74
|
+
|
|
75
|
+
**位置**:`org.dromara.common.websocket.holder.WebSocketSessionHolder`
|
|
76
|
+
|
|
77
|
+
```java
|
|
78
|
+
boolean online = WebSocketSessionHolder.existSession(userId);
|
|
79
|
+
Set<Long> onlineUsers = WebSocketSessionHolder.getSessionsAll();
|
|
80
|
+
WebSocketSession session = WebSocketSessionHolder.getSessions(userId);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 1.5 前端连接
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
const ws = new WebSocket(
|
|
87
|
+
`ws://localhost:8080/resource/websocket?clientid=${clientId}&Authorization=${token}`
|
|
88
|
+
);
|
|
89
|
+
ws.onmessage = (event) => {
|
|
90
|
+
const data = JSON.parse(event.data);
|
|
91
|
+
// 根据 data.type 路由处理
|
|
92
|
+
};
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 二、SSE
|
|
98
|
+
|
|
99
|
+
### 2.1 配置
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
sse:
|
|
103
|
+
enabled: true
|
|
104
|
+
path: /resource/sse
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 2.2 SseMessageUtils API
|
|
108
|
+
|
|
109
|
+
**位置**:`org.dromara.common.sse.utils.SseMessageUtils`
|
|
110
|
+
|
|
111
|
+
```java
|
|
112
|
+
import org.dromara.common.sse.utils.SseMessageUtils;
|
|
113
|
+
import org.dromara.common.sse.dto.SseMessageDto;
|
|
114
|
+
|
|
115
|
+
// 单实例:向指定用户 / 所有用户
|
|
116
|
+
SseMessageUtils.sendMessage(userId, "消息");
|
|
117
|
+
SseMessageUtils.sendMessage("广播消息");
|
|
118
|
+
|
|
119
|
+
// 多实例:通过 Redis Pub/Sub
|
|
120
|
+
SseMessageDto dto = new SseMessageDto();
|
|
121
|
+
dto.setUserIds(List.of(userId1, userId2));
|
|
122
|
+
dto.setMessage("消息内容");
|
|
123
|
+
SseMessageUtils.publishMessage(dto);
|
|
124
|
+
|
|
125
|
+
// 群发(多实例)
|
|
126
|
+
SseMessageUtils.publishAll("系统通知");
|
|
127
|
+
|
|
128
|
+
// 检查是否启用
|
|
129
|
+
if (SseMessageUtils.isEnable()) { ... }
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 2.3 SseMessageDto
|
|
133
|
+
|
|
134
|
+
```java
|
|
135
|
+
@Data
|
|
136
|
+
public class SseMessageDto implements Serializable {
|
|
137
|
+
private List<Long> userIds; // 目标用户ID(空则群发)
|
|
138
|
+
private String message;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### 2.4 前端连接
|
|
143
|
+
|
|
144
|
+
```javascript
|
|
145
|
+
const eventSource = new EventSource(`/resource/sse?Authorization=${token}`);
|
|
146
|
+
eventSource.addEventListener('message', (event) => {
|
|
147
|
+
const data = JSON.parse(event.data);
|
|
148
|
+
});
|
|
149
|
+
// 页面关闭时主动关闭
|
|
150
|
+
window.onbeforeunload = () => {
|
|
151
|
+
eventSource.close();
|
|
152
|
+
navigator.sendBeacon('/resource/sse/close');
|
|
153
|
+
};
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 三、多实例消息同步
|
|
159
|
+
|
|
160
|
+
通过 Redis Pub/Sub 实现跨实例消息投递:
|
|
161
|
+
|
|
162
|
+
| 方案 | Redis Topic | 方法 |
|
|
163
|
+
|------|------------|------|
|
|
164
|
+
| WebSocket | `global:websocket` | `publishMessage()` / `publishAll()` |
|
|
165
|
+
| SSE | `global:sse` | `publishMessage()` / `publishAll()` |
|
|
166
|
+
|
|
167
|
+
**流程**:调用 publish 方法 -> 发布到 Redis Topic -> 所有实例的 Listener 接收 -> 匹配本地用户并投递。
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 四、业务集成示例
|
|
172
|
+
|
|
173
|
+
```java
|
|
174
|
+
@Service
|
|
175
|
+
@RequiredArgsConstructor
|
|
176
|
+
public class OrderNotifyService {
|
|
177
|
+
|
|
178
|
+
public void notifyOrderStatusChange(Order order) {
|
|
179
|
+
String message = JsonUtils.toJsonString(Map.of(
|
|
180
|
+
"type", "ORDER_STATUS_CHANGE",
|
|
181
|
+
"orderId", order.getId(),
|
|
182
|
+
"status", order.getStatus(),
|
|
183
|
+
"updateTime", DateUtils.getTime()
|
|
184
|
+
));
|
|
185
|
+
|
|
186
|
+
// SSE 通知买家
|
|
187
|
+
SseMessageDto buyerDto = new SseMessageDto();
|
|
188
|
+
buyerDto.setUserIds(List.of(order.getBuyerId()));
|
|
189
|
+
buyerDto.setMessage(message);
|
|
190
|
+
SseMessageUtils.publishMessage(buyerDto);
|
|
191
|
+
|
|
192
|
+
// WebSocket 通知卖家(需要双向通信时)
|
|
193
|
+
WebSocketMessageDto sellerDto = new WebSocketMessageDto();
|
|
194
|
+
sellerDto.setSessionKeys(List.of(order.getSellerId()));
|
|
195
|
+
sellerDto.setMessage(message);
|
|
196
|
+
WebSocketUtils.publishMessage(sellerDto);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## 五、常见错误
|
|
204
|
+
|
|
205
|
+
```java
|
|
206
|
+
// ❌ 多实例环境使用 sendMessage(只能发给当前实例用户)
|
|
207
|
+
WebSocketUtils.sendMessage(userId, "消息");
|
|
208
|
+
|
|
209
|
+
// ✅ 使用 publishMessage 支持多实例
|
|
210
|
+
WebSocketMessageDto dto = new WebSocketMessageDto();
|
|
211
|
+
dto.setSessionKeys(List.of(userId));
|
|
212
|
+
dto.setMessage("消息");
|
|
213
|
+
WebSocketUtils.publishMessage(dto);
|
|
214
|
+
|
|
215
|
+
// ❌ 循环逐个发送
|
|
216
|
+
for (Long uid : userIds) {
|
|
217
|
+
dto.setSessionKeys(List.of(uid));
|
|
218
|
+
WebSocketUtils.publishMessage(dto);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ✅ 批量发送
|
|
222
|
+
dto.setSessionKeys(userIds);
|
|
223
|
+
WebSocketUtils.publishMessage(dto);
|
|
224
|
+
|
|
225
|
+
// ❌ 发送纯字符串(前端难解析)
|
|
226
|
+
WebSocketUtils.publishAll("订单已更新");
|
|
227
|
+
|
|
228
|
+
// ✅ JSON 格式 + type 字段
|
|
229
|
+
WebSocketUtils.publishAll(JsonUtils.toJsonString(Map.of(
|
|
230
|
+
"type", "ORDER_UPDATE", "data", orderData
|
|
231
|
+
)));
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 六、API 速查
|
|
237
|
+
|
|
238
|
+
### WebSocket
|
|
239
|
+
|
|
240
|
+
| 方法 | 说明 |
|
|
241
|
+
|------|------|
|
|
242
|
+
| `WebSocketUtils.sendMessage(userId, msg)` | 发给指定用户(当前实例) |
|
|
243
|
+
| `WebSocketUtils.publishMessage(dto)` | 发给指定用户(多实例) |
|
|
244
|
+
| `WebSocketUtils.publishAll(msg)` | 群发(多实例) |
|
|
245
|
+
| `WebSocketSessionHolder.existSession(userId)` | 检查在线 |
|
|
246
|
+
| `WebSocketSessionHolder.getSessionsAll()` | 所有在线用户 |
|
|
247
|
+
|
|
248
|
+
### SSE
|
|
249
|
+
|
|
250
|
+
| 方法 | 说明 |
|
|
251
|
+
|------|------|
|
|
252
|
+
| `SseMessageUtils.sendMessage(userId, msg)` | 发给指定用户(当前实例) |
|
|
253
|
+
| `SseMessageUtils.sendMessage(msg)` | 发给所有用户(当前实例) |
|
|
254
|
+
| `SseMessageUtils.publishMessage(dto)` | 发给指定用户(多实例) |
|
|
255
|
+
| `SseMessageUtils.publishAll(msg)` | 群发(多实例) |
|
|
256
|
+
| `SseMessageUtils.isEnable()` | 检查是否启用 |
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## 七、参考代码位置
|
|
261
|
+
|
|
262
|
+
| 类型 | 位置 |
|
|
263
|
+
|------|------|
|
|
264
|
+
| WebSocket 工具类 | `ruoyi-common/ruoyi-common-websocket/.../utils/WebSocketUtils.java` |
|
|
265
|
+
| WebSocket 会话管理 | `ruoyi-common/ruoyi-common-websocket/.../holder/WebSocketSessionHolder.java` |
|
|
266
|
+
| WebSocket 消息DTO | `ruoyi-common/ruoyi-common-websocket/.../dto/WebSocketMessageDto.java` |
|
|
267
|
+
| WebSocket 配置 | `ruoyi-common/ruoyi-common-websocket/.../config/WebSocketConfig.java` |
|
|
268
|
+
| SSE 工具类 | `ruoyi-common/ruoyi-common-sse/.../utils/SseMessageUtils.java` |
|
|
269
|
+
| SSE 连接管理 | `ruoyi-common/ruoyi-common-sse/.../core/SseEmitterManager.java` |
|
|
270
|
+
| SSE 控制器 | `ruoyi-common/ruoyi-common-sse/.../controller/SseController.java` |
|
|
271
|
+
| SSE 消息DTO | `ruoyi-common/ruoyi-common-sse/.../dto/SseMessageDto.java` |
|