From 8f1ea1d5accb1980928518eb60d82fbfca33c0d1 Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Wed, 26 Feb 2025 20:51:18 +0800 Subject: [PATCH] first push --- config.go | 9 ++ constant.go | 17 ++++ export.go | 217 ++++++++++++++++++++++++++++++++++++++++++++ export_err/error.go | 12 +++ export_test.go | 63 +++++++++++++ go.mod | 16 ++++ option.go | 47 ++++++++++ pkg/pkg.go | 116 +++++++++++++++++++++++ types/types.go | 13 +++ 9 files changed, 510 insertions(+) create mode 100644 config.go create mode 100644 constant.go create mode 100644 export.go create mode 100644 export_err/error.go create mode 100644 export_test.go create mode 100644 go.mod create mode 100644 option.go create mode 100644 pkg/pkg.go create mode 100644 types/types.go diff --git a/config.go b/config.go new file mode 100644 index 0000000..040a54d --- /dev/null +++ b/config.go @@ -0,0 +1,9 @@ +package excel_export + +type Config struct { + Data interface{} + FileName string + Ext Ext + SavePath string + Head []*FiledMapping +} diff --git a/constant.go b/constant.go new file mode 100644 index 0000000..0c9a720 --- /dev/null +++ b/constant.go @@ -0,0 +1,17 @@ +package excel_export + +type Ext string + +const ( + Xlam Ext = ".xlam" + Xlsm Ext = ".xlsm" + Xlsx Ext = ".xlsx" + Xltm Ext = ".xltm" +) + +const ( + Err int8 = 0 + Init int8 = 1 + Running int8 = 2 + Finish int8 = 3 +) diff --git a/export.go b/export.go new file mode 100644 index 0000000..8c29e91 --- /dev/null +++ b/export.go @@ -0,0 +1,217 @@ +package excel_export + +import ( + "encoding/json" + "fmt" + "gitea.cdlsxd.cn/self-tools/l_excel_export/pkg" + "gitea.cdlsxd.cn/self-tools/l_excel_export/types" + "time" + + "gitea.cdlsxd.cn/self-tools/l_excel_export/export_err" + "github.com/xuri/excelize/v2" + "os" +) + +type ExportExcel struct { + config *Config + password string + saveFunc func(file *excelize.File) (url string, err error) + sheetName string + logPath string + jobName string + task *types.Task + + exportData [][]interface{} + header []interface{} +} + +func NewExport(config *Config, opts ...Option) (*ExportExcel, error) { + export := &ExportExcel{ + config: config, + } + for _, opt := range opts { + opt(export) // 应用选项 + } + err := export.check() + return export, err +} + +func (e *ExportExcel) Run() (taskId string, err error) { + err = e.init() + if err != nil { + return + } + f, err := e.getFile() + if err != nil { + return + } + go func(f *excelize.File) { + defer func() { + if r := recover(); r != nil { + f.Close() + fmt.Println(r) + } + }() + e.task.Status = Running + e.updateTask() + err = e.run(f) + if err != nil { + e.task.Status = Err + e.updateTask() + return + } + e.task.Ftime = time.Now().Format(time.DateTime) + e.finish(f) + }(f) + return e.task.TaskId, nil +} + +func (e *ExportExcel) run(f *excelize.File) (err error) { + + index, err := f.NewStreamWriter(e.getSheetName()) + if err != nil { + return err + } + index.SetRow("A1", e.header) + if err != nil { + return err + } + count := len(e.exportData) + for i, v := range e.exportData { + cell, _ := excelize.CoordinatesToCellName(1, i+2) + index.SetRow(cell, v) + e.task.Process = i * 100 / count + e.updateTask() + } + + if err = e.save(f); err != nil { + return err + } + return err +} + +func (e *ExportExcel) finish(f *excelize.File) { + f.Close() + e.task.Status = Finish + e.task.Process = 100 + e.updateTask() +} + +func (e *ExportExcel) save(f *excelize.File) error { + e.task.Url = e.getUrl() + return f.SaveAs(e.task.Url) +} + +func (e *ExportExcel) getUrl() string { + return fmt.Sprintf("%s/%s_%s%s", e.config.SavePath, e.config.FileName, e.task.TaskId, e.config.Ext) +} + +func (e *ExportExcel) init() error { + e.task = &types.Task{ + TaskId: pkg.CreateTaskId(), + Process: 0, + Url: "", + Ctime: time.Now().Format(time.DateTime), + Ftime: "", + Status: Init, + FileAddr: "", + } + err := e.updateTask() + if err != nil { + return err + } + dataMap := pkg.GetData(e.config.Data) + if len(dataMap) == 0 { + return export_err.ErrDataError + } + + if len(e.config.Head) == 0 { + return export_err.ErrHeadNotSet + } + + for _, v := range e.config.Head { + + e.header = append(e.header, v.ColName) + } + + //todo:这一步目的是为了保证数据与header未知一致,但是在大数据量的时候存在性能问题 + for _, v := range dataMap { + var ( + slice []interface{} + ) + for _, vv := range e.config.Head { + slice = append(slice, v[vv.FieldName]) + } + e.exportData = append(e.exportData, slice) + } + //检测文件是否存在 + if _, err = os.Stat(e.config.SavePath); os.IsNotExist(err) { + // 文件夹不存在,尝试创建 + err = os.MkdirAll(e.config.SavePath, os.ModePerm) + if err != nil { + return export_err.ErrCreateExcelPathFail + } + } else if err != nil { + return export_err.ErrCheckExcelPathFail + } + + return nil +} + +func (e *ExportExcel) updateTask() error { + + file, err := os.OpenFile(e.logFile(), os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0766) + if err != nil { + return err + } + defer file.Close() + + taskInfo, err := json.Marshal(e.task) + if err != nil { + return err + } + jsonInfo := string(taskInfo) + _, err = file.WriteString(jsonInfo) + if err != nil { + return err + } + return nil +} + +func (e *ExportExcel) logFile() string { + return fmt.Sprintf("%s/%s", e.logPath, e.task.TaskId) +} + +func (e *ExportExcel) getFile() (*excelize.File, error) { + f, err := excelize.OpenFile(e.path()) + if err != nil { + f = excelize.NewFile() + } + return f, nil +} + +func (e *ExportExcel) path() string { + return fmt.Sprintf("%s/%s.%s", e.config.SavePath, e.config.FileName, e.config.Ext) +} + +func (e *ExportExcel) check() (err error) { + if len(e.jobName) == 0 { + e.jobName = time.Now().Format("default") + } + if e.config.SavePath == "" && e.saveFunc == nil { + return export_err.ErrNotSetSaveWay + } + + if len(e.logPath) == 0 { + e.logPath, err = pkg.DefaultLogPath(e.jobName) + return + } + return +} + +func (e *ExportExcel) getSheetName() string { + if len(e.sheetName) == 0 { + e.sheetName = "Sheet1" + } + return e.sheetName +} diff --git a/export_err/error.go b/export_err/error.go new file mode 100644 index 0000000..481ffb2 --- /dev/null +++ b/export_err/error.go @@ -0,0 +1,12 @@ +package export_err + +import "errors" + +var ( + ErrNotSetSaveWay = errors.New("请设置文件保存路径SavePath或保存方式WithSaveFunc") + ErrFileNotExist = errors.New("文件不存在") + ErrDataError = errors.New("导出数据不存在或格式错误") + ErrHeadNotSet = errors.New("数据头Header为空") + ErrCreateExcelPathFail = errors.New("创建excel存放文件夹失败") + ErrCheckExcelPathFail = errors.New("检测excel存放文件夹失败") +) diff --git a/export_test.go b/export_test.go new file mode 100644 index 0000000..06b02a9 --- /dev/null +++ b/export_test.go @@ -0,0 +1,63 @@ +package excel_export + +import ( + "fmt" + "github.com/xuri/excelize/v2" + "os" + "testing" +) + +func TestExport(t *testing.T) { + f, err := excelize.OpenFile("./a.xlsx") + if err == os.ErrNotExist { + fmt.Println(111) + } + _ = fmt.Sprint(f, err) +} + +type Order struct { + OrderNum string `json:"order_num"` + OrderID int `json:"order_id"` + CusNum string `json:"cus_num"` +} + +func TestData(t *testing.T) { + var c = []interface{}{ + Order{OrderNum: "aasdsad", OrderID: 1, CusNum: "hghghfg"}, + Order{OrderNum: "vqewqewq", OrderID: 2, CusNum: "iuyiuyiyu"}, + Order{OrderNum: "adfgf", OrderID: 3, CusNum: "ewewew"}, + Order{OrderNum: "abbbb", OrderID: 4, CusNum: "xcxcxc"}, + Order{OrderNum: "aqwewqeqw", OrderID: 5, CusNum: "fdfdfd"}, + Order{OrderNum: "ahhhhhh", OrderID: 6, CusNum: "asdadsa"}, + Order{OrderNum: "vvvvv", OrderID: 7, CusNum: "vcvcv"}, + Order{OrderNum: "aaaa", OrderID: 8, CusNum: "asdasdwqewqe"}, + Order{OrderNum: "wwww", OrderID: 9, CusNum: "bvbvbvbvbv"}, + Order{OrderNum: "ffff", OrderID: 10, CusNum: "gfgfgf"}, + Order{OrderNum: "tttt", OrderID: 11, CusNum: "zxczczxczxczx"}, + Order{OrderNum: "hhhh", OrderID: 12, CusNum: "gjhfgjghjgh"}, + Order{OrderNum: "bbvbv", OrderID: 13, CusNum: "ytytytuty"}, + Order{OrderNum: "zcxzczx", OrderID: 14, CusNum: "rqwrqrqrqrqr"}, + Order{OrderNum: "asdasd", OrderID: 15, CusNum: "asdzczxfaxc"}, + } + + out, err := NewExport(&Config{ + FileName: "a", + Data: c, + Ext: Xlsx, + Head: []*FiledMapping{ + {FieldName: "order_num", ColName: "订单编号"}, + {FieldName: "order_id", ColName: "订单id"}, + {FieldName: "cus_num", ColName: "顾客编号 "}, + }, + SavePath: "./path", + }) + if err != nil { + panic(err) + } + task_id, err := out.Run() + if err != nil { + panic(err) + } + fmt.Println(task_id) + select {} +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e3c5c14 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module gitea.cdlsxd.cn/self-tools/l_excel_export + +go 1.22.2 + +require github.com/xuri/excelize/v2 v2.9.0 + +require ( + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.4 // indirect + github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect + github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/text v0.19.0 // indirect +) diff --git a/option.go b/option.go new file mode 100644 index 0000000..e43e323 --- /dev/null +++ b/option.go @@ -0,0 +1,47 @@ +package excel_export + +import "github.com/xuri/excelize/v2" + +type ( + Option func(export *ExportExcel) + + FiledMapping struct { + FieldName string `json:"filed_name"` //原字段 + ColName string `json:"col_name"` //excel列名 + } +) + +// WithPassword excel打开密码 +func WithPassword(password string) Option { + return func(s *ExportExcel) { + s.password = password + } +} + +// WithSaveFunc 抛出file文件交由外部处理 +func WithSaveFunc(saveFunc func(file *excelize.File) (url string, err error)) Option { + return func(s *ExportExcel) { + s.saveFunc = saveFunc + } +} + +// WithSheet excel的sheet名称 +func WithSheet(sheetName string) Option { + return func(s *ExportExcel) { + s.sheetName = sheetName + } +} + +// WithLogPath 日志保存文件 +func WithLogPath(logPath string) Option { + return func(s *ExportExcel) { + s.logPath = logPath + } +} + +// WithJobName 任务名称 +func WithJobName(name string) Option { + return func(s *ExportExcel) { + s.jobName = name + } +} diff --git a/pkg/pkg.go b/pkg/pkg.go new file mode 100644 index 0000000..bdbd9b9 --- /dev/null +++ b/pkg/pkg.go @@ -0,0 +1,116 @@ +package pkg + +import ( + "encoding/json" + "fmt" + "os" + "reflect" + "strings" + "time" +) + +func DefaultLogPath(jobName string) (string, error) { + path, err := os.Getwd() + path = fmt.Sprintf("%s/%s/%s", path, "log/export", jobName) + err = CheckDir(path) + return path, err +} + +func CheckDir(path string) error { + // 判断目录是否存在 + if _, err := os.Stat(path); os.IsNotExist(err) { + // 如果目录不存在,则创建它 + err = os.MkdirAll(path, os.ModePerm) + if err != nil { + return err + } + } else if err != nil { + // 如果Stat返回了其他错误(比如权限问题) + return err + } + return nil +} + +func CreateTaskId() string { + return fmt.Sprintf("%d", time.Now().UnixNano()) +} + +// toMap 将结构体转换为map[string]interface{} +// StructToMap 将一个struct转换为map[string]interface{} +func StructToMap(obj interface{}) map[string]interface{} { + // 获取obj的类型 + val := reflect.ValueOf(obj) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + // 确保obj是一个struct + if val.Kind() != reflect.Struct { + return nil + } + + // 创建一个map来保存结果 + data := make(map[string]interface{}) + + // 遍历struct的字段 + for i := 0; i < val.NumField(); i++ { + // 获取字段的类型和值 + valueField := val.Field(i) + typeField := val.Type().Field(i) + jsonTag := typeField.Tag.Get("json") + if idx := strings.Index(jsonTag, ","); idx != -1 { + // 如果有逗号,则取逗号之前的部分 + jsonTag = jsonTag[:idx] + } + // 忽略未导出的字段(字段名首字母小写) + if !typeField.IsExported() { + continue + } + + // 将字段名和值添加到map中 + data[jsonTag] = valueField.Interface() + } + + return data +} + +func GetData(data interface{}) (out []map[string]interface{}) { + + switch data.(type) { + case []map[string]interface{}: + for _, item := range data.([]map[string]interface{}) { + out = append(out, item) + } + + case []map[string]string: + for _, maps := range data.([]map[string]string) { + newMap := make(map[string]interface{}) + for key, item := range maps { + newMap[key] = item + } + out = append(out, newMap) + } + case []interface{}: + for _, item := range data.([]interface{}) { + dataMap := StructToMap(item) + if dataMap == nil { + return nil + } + out = append(out, dataMap) + } + case string: + err := json.Unmarshal([]byte(data.(string)), &out) + if err != nil { + return nil + } + case []byte: + err := json.Unmarshal(data.([]byte), &out) + if err != nil { + return nil + } + default: + return nil + } + + return +} diff --git a/types/types.go b/types/types.go new file mode 100644 index 0000000..118b374 --- /dev/null +++ b/types/types.go @@ -0,0 +1,13 @@ +package types + +type ( + Task struct { + TaskId string `json:"task_id"` + Url string `json:"url"` + Process int `json:"process"` + Ctime string `json:"ctime"` + Ftime string `json:"ftime"` + Status int8 `json:"status"` + FileAddr string `json:"file_addr"` + } +)