ai-engineering-init 1.7.0 → 1.10.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.
Files changed (139) hide show
  1. package/.claude/agents/bug-analyzer.md +103 -0
  2. package/.claude/agents/code-reviewer.md +115 -5
  3. package/.claude/agents/image-reader.md +154 -0
  4. package/.claude/agents/loki-runner.md +80 -0
  5. package/.claude/agents/mysql-runner.md +81 -0
  6. package/.claude/agents/requirements-analyzer.md +162 -0
  7. package/.claude/agents/task-fetcher.md +75 -0
  8. package/.claude/commands/dev.md +29 -0
  9. package/.claude/commands/next.md +31 -1
  10. package/.claude/commands/progress.md +23 -1
  11. package/.claude/hooks/skill-forced-eval.js +46 -62
  12. package/.claude/settings.json +10 -1
  13. package/.claude/skills/api-development/SKILL.md +179 -130
  14. package/.claude/skills/architecture-design/SKILL.md +102 -212
  15. package/.claude/skills/backend-annotations/SKILL.md +166 -220
  16. package/.claude/skills/bug-detective/SKILL.md +225 -186
  17. package/.claude/skills/code-patterns/SKILL.md +127 -244
  18. package/.claude/skills/collaborating-with-codex/SKILL.md +96 -113
  19. package/.claude/skills/crud-development/SKILL.md +226 -307
  20. package/.claude/skills/data-permission/SKILL.md +131 -202
  21. package/.claude/skills/database-ops/SKILL.md +158 -355
  22. package/.claude/skills/error-handler/SKILL.md +224 -285
  23. package/.claude/skills/file-oss-management/SKILL.md +174 -169
  24. package/.claude/skills/git-workflow/SKILL.md +123 -341
  25. package/.claude/skills/json-serialization/SKILL.md +121 -137
  26. package/.claude/skills/performance-doctor/SKILL.md +83 -89
  27. package/.claude/skills/redis-cache/SKILL.md +134 -185
  28. package/.claude/skills/scheduled-jobs/SKILL.md +187 -224
  29. package/.claude/skills/security-guard/SKILL.md +168 -276
  30. package/.claude/skills/sms-mail/SKILL.md +266 -228
  31. package/.claude/skills/social-login/SKILL.md +257 -195
  32. package/.claude/skills/tenant-management/SKILL.md +172 -188
  33. package/.claude/skills/utils-toolkit/SKILL.md +214 -222
  34. package/.claude/skills/websocket-sse/SKILL.md +251 -172
  35. package/.claude/skills/workflow-engine/SKILL.md +178 -250
  36. package/.codex/skills/api-development/SKILL.md +179 -130
  37. package/.codex/skills/architecture-design/SKILL.md +102 -212
  38. package/.codex/skills/backend-annotations/SKILL.md +166 -220
  39. package/.codex/skills/bug-detective/SKILL.md +225 -186
  40. package/.codex/skills/code-patterns/SKILL.md +127 -244
  41. package/.codex/skills/collaborating-with-codex/SKILL.md +96 -113
  42. package/.codex/skills/crud-development/SKILL.md +226 -307
  43. package/.codex/skills/data-permission/SKILL.md +131 -202
  44. package/.codex/skills/database-ops/SKILL.md +158 -355
  45. package/.codex/skills/dev/SKILL.md +476 -131
  46. package/.codex/skills/error-handler/SKILL.md +224 -285
  47. package/.codex/skills/file-oss-management/SKILL.md +174 -169
  48. package/.codex/skills/git-workflow/SKILL.md +123 -341
  49. package/.codex/skills/json-serialization/SKILL.md +121 -137
  50. package/.codex/skills/next/SKILL.md +186 -42
  51. package/.codex/skills/performance-doctor/SKILL.md +83 -89
  52. package/.codex/skills/progress/SKILL.md +147 -76
  53. package/.codex/skills/redis-cache/SKILL.md +134 -185
  54. package/.codex/skills/scheduled-jobs/SKILL.md +187 -224
  55. package/.codex/skills/security-guard/SKILL.md +168 -276
  56. package/.codex/skills/sms-mail/SKILL.md +266 -228
  57. package/.codex/skills/social-login/SKILL.md +257 -195
  58. package/.codex/skills/tenant-management/SKILL.md +172 -188
  59. package/.codex/skills/utils-toolkit/SKILL.md +214 -222
  60. package/.codex/skills/websocket-sse/SKILL.md +251 -172
  61. package/.codex/skills/workflow-engine/SKILL.md +178 -250
  62. package/.cursor/agents/bug-analyzer.md +102 -0
  63. package/.cursor/agents/code-reviewer.md +80 -97
  64. package/.cursor/agents/image-reader.md +154 -0
  65. package/.cursor/agents/loki-runner.md +80 -0
  66. package/.cursor/agents/mysql-runner.md +81 -0
  67. package/.cursor/agents/project-manager.md +1 -1
  68. package/.cursor/agents/requirements-analyzer.md +141 -0
  69. package/.cursor/agents/task-fetcher.md +75 -0
  70. package/.cursor/hooks/cursor-skill-eval.js +66 -6
  71. package/.cursor/skills/api-development/SKILL.md +179 -130
  72. package/.cursor/skills/architecture-design/SKILL.md +102 -212
  73. package/.cursor/skills/backend-annotations/SKILL.md +166 -220
  74. package/.cursor/skills/bug-detective/SKILL.md +225 -186
  75. package/.cursor/skills/code-patterns/SKILL.md +127 -244
  76. package/.cursor/skills/collaborating-with-codex/SKILL.md +96 -113
  77. package/.cursor/skills/crud-development/SKILL.md +226 -307
  78. package/.cursor/skills/data-permission/SKILL.md +131 -202
  79. package/.cursor/skills/database-ops/SKILL.md +158 -355
  80. package/.cursor/skills/error-handler/SKILL.md +224 -285
  81. package/.cursor/skills/file-oss-management/SKILL.md +174 -169
  82. package/.cursor/skills/git-workflow/SKILL.md +123 -341
  83. package/.cursor/skills/json-serialization/SKILL.md +121 -137
  84. package/.cursor/skills/performance-doctor/SKILL.md +83 -89
  85. package/.cursor/skills/redis-cache/SKILL.md +134 -185
  86. package/.cursor/skills/scheduled-jobs/SKILL.md +187 -224
  87. package/.cursor/skills/security-guard/SKILL.md +168 -276
  88. package/.cursor/skills/sms-mail/SKILL.md +266 -228
  89. package/.cursor/skills/social-login/SKILL.md +257 -195
  90. package/.cursor/skills/tenant-management/SKILL.md +172 -188
  91. package/.cursor/skills/utils-toolkit/SKILL.md +214 -222
  92. package/.cursor/skills/websocket-sse/SKILL.md +251 -172
  93. package/.cursor/skills/workflow-engine/SKILL.md +178 -250
  94. package/AGENTS.md +117 -540
  95. package/CLAUDE.md +105 -117
  96. package/README.md +37 -6
  97. package/bin/index.js +5 -1
  98. package/package.json +1 -1
  99. package/src/skills/api-development/SKILL.md +179 -130
  100. package/src/skills/architecture-design/SKILL.md +102 -212
  101. package/src/skills/backend-annotations/SKILL.md +166 -220
  102. package/src/skills/bug-detective/SKILL.md +225 -186
  103. package/src/skills/code-patterns/SKILL.md +127 -244
  104. package/src/skills/collaborating-with-codex/SKILL.md +96 -113
  105. package/src/skills/crud-development/SKILL.md +226 -307
  106. package/src/skills/data-permission/SKILL.md +131 -202
  107. package/src/skills/database-ops/SKILL.md +158 -355
  108. package/src/skills/error-handler/SKILL.md +224 -285
  109. package/src/skills/file-oss-management/SKILL.md +174 -169
  110. package/src/skills/git-workflow/SKILL.md +123 -341
  111. package/src/skills/json-serialization/SKILL.md +121 -137
  112. package/src/skills/performance-doctor/SKILL.md +83 -89
  113. package/src/skills/redis-cache/SKILL.md +134 -185
  114. package/src/skills/scheduled-jobs/SKILL.md +187 -224
  115. package/src/skills/security-guard/SKILL.md +168 -276
  116. package/src/skills/sms-mail/SKILL.md +266 -228
  117. package/src/skills/social-login/SKILL.md +257 -195
  118. package/src/skills/tenant-management/SKILL.md +172 -188
  119. package/src/skills/utils-toolkit/SKILL.md +214 -222
  120. package/src/skills/websocket-sse/SKILL.md +251 -172
  121. package/src/skills/workflow-engine/SKILL.md +178 -250
  122. package/.claude/skills/skill-creator/LICENSE.txt +0 -202
  123. package/.claude/skills/skill-creator/SKILL.md +0 -479
  124. package/.claude/skills/skill-creator/agents/analyzer.md +0 -274
  125. package/.claude/skills/skill-creator/agents/comparator.md +0 -202
  126. package/.claude/skills/skill-creator/agents/grader.md +0 -223
  127. package/.claude/skills/skill-creator/assets/eval_review.html +0 -146
  128. package/.claude/skills/skill-creator/eval-viewer/generate_review.py +0 -471
  129. package/.claude/skills/skill-creator/eval-viewer/viewer.html +0 -1325
  130. package/.claude/skills/skill-creator/references/schemas.md +0 -430
  131. package/.claude/skills/skill-creator/scripts/__init__.py +0 -0
  132. package/.claude/skills/skill-creator/scripts/aggregate_benchmark.py +0 -401
  133. package/.claude/skills/skill-creator/scripts/generate_report.py +0 -326
  134. package/.claude/skills/skill-creator/scripts/improve_description.py +0 -248
  135. package/.claude/skills/skill-creator/scripts/package_skill.py +0 -136
  136. package/.claude/skills/skill-creator/scripts/quick_validate.py +0 -103
  137. package/.claude/skills/skill-creator/scripts/run_eval.py +0 -310
  138. package/.claude/skills/skill-creator/scripts/run_loop.py +0 -332
  139. package/.claude/skills/skill-creator/scripts/utils.py +0 -47
@@ -1,271 +1,350 @@
1
1
  ---
2
2
  name: websocket-sse
3
3
  description: |
4
- 当需要实现实时通信、消息推送、在线状态管理时自动使用此 Skill。
5
-
4
+ 通用实时通信开发指南。涵盖 WebSocket 双向通信和 SSE 服务端推送的原生 Spring 实现。
6
5
  触发场景:
7
- - 需要实现服务端向客户端推送消息
8
- - 需要实现双向实时通信(聊天、协作)
9
- - 需要管理用户在线状态
10
- - 需要实现系统通知、订单状态变更等实时推送
11
- - 需要在多实例部署环境下同步消息
12
-
13
- 触发词:WebSocket、SSE、实时推送、消息通知、在线状态、双向通信、Server-Sent Events、实时通信、消息推送、SseEmitter、WebSocketUtils、SseMessageUtils
6
+ - 实现服务端向客户端推送消息
7
+ - 实现双向实时通信(聊天、协作)
8
+ - 管理用户在线状态
9
+ - 实现系统通知、状态变更实时推送
10
+ - 多实例部署环境下消息同步
11
+ 触发词:WebSocket、SSE、实时推送、消息通知、在线状态、双向通信、Server-Sent Events、SseEmitter、消息推送
12
+ 注意:如果项目有专属技能,优先使用专属版本。
14
13
  ---
15
14
 
16
15
  # 实时通信开发指南(WebSocket & SSE)
17
16
 
18
- > **适用模块**:`ruoyi-common-websocket`、`ruoyi-common-sse`
19
-
20
- ## 方案选型
17
+ > 通用模板。如果项目有专属技能,优先使用。
21
18
 
22
- | 方案 | 模块 | 方向 | 场景 |
23
- |------|------|------|------|
24
- | **WebSocket** | `ruoyi-common-websocket` | 双向 | 聊天、协作、低延迟交互 |
25
- | **SSE** | `ruoyi-common-sse` | 服务端→客户端 | 通知推送、状态更新、AI流式响应 |
19
+ ## 设计原则
26
20
 
27
- **共同特性**:Sa-Token 认证集成、Redis 发布订阅(多实例消息同步)、配置启用即可。
21
+ 1. **选择合适的协议**:单向推送用 SSE,双向通信用 WebSocket。
22
+ 2. **认证不可少**:连接建立时必须验证身份(Token / Session)。
23
+ 3. **多实例支持**:通过 Redis Pub/Sub 或消息队列同步跨实例消息。
24
+ 4. **优雅降级**:客户端应处理断线重连、消息丢失等异常场景。
28
25
 
29
26
  ---
30
27
 
31
- ## 一、WebSocket
28
+ ## 方案对比
32
29
 
33
- ### 1.1 配置
30
+ | 维度 | WebSocket | SSE (Server-Sent Events) |
31
+ |------|-----------|--------------------------|
32
+ | 通信方向 | 双向(全双工) | 单向(服务端 -> 客户端) |
33
+ | 协议 | `ws://` / `wss://` | HTTP(长连接) |
34
+ | 浏览器支持 | 所有现代浏览器 | 所有现代浏览器(IE 除外) |
35
+ | 自动重连 | 需手动实现 | 浏览器原生支持 |
36
+ | 数据格式 | 二进制 / 文本 | 文本(通常 JSON) |
37
+ | 代理兼容 | 可能需特殊配置 | 天然 HTTP 兼容 |
38
+ | 适用场景 | 聊天、协作编辑、游戏 | 通知推送、状态更新、AI 流式响应 |
34
39
 
35
- ```yaml
36
- websocket:
37
- enabled: true
38
- path: /resource/websocket
39
- allowedOrigins: "*" # 生产环境应限制
40
+ ### 选型决策
41
+
42
+ ```
43
+ 需要客户端向服务端发送数据?
44
+ ├── → WebSocket
45
+ └── 否 → 需要二进制数据传输?
46
+ ├── 是 → WebSocket
47
+ └── 否 → SSE(更简单、更稳定)
40
48
  ```
41
49
 
42
- ### 1.2 WebSocketUtils API
50
+ ---
51
+
52
+ ## 实现模式
43
53
 
44
- **位置**:`org.dromara.common.websocket.utils.WebSocketUtils`
54
+ ### 一、WebSocket(Spring 原生)
55
+
56
+ #### 1. 配置
45
57
 
46
58
  ```java
47
- import org.dromara.common.websocket.utils.WebSocketUtils;
48
- import org.dromara.common.websocket.dto.WebSocketMessageDto;
59
+ @Configuration
60
+ @EnableWebSocket
61
+ public class WebSocketConfig implements WebSocketConfigurer {
49
62
 
50
- // 单实例:向指定用户发送
51
- WebSocketUtils.sendMessage(userId, "消息内容");
63
+ @Autowired
64
+ private [你的WebSocket处理器] handler;
52
65
 
53
- // 多实例:通过 Redis Pub/Sub 广播(推荐)
54
- WebSocketMessageDto dto = new WebSocketMessageDto();
55
- dto.setSessionKeys(List.of(userId1, userId2));
56
- dto.setMessage("消息内容");
57
- WebSocketUtils.publishMessage(dto);
66
+ @Autowired
67
+ private [你的认证拦截器] authInterceptor;
58
68
 
59
- // 群发所有在线用户
60
- WebSocketUtils.publishAll("系统广播消息");
69
+ @Override
70
+ public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
71
+ registry.addHandler(handler, "/ws")
72
+ .addInterceptors(authInterceptor)
73
+ .setAllowedOrigins("https://your-domain.com"); // 生产不用 *
74
+ }
75
+ }
61
76
  ```
62
77
 
63
- ### 1.3 WebSocketMessageDto
78
+ #### 2. 消息处理器
64
79
 
65
80
  ```java
66
- @Data
67
- public class WebSocketMessageDto implements Serializable {
68
- private List<Long> sessionKeys; // 目标用户ID(空则群发)
69
- private String message;
70
- }
71
- ```
81
+ @Component
82
+ public class AppWebSocketHandler extends TextWebSocketHandler {
83
+
84
+ // 会话管理:userId -> Session
85
+ private final ConcurrentHashMap<Long, WebSocketSession> sessions = new ConcurrentHashMap<>();
72
86
 
73
- ### 1.4 WebSocketSessionHolder(会话管理)
87
+ @Override
88
+ public void afterConnectionEstablished(WebSocketSession session) {
89
+ Long userId = getUserId(session);
90
+ sessions.put(userId, session);
91
+ log.info("WebSocket 连接建立, userId: {}", userId);
92
+ }
74
93
 
75
- **位置**:`org.dromara.common.websocket.holder.WebSocketSessionHolder`
94
+ @Override
95
+ protected void handleTextMessage(WebSocketSession session, TextMessage message) {
96
+ // 处理客户端发来的消息
97
+ String payload = message.getPayload();
98
+ log.info("收到消息: {}", payload);
99
+ }
76
100
 
77
- ```java
78
- boolean online = WebSocketSessionHolder.existSession(userId);
79
- Set<Long> onlineUsers = WebSocketSessionHolder.getSessionsAll();
80
- WebSocketSession session = WebSocketSessionHolder.getSessions(userId);
101
+ @Override
102
+ public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
103
+ Long userId = getUserId(session);
104
+ sessions.remove(userId);
105
+ log.info("WebSocket 连接关闭, userId: {}", userId);
106
+ }
107
+
108
+ // 发送消息给指定用户
109
+ public void sendMessage(Long userId, String message) {
110
+ WebSocketSession session = sessions.get(userId);
111
+ if (session != null && session.isOpen()) {
112
+ try {
113
+ session.sendMessage(new TextMessage(message));
114
+ } catch (IOException e) {
115
+ log.error("发送消息失败, userId: {}", userId, e);
116
+ }
117
+ }
118
+ }
119
+
120
+ // 广播给所有在线用户
121
+ public void broadcast(String message) {
122
+ sessions.values().forEach(session -> {
123
+ try {
124
+ if (session.isOpen()) {
125
+ session.sendMessage(new TextMessage(message));
126
+ }
127
+ } catch (IOException e) {
128
+ log.error("广播消息失败", e);
129
+ }
130
+ });
131
+ }
132
+
133
+ // 检查用户是否在线
134
+ public boolean isOnline(Long userId) {
135
+ WebSocketSession session = sessions.get(userId);
136
+ return session != null && session.isOpen();
137
+ }
138
+ }
81
139
  ```
82
140
 
83
- ### 1.5 前端连接
141
+ #### 3. 前端连接
84
142
 
85
143
  ```javascript
86
- const ws = new WebSocket(
87
- `ws://localhost:8080/resource/websocket?clientid=${clientId}&Authorization=${token}`
88
- );
144
+ const token = localStorage.getItem('token');
145
+ const ws = new WebSocket(`wss://your-domain.com/ws?token=${token}`);
146
+
147
+ ws.onopen = () => console.log('连接已建立');
89
148
  ws.onmessage = (event) => {
90
149
  const data = JSON.parse(event.data);
91
150
  // 根据 data.type 路由处理
92
151
  };
152
+ ws.onclose = () => {
153
+ // 断线重连
154
+ setTimeout(() => reconnect(), 3000);
155
+ };
156
+ ws.onerror = (error) => console.error('WebSocket 错误', error);
93
157
  ```
94
158
 
95
159
  ---
96
160
 
97
- ## 二、SSE
98
-
99
- ### 2.1 配置
161
+ ### 二、SSE(Spring 原生)
100
162
 
101
- ```yaml
102
- sse:
103
- enabled: true
104
- path: /resource/sse
105
- ```
163
+ #### 1. Controller
106
164
 
107
- ### 2.2 SseMessageUtils API
165
+ ```java
166
+ @RestController
167
+ @RequestMapping("/sse")
168
+ public class SseController {
108
169
 
109
- **位置**:`org.dromara.common.sse.utils.SseMessageUtils`
170
+ private final ConcurrentHashMap<Long, SseEmitter> emitters = new ConcurrentHashMap<>();
110
171
 
111
- ```java
112
- import org.dromara.common.sse.utils.SseMessageUtils;
113
- import org.dromara.common.sse.dto.SseMessageDto;
172
+ // 建立 SSE 连接
173
+ @GetMapping("/connect")
174
+ public SseEmitter connect(@RequestParam Long userId) {
175
+ SseEmitter emitter = new SseEmitter(0L); // 0 = 不超时
114
176
 
115
- // 单实例:向指定用户 / 所有用户
116
- SseMessageUtils.sendMessage(userId, "消息");
117
- SseMessageUtils.sendMessage("广播消息");
177
+ emitters.put(userId, emitter);
118
178
 
119
- // 多实例:通过 Redis Pub/Sub
120
- SseMessageDto dto = new SseMessageDto();
121
- dto.setUserIds(List.of(userId1, userId2));
122
- dto.setMessage("消息内容");
123
- SseMessageUtils.publishMessage(dto);
179
+ emitter.onCompletion(() -> emitters.remove(userId));
180
+ emitter.onTimeout(() -> emitters.remove(userId));
181
+ emitter.onError(e -> emitters.remove(userId));
124
182
 
125
- // 群发(多实例)
126
- SseMessageUtils.publishAll("系统通知");
183
+ return emitter;
184
+ }
127
185
 
128
- // 检查是否启用
129
- if (SseMessageUtils.isEnable()) { ... }
186
+ // 关闭连接
187
+ @GetMapping("/close")
188
+ public void close(@RequestParam Long userId) {
189
+ SseEmitter emitter = emitters.remove(userId);
190
+ if (emitter != null) {
191
+ emitter.complete();
192
+ }
193
+ }
194
+ }
130
195
  ```
131
196
 
132
- ### 2.3 SseMessageDto
197
+ #### 2. 消息推送服务
133
198
 
134
199
  ```java
135
- @Data
136
- public class SseMessageDto implements Serializable {
137
- private List<Long> userIds; // 目标用户ID(空则群发)
138
- private String message;
200
+ @Service
201
+ public class SseMessageService {
202
+
203
+ @Autowired
204
+ private SseController sseController;
205
+
206
+ // 推送给指定用户
207
+ public void sendMessage(Long userId, String message) {
208
+ SseEmitter emitter = sseController.getEmitter(userId);
209
+ if (emitter != null) {
210
+ try {
211
+ emitter.send(SseEmitter.event()
212
+ .name("message")
213
+ .data(message));
214
+ } catch (IOException e) {
215
+ sseController.removeEmitter(userId);
216
+ log.error("SSE 推送失败, userId: {}", userId, e);
217
+ }
218
+ }
219
+ }
220
+
221
+ // 推送给所有用户
222
+ public void broadcast(String message) {
223
+ sseController.getAllEmitters().forEach((userId, emitter) -> {
224
+ try {
225
+ emitter.send(SseEmitter.event()
226
+ .name("message")
227
+ .data(message));
228
+ } catch (IOException e) {
229
+ sseController.removeEmitter(userId);
230
+ }
231
+ });
232
+ }
139
233
  }
140
234
  ```
141
235
 
142
- ### 2.4 前端连接
236
+ #### 3. 前端连接
143
237
 
144
238
  ```javascript
145
- const eventSource = new EventSource(`/resource/sse?Authorization=${token}`);
239
+ const token = localStorage.getItem('token');
240
+ const eventSource = new EventSource(`/sse/connect?userId=${userId}&token=${token}`);
241
+
146
242
  eventSource.addEventListener('message', (event) => {
147
243
  const data = JSON.parse(event.data);
244
+ // 处理消息
148
245
  });
149
- // 页面关闭时主动关闭
150
- window.onbeforeunload = () => {
151
- eventSource.close();
152
- navigator.sendBeacon('/resource/sse/close');
246
+
247
+ eventSource.onerror = () => {
248
+ // SSE 原生支持自动重连,通常无需手动处理
249
+ console.warn('SSE 连接异常');
153
250
  };
251
+
252
+ // 页面卸载时关闭
253
+ window.addEventListener('beforeunload', () => {
254
+ eventSource.close();
255
+ navigator.sendBeacon('/sse/close?userId=' + userId);
256
+ });
154
257
  ```
155
258
 
156
259
  ---
157
260
 
158
- ## 三、多实例消息同步
261
+ ### 三、多实例消息同步
262
+
263
+ 单实例时直接操作内存中的 Session/Emitter 即可。多实例部署时需通过 Redis Pub/Sub 同步:
264
+
265
+ ```java
266
+ @Service
267
+ public class MessageBroadcaster {
268
+
269
+ @Autowired
270
+ private StringRedisTemplate redisTemplate;
271
+
272
+ private static final String CHANNEL = "realtime:messages";
159
273
 
160
- 通过 Redis Pub/Sub 实现跨实例消息投递:
274
+ // 发布消息到 Redis
275
+ public void publish(MessageDTO dto) {
276
+ redisTemplate.convertAndSend(CHANNEL, JsonUtils.toJson(dto));
277
+ }
161
278
 
162
- | 方案 | Redis Topic | 方法 |
163
- |------|------------|------|
164
- | WebSocket | `global:websocket` | `publishMessage()` / `publishAll()` |
165
- | SSE | `global:sse` | `publishMessage()` / `publishAll()` |
279
+ // 订阅 Redis 消息,投递到本地连接
280
+ @Bean
281
+ public RedisMessageListenerContainer listenerContainer(RedisConnectionFactory factory) {
282
+ var container = new RedisMessageListenerContainer();
283
+ container.setConnectionFactory(factory);
284
+ container.addMessageListener((message, pattern) -> {
285
+ MessageDTO dto = JsonUtils.parse(message.toString(), MessageDTO.class);
286
+ // 匹配本地在线用户并投递
287
+ localDelivery(dto);
288
+ }, new ChannelTopic(CHANNEL));
289
+ return container;
290
+ }
291
+ }
292
+ ```
166
293
 
167
- **流程**:调用 publish 方法 -> 发布到 Redis Topic -> 所有实例的 Listener 接收 -> 匹配本地用户并投递。
294
+ **流程**:业务代码调用 publish -> 发布到 Redis Channel -> 所有实例的 Listener 接收 -> 匹配本地在线用户并投递。
168
295
 
169
296
  ---
170
297
 
171
- ## 四、业务集成示例
298
+ ## 业务集成示例
172
299
 
173
300
  ```java
174
301
  @Service
175
- @RequiredArgsConstructor
176
302
  public class OrderNotifyService {
177
303
 
304
+ @Autowired
305
+ private SseMessageService sseService;
306
+
178
307
  public void notifyOrderStatusChange(Order order) {
179
- String message = JsonUtils.toJsonString(Map.of(
308
+ String message = JsonUtils.toJson(Map.of(
180
309
  "type", "ORDER_STATUS_CHANGE",
181
310
  "orderId", order.getId(),
182
311
  "status", order.getStatus(),
183
- "updateTime", DateUtils.getTime()
312
+ "updateTime", LocalDateTime.now()
184
313
  ));
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);
314
+ sseService.sendMessage(order.getBuyerId(), message);
197
315
  }
198
316
  }
199
317
  ```
200
318
 
201
319
  ---
202
320
 
203
- ## 五、常见错误
321
+ ## 常见错误
204
322
 
205
323
  ```java
206
- // 多实例环境使用 sendMessage(只能发给当前实例用户)
207
- WebSocketUtils.sendMessage(userId, "消息");
324
+ // 1. 多实例环境只发本地(消息丢失)
325
+ handler.sendMessage(userId, message); // 用户可能在其他实例
326
+ // 应使用 Redis Pub/Sub 广播
208
327
 
209
- // 使用 publishMessage 支持多实例
210
- WebSocketMessageDto dto = new WebSocketMessageDto();
211
- dto.setSessionKeys(List.of(userId));
212
- dto.setMessage("消息");
213
- WebSocketUtils.publishMessage(dto);
328
+ // 2. 发送纯字符串(前端难解析、不可扩展)
329
+ handler.broadcast("订单已更新");
330
+ // 应使用 JSON + type 字段
331
+ handler.broadcast(JsonUtils.toJson(Map.of("type", "ORDER_UPDATE", "data", orderData)));
214
332
 
215
- // 循环逐个发送
333
+ // 3. 循环逐个发送(性能差)
216
334
  for (Long uid : userIds) {
217
- dto.setSessionKeys(List.of(uid));
218
- WebSocketUtils.publishMessage(dto);
335
+ sendMessage(uid, message);
219
336
  }
337
+ // 应批量发送或使用广播
220
338
 
221
- // 批量发送
222
- dto.setSessionKeys(userIds);
223
- WebSocketUtils.publishMessage(dto);
339
+ // 4. SSE 超时设置不当
340
+ new SseEmitter(30000L); // 30秒后断开
341
+ // 长连接应设置 0L(不超时)或较大值
224
342
 
225
- // 发送纯字符串(前端难解析)
226
- WebSocketUtils.publishAll("订单已更新");
343
+ // 5. 忘记清理已断开的连接
344
+ // Session/Emitter 断开后仍在 Map 中 -> 内存泄漏
345
+ // 应在 onCompletion/onClose/onError 回调中移除
227
346
 
228
- // JSON 格式 + type 字段
229
- WebSocketUtils.publishAll(JsonUtils.toJsonString(Map.of(
230
- "type", "ORDER_UPDATE", "data", orderData
231
- )));
347
+ // 6. WebSocket 未配置认证
348
+ // 连接建立时不校验 Token -> 任何人可连接
349
+ // 应在 HandshakeInterceptor 中校验身份
232
350
  ```
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` |