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

This commit is contained in:
zyh
2025-04-10 07:26:36 +00:00
parent de4a711318
commit d676fccad9
4 changed files with 279 additions and 1 deletions

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -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
server/routes_messages.ts Normal file
View File

@@ -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
}