添加异步导出功能,支持大文件处理和进度追踪

This commit is contained in:
renzhiyuan 2025-12-22 20:05:02 +08:00
parent ff5f831b61
commit da11dac738
2 changed files with 138 additions and 13 deletions

View File

@ -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 等)的场景。
- 需要进度跟踪和任务状态管理的场景。

View File

@ -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
}