ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

两周速成交付:Go + Vue3 + Cursor 复刻 B 站(cakecake),弹幕/私信/AI 助手全链路

两周速成交付:Go + Vue3 + Cursor 复刻 B 站(cakecake),弹幕/私信/AI 助手全链路 1. 为什么我选择用 Go Vue3 Cursor 复刻 B 站 cakecake如果你正在找一个能同时练到 Go 后端、Vue3 前端、WebSocket 实时通信和 AI 助手接入的全栈项目cakecake 这个仿 B 站视频社区是个很合适的目标。它把视频投稿、实时弹幕、私信、全文搜索、AI 问答这些真实业务场景都串起来了不是那种只做个 CRUD 的玩具项目。我实测下来用 Cursor 辅助写代码两周内跑通弹幕、私信、AI 助手三条链路是可行的前提是配置和接口约定要提前定好。cakecake 的技术栈是 Go 1.25 Gin GORM Vue 3.5 Vite 6 TypeScript MySQL Redis RabbitMQ Elasticsearch FFmpeg WebSocket。听起来多但核心链路其实就三块弹幕走 WebSocket 长连接私信走 WebSocket 离线消息落库AI 助手走 HTTP 流式接口对接大模型。你不需要一次性把所有中间件都装齐先跑通 MySQL Redis WebSocket 就能看到弹幕飞起来。适合谁跟做有 Go 基础语法、Vue3 组合式 API 用过一点、想通过一个完整项目把前后端串起来的同学。如果你完全没写过 WebSocket也没关系我会把关键代码和配置都贴出来你复制改改就能跑。Cursor 在这里的角色是帮你补全重复代码、生成类型定义、排查报错而不是替你理解业务。我踩过的坑是一开始让 Cursor 直接生成整个弹幕模块结果它把消息广播写成了单播调了半天才发现。所以规则文件要先约束好。这篇会按「环境准备 → Cursor 规则配置 → Go 后端接口 → Vue3 前端组件 → 联调验证 → 报错排查」的顺序走每一步都给可复制的代码和命令。TaoToken 在这里的作用是统一 AI 助手的 Key 和 API 通道你不用在多个模型平台之间来回切换一个 Key 就能调通对话接口。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 后面配置 AI 助手时会用到。2. TaoToken 前置准备统一 Key 与 API 通道接入 cakecake AI 助手cakecake 的 AI 助手模块原本对接的是 DeepSeek但实际开发中你可能会想换模型、做对比或者团队里多人共用一套额度。这时候如果每个模型都单独申请 Key、单独改代码维护成本很高。TaoToken 的思路是提供一个统一的 API 通道你用同一个 Key 和 Base URL 就能调用不同模型代码里只需要改 Model ID 这一个参数。对 cakecake 这种需要快速迭代 AI 助手的项目来说省去了反复改配置的麻烦。先说清楚它是什么TaoToken 是一个大模型 API 聚合通道兼容 OpenAI 风格的接口格式。你拿到 Key 之后把 Base URL 指向 https://taotoken.net/api 然后在请求体里指定模型名就能发起对话。它不改变你原有的调用逻辑只是把「找多个平台、管多个 Key」这件事收敛成一个入口。适合谁适合正在做 AI 功能原型、需要频繁切换模型对比效果、或者不想在 Key 管理上花太多时间的开发者。接入步骤很直接。第一步打开 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 创建 API Key复制保存好后面 Go 后端的环境变量里要用。第二步确认你要用的模型 ID比如 deepseek-chat、gpt-4o-mini 这类具体以文档为准文档地址在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。第三步在 cakecake 后端的 .env 里加两个变量TAOTOKEN_API_KEY 和 TAOTOKEN_BASE_URLBase URL 填 https://taotoken.net/api 。这里有个细节要注意TaoToken 的接口路径是 /v1/chat/completions所以你在 Go 代码里拼接的时候Base URL 后面要带上 /v1。我一开始只写了 https://taotoken.net/api 结果请求 404后来在文档里看到完整路径才改对。另外如果你用的是流式输出AI 助手打字机效果请求体里要加 stream: trueGo 侧用 bufio.Scanner 逐行读 SSE 数据。这部分代码在下一节会给。还有一点TaoToken 的 Key 不要硬编码在前端一定要放在 Go 后端的服务端环境变量里前端只调你自己的后端接口。这样既安全也方便你做请求频率控制和日志记录。如果你后面要上 Coding Plan 做长期编码辅助可以看 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 不过 cakecake 项目本身用按量计费的 API Key 就够了。3. 可复制配置Cursor 规则文件与 Go/Vue3 环境变量这一节直接给可复制的配置文件你照着建文件、填内容就行。先说 Cursor 的规则文件。Cursor 支持在项目根目录放 .cursorrules 文件用来约束 AI 生成代码的风格和边界。cakecake 是前后端分离的我建议在根目录放一份总规则然后在 backend 和 frontend 目录各放一份细分规则。总规则内容如下你直接复制到 .cursorrules # cakecake 项目规则 - 后端使用 Go 1.25 Gin GORM目录结构main.go 入口internal/handler 放 HTTP 处理internal/service 放业务逻辑internal/model 放数据模型。 - 前端使用 Vue 3.5 Vite 6 TypeScript组件放 src/components页面放 src/viewsAPI 请求统一走 src/api 目录下的封装。 - WebSocket 消息格式统一为 JSON{type:danmaku|private_msg,payload:{...}}type 字段必填。 - 弹幕广播必须用房间维度禁止全局广播。私信必须落库后再推送。 - AI 助手接口走服务端代理前端不直接持有任何模型 API Key。 - 所有新增接口必须在 docs/api.md 里补一行说明。这份规则的作用是让 Cursor 在补全代码时知道项目结构不会把 handler 写到 main.go 里也不会把弹幕写成全局广播。我实测下来加了这份规则之后Cursor 生成的 WebSocket 代码一次通过率明显提高。如果你用的是 Cursor 的 Composer 功能可以在对话里 这个文件让它按规则生成。接下来是后端环境变量。在 cakecake 后端根目录复制 .env.example 为 .env 然后填入以下内容# 数据库 DB_HOST127.0.0.1 DB_PORT3306 DB_USERroot DB_PASSWORDyour_password DB_NAMEcakecake # Redis REDIS_ADDR127.0.0.1:6379 REDIS_PASSWORD # JWT JWT_SECRETyour_jwt_secret_change_me # TaoToken AI 助手 TAOTOKEN_API_KEYsk-你的Key TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_MODELdeepseek-chat # 服务端口 SERVER_PORT8080前端环境变量在 cakecake-vue/bilibili-vue 目录下复制 .env.example 为 .env VITE_API_BASE_URLhttp://localhost:8080/api VITE_WS_URLws://localhost:8080/ws这两个文件建好之后后端 go mod tidy go run main.go 前端 npm install npm run dev 访问 http://localhost:8888 就能看到首页。如果数据库还没建先执行建库语句CREATE DATABASE cakecake DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;然后 GORM 的 AutoMigrate 会自动建表。这里有个坑MySQL 8.0 默认认证插件是 caching_sha2_passwordGo 的驱动有时候连不上你可以在 MySQL 配置里改成 mysql_native_password或者升级驱动版本。我遇到的是连接超时后来发现是 DB_HOST 写成了 localhost 但 MySQL 只监听 127.0.0.1改成 127.0.0.1 就好了。Cursor 规则和两份 .env 都配好之后你的项目骨架就具备了。下一节开始写弹幕和私信的 Go 后端接口。4. Go 后端接口弹幕 WebSocket 与私信落库实现弹幕是 cakecake 最核心的实时功能用 WebSocket 实现。Go 侧我用的是 gorilla/websocket 库先装依赖go get github.com/gorilla/websocket然后在 internal/handler 下新建 danmaku.go 核心逻辑是维护一个房间维度的连接池。每个视频房间对应一个 HubHub 里存所有连接收到弹幕消息就广播给同房间的所有连接。代码骨架如下package handler import ( encoding/json log net/http sync github.com/gin-gonic/gin github.com/gorilla/websocket ) type DanmakuMsg struct { Type string json:type Payload struct { VideoID int64 json:video_id Content string json:content Color string json:color UserID int64 json:user_id } json:payload } type Hub struct { mu sync.RWMutex rooms map[int64]map[*websocket.Conn]bool } var hub Hub{rooms: make(map[int64]map[*websocket.Conn]bool)} var upgrader websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } func DanmakuWS(c *gin.Context) { conn, err : upgrader.Upgrade(c.Writer, c.Request, nil) if err ! nil { log.Println(upgrade err:, err) return } defer conn.Close() var videoID int64 for { _, data, err : conn.ReadMessage() if err ! nil { hub.remove(videoID, conn) return } var msg DanmakuMsg if err : json.Unmarshal(data, msg); err ! nil { continue } if msg.Type danmaku { videoID msg.Payload.VideoID hub.add(videoID, conn) hub.broadcast(videoID, data) } } } func (h *Hub) add(videoID int64, conn *websocket.Conn) { h.mu.Lock() defer h.mu.Unlock() if h.rooms[videoID] nil { h.rooms[videoID] make(map[*websocket.Conn]bool) } h.rooms[videoID][conn] true } func (h *Hub) remove(videoID int64, conn *websocket.Conn) { h.mu.Lock() defer h.mu.Unlock() if h.rooms[videoID] ! nil { delete(h.rooms[videoID], conn) } } func (h *Hub) broadcast(videoID int64, data []byte) { h.mu.RLock() defer h.mu.RUnlock() for conn : range h.rooms[videoID] { conn.WriteMessage(websocket.TextMessage, data) } }这段代码的关键点是 videoID 从第一条消息里取之后这个连接就绑定到该房间。广播时只发给同房间的连接不会串台。你可以在 main.go 里注册路由r.GET(/ws/danmaku, handler.DanmakuWS)私信模块比弹幕多一步落库。用户 A 给用户 B 发消息先写 MySQL再查 B 是否在线在线就通过 WebSocket 推送不在线就等 B 上线后拉取离线消息。私信的消息结构体里要有 from_user_id、to_user_id、content、created_at。落库用 GORMtype PrivateMessage struct { ID int64 gorm:primaryKey FromUserID int64 gorm:index ToUserID int64 gorm:index Content string gorm:type:text IsRead bool gorm:default:false CreatedAt int64 gorm:autoCreateTime }发送接口用 POST /api/message/send 收到请求后先 db.Create(msg) 然后查 Redis 里有没有 to_user_id 的连接标识有就推。这里我用 Redis 存了 user_id 到 conn_id 的映射因为 WebSocket 连接可能分布在多个实例上单机的话直接用内存 map 也行。AI 助手的接口单独走 HTTP在 internal/handler/ai.go 里用 http.Client 调 TaoTokenfunc AIAssistant(c *gin.Context) { var req struct { Question string json:question } if err : c.ShouldBindJSON(req); err ! nil { c.JSON(400, gin.H{error: invalid request}) return } body : map[string]interface{}{ model: os.Getenv(TAOTOKEN_MODEL), messages: []map[string]string{ {role: user, content: req.Question}, }, stream: false, } jsonBody, _ : json.Marshal(body) httpReq, _ : http.NewRequest(POST, os.Getenv(TAOTOKEN_BASE_URL)/v1/chat/completions, bytes.NewBuffer(jsonBody)) httpReq.Header.Set(Content-Type, application/json) httpReq.Header.Set(Authorization, Bearer os.Getenv(TAOTOKEN_API_KEY)) resp, err : http.DefaultClient.Do(httpReq) if err ! nil { c.JSON(500, gin.H{error: err.Error()}) return } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(result) c.JSON(200, result) }注意 Base URL 后面拼的是 /v1/chat/completions 这是 TaoToken 兼容 OpenAI 格式的标准路径。如果你要流式输出把 stream 改成 true然后用 c.Stream 逐块转发。后端接口写完后用 curl 测一下 AI 助手curl -X POST http://localhost:8080/api/ai/ask \ -H Content-Type: application/json \ -d {question:cakecake 的弹幕是怎么实现的}返回里有 choices 字段就说明通了。下一节写 Vue3 前端组件。5. Vue3 前端组件弹幕渲染、私信面板与 AI 助手对接前端我用 Vue 3.5 的组合式 API 写弹幕组件放在 src/components/Danmaku.vue 。核心逻辑是建立 WebSocket 连接收到消息后创建弹幕元素用 CSS 动画让它从右往左飘。先看 WebSocket 封装放在 src/api/ws.ts export function createDanmakuWS(videoId: number, onMessage: (msg: any) void) { const ws new WebSocket(${import.meta.env.VITE_WS_URL}/danmaku) ws.onopen () { ws.send(JSON.stringify({ type: danmaku, payload: { video_id: videoId, content: , color: #fff, user_id: 0 } })) } ws.onmessage (e) { const msg JSON.parse(e.data) if (msg.type danmaku) onMessage(msg.payload) } return ws }这里有个细节连接建立后先发一条空弹幕目的是让后端把这条连接绑定到 videoId 房间。之后收到的消息就都是这个房间的了。Danmaku.vue 里维护一个数组每条弹幕有 id、content、color、top 偏移量用 v-for 渲染template div classdanmaku-layer div v-foritem in danmakuList :keyitem.id classdanmaku-item :style{ top: item.top px, color: item.color } {{ item.content }} /div /div /template script setup langts import { ref, onMounted, onUnmounted } from vue import { createDanmakuWS } from /api/ws const props defineProps{ videoId: number }() const danmakuList refany[]([]) let ws: WebSocket | null null onMounted(() { ws createDanmakuWS(props.videoId, (payload) { const id Date.now() Math.random() danmakuList.value.push({ id, content: payload.content, color: payload.color || #fff, top: Math.floor(Math.random() * 300) }) setTimeout(() { danmakuList.value danmakuList.value.filter(d d.id ! id) }, 8000) }) }) onUnmounted(() ws?.close()) /script style scoped .danmaku-layer { position: absolute; inset: 0; pointer-events: none; overflow: hidden; } .danmaku-item { position: absolute; right: -200px; white-space: nowrap; animation: move-left 8s linear forwards; font-size: 18px; } keyframes move-left { to { transform: translateX(-120vw); } } /style私信面板放在 src/components/MessagePanel.vue 逻辑是拉取历史消息 监听 WebSocket 推送。历史消息走 GET /api/message/list?to_user_idxxx 新消息走 WebSocket 的 private_msg 类型。AI 助手组件放在 src/components/AIAssistant.vue 调后端 /api/ai/ask 接口async function askAI() { const res await fetch(${import.meta.env.VITE_API_BASE_URL}/ai/ask, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ question: input.value }) }) const data await res.json() answer.value data.choices?.[0]?.message?.content || 无回复 }注意前端不直接调 TaoToken所有 AI 请求都经过 Go 后端代理。这样 Key 不会暴露在浏览器里。如果你要打字机效果后端改成流式前端用 ReadableStream 读。三个组件写完后在视频播放页引入 Danmaku在消息中心引入 MessagePanel在侧边栏引入 AIAssistant。启动前端 npm run dev 打开两个浏览器窗口一个发弹幕一个看能同步飘出来就说明 WebSocket 通了。6. 联调验证与常见报错排查401、proxy failed、choices 为空联调阶段最容易卡在几个固定报错上我按实际遇到的顺序列出来你对照排查。第一个是 401 Unauthorized。如果你在调 AI 助手时看到这个先检查 Go 后端 .env 里的 TAOTOKEN_API_KEY 有没有填对注意不要有多余空格。然后确认请求头是 Authorization: Bearer sk-xxx Bearer 后面有一个空格。如果 Key 是对的还报 401可能是 Key 被禁用或额度用完去 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 看一下状态。第二个是 local proxy failed 或 connection refused。这个通常出现在前端连 WebSocket 的时候VITE_WS_URL 写成了 ws://localhost:8080/ws 但后端注册的路由是 /ws/danmaku 路径不匹配。检查 main.go 里的 r.GET 路径和前端拼接的路径是否一致。另外如果你用了 Vite 的 proxy 配置确认 proxy 里 ws: true 开了。第三个是 reading choices 报错或 choices 为空。这说明请求发出去了但返回体里没有 choices 字段。常见原因是模型 ID 写错了比如把 deepseek-chat 写成了 deepseek 或者 Base URL 少了 /v1 。你可以先用 curl 直接测 TaoToken 接口确认返回结构curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-你的Key \ -d {model:deepseek-chat,messages:[{role:user,content:hi}]}如果 curl 通了但 Go 里不通检查 Go 的 http.NewRequest 里 URL 拼接有没有多斜杠或漏斜杠。我遇到过一次是 Base URL 结尾带了 / 代码里又拼了 /v1 结果变成 //v1 服务器返回 404。第四个是 OAuth 相关报错。如果你在 Cursor 里配置了 Claude Code 或 Codex 的认证可能会看到 OAuth token expired。这个跟 cakecake 项目本身无关是 Cursor 插件的认证问题。你可以在 Cursor 设置里重新登录或者检查 ~/.cursor/ 下的配置文件。如果你用的是 CC Switch 或 Cline MCP记得把 Base URL、Key、Model ID 三件套都填全缺一个都会报认证失败。第五个是弹幕不显示但 WebSocket 显示已连接。这种情况多半是消息格式对不上。后端广播的是 {type:danmaku,payload:{...}} 前端解析时如果直接取 msg.content 就会 undefined。检查前端 onMessage 里有没有先取 msg.payload 。另外弹幕的 CSS 动画如果被父容器 overflow: hidden 裁掉了也会看不到确认 danmaku-layer 的定位是 absolute 且父容器 relative。排查顺序建议先 curl 测后端接口再用浏览器 Network 面板看 WebSocket 帧最后看前端控制台报错。三层都确认一遍基本能定位到问题。如果 AI 助手一直超时检查服务器能不能访问 https://taotoken.net/api 有些内网环境需要配代理但注意不要用违规的网络工具直接用服务器本身的出网能力即可。7. 从跑通到交付把 cakecake 链路收尾的实用建议两周速成交付的关键不是把每个功能做到完美而是先把三条链路跑通再补细节。我的建议是第一周搞定环境、数据库、弹幕 WebSocket 和视频播放页第二周搞私信落库和 AI 助手接入最后两天做联调和部署。Cursor 在这个过程中主要帮你写重复的 CRUD 和类型定义业务逻辑还是得自己理清楚。部署的时候后端用 go build 打包成二进制前端 npm run build 出静态文件用 Nginx 托管。WebSocket 需要在 Nginx 里加 Upgrade 和 Connection 头转发。AI 助手的 Key 放在服务器环境变量里不要提交到 Git。如果你要长期做编码辅助可以看看 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 它适合需要频繁调用模型的场景。最后说一个实际经验弹幕的敏感词过滤不要放在前端做前端过滤容易被绕过放在 Go 后端广播之前用 Trie 树过滤一遍成本很低但效果好。私信的已读未读状态用 Redis 存比每次查 MySQL 快很多。AI 助手的回答如果要做结构化展示让模型返回 JSON 格式后端解析后再给前端这样前端渲染工具结果会方便很多。这些细节做完cakecake 的完整链路就真正交付了。
返回列表