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.
- package/.claude/hooks/skill-forced-eval.js +46 -62
- package/.claude/settings.json +10 -1
- package/.claude/skills/api-development/SKILL.md +179 -130
- package/.claude/skills/architecture-design/SKILL.md +102 -212
- package/.claude/skills/backend-annotations/SKILL.md +166 -220
- package/.claude/skills/bug-detective/SKILL.md +225 -186
- package/.claude/skills/code-patterns/SKILL.md +127 -244
- package/.claude/skills/collaborating-with-codex/SKILL.md +96 -113
- package/.claude/skills/crud-development/SKILL.md +226 -307
- package/.claude/skills/data-permission/SKILL.md +131 -202
- package/.claude/skills/database-ops/SKILL.md +158 -355
- package/.claude/skills/error-handler/SKILL.md +224 -285
- package/.claude/skills/file-oss-management/SKILL.md +174 -169
- package/.claude/skills/git-workflow/SKILL.md +123 -341
- package/.claude/skills/json-serialization/SKILL.md +121 -137
- package/.claude/skills/performance-doctor/SKILL.md +83 -89
- package/.claude/skills/redis-cache/SKILL.md +134 -185
- package/.claude/skills/scheduled-jobs/SKILL.md +187 -224
- package/.claude/skills/security-guard/SKILL.md +168 -276
- package/.claude/skills/sms-mail/SKILL.md +266 -228
- package/.claude/skills/social-login/SKILL.md +257 -195
- package/.claude/skills/tenant-management/SKILL.md +172 -188
- package/.claude/skills/utils-toolkit/SKILL.md +214 -222
- package/.claude/skills/websocket-sse/SKILL.md +251 -172
- package/.claude/skills/workflow-engine/SKILL.md +178 -250
- package/.codex/skills/api-development/SKILL.md +179 -130
- package/.codex/skills/architecture-design/SKILL.md +102 -212
- package/.codex/skills/backend-annotations/SKILL.md +166 -220
- package/.codex/skills/bug-detective/SKILL.md +225 -186
- package/.codex/skills/code-patterns/SKILL.md +127 -244
- package/.codex/skills/collaborating-with-codex/SKILL.md +96 -113
- package/.codex/skills/crud-development/SKILL.md +226 -307
- package/.codex/skills/data-permission/SKILL.md +131 -202
- package/.codex/skills/database-ops/SKILL.md +158 -355
- package/.codex/skills/error-handler/SKILL.md +224 -285
- package/.codex/skills/file-oss-management/SKILL.md +174 -169
- package/.codex/skills/git-workflow/SKILL.md +123 -341
- package/.codex/skills/json-serialization/SKILL.md +121 -137
- package/.codex/skills/performance-doctor/SKILL.md +83 -89
- package/.codex/skills/redis-cache/SKILL.md +134 -185
- package/.codex/skills/scheduled-jobs/SKILL.md +187 -224
- package/.codex/skills/security-guard/SKILL.md +168 -276
- package/.codex/skills/sms-mail/SKILL.md +266 -228
- package/.codex/skills/social-login/SKILL.md +257 -195
- package/.codex/skills/tenant-management/SKILL.md +172 -188
- package/.codex/skills/utils-toolkit/SKILL.md +214 -222
- package/.codex/skills/websocket-sse/SKILL.md +251 -172
- package/.codex/skills/workflow-engine/SKILL.md +178 -250
- package/.cursor/hooks/cursor-skill-eval.js +66 -6
- package/.cursor/skills/api-development/SKILL.md +179 -130
- package/.cursor/skills/architecture-design/SKILL.md +102 -212
- package/.cursor/skills/backend-annotations/SKILL.md +166 -220
- package/.cursor/skills/bug-detective/SKILL.md +225 -186
- package/.cursor/skills/code-patterns/SKILL.md +127 -244
- package/.cursor/skills/collaborating-with-codex/SKILL.md +96 -113
- package/.cursor/skills/crud-development/SKILL.md +226 -307
- package/.cursor/skills/data-permission/SKILL.md +131 -202
- package/.cursor/skills/database-ops/SKILL.md +158 -355
- package/.cursor/skills/error-handler/SKILL.md +224 -285
- package/.cursor/skills/file-oss-management/SKILL.md +174 -169
- package/.cursor/skills/git-workflow/SKILL.md +123 -341
- package/.cursor/skills/json-serialization/SKILL.md +121 -137
- package/.cursor/skills/performance-doctor/SKILL.md +83 -89
- package/.cursor/skills/redis-cache/SKILL.md +134 -185
- package/.cursor/skills/scheduled-jobs/SKILL.md +187 -224
- package/.cursor/skills/security-guard/SKILL.md +168 -276
- package/.cursor/skills/sms-mail/SKILL.md +266 -228
- package/.cursor/skills/social-login/SKILL.md +257 -195
- package/.cursor/skills/tenant-management/SKILL.md +172 -188
- package/.cursor/skills/utils-toolkit/SKILL.md +214 -222
- package/.cursor/skills/websocket-sse/SKILL.md +251 -172
- package/.cursor/skills/workflow-engine/SKILL.md +178 -250
- package/AGENTS.md +49 -540
- package/CLAUDE.md +73 -119
- package/README.md +37 -6
- package/bin/index.js +5 -1
- package/package.json +1 -1
- package/src/skills/api-development/SKILL.md +179 -130
- package/src/skills/architecture-design/SKILL.md +102 -212
- package/src/skills/backend-annotations/SKILL.md +166 -220
- package/src/skills/bug-detective/SKILL.md +225 -186
- package/src/skills/code-patterns/SKILL.md +127 -244
- package/src/skills/collaborating-with-codex/SKILL.md +96 -113
- package/src/skills/crud-development/SKILL.md +226 -307
- package/src/skills/data-permission/SKILL.md +131 -202
- package/src/skills/database-ops/SKILL.md +158 -355
- package/src/skills/error-handler/SKILL.md +224 -285
- package/src/skills/file-oss-management/SKILL.md +174 -169
- package/src/skills/git-workflow/SKILL.md +123 -341
- package/src/skills/json-serialization/SKILL.md +121 -137
- package/src/skills/performance-doctor/SKILL.md +83 -89
- package/src/skills/redis-cache/SKILL.md +134 -185
- package/src/skills/scheduled-jobs/SKILL.md +187 -224
- package/src/skills/security-guard/SKILL.md +168 -276
- package/src/skills/sms-mail/SKILL.md +266 -228
- package/src/skills/social-login/SKILL.md +257 -195
- package/src/skills/tenant-management/SKILL.md +172 -188
- package/src/skills/utils-toolkit/SKILL.md +214 -222
- package/src/skills/websocket-sse/SKILL.md +251 -172
- package/src/skills/workflow-engine/SKILL.md +178 -250
- package/.claude/skills/skill-creator/LICENSE.txt +0 -202
- package/.claude/skills/skill-creator/SKILL.md +0 -479
- package/.claude/skills/skill-creator/agents/analyzer.md +0 -274
- package/.claude/skills/skill-creator/agents/comparator.md +0 -202
- package/.claude/skills/skill-creator/agents/grader.md +0 -223
- package/.claude/skills/skill-creator/assets/eval_review.html +0 -146
- package/.claude/skills/skill-creator/eval-viewer/generate_review.py +0 -471
- package/.claude/skills/skill-creator/eval-viewer/viewer.html +0 -1325
- package/.claude/skills/skill-creator/references/schemas.md +0 -430
- package/.claude/skills/skill-creator/scripts/__init__.py +0 -0
- package/.claude/skills/skill-creator/scripts/aggregate_benchmark.py +0 -401
- package/.claude/skills/skill-creator/scripts/generate_report.py +0 -326
- package/.claude/skills/skill-creator/scripts/improve_description.py +0 -248
- package/.claude/skills/skill-creator/scripts/package_skill.py +0 -136
- package/.claude/skills/skill-creator/scripts/quick_validate.py +0 -103
- package/.claude/skills/skill-creator/scripts/run_eval.py +0 -310
- package/.claude/skills/skill-creator/scripts/run_loop.py +0 -332
- 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
|
-
|
|
5
|
-
|
|
4
|
+
通用实时通信开发指南。涵盖 WebSocket 双向通信和 SSE 服务端推送的原生 Spring 实现。
|
|
6
5
|
触发场景:
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
- 实现服务端向客户端推送消息
|
|
7
|
+
- 实现双向实时通信(聊天、协作)
|
|
8
|
+
- 管理用户在线状态
|
|
9
|
+
- 实现系统通知、状态变更实时推送
|
|
10
|
+
- 多实例部署环境下消息同步
|
|
11
|
+
触发词:WebSocket、SSE、实时推送、消息通知、在线状态、双向通信、Server-Sent Events、SseEmitter、消息推送
|
|
12
|
+
注意:如果项目有专属技能,优先使用专属版本。
|
|
14
13
|
---
|
|
15
14
|
|
|
16
15
|
# 实时通信开发指南(WebSocket & SSE)
|
|
17
16
|
|
|
18
|
-
>
|
|
19
|
-
|
|
20
|
-
## 方案选型
|
|
17
|
+
> 通用模板。如果项目有专属技能,优先使用。
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|------|------|------|------|
|
|
24
|
-
| **WebSocket** | `ruoyi-common-websocket` | 双向 | 聊天、协作、低延迟交互 |
|
|
25
|
-
| **SSE** | `ruoyi-common-sse` | 服务端→客户端 | 通知推送、状态更新、AI流式响应 |
|
|
19
|
+
## 设计原则
|
|
26
20
|
|
|
27
|
-
|
|
21
|
+
1. **选择合适的协议**:单向推送用 SSE,双向通信用 WebSocket。
|
|
22
|
+
2. **认证不可少**:连接建立时必须验证身份(Token / Session)。
|
|
23
|
+
3. **多实例支持**:通过 Redis Pub/Sub 或消息队列同步跨实例消息。
|
|
24
|
+
4. **优雅降级**:客户端应处理断线重连、消息丢失等异常场景。
|
|
28
25
|
|
|
29
26
|
---
|
|
30
27
|
|
|
31
|
-
##
|
|
28
|
+
## 方案对比
|
|
32
29
|
|
|
33
|
-
|
|
30
|
+
| 维度 | WebSocket | SSE (Server-Sent Events) |
|
|
31
|
+
|------|-----------|--------------------------|
|
|
32
|
+
| 通信方向 | 双向(全双工) | 单向(服务端 -> 客户端) |
|
|
33
|
+
| 协议 | `ws://` / `wss://` | HTTP(长连接) |
|
|
34
|
+
| 浏览器支持 | 所有现代浏览器 | 所有现代浏览器(IE 除外) |
|
|
35
|
+
| 自动重连 | 需手动实现 | 浏览器原生支持 |
|
|
36
|
+
| 数据格式 | 二进制 / 文本 | 文本(通常 JSON) |
|
|
37
|
+
| 代理兼容 | 可能需特殊配置 | 天然 HTTP 兼容 |
|
|
38
|
+
| 适用场景 | 聊天、协作编辑、游戏 | 通知推送、状态更新、AI 流式响应 |
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
### 选型决策
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
需要客户端向服务端发送数据?
|
|
44
|
+
├── 是 → WebSocket
|
|
45
|
+
└── 否 → 需要二进制数据传输?
|
|
46
|
+
├── 是 → WebSocket
|
|
47
|
+
└── 否 → SSE(更简单、更稳定)
|
|
40
48
|
```
|
|
41
49
|
|
|
42
|
-
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 实现模式
|
|
43
53
|
|
|
44
|
-
|
|
54
|
+
### 一、WebSocket(Spring 原生)
|
|
55
|
+
|
|
56
|
+
#### 1. 配置
|
|
45
57
|
|
|
46
58
|
```java
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
@Configuration
|
|
60
|
+
@EnableWebSocket
|
|
61
|
+
public class WebSocketConfig implements WebSocketConfigurer {
|
|
49
62
|
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
@Autowired
|
|
64
|
+
private [你的WebSocket处理器] handler;
|
|
52
65
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
dto.setSessionKeys(List.of(userId1, userId2));
|
|
56
|
-
dto.setMessage("消息内容");
|
|
57
|
-
WebSocketUtils.publishMessage(dto);
|
|
66
|
+
@Autowired
|
|
67
|
+
private [你的认证拦截器] authInterceptor;
|
|
58
68
|
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
78
|
+
#### 2. 消息处理器
|
|
64
79
|
|
|
65
80
|
```java
|
|
66
|
-
@
|
|
67
|
-
public class
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
+
@Override
|
|
95
|
+
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
|
|
96
|
+
// 处理客户端发来的消息
|
|
97
|
+
String payload = message.getPayload();
|
|
98
|
+
log.info("收到消息: {}", payload);
|
|
99
|
+
}
|
|
76
100
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
141
|
+
#### 3. 前端连接
|
|
84
142
|
|
|
85
143
|
```javascript
|
|
86
|
-
const
|
|
87
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
### 2.1 配置
|
|
161
|
+
### 二、SSE(Spring 原生)
|
|
100
162
|
|
|
101
|
-
|
|
102
|
-
sse:
|
|
103
|
-
enabled: true
|
|
104
|
-
path: /resource/sse
|
|
105
|
-
```
|
|
163
|
+
#### 1. Controller
|
|
106
164
|
|
|
107
|
-
|
|
165
|
+
```java
|
|
166
|
+
@RestController
|
|
167
|
+
@RequestMapping("/sse")
|
|
168
|
+
public class SseController {
|
|
108
169
|
|
|
109
|
-
|
|
170
|
+
private final ConcurrentHashMap<Long, SseEmitter> emitters = new ConcurrentHashMap<>();
|
|
110
171
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
183
|
+
return emitter;
|
|
184
|
+
}
|
|
127
185
|
|
|
128
|
-
//
|
|
129
|
-
|
|
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
|
-
|
|
197
|
+
#### 2. 消息推送服务
|
|
133
198
|
|
|
134
199
|
```java
|
|
135
|
-
@
|
|
136
|
-
public class
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
236
|
+
#### 3. 前端连接
|
|
143
237
|
|
|
144
238
|
```javascript
|
|
145
|
-
const
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
274
|
+
// 发布消息到 Redis
|
|
275
|
+
public void publish(MessageDTO dto) {
|
|
276
|
+
redisTemplate.convertAndSend(CHANNEL, JsonUtils.toJson(dto));
|
|
277
|
+
}
|
|
161
278
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|
|
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
|
-
//
|
|
207
|
-
|
|
324
|
+
// 1. 多实例环境只发本地(消息丢失)
|
|
325
|
+
handler.sendMessage(userId, message); // 用户可能在其他实例
|
|
326
|
+
// 应使用 Redis Pub/Sub 广播
|
|
208
327
|
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
218
|
-
WebSocketUtils.publishMessage(dto);
|
|
335
|
+
sendMessage(uid, message);
|
|
219
336
|
}
|
|
337
|
+
// 应批量发送或使用广播
|
|
220
338
|
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
339
|
+
// 4. SSE 超时设置不当
|
|
340
|
+
new SseEmitter(30000L); // 30秒后断开
|
|
341
|
+
// 长连接应设置 0L(不超时)或较大值
|
|
224
342
|
|
|
225
|
-
//
|
|
226
|
-
|
|
343
|
+
// 5. 忘记清理已断开的连接
|
|
344
|
+
// Session/Emitter 断开后仍在 Map 中 -> 内存泄漏
|
|
345
|
+
// 应在 onCompletion/onClose/onError 回调中移除
|
|
227
346
|
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
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` |
|