ai-engineering-init 1.7.0 → 1.8.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 (118) hide show
  1. package/.claude/hooks/skill-forced-eval.js +46 -62
  2. package/.claude/settings.json +10 -1
  3. package/.claude/skills/api-development/SKILL.md +179 -130
  4. package/.claude/skills/architecture-design/SKILL.md +102 -212
  5. package/.claude/skills/backend-annotations/SKILL.md +166 -220
  6. package/.claude/skills/bug-detective/SKILL.md +225 -186
  7. package/.claude/skills/code-patterns/SKILL.md +127 -244
  8. package/.claude/skills/collaborating-with-codex/SKILL.md +96 -113
  9. package/.claude/skills/crud-development/SKILL.md +226 -307
  10. package/.claude/skills/data-permission/SKILL.md +131 -202
  11. package/.claude/skills/database-ops/SKILL.md +158 -355
  12. package/.claude/skills/error-handler/SKILL.md +224 -285
  13. package/.claude/skills/file-oss-management/SKILL.md +174 -169
  14. package/.claude/skills/git-workflow/SKILL.md +123 -341
  15. package/.claude/skills/json-serialization/SKILL.md +121 -137
  16. package/.claude/skills/performance-doctor/SKILL.md +83 -89
  17. package/.claude/skills/redis-cache/SKILL.md +134 -185
  18. package/.claude/skills/scheduled-jobs/SKILL.md +187 -224
  19. package/.claude/skills/security-guard/SKILL.md +168 -276
  20. package/.claude/skills/sms-mail/SKILL.md +266 -228
  21. package/.claude/skills/social-login/SKILL.md +257 -195
  22. package/.claude/skills/tenant-management/SKILL.md +172 -188
  23. package/.claude/skills/utils-toolkit/SKILL.md +214 -222
  24. package/.claude/skills/websocket-sse/SKILL.md +251 -172
  25. package/.claude/skills/workflow-engine/SKILL.md +178 -250
  26. package/.codex/skills/api-development/SKILL.md +179 -130
  27. package/.codex/skills/architecture-design/SKILL.md +102 -212
  28. package/.codex/skills/backend-annotations/SKILL.md +166 -220
  29. package/.codex/skills/bug-detective/SKILL.md +225 -186
  30. package/.codex/skills/code-patterns/SKILL.md +127 -244
  31. package/.codex/skills/collaborating-with-codex/SKILL.md +96 -113
  32. package/.codex/skills/crud-development/SKILL.md +226 -307
  33. package/.codex/skills/data-permission/SKILL.md +131 -202
  34. package/.codex/skills/database-ops/SKILL.md +158 -355
  35. package/.codex/skills/error-handler/SKILL.md +224 -285
  36. package/.codex/skills/file-oss-management/SKILL.md +174 -169
  37. package/.codex/skills/git-workflow/SKILL.md +123 -341
  38. package/.codex/skills/json-serialization/SKILL.md +121 -137
  39. package/.codex/skills/performance-doctor/SKILL.md +83 -89
  40. package/.codex/skills/redis-cache/SKILL.md +134 -185
  41. package/.codex/skills/scheduled-jobs/SKILL.md +187 -224
  42. package/.codex/skills/security-guard/SKILL.md +168 -276
  43. package/.codex/skills/sms-mail/SKILL.md +266 -228
  44. package/.codex/skills/social-login/SKILL.md +257 -195
  45. package/.codex/skills/tenant-management/SKILL.md +172 -188
  46. package/.codex/skills/utils-toolkit/SKILL.md +214 -222
  47. package/.codex/skills/websocket-sse/SKILL.md +251 -172
  48. package/.codex/skills/workflow-engine/SKILL.md +178 -250
  49. package/.cursor/hooks/cursor-skill-eval.js +66 -6
  50. package/.cursor/skills/api-development/SKILL.md +179 -130
  51. package/.cursor/skills/architecture-design/SKILL.md +102 -212
  52. package/.cursor/skills/backend-annotations/SKILL.md +166 -220
  53. package/.cursor/skills/bug-detective/SKILL.md +225 -186
  54. package/.cursor/skills/code-patterns/SKILL.md +127 -244
  55. package/.cursor/skills/collaborating-with-codex/SKILL.md +96 -113
  56. package/.cursor/skills/crud-development/SKILL.md +226 -307
  57. package/.cursor/skills/data-permission/SKILL.md +131 -202
  58. package/.cursor/skills/database-ops/SKILL.md +158 -355
  59. package/.cursor/skills/error-handler/SKILL.md +224 -285
  60. package/.cursor/skills/file-oss-management/SKILL.md +174 -169
  61. package/.cursor/skills/git-workflow/SKILL.md +123 -341
  62. package/.cursor/skills/json-serialization/SKILL.md +121 -137
  63. package/.cursor/skills/performance-doctor/SKILL.md +83 -89
  64. package/.cursor/skills/redis-cache/SKILL.md +134 -185
  65. package/.cursor/skills/scheduled-jobs/SKILL.md +187 -224
  66. package/.cursor/skills/security-guard/SKILL.md +168 -276
  67. package/.cursor/skills/sms-mail/SKILL.md +266 -228
  68. package/.cursor/skills/social-login/SKILL.md +257 -195
  69. package/.cursor/skills/tenant-management/SKILL.md +172 -188
  70. package/.cursor/skills/utils-toolkit/SKILL.md +214 -222
  71. package/.cursor/skills/websocket-sse/SKILL.md +251 -172
  72. package/.cursor/skills/workflow-engine/SKILL.md +178 -250
  73. package/AGENTS.md +49 -540
  74. package/CLAUDE.md +73 -119
  75. package/README.md +37 -6
  76. package/bin/index.js +5 -1
  77. package/package.json +1 -1
  78. package/src/skills/api-development/SKILL.md +179 -130
  79. package/src/skills/architecture-design/SKILL.md +102 -212
  80. package/src/skills/backend-annotations/SKILL.md +166 -220
  81. package/src/skills/bug-detective/SKILL.md +225 -186
  82. package/src/skills/code-patterns/SKILL.md +127 -244
  83. package/src/skills/collaborating-with-codex/SKILL.md +96 -113
  84. package/src/skills/crud-development/SKILL.md +226 -307
  85. package/src/skills/data-permission/SKILL.md +131 -202
  86. package/src/skills/database-ops/SKILL.md +158 -355
  87. package/src/skills/error-handler/SKILL.md +224 -285
  88. package/src/skills/file-oss-management/SKILL.md +174 -169
  89. package/src/skills/git-workflow/SKILL.md +123 -341
  90. package/src/skills/json-serialization/SKILL.md +121 -137
  91. package/src/skills/performance-doctor/SKILL.md +83 -89
  92. package/src/skills/redis-cache/SKILL.md +134 -185
  93. package/src/skills/scheduled-jobs/SKILL.md +187 -224
  94. package/src/skills/security-guard/SKILL.md +168 -276
  95. package/src/skills/sms-mail/SKILL.md +266 -228
  96. package/src/skills/social-login/SKILL.md +257 -195
  97. package/src/skills/tenant-management/SKILL.md +172 -188
  98. package/src/skills/utils-toolkit/SKILL.md +214 -222
  99. package/src/skills/websocket-sse/SKILL.md +251 -172
  100. package/src/skills/workflow-engine/SKILL.md +178 -250
  101. package/.claude/skills/skill-creator/LICENSE.txt +0 -202
  102. package/.claude/skills/skill-creator/SKILL.md +0 -479
  103. package/.claude/skills/skill-creator/agents/analyzer.md +0 -274
  104. package/.claude/skills/skill-creator/agents/comparator.md +0 -202
  105. package/.claude/skills/skill-creator/agents/grader.md +0 -223
  106. package/.claude/skills/skill-creator/assets/eval_review.html +0 -146
  107. package/.claude/skills/skill-creator/eval-viewer/generate_review.py +0 -471
  108. package/.claude/skills/skill-creator/eval-viewer/viewer.html +0 -1325
  109. package/.claude/skills/skill-creator/references/schemas.md +0 -430
  110. package/.claude/skills/skill-creator/scripts/__init__.py +0 -0
  111. package/.claude/skills/skill-creator/scripts/aggregate_benchmark.py +0 -401
  112. package/.claude/skills/skill-creator/scripts/generate_report.py +0 -326
  113. package/.claude/skills/skill-creator/scripts/improve_description.py +0 -248
  114. package/.claude/skills/skill-creator/scripts/package_skill.py +0 -136
  115. package/.claude/skills/skill-creator/scripts/quick_validate.py +0 -103
  116. package/.claude/skills/skill-creator/scripts/run_eval.py +0 -310
  117. package/.claude/skills/skill-creator/scripts/run_loop.py +0 -332
  118. 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` |