commit 8f1ea1d5accb1980928518eb60d82fbfca33c0d1
Author: renzhiyuan <465386466@qq.com>
Date:   Wed Feb 26 20:51:18 2025 +0800

    first push

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"`
+	}
+)