添加异步导出功能,支持大文件处理和进度追踪
This commit is contained in:
parent
ff5f831b61
commit
da11dac738
95
README.md
95
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 等)的场景。
|
||||
- 需要进度跟踪和任务状态管理的场景。
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue