Browse Source

Merge branch 'fork' of 1030-6/d8d-admin-mobile-starter-public into main

d8dfun 6 months ago
parent
commit
9587c5293c

+ 4 - 0
HISTORY.md

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

+ 70 - 14
client/admin/pages_messages.tsx

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

+ 72 - 2
client/mobile/pages_messages.tsx

@@ -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'],

+ 3 - 1
deno.json

@@ -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 - 0
deno.lock

@@ -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 - 0
docs/message-system-architecture.md

@@ -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 - 标记消息为已读
+
+### 权限控制
+- 系统消息: 仅管理员可发送
+- 公告: 管理员和特定角色可发送
+- 私信: 所有用户可发送

+ 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);
+  });
+}

+ 324 - 0
server/routes_io_messages.ts

@@ -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', '标记消息为已读失败');
+    }
+  });
+}

+ 74 - 26
server/run_app.ts

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