package export import ( "database/sql" "path/filepath" "testing" "github.com/xuri/excelize/v2" ) // seedTieAndRelay 在 seedEvent 之上补两类现场常见、却最容易把列宽顶爆的数据: // 并列名次(名次列会写成「3(并列)」)与接力队名单(运动员/队 列写成顿号列表)。 func seedTieAndRelay(t *testing.T, db *sql.DB) { t.Helper() stmts := []string{ `UPDATE results SET rank = 3, is_tie = 1`, `INSERT INTO events(sport_id, code, name, event_type, result_mode, result_type, compare, status) VALUES (1, 'E2', '4×100米接力', 'team', 'auto', 'time', 'asc', 'published')`, `INSERT INTO event_categories(event_id, name, sort_order) VALUES (2, '男子', 1)`, `INSERT INTO event_score_channels(event_id, scope, basis, profile_id) SELECT 2, 'team', 'result', id FROM scoring_profiles WHERE code = 'team_top6'`, `INSERT INTO entries(event_id, category_id, team_id, member_names, bib) VALUES (2, 2, 1, '["王磊","赵雷","陈刚2","郑凯"]', '701')`, `INSERT INTO results(event_id, entry_id, rank, result_text, result_value, unit, state) VALUES (2, 3, 1, '0:44.12', 44.12, 's', 'published')`, `INSERT INTO score_ledger(team_id, athlete_id, event_id, entry_id, result_id, source_type, source_id, points) VALUES (1, NULL, 2, 3, 3, 'result', 3, 15)`, } for _, s := range stmts { if _, err := db.Exec(s); err != nil { t.Fatalf("造数据 %q: %v", s, err) } } } // TestColumnsFitContent 守住「不折行的列必须放得下实际内容」: // 名次列写「3(并列)」、运动员/队 列写接力队名单,列宽不够时 Excel 会把右侧的字硬裁掉 // (这一类列没有折行),领导拿到的表就是缺字的。kindText 列有折行,超宽只会换行,不在此列。 func TestColumnsFitContent(t *testing.T) { db := openEventDB(t) seedEvent(t, db) seedTieAndRelay(t, db) seedChannelRanks(t, db) cases := []struct { name string file string export func(dst string) error sheets []sheetLayout }{ { name: "分项成绩表", file: "分项.xlsx", export: func(dst string) error { return ExportEventXLSX(db, 0, dst, Options{}) }, sheets: []sheetLayout{ {"田径·100米跑·男子", eventColumns}, {"田径·4×100米接力·男子", eventColumns}, }, }, { name: "全部成绩(单页)", file: "单页.xlsx", export: func(dst string) error { return ExportFlatXLSX(db, dst, Options{}) }, sheets: []sheetLayout{ {flatSheet, flatColumns}, }, }, { name: "团体奖牌明细", file: "奖牌.xlsx", export: func(dst string) error { return ExportMedalDetailXLSX(db, dst, Options{}) }, sheets: []sheetLayout{ {medalDetailSheet, medalDetailColumns}, {"第一分公司奖牌明细", medalTeamColumns}, }, }, } dir := t.TempDir() for _, tc := range cases { dst := filepath.Join(dir, tc.file) if err := tc.export(dst); err != nil { t.Fatalf("导出%s: %v", tc.name, err) } f := openXLSX(t, dst) for _, sl := range tc.sheets { assertColumnsFit(t, f, sl.sheet, sl.columns) } if err := f.Close(); err != nil { t.Fatalf("关闭 %s: %v", dst, err) } } } // sheetLayout 是一张表的列定义,用来知道哪些列不折行。 type sheetLayout struct { sheet string columns []column } // participantTitle 是接力队名单所在列的表头,分项成绩表与奖牌明细都印这一列。 const participantTitle = "运动员/队" // assertColumnsFit 逐列核对:不折行的列,列宽必须放得下该列最长的一条数据。 func assertColumnsFit(t *testing.T, f *excelize.File, sheet string, cols []column) { t.Helper() rows, err := f.GetRows(sheet) if err != nil { t.Fatalf("读取 %s: %v", sheet, err) } for i, c := range cols { // 折行列(kindText)超宽只会换行,不丢字;但「运动员/队」是接力队名单, // 一个名单折成两三行会把每页能印的行数压掉一半,所以它也按「一行放得下」要求。 if c.kind == kindText && c.title != participantTitle { continue } name, err := excelize.ColumnNumberToName(i + 1) if err != nil { t.Fatalf("第 %d 列的列名: %v", i+1, err) } width, err := f.GetColWidth(sheet, name) if err != nil { t.Fatalf("读取 %s!%s 的列宽: %v", sheet, name, err) } if width != c.width { t.Errorf("%s「%s」列宽 = %v,期望 %v(版式与列定义应同源)", sheet, c.title, width, c.width) } // 前 4 行是标题 / 副标题 / 空行 / 表头,数据从第 5 行(下标 4)起。 for r := 4; r < len(rows); r++ { v := cell(rows, r, i) w := displayWidth(v) if w <= width { continue } avail := width if c.kind != kindText { // 不折行的列:右侧连着空格时 Excel 会让文字溢出显示,真正被裁的是 // 「溢出到有内容的那一列」——名次列右边紧挨着非空的号码,正是缺陷现场。 for j := i + 1; j < len(cols) && cell(rows, r, j) == ""; j++ { avail += cols[j].width } } if w > avail { t.Errorf("%s 第 %d 行「%s」列 = %q 需要 %v 个显示宽度,右侧可用的只有 %v,会被裁掉或折行", sheet, r+1, c.title, v, w, avail) } } } } // displayWidth 按 Excel 的粗略口径估算显示宽度:东亚全角字符(汉字、全角括号、顿号)算 2, // 其余算 1。列宽就是按这个口径定的,用它判断一格会不会被裁。 func displayWidth(s string) float64 { var w float64 for _, r := range s { switch { case r >= 0x1100 && (r <= 0x115F || r == 0x2329 || r == 0x232A || (r >= 0x2E80 && r <= 0xA4CF) || (r >= 0xAC00 && r <= 0xD7A3) || (r >= 0xF900 && r <= 0xFAFF) || (r >= 0xFE30 && r <= 0xFE6F) || (r >= 0xFF00 && r <= 0xFF60) || (r >= 0xFFE0 && r <= 0xFFE6)): w += 2 default: w++ } } return w }