feat: M3-M7 全模块落地 + 集成接线

- M3 认证/权限/审计/限流 + 全部 REST 接口
- M4 管理面板(MIUIx + 成绩录入/积分榜下钻/大屏控制)
- M5a 大屏页面(六页 + 明细覆盖层 + 缩放适配)
- M6 WebSocket 实时同步中心
- M7 xlsx 导出 + 赛事包 zip + 备份
- 规则包 YAML 导入导出(差异预览)
- 字体 vendor 子集化(4 套 x2 字重)
- 集成:realtime/export/rules 接入 main+httpapi;backup 子命令
- 修复:中间件包装 ResponseWriter 丢失 Hijacker,导致 WS 握手 501
This commit is contained in:
AskaEth committed 2026-09-09 19:02:24 +08:00
1 parent d0e3b93e76
commit bc8b1ee6c0
145 files changed
+25159 -321

No files matched your search

+152
View File
@@ -0,0 +1,152 @@
package export
import (
"database/sql"
"io"
"strings"
"github.com/xuri/excelize/v2"
)
// EventResultRow 是分项成绩表的一行。
// Rank 用字符串是为了承载「1(并列)」这种展示值。
type EventResultRow struct {
Rank string
Bib string
TeamName string
Participant string
ResultText string
Unit string
Points float64
Status string
Remark string
}
// EventSheet 是一个小项的一个组别(无组别时 Category 为空)对应的一张表。
type EventSheet struct {
SportName string
EventName string
Category string
Rows []EventResultRow
}
// EventsWorkbook 是「分项成绩表」的内容:一个小项可含多个组别 sheet,
// 也可承载全部小项(ExportAllXLSX 用)。
type EventsWorkbook struct {
Meta Meta
Sheets []EventSheet
}
var eventColumns = []column{
{"名次", 8, kindCenter},
{"号码", 9, kindCenter},
{"单位", 22, kindText},
{"运动员/队", 18, kindText},
{"成绩", 12, kindCenter},
{"积分", 8, kindNumber},
{"状态", 8, kindCenter},
{"备注", 20, kindText},
}
// ExportEventXLSX 导出单个小项的成绩表;eventID 为 0 时导出全部小项。
func ExportEventXLSX(db *sql.DB, eventID int64, dst string, opts Options) error {
wb, err := LoadEventWorkbook(db, eventID, opts)
if err != nil {
return err
}
f, err := eventFile(wb)
if err != nil {
return err
}
return saveWorkbook(f, dst)
}
// WriteEventXLSX 把分项成绩表写成 xlsx 流。
func WriteEventXLSX(w io.Writer, wb EventsWorkbook) error {
f, err := eventFile(wb)
if err != nil {
return err
}
return f.Write(w)
}
func eventFile(wb EventsWorkbook) (*excelize.File, error) {
return buildWorkbook("成绩表", func(f *excelize.File, st *styleSet) error {
return writeEventSheets(f, st, wb.Meta, wb.Sheets, &sheetNamer{})
})
}
func writeEventSheets(f *excelize.File, st *styleSet, meta Meta, sheets []EventSheet, namer *sheetNamer) error {
meta = meta.normalized()
for _, sh := range sheets {
name := namer.add(eventSheetBase(sh))
if err := addSheet(f, name); err != nil {
return err
}
if err := writeSheet(f, st, name, meta, eventSheetTitle(meta, sh), eventColumns, eventRows(sh.Rows)); err != nil {
return err
}
}
return nil
}
// eventSheetBase 是工作表名:大项·小项·组别,够短且能区分。
func eventSheetBase(sh EventSheet) string {
parts := make([]string, 0, 3)
if sh.SportName != "" {
parts = append(parts, sh.SportName)
}
parts = append(parts, sh.EventName)
if sh.Category != "" {
parts = append(parts, sh.Category)
}
return strings.Join(parts, "·")
}
// eventSheetTitle 是表内大标题,带赛事名。
func eventSheetTitle(meta Meta, sh EventSheet) string {
base := eventSheetBase(sh)
if base == "" {
base = "成绩表"
}
return sheetTitle(meta, base)
}
func eventRows(rows []EventResultRow) [][]any {
out := make([][]any, 0, len(rows))
for _, r := range rows {
out = append(out, []any{
r.Rank, r.Bib, r.TeamName, r.Participant,
displayResult(r.ResultText, r.Unit), r.Points, r.Status, r.Remark,
})
}
return out
}
// displayResult 把成绩与单位拼成「11.58 秒」;单位已含在文本里时不重复拼接。
func displayResult(text, unit string) string {
text = strings.TrimSpace(text)
unit = strings.TrimSpace(unit)
if text == "" {
return ""
}
if unit == "" || strings.HasSuffix(text, unit) {
return text
}
return text + " " + unitLabel(unit)
}
func unitLabel(unit string) string {
switch unit {
case "s":
return "秒"
case "m":
return "米"
case "pts":
return "分"
case "次":
return "次"
default:
return unit
}
}