package httpapi import ( "io" "net/http" "net/url" "os" "path/filepath" "time" "git.yeij.top/AskaEth/Scoreboard/server/internal/auth" "git.yeij.top/AskaEth/Scoreboard/server/internal/export" ) const xlsxMIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" // exportRoutes 挂载 xlsx 导出、赛事项目包与备份查询。 func (s *server) exportRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /api/export/standings.xlsx", s.read(auth.RoleGuest, s.exportStandingsXLSX)) mux.HandleFunc("GET /api/export/all.xlsx", s.read(auth.RoleGuest, s.exportAllXLSX)) // Go 1.22 ServeMux 的通配段必须独占一段,所以分项/单位导出用 /{id},文件名由响应头给出。 mux.HandleFunc("GET /api/export/events/{id}", s.read(auth.RoleGuest, s.exportEventXLSX)) mux.HandleFunc("GET /api/export/teams/{id}", s.read(auth.RoleGuest, s.exportTeamXLSX)) mux.HandleFunc("GET /api/export/project.zip", s.read(auth.RoleAdmin, s.exportProjectZip)) mux.HandleFunc("POST /api/export/project/import", s.write(auth.RoleAdmin, s.importProjectZip)) mux.HandleFunc("GET /api/export/backups", s.read(auth.RoleAdmin, s.listBackups)) } // streamDownload 直接写响应体;返回 errHandled 让统一响应层不再追加 JSON。 func (s *server) streamDownload(w http.ResponseWriter, filename, contentType string, write func(io.Writer) error) (any, error) { w.Header().Set("Content-Type", contentType) w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+url.PathEscape(filename)) if err := write(w); err != nil { s.logger.Error("导出写出失败", "file", filename, "err", err) } return nil, errHandled } func (s *server) exportStandingsXLSX(w http.ResponseWriter, r *http.Request) (any, error) { wb, err := export.LoadStandingsWorkbook(s.ev, export.Options{}) if err != nil { return nil, internal(err) } return s.streamDownload(w, "总积分榜.xlsx", xlsxMIME, func(out io.Writer) error { return export.WriteStandingsXLSX(out, wb) }) } func (s *server) exportEventXLSX(w http.ResponseWriter, r *http.Request) (any, error) { id, err := pathID(r, "id") if err != nil { return nil, err } wb, err := export.LoadEventWorkbook(s.ev, id, export.Options{}) if err != nil { return nil, internal(err) } return s.streamDownload(w, "分项成绩表.xlsx", xlsxMIME, func(out io.Writer) error { return export.WriteEventXLSX(out, wb) }) } func (s *server) exportAllXLSX(w http.ResponseWriter, r *http.Request) (any, error) { wb, err := export.LoadAllWorkbook(s.ev, export.Options{}) if err != nil { return nil, internal(err) } return s.streamDownload(w, "赛事成绩总表.xlsx", xlsxMIME, func(out io.Writer) error { return export.WriteAllXLSX(out, wb) }) } func (s *server) exportTeamXLSX(w http.ResponseWriter, r *http.Request) (any, error) { id, err := pathID(r, "id") if err != nil { return nil, err } wb, err := export.LoadTeamDetailWorkbook(s.ev, id, export.Options{}) if err != nil { return nil, internal(err) } return s.streamDownload(w, "单位积分明细.xlsx", xlsxMIME, func(out io.Writer) error { return export.WriteTeamDetailXLSX(out, wb) }) } func (s *server) exportProjectZip(w http.ResponseWriter, r *http.Request) (any, error) { dir := filepath.Join(s.dataDir, "exports") if err := os.MkdirAll(dir, 0o755); err != nil { return nil, internal(err) } dst := filepath.Join(dir, "project-"+time.Now().Format("20060102-150405")+".zip") if _, err := export.ExportProjectZip(s.ev, dst, export.ProjectExportOptions{}); err != nil { return nil, internal(err) } defer func() { _ = os.Remove(dst) }() w.Header().Set("Content-Type", "application/zip") w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+url.PathEscape("赛事项目包.zip")) http.ServeFile(w, r, dst) return nil, errHandled } func (s *server) importProjectZip(w http.ResponseWriter, r *http.Request) (any, error) { if err := r.ParseMultipartForm(512 << 20); err != nil { return nil, badRequest("解析上传失败", nil) } file, _, err := r.FormFile("file") if err != nil { return nil, badRequest("缺少上传字段 file", nil) } defer func() { _ = file.Close() }() tmpDir, err := os.MkdirTemp("", "scoreboard-import-") if err != nil { return nil, internal(err) } defer func() { _ = os.RemoveAll(tmpDir) }() tmp := filepath.Join(tmpDir, "project.zip") out, err := os.Create(tmp) if err != nil { return nil, internal(err) } if _, err := io.Copy(out, file); err != nil { _ = out.Close() return nil, internal(err) } if err := out.Close(); err != nil { return nil, internal(err) } res, err := export.ImportProjectZip(tmp, filepath.Join(s.dataDir, "event_projs"), export.ImportOptions{ Overwrite: r.FormValue("overwrite") == "1", Rename: r.FormValue("rename"), }) if err != nil { return nil, badRequest(err.Error(), nil) } return res, nil } func (s *server) listBackups(w http.ResponseWriter, r *http.Request) (any, error) { items, err := export.ListBackups(filepath.Join(s.dataDir, "backups"), "") if err != nil { return nil, internal(err) } return items, nil }