Skip to content

架构 ​

整体长什么样 ​

┌─────────────┐
│   浏览器     │  或桌面客户端
└──────┬──────┘
       │
       ├──── HTTPS ─────► Go 应用 :8998 ──► 数据库
       │                    │
       ├──── WebSocket ─────┘  (信令:谁进谁出、谁在说话、新消息)
       │
       └──── WebRTC ──────► LiveKit :7880/:7881/:7882  (音频)

三块东西,各干各的:

  • Go 应用:业务逻辑、权限、数据存储,外加一个 WebSocket 信令服务
  • LiveKit:只负责音频转发,不碰业务
  • 数据库:SQLite / PostgreSQL / MySQL

音频不经过 Go 应用。 这是整个设计里最重要的一点——应用不会被音频流量拖垮,两者可以独立扩容。

为什么这么拆 ​

如果音频也走应用:

  • 一个 20 人房间,每人说一句话要复制 19 份转发,应用直接跪
  • 音频卡顿会拖累所有 HTTP 请求
  • 想扩容就得把业务和媒体绑在一起扩

拆开之后,应用只需要:签个令牌、告诉前端连哪、在成员进出时更新状态。

后端分层 ​

请求
 │
 ├─ Router           路由匹配
 ├─ Middleware       验令牌、查权限、限流
 ├─ Handler          解析参数、调 service、返回 JSON
 ├─ Service          业务逻辑,唯一有权限返回 AppError 的层
 ├─ Repository       数据访问
 └─ DB

规则很死:

  • 每层只能调下一层,不能跨层
  • Service 返回 *AppError,Handler 决定怎么转成响应
  • Handler 里不写业务逻辑,Repository 里不写业务规则

这样改一个地方不会牵连一片。

目录结构 ​

app/
├── server/                  Go 后端
│   ├── main.go              入口(cobra)
│   ├── server/gin.go        依赖注入,把所有层组装起来
│   └── internal/
│       ├── config/          环境变量
│       ├── model/           GORM 实体
│       ├── repository/      数据访问
│       ├── service/         业务逻辑
│       ├── handler/         HTTP 控制器
│       ├── middleware/      认证、权限、限流
│       ├── router/          路由注册(按模块分组)
│       ├── signal/          WebSocket 信令
│       ├── sfu/             语音服务抽象层
│       ├── storage/         对象存储抽象
│       ├── authstate/       令牌黑名单、密钥轮换
│       └── pkg/             错误、响应、JWT、OAuth
├── web/                     React 19 前端
│   └── src/
│       ├── pages/           文件路由,自动生成 routeTree
│       ├── api/             HTTP 客户端
│       ├── socket/          WebSocket 客户端
│       ├── stores/          zustand 状态
│       ├── components/      业务组件
│       ├── layouts/         布局
│       └── hooks/           React hooks
├── desktop/                 桌面端宿主(Go + MyGo)
└── docs/                    这个文档站

packages/
├── components/              UI 组件库(Radix + Tailwind)
├── sfu-client/              前端语音客户端抽象
└── desktop/                 桌面能力 SDK

语音抽象层 ​

代码里有个 sfu.Provider 接口,把语音服务的能力抽象出来:

go
type Provider interface {
    GenerateToken(room, identity string) (string, error)
    ListRooms() ([]RoomSummary, error)
    ListParticipants(room string) ([]ParticipantSummary, error)
    MuteParticipant(room, identity, trackSid string, muted bool) error
    RemoveParticipant(room, identity string) error
    DeleteRoom(room string) error
    GetHost() string
}

当前只有 LiveKit 一个实现。接口存在的意义是:业务层不关心底层是谁,将来接别的服务不用改业务代码。

sfu.NewDynamicProvider() 会在运行时解析当前用哪个实现——支持在管理后台切换而不重启。

数据模型 ​

启动时 GORM 自动建表、加字段。不用手动跑 SQL。

用户与权限 ​

表存什么
users用户账号,含角色、令牌版本、是否访客
roles角色(admin / user / ban)
permissions权限码定义
role_permissions角色和权限的关联
user_groups用户组

域与房间 ​

表存什么
domains语音域,含访客开关
domain_members域的成员关系
domain_roles域内角色
domain_role_permissions域角色的权限
domain_guest_bans访客封禁列表
rooms频道/房间

消息 ​

表存什么
messages消息,软删除
message_reactions表情反应
message_mentions@ 提及
conversation_participants私聊会话参与人

其他 ​

表存什么
mutes禁言记录
audit_logs审计日志
oauth_providers / oauth_accounts第三方登录配置与绑定
email_configs / email_verification_codes邮件配置与验证码
storage_configs存储配置(密钥加密存)
sfu_configs / sfu_active_providers语音服务配置

状态存在哪 ​

东西位置为什么
用户、消息、房间数据库要持久
登录令牌黑名单进程内 NATS KV高频读写,过期自动清
房间成员状态进程内 NATS KV + 内存实时性要求高,不需要落盘
前端界面状态localStorage / IndexedDB主题、音量偏好这些
上传的文件本机磁盘或对象存储体积大

NATS 是进程内嵌的,不是外部服务。它承担内部事件分发、KV 状态、异步任务队列,随应用启动,不用也不该单独部署。

前端架构 ​

React 19 + TanStack Router(文件路由)+ TanStack Query(服务端状态)+ zustand(客户端状态)。

状态管理分两类:

  • 服务端状态(房间列表、消息、用户资料)→ TanStack Query,自带缓存和失效
  • 客户端状态(主题、当前选中、音量偏好)→ zustand,useXxxStore 订阅

错误处理有三层边界:

层组件管什么
组件ErrorBoundary某个小组件崩了,不影响整页
页面PageErrorBoundary整个页面崩了,能重试
路由RouteErrorComponent路由级兜底

异步错误(Promise、事件回调里的)由 useErrorHandler 重抛到渲染阶段,交给最近的边界。

桌面端 ​

桌面端是独立的一套:Go + MyGo 写的原生宿主,把同一份前端产物装进系统 WebView。

桌面窗口 ──► 127.0.0.1:随机端口(本地代理)
                    ├─ 前端(内嵌)
                    ├─ /api  ──► 你配的服务器
                    ├─ /ws   ──► 你配的服务器
                    └─ /uploads ──► 你配的服务器

起本地代理是为了让页面和接口同源——没有跨域问题,Cookie 也能正常用。

前端代码用编译期常量区分平台:桌面独享的模块(托盘、浮窗)在 Web 构建时被整体剔除,不进包。

部署形态 ​

             公网
               │
          ┌────▼────┐
          │  Nginx  │  HTTPS 终结、反代
          └────┬────┘
               │
     ┌─────────┼──────────┐
     │         │          │
┌────▼────┐ ┌──▼──────┐ ┌─▼──────┐
│ Go 应用 │ │ LiveKit │ │ 数据库  │
└─────────┘ └─────────┘ └────────┘
                 ▲
                 │ WebRTC(客户端直连,不经 Nginx)
                 │
            浏览器/桌面端

小规模可以全塞一台机器。规模上去之后,LiveKit 单独一台、数据库单独一台。

技术选型 ​

层用了什么为什么
后端语言Go单文件部署、并发好、编译快
HTTP 框架Gin生态成熟、中间件机制清晰
ORMGORM自动迁移,三种数据库通用
数据库SQLite / PostgreSQL / MySQL渐进式,从小到大都覆盖
事件总线内嵌 NATS不引入外部依赖,KV + 队列都有
语音LiveKit功能完整,自建和云服务都能用
前端React 19生态最大,招人好招
路由TanStack Router文件路由 + 类型安全
状态zustand比 Redux 简单,够用
样式Tailwind 4不用维护 CSS 文件
组件Radix + 自建无样式、可访问性好
构建rolldown-vite比 vite 快
桌面MyGo纯 Go,无 cgo,产物小

下一步 ​

最后更新:

基于 Apache 2.0 许可开源