feat: 补齐第一批已知缺口(关闭握手 / 定时备份 / Origin 白名单 / DTO 收敛)

- httpapi:新增 POST /api/shutdown(admin 会话或 X-Shutdown-Token 常量时间比对)
  与 GET /api/rules/switches(recorder 可读,只回布尔开关);settings/standings 的
  重复 DTO 收敛到 internal/dto;standings.showIndividualMedals 贯通到设置读写与前端
- export:抽出 export.Service,新增自动备份定时任务(backup.intervalMinutes/keep),
  自动、手动、发布前三类备份共用同一把互斥锁与滚动清理
- realtime:/ws 去掉 InsecureSkipVerify,改用 OriginPatterns(同源与无 Origin 放行,
  跨站需命中 server.allowedOrigins)
- console:停止服务改走 POST /api/shutdown 优雅关闭(5 秒宽限后回退强杀),
  服务管理器新增「已连接的大屏实例」卡片
- deploy:build.bat 改用 for /f 读内部署名——set /p 在 chcp 65001 下返回空,
  导致最近这次构建的 exe 里根本没注入署名
- docs:备份策略、关闭超时、Origin 配置的说明与实现对齐
This commit is contained in:
AskaEth committed 2026-09-09 21:41:08 +08:00
1 parent 4d56c73ae6
commit 2c9ff65fc4
42 files changed
+1271 -275

No files matched your search

+12
View File
@@ -14,13 +14,24 @@ import (
type Config struct {
Server Server `yaml:"server"`
DataDir string `yaml:"dataDir"`
Backup Backup `yaml:"backup"`
Log Log `yaml:"log"`
}
// Backup 是自动备份配置。
type Backup struct {
// IntervalMinutes 是自动备份间隔(分钟);<=0 表示关闭自动备份。
IntervalMinutes int `yaml:"intervalMinutes"`
// Keep 是每个库滚动保留的份数;<=0 时用 export.DefaultBackupKeep。
Keep int `yaml:"keep"`
}
// Server 是 HTTP 监听配置。
type Server struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
// AllowedOrigins 是 WebSocket 跨站 Origin 白名单;为空时只放行同源与不带 Origin 的客户端。
AllowedOrigins []string `yaml:"allowedOrigins"`
}
// Log 是日志配置。
@@ -33,6 +44,7 @@ func Default() *Config {
return &Config{
Server: Server{Host: "0.0.0.0", Port: 19540},
DataDir: "data",
Backup: Backup{IntervalMinutes: 5, Keep: 20},
Log: Log{Level: "info"},
}
}
+31
View File
@@ -0,0 +1,31 @@
package config
import (
"os"
"path/filepath"
"reflect"
"testing"
)
func TestDefaultAllowedOrigins(t *testing.T) {
if got := Default().Server.AllowedOrigins; len(got) != 0 {
t.Fatalf("默认白名单应为空: %v", got)
}
}
func TestLoadAllowedOrigins(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yaml")
raw := "server:\n allowedOrigins:\n - http://a.example\n - https://b.example\n"
if err := os.WriteFile(path, []byte(raw), 0o644); err != nil {
t.Fatalf("写入配置: %v", err)
}
cfg, err := Load(path)
if err != nil {
t.Fatalf("加载配置: %v", err)
}
want := []string{"http://a.example", "https://b.example"}
if !reflect.DeepEqual(cfg.Server.AllowedOrigins, want) {
t.Fatalf("白名单解析错误: got %v want %v", cfg.Server.AllowedOrigins, want)
}
}