excel.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. package excel
  2. import (
  3. "fmt"
  4. "github.com/xuri/excelize/v2"
  5. "io"
  6. "math"
  7. "net/http"
  8. "strconv"
  9. "time"
  10. )
  11. // ExcelGenerator 通用Excel生成器
  12. type ExcelGenerator struct {
  13. file *excelize.File
  14. sheetName string
  15. headers []string
  16. headerMap map[string]string // 表头映射(英文字段名->中文显示名)
  17. currentRow int // 当前行号
  18. }
  19. // NewExcelGenerator 创建新的Excel生成器
  20. func NewExcelGenerator(sheetName string, headers []string, headerMap map[string]string) *ExcelGenerator {
  21. f := excelize.NewFile()
  22. // 默认sheet名称是Sheet1,如果传入的是其他名称,则创建新sheet并删除默认sheet
  23. defaultSheetName := "Sheet1"
  24. if sheetName != defaultSheetName {
  25. f.NewSheet(sheetName)
  26. f.DeleteSheet(defaultSheetName)
  27. }
  28. return &ExcelGenerator{
  29. file: f,
  30. sheetName: sheetName,
  31. headers: headers,
  32. headerMap: headerMap,
  33. currentRow: 1, // 从第1行开始(通常第1行是表头)
  34. }
  35. }
  36. // WriteHeaders 写入表头
  37. func (g *ExcelGenerator) WriteHeaders() error {
  38. // 写入表头
  39. for i, header := range g.headers {
  40. // 获取对应的显示名,如果没有映射则使用原字段名
  41. displayName, exists := g.headerMap[header]
  42. if !exists {
  43. displayName = header
  44. }
  45. cell := fmt.Sprintf("%s%d", columnName(i), g.currentRow)
  46. if err := g.file.SetCellValue(g.sheetName, cell, displayName); err != nil {
  47. return err
  48. }
  49. // 设置表头样式(加粗、居中等)
  50. style, err := g.file.NewStyle(&excelize.Style{
  51. Font: &excelize.Font{
  52. Bold: true,
  53. },
  54. Alignment: &excelize.Alignment{
  55. Horizontal: "center",
  56. Vertical: "center",
  57. },
  58. })
  59. if err != nil {
  60. return err
  61. }
  62. if err := g.file.SetCellStyle(g.sheetName, cell, cell, style); err != nil {
  63. return err
  64. }
  65. }
  66. g.currentRow++ // 表头写完后,行号+1
  67. return nil
  68. }
  69. // WriteRows 写入多行数据
  70. func (g *ExcelGenerator) WriteRows(data []map[string]interface{}) error {
  71. for _, row := range data {
  72. if err := g.WriteRow(row); err != nil {
  73. return err
  74. }
  75. }
  76. return nil
  77. }
  78. // WriteRow 写入单行数据
  79. func (g *ExcelGenerator) WriteRow(rowData map[string]interface{}) error {
  80. for i, field := range g.headers {
  81. value, exists := rowData[field]
  82. if !exists {
  83. value = "" // 如果数据中不存在该字段,则写入空值
  84. }
  85. cell := fmt.Sprintf("%s%d", columnName(i), g.currentRow)
  86. // 根据不同的数据类型处理
  87. switch v := value.(type) {
  88. case time.Time:
  89. // 时间格式化为 YYYY-MM-DD HH:MM:SS
  90. if err := g.file.SetCellValue(g.sheetName, cell, v.Format("2006-01-02 15:04:05")); err != nil {
  91. return err
  92. }
  93. default:
  94. if err := g.file.SetCellValue(g.sheetName, cell, v); err != nil {
  95. return err
  96. }
  97. }
  98. }
  99. g.currentRow++ // 一行写完后,行号+1
  100. return nil
  101. }
  102. // SaveToWriter 保存到io.Writer接口
  103. func (g *ExcelGenerator) SaveToWriter(w io.Writer) error {
  104. return g.file.Write(w)
  105. }
  106. // SaveToBuffer 保存到内存
  107. func (g *ExcelGenerator) SaveToBuffer() ([]byte, error) {
  108. buffer, err := g.file.WriteToBuffer()
  109. if err != nil {
  110. return nil, err
  111. }
  112. return buffer.Bytes(), nil
  113. }
  114. // columnName 将列索引转换为Excel列名(A, B, C, ... Z, AA, AB, ...)
  115. func columnName(colIndex int) string {
  116. if colIndex < 26 {
  117. return string('A' + colIndex)
  118. }
  119. // 超过26列时需要用两个或更多字母表示
  120. result := ""
  121. for colIndex >= 0 {
  122. remainder := colIndex % 26
  123. result = string('A'+remainder) + result
  124. colIndex = colIndex/26 - 1
  125. if colIndex < 0 {
  126. break
  127. }
  128. }
  129. return result
  130. }
  131. // TransferOption 传输选项
  132. type TransferOption struct {
  133. FileName string
  134. ContentType string
  135. }
  136. // ExportType 导出类型枚举
  137. type ExportType int
  138. const (
  139. // ExportTypeNormal 普通导出(小文件,加载到内存后直接传输)
  140. ExportTypeNormal ExportType = iota
  141. // ExportTypeStream 流式导出(大文件,流式传输避免占用过多内存)
  142. ExportTypeStream
  143. // ExportTypeChunk 分块导出(超大文件,分块处理并传输)
  144. ExportTypeChunk
  145. )
  146. // SmartExport 智能选择导出方式
  147. // dataCount: 数据条数
  148. // rowSize: 每行数据的估计大小(字节数)
  149. func SmartExport(dataCount int, rowSize int) ExportType {
  150. // 估算导出文件大小(表头+数据)
  151. estimatedSize := (dataCount + 1) * rowSize
  152. // 根据估算大小选择不同的导出方式
  153. switch {
  154. case estimatedSize <= 5*1024*1024: // 5MB以下用普通导出
  155. return ExportTypeNormal
  156. case estimatedSize <= 50*1024*1024: // 5MB-50MB用流式导出
  157. return ExportTypeStream
  158. default: // 超过50MB用分块导出
  159. return ExportTypeChunk
  160. }
  161. }
  162. // NormalExport 普通导出(小文件,一次性加载到内存)
  163. func NormalExport(g *ExcelGenerator, w http.ResponseWriter, option TransferOption) error {
  164. // 设置响应头
  165. w.Header().Set("Content-Type", option.ContentType)
  166. w.Header().Set("Content-Disposition", "attachment; filename="+option.FileName)
  167. // 直接写入响应
  168. return g.SaveToWriter(w)
  169. }
  170. // StreamExport 流式导出(大文件,流式传输)
  171. func StreamExport(g *ExcelGenerator, w http.ResponseWriter, option TransferOption) error {
  172. // 设置响应头
  173. w.Header().Set("Content-Type", option.ContentType)
  174. w.Header().Set("Content-Disposition", "attachment; filename="+option.FileName)
  175. w.Header().Set("Transfer-Encoding", "chunked")
  176. // 流式写入
  177. return g.SaveToWriter(w)
  178. }
  179. // ChunkExport 分块导出(超大文件)
  180. // 这个方法需要配合前端实现,例如通过分页API多次获取数据并合并
  181. func ChunkExport(w http.ResponseWriter, option TransferOption, totalRecords int, pageSize int) {
  182. totalPages := int(math.Ceil(float64(totalRecords) / float64(pageSize)))
  183. // 设置响应头
  184. w.Header().Set("Content-Type", "application/json")
  185. w.Header().Set("X-Total-Pages", strconv.Itoa(totalPages))
  186. w.Header().Set("X-Total-Records", strconv.Itoa(totalRecords))
  187. w.Header().Set("X-Page-Size", strconv.Itoa(pageSize))
  188. // 返回分块导出信息
  189. w.Write([]byte(fmt.Sprintf(`{
  190. "message": "File is too large for direct download. Please use paginated export.",
  191. "total_records": %d,
  192. "total_pages": %d,
  193. "page_size": %d
  194. }`, totalRecords, totalPages, pageSize)))
  195. }