Merge branch 'fork' of 1030-6/d8d-admin-mobile-starter-public into main
This commit is contained in:
@@ -3,6 +3,10 @@
|
||||
迁移管理页面,在正式环境中,需要验证env中配置的密码参数才能打开
|
||||
|
||||
2025.05.15 0.1.6
|
||||
站内消息支持三种类型,admin发送,mobile订阅
|
||||
增加admin, mobile 消息io连接
|
||||
增加socketio 路由 支持
|
||||
增加socketio server 支持
|
||||
修正文件分类后端api路由查询表名为file_categories
|
||||
将react版本降为18.3.1
|
||||
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Button, Table, Space, Modal, Form, Input, Select, message } from 'antd';
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import type { TableProps } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
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 = () => {
|
||||
export const MessagesPage = () => {
|
||||
const { token } = useAuth();
|
||||
const [socket, setSocket] = useState<Socket | null>(null);
|
||||
const [isSocketConnected, setIsSocketConnected] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
const [form] = Form.useForm();
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
@@ -59,8 +64,58 @@ export const MessagesPage = () => {
|
||||
});
|
||||
|
||||
// 发送消息
|
||||
// 初始化Socket.IO连接
|
||||
useEffect(() => {
|
||||
if (!token) return;
|
||||
|
||||
const newSocket = io('/', {
|
||||
path: '/socket.io',
|
||||
transports: ['websocket'],
|
||||
autoConnect: false,
|
||||
query: {
|
||||
socket_token: token
|
||||
}
|
||||
});
|
||||
|
||||
newSocket.on('connect', () => {
|
||||
setIsSocketConnected(true);
|
||||
message.success('实时消息连接已建立');
|
||||
});
|
||||
|
||||
newSocket.on('disconnect', () => {
|
||||
setIsSocketConnected(false);
|
||||
message.warning('实时消息连接已断开');
|
||||
});
|
||||
|
||||
newSocket.on('error', (err) => {
|
||||
message.error(`实时消息错误: ${err}`);
|
||||
});
|
||||
|
||||
newSocket.connect();
|
||||
setSocket(newSocket);
|
||||
|
||||
return () => {
|
||||
newSocket.disconnect();
|
||||
};
|
||||
}, [token]);
|
||||
|
||||
const sendMessageMutation = useMutation({
|
||||
mutationFn: (data: any) => MessageAPI.sendMessage(data),
|
||||
mutationFn: async (data: any) => {
|
||||
// 优先使用Socket.IO发送
|
||||
if (isSocketConnected && socket) {
|
||||
return new Promise((resolve, reject) => {
|
||||
socket.emit('message:send', data, (response: any) => {
|
||||
if (response.error) {
|
||||
reject(new Error(response.error));
|
||||
} else {
|
||||
resolve(response.data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// 回退到HTTP API
|
||||
return MessageAPI.sendMessage(data);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['messages'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['unreadCount'] });
|
||||
@@ -167,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>
|
||||
@@ -178,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>
|
||||
@@ -235,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>
|
||||
@@ -266,10 +321,11 @@ export const MessagesPage = () => {
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={sendMessageMutation.status === 'pending'}
|
||||
icon={isSocketConnected ? <span style={{color:'green'}}>●</span> : <span style={{color:'red'}}>●</span>}
|
||||
>
|
||||
发送
|
||||
</Button>
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import { BellIcon } from '@heroicons/react/24/outline';
|
||||
import { MessageStatus } from '../share/types.ts';
|
||||
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
|
||||
// 添加通知页面组件
|
||||
import { MessageAPI } from './api/index.ts';
|
||||
import { useAuth } from "./hooks.tsx";
|
||||
|
||||
export const NotificationsPage = () => {
|
||||
const { token , user} = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
const [socket, setSocket] = React.useState<Socket | null>(null);
|
||||
const [isSubscribed, setIsSubscribed] = React.useState(false);
|
||||
|
||||
// 获取消息列表
|
||||
const { data: messages, isLoading } = useQuery({
|
||||
@@ -18,6 +22,72 @@ export const NotificationsPage = () => {
|
||||
queryFn: () => MessageAPI.getMessages(),
|
||||
});
|
||||
|
||||
// 初始化Socket.IO连接
|
||||
useEffect(() => {
|
||||
if (!token || !user) return;
|
||||
|
||||
const newSocket = io('/', {
|
||||
path: '/socket.io',
|
||||
transports: ['websocket'],
|
||||
withCredentials: true,
|
||||
query: {
|
||||
socket_token: token
|
||||
}
|
||||
});
|
||||
|
||||
setSocket(newSocket);
|
||||
|
||||
// 订阅消息频道
|
||||
newSocket.on('connect', () => {
|
||||
// 订阅个人频道
|
||||
newSocket.emit('message:subscribe', `user_${user.id}`);
|
||||
// 订阅系统频道
|
||||
newSocket.emit('message:subscribe', 'system');
|
||||
// 订阅公告频道
|
||||
newSocket.emit('message:subscribe', 'announce');
|
||||
setIsSubscribed(true);
|
||||
});
|
||||
|
||||
// 处理实时消息
|
||||
const handleNewMessage = (newMessage: any) => {
|
||||
queryClient.setQueryData(['messages'], (oldData: any) => {
|
||||
if (!oldData) return oldData;
|
||||
return {
|
||||
...oldData,
|
||||
data: [newMessage, ...oldData.data]
|
||||
};
|
||||
});
|
||||
|
||||
// 更新未读计数
|
||||
queryClient.setQueryData(['unreadCount'], (oldData: any) => {
|
||||
if (!oldData) return oldData;
|
||||
return {
|
||||
...oldData,
|
||||
count: oldData.count + 1
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// 处理广播消息
|
||||
newSocket.on('message:broadcasted', handleNewMessage);
|
||||
// 处理频道推送消息
|
||||
newSocket.on('message:received', handleNewMessage);
|
||||
|
||||
// 错误处理
|
||||
newSocket.on('error', (error) => {
|
||||
console.error('Socket error:', error);
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (newSocket) {
|
||||
newSocket.emit('message:unsubscribe', `user_${user.id}`);
|
||||
newSocket.emit('message:unsubscribe', 'system');
|
||||
newSocket.emit('message:unsubscribe', 'announce');
|
||||
newSocket.disconnect();
|
||||
}
|
||||
};
|
||||
}, [queryClient, token]);
|
||||
|
||||
// 获取未读消息数量
|
||||
const { data: unreadCount } = useQuery({
|
||||
queryKey: ['unreadCount'],
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
"@ant-design/plots": "https://esm.d8d.fun/@ant-design/plots@2.1.13?dev&deps=react@18.3.1,react-dom@18.3.1",
|
||||
"react-hook-form": "https://esm.d8d.fun/react-hook-form@7.55.0?dev&deps=react@18.3.1,react-dom@18.3.1",
|
||||
"@heroicons/react/24/outline": "https://esm.d8d.fun/@heroicons/react@2.1.1/24/outline?dev&deps=react@18.3.1,react-dom@18.3.1",
|
||||
"@heroicons/react/24/solid": "https://esm.d8d.fun/@heroicons/react@2.1.1/24/solid?dev&deps=react@18.3.1,react-dom@18.3.1"
|
||||
"@heroicons/react/24/solid": "https://esm.d8d.fun/@heroicons/react@2.1.1/24/solid?dev&deps=react@18.3.1,react-dom@18.3.1",
|
||||
"socket.io": "https://deno.land/x/socket_io@0.2.1/mod.ts",
|
||||
"socket.io-client": "https://esm.d8d.fun/socket.io-client@4.8.1"
|
||||
},
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext", "deno.ns"]
|
||||
|
||||
68
deno.lock
generated
68
deno.lock
generated
@@ -2575,9 +2575,32 @@
|
||||
"https://esm.d8d.fun/xmlhttprequest-ssl@~2.1.1?target=denonext": "https://esm.d8d.fun/xmlhttprequest-ssl@2.1.2?target=denonext"
|
||||
},
|
||||
"remote": {
|
||||
"https://deno.land/std@0.150.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74",
|
||||
"https://deno.land/std@0.150.0/bytes/bytes_list.ts": "aba5e2369e77d426b10af1de0dcc4531acecec27f9b9056f4f7bfbf8ac147ab4",
|
||||
"https://deno.land/std@0.150.0/bytes/equals.ts": "3c3558c3ae85526f84510aa2b48ab2ad7bdd899e2e0f5b7a8ffc85acb3a6043a",
|
||||
"https://deno.land/std@0.150.0/bytes/mod.ts": "763f97d33051cc3f28af1a688dfe2830841192a9fea0cbaa55f927b49d49d0bf",
|
||||
"https://deno.land/std@0.150.0/fmt/colors.ts": "6f9340b7fb8cc25a993a99e5efc56fe81bb5af284ff412129dd06df06f53c0b4",
|
||||
"https://deno.land/std@0.150.0/fs/exists.ts": "cb734d872f8554ea40b8bff77ad33d4143c1187eac621a55bf37781a43c56f6d",
|
||||
"https://deno.land/std@0.150.0/io/buffer.ts": "bd0c4bf53db4b4be916ca5963e454bddfd3fcd45039041ea161dbf826817822b",
|
||||
"https://deno.land/std@0.150.0/log/handlers.ts": "b88c24df61eaeee8581dbef3622f21aebfd061cd2fda49affc1711c0e54d57da",
|
||||
"https://deno.land/std@0.150.0/log/levels.ts": "82c965b90f763b5313e7595d4ba78d5095a13646d18430ebaf547526131604d1",
|
||||
"https://deno.land/std@0.150.0/log/logger.ts": "4d25581bc02dfbe3ad7e8bb480e1f221793a85be5e056185a0cea134f7a7fdf4",
|
||||
"https://deno.land/std@0.150.0/log/mod.ts": "65d2702785714b8d41061426b5c279f11b3dcbc716f3eb5384372a430af63961",
|
||||
"https://deno.land/std@0.150.0/media_types/_util.ts": "ce9b4fc4ba1c447dafab619055e20fd88236ca6bdd7834a21f98bd193c3fbfa1",
|
||||
"https://deno.land/std@0.150.0/media_types/mod.ts": "2d4b6f32a087029272dc59e0a55ae3cc4d1b27b794ccf528e94b1925795b3118",
|
||||
"https://deno.land/std@0.150.0/media_types/vendor/mime-db.v1.52.0.ts": "724cee25fa40f1a52d3937d6b4fbbfdd7791ff55e1b7ac08d9319d5632c7f5af",
|
||||
"https://deno.land/std@0.150.0/testing/_diff.ts": "029a00560b0d534bc0046f1bce4bd36b3b41ada3f2a3178c85686eb2ff5f1413",
|
||||
"https://deno.land/std@0.150.0/testing/_format.ts": "0d8dc79eab15b67cdc532826213bbe05bccfd276ca473a50a3fc7bbfb7260642",
|
||||
"https://deno.land/std@0.150.0/testing/_test_suite.ts": "ad453767aeb8c300878a6b7920e20370f4ce92a7b6c8e8a5d1ac2b7c14a09acb",
|
||||
"https://deno.land/std@0.150.0/testing/asserts.ts": "0ee58a557ac764e762c62bb21f00e7d897e3919e71be38b2d574fb441d721005",
|
||||
"https://deno.land/std@0.150.0/testing/bdd.ts": "182bb823e09bd75b76063ecf50722870101b7cfadf97a09fa29127279dc21128",
|
||||
"https://deno.land/std@0.158.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74",
|
||||
"https://deno.land/std@0.158.0/async/deferred.ts": "c01de44b9192359cebd3fe93273fcebf9e95110bf3360023917da9a2d1489fae",
|
||||
"https://deno.land/std@0.158.0/async/delay.ts": "0419dfc993752849692d1f9647edf13407c7facc3509b099381be99ffbc9d699",
|
||||
"https://deno.land/std@0.158.0/bytes/bytes_list.ts": "aba5e2369e77d426b10af1de0dcc4531acecec27f9b9056f4f7bfbf8ac147ab4",
|
||||
"https://deno.land/std@0.158.0/bytes/equals.ts": "3c3558c3ae85526f84510aa2b48ab2ad7bdd899e2e0f5b7a8ffc85acb3a6043a",
|
||||
"https://deno.land/std@0.158.0/bytes/mod.ts": "763f97d33051cc3f28af1a688dfe2830841192a9fea0cbaa55f927b49d49d0bf",
|
||||
"https://deno.land/std@0.158.0/io/buffer.ts": "fae02290f52301c4e0188670e730cd902f9307fb732d79c4aa14ebdc82497289",
|
||||
"https://deno.land/std@0.217.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
|
||||
"https://deno.land/std@0.217.0/assert/_diff.ts": "dcc63d94ca289aec80644030cf88ccbf7acaa6fbd7b0f22add93616b36593840",
|
||||
"https://deno.land/std@0.217.0/assert/_format.ts": "0ba808961bf678437fb486b56405b6fefad2cf87b5809667c781ddee8c32aff4",
|
||||
@@ -2654,6 +2677,51 @@
|
||||
"https://deno.land/x/deno_dom@v0.1.48/src/dom/utils-types.ts": "96db30e3e4a75b194201bb9fa30988215da7f91b380fca6a5143e51ece2a8436",
|
||||
"https://deno.land/x/deno_dom@v0.1.48/src/dom/utils.ts": "4c6206516fb8f61f37a209c829e812c4f5a183e46d082934dd14c91bde939263",
|
||||
"https://deno.land/x/deno_dom@v0.1.48/src/parser.ts": "e06b2300d693e6ae7564e53dfa5c9a9e97fdb8c044c39c52c8b93b5d60860be3",
|
||||
"https://deno.land/x/socket_io@0.2.1/deps.ts": "2c9c7fd0f00c9f8774a7cbf6bea2e73b274989aacb3ebfbd289a3c1bbe632bcb",
|
||||
"https://deno.land/x/socket_io@0.2.1/mod.ts": "29050911ca6f9605623c672238bb209ca37ed23606c596d199e6e33de04168f9",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/engine.io-parser/base64-arraybuffer.ts": "57ccea6679609df5416159fcc8a47936ad28ad6fe32235ef78d9223a3a823407",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/engine.io-parser/mod.ts": "27d35094e2159ba49f6e74f11ed83b6208a6adb5a2d5ab3cbbdcdc9dc0e36ae7",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/engine.io/lib/cors.ts": "e39b530dc3526ef85f288766ce592fa5cce2ec38b3fa19922041a7885b79b67c",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/engine.io/lib/server.ts": "2faf79492858e532ad199c32b565bb04528fa9ea55d167e223dc9d019ef9315f",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/engine.io/lib/socket.ts": "feb50d196decd7b1fc79af2706465cc7b4b8b18ebb7ca3dc8cedadb0a393103e",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/engine.io/lib/transport.ts": "b09c589a099d539cd0b61f7b3be0b9d2d9ba7b8cbafdf4824425176ecf8d89c9",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/engine.io/lib/transports/polling.ts": "5650189f6cd742ec0fd45f8c262105f07463b24e8183da6ffb2bf775baf21395",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/engine.io/lib/transports/websocket.ts": "4a868e73d3b8b207d822d358f14723bd8d1cd80c6926be9c4bf6c4234e8a0d00",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/engine.io/lib/util.ts": "9f396a141422c8a2e2ef4cbb31c8b7ec96665d8f1ca397888eaaa9ad28ca8c65",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/engine.io/mod.ts": "3f7d85ebd3bee6e17838f4867927d808f35090a71e088fd4dd802e3255d44c4a",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/event-emitter/mod.ts": "dcb2cb9c0b409060cf15a6306a8dbebea844aa3c58f782ed1d4bc3ccef7c2835",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/msgpack/lib/decode.ts": "5906fa37474130b09fe308adb53c95e40d2484a015891be3249fb2f626c462bb",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/msgpack/lib/encode.ts": "15dab78be56d539c03748c9d57086f7fd580eb8fbe2f8209c28750948c7d962e",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/msgpack/mod.ts": "c7f4a9859af3e0b23794b400546d93475b19ba5110a02245112a0a994a31d309",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/socket.io-parser/mod.ts": "44479cf563b0ad80efedd1059cd40114bc5db199b45e623c2764e80f4a264f8c",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/socket.io-redis-adapter/mod.ts": "45d6b7f077f94fec385152bda7fda5ac3153c2ca3548cf4859891af673fa97cc",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/socket.io/lib/adapter.ts": "594fbe748497ce346e8b783065cb19b284f27d702ff661d872971d14ccf6cf29",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/socket.io/lib/broadcast-operator.ts": "d842eb933acc996a05ac701f6d83ffee49ee9c905c9adbdee70832776045bf63",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/socket.io/lib/client.ts": "5e5d8e39d58cc5eb14f219e85ae6eef8fe6dd5f685f4c73496531aa48529c2c6",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/socket.io/lib/namespace.ts": "d9ec734c8b45f4204c40382e1d3a8fb4d1b8bffab4bca4d2d69baece0703d4f8",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/socket.io/lib/parent-namespace.ts": "9628cbf54e35bea01956825f557b4a82a37c8f1343423c0e7d952d475bf68ea0",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/socket.io/lib/server.ts": "19b10d05e09e436fda0d8d205ba1ce06bb5877f516176b8148de8a3c26b37688",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/socket.io/lib/socket.ts": "c448032d0f819d40d6dc30eb312c391c5d902eeeeb67bfd4f4f4c3499545eb7e",
|
||||
"https://deno.land/x/socket_io@0.2.1/packages/socket.io/mod.ts": "dfd465bdcf23161af0c4d79fb8fc8912418c46a20d15e8b314cec6d9fb508196",
|
||||
"https://deno.land/x/socket_io@0.2.1/test_deps.ts": "42e6bff240c54a2d7ade82154f8655650664a65ad9228623a0fdb3d0cde11ef0",
|
||||
"https://deno.land/x/socket_io@0.2.1/vendor/deno.land/x/redis@v0.27.1/backoff.ts": "33e4a6e245f8743fbae0ce583993a671a3ac2ecee433a3e7f0bd77b5dd541d84",
|
||||
"https://deno.land/x/socket_io@0.2.1/vendor/deno.land/x/redis@v0.27.1/command.ts": "802df3a1f49f6c49fe3e8fcf13fd0cc360b8a02369de0310a72d7f0c8e4ceaab",
|
||||
"https://deno.land/x/socket_io@0.2.1/vendor/deno.land/x/redis@v0.27.1/connection.ts": "b325d5b720af8132cd81d6d8b6a2e61936631b36d0dd08907d5421107bf50723",
|
||||
"https://deno.land/x/socket_io@0.2.1/vendor/deno.land/x/redis@v0.27.1/errors.ts": "bc8f7091cb9f36cdd31229660e0139350b02c26851e3ac69d592c066745feb27",
|
||||
"https://deno.land/x/socket_io@0.2.1/vendor/deno.land/x/redis@v0.27.1/executor.ts": "03e5f43df4e0c9c62b0e1be778811d45b6a1966ddf406e21ed5a227af70b7183",
|
||||
"https://deno.land/x/socket_io@0.2.1/vendor/deno.land/x/redis@v0.27.1/mod.ts": "20908f005f5c102525ce6aa9261648c95c5f61c6cf782b2cbb2fce88b1220f69",
|
||||
"https://deno.land/x/socket_io@0.2.1/vendor/deno.land/x/redis@v0.27.1/pipeline.ts": "80cc26a881149264d51dd019f1044c4ec9012399eca9f516057dc81c9b439370",
|
||||
"https://deno.land/x/socket_io@0.2.1/vendor/deno.land/x/redis@v0.27.1/protocol/_util.ts": "0525f7f444a96b92cd36423abdfe221f8d8de4a018dc5cb6750a428a5fc897c2",
|
||||
"https://deno.land/x/socket_io@0.2.1/vendor/deno.land/x/redis@v0.27.1/protocol/command.ts": "b1efd3b62fe5d1230e6d96b5c65ba7de1592a1eda2cc927161e5997a15f404ac",
|
||||
"https://deno.land/x/socket_io@0.2.1/vendor/deno.land/x/redis@v0.27.1/protocol/mod.ts": "f2601df31d8adc71785b5d19f6a7e43dfce94adbb6735c4dafc1fb129169d11a",
|
||||
"https://deno.land/x/socket_io@0.2.1/vendor/deno.land/x/redis@v0.27.1/protocol/reply.ts": "beac2061b03190bada179aef1a5d92b47a5104d9835e8c7468a55c24812ae9e4",
|
||||
"https://deno.land/x/socket_io@0.2.1/vendor/deno.land/x/redis@v0.27.1/protocol/types.ts": "40b0a568cb7fd4dc9107997062584d24e5c6ffa1f21acb6410aa19c92f89e9e1",
|
||||
"https://deno.land/x/socket_io@0.2.1/vendor/deno.land/x/redis@v0.27.1/pubsub.ts": "324b87dae0700e4cb350780ce3ae5bc02780f79f3de35e01366b894668b016c6",
|
||||
"https://deno.land/x/socket_io@0.2.1/vendor/deno.land/x/redis@v0.27.1/redis.ts": "a5c2cf8c72e7c92c9c8c6911f98227062649f6cba966938428c5414200f3aa54",
|
||||
"https://deno.land/x/socket_io@0.2.1/vendor/deno.land/x/redis@v0.27.1/stream.ts": "f116d73cfe04590ff9fa8a3c08be8ff85219d902ef2f6929b8c1e88d92a07810",
|
||||
"https://deno.land/x/socket_io@0.2.1/vendor/deno.land/x/redis@v0.27.1/vendor/https/deno.land/std/async/deferred.ts": "7391210927917113e04247ef013d800d54831f550e9a0b439244675c56058c55",
|
||||
"https://deno.land/x/socket_io@0.2.1/vendor/deno.land/x/redis@v0.27.1/vendor/https/deno.land/std/async/delay.ts": "c7e2604f7cb5ef00d595de8dd600604902d5be03a183b515b3d6d4bbb48e1700",
|
||||
"https://deno.land/x/socket_io@0.2.1/vendor/deno.land/x/redis@v0.27.1/vendor/https/deno.land/std/io/buffer.ts": "8c5f84b7ecf71bc3e12aa299a9fae9e72e495db05281fcdd62006ecd3c5ed3f3",
|
||||
"https://deno.land/x/xhr@0.3.0/mod.ts": "094aacd627fd9635cd942053bf8032b5223b909858fa9dc8ffa583752ff63b20",
|
||||
"https://esm.d8d.fun/@ant-design/charts-util@0.0.1-alpha.5/X-ZHJlYWN0LWRvbUAxOS4wLjAscmVhY3RAMTkuMC4w/denonext/charts-util.development.mjs": "92a5ac00883b6b3b33b5498616d35fdd262d6ecbca85914aaaa1612d501c33a4",
|
||||
"https://esm.d8d.fun/@ant-design/charts-util@0.0.1-alpha.5/X-ZHJlYWN0LWRvbUAxOS4wLjAscmVhY3RAMTkuMC4w/denonext/charts-util.mjs": "2b5590c1c3b095fd2cc95448c174aa747dd187929dd0f0ecf79a19a95e2f48ba",
|
||||
|
||||
99
docs/message-system-architecture.md
Normal file
99
docs/message-system-architecture.md
Normal file
@@ -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 - 标记消息为已读
|
||||
|
||||
### 权限控制
|
||||
- 系统消息: 仅管理员可发送
|
||||
- 公告: 管理员和特定角色可发送
|
||||
- 私信: 所有用户可发送
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
server/router_io.ts
Normal file
73
server/router_io.ts
Normal file
@@ -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);
|
||||
});
|
||||
}
|
||||
324
server/routes_io_messages.ts
Normal file
324
server/routes_io_messages.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
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:subscribe', (channel: string) => {
|
||||
try {
|
||||
socket.join(channel);
|
||||
socket.emit('message:subscribed', {
|
||||
message: `成功订阅频道: ${channel}`,
|
||||
channel
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('订阅频道失败:', error);
|
||||
socket.emit('error', '订阅频道失败');
|
||||
}
|
||||
});
|
||||
|
||||
// 取消订阅
|
||||
socket.on('message:unsubscribe', (channel: string) => {
|
||||
try {
|
||||
socket.leave(channel);
|
||||
socket.emit('message:unsubscribed', {
|
||||
message: `已取消订阅频道: ${channel}`,
|
||||
channel
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('取消订阅失败:', error);
|
||||
socket.emit('error', '取消订阅失败');
|
||||
}
|
||||
});
|
||||
|
||||
// 广播消息
|
||||
socket.on('message:broadcast', async (data: {
|
||||
channel?: string;
|
||||
title: string;
|
||||
content: string;
|
||||
type: MessageType;
|
||||
}) => {
|
||||
try {
|
||||
const { channel, title, content, type } = data;
|
||||
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,
|
||||
is_broadcast: 1,
|
||||
created_at: apiClient.database.fn.now(),
|
||||
updated_at: apiClient.database.fn.now()
|
||||
});
|
||||
|
||||
// 广播到所有客户端或特定频道
|
||||
const broadcastTarget = channel ? socket.to(channel) : socket.broadcast;
|
||||
broadcastTarget.emit('message:broadcasted', {
|
||||
id: messageId,
|
||||
title,
|
||||
content,
|
||||
type,
|
||||
sender_id: user.id,
|
||||
sender_name: user.username,
|
||||
created_at: new Date().toISOString()
|
||||
});
|
||||
|
||||
socket.emit('message:broadcasted', {
|
||||
message: '广播消息发送成功',
|
||||
data: { id: messageId }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('广播消息失败:', error);
|
||||
socket.emit('error', '广播消息失败');
|
||||
}
|
||||
});
|
||||
|
||||
// 发送消息
|
||||
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);
|
||||
|
||||
// 根据消息类型推送到不同频道
|
||||
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 }
|
||||
});
|
||||
} 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', '标记消息为已读失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,8 +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 './router_io.ts'
|
||||
|
||||
// 初始化debug实例
|
||||
const log = {
|
||||
@@ -36,6 +40,46 @@ 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) 平台注册并开通工作空间后获取
|
||||
const workspaceKey = Deno.env.get('WORKSPACE_KEY') || ''
|
||||
if (!workspaceKey) {
|
||||
console.warn('未设置WORKSPACE_KEY,请前往 多八多(www.d8d.fun) 注册并开通工作空间以获取密钥')
|
||||
}
|
||||
const apiClient = await getApiClient(workspaceKey)
|
||||
const auth = await initAuth(apiClient);
|
||||
|
||||
// 创建Hono应用实例
|
||||
const app = new Hono()
|
||||
@@ -43,39 +87,43 @@ const app = new Hono()
|
||||
// 注册CORS中间件
|
||||
app.use('/*', cors())
|
||||
|
||||
// 创建Socket.IO实例
|
||||
const io = new Server({
|
||||
cors: {
|
||||
origin: "*",
|
||||
methods: ["GET", "POST"],
|
||||
credentials: true
|
||||
}
|
||||
})
|
||||
|
||||
setupSocketIO({io, auth, apiClient});
|
||||
|
||||
// 动态加载并运行模板
|
||||
const runTemplate = async () => {
|
||||
const runTemplate = () => {
|
||||
try {
|
||||
// 创建基础app实例
|
||||
const moduleApp = new Hono()
|
||||
|
||||
// 初始化API Client
|
||||
// 注意:WORKSPACE_KEY 需要在 多八多(www.d8d.fun) 平台注册并开通工作空间后获取
|
||||
const workspaceKey = Deno.env.get('WORKSPACE_KEY') || ''
|
||||
if (!workspaceKey) {
|
||||
console.warn('未设置WORKSPACE_KEY,请前往 多八多(www.d8d.fun) 注册并开通工作空间以获取密钥')
|
||||
}
|
||||
const apiClient = await getApiClient(workspaceKey)
|
||||
|
||||
// 导入模板主模块
|
||||
const templateModule = await import('./app.tsx')
|
||||
|
||||
if (templateModule.default) {
|
||||
// 传入必要参数并初始化应用
|
||||
const appInstance = templateModule.default({
|
||||
apiClient: apiClient,
|
||||
app: moduleApp,
|
||||
moduleDir: './'
|
||||
})
|
||||
|
||||
// 启动服务器
|
||||
Deno.serve({ port: 8080 }, appInstance.fetch)
|
||||
console.log('应用已启动,监听端口: 8080')
|
||||
}
|
||||
|
||||
// 传入必要参数并初始化应用
|
||||
const appInstance = httpServer({
|
||||
apiClient: apiClient,
|
||||
app: moduleApp,
|
||||
moduleDir: './',
|
||||
auth
|
||||
})
|
||||
// 启动服务器
|
||||
Deno.serve({
|
||||
handler: io.handler(async (req) => {
|
||||
return await appInstance.fetch(req) || new Response(null, { status: 404 });
|
||||
}),
|
||||
port: 8080
|
||||
})
|
||||
|
||||
console.log('应用已启动,监听端口: 8080')
|
||||
} catch (error) {
|
||||
console.error('模板加载失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 执行模板
|
||||
runTemplate()
|
||||
runTemplate()
|
||||
Reference in New Issue
Block a user