package export import ( "database/sql" "io" "strconv" "github.com/xuri/excelize/v2" "git.yeij.top/AskaEth/Scoreboard/server/internal/domain" ) // TeamDetailRow 是单位积分明细的一行:一分一行的得分来源。 type TeamDetailRow struct { SportName string EventName string Category string AthleteName string Rank int ResultText string Source string Points float64 Remark string // individual 标记该条流水记到个人:合计要按 individual_to_team_weight 折算。 individual bool // squad 是这条流水对应报名项的队名(entries.squad),只有总明细的「单位」列用它: // 单位分表的表名就是单位(一份单位级清单),没有单位列可写,所以那里不展示队名。 squad string } // TeamDetailWorkbook 是「单位积分明细」的内容。 type TeamDetailWorkbook struct { Meta Meta Team domain.Team Rows []TeamDetailRow // IndividualToTeamWeight 与总积分榜同源,用于把个人分折算进「合计」。 IndividualToTeamWeight float64 } var teamDetailColumns = []column{ {"序号", 6, kindInt}, {"大项", 14, kindText}, {"小项", 18, kindText}, {"组别", 14, kindText}, {"运动员", 14, kindText}, {"名次", 7, kindCenter}, {"成绩", 12, kindCenter}, {"来源", 12, kindCenter}, {"积分", 8, kindNumber}, {"备注", 20, kindText}, } // ExportTeamDetailXLSX 导出某单位的积分明细(逐项列出得分来源)。 // 仅测试使用(生产走 Load* + Write* 直接写响应体,不落盘)——见 saveWorkbook 的说明。 func ExportTeamDetailXLSX(db *sql.DB, teamID int64, dst string, opts Options) error { wb, err := LoadTeamDetailWorkbook(db, teamID, opts) if err != nil { return err } f, err := teamDetailFile(wb) if err != nil { return err } return saveWorkbook(f, dst) } // WriteTeamDetailXLSX 把单位积分明细写成 xlsx 流。 func WriteTeamDetailXLSX(w io.Writer, wb TeamDetailWorkbook) error { f, err := teamDetailFile(wb) if err != nil { return err } return f.Write(w) } func teamDetailFile(wb TeamDetailWorkbook) (*excelize.File, error) { return buildWorkbook("积分明细", func(f *excelize.File, st *styleSet) error { meta := wb.Meta.normalized() name := sanitizeSheetName(displayName(wb.Team.Name, wb.Team.ShortName) + "积分明细") if err := addSheet(f, name); err != nil { return err } title := sheetTitle(meta, displayName(wb.Team.Name, wb.Team.ShortName)+" · 积分明细") return writeSheet(f, st, name, meta, title, teamDetailColumns, teamDetailRows(wb.Rows, wb.IndividualToTeamWeight)) }) } // teamDetailDataRows 生成明细的数据行(不含「合计」)并返回合计值。与 REST 的单位积分明细同口径: // 记到个人的流水按 individual_to_team_weight 折算后展示,合计即各行之和。 // 总明细表复用它拼各单位的数据行,保证单表与总表逐格一致。 func teamDetailDataRows(rows []TeamDetailRow, weight float64) ([][]any, float64) { if weight <= 0 { weight = 1 } out := make([][]any, 0, len(rows)) var total float64 for i, r := range rows { points := r.Points if r.individual { points *= weight } total += points rank := "" if r.Rank > 0 { rank = strconv.Itoa(r.Rank) } out = append(out, []any{ i + 1, r.SportName, r.EventName, r.Category, r.AthleteName, rank, r.ResultText, r.Source, points, r.Remark, }) } return out, total } // teamDetailRows 把明细行排成表格:数据行后跟一行「合计」;没有流水时也保留合计行(写 0)。 // 手册(docs/使用手册.md 第 10 节)承诺「末行是合计」,总明细版也恒有合计行; // 单单位版若在空数据时省掉它,同一份表在两种入口下版式不一致,读表的人会以为少印了一行。 func teamDetailRows(rows []TeamDetailRow, weight float64) [][]any { data, total := teamDetailDataRows(rows, weight) return append(data, []any{"合计", "", "", "", "", "", "", "", total, ""}) }