diff --git a/README.md b/README.md index 0a07ee1..a6515e6 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,93 @@ ## 安装 ```bash -$ go get gitea.cdlsxd.cn/self-tools/l_ai_excel_header_match +$ go get gitea.cdlsxd.cn/self-tools/l-export-async ``` - ## 使用 ```go -func TestAddress(t *testing.T) { - res, err := ExcelMatch(context.Background(), a, b, "", "") - t.Log(res, err) +task, err := export_async.NewExportAsync( +fmt.Sprintf("%s%s", "供应商结算交易流水", time.Now().Format("20060102150405")), +supplierOrderTransRecordsFields(), +s.c.Rpc.GetAttachmentDomain(), +func(ctx context.Context, pageNum, limit int) ([][]interface{}, error) { +req.Page = &api.PageReq{ +Page: int32(pageNum), +PageSize: int32(limit), } +list, err := s.GetSupplierOrderTransRecordList(ctx, req) +if err != nil { +return nil, err +} +if list == nil { +return nil, nil +} +return supplierOrderTransRecordsToCollect(list.List), nil +}, +export_async.NewRedisTaskStore(s.d.Rdb), +export_async.WithLogPrint(s.log), +export_async.WithProcess(count), +export_async.WithMaxRowPerFile(1000000), +).Run(ctx) +return nil, myerr.ErrorParamError("导出失败,请稍后重试:" + err.Error()) +} +return &api.TaskReply{Task: task}, nil -var ( -a = []string{ -"条码", "分类名称", "货品名称", "货品编号", "商品货号", "品牌", "单位", "规格参数", "货品说明", "保质期", "保质期单位", "链接", "货品图片", "电商销售价格", "销售价", "供应商报价", "税率", "默认供应商", "默认存放仓库", "第三方商品编码", "备注", "长", "宽", "高", "重量", "SPU编码", "SPU名称", -} -b = []string{ -"商品名称(手工输入)", "品牌(单选)", "商品型号(手工输入)", "商品条码/ISBN/ISSN(手工输入)", "条形码资质1(手工输入)", "条形码资质2(手工输入)", "条形码资质3(手工输入)", "产地(国家)(单选)", "产地(省份)(单选)", "产地(市)(单选)", "长度(手工输入,单位:毫米)", "宽度(手工输入,单位:毫米)", "高度(手工输入,单位:毫米)", "体积(手工输入,单位:立方厘米)", "毛重(手工输入,单位:千克)", "厂家包装含量(手工输入)", "商品税率(单选)(单选)", "采购单位", "供应商商品编码(手工输入)", "商品详情(手工输入)", "发货清单1(手工输入)", "发货清单2(手工输入)", "发货清单3(手工输入)", "发货清单4(手工输入)", "发货清单5(手工输入)", "商品标题(手工输入)", "商品卖点(手工输入)", "促销常规卖点(手工输入)", "促销常规卖点生效时间", "促销常规卖点失效时间", "促销高级卖点(手工输入)", "促销高级卖点生效时间", "促销高级卖点失效时间", "活动关联文案(手工输入)", "电脑端链接(手工输入)", "移动端链接(手工输入)", "活动链接生效时间", "活动链接失效时间", "商品图片1(手工输入)", "商品图片2(手工输入)", "商品图片3(手工输入)", "商品图片4(手工输入)", "商品图片5(手工输入)", "生产者(制造商)名称(手工输入)", "生产商(制造商)地址(手工输入)", "执行标准(手工输入)", "类别(单选)", "茶具类型(单选)", "国产/进口(单选)", "茶具材质(单选)", "上市时间(月)(手工输入)", "茶盘材质(多选)(可选项为:石质,电木,树脂,陶瓷,竹质,木质,其它)", "工艺(单选)", "风格(单选)", "适用人数(单选)", "功能(单选)", "容量(手工输入,单位:毫升)", -} -) ``` + + +## 主要功能 +1. 异步导出任务管理 +- 使用 context.Context 支持上下文取消和超时控制。 +- 通过 Task 结构体跟踪任务状态(ID、进度、错误信息、数据量等)。 +- 支持任务进度更新和持久化存储(通过 TaskSaveTool 接口)。 +**** +2. 并发处理 +- CSV 导出:将数据分页写入多个 CSV 文件。 +- Excel (XLSX) 导出:将多个 CSV 文件合并为多个 Excel 文件(支持流式处理,避免内存溢出)。 +- ZIP 打包:将生成的 Excel 文件打包为 ZIP 文件。 +**** +3. 数据分页与批量处理 +- 支持分页查询数据(batchSize 控制每页大小) +- 支持限制每个 Excel 文件的最大行数(maxRowPerFile)。 +- 支持 CSV 转 Excel 时的批量写入缓冲区大小(csvToExcelBatch)。 +**** +4. 文件上传 +- 支持将生成的 ZIP 文件上传到指定服务器(通过 Uploader 配置)。 +- 上传后生成可访问的 URL(通过 GeneratePreviewPrivateUrl 生成预览链接)。 +**** +5. 日志记录 +- 通过 LogTool 接口记录任务执行过程中的日志(如进度、错误信息等)。 +**** +6. 临时文件管理 +- 自动创建临时目录(/csv/、/xlsx/、/zip/)并清理。 +- 支持自定义文件扩展名(extension)、Sheet 名称(sheetName)等。 +**** +7. 可扩展性 +- 通过 ExportOption 函数式选项模式支持灵活配置(如自定义上传器、日志工具、工作协程数等)。 +- 通过接口(DataProviderFn、TaskSaveTool、LogTool)支持自定义实现。 + +## 亮点 +1. 高性能与低内存占用 +- 使用流式处理(csv.Writer 和 zip.Writer)避免一次性加载所有数据到内存。 +- 通过分页查询和并发处理减少数据库压力。 +- 使用 sync.Pool 复用 ExportAsync 实例,减少内存分配。 +**** +2. 进度跟踪与任务状态管理 +- 支持任务状态持久化(如存储到 Redis),方便前端查询进度 +**** +3. 灵活的配置 +- 支持通过函数式选项(ExportOption)动态配置导出任务(如自定义上传器、日志工具、工作协程数等)。 +- 默认配置合理(如 batchSize=10000、maxRowPerFile=10000),同时支持覆盖。 +**** +4. 支持大数据量导出 +- 通过分页和并发处理支持导出千万级数据。 +- 支持限制每个 Excel 文件的行数,避免单个文件过大。 +**** + +## 适用场景 +- 需要导出大量数据(如报表、日志、用户数据等)到 Excel 或 CSV 的场景。 +- 需要支持异步导出(避免阻塞主流程)的场景。 +- 需要将导出的文件上传到服务器(如 OSS、FTP 等)的场景。 +- 需要进度跟踪和任务状态管理的场景。 \ No newline at end of file diff --git a/export_async_test.go b/export_async_test.go index dfdd061..3d0d746 100644 --- a/export_async_test.go +++ b/export_async_test.go @@ -1,9 +1,14 @@ package l_export_async import ( + "context" "fmt" "os" + "strings" "testing" + "time" + + "github.com/redis/go-redis/v9" ) func Test_Merge(t *testing.T) { @@ -22,6 +27,30 @@ func Test_Merge(t *testing.T) { t.Log(err) } +func Test_Tsk(t *testing.T) { + taskId := "c1f316c5-defa-11f0-bcc5-00155d5ef0f9" + t.Log(NewTask(NewRedisTaskStore(dat())).GetTaskInfo(context.Background(), taskId)) +} + +func Test_Upload(t *testing.T) { + task_id := "c0abe2b3-dede-11f0-9178-00155d5ef0f92369485728" + filename := "供应商结算交易流水20251222100502" + + file := "/tmp/" + task_id + "/zip/" + filename + ".zip" + //file := "/tmp/a03f82a8-deda-11f0-9c0b-00155d5ef0f9796292421/zip/供应商结算交易流水20251222100502.zip" + + host := "http://192.168.6.194:8004" + sys := "crmApi" + business := "download" + fieldFormName := "file" + resp, err := Upload(host, file, sys, business, fieldFormName) + if err != nil { + t.Log(err) + } + url := GeneratePreviewPrivateUrl(host, "", resp.Url, "", strings.TrimSuffix("供应商结算交易流水20251222100502", ".zip"), time.Now().Unix()+300) + t.Log(url, err) +} + func listFiles(dirPath string) ([]string, error) { entries, err := os.ReadDir(dirPath) if err != nil { @@ -36,3 +65,30 @@ func listFiles(dirPath string) ([]string, error) { } return files, nil } + +var ( + Name = "test" + Version string + id, _ = os.Hostname() +) + +func dat() *redis.Client { + + return buildRdb() +} + +func buildRdb() *redis.Client { + + rdb := redis.NewClient(&redis.Options{ + Addr: Redis.Addr, + Password: Redis.Password, + ReadTimeout: Redis.ReadTimeout.AsDuration(), + WriteTimeout: Redis.WriteTimeout.AsDuration(), + PoolSize: int(Redis.PoolSize), + MinIdleConns: int(Redis.MinIdleConns), + ConnMaxIdleTime: Redis.ConnMaxIdleTime.AsDuration(), + DB: int(GetRedis().GetDb()), + }) + // 此时并没有发起连接,在使用时才会 + return rdb +}