package export import ( "database/sql" "path/filepath" "testing" ) // seedSquads 造一场「一个单位派两支队」的团体赛:羽毛球男子双打,第一分公司派一队 / 二队, // 两队分别拿第 1、第 2 名(口径 3:一个单位两个队各占一个名次、各拿一块牌,不去重不合并), // 第二分公司一支不区分队伍的队(squad 留空 = 老写法)拿第 3 名。 func seedSquads(t *testing.T, db *sql.DB) { t.Helper() stmts := []string{ `INSERT INTO teams(code, name, short_name, sort_order) VALUES ('T1', '第一分公司', '一公司', 1)`, `INSERT INTO teams(code, name, short_name, sort_order) VALUES ('T2', '第二分公司', '二公司', 2)`, `INSERT INTO sports(code, name) VALUES ('SP1', '羽毛球')`, `INSERT INTO events(sport_id, code, name, event_type, result_mode, result_type, compare, status) VALUES (1, 'E1', '男子双打', 'team', 'auto', 'score', 'desc', 'published')`, `INSERT INTO event_categories(event_id, name, sort_order) VALUES (1, '男子', 1)`, `INSERT INTO event_score_channels(event_id, scope, basis, profile_id) SELECT 1, 'team', 'result', id FROM scoring_profiles WHERE code = 'individual_top6'`, // 同一个单位在同一组别的两条团体报名:靠 squad 区分,只有带上 squad 的唯一索引才放得下。 `INSERT INTO entries(event_id, category_id, team_id, member_names, squad, bib) VALUES (1, 1, 1, '["王磊","赵雷"]', '一队', '101')`, `INSERT INTO entries(event_id, category_id, team_id, member_names, squad, bib) VALUES (1, 1, 1, '["陈刚","郑凯"]', '二队', '102')`, `INSERT INTO entries(event_id, category_id, team_id, member_names, bib) VALUES (1, 1, 2, '["孙七","周八"]', '103')`, `INSERT INTO results(event_id, entry_id, rank, result_text, result_value, unit, state) VALUES (1, 1, 1, '2:0', 2, 'pts', 'published')`, `INSERT INTO results(event_id, entry_id, rank, result_text, result_value, unit, state) VALUES (1, 2, 2, '2:1', 2, 'pts', 'published')`, `INSERT INTO results(event_id, entry_id, rank, result_text, result_value, unit, state) VALUES (1, 3, 3, '2:1', 2, 'pts', 'published')`, `INSERT INTO score_ledger(team_id, event_id, entry_id, result_id, source_type, source_id, points) VALUES (1, 1, 1, 1, 'result', 1, 10)`, `INSERT INTO score_ledger(team_id, event_id, entry_id, result_id, source_type, source_id, points) VALUES (1, 1, 2, 2, 'result', 2, 8)`, `INSERT INTO score_ledger(team_id, event_id, entry_id, result_id, source_type, source_id, points) VALUES (2, 1, 3, 3, 'result', 3, 6)`, `UPDATE settings SET value = '测试运动会' WHERE key = 'event.name'`, `UPDATE settings SET value = '测试主办方' WHERE key = 'event.organizer'`, } for _, s := range stmts { if _, err := db.Exec(s); err != nil { t.Fatalf("造数据 %q: %v", s, err) } } } // 分项成绩表:单位列在队名非空时写「单位名+队名」,一个单位的两个队各占一行。 func TestEventSheetShowsSquadAfterUnitName(t *testing.T) { db := openEventDB(t) seedSquads(t, db) seedChannelRanks(t, db) dst := filepath.Join(t.TempDir(), "成绩表.xlsx") if err := ExportEventXLSX(db, 0, dst, Options{}); err != nil { t.Fatalf("导出分项成绩表: %v", err) } rows := readSheet(t, dst, "羽毛球·男子双打·男子") // 4 行版式 + 3 条报名,按名次排:一队、二队、第二分公司。 want := []string{"第一分公司一队", "第一分公司二队", "第二分公司"} for i, w := range want { if got := cell(rows, 4+i, 2); got != w { t.Errorf("第 %d 行单位 = %q,期望 %q", i+1, got, w) } } } // 全部成绩(单页):与分项成绩表同源,单位列同样带上队名。 func TestFlatSheetShowsSquadAfterUnitName(t *testing.T) { db := openEventDB(t) seedSquads(t, db) seedChannelRanks(t, db) dst := filepath.Join(t.TempDir(), "单页.xlsx") if err := ExportFlatXLSX(db, dst, Options{}); err != nil { t.Fatalf("导出全部成绩(单页): %v", err) } rows := readSheet(t, dst, "全部成绩") // 单页表在「名次」前多了 大项/小项/组别 三列,单位列是第 6 列。 want := []string{"第一分公司一队", "第一分公司二队", "第二分公司"} for i, w := range want { if got := cell(rows, 4+i, 5); got != w { t.Errorf("第 %d 行单位 = %q,期望 %q", i+1, got, w) } } } // 报名名单:单位名列在队名非空时写「单位名+队名」,不区分队伍的报名照旧只写单位名。 func TestEntriesSheetShowsSquadAfterUnitName(t *testing.T) { db := openEventDB(t) seedSquads(t, db) dst := filepath.Join(t.TempDir(), "报名名单.xlsx") if err := ExportEntriesXLSX(db, dst, EntriesFilter{}, Options{}); err != nil { t.Fatalf("导出报名名单: %v", err) } rows := readSheet(t, dst, "报名名单") // 列序:小项编码/小项名/组别/单位编码/单位名/…,单位名是第 5 列。 want := []string{"第一分公司一队", "第一分公司二队", "第二分公司"} for i, w := range want { if got := cell(rows, 4+i, 4); got != w { t.Errorf("第 %d 行单位名 = %q,期望 %q", i+1, got, w) } } } // 单位奖牌明细:总表的单位列按该奖牌对应报名项的队名写全;一个单位两个队各拿一块牌, // 就是两块牌(口径 3),不合并、不去重。 func TestMedalDetailShowsSquadAfterUnitName(t *testing.T) { db := openEventDB(t) seedSquads(t, db) seedChannelRanks(t, db) dst := filepath.Join(t.TempDir(), "奖牌.xlsx") if err := ExportMedalDetailXLSX(db, dst, Options{}); err != nil { t.Fatalf("导出单位奖牌明细: %v", err) } f := openXLSX(t, dst) defer func() { _ = f.Close() }() wantSheets := []string{"单位奖牌明细", "第一分公司奖牌明细", "第二分公司奖牌明细"} if got := f.GetSheetList(); !equalStrings(got, wantSheets) { t.Fatalf("sheet 列表 = %v,期望 %v", got, wantSheets) } master, err := f.GetRows("单位奖牌明细") if err != nil { t.Fatalf("读取总表: %v", err) } want := []string{"第一分公司一队", "第一分公司二队", "第二分公司"} for i, w := range want { if got := cell(master, 4+i, 0); got != w { t.Errorf("总表第 %d 行单位 = %q,期望 %q", i+1, got, w) } } // 第一分公司是一枚金牌加一枚银牌(两个队各一块),分表的小计照实写两块。 unit, err := f.GetRows("第一分公司奖牌明细") if err != nil { t.Fatalf("读取分表: %v", err) } if got := cell(unit, len(unit)-1, 4); got != "金 1 / 银 1 / 铜 0" { t.Errorf("第一分公司小计 = %q,期望 金 1 / 银 1 / 铜 0", got) } } // 单位积分总明细:总表的单位列按该条流水对应报名项的队名写全(分表没有单位列,表名就是单位)。 func TestAllTeamDetailsShowsSquadAfterUnitName(t *testing.T) { db := openEventDB(t) seedSquads(t, db) seedChannelRanks(t, db) dst := filepath.Join(t.TempDir(), "总明细.xlsx") if err := ExportAllTeamDetailsXLSX(db, dst, Options{}); err != nil { t.Fatalf("导出总明细: %v", err) } rows := readSheet(t, dst, "积分总明细") // 列序:单位/序号/大项/小项/组别/运动员/名次/成绩/来源/积分/备注; // 行序按单位总分榜(第一分公司在前),单位内按名次:一队 10 分、二队 8 分,第二分公司 6 分。 want := []string{"第一分公司一队", "第一分公司二队", "第二分公司"} for i, w := range want { if got := cell(rows, 4+i, 0); got != w { t.Errorf("总明细第 %d 行单位 = %q,期望 %q", i+1, got, w) } } if got := cell(rows, 7, 0); got != "合计" { t.Errorf("末行首列 = %q,期望 合计", got) } } // 队名为空(不区分队伍)时,五份导出件的单位名列一个字都不多:这是老数据与单队单位的默认, // 导出件必须与升级前逐行一致。 func TestEmptySquadKeepsPlainUnitName(t *testing.T) { db := openEventDB(t) seedSquads(t, db) seedChannelRanks(t, db) dir := t.TempDir() entriesDst := filepath.Join(dir, "报名名单.xlsx") if err := ExportEntriesXLSX(db, entriesDst, EntriesFilter{}, Options{}); err != nil { t.Fatalf("导出报名名单: %v", err) } // 第三条报名(第二分公司)没有队名。 if got := cell(readSheet(t, entriesDst, "报名名单"), 6, 4); got != "第二分公司" { t.Errorf("空队名的单位名 = %q,期望 第二分公司", got) } medalDst := filepath.Join(dir, "奖牌.xlsx") if err := ExportMedalDetailXLSX(db, medalDst, Options{}); err != nil { t.Fatalf("导出单位奖牌明细: %v", err) } if got := cell(readSheet(t, medalDst, "单位奖牌明细"), 6, 0); got != "第二分公司" { t.Errorf("空队名的奖牌总表单位 = %q,期望 第二分公司", got) } detailDst := filepath.Join(dir, "总明细.xlsx") if err := ExportAllTeamDetailsXLSX(db, detailDst, Options{}); err != nil { t.Fatalf("导出总明细: %v", err) } if got := cell(readSheet(t, detailDst, "积分总明细"), 6, 0); got != "第二分公司" { t.Errorf("空队名的总明细单位 = %q,期望 第二分公司", got) } }