fix: 实机复测六批发现的 16 条缺陷(导入编码静默错误 / 导出观感 / 错误分级 / 大屏裁切)
先把「实机复测」能在这台 Linux 机器上验的全部自己验掉(真服务端 + 真 Chromium + 真样本),
六批全部通过,另挖出 16 条缺陷,逐条修完并复验:
导入
- 坏字节被静默替换成 U+FFFD 却报「组别不存在」→ 检测 RuneError 回 400 + 可操作中文文案
- UTF-16 无 BOM 误报「缺少列 event_code」→ 保守判据识别并正确解码
- CSV 语法错只有英文 → 中文提示,英文原文保留在 detail
- 表头缺列补「第 1 行应是表头」的指向
导出
- 名次列宽 8 → 11(放得下「3(并列)」,Excel 里不再硬裁)
- 运动员/队列宽 18 → 24(含奖牌明细的同一列)
- 聚合流水行补组别(仅单位在该小项只参加一个组别时回推;多组别宁可留空也不猜)
- docProps 作者从 excelize 默认的 xuri 改成中性名 + 真实导出时间
- Content-Disposition 补 ASCII 退化 filename(9 个下载端点,含此前没有测试的 project.zip)
错误分级与限流
- 未知大屏指令 500 → 400 VALIDATION_FAILED(传输层真失败仍走 5xx)
- 5xx 的 ERROR 日志补 role / ip(4xx 的 WARN 一直有)
- 写限流 30 → 120 次/分钟/用户(登录 5 次/分/IP 不动,性质不同)
- 赛事名含非法字符时导出给 400 + 可操作提示(原来永远 500「服务端异常」)
大屏与基础设施
- 实时赛况 2 栏成绩末位被裁(1:09.00 显示成 1:09.0)→ 成绩列按内容定宽 + nowrap;
字号不缩,让位给可省略号截断的运动员列。真 Chromium 实测 slack −45px → +6px、0 裁切、
scrollWidth 恒 1920(无横向滚动条);顺带修掉 1 栏长成绩同样被裁 8px 的问题
- logging 启动即预建两个日志目录(原来 interceptions/ 懒创建,现场会以为日志坏了)
- C→S 裸帧由静默忽略改为 WARN,并把「要套 {event,data} 外壳」写进日志
- 建单位/大项漏传 active 不再静默建成「停用」(新增 decodeJSONPresence 按键是否存在判意图)
文档
- README 单位 logo 措辞收窄到「大屏端」(管理端只负责上传配置)
- 导入规范补三类编码报错说明 + 现场另存结论 + 限流提示
- 部署指南补「导出前先看赛事名称」与「切换后怎么确认真的切过去了」
- devdocs 05/08/13 同步(含第三十五轮的完整记录与教训)
自检:服务端 13 包连跑两遍全绿;前端 257 例;vue-tsc + build 通过;Console 18 文件。
版本号 dev_0.1.4 → dev_0.1.5。
This commit is contained in:
1 parent
a52e6a4a3b
commit
8dd599bd97
39 files changed
+1308
-106
No files matched your search
@@ -3,6 +3,7 @@ package realtime
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -165,7 +166,16 @@ func (c *client) handleMessage(ctx context.Context, data []byte) {
|
||||
c.hub.sendInitial(ctx, c, nil)
|
||||
|
||||
default:
|
||||
c.hub.log.Debug("忽略未知实时事件", "event", env.Event, "remote", c.remoteAddr)
|
||||
// 客户端最常见的错法:只发了 payload,没套 `{event,data}` 外壳。
|
||||
// 那种帧解出来 Event == "",以前只在 DEBUG 里留一行 —— 现场表现是
|
||||
// 「连上了但一直没数据,15 秒后被断开」,而且客户端拿不到任何提示。
|
||||
// 这里升到 WARN 并把「该怎么发」写进日志,省掉一次翻源码。
|
||||
if strings.TrimSpace(env.Event) == "" {
|
||||
c.hub.log.Warn("实时帧缺少 event 字段,已忽略(C→S 帧同样要套 {event,data} 外壳)",
|
||||
"remote", c.remoteAddr)
|
||||
return
|
||||
}
|
||||
c.hub.log.Warn("忽略未知实时事件", "event", env.Event, "remote", c.remoteAddr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package realtime
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
@@ -546,6 +547,11 @@ func (h *Hub) SendCommandTo(room string, cmd DisplayCommand) {
|
||||
}
|
||||
}
|
||||
|
||||
// ErrUnknownAction 表示指令的 action 不在协议清单内。
|
||||
// 它不是服务端异常:action 是请求体入参,HTTP 层据此用 errors.Is 把它映射成 400,
|
||||
// 只有真正的下发失败(写 socket 失败等)才该按 5xx 处理。
|
||||
var ErrUnknownAction = errors.New("realtime: 未知的大屏指令")
|
||||
|
||||
// SendCommandToInstances 按实例定向下发 display:command,返回命中的实例数。
|
||||
//
|
||||
// targets 是要控制的 instanceID(GET /api/display/instances 里的 id);为空时退化成广播,
|
||||
@@ -554,7 +560,8 @@ func (h *Hub) SendCommandTo(room string, cmd DisplayCommand) {
|
||||
// 定向指令需要一个明确的失败信号。
|
||||
func (h *Hub) SendCommandToInstances(targets []string, cmd DisplayCommand) (int, error) {
|
||||
if !ValidDisplayAction(cmd.Action) {
|
||||
return 0, fmt.Errorf("未知的大屏指令 %q", cmd.Action)
|
||||
// 包上哨兵,调用方才能把「入参非法」与「下发失败」分开映射成 4xx / 5xx。
|
||||
return 0, fmt.Errorf("未知的大屏指令 %q: %w", cmd.Action, ErrUnknownAction)
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
h.SendCommand(cmd)
|
||||
|
||||
Reference in new issue
Block a user