Преглед на файлове

站内消息支持三种类型,admin发送,mobile订阅

yourname преди 6 месеца
родител
ревизия
f3042583df
променени са 4 файла, в които са добавени 143 реда и са изтрити 11 реда
  1. 9 9
      client/admin/pages_messages.tsx
  2. 14 2
      client/mobile/pages_messages.tsx
  3. 99 0
      docs/message-system-architecture.md
  4. 21 0
      server/routes_io_messages.ts

+ 9 - 9
client/admin/pages_messages.tsx

@@ -8,7 +8,7 @@ import 'dayjs/locale/zh-cn';
 
 import { MessageAPI , UserAPI } from './api/index.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";
 
 export const MessagesPage = () => {
@@ -222,9 +222,9 @@ export const MessagesPage = () => {
               style={{ width: 120 }}
               allowClear
               options={[
-                { value: 'SYSTEM', label: '系统消息' },
-                { value: 'NOTICE', label: '公告' },
-                { value: 'PERSONAL', label: '个人消息' },
+                { value: MessageType.SYSTEM, label: '系统消息' },
+                { value: MessageType.ANNOUNCE, label: '公告' },
+                { value: MessageType.PRIVATE, label: '个人消息' },
               ]}
             />
           </Form.Item>
@@ -233,8 +233,8 @@ export const MessagesPage = () => {
               style={{ width: 120 }}
               allowClear
               options={[
-                { value: 'UNREAD', label: '未读' },
-                { value: 'READ', label: '已读' },
+                { value: MessageStatus.UNREAD, label: '未读' },
+                { value: MessageStatus.READ, label: '已读' },
               ]}
             />
           </Form.Item>
@@ -290,9 +290,9 @@ export const MessagesPage = () => {
           >
             <Select
               options={[
-                { value: 'SYSTEM', label: '系统消息' },
-                { value: 'NOTICE', label: '公告' },
-                { value: 'PERSONAL', label: '个人消息' },
+                { value: MessageType.SYSTEM, label: '系统消息' },
+                { value: MessageType.ANNOUNCE, label: '公告' },
+                { value: MessageType.PRIVATE, label: '个人消息' },
               ]}
             />
           </Form.Item>

+ 14 - 2
client/mobile/pages_messages.tsx

@@ -39,12 +39,17 @@ export const NotificationsPage = () => {
 
     // 订阅消息频道
     newSocket.on('connect', () => {
+      // 订阅个人频道
       newSocket.emit('message:subscribe', `user_${user.id}`);
+      // 订阅系统频道
+      newSocket.emit('message:subscribe', 'system');
+      // 订阅公告频道
+      newSocket.emit('message:subscribe', 'announce');
       setIsSubscribed(true);
     });
 
     // 处理实时消息
-    newSocket.on('message:broadcasted', (newMessage) => {
+    const handleNewMessage = (newMessage: any) => {
       queryClient.setQueryData(['messages'], (oldData: any) => {
         if (!oldData) return oldData;
         return {
@@ -61,7 +66,12 @@ export const NotificationsPage = () => {
           count: oldData.count + 1
         };
       });
-    });
+    };
+
+    // 处理广播消息
+    newSocket.on('message:broadcasted', handleNewMessage);
+    // 处理频道推送消息
+    newSocket.on('message:received', handleNewMessage);
 
     // 错误处理
     newSocket.on('error', (error) => {
@@ -71,6 +81,8 @@ export const NotificationsPage = () => {
     return () => {
       if (newSocket) {
         newSocket.emit('message:unsubscribe', `user_${user.id}`);
+        newSocket.emit('message:unsubscribe', 'system');
+        newSocket.emit('message:unsubscribe', 'announce');
         newSocket.disconnect();
       }
     };

+ 99 - 0
docs/message-system-architecture.md

@@ -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 - 标记消息为已读
+
+### 权限控制
+- 系统消息: 仅管理员可发送
+- 公告: 管理员和特定角色可发送
+- 私信: 所有用户可发送

+ 21 - 0
server/routes_io_messages.ts

@@ -132,6 +132,27 @@ export function setupMessageEvents({ socket , apiClient }:Variables) {
 
       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', {
         message: '消息发送成功',
         data: { id: messageId }