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:
1 parent
d0e3b93e76
commit
bc8b1ee6c0
145 files changed
+25159
-321
No files matched your search
@@ -0,0 +1,109 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"git.yeij.top/AskaEth/Scoreboard/server/internal/auth"
|
||||
"git.yeij.top/AskaEth/Scoreboard/server/internal/rules"
|
||||
)
|
||||
|
||||
// rulesRoutes 挂载规则包的导出与导入(导入支持 ?preview=1 只预览差异)。
|
||||
func (s *server) rulesRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /api/rules/export", s.read(auth.RoleAdmin, s.exportRules))
|
||||
mux.HandleFunc("POST /api/rules/import", s.write(auth.RoleAdmin, s.importRules))
|
||||
}
|
||||
|
||||
func (s *server) exportRules(w http.ResponseWriter, r *http.Request) (any, error) {
|
||||
pack, err := rules.Export(s.ev, rules.Meta{})
|
||||
if err != nil {
|
||||
return nil, internal(err)
|
||||
}
|
||||
body, err := rules.Marshal(pack)
|
||||
if err != nil {
|
||||
return nil, internal(err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/yaml; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="rules.yaml"`)
|
||||
if _, err := w.Write(body); err != nil {
|
||||
s.logger.Error("规则包写出失败", "err", err)
|
||||
}
|
||||
return nil, errHandled
|
||||
}
|
||||
|
||||
func (s *server) importRules(w http.ResponseWriter, r *http.Request) (any, error) {
|
||||
body, err := io.ReadAll(io.LimitReader(r.Body, 4<<20))
|
||||
if err != nil {
|
||||
return nil, badRequest("读取请求体失败", nil)
|
||||
}
|
||||
if len(body) == 0 {
|
||||
return nil, badRequest("请求体为空", nil)
|
||||
}
|
||||
|
||||
pack, err := rules.Parse(body)
|
||||
if err != nil {
|
||||
return nil, badRequest(err.Error(), nil)
|
||||
}
|
||||
cur, err := rules.Load(s.ev)
|
||||
if err != nil {
|
||||
return nil, internal(err)
|
||||
}
|
||||
cs, err := rules.Diff(cur, pack)
|
||||
if err != nil {
|
||||
return nil, badRequest(err.Error(), nil)
|
||||
}
|
||||
|
||||
// 先预览再落地:?preview=1 只返回差异。
|
||||
if r.URL.Query().Get("preview") == "1" {
|
||||
return cs, nil
|
||||
}
|
||||
|
||||
res, err := rules.Apply(s.ev, cs)
|
||||
if err != nil {
|
||||
return nil, internal(err)
|
||||
}
|
||||
// 分值一变,历史成绩的积分必须全量重算。
|
||||
if err := s.recomputeAllEvents(r.Context(), middlewareUserID(r)); err != nil {
|
||||
return nil, internal(err)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// recomputeAllEvents 逐项目重算积分流水,用于规则包导入等会改变分值的场景。
|
||||
func (s *server) recomputeAllEvents(ctx context.Context, actorID int64) error {
|
||||
rows, err := s.ev.QueryContext(ctx, `SELECT id FROM events`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var ids []int64
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
_ = rows.Close()
|
||||
return err
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, eventID := range ids {
|
||||
tx, err := s.ev.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := recomputeLedgerTx(ctx, tx, eventID, 0, actorID, "rules-import"); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user