Ver código fonte

增加socketio 路由 支持

yourname 6 meses atrás
pai
commit
dea7ec5316

+ 1 - 0
HISTORY.md

@@ -3,6 +3,7 @@
 迁移管理页面,在正式环境中,需要验证env中配置的密码参数才能打开
 
 2025.05.15 0.1.6
+增加socketio 路由 支持
 增加socketio server 支持
 修正文件分类后端api路由查询表名为file_categories
 将react版本降为18.3.1

+ 5 - 3
server/app.tsx

@@ -4,6 +4,7 @@ import React from 'hono/jsx'
 import type { Context as HonoContext } from 'hono'
 import { serveStatic } from 'hono/deno'
 import { APIClient } from '@d8d-appcontainer/api'
+import { Auth } from '@d8d-appcontainer/auth';
 import debug from "debug"
 import dayjs from 'dayjs';
 import utc from 'dayjs/plugin/utc';
@@ -54,16 +55,17 @@ interface EsmScriptConfig {
 
 // 定义模块参数接口
 interface ModuleParams {
-  apiClient: APIClient
+  apiClient: APIClient,
+  auth: Auth,
   app: Hono
   moduleDir: string
 }
 
-export default function({ apiClient, app, moduleDir }: ModuleParams) {
+export default function({ apiClient, app, moduleDir , auth}: ModuleParams) {
   const honoApp = app
   
   // 创建路由
-  const router = createRouter(apiClient, moduleDir)
+  const router = createRouter(apiClient, moduleDir, auth)
   honoApp.route('/', router)
  
   // 首页路由 - SSR

+ 3 - 33
server/middlewares.ts

@@ -45,11 +45,11 @@ export const withAuth = async (c: HonoContext<{ Variables: Variables }>, next: (
 export type WithAuth = typeof withAuth;
 
 // 环境变量设置中间件
-export const setEnvVariables = (apiClient: APIClient, moduleDir: string) => {
+export const setEnvVariables = (apiClient: APIClient, moduleDir: string, auth: Auth) => {
   return async (c: HonoContext<{ Variables: Variables }>, next: () => Promise<void>) => {
     c.set('apiClient', apiClient)
     c.set('moduleDir', moduleDir)
-    c.set('auth', await initAuth(apiClient))
+    c.set('auth', auth)
     c.set('systemSettings', await initSystemSettings(apiClient))
     await next()
   }
@@ -58,37 +58,7 @@ export const setEnvVariables = (apiClient: APIClient, moduleDir: string) => {
 // CORS中间件
 export const corsMiddleware = cors()
 
-// 初始化Auth实例
-const initAuth = async (apiClient: APIClient) => {
-  try {
-    log.auth('正在初始化Auth实例')
-    
-    const auth = new Auth(apiClient as any, {
-      jwtSecret: Deno.env.get("JWT_SECRET") || 'your-jwt-secret-key',
-      initialUsers: [],
-      storagePrefix: '',
-      userTable: 'users',
-      fieldNames: {
-        id: 'id',
-        username: 'username',
-        password: 'password',
-        phone: 'phone',
-        email: 'email',
-        is_disabled: 'is_disabled',
-        is_deleted: 'is_deleted'
-      },
-      tokenExpiry: 24 * 60 * 60,
-      refreshTokenExpiry: 7 * 24 * 60 * 60
-    })
-    
-    log.auth('Auth实例初始化完成')
-    return auth
-    
-  } catch (error) {
-    log.auth('Auth初始化失败:', error)
-    throw error
-  }
-}
+
 
 // 初始化系统设置
 const initSystemSettings = async (apiClient: APIClient) => {

+ 3 - 2
server/router.ts

@@ -2,6 +2,7 @@
 import { Hono } from 'hono'
 import { corsMiddleware, withAuth, setEnvVariables } from './middlewares.ts'
 import type { APIClient } from '@d8d-appcontainer/api'
+import { Auth } from '@d8d-appcontainer/auth';
 
 // 导入路由模块
 import { createAuthRoutes } from "./routes_auth.ts"
@@ -17,7 +18,7 @@ import { createMessagesRoutes } from "./routes_messages.ts"
 import { createMigrationsRoutes } from "./routes_migrations.ts"
 import { createHomeRoutes } from "./routes_home.ts"
 
-export function createRouter(apiClient: APIClient, moduleDir: string) {
+export function createRouter(apiClient: APIClient, moduleDir: string , auth: Auth) {
   const router = new Hono()
 
   // 添加CORS中间件
@@ -27,7 +28,7 @@ export function createRouter(apiClient: APIClient, moduleDir: string) {
   const api = new Hono()
   
   // 设置环境变量
-  api.use('*', setEnvVariables(apiClient, moduleDir))
+  api.use('*', setEnvVariables(apiClient, moduleDir, auth))
 
   // 注册所有路由
   api.route('/auth', createAuthRoutes(withAuth))

+ 73 - 0
server/router_io.ts

@@ -0,0 +1,73 @@
+import { Socket, Server } from "socket.io";
+import { Auth } from '@d8d-appcontainer/auth';
+import type { User as AuthUser } from '@d8d-appcontainer/auth';
+import { APIClient } from '@d8d-appcontainer/api';
+import { setupMessageEvents } from './routes_io_messages.ts';
+import debug from "debug";
+
+const log = debug('socketio:auth');
+
+interface SetupSocketIOProps {
+  io: Server, auth: Auth, apiClient: APIClient
+}
+
+export interface SocketWithUser extends Socket {
+  user?: AuthUser;
+}
+
+// 定义自定义上下文类型
+export interface Variables {
+  socket: SocketWithUser
+  auth: Auth
+  user: AuthUser
+  apiClient: APIClient
+  // moduleDir: string
+  // systemSettings?: SystemSettingRecord
+}
+
+export function setupSocketIO({ io, auth, apiClient }:SetupSocketIOProps) {
+  // Socket.IO认证中间件
+  io.use(async (socket: SocketWithUser) => {
+    try {
+      const token = socket.handshake.query.get('socket_token');
+      if (!token) {
+        log(`未提供token,拒绝连接: ${socket.id}`);
+        throw new Error('未授权')
+      }
+
+      const userData = await auth.verifyToken(token);
+      if (!userData) {
+        log(`无效token,拒绝连接: ${socket.id}`);
+        throw new Error('无效凭证')
+      }
+
+      socket.user = userData;
+      log(`认证成功: ${socket.id} 用户: ${userData.username}`);
+    } catch (error) {
+      log(`认证错误: ${socket.id}`, error);
+    }
+  });
+
+  io.on("connection", (socket: SocketWithUser) => {
+    if (!socket.user) {
+      socket.disconnect(true);
+      return;
+    }
+
+    console.log(`socket ${socket.id} 已连接,用户: ${socket.user.username}`);
+
+    socket.on("disconnect", (reason) => {
+      console.log(`socket ${socket.id} 断开连接,原因: ${reason}`);
+    });
+
+    const context: Variables = {
+      socket,
+      auth,
+      apiClient,
+      user: socket.user,
+    }
+
+    // 初始化消息路由
+    setupMessageEvents(context);
+  });
+}

+ 226 - 0
server/routes_io_messages.ts

@@ -0,0 +1,226 @@
+import { SocketWithUser , Variables} from './router_io.ts';
+import { MessageType, MessageStatus } from '../client/share/types.ts'
+import { APIClient } from "@d8d-appcontainer/api";
+
+interface MessageSendData {
+  title: string;
+  content: string;
+  type: MessageType;
+  receiver_ids: number[];
+}
+
+interface MessageListData {
+  page?: number;
+  pageSize?: number;
+  type?: MessageType;
+  status?: MessageStatus;
+}
+
+export function setupMessageEvents({ socket , apiClient }:Variables) {
+  // 发送消息
+  socket.on('message:send', async (data: MessageSendData) => {
+    try {
+      const { title, content, type, receiver_ids } = data;
+
+      if (!title || !content || !type || !receiver_ids?.length) {
+        socket.emit('error', '缺少必要参数');
+        return;
+      }
+
+      const user = socket.user;
+      if (!user) {
+        socket.emit('error', '未授权访问');
+        return;
+      }
+      
+      // 创建消息
+      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);
+
+      socket.emit('message:sent', {
+        message: '消息发送成功',
+        data: { id: messageId }
+      });
+    } catch (error) {
+      console.error('发送消息失败:', error);
+      socket.emit('error', '发送消息失败');
+    }
+  });
+  
+  // 获取消息列表
+  socket.on('message:list', async (data: MessageListData) => {
+    try {
+      const { page = 1, pageSize = 20, type, status } = data;
+      const user = socket.user;
+      if (!user) {
+        socket.emit('error', '未授权访问');
+        return;
+      }
+      
+      const query = apiClient.database.table('user_messages as um')
+        .select('m.*', 'um.status as user_status', 'um.read_at', 'um.id as user_message_id')
+        .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 countQuery = query.clone();
+      const messages = await query;
+      
+      // 获取总数用于分页
+      const total = await countQuery.count();
+      const totalCount = Number(total);
+      const totalPages = Math.ceil(totalCount / pageSize);
+
+      socket.emit('message:list', {
+        data: messages,
+        pagination: {
+          total: totalCount,
+          current: page,
+          pageSize,
+          totalPages
+        }
+      });
+    } catch (error) {
+      console.error('获取消息列表失败:', error);
+      socket.emit('error', '获取消息列表失败');
+    }
+  });
+
+  // 获取消息详情
+  socket.on('message:detail', async (messageId: number) => {
+    try {
+      const user = socket.user;
+      if (!user) {
+        socket.emit('error', '未授权访问');
+        return;
+      }
+      
+      const message = await apiClient.database.table('user_messages as um')
+        .select('m.*', 'um.status as user_status', 'um.read_at')
+        .leftJoin('messages as m', 'um.message_id', 'm.id')
+        .where('um.user_id', user.id)
+        .where('um.message_id', messageId)
+        .first();
+
+      if (!message) {
+        socket.emit('error', '消息不存在或无权访问');
+        return;
+      }
+
+      // 标记为已读
+      if (message.user_status === MessageStatus.UNREAD) {
+        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()
+          });
+      }
+
+      socket.emit('message:detail', {
+        message: '获取消息成功',
+        data: message
+      });
+    } catch (error) {
+      console.error('获取消息详情失败:', error);
+      socket.emit('error', '获取消息详情失败');
+    }
+  });
+
+  // 删除消息
+  socket.on('message:delete', async (messageId: number) => {
+    try {
+      const user = socket.user;
+      if (!user) {
+        socket.emit('error', '未授权访问');
+        return;
+      }
+      
+      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()
+        });
+
+      socket.emit('message:deleted', { message: '消息已删除' });
+    } catch (error) {
+      console.error('删除消息失败:', error);
+      socket.emit('error', '删除消息失败');
+    }
+  });
+
+  // 获取未读消息数
+  socket.on('message:count', async () => {
+    try {
+      const user = socket.user;
+      if (!user) {
+        socket.emit('error', '未授权访问');
+        return;
+      }
+      
+      const count = await apiClient.database.table('user_messages')
+        .where('user_id', user.id)
+        .where('status', MessageStatus.UNREAD)
+        .where('is_deleted', 0)
+        .count();
+
+      socket.emit('message:count', { count: Number(count) });
+    } catch (error) {
+      console.error('获取未读消息数失败:', error);
+      socket.emit('error', '获取未读消息数失败');
+    }
+  });
+
+  // 标记消息为已读
+  socket.on('message:read', async (messageId: number) => {
+    try {
+      const user = socket.user;
+      if (!user) {
+        socket.emit('error', '未授权访问');
+        return;
+      }
+      
+      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()
+        });
+
+      socket.emit('message:read', { message: '消息已标记为已读' });
+    } catch (error) {
+      console.error('标记消息为已读失败:', error);
+      socket.emit('error', '标记消息为已读失败');
+    }
+  });
+}

+ 0 - 15
server/routes_socketio.ts

@@ -1,15 +0,0 @@
-import { Socket, Server } from "socket.io";
-
-export function setupSocketIO(io: Server) {
-  io.on("connection", (socket: Socket) => {
-    console.log(`socket ${socket.id} connected`);
-
-    socket.emit("hello", "world");
-
-    socket.on("disconnect", (reason) => {
-      console.log(`socket ${socket.id} disconnected due to ${reason}`);
-    });
-
-    // 可在此添加更多socket事件处理
-  });
-}

+ 39 - 5
server/run_app.ts

@@ -1,11 +1,12 @@
 // 导入所需模块
 import { Hono } from 'hono'
 import { APIClient } from '@d8d-appcontainer/api'
+import { Auth } from '@d8d-appcontainer/auth';
 import debug from "debug"
 import { cors } from 'hono/cors'
 import { Server } from "socket.io"
 import httpServer from './app.tsx'
-import { setupSocketIO } from './routes_socketio.ts'
+import { setupSocketIO } from './router_io.ts'
 
 // 初始化debug实例
 const log = {
@@ -39,6 +40,37 @@ const getApiClient = async (workspaceKey: string, serverUrl?: string) => {
     throw error
   }
 }
+// 初始化Auth实例
+const initAuth = async (apiClient: APIClient) => {
+  try {
+    log.auth('正在初始化Auth实例')
+    
+    const auth = new Auth(apiClient as any, {
+      jwtSecret: Deno.env.get("JWT_SECRET") || 'your-jwt-secret-key',
+      initialUsers: [],
+      storagePrefix: '',
+      userTable: 'users',
+      fieldNames: {
+        id: 'id',
+        username: 'username',
+        password: 'password',
+        phone: 'phone',
+        email: 'email',
+        is_disabled: 'is_disabled',
+        is_deleted: 'is_deleted'
+      },
+      tokenExpiry: 24 * 60 * 60,
+      refreshTokenExpiry: 7 * 24 * 60 * 60
+    })
+    
+    log.auth('Auth实例初始化完成')
+    return auth
+    
+  } catch (error) {
+    log.auth('Auth初始化失败:', error)
+    throw error
+  }
+}
 
 // 初始化API Client
 // 注意:WORKSPACE_KEY 需要在 多八多(www.d8d.fun) 平台注册并开通工作空间后获取
@@ -47,6 +79,7 @@ if (!workspaceKey) {
   console.warn('未设置WORKSPACE_KEY,请前往 多八多(www.d8d.fun) 注册并开通工作空间以获取密钥')
 }
 const apiClient = await getApiClient(workspaceKey)
+const auth = await initAuth(apiClient);
 
 // 创建Hono应用实例
 const app = new Hono()
@@ -63,10 +96,10 @@ const io = new Server({
   }
 })
 
-setupSocketIO(io);
+setupSocketIO({io, auth, apiClient});
 
 // 动态加载并运行模板
-const runTemplate = async () => {
+const runTemplate = () => {
   try {
     // 创建基础app实例
     const moduleApp = new Hono()
@@ -75,7 +108,8 @@ const runTemplate = async () => {
     const appInstance = httpServer({
       apiClient: apiClient,
       app: moduleApp,
-      moduleDir: './'
+      moduleDir: './',
+      auth
     }) 
     // 启动服务器
     Deno.serve({ 
@@ -92,4 +126,4 @@ const runTemplate = async () => {
 }
 
 // 执行模板
-runTemplate()
+runTemplate()