2
0
Эх сурвалжийг харах

新增消息功能,包括消息类型和状态的定义,创建消息和用户消息关联的数据库迁移,添加消息相关的API路由,提升系统的消息处理能力和用户体验。

zyh 8 сар өмнө
parent
commit
d676fccad9

+ 49 - 0
client/share/types.ts

@@ -448,3 +448,52 @@ export interface LoginLocation {
   /** 登录时间 */
   login_time?: string;
 }
+
+// 消息类型枚举
+export enum MessageType {
+  SYSTEM = 'system',   // 系统通知
+  PRIVATE = 'private', // 私信
+  ANNOUNCE = 'announce' // 公告
+}
+
+// 消息状态枚举
+export enum MessageStatus {
+  UNREAD = 0,   // 未读
+  READ = 1,     // 已读
+  DELETED = 2   // 已删除
+}
+
+// 消息状态中文映射
+export const MessageStatusNameMap: Record<MessageStatus, string> = {
+  [MessageStatus.UNREAD]: '未读',
+  [MessageStatus.READ]: '已读',
+  [MessageStatus.DELETED]: '已删除'
+};
+
+// 消息实体接口
+export interface Message {
+  id: number;
+  title: string;
+  content: string;
+  type: MessageType;
+  sender_id?: number;  // 发送者ID(系统消息可为空)
+  sender_name?: string; // 发送者名称
+  created_at: string;
+  updated_at: string;
+}
+
+// 用户消息关联接口
+export interface UserMessage {
+  id: number;
+  user_id: number;
+  message_id: number;
+  status: MessageStatus;
+  is_deleted?: DeleteStatus;
+  read_at?: string;
+  created_at: string;
+  updated_at: string;
+  
+  // 关联信息
+  message?: Message;
+  sender?: User;
+}

+ 2 - 0
server/app.tsx

@@ -34,6 +34,7 @@ import { migrations } from './migrations.ts';
 // 导入基础路由
 import { createAuthRoutes } from "./routes_auth.ts";
 import { createUserRoutes } from "./routes_users.ts";
+import { createMessagesRoutes } from "./routes_messages.ts";
 
 dayjs.extend(utc)
 // 初始化debug实例
@@ -328,6 +329,7 @@ export default function({ apiClient, app, moduleDir }: ModuleParams) {
   api.route('/charts', createChartRoutes(withAuth)) // 添加图表数据路由
   api.route('/map', createMapRoutes(withAuth)) // 添加地图数据路由
   api.route('/settings', createSystemSettingsRoutes(withAuth)) // 添加系统设置路由
+  api.route('/messages', createMessagesRoutes(withAuth)) // 添加消息路由
 
   // 注册API路由
   honoApp.route('/api', api)

+ 50 - 1
server/migrations.ts

@@ -390,6 +390,53 @@ const seedInitialData: MigrationLiveDefinition = {
   }
 };
 
+// 创建消息表迁移
+const createMessagesTable: MigrationLiveDefinition = {
+  name: "create_messages_table",
+  up: async (api) => {
+    await api.schema.createTable('messages', (table) => {
+      table.increments('id').primary().comment('消息ID');
+      table.string('title').notNullable().comment('消息标题');
+      table.text('content').notNullable().comment('消息内容');
+      table.enum('type', ['system', 'private', 'announce']).notNullable().comment('消息类型');
+      table.integer('sender_id').unsigned().references('id').inTable('users').onDelete('SET NULL').comment('发送者ID');
+      table.string('sender_name').comment('发送者名称');
+      table.timestamps(true, true);
+      
+      // 添加索引
+      table.index('type');
+      table.index('sender_id');
+    });
+  },
+  down: async (api) => {
+    await api.schema.dropTable('messages');
+  }
+};
+
+// 创建用户消息关联表迁移
+const createUserMessagesTable: MigrationLiveDefinition = {
+  name: "create_user_messages_table",
+  up: async (api) => {
+    await api.schema.createTable('user_messages', (table) => {
+      table.increments('id').primary().comment('关联ID');
+      table.integer('user_id').unsigned().references('id').inTable('users').onDelete('CASCADE').comment('用户ID');
+      table.integer('message_id').unsigned().references('id').inTable('messages').onDelete('CASCADE').comment('消息ID');
+      table.integer('status').defaultTo(0).comment('阅读状态(0=未读,1=已读)');
+      table.integer('is_deleted').defaultTo(0).comment('删除状态(0=未删除,1=已删除)');
+      table.timestamp('read_at').nullable().comment('阅读时间');
+      table.timestamps(true, true);
+      
+      // 添加复合索引
+      table.index(['user_id', 'status']);
+      table.index(['user_id', 'is_deleted']);
+      table.unique(['user_id', 'message_id']);
+    });
+  },
+  down: async (api) => {
+    await api.schema.dropTable('user_messages');
+  }
+};
+
 // 导出所有迁移
 export const migrations = [
   createUsersTable,
@@ -399,5 +446,7 @@ export const migrations = [
   createFileLibraryTable,
   createThemeSettingsTable,
   createSystemSettingsTable,
-  seedInitialData
+  createMessagesTable,
+  createUserMessagesTable,
+  seedInitialData,
 ];

+ 178 - 0
server/routes_messages.ts

@@ -0,0 +1,178 @@
+import { Hono } from 'hono'
+import type { Variables } from './app.tsx'
+import type { WithAuth } from './app.tsx'
+import { MessageType, MessageStatus } from '../client/share/types.ts'
+
+export function createMessagesRoutes(withAuth: WithAuth) {
+  const messagesRoutes = new Hono<{ Variables: Variables }>()
+
+  // 发送消息
+  messagesRoutes.post('/', withAuth, async (c) => {
+    try {
+      const auth = c.get('auth')
+      const apiClient = c.get('apiClient')
+      const { title, content, type, receiver_ids } = await c.req.json()
+
+      if (!title || !content || !type || !receiver_ids?.length) {
+        return c.json({ error: '缺少必要参数' }, 400)
+      }
+
+      // 创建消息
+      const user = c.get('user')
+      if (!user) return c.json({ error: '未授权访问' }, 401)
+      
+      const [messageId] = await apiClient.database.table('messages').insert({
+        title,
+        content,
+        type,
+        sender_id: user.id,
+        sender_name: user.username,
+        created_at: apiClient.database.fn.now(),
+        updated_at: apiClient.database.fn.now()
+      })
+
+      // 关联用户消息
+      const userMessages = receiver_ids.map((userId: number) => ({
+        user_id: userId,
+        message_id: messageId,
+        status: MessageStatus.UNREAD,
+        created_at: apiClient.database.fn.now(),
+        updated_at: apiClient.database.fn.now()
+      }))
+
+      await apiClient.database.table('user_messages').insert(userMessages)
+
+      return c.json({ message: '消息发送成功', id: messageId }, 201)
+    } catch (error) {
+      console.error('发送消息失败:', error)
+      return c.json({ error: '发送消息失败' }, 500)
+    }
+  })
+
+  // 获取用户消息列表
+  messagesRoutes.get('/', withAuth, async (c) => {
+    try {
+      const apiClient = c.get('apiClient')
+      
+      const page = Number(c.req.query('page')) || 1
+      const pageSize = Number(c.req.query('pageSize')) || 20
+      const type = c.req.query('type')
+      const status = c.req.query('status')
+
+      const user = c.get('user')
+      if (!user) return c.json({ error: '未授权访问' }, 401)
+      
+      const query = apiClient.database.table('user_messages')
+        .select('m.*', 'um.status as user_status', 'um.read_at', 'um.id as user_message_id')
+        .from('user_messages as um')
+        .leftJoin('messages as m', 'um.message_id', 'm.id')
+        .where('um.user_id', user.id)
+        .where('um.is_deleted', 0)
+        .orderBy('m.created_at', 'desc')
+        .limit(pageSize)
+        .offset((page - 1) * pageSize)
+
+      if (type) query.where('m.type', type)
+      if (status) query.where('um.status', status)
+
+      const messages = await query
+
+      return c.json(messages)
+    } catch (error) {
+      console.error('获取消息列表失败:', error)
+      return c.json({ error: '获取消息列表失败' }, 500)
+    }
+  })
+
+  // 获取消息详情
+  messagesRoutes.get('/:id', withAuth, async (c) => {
+    try {
+      const apiClient = c.get('apiClient')
+      
+      const messageId = c.req.param('id')
+
+      const user = c.get('user')
+      if (!user) return c.json({ error: '未授权访问' }, 401)
+      
+      const message = await apiClient.database.table('user_messages')
+        .select('m.*', 'um.status as user_status', 'um.read_at')
+        .from('user_messages as um')
+        .leftJoin('messages as m', 'um.message_id', 'm.id')
+        .where('um.user_id', user.id)
+        .where('um.message_id', messageId)
+        .first()
+
+      if (!message) {
+        return c.json({ error: '消息不存在或无权访问' }, 404)
+      }
+
+      // 标记为已读
+      if (message.user_status === MessageStatus.UNREAD) {
+        const user = c.get('user')
+        if (!user) return c.json({ error: '未授权访问' }, 401)
+        
+        await apiClient.database.table('user_messages')
+          .where('user_id', user.id)
+          .where('message_id', messageId)
+          .update({
+            status: MessageStatus.READ,
+            read_at: apiClient.database.fn.now(),
+            updated_at: apiClient.database.fn.now()
+          })
+      }
+
+      return c.json(message)
+    } catch (error) {
+      console.error('获取消息详情失败:', error)
+      return c.json({ error: '获取消息详情失败' }, 500)
+    }
+  })
+
+  // 删除消息(软删除)
+  messagesRoutes.delete('/:id', withAuth, async (c) => {
+    try {
+      const apiClient = c.get('apiClient')
+      const user = c.get('user')
+      if (!user) return c.json({ error: '未授权访问' }, 401)
+      
+      const messageId = c.req.param('id')
+
+      await apiClient.database.table('user_messages')
+        .where('user_id', user.id)
+        .where('message_id', messageId)
+        .update({
+          is_deleted: 1,
+          updated_at: apiClient.database.fn.now()
+        })
+
+      return c.json({ message: '消息已删除' })
+    } catch (error) {
+      console.error('删除消息失败:', error)
+      return c.json({ error: '删除消息失败' }, 500)
+    }
+  })
+
+  // 获取未读消息数量
+  messagesRoutes.get('/unread-count', withAuth, async (c) => {
+    try {
+      const apiClient = c.get('apiClient')
+
+      const user = c.get('user')
+      if (!user) return c.json({ error: '未授权访问' }, 401)
+      
+      const count = await apiClient.database.table('user_messages')
+        .where('user_id', user.id)
+        .where('status', MessageStatus.UNREAD)
+        .where('is_deleted', 0)
+        .clone()
+        .count()
+
+      return c.json({ count: Number(count) })
+    } catch (error) {
+      console.error('获取未读消息数失败:', error)
+      return c.json({ error: '获取未读消息数失败' }, 500)
+    }
+  })
+
+  return messagesRoutes
+}