# WebSocket 服务端交付报告 ## 协议 新增 `GET /api/ws`。浏览器使用当前后台同源的 `wss://.../api/ws`;令牌不出现在 URL,任何 query string 均拒绝。Origin 必须精确匹配请求 Host(含端口),或来自 CORS_ORIGINS 明确配置的源;拒绝缺失、null、路径、userinfo 等异常 Origin。跨域后台必须把实际后台 HTTPS Origin 配入 CORS_ORIGINS。WebSocket 协议升级返回 101;Origin/升级错误允许真实 HTTP 错误状态,既有 JSON 契约保持原样。 客户端连接后 5 秒内发送首个文本 JSON: ```json {"type":"auth","token":"后台登录返回的token"} ``` 首帧与后续应用消息均限制 4096 字节。服务端直接查询 Redis 会话,必须有合法 user ID、storeId 与 admin/staff 角色;访客无法订阅。鉴权成功发送 `{"type":"ready"}`。服务请求新增和状态变更在 PostgreSQL 事务成功提交后,向对应 storeId 的员工连接发送 `{"type":"tasks.changed"}`。消息仅提示重新 GET /api/service-requests,没有任务正文、顾客信息、桌码或图纸内容。相同 requestId 重试、内容冲突和失败的状态变更均不广播。 每 20 秒服务端重验 Redis 会话,并发送 WebSocket ping 控制帧与 `{"type":"heartbeat"}`。浏览器协议栈自动回应 pong;前端无需发送应用层消息,也允许 `{"type":"pong"}`。heartbeat 用于前端判活,tasks.changed 用于刷新列表,再由前端选择播报。JSON 通知不是可靠事件队列;就绪与重连后必须主动拉取最新任务。 关闭码:4403 鉴权失败、5 秒首帧超时、会话过期/撤销或身份/门店变更;4400 非约定应用消息;1001 服务关闭;消息过大也可能由协议库以 1009 关闭。Redis 故障按失效会话关闭,客户端应重新验证登录并采用重连退避。 ## 实现约束 - 连接读期限 45 秒,由协议 pong 延长;写操作最多 5 秒,Redis 查询为独立最多 3 秒 context。WS 升级请求不套用常规 HTTP 的 15 秒业务 context,因此连接可长期存活。 - 每个连接一个 reader 和一个 writer。应用 JSON、ping/pong 与正常关闭均通过 writer 发出;鉴权阶段尚未启动 writer,由握手处理同步完成。 - 单实例、按门店分组的轻量 hub。每连接容量 1 的变更通道合并失效通知,广播不等待浏览器写入,慢连接不会阻塞业务事务。控制 pong 使用容量 4 的独立小通道。 - 服务关闭显式通知已鉴权的 hijacked WS 连接,HTTP graceful shutdown 不依赖隐式关闭 WS。 - 当前广播是进程内的。未来多实例需要 Redis PubSub(门店主题)或可靠消息设施,否则其他实例上的连接不会收到更新。业务真源仍为 PostgreSQL。 Gorilla WebSocket 锁定 `v1.5.3`,使用前查询了[官方发布页](https://github.com/gorilla/websocket/releases)。该页当前标记 v1.5.3 为 latest;Go 官方数据库列出的弱 mask key 报告影响 v1.5.3 之前的版本。[Go 漏洞报告](https://pkg.go.dev/vuln/GO-2026-6278) ## 代理与构建 仅修改指定的 `deploy/nginx-https.example.conf` 和 `perler-beads-webui/nginx.conf`:新增精确 `/api/ws` 代理,HTTP/1.1 + Upgrade/Connection,75 秒读期限,10 秒发送期限,禁用代理缓冲。WS 路由使用 `Host $http_host` 保留原始 Host 与非标准端口(例如 Compose :8088),以便正确校验同源。生产 DNS、TLS、frp 和 Nginx 二进制未在本次 WS 测试运行,需在真实部署验证隧道。 新二进制:`/tmp/perler-ws-server`,主任务已接管启动本机 8080,并完成真实浏览器 WebSocket 与 TTS 联调。 ## 真实执行验证 先写实际 Redis 鉴权测试,缺少端点时观察到预期失败:`websocket: bad handshake (status 200)`。新增/更新推送测试在接入提交钩子前观察到预期失败:`committed task did not push notification`,然后接入提交后广播。 使用真实本地 PostgreSQL 独立 `perler_test` 库(127.0.0.1:55432)与 Redis 专用测试 DB15(127.0.0.1:56379)。Redis session key 和 PostgreSQL fixture 使用随机 ID,仅删除测试自己创建的数据,不清空共享库。 实际执行通过: ```sh export GOPATH=/tmp/perler-go GOCACHE=/tmp/perler-go-cache GOMODCACHE=/tmp/perler-go-mod export TEST_DATABASE_URL='独立测试 PostgreSQL 连接串,数据库名需包含 test' export TEST_REDIS_URL='redis://127.0.0.1:56379/15' GIN_MODE=release go test ./... -count=1 GIN_MODE=release go test -race ./... -count=1 go vet ./... go build -o /tmp/perler-ws-server ./cmd/server ``` 普通测试约 22 秒,race 测试约 23 秒,均通过。覆盖管理员/员工认证、访客/无效令牌/空门店拒绝、同源/白名单/恶意 Origin、消息上限、未知协议、5 秒首帧超时、20 秒 heartbeat(证明超过 15 秒 HTTP context)、会话撤销关闭、真实事务提交后通知、跨门店不通知、幂等重放及冲突静默、越权状态变更静默、失效消息无私有字段,以及慢订阅者通知合并和广播不阻塞。原有 PostgreSQL 单连接池回归测试同时通过。