diff --git a/README.md b/README.md index 1091236..a24a94c 100644 --- a/README.md +++ b/README.md @@ -67,10 +67,20 @@ ### 8. 短信工具 (sms) 提供阿里云短信发送功能,支持模板短信和批量发送,使用Go标准库实现。 -### 9. 工厂工具 (factory) -提供从配置文件直接创建已初始化客户端对象的功能,包括数据库、Redis、邮件、短信、日志等,避免调用方重复实现创建逻辑。 +### 9. Excel导出工具 (excel) +提供数据导出到Excel文件的功能,支持结构体切片、自定义格式化、多工作表等特性。 -### 10. 日志工具 (logger) +**功能特性**: +- 支持结构体切片自动导出 +- 支持嵌套字段访问(如 "User.Name") +- 支持自定义格式化函数 +- 自动调整列宽和表头样式 +- 支持导出到文件或HTTP响应 + +### 10. 工厂工具 (factory) +提供从配置文件直接创建已初始化客户端对象的功能,包括数据库、Redis、邮件、短信、日志、Excel等,避免调用方重复实现创建逻辑。 + +### 11. 日志工具 (logger) 提供统一的日志记录功能,支持多种日志级别和输出方式,使用Go标准库实现。 --- @@ -90,6 +100,7 @@ | 邮件 | `SendEmail()` | 直接调用 | ⭐⭐⭐ | | 短信 | `SendSMS()` | 直接调用 | ⭐⭐⭐ | | 存储 | `UploadFile()`, `GetFileURL()` | 直接调用 | ⭐⭐⭐ | +| Excel导出 | `ExportToExcel()`, `ExportToExcelFile()` | 直接调用 | ⭐⭐⭐ | | **Get方法(高级功能)** | | | | | 数据库 | `GetDatabase()` | 返回GORM对象,用于复杂查询 | ⭐⭐ | | Redis高级 | `GetRedisClient()` | 返回Redis客户端,用于Hash/List/Set等 | ⭐ | @@ -277,6 +288,14 @@ fac.SendSMS([]string{"13800138000"}, map[string]string{"code": "123456"}) // 文件上传 url, _ := fac.UploadFile(ctx, "images/test.jpg", file, "image/jpeg") +// Excel导出 +columns := []factory.ExportColumn{ + {Header: "ID", Field: "ID", Width: 10}, + {Header: "姓名", Field: "Name", Width: 20}, + {Header: "邮箱", Field: "Email", Width: 30}, +} +fac.ExportToExcelFile("users.xlsx", "用户列表", columns, users) + // 数据库(高级功能) db, _ := fac.GetDatabase() db.Find(&users) diff --git a/docs/README.md b/docs/README.md index 1f98fc7..0685527 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,6 +11,7 @@ - [存储工具](./storage.md) - 文件上传和查看(OSS、MinIO) - [邮件工具](./email.md) - SMTP邮件发送 - [短信工具](./sms.md) - 阿里云短信发送 +- [Excel导出工具](./excel.md) - 数据导出到Excel文件 - [工厂工具](./factory.md) - 从配置直接创建已初始化客户端对象 - [日志工具](./logger.md) - 统一的日志记录功能 - [国际化工具](./i18n.md) - 多语言内容管理和国际化支持 diff --git a/docs/excel.md b/docs/excel.md new file mode 100644 index 0000000..acfe5cd --- /dev/null +++ b/docs/excel.md @@ -0,0 +1,426 @@ +# Excel导出工具文档 + +## 概述 + +Excel导出工具提供了将数据导出到Excel文件的功能,支持结构体切片、自定义格式化、多工作表等特性。通过工厂模式,外部项目可以方便地使用Excel导出功能。 + +## 功能特性 + +- **黑盒模式**:提供直接调用的方法,无需获取Excel对象 +- **延迟初始化**:Excel导出器在首次使用时才创建 +- **支持结构体切片**:自动将结构体切片转换为Excel行数据 +- **支持嵌套字段**:支持访问嵌套结构体字段(如 "User.Name") +- **自定义格式化**:支持自定义字段值的格式化函数 +- **自动列宽**:自动调整列宽以适应内容 +- **表头样式**:自动应用表头样式(加粗、背景色等) +- **ExportData接口**:支持实现ExportData接口进行高级定制 + +## 使用方法 + +### 1. 创建工厂(推荐) + +```go +import "git.toowon.com/jimmy/go-common/factory" + +// 方式1:从配置文件创建(推荐) +fac, err := factory.NewFactoryFromFile("./config.json") +if err != nil { + log.Fatal(err) +} + +// 方式2:从配置对象创建 +cfg, _ := config.LoadFromFile("./config.json") +fac := factory.NewFactory(cfg) + +// 方式3:Excel导出不需要配置,可以传nil +fac := factory.NewFactory(nil) +``` + +### 2. 导出结构体切片到文件(黑盒模式,推荐) + +```go +// 定义结构体 +type User struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + CreatedAt time.Time `json:"created_at"` + Status int `json:"status"` +} + +// 准备数据 +users := []User{ + {ID: 1, Name: "Alice", Email: "alice@example.com", CreatedAt: time.Now(), Status: 1}, + {ID: 2, Name: "Bob", Email: "bob@example.com", CreatedAt: time.Now(), Status: 1}, +} + +// 定义导出列 +columns := []factory.ExportColumn{ + {Header: "ID", Field: "ID", Width: 10}, + {Header: "姓名", Field: "Name", Width: 20}, + {Header: "邮箱", Field: "Email", Width: 30}, + {Header: "创建时间", Field: "CreatedAt", Width: 20}, + {Header: "状态", Field: "Status", Width: 10}, +} + +// 导出到文件 +err := fac.ExportToExcelFile("users.xlsx", "用户列表", columns, users) +if err != nil { + log.Fatal(err) +} +``` + +### 3. 导出到HTTP响应(黑盒模式,推荐) + +```go +import "net/http" + +func exportUsersHandler(w http.ResponseWriter, r *http.Request) { + fac, _ := factory.NewFactoryFromFile("./config.json") + + users := []User{ + {ID: 1, Name: "Alice", Email: "alice@example.com", CreatedAt: time.Now(), Status: 1}, + {ID: 2, Name: "Bob", Email: "bob@example.com", CreatedAt: time.Now(), Status: 1}, + } + + columns := []factory.ExportColumn{ + {Header: "ID", Field: "ID"}, + {Header: "姓名", Field: "Name"}, + {Header: "邮箱", Field: "Email"}, + } + + // 设置HTTP响应头 + w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + w.Header().Set("Content-Disposition", "attachment; filename=users.xlsx") + + // 导出到响应 + err := fac.ExportToExcel(w, "用户列表", columns, users) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} +``` + +### 4. 使用格式化函数(黑盒模式,推荐) + +```go +import "git.toowon.com/jimmy/go-common/excel" + +columns := []factory.ExportColumn{ + {Header: "ID", Field: "ID", Width: 10}, + {Header: "姓名", Field: "Name", Width: 20}, + { + Header: "创建时间", + Field: "CreatedAt", + Width: 20, + Format: excel.FormatDateTimeDefault, // 使用便捷的格式化函数 + }, + { + Header: "状态", + Field: "Status", + Width: 10, + Format: func(value interface{}) string { + // 自定义格式化函数 + if status, ok := value.(int); ok { + if status == 1 { + return "启用" + } + return "禁用" + } + return "" + }, + }, +} + +err := fac.ExportToExcelFile("users.xlsx", "用户列表", columns, users) +``` + +### 5. 使用ExportData接口(高级功能) + +```go +// 实现ExportData接口 +type UserExportData struct { + users []User +} + +// GetExportColumns 获取导出列定义 +func (d *UserExportData) GetExportColumns() []excel.ExportColumn { + return []excel.ExportColumn{ + {Header: "ID", Field: "ID", Width: 10}, + {Header: "姓名", Field: "Name", Width: 20}, + {Header: "邮箱", Field: "Email", Width: 30}, + } +} + +// GetExportRows 获取导出数据行 +func (d *UserExportData) GetExportRows() [][]interface{} { + rows := make([][]interface{}, 0, len(d.users)) + for _, user := range d.users { + row := []interface{}{ + user.ID, + user.Name, + user.Email, + } + rows = append(rows, row) + } + return rows +} + +// 使用 +exportData := &UserExportData{users: users} +columns := exportData.GetExportColumns() +err := fac.ExportToExcelFile("users.xlsx", "用户列表", columns, exportData) +``` + +### 6. 获取Excel对象(高级功能) + +```go +// 获取Excel导出器对象(仅在需要高级功能时使用) +excel, err := fac.GetExcel() +if err != nil { + log.Fatal(err) +} + +// 获取excelize.File对象,用于高级操作 +file := excel.GetFile() + +// 创建多个工作表 +file.NewSheet("Sheet2") +file.SetCellValue("Sheet2", "A1", "数据") + +// 自定义样式 +style, _ := file.NewStyle(&excelize.Style{ + Font: &excelize.Font{ + Bold: true, + Size: 14, + }, +}) +file.SetCellStyle("Sheet2", "A1", "A1", style) +``` + +## API 参考 + +### 工厂方法(黑盒模式,推荐使用) + +#### ExportToExcel(w io.Writer, sheetName string, columns []ExportColumn, data interface{}) error + +导出数据到Writer。 + +**参数:** +- `w`: Writer对象(如http.ResponseWriter) +- `sheetName`: 工作表名称(可选,默认为"Sheet1") +- `columns`: 列定义 +- `data`: 数据列表(可以是结构体切片或实现了ExportData接口的对象) + +**返回:** 错误信息 + +**示例:** +```go +fac.ExportToExcel(w, "用户列表", columns, users) +``` + +#### ExportToExcelFile(filePath string, sheetName string, columns []ExportColumn, data interface{}) error + +导出数据到文件。 + +**参数:** +- `filePath`: 文件路径 +- `sheetName`: 工作表名称(可选,默认为"Sheet1") +- `columns`: 列定义 +- `data`: 数据列表 + +**返回:** 错误信息 + +**示例:** +```go +fac.ExportToExcelFile("users.xlsx", "用户列表", columns, users) +``` + +### 高级方法 + +#### GetExcel() (*excel.Excel, error) + +获取Excel导出器对象。 + +**返回:** Excel导出器对象和错误信息 + +**说明:** +- 仅在需要使用高级功能时使用 +- 推荐使用黑盒方法:`ExportToExcel()`、`ExportToExcelFile()` + +### 结构体类型 + +#### ExportColumn + +导出列定义。 + +```go +type ExportColumn struct { + Header string // 表头名称 + Field string // 数据字段名(支持嵌套字段,如 "User.Name") + Width float64 // 列宽(可选,0表示自动) + Format func(interface{}) string // 格式化函数(可选) +} +``` + +**字段说明:** +- `Header`: 表头显示的名称 +- `Field`: 数据字段名,支持嵌套字段(如 "User.Name") +- `Width`: 列宽,0表示自动调整 +- `Format`: 格式化函数,用于自定义字段值的显示格式 + +### 便捷函数 + +#### excel.FormatDateTime(layout string) func(interface{}) string + +创建日期时间格式化函数。 + +**参数:** +- `layout`: 时间格式,如 "2006-01-02 15:04:05" + +**返回:** 格式化函数 + +**示例:** +```go +Format: excel.FormatDateTime("2006-01-02 15:04:05") +``` + +#### excel.FormatDate(value interface{}) string + +格式化日期(格式:2006-01-02)。 + +**示例:** +```go +Format: excel.FormatDate +``` + +#### excel.FormatDateTimeDefault(value interface{}) string + +格式化日期时间(格式:2006-01-02 15:04:05)。 + +**示例:** +```go +Format: excel.FormatDateTimeDefault +``` + +### ExportData接口 + +实现此接口的结构体可以直接导出。 + +```go +type ExportData interface { + // GetExportColumns 获取导出列定义 + GetExportColumns() []ExportColumn + // GetExportRows 获取导出数据行 + GetExportRows() [][]interface{} +} +``` + +## 完整示例 + +```go +package main + +import ( + "net/http" + "time" + + "git.toowon.com/jimmy/go-common/excel" + "git.toowon.com/jimmy/go-common/factory" +) + +type User struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + CreatedAt time.Time `json:"created_at"` + Status int `json:"status"` +} + +func exportUsersHandler(w http.ResponseWriter, r *http.Request) { + fac, _ := factory.NewFactoryFromFile("./config.json") + + // 从数据库或其他数据源获取数据 + users := []User{ + {ID: 1, Name: "Alice", Email: "alice@example.com", CreatedAt: time.Now(), Status: 1}, + {ID: 2, Name: "Bob", Email: "bob@example.com", CreatedAt: time.Now(), Status: 1}, + } + + // 定义导出列 + columns := []factory.ExportColumn{ + {Header: "ID", Field: "ID", Width: 10}, + {Header: "姓名", Field: "Name", Width: 20}, + {Header: "邮箱", Field: "Email", Width: 30}, + { + Header: "创建时间", + Field: "CreatedAt", + Width: 20, + Format: excel.FormatDateTimeDefault, + }, + { + Header: "状态", + Field: "Status", + Width: 10, + Format: func(value interface{}) string { + if status, ok := value.(int); ok { + if status == 1 { + return "启用" + } + return "禁用" + } + return "" + }, + }, + } + + // 设置HTTP响应头 + w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + w.Header().Set("Content-Disposition", "attachment; filename=users.xlsx") + + // 导出到响应 + err := fac.ExportToExcel(w, "用户列表", columns, users) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +func main() { + http.HandleFunc("/export/users", exportUsersHandler) + http.ListenAndServe(":8080", nil) +} +``` + +## 设计优势 + +### 优势总结 + +1. **降低复杂度**:调用方无需关心Excel文件对象的创建和管理 +2. **延迟初始化**:Excel导出器在首次使用时才创建,提高性能 +3. **统一接口**:所有操作通过工厂方法调用,接口统一 +4. **灵活扩展**:支持结构体切片、自定义格式化、ExportData接口等多种方式 +5. **自动优化**:自动调整列宽、应用表头样式等 + +## 注意事项 + +1. **配置检查**:Excel导出不需要配置,可以传nil创建工厂 +2. **错误处理**:所有方法都可能返回错误,需要正确处理 +3. **延迟初始化**:Excel导出器在首次使用时才创建,首次调用可能稍慢 +4. **字段名匹配**:Field字段名必须与结构体字段名匹配(区分大小写) +5. **嵌套字段**:支持嵌套字段访问(如 "User.Name"),但需要确保字段路径正确 +6. **格式化函数**:格式化函数返回的字符串会直接写入Excel单元格 +7. **列宽设置**:Width为0时会自动调整列宽,但可能影响性能(大数据量时建议设置固定宽度) + +## 最佳实践 + +1. **使用黑盒方法**:推荐使用 `ExportToExcel()` 和 `ExportToExcelFile()`,无需获取Excel对象 +2. **设置列宽**:对于大数据量,建议设置固定列宽以提高性能 +3. **使用格式化函数**:对于日期时间、状态等字段,使用格式化函数提高可读性 +4. **错误处理**:始终检查导出方法的返回值 +5. **HTTP响应**:导出到HTTP响应时,记得设置正确的Content-Type和Content-Disposition头 + +## 示例 + +完整示例请参考 `examples/excel_example.go` + diff --git a/docs/factory.md b/docs/factory.md index 28cb299..8defa01 100644 --- a/docs/factory.md +++ b/docs/factory.md @@ -26,6 +26,7 @@ | **邮件** | `SendEmail()` | `fac.SendEmail(to, subject, body)` | | **短信** | `SendSMS()` | `fac.SendSMS(phones, params)` | | **存储** | `UploadFile()`, `GetFileURL()` | `fac.UploadFile(ctx, key, file)` | +| **Excel导出** | `ExportToExcel()`, `ExportToExcelFile()` | `fac.ExportToExcelFile("users.xlsx", "用户列表", columns, users)` | | **日期时间** | `Now()`, `ParseDateTime()`, `FormatDateTime()` 等 | `fac.Now("Asia/Shanghai")` | | **时间工具** | `GetTimestamp()`, `IsToday()`, `GetBeginOfWeek()` 等 | `fac.GetTimestamp()` | | **加密工具** | `HashPassword()`, `MD5()`, `SHA256()`, `GenerateSMSCode()` 等 | `fac.HashPassword("password")` | @@ -42,6 +43,7 @@ |------|----------|----------| | `GetDatabase()` | `*gorm.DB` | 数据库复杂查询、事务、关联查询等 | | `GetRedisClient()` | `*redis.Client` | Hash、List、Set、ZSet、Pub/Sub等高级操作 | +| `GetExcel()` | `*excel.Excel` | 多工作表、自定义样式、图表等高级操作 | | `GetLogger()` | `*logger.Logger` | Close()、设置全局logger等 | ## 使用方法 @@ -145,7 +147,54 @@ url, _ := fac.GetFileURL("images/test.jpg", 0) url, _ := fac.GetFileURL("images/test.jpg", 3600) ``` -### 6. Redis操作(黑盒模式,推荐) +### 6. Excel导出(黑盒模式,推荐) + +```go +import ( + "net/http" + "time" + "git.toowon.com/jimmy/go-common/excel" +) + +// 定义结构体 +type User struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + CreatedAt time.Time `json:"created_at"` +} + +// 准备数据 +users := []User{ + {ID: 1, Name: "Alice", Email: "alice@example.com", CreatedAt: time.Now()}, + {ID: 2, Name: "Bob", Email: "bob@example.com", CreatedAt: time.Now()}, +} + +// 定义导出列 +columns := []factory.ExportColumn{ + {Header: "ID", Field: "ID", Width: 10}, + {Header: "姓名", Field: "Name", Width: 20}, + {Header: "邮箱", Field: "Email", Width: 30}, + { + Header: "创建时间", + Field: "CreatedAt", + Width: 20, + Format: excel.FormatDateTimeDefault, + }, +} + +// 导出到文件 +err := fac.ExportToExcelFile("users.xlsx", "用户列表", columns, users) + +// 导出到HTTP响应 +func exportUsersHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + w.Header().Set("Content-Disposition", "attachment; filename=users.xlsx") + fac.ExportToExcel(w, "用户列表", columns, users) +} +``` + +### 7. Redis操作(黑盒模式,推荐) ```go import "context" @@ -168,7 +217,7 @@ err := fac.RedisDelete(ctx, "user:123", "user:456") exists, err := fac.RedisExists(ctx, "user:123") ``` -### 7. 数据库操作(黑盒模式) +### 8. 数据库操作(黑盒模式) ```go // 获取数据库对象(已初始化,黑盒模式) @@ -183,7 +232,7 @@ db.Find(&users) db.Create(&user) ``` -### 8. 日期时间操作(黑盒模式) +### 9. 日期时间操作(黑盒模式) ```go // 获取当前时间 @@ -205,7 +254,7 @@ unix := fac.ToUnix(now) t2 := fac.FromUnix(unix, "Asia/Shanghai") ``` -### 9. 时间操作(黑盒模式) +### 10. 时间操作(黑盒模式) ```go // 时间戳 @@ -240,7 +289,7 @@ timeInfo := fac.GenerateTimeInfoWithTimezone(now, "Asia/Shanghai") fmt.Printf("UTC: %s, Local: %s, Unix: %d\n", timeInfo.UTC, timeInfo.Local, timeInfo.Unix) ``` -### 10. 金额计算(黑盒模式) +### 11. 金额计算(黑盒模式) ```go // 元转分 @@ -260,7 +309,7 @@ version := fac.GetVersion() fmt.Println("当前版本:", version) ``` -### 13. HTTP响应(黑盒模式) +### 13. HTTP响应(黑盒模式,推荐) ```go import "net/http" @@ -277,7 +326,7 @@ fac.Error(w, 1001, "用户不存在") fac.SystemError(w, "系统错误") ``` -### 14. HTTP请求解析(黑盒模式) +### 14. HTTP请求解析(黑盒模式,推荐) ```go import "net/http" @@ -300,7 +349,7 @@ isActive := fac.ConvertBool(r.FormValue("is_active"), false) timezone := fac.GetTimezone(r) ``` -### 15. Redis操作(获取客户端对象) +### 15. Redis操作(获取客户端对象,高级功能) ```go import ( @@ -525,6 +574,54 @@ func main() { **返回:** 文件访问URL和错误信息 +### Excel导出方法(黑盒模式) + +#### ExportToExcel(w io.Writer, sheetName string, columns []ExportColumn, data interface{}) error + +导出数据到Writer。 + +**参数:** +- `w`: Writer对象(如http.ResponseWriter) +- `sheetName`: 工作表名称(可选,默认为"Sheet1") +- `columns`: 列定义 +- `data`: 数据列表(可以是结构体切片或实现了ExportData接口的对象) + +**返回:** 错误信息 + +**示例:** +```go +fac.ExportToExcel(w, "用户列表", columns, users) +``` + +#### ExportToExcelFile(filePath string, sheetName string, columns []ExportColumn, data interface{}) error + +导出数据到文件。 + +**参数:** +- `filePath`: 文件路径 +- `sheetName`: 工作表名称(可选,默认为"Sheet1") +- `columns`: 列定义 +- `data`: 数据列表 + +**返回:** 错误信息 + +**示例:** +```go +fac.ExportToExcelFile("users.xlsx", "用户列表", columns, users) +``` + +#### GetExcel() (*excel.Excel, error) + +获取Excel导出器对象(高级功能时使用)。 + +**返回:** Excel导出器对象和错误信息 + +**说明:** +- 仅在需要使用高级功能时使用 +- 推荐使用黑盒方法:`ExportToExcel()`、`ExportToExcelFile()` + +**详细说明请参考:[Excel导出工具文档](./excel.md)** + ### Redis方法(黑盒模式) #### RedisGet(ctx context.Context, key string) (string, error) diff --git a/examples/excel_example.go b/examples/excel_example.go new file mode 100644 index 0000000..0ba8247 --- /dev/null +++ b/examples/excel_example.go @@ -0,0 +1,233 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "time" + + "git.toowon.com/jimmy/go-common/excel" + "git.toowon.com/jimmy/go-common/factory" +) + +// User 用户结构体示例 +type User struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + CreatedAt time.Time `json:"created_at"` + Status int `json:"status"` +} + +func main() { + // 创建工厂(可选,Excel导出不需要配置) + fac, err := factory.NewFactoryFromFile("./config/example.json") + if err != nil { + // Excel导出不需要配置,可以传nil + fac = factory.NewFactory(nil) + } + + // 示例1:导出结构体切片到文件 + fmt.Println("=== Example 1: Export Struct Slice to File ===") + example1(fac) + + // 示例2:导出到HTTP响应 + fmt.Println("\n=== Example 2: Export to HTTP Response ===") + example2(fac) + + // 示例3:使用格式化函数 + fmt.Println("\n=== Example 3: Export with Format Functions ===") + example3(fac) + + // 示例4:使用ExportData接口 + fmt.Println("\n=== Example 4: Export with ExportData Interface ===") + example4(fac) +} + +// 示例1:导出结构体切片到文件 +func example1(fac *factory.Factory) { + users := []User{ + {ID: 1, Name: "Alice", Email: "alice@example.com", CreatedAt: time.Now(), Status: 1}, + {ID: 2, Name: "Bob", Email: "bob@example.com", CreatedAt: time.Now().Add(-24 * time.Hour), Status: 1}, + {ID: 3, Name: "Charlie", Email: "charlie@example.com", CreatedAt: time.Now().Add(-48 * time.Hour), Status: 0}, + } + + columns := []factory.ExportColumn{ + {Header: "ID", Field: "ID", Width: 10}, + {Header: "姓名", Field: "Name", Width: 20}, + {Header: "邮箱", Field: "Email", Width: 30}, + {Header: "创建时间", Field: "CreatedAt", Width: 20}, + {Header: "状态", Field: "Status", Width: 10}, + } + + err := fac.ExportToExcelFile("users.xlsx", "用户列表", columns, users) + if err != nil { + log.Printf("Failed to export to file: %v", err) + } else { + fmt.Println("Excel file exported successfully: users.xlsx") + } +} + +// 示例2:导出到HTTP响应 +func example2(fac *factory.Factory) { + // 模拟HTTP响应 + w := &mockResponseWriter{} + + users := []User{ + {ID: 1, Name: "Alice", Email: "alice@example.com", CreatedAt: time.Now(), Status: 1}, + {ID: 2, Name: "Bob", Email: "bob@example.com", CreatedAt: time.Now(), Status: 1}, + } + + columns := []factory.ExportColumn{ + {Header: "ID", Field: "ID"}, + {Header: "姓名", Field: "Name"}, + {Header: "邮箱", Field: "Email"}, + } + + // 设置HTTP响应头 + w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + w.Header().Set("Content-Disposition", "attachment; filename=users.xlsx") + + err := fac.ExportToExcel(w, "用户列表", columns, users) + if err != nil { + log.Printf("Failed to export to HTTP response: %v", err) + } else { + fmt.Printf("Excel exported to HTTP response successfully, size: %d bytes\n", len(w.data)) + } +} + +// 示例3:使用格式化函数 +func example3(fac *factory.Factory) { + users := []User{ + {ID: 1, Name: "Alice", Email: "alice@example.com", CreatedAt: time.Now(), Status: 1}, + {ID: 2, Name: "Bob", Email: "bob@example.com", CreatedAt: time.Now().Add(-24 * time.Hour), Status: 0}, + } + + columns := []factory.ExportColumn{ + {Header: "ID", Field: "ID", Width: 10}, + {Header: "姓名", Field: "Name", Width: 20}, + {Header: "邮箱", Field: "Email", Width: 30}, + { + Header: "创建时间", + Field: "CreatedAt", + Width: 20, + Format: excel.FormatDateTimeDefault, // 使用便捷的格式化函数 + }, + { + Header: "状态", + Field: "Status", + Width: 10, + Format: func(value interface{}) string { + // 自定义格式化函数 + if status, ok := value.(int); ok { + if status == 1 { + return "启用" + } + return "禁用" + } + return "" + }, + }, + } + + err := fac.ExportToExcelFile("users_formatted.xlsx", "用户列表", columns, users) + if err != nil { + log.Printf("Failed to export with format: %v", err) + } else { + fmt.Println("Excel file exported with format successfully: users_formatted.xlsx") + } +} + +// 示例4:使用ExportData接口 +func example4(fac *factory.Factory) { + // 创建实现了ExportData接口的数据对象 + exportData := &UserExportData{ + users: []User{ + {ID: 1, Name: "Alice", Email: "alice@example.com", CreatedAt: time.Now(), Status: 1}, + {ID: 2, Name: "Bob", Email: "bob@example.com", CreatedAt: time.Now(), Status: 1}, + }, + } + + // 使用接口方法获取列定义和数据 + columns := exportData.GetExportColumns() + + err := fac.ExportToExcelFile("users_interface.xlsx", "用户列表", columns, exportData) + if err != nil { + log.Printf("Failed to export with interface: %v", err) + } else { + fmt.Println("Excel file exported with interface successfully: users_interface.xlsx") + } +} + +// UserExportData 实现了ExportData接口的用户导出数据 +type UserExportData struct { + users []User +} + +// GetExportColumns 获取导出列定义 +func (d *UserExportData) GetExportColumns() []excel.ExportColumn { + return []excel.ExportColumn{ + {Header: "ID", Field: "ID", Width: 10}, + {Header: "姓名", Field: "Name", Width: 20}, + {Header: "邮箱", Field: "Email", Width: 30}, + { + Header: "创建时间", + Field: "CreatedAt", + Width: 20, + Format: excel.FormatDateTimeDefault, + }, + { + Header: "状态", + Field: "Status", + Width: 10, + Format: func(value interface{}) string { + if status, ok := value.(int); ok { + if status == 1 { + return "启用" + } + return "禁用" + } + return "" + }, + }, + } +} + +// GetExportRows 获取导出数据行 +func (d *UserExportData) GetExportRows() [][]interface{} { + rows := make([][]interface{}, 0, len(d.users)) + for _, user := range d.users { + row := []interface{}{ + user.ID, + user.Name, + user.Email, + user.CreatedAt, + user.Status, + } + rows = append(rows, row) + } + return rows +} + +// mockResponseWriter 模拟HTTP响应写入器(用于示例) +type mockResponseWriter struct { + header http.Header + data []byte +} + +func (w *mockResponseWriter) Header() http.Header { + if w.header == nil { + w.header = make(http.Header) + } + return w.header +} + +func (w *mockResponseWriter) Write(data []byte) (int, error) { + w.data = append(w.data, data...) + return len(data), nil +} + +func (w *mockResponseWriter) WriteHeader(statusCode int) { + // 模拟实现 +} + diff --git a/excel/excel.go b/excel/excel.go new file mode 100644 index 0000000..43e65fa --- /dev/null +++ b/excel/excel.go @@ -0,0 +1,481 @@ +package excel + +import ( + "fmt" + "io" + "reflect" + "time" + + "github.com/xuri/excelize/v2" +) + +// Excel Excel导出器 +type Excel struct { + file *excelize.File +} + +// NewExcel 创建Excel导出器 +func NewExcel() *Excel { + return &Excel{ + file: excelize.NewFile(), + } +} + +// ExportColumn 导出列定义 +type ExportColumn struct { + // Header 表头名称 + Header string + // Field 数据字段名(支持嵌套字段,如 "User.Name") + Field string + // Width 列宽(可选,0表示自动) + Width float64 + // Format 格式化函数(可选,用于自定义字段值的格式化) + Format func(value interface{}) string +} + +// ExportData 导出数据接口 +// 实现此接口的结构体可以直接导出 +type ExportData interface { + // GetExportColumns 获取导出列定义 + GetExportColumns() []ExportColumn + // GetExportRows 获取导出数据行 + GetExportRows() [][]interface{} +} + +// ExportToWriter 导出数据到Writer(黑盒模式,推荐使用) +// sheetName: 工作表名称(可选,默认为"Sheet1") +// columns: 列定义 +// data: 数据列表(可以是结构体切片或实现了ExportData接口的对象) +// 返回错误信息 +// +// 示例1:导出结构体切片 +// +// type User struct { +// ID int `json:"id"` +// Name string `json:"name"` +// Email string `json:"email"` +// } +// +// users := []User{ +// {ID: 1, Name: "Alice", Email: "alice@example.com"}, +// {ID: 2, Name: "Bob", Email: "bob@example.com"}, +// } +// +// columns := []ExportColumn{ +// {Header: "ID", Field: "ID"}, +// {Header: "姓名", Field: "Name"}, +// {Header: "邮箱", Field: "Email"}, +// } +// +// excel := excel.NewExcel() +// err := excel.ExportToWriter(w, "用户列表", columns, users) +// +// 示例2:使用格式化函数 +// +// columns := []ExportColumn{ +// {Header: "ID", Field: "ID"}, +// {Header: "姓名", Field: "Name"}, +// {Header: "创建时间", Field: "CreatedAt", Format: func(v interface{}) string { +// if t, ok := v.(time.Time); ok { +// return t.Format("2006-01-02 15:04:05") +// } +// return "" +// }}, +// } +// +// excel.ExportToWriter(w, "用户列表", columns, users) +func (e *Excel) ExportToWriter(w io.Writer, sheetName string, columns []ExportColumn, data interface{}) error { + if e.file == nil { + e.file = excelize.NewFile() + } + + // 设置工作表名称 + if sheetName == "" { + sheetName = "Sheet1" + } + + // 删除默认工作表(如果存在) + index, err := e.file.GetSheetIndex("Sheet1") + if err == nil && index > 0 { + e.file.DeleteSheet("Sheet1") + } + + // 创建新工作表 + _, err = e.file.NewSheet(sheetName) + if err != nil { + return fmt.Errorf("failed to create sheet: %w", err) + } + + // 设置活动工作表 + sheetIndex, err := e.file.GetSheetIndex(sheetName) + if err == nil && sheetIndex > 0 { + e.file.SetActiveSheet(sheetIndex) + } + + // 写入表头 + headerStyle, _ := e.file.NewStyle(&excelize.Style{ + Font: &excelize.Font{ + Bold: true, + Size: 12, + }, + Fill: excelize.Fill{ + Type: "pattern", + Color: []string{"#E0E0E0"}, + Pattern: 1, + }, + Alignment: &excelize.Alignment{ + Horizontal: "center", + Vertical: "center", + }, + }) + + for i, col := range columns { + cell := fmt.Sprintf("%c1", 'A'+i) + e.file.SetCellValue(sheetName, cell, col.Header) + + // 设置表头样式 + e.file.SetCellStyle(sheetName, cell, cell, headerStyle) + + // 设置列宽 + if col.Width > 0 { + colName, _ := excelize.ColumnNumberToName(i + 1) + e.file.SetColWidth(sheetName, colName, colName, col.Width) + } + } + + // 处理数据 + var rows [][]interface{} + + // 检查数据是否实现了ExportData接口 + if exportData, ok := data.(ExportData); ok { + // 使用接口方法获取数据 + rows = exportData.GetExportRows() + } else { + // 处理结构体切片 + rows, err = e.convertDataToRows(data, columns) + if err != nil { + return fmt.Errorf("failed to convert data to rows: %w", err) + } + } + + // 写入数据行 + for rowIndex, row := range rows { + for colIndex, value := range row { + cell := fmt.Sprintf("%c%d", 'A'+colIndex, rowIndex+2) // +2 因为第一行是表头 + + // 应用格式化函数 + var cellValue interface{} = value + if colIndex < len(columns) && columns[colIndex].Format != nil { + cellValue = columns[colIndex].Format(value) + } + + e.file.SetCellValue(sheetName, cell, cellValue) + } + } + + // 自动调整列宽(如果未设置宽度) + for i, col := range columns { + if col.Width == 0 { + colName, _ := excelize.ColumnNumberToName(i + 1) + // 获取列的最大宽度 + maxWidth := e.getColumnMaxWidth(sheetName, i+1, len(rows)+1) + if maxWidth > 0 { + e.file.SetColWidth(sheetName, colName, colName, maxWidth) + } + } + } + + // 写入到Writer + return e.file.Write(w) +} + +// ExportToFile 导出数据到文件(黑盒模式,推荐使用) +// filePath: 文件路径 +// sheetName: 工作表名称(可选,默认为"Sheet1") +// columns: 列定义 +// data: 数据列表 +// 返回错误信息 +// +// 示例: +// +// excel := excel.NewExcel() +// err := excel.ExportToFile("users.xlsx", "用户列表", columns, users) +func (e *Excel) ExportToFile(filePath string, sheetName string, columns []ExportColumn, data interface{}) error { + if e.file == nil { + e.file = excelize.NewFile() + } + + // 设置工作表名称 + if sheetName == "" { + sheetName = "Sheet1" + } + + // 删除默认工作表(如果存在) + index, err := e.file.GetSheetIndex("Sheet1") + if err == nil && index > 0 { + e.file.DeleteSheet("Sheet1") + } + + // 创建新工作表 + _, err = e.file.NewSheet(sheetName) + if err != nil { + return fmt.Errorf("failed to create sheet: %w", err) + } + + // 设置活动工作表 + sheetIndex, err := e.file.GetSheetIndex(sheetName) + if err == nil && sheetIndex > 0 { + e.file.SetActiveSheet(sheetIndex) + } + + // 写入表头 + headerStyle, _ := e.file.NewStyle(&excelize.Style{ + Font: &excelize.Font{ + Bold: true, + Size: 12, + }, + Fill: excelize.Fill{ + Type: "pattern", + Color: []string{"#E0E0E0"}, + Pattern: 1, + }, + Alignment: &excelize.Alignment{ + Horizontal: "center", + Vertical: "center", + }, + }) + + for i, col := range columns { + cell := fmt.Sprintf("%c1", 'A'+i) + e.file.SetCellValue(sheetName, cell, col.Header) + + // 设置表头样式 + e.file.SetCellStyle(sheetName, cell, cell, headerStyle) + + // 设置列宽 + if col.Width > 0 { + colName, _ := excelize.ColumnNumberToName(i + 1) + e.file.SetColWidth(sheetName, colName, colName, col.Width) + } + } + + // 处理数据 + var rows [][]interface{} + + // 检查数据是否实现了ExportData接口 + if exportData, ok := data.(ExportData); ok { + // 使用接口方法获取数据 + rows = exportData.GetExportRows() + } else { + // 处理结构体切片 + rows, err = e.convertDataToRows(data, columns) + if err != nil { + return fmt.Errorf("failed to convert data to rows: %w", err) + } + } + + // 写入数据行 + for rowIndex, row := range rows { + for colIndex, value := range row { + cell := fmt.Sprintf("%c%d", 'A'+colIndex, rowIndex+2) // +2 因为第一行是表头 + + // 应用格式化函数 + var cellValue interface{} = value + if colIndex < len(columns) && columns[colIndex].Format != nil { + cellValue = columns[colIndex].Format(value) + } + + e.file.SetCellValue(sheetName, cell, cellValue) + } + } + + // 自动调整列宽(如果未设置宽度) + for i, col := range columns { + if col.Width == 0 { + colName, _ := excelize.ColumnNumberToName(i + 1) + // 获取列的最大宽度 + maxWidth := e.getColumnMaxWidth(sheetName, i+1, len(rows)+1) + if maxWidth > 0 { + e.file.SetColWidth(sheetName, colName, colName, maxWidth) + } + } + } + + // 保存文件 + return e.file.SaveAs(filePath) +} + +// GetFile 获取Excel文件对象(高级功能时使用) +// 返回excelize.File对象,可用于高级操作 +// +// ℹ️ 推荐使用黑盒方法: +// - ExportToWriter():导出到Writer +// - ExportToFile():导出到文件 +// +// 仅在需要使用高级功能时获取对象: +// - 多工作表操作 +// - 自定义样式 +// - 图表、公式等高级功能 +// +// 示例(常用操作,推荐): +// +// excel := excel.NewExcel() +// excel.ExportToFile("users.xlsx", "用户列表", columns, users) +// +// 示例(高级功能): +// +// file := excel.GetFile() +// file.NewSheet("Sheet2") +// file.SetCellValue("Sheet2", "A1", "数据") +func (e *Excel) GetFile() *excelize.File { + if e.file == nil { + e.file = excelize.NewFile() + } + return e.file +} + +// convertDataToRows 将数据转换为行数据 +func (e *Excel) convertDataToRows(data interface{}, columns []ExportColumn) ([][]interface{}, error) { + // 使用反射处理数据 + val := reflect.ValueOf(data) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + if val.Kind() != reflect.Slice { + return nil, fmt.Errorf("data must be a slice") + } + + rows := make([][]interface{}, 0, val.Len()) + + for i := 0; i < val.Len(); i++ { + item := val.Index(i) + if item.Kind() == reflect.Ptr { + item = item.Elem() + } + + row := make([]interface{}, len(columns)) + for j, col := range columns { + value := e.getFieldValue(item, col.Field) + row[j] = value + } + rows = append(rows, row) + } + + return rows, nil +} + +// getFieldValue 获取字段值(支持嵌套字段) +func (e *Excel) getFieldValue(v reflect.Value, fieldPath string) interface{} { + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + if v.Kind() != reflect.Struct { + return nil + } + + // 处理嵌套字段(如 "User.Name") + fields := splitFieldPath(fieldPath) + current := v + + for i, fieldName := range fields { + if current.Kind() == reflect.Ptr { + current = current.Elem() + } + + if current.Kind() != reflect.Struct { + return nil + } + + field := current.FieldByName(fieldName) + if !field.IsValid() { + return nil + } + + // 如果是最后一个字段,返回值 + if i == len(fields)-1 { + if field.CanInterface() { + return field.Interface() + } + return nil + } + + // 继续嵌套查找 + current = field + } + + return nil +} + +// splitFieldPath 分割字段路径(如 "User.Name" -> ["User", "Name"]) +func splitFieldPath(path string) []string { + result := make([]string, 0) + current := "" + for _, char := range path { + if char == '.' { + if current != "" { + result = append(result, current) + current = "" + } + } else { + current += string(char) + } + } + if current != "" { + result = append(result, current) + } + return result +} + +// getColumnMaxWidth 获取列的最大宽度(用于自动调整列宽) +func (e *Excel) getColumnMaxWidth(sheetName string, colIndex int, maxRow int) float64 { + maxWidth := 10.0 // 默认最小宽度 + + for row := 1; row <= maxRow; row++ { + cell, _ := excelize.CoordinatesToCellName(colIndex, row) + value, err := e.file.GetCellValue(sheetName, cell) + if err == nil { + width := float64(len(value)) + 2 // 加2作为边距 + if width > maxWidth { + maxWidth = width + } + } + } + + // 限制最大宽度 + if maxWidth > 50 { + maxWidth = 50 + } + + return maxWidth +} + +// FormatDateTime 格式化日期时间(便捷函数) +// 用于ExportColumn的Format字段 +func FormatDateTime(layout string) func(interface{}) string { + return func(value interface{}) string { + if t, ok := value.(time.Time); ok { + return t.Format(layout) + } + return "" + } +} + +// FormatDate 格式化日期(便捷函数) +// 用于ExportColumn的Format字段,格式:2006-01-02 +func FormatDate(value interface{}) string { + if t, ok := value.(time.Time); ok { + return t.Format("2006-01-02") + } + return "" +} + +// FormatDateTimeDefault 格式化日期时间(便捷函数) +// 用于ExportColumn的Format字段,格式:2006-01-02 15:04:05 +func FormatDateTimeDefault(value interface{}) string { + if t, ok := value.(time.Time); ok { + return t.Format("2006-01-02 15:04:05") + } + return "" +} diff --git a/factory/factory.go b/factory/factory.go index 3d86267..ed72117 100644 --- a/factory/factory.go +++ b/factory/factory.go @@ -10,6 +10,7 @@ import ( "git.toowon.com/jimmy/go-common/config" "git.toowon.com/jimmy/go-common/email" + "git.toowon.com/jimmy/go-common/excel" commonhttp "git.toowon.com/jimmy/go-common/http" "git.toowon.com/jimmy/go-common/i18n" "git.toowon.com/jimmy/go-common/logger" @@ -121,6 +122,7 @@ type Factory struct { db *gorm.DB // 数据库连接(延迟初始化) redis *redis.Client // Redis客户端(延迟初始化) i18n *i18n.I18n // 国际化工具(延迟初始化) + excel *excel.Excel // Excel导出器(延迟初始化) } // NewFactory 创建工厂实例 @@ -1553,3 +1555,109 @@ func (f *Factory) GetMessage(lang, code string, args ...interface{}) string { func (f *Factory) GetI18n() (*i18n.I18n, error) { return f.getI18n() } + +// ========== Excel 导出工具(黑盒模式,推荐使用) ========== +// +// 这些方法提供数据导出到Excel的功能,支持结构体切片和自定义数据格式。 + +// getExcelClient 获取Excel导出器实例(内部方法,延迟初始化) +func (f *Factory) getExcelClient() (*excel.Excel, error) { + if f.excel != nil { + return f.excel, nil + } + + f.excel = excel.NewExcel() + return f.excel, nil +} + +// GetExcel 获取Excel导出器对象(高级功能时使用) +// 返回已初始化的Excel导出器对象 +// +// ℹ️ 推荐使用黑盒方法: +// - ExportToExcel():导出到Writer +// - ExportToExcelFile():导出到文件 +// +// 仅在需要使用高级功能时获取对象: +// - 多工作表操作 +// - 自定义样式 +// - 图表、公式等高级功能 +// +// 示例(常用操作,推荐): +// +// fac.ExportToExcel(w, "用户列表", columns, users) +// +// 示例(高级功能): +// +// excel, _ := fac.GetExcel() +// file := excel.GetFile() +// file.NewSheet("Sheet2") +func (f *Factory) GetExcel() (*excel.Excel, error) { + return f.getExcelClient() +} + +// ExportColumn Excel导出列定义(暴露给外部项目使用) +// 外部项目可以直接使用 factory.ExportColumn 创建列定义 +type ExportColumn = excel.ExportColumn + +// ExportToExcel 导出数据到Writer(黑盒模式,推荐使用) +// w: Writer对象(如http.ResponseWriter) +// sheetName: 工作表名称(可选,默认为"Sheet1") +// columns: 列定义 +// data: 数据列表(可以是结构体切片或实现了ExportData接口的对象) +// 返回错误信息 +// +// 示例1:导出结构体切片 +// +// type User struct { +// ID int `json:"id"` +// Name string `json:"name"` +// Email string `json:"email"` +// } +// +// users := []User{ +// {ID: 1, Name: "Alice", Email: "alice@example.com"}, +// {ID: 2, Name: "Bob", Email: "bob@example.com"}, +// } +// +// columns := []factory.ExportColumn{ +// {Header: "ID", Field: "ID"}, +// {Header: "姓名", Field: "Name"}, +// {Header: "邮箱", Field: "Email"}, +// } +// +// fac.ExportToExcel(w, "用户列表", columns, users) +// +// 示例2:使用格式化函数 +// +// columns := []factory.ExportColumn{ +// {Header: "ID", Field: "ID"}, +// {Header: "姓名", Field: "Name"}, +// {Header: "创建时间", Field: "CreatedAt", Format: excel.FormatDateTimeDefault}, +// } +// +// fac.ExportToExcel(w, "用户列表", columns, users) +func (f *Factory) ExportToExcel(w io.Writer, sheetName string, columns []ExportColumn, data interface{}) error { + e, err := f.getExcelClient() + if err != nil { + return err + } + return e.ExportToWriter(w, sheetName, columns, data) +} + +// ExportToExcelFile 导出数据到文件(黑盒模式,推荐使用) +// filePath: 文件路径 +// sheetName: 工作表名称(可选,默认为"Sheet1") +// columns: 列定义 +// data: 数据列表 +// 返回错误信息 +// +// 示例: +// +// fac.ExportToExcelFile("users.xlsx", "用户列表", columns, users) +func (f *Factory) ExportToExcelFile(filePath string, sheetName string, columns []ExportColumn, data interface{}) error { + e, err := f.getExcelClient() + if err != nil { + return err + } + return e.ExportToFile(filePath, sheetName, columns, data) +} diff --git a/go.mod b/go.mod index 751bea9..8f29c6e 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,14 @@ module git.toowon.com/jimmy/go-common -go 1.23.0 +go 1.24.0 toolchain go1.24.10 require ( github.com/minio/minio-go/v7 v7.0.97 github.com/redis/go-redis/v9 v9.17.1 + github.com/xuri/excelize/v2 v2.10.0 + golang.org/x/crypto v0.43.0 gorm.io/driver/mysql v1.5.2 gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 @@ -34,13 +36,17 @@ require ( github.com/minio/crc64nvme v1.1.0 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/philhofer/fwd v1.2.0 // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.4 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/xid v1.6.0 // indirect + github.com/tiendc/go-deepcopy v1.7.1 // indirect github.com/tinylib/msgp v1.3.0 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/text v0.26.0 // indirect + github.com/xuri/efp v0.0.1 // indirect + github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 15e0ee8..a44a638 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,11 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/redis/go-redis/v9 v9.17.1 h1:7tl732FjYPRT9H9aNfyTwKg9iTETjWjGKEJ2t/5iWTs= github.com/redis/go-redis/v9 v9.17.1/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= @@ -63,20 +68,30 @@ github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4= +github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= +github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4= +github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=