From d676fccad96280f40ee5d738a0ec8a85c85b8558 Mon Sep 17 00:00:00 2001 From: zyh Date: Thu, 10 Apr 2025 07:26:36 +0000 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=B6=88=E6=81=AF=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E5=8C=85=E6=8B=AC=E6=B6=88=E6=81=AF=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E5=92=8C=E7=8A=B6=E6=80=81=E7=9A=84=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=EF=BC=8C=E5=88=9B=E5=BB=BA=E6=B6=88=E6=81=AF=E5=92=8C=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=B6=88=E6=81=AF=E5=85=B3=E8=81=94=E7=9A=84=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E8=BF=81=E7=A7=BB=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E7=9B=B8=E5=85=B3=E7=9A=84API=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=EF=BC=8C=E6=8F=90=E5=8D=87=E7=B3=BB=E7=BB=9F=E7=9A=84?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E5=A4=84=E7=90=86=E8=83=BD=E5=8A=9B=E5=92=8C?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/share/types.ts | 49 +++++++++++ server/app.tsx | 2 + server/migrations.ts | 51 ++++++++++- server/routes_messages.ts | 178 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 server/routes_messages.ts diff --git a/client/share/types.ts b/client/share/types.ts index d34f7c1..a58bdfd 100644 --- a/client/share/types.ts +++ b/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.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; +} diff --git a/server/app.tsx b/server/app.tsx index b2ae68c..fb9a398 100644 --- a/server/app.tsx +++ b/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) diff --git a/server/migrations.ts b/server/migrations.ts index 0e7b8a8..3187732 100644 --- a/server/migrations.ts +++ b/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, ]; \ No newline at end of file diff --git a/server/routes_messages.ts b/server/routes_messages.ts new file mode 100644 index 0000000..9b2d9fe --- /dev/null +++ b/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 +} \ No newline at end of file