From ec3f76e0c0bc49dbd5be8fec5ff1b7da73cf0fd7 Mon Sep 17 00:00:00 2001 From: loveuer Date: Wed, 14 May 2025 17:48:06 +0800 Subject: [PATCH] =?UTF-8?q?wip:=200.2.0=20=20=201.=20websocket=20=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5,=E9=80=80=E5=87=BA,=E6=B6=88=E6=81=AF=20=20=202.=20?= =?UTF-8?q?=E5=9F=BA=E6=9C=AC=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/main.tsx | 10 +- frontend/src/page/local.tsx | 227 ++++++++++++++++++++++++++++++++++++ frontend/src/store/ws.ts | 74 ++++++++++++ frontend/vite.config.ts | 5 + go.mod | 2 + go.sum | 4 + internal/api/api.go | 5 + internal/controller/room.go | 172 +++++++++++++++++++++++++++ internal/handler/local.go | 35 ++++++ internal/pkg/tool/ip.go | 59 ++++++++++ internal/pkg/tool/random.go | 31 ++++- main.go | 1 + page/bubble.html | 149 +++++++++++++++++++++++ 13 files changed, 765 insertions(+), 9 deletions(-) create mode 100644 frontend/src/page/local.tsx create mode 100644 frontend/src/store/ws.ts create mode 100644 internal/controller/room.go create mode 100644 internal/handler/local.go create mode 100644 internal/pkg/tool/ip.go create mode 100644 page/bubble.html diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 966aef1..43eb85c 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,19 +1,21 @@ -import { StrictMode } from 'react' +// import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import {createBrowserRouter, RouterProvider} from "react-router-dom"; import {Login} from "./page/login.tsx"; import {FileSharing} from "./page/share.tsx"; +import {LocalSharing} from "./page/local.tsx"; const container = document.getElementById('root') const root = createRoot(container!) const router = createBrowserRouter([ {path: "/login", element: }, - {path: "*", element: }, + {path: "/share", element: }, + {path: "*", element: }, ]) root.render( - + // - , + // , ) diff --git a/frontend/src/page/local.tsx b/frontend/src/page/local.tsx new file mode 100644 index 0000000..288c441 --- /dev/null +++ b/frontend/src/page/local.tsx @@ -0,0 +1,227 @@ +import {CloudBackground} from "../component/fluid/cloud.tsx"; +import {useEffect, useState} from "react"; +import {createUseStyles} from "react-jss"; +import {useRoom} from "../store/ws.ts"; + +const useClass = createUseStyles({ + '@global': { + '@keyframes emerge': { + '0%': { + transform: 'scale(0) translate(-50%, -50%)', + opacity: 0 + }, + '80%': { + transform: 'scale(1.1) translate(-50%, -50%)', + opacity: 1 + }, + '100%': { + transform: 'scale(1) translate(-50%, -50%)', + opacity: 1 + } + } + }, + container: { + margin: "0", + height: "100vh", + // background: "linear-gradient(45deg, #e6e9f0, #eef1f5)", + overflow: "hidden", + position: "relative", + }, + bubble: { + position: "absolute", + width: "100px", + height: "100px", + borderRadius: "50%", + display: "flex", + justifyContent: "center", + alignItems: "center", + textAlign: "center", + cursor: "pointer", + fontFamily: "'Microsoft Yahei', sans-serif", + fontSize: "14px", + color: "rgba(255, 255, 255, 0.9)", + textShadow: "1px 1px 3px rgba(0,0,0,0.3)", + transition: "transform 0.3s ease", + transform: 'translate(-50%, -50%)', + animation: 'emerge 0.5s ease-out forwards,float 6s 0.5s ease-in-out infinite', + background: "radial-gradient(circle at 30% 30%,rgba(255, 255, 255, 0.8) 10%,rgba(255, 255, 255, 0.3) 50%,transparent 100%)", + border: "2px solid rgba(255, 255, 255, 0.5)", + boxShadow: "inset 0 -5px 15px rgba(255,255,255,0.3),0 5px 15px rgba(0,0,0,0.1)", + } +}) + +interface Client { + id: string; + name: string; +} + +interface BubblePosition { + id: string; + x: number; + y: number; + color: string; + radius: number; // 新增半径属性 + angle: number; // 新增角度属性 +} + +export const LocalSharing: React.FC = () => { + const classes = useClass(); + const [clients, setClients] = useState([]); + const [bubbles, setBubbles] = useState([]); + const {register, cleanup} = useRoom(); + const BUBBLE_SIZE = 100; + + // 生成随机颜色 + const generateColor = () => { + const hue = Math.random() * 360; + return `hsla(${hue}, + ${Math.random() * 30 + 40}%, + ${Math.random() * 10 + 75}%, 0.9)`; + }; + + // 防碰撞位置生成 + const generatePosition = (existing: BubblePosition[]) => { + const centerX = window.innerWidth / 2; + const centerY = window.innerHeight / 2; + const maxRadius = Math.min(centerX, centerY) - BUBBLE_SIZE; + + // 初始化参数 + let radius = 0; + let angle = Math.random() * Math.PI * 2; + let attempts = 0; + + do { + // 极坐标转笛卡尔坐标 + const x = centerX + radius * Math.cos(angle); + const y = centerY + radius * Math.sin(angle); + + // 边界检测 + if (x < 0 || x > window.innerWidth - BUBBLE_SIZE || + y < 0 || y > window.innerHeight - BUBBLE_SIZE) { + radius = 0; + angle += Math.PI / 6; + continue; + } + + // 碰撞检测 + const collision = existing.some(bubble => { + const distance = Math.sqrt( + Math.pow(bubble.x - x, 2) + + Math.pow(bubble.y - y, 2) + ); + return distance < BUBBLE_SIZE * 1.5; + }); + + if (!collision) { + return { + x, + y, + radius, + angle + }; + } + + // 逐步扩大搜索半径和角度 + radius += BUBBLE_SIZE * 0.7; + if (radius > maxRadius) { + radius = 0; + angle += Math.PI / 6; // 每30度尝试一次 + } + + attempts++; + } while (attempts < 200); + + return null; + }; + + // 修改updateBubbles中的生成逻辑 + const updateBubbles = (newClients: Client[]) => { + const newBubbles: BubblePosition[] = []; + + newClients.forEach(client => { + const existing = bubbles.find(b => b.id === client.id); + if (existing) { + newBubbles.push(existing); + return; + } + + const position = generatePosition([...bubbles, ...newBubbles]); + if (position) { + newBubbles.push({ + id: client.id, + ...position, + color: generateColor() + }); + } + }); + + setBubbles(newBubbles); + }; + + useEffect(() => { + // 模拟API获取数据 + const fetchData = async () => { + // const response = await fetch('/api/clients'); + // const data = await response.json(); + + await register(); + + const mockData: Client[] = [ + { id: '1', name: '宁静的梦境' }, + { id: '2', name: '温暖的时光' }, + { id: '3', name: '甜蜜的旋律' }, + { id: '4', name: '柔和的花园' } + ]; + setClients(mockData); + updateBubbles(mockData); + + return () => cleanup(); + }; + fetchData(); + }, []); + + // 窗口尺寸变化处理 + useEffect(() => { + const handleResize = () => { + const validBubbles = bubbles.filter(bubble => + bubble.x <= window.innerWidth - BUBBLE_SIZE && + bubble.y <= window.innerHeight - BUBBLE_SIZE + ); + setBubbles(validBubbles); + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [bubbles]); + + // 气泡点击处理 + const handleBubbleClick = (id: string) => { + // 实际开发中这里调用API删除 + setClients(prev => prev.filter(c => c.id !== id)); + setBubbles(prev => prev.filter(b => b.id !== id)); + }; + + return
+ + {bubbles.map(bubble => { + const client = clients.find(c => c.id === bubble.id); + return client ? ( +
handleBubbleClick(bubble.id)} + > + {client.name} +
+ ) : null; + })} +
+} \ No newline at end of file diff --git a/frontend/src/store/ws.ts b/frontend/src/store/ws.ts new file mode 100644 index 0000000..5ea36a2 --- /dev/null +++ b/frontend/src/store/ws.ts @@ -0,0 +1,74 @@ +import { create } from 'zustand' + +type RoomState = { + conn: WebSocket | null + retryCount: number + reconnectTimer: number | null +} + +type RoomActions = { + register: () => Promise + cleanup: () => void +} + +const MAX_RETRY_DELAY = 30000 // 最大重试间隔30秒 +const NORMAL_CLOSE_CODE = 1000 // 正常关闭的状态码 + +export const useRoom = create()((set, get) => ({ + conn: null, + retryCount: 0, + reconnectTimer: null, + + register: async () => { + const { conn, reconnectTimer } = get() + + // 清理旧连接和定时器 + if (reconnectTimer) clearTimeout(reconnectTimer) + if (conn) conn.close() + + const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://' + const wsUrl = protocol + window.location.host + '/api/ulocal/registry' + const newConn = new WebSocket(wsUrl) + + newConn.onopen = () => { + console.log('WebSocket connected') + set({ conn: newConn, retryCount: 0 }) // 重置重试计数器 + } + + newConn.onerror = (error) => { + console.error('WebSocket error:', error) + } + + newConn.onmessage = (event) => { + console.log("[D] websocket message =", event) + } + + newConn.onclose = (event) => { + // 非正常关闭时触发重连 + if (event.code !== NORMAL_CLOSE_CODE) { + const { retryCount } = get() + const nextRetry = retryCount + 1 + const delay = Math.min(1000 * Math.pow(2, nextRetry), MAX_RETRY_DELAY) + + const timer = setTimeout(() => { + get().register() + }, delay) + + set({ + retryCount: nextRetry, + reconnectTimer: timer, + conn: null + }) + } + } + + set({ conn: newConn, reconnectTimer: null }) + }, + + cleanup: () => { + const { conn, reconnectTimer } = get() + if (reconnectTimer) clearTimeout(reconnectTimer) + if (conn) conn.close() + set({ conn: null, retryCount: 0, reconnectTimer: null }) + } +})) diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 8cca44f..6d09045 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -10,6 +10,11 @@ export default defineConfig({ target: 'http://127.0.0.1:9119', changeOrigin: true }, + '/api/ulocal/registry': { + target: 'ws://127.0.0.1:9119', + rewriteWsOrigin: true, + ws: true, + }, '/ushare': { target: 'http://127.0.0.1:9119', changeOrigin: true diff --git a/go.mod b/go.mod index 6c3e22a..a3c7de4 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jedib0t/go-pretty/v6 v6.6.7 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect @@ -30,6 +31,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mileusna/useragent v1.3.5 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect diff --git a/go.sum b/go.sum index f5da7c5..1d419f3 100644 --- a/go.sum +++ b/go.sum @@ -46,6 +46,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= @@ -70,6 +72,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws= +github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= diff --git a/internal/api/api.go b/internal/api/api.go index 3774e7c..46855be 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -23,6 +23,11 @@ func Start(ctx context.Context) <-chan struct{} { app.Post("/api/ushare/:code", handler.ShareUpload()) // 分片上传接口 app.Post("/api/uauth/login", handler.AuthLogin()) + { + api := app.Group("/api/ulocal") + api.Get("/registry", handler.LocalRegistry()) + } + ready := make(chan struct{}) ln, err := net.Listen("tcp", opt.Cfg.Address) if err != nil { diff --git a/internal/controller/room.go b/internal/controller/room.go new file mode 100644 index 0000000..3994119 --- /dev/null +++ b/internal/controller/room.go @@ -0,0 +1,172 @@ +// room controller: +// local share websocket room controller +// same remote ip as a +package controller + +import ( + "context" + "encoding/json" + "github.com/google/uuid" + "github.com/gorilla/websocket" + "github.com/loveuer/nf/nft/log" + "github.com/loveuer/ushare/internal/pkg/tool" + "github.com/mileusna/useragent" + "sync" + "time" +) + +type RoomClientType string + +const ( + ClientTypeDesktop RoomClientType = "desktop" + ClientTypeMobile RoomClientType = "mobile" + ClientTypeTablet RoomClientType = "tablet" +) + +type RoomAppType string + +const ( + RoomAppTypeWeb = "web" +) + +type roomClient struct { + controller *roomController + conn *websocket.Conn + clientType RoomClientType + appType RoomAppType + ip string + name string + id string + msgChan chan any +} + +func (rc *roomClient) start(ctx context.Context) { + go func() { + for { + select { + case <-ctx.Done(): + return + case msg, _ := <-rc.msgChan: + err := rc.conn.WriteJSON(msg) + log.Debug("RoomClient: write json message, ip = %s, id = %s, name = %s, err = %v", rc.ip, rc.id, rc.name, err) + if err != nil { + log.Error("RoomClient: write json message failed, ip = %s, id = %s, name = %s, err = %s", rc.ip, rc.id, rc.name, err.Error()) + } + default: + mt, bs, err := rc.conn.ReadMessage() + if err != nil { + log.Error("RoomClient: read message failed, ip = %s, id = %s, name = %s, err = %s", rc.ip, rc.id, rc.name, err.Error()) + rc.controller.Unregister(rc) + return + } + + switch mt { + case websocket.PingMessage: + rs, _ := json.Marshal(map[string]any{"type": "pong", "time": time.Now().UnixMilli(), "id": rc.id, "name": rc.name}) + if err := rc.conn.WriteMessage(websocket.PongMessage, rs); err != nil { + log.Error("RoomClient: response ping message failed, ip = %s, id = %s, name = %s, err = %s", rc.ip, rc.id, rc.name, err.Error()) + } + case websocket.CloseMessage: + log.Debug("RoomClient: received close message, unregister ip = %s id = %s, name = %s", rc.ip, rc.id, rc.name) + rc.controller.Unregister(rc) + return + case websocket.TextMessage: + log.Info("RoomClient: received text message, ip = %s, id = %s, name = %s, text = %s", rc.ip, rc.id, rc.name, string(bs)) + case websocket.BinaryMessage: + // todo + log.Info("RoomClient: received bytes message, ip = %s, id = %s, name = %s, text = %s", rc.ip, rc.id, rc.name, string(bs)) + } + } + } + }() +} + +type roomController struct { + sync.Mutex + ctx context.Context + rooms map[string]map[string]*roomClient // map[room_id(remote-ip)][id] +} + +var ( + RoomController = &roomController{ + rooms: make(map[string]map[string]*roomClient), + } +) + +func (rc *roomController) Start(ctx context.Context) { + rc.ctx = ctx + + go func() { + ticker := time.NewTicker(10 * time.Second) + for { + select { + case <-ctx.Done(): + return + case now := <-ticker.C: + for room := range rc.rooms { + rc.Broadcast(room, now.String()) + } + } + } + }() +} + +func (rc *roomController) Register(c *websocket.Conn, ip, userAgent string) { + nrc := &roomClient{ + controller: rc, + conn: c, + clientType: ClientTypeDesktop, + appType: RoomAppTypeWeb, + ip: ip, + id: uuid.Must(uuid.NewV7()).String(), + name: tool.RandomName(), + msgChan: make(chan any, 1), + } + + ua := useragent.Parse(userAgent) + switch { + case ua.Mobile: + nrc.clientType = ClientTypeMobile + case ua.Tablet: + nrc.clientType = ClientTypeTablet + } + + key := "local" + if !tool.IsPrivateIP(ip) { + key = ip + } + + rc.Lock() + + if _, ok := rc.rooms[key]; !ok { + rc.rooms[key] = make(map[string]*roomClient) + } + + nrc.start(rc.ctx) + log.Debug("controller.room: registry client, ip = %s(%s), id = %s, name = %s", key, nrc.ip, nrc.id, nrc.name) + rc.rooms[key][nrc.id] = nrc + + rc.Unlock() + + rc.Broadcast(key, "new member") +} + +func (rc *roomController) Broadcast(room string, msg any) { + for _, client := range rc.rooms[room] { + client.msgChan <- msg + } +} + +func (rc *roomController) Unregister(client *roomClient) { + key := "local" + if !tool.IsPrivateIP(client.ip) { + key = client.ip + } + + rc.Lock() + defer rc.Unlock() + + log.Debug("controller.room: unregister client, ip = %s(%s), id = %s, name = %s", client.ip, key, client.id, client.name) + + delete(rc.rooms[key], client.id) +} diff --git a/internal/handler/local.go b/internal/handler/local.go new file mode 100644 index 0000000..d7bc3c8 --- /dev/null +++ b/internal/handler/local.go @@ -0,0 +1,35 @@ +package handler + +import ( + "github.com/gorilla/websocket" + "github.com/loveuer/nf" + "github.com/loveuer/nf/nft/log" + "github.com/loveuer/ushare/internal/controller" + "net/http" +) + +func LocalRegistry() nf.HandlerFunc { + upgrader := websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return true + }, + } + + return func(c *nf.Ctx) error { + + ip := c.IP(true) + ua := c.Get("User-Agent") + + conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + log.Error("LocalRegistry: failed to upgrade websocket connection, err = %s", err.Error()) + return err + } + + controller.RoomController.Register(conn, ip, ua) + + return nil + } +} diff --git a/internal/pkg/tool/ip.go b/internal/pkg/tool/ip.go new file mode 100644 index 0000000..587cfbf --- /dev/null +++ b/internal/pkg/tool/ip.go @@ -0,0 +1,59 @@ +package tool + +import ( + "net" +) + +var ( + privateIPv4Blocks []*net.IPNet + privateIPv6Blocks []*net.IPNet +) + +func init() { + // IPv4私有地址段 + for _, cidr := range []string{ + "10.0.0.0/8", // A类私有地址 + "172.16.0.0/12", // B类私有地址 + "192.168.0.0/16", // C类私有地址 + "169.254.0.0/16", // 链路本地地址 + "127.0.0.0/8", // 环回地址 + } { + _, block, _ := net.ParseCIDR(cidr) + privateIPv4Blocks = append(privateIPv4Blocks, block) + } + + // IPv6私有地址段 + for _, cidr := range []string{ + "fc00::/7", // 唯一本地地址 + "fe80::/10", // 链路本地地址 + "::1/128", // 环回地址 + } { + _, block, _ := net.ParseCIDR(cidr) + privateIPv6Blocks = append(privateIPv6Blocks, block) + } +} + +func IsPrivateIP(ipStr string) bool { + ip := net.ParseIP(ipStr) + if ip == nil { + return false + } + + // 处理IPv4和IPv4映射的IPv6地址 + if ip4 := ip.To4(); ip4 != nil { + for _, block := range privateIPv4Blocks { + if block.Contains(ip4) { + return true + } + } + return false + } + + // 处理IPv6地址 + for _, block := range privateIPv6Blocks { + if block.Contains(ip) { + return true + } + } + return false +} diff --git a/internal/pkg/tool/random.go b/internal/pkg/tool/random.go index 266cb4c..a5f1b2d 100644 --- a/internal/pkg/tool/random.go +++ b/internal/pkg/tool/random.go @@ -3,14 +3,31 @@ package tool import ( "crypto/rand" "math/big" + mrand "math/rand" ) var ( - letters = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - letterNum = []byte("0123456789") - letterLow = []byte("abcdefghijklmnopqrstuvwxyz") - letterCap = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ") - letterSyb = []byte("!@#$%^&*()_+-=") + letters = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + letterNum = []byte("0123456789") + letterLow = []byte("abcdefghijklmnopqrstuvwxyz") + letterCap = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ") + letterSyb = []byte("!@#$%^&*()_+-=") + adjectives = []string{ + "开心的", "灿烂的", "温暖的", "阳光的", "活泼的", + "聪明的", "优雅的", "幸运的", "甜蜜的", "勇敢的", + "宁静的", "热情的", "温柔的", "幽默的", "坚强的", + "迷人的", "神奇的", "快乐的", "健康的", "自由的", + "梦幻的", "勤劳的", "真诚的", "浪漫的", "自信的", + } + + plants = []string{ + "苹果", "香蕉", "橘子", "葡萄", "草莓", + "西瓜", "樱桃", "菠萝", "柠檬", "蜜桃", + "蓝莓", "芒果", "石榴", "甜瓜", "雪梨", + "番茄", "南瓜", "土豆", "青椒", "洋葱", + "黄瓜", "萝卜", "豌豆", "玉米", "蘑菇", + "菠菜", "茄子", "芹菜", "莲藕", "西兰花", + } ) func RandomInt(max int64) int64 { @@ -52,3 +69,7 @@ func RandomPassword(length int, withSymbol bool) string { } return string(result) } + +func RandomName() string { + return adjectives[mrand.Intn(len(adjectives))] + plants[mrand.Intn(len(plants))] +} diff --git a/main.go b/main.go index 47c8044..d84ea02 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,7 @@ func main() { opt.Init(ctx) controller.UserManager.Start(ctx) controller.MetaManager.Start(ctx) + controller.RoomController.Start(ctx) api.Start(ctx) <-ctx.Done() diff --git a/page/bubble.html b/page/bubble.html new file mode 100644 index 0000000..1601bff --- /dev/null +++ b/page/bubble.html @@ -0,0 +1,149 @@ + + + + + 柔和气泡 + + + + + + \ No newline at end of file