站内消息支持三种类型,admin发送,mobile订阅
This commit is contained in:
@@ -8,7 +8,7 @@ import 'dayjs/locale/zh-cn';
|
|||||||
|
|
||||||
import { MessageAPI , UserAPI } from './api/index.ts';
|
import { MessageAPI , UserAPI } from './api/index.ts';
|
||||||
import type { UserMessage } from '../share/types.ts';
|
import type { UserMessage } from '../share/types.ts';
|
||||||
import { MessageStatusNameMap , MessageStatus} from '../share/types.ts';
|
import { MessageStatusNameMap , MessageStatus, MessageType } from '../share/types.ts';
|
||||||
import { useAuth } from "./hooks_sys.tsx";
|
import { useAuth } from "./hooks_sys.tsx";
|
||||||
|
|
||||||
export const MessagesPage = () => {
|
export const MessagesPage = () => {
|
||||||
@@ -222,9 +222,9 @@ export const MessagesPage = () => {
|
|||||||
style={{ width: 120 }}
|
style={{ width: 120 }}
|
||||||
allowClear
|
allowClear
|
||||||
options={[
|
options={[
|
||||||
{ value: 'SYSTEM', label: '系统消息' },
|
{ value: MessageType.SYSTEM, label: '系统消息' },
|
||||||
{ value: 'NOTICE', label: '公告' },
|
{ value: MessageType.ANNOUNCE, label: '公告' },
|
||||||
{ value: 'PERSONAL', label: '个人消息' },
|
{ value: MessageType.PRIVATE, label: '个人消息' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -233,8 +233,8 @@ export const MessagesPage = () => {
|
|||||||
style={{ width: 120 }}
|
style={{ width: 120 }}
|
||||||
allowClear
|
allowClear
|
||||||
options={[
|
options={[
|
||||||
{ value: 'UNREAD', label: '未读' },
|
{ value: MessageStatus.UNREAD, label: '未读' },
|
||||||
{ value: 'READ', label: '已读' },
|
{ value: MessageStatus.READ, label: '已读' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -290,9 +290,9 @@ export const MessagesPage = () => {
|
|||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
options={[
|
options={[
|
||||||
{ value: 'SYSTEM', label: '系统消息' },
|
{ value: MessageType.SYSTEM, label: '系统消息' },
|
||||||
{ value: 'NOTICE', label: '公告' },
|
{ value: MessageType.ANNOUNCE, label: '公告' },
|
||||||
{ value: 'PERSONAL', label: '个人消息' },
|
{ value: MessageType.PRIVATE, label: '个人消息' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -39,12 +39,17 @@ export const NotificationsPage = () => {
|
|||||||
|
|
||||||
// 订阅消息频道
|
// 订阅消息频道
|
||||||
newSocket.on('connect', () => {
|
newSocket.on('connect', () => {
|
||||||
|
// 订阅个人频道
|
||||||
newSocket.emit('message:subscribe', `user_${user.id}`);
|
newSocket.emit('message:subscribe', `user_${user.id}`);
|
||||||
|
// 订阅系统频道
|
||||||
|
newSocket.emit('message:subscribe', 'system');
|
||||||
|
// 订阅公告频道
|
||||||
|
newSocket.emit('message:subscribe', 'announce');
|
||||||
setIsSubscribed(true);
|
setIsSubscribed(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理实时消息
|
// 处理实时消息
|
||||||
newSocket.on('message:broadcasted', (newMessage) => {
|
const handleNewMessage = (newMessage: any) => {
|
||||||
queryClient.setQueryData(['messages'], (oldData: any) => {
|
queryClient.setQueryData(['messages'], (oldData: any) => {
|
||||||
if (!oldData) return oldData;
|
if (!oldData) return oldData;
|
||||||
return {
|
return {
|
||||||
@@ -61,7 +66,12 @@ export const NotificationsPage = () => {
|
|||||||
count: oldData.count + 1
|
count: oldData.count + 1
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// 处理广播消息
|
||||||
|
newSocket.on('message:broadcasted', handleNewMessage);
|
||||||
|
// 处理频道推送消息
|
||||||
|
newSocket.on('message:received', handleNewMessage);
|
||||||
|
|
||||||
// 错误处理
|
// 错误处理
|
||||||
newSocket.on('error', (error) => {
|
newSocket.on('error', (error) => {
|
||||||
@@ -71,6 +81,8 @@ export const NotificationsPage = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
if (newSocket) {
|
if (newSocket) {
|
||||||
newSocket.emit('message:unsubscribe', `user_${user.id}`);
|
newSocket.emit('message:unsubscribe', `user_${user.id}`);
|
||||||
|
newSocket.emit('message:unsubscribe', 'system');
|
||||||
|
newSocket.emit('message:unsubscribe', 'announce');
|
||||||
newSocket.disconnect();
|
newSocket.disconnect();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
99
docs/message-system-architecture.md
Normal file
99
docs/message-system-architecture.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# 消息系统架构设计方案
|
||||||
|
|
||||||
|
## 1. 架构图
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph Admin端
|
||||||
|
A[发送消息] -->|类型转换| B(server/routes_io_messages.ts)
|
||||||
|
end
|
||||||
|
subgraph Server
|
||||||
|
B --> C{消息类型}
|
||||||
|
C -->|ANNOUNCE| D[存入DB+推announce]
|
||||||
|
C -->|PRIVATE| E[存入DB+推user_[id]]
|
||||||
|
C -->|SYSTEM| F[存入DB+推system]
|
||||||
|
D & E & F --> G[Socket推送]
|
||||||
|
end
|
||||||
|
subgraph Mobile端
|
||||||
|
G --> H[多频道订阅]
|
||||||
|
H --> I[按类型处理UI]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 关键数据结构
|
||||||
|
|
||||||
|
### 消息类型枚举 (client/share/types.ts)
|
||||||
|
```typescript
|
||||||
|
export enum MessageType {
|
||||||
|
SYSTEM = 'system', // 系统消息
|
||||||
|
ANNOUNCE = 'announce', // 公告
|
||||||
|
PRIVATE = 'private' // 私信
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MessageStatus {
|
||||||
|
UNREAD = 0, // 未读
|
||||||
|
READ = 1, // 已读
|
||||||
|
DELETED = 2 // 已删除
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 消息表结构
|
||||||
|
```sql
|
||||||
|
CREATE TABLE messages (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
type ENUM('SYSTEM','ANNOUNCE','PRIVATE') NOT NULL,
|
||||||
|
sender_id INTEGER REFERENCES users(id),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_messages (
|
||||||
|
user_id INTEGER REFERENCES users(id),
|
||||||
|
message_id INTEGER REFERENCES messages(id),
|
||||||
|
status ENUM('UNREAD','READ') DEFAULT 'UNREAD',
|
||||||
|
PRIMARY KEY (user_id, message_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 事件流说明
|
||||||
|
|
||||||
|
### Socket.IO 事件规范
|
||||||
|
| 事件名称 | 方向 | 描述 |
|
||||||
|
|---------|------|------|
|
||||||
|
| message:subscribe | 客户端→服务端 | 订阅消息频道 |
|
||||||
|
| message:unsubscribe | 客户端→服务端 | 取消订阅 |
|
||||||
|
| message:send | 客户端→服务端 | 发送消息 |
|
||||||
|
| message:received | 服务端→客户端 | 消息接收确认 |
|
||||||
|
| message:broadcasted | 服务端→客户端 | 广播新消息 |
|
||||||
|
|
||||||
|
### 频道订阅规范
|
||||||
|
| 消息类型 | 目标频道 | 订阅方式 |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| SYSTEM | system | socket.join('system') |
|
||||||
|
| ANNOUNCE | announce | socket.join('announce') |
|
||||||
|
| PRIVATE | user_[id]| socket.join(`user_${userId}`) |
|
||||||
|
|
||||||
|
### 实时推送流程
|
||||||
|
1. Admin发送消息 → 服务端接收(message:send)
|
||||||
|
2. 服务端处理:
|
||||||
|
- 存储消息到数据库
|
||||||
|
- 根据类型推送:
|
||||||
|
* SYSTEM: io.to('system').emit('message:broadcasted')
|
||||||
|
* ANNOUNCE: io.to('announce').emit('message:broadcasted')
|
||||||
|
* PRIVATE: io.to(`user_${targetId}`).emit('message:broadcasted')
|
||||||
|
3. Mobile端:
|
||||||
|
- 初始化时订阅相关频道
|
||||||
|
- 按频道接收处理消息
|
||||||
|
|
||||||
|
## 4. 接口定义
|
||||||
|
|
||||||
|
### HTTP API
|
||||||
|
- GET /api/messages - 获取消息列表
|
||||||
|
- POST /api/messages - 发送消息
|
||||||
|
- GET /api/messages/unread - 获取未读消息数
|
||||||
|
- PUT /api/messages/:id/read - 标记消息为已读
|
||||||
|
|
||||||
|
### 权限控制
|
||||||
|
- 系统消息: 仅管理员可发送
|
||||||
|
- 公告: 管理员和特定角色可发送
|
||||||
|
- 私信: 所有用户可发送
|
||||||
@@ -132,6 +132,27 @@ export function setupMessageEvents({ socket , apiClient }:Variables) {
|
|||||||
|
|
||||||
await apiClient.database.table('user_messages').insert(userMessages);
|
await apiClient.database.table('user_messages').insert(userMessages);
|
||||||
|
|
||||||
|
// 根据消息类型推送到不同频道
|
||||||
|
const messageData = {
|
||||||
|
id: messageId,
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
type,
|
||||||
|
sender_id: user.id,
|
||||||
|
sender_name: user.username,
|
||||||
|
created_at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type === MessageType.SYSTEM) {
|
||||||
|
socket.to('system').emit('message:received', messageData);
|
||||||
|
} else if (type === MessageType.ANNOUNCE) {
|
||||||
|
socket.to('announce').emit('message:received', messageData);
|
||||||
|
} else if (type === MessageType.PRIVATE) {
|
||||||
|
receiver_ids.forEach(userId => {
|
||||||
|
socket.to(`user_${userId}`).emit('message:received', messageData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
socket.emit('message:sent', {
|
socket.emit('message:sent', {
|
||||||
message: '消息发送成功',
|
message: '消息发送成功',
|
||||||
data: { id: messageId }
|
data: { id: messageId }
|
||||||
|
|||||||
Reference in New Issue
Block a user