golang 10w数据流式导出excel并且记录进度条

前序

  1. 流式导出excel网上都有
  2. 并发写入的话需要保证写入的顺序性(A1,A2,A3……)
  3. 所以网上的大部分都是一个循环获取数据,写入数据
  4. 我这里获取数据和写入数据做了拆分,引入缓冲层
  5. 我的这包直接引入项目就可以使用了
  6. progress是进度条的库

代码如下

excel_export.go

package excel_export

import (
	"context"
	"fmt"
	"time"

	"xxx/dmp/demo/common/progress"
	"xxx/dmp/demo/util"
	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/xuri/excelize/v2"
)

const (
	defaultColWidth        = 20.0
	defaultRowHeight       = 17.5
	defaultSheetName       = "Sheet1"
	defaultHeaderStyleSize = 14
	defaultNormalStyleSize = 12
	ExportDir              = "tmp/export"
	ExpireDuration         = time.Hour
)

type ExcelExporterImpl interface {
	Total() (int64, error)
	Headers() []interface{}
	GetData(offset int, limit int) ([][]interface{}, error)
}

type ExcelExportBuilder struct {
	file       *excelize.File
	ProgressTx string // 进度条
	ctx        *gin.Context
}

func init() {
	// 清理文件
	go func() {
		for range time.Tick(time.Minute * 5) {
			util.CleanOldFiles(ExportDir, time.Hour)
		}
	}()
}

func GetExcelExportFilePath(filename string) string {
	return fmt.Sprintf("%s/%s-%s.xlsx", ExportDir, filename, time.Now().Format("200601021504"))
}

func NewExcelExportBuilder(ctx *gin.Context, filepath string) (*ExcelExportBuilder, error) {
	f, width, height := excelize.NewFile(), defaultColWidth, defaultRowHeight
	f.Path = filepath

	if err := f.SetSheetProps(defaultSheetName, &excelize.SheetPropsOptions{
		DefaultColWidth:  &width,
		DefaultRowHeight: &height,
	}); err != nil {
		return nil, err
	}

	return &ExcelExportBuilder{
		file:       f,
		ProgressTx: uuid.New().String(),
		ctx:        ctx,
	}, nil
}

func (builder *ExcelExportBuilder) Save(impl ExcelExporterImpl) error {
	// 关闭文件
	defer builder.file.Close()
	// 打开sheet1
	sw, err := builder.file.NewStreamWriter("Sheet1")
	if err != nil {
		return err
	}
	// 表头样式
	headerStyleID, err := builder.file.NewStyle(&excelize.Style{Font: &excelize.Font{Bold: true, Size: defaultHeaderStyleSize}})
	if err != nil {
		return err
	}
	// 这里设置表头
	headers := impl.Headers()
	err = sw.SetRow("A1", headers, excelize.RowOpts{StyleID: headerStyleID})
	if err != nil {
		return err
	}
	// 获取总量
	totalRows, err := impl.Total()
	if err != nil {
		return err
	}
	// 进度条
	progre := progress.NewProgress(builder.ctx, builder.ProgressTx, totalRows)
	// 创建一个父上下文用于协程之间的协作
	ctx, cancel := context.WithCancel(context.Background())
	var errCtx error
	defer cancel()
	// 并发写入数据
	var batchSize int64 = 1000
	var offset int64
	// 创建一个带有缓冲的通道,用于获取数据缓冲
	type ExportBuffer struct {
		data   [][]interface{}
		offset int64
	}
	// 并发获取数据 --> 放入队列 --> 保证写入数据时顺序 --> 这里是buffer层
	bufferCh := make(chan ExportBuffer, 10) // 这里设置适当的缓冲大小
	go func() {
		defer close(bufferCh) // 关闭通道表示数据获取完成
		for offset < totalRows {
			// 检查上下文是否已取消
			if util.CheckCanceled(ctx) {
				return
			}
			data, err := impl.GetData(int(offset), int(batchSize))
			if err != nil {
				cancel()
				errCtx = err
				return
			}
			bufferCh <- ExportBuffer{
				data:   data,
				offset: offset,
			}
			offset += batchSize
		}
	}()

	// 循环缓冲写入数据
	for buffer := range bufferCh {
		// fmt.Println("buffer.offset:", buffer.offset)
		for index, row := range buffer.data {
			line := buffer.offset + int64(index) + 2
			err := sw.SetRow(fmt.Sprintf("A%d", line), row)
			if err != nil {
				fmt.Println(err)
			}
			// 写入进度
			progre.AddSuccessNumber(1)
		}
	}

	// 检查上下文是否已取消
	if util.CheckCanceled(ctx) {
		progre.SetFail(errCtx.Error())
		return errCtx
	}

	// 写入进度 100%
	progre.SetProgress(100)

	if err = sw.Flush(); err != nil {
		return err
	}

	return builder.file.Save()
}

进度条
progress.go

package progress

import (
	"encoding/json"
	"fmt"
	"sync"
	"time"

	"xxx/dmp/demo/common/global"
	"github.com/garyburd/redigo/redis"
	"github.com/gin-gonic/gin"
)

const (
	ProgressInit    = 0
	ProgressIn      = 1
	ProgressSuccess = 2
	ProgressFail    = 3
)
const ProgressRedisExp = 3600
const ProgressGap = 5 // 存储间隙 就是多少个百分点存储 目的减少redis set

type Progress struct {
	ProgressTx          string  `json:"progressTx"`     // 可以理解唯一id
	Percentage          float64 `json:"percentage"`     // 当前进度百分比
	Status              int     `json:"status"`         // 状态
	Message             string  `json:"message"`        // 描述
	PrevPercentage      float64 `json:"prevPercentage"` // 上次百分比
	TotalNumber         int64   `json:"totalNumber"`    // 总数量
	SuccessNumber       int64   `json:"successNumber"`  // 完成数量
	FilePath            string  `json:"filePath"`       // 文件下载路径  --> 导出接口才有
	redisConn           redis.Conn
	redisLock           sync.Mutex
	addSuccessNumberQue chan int64
	closed              bool
	ctx                 *gin.Context
}

func GetProgressRedisKey(key string) string {
	return fmt.Sprintf("progress:%s", key)
}

// 传入上下文 -> 多租户的情况下需要上下文取租户号处理
func NewProgress(c *gin.Context, progressTx string, total int64) *Progress {
	// ctx := context.
	progress := &Progress{
		ProgressTx:          progressTx,
		TotalNumber:         total,
		redisConn:           global.REDISPoll.Get(),
		addSuccessNumberQue: make(chan int64, 100),
		ctx:                 c,
	}
	// 定时存档和保活
	go func(p *Progress) {
		// 存档和redis连接保活
		ticker := time.NewTicker(time.Second * 10)
		now := time.Now()
		defer p.redisConn.Close()
		defer ticker.Stop()
		defer p.closeAddSuccessNumberQue()
		for range ticker.C {
			// redis存档
			p.save()
			// 判断状态
			if p.IsFinalState() || time.Since(now) > time.Hour {
				// fmt.Println("????????????????????")
				return
			}
		}
	}(progress)
	// 进度条处理
	go func(p *Progress) {
		for num := range p.addSuccessNumberQue {
			p.SuccessNumber += num
			// 设置进度条
			percentage := float64(p.SuccessNumber*10000/p.TotalNumber) / 100
			p.SetProgress(percentage)
			// fmt.Println(p.TotalNumber, "  ", p.SuccessNumber, "  ", percentage)
		}
	}(progress)
	return progress
}

// 关闭通道
func (p *Progress) closeAddSuccessNumberQue() {
	// fmt.Println("关闭通道")
	if !p.closed {
		p.closed = true
		close(p.addSuccessNumberQue)
		// 处理通道内数据
		for range p.addSuccessNumberQue {
		}
	}
}

func (p *Progress) IsFinalState() bool {
	// 判断最终状态
	return p.Status == ProgressSuccess || p.Status == ProgressFail
}

// 获取进度条数据
func GetProgress(c *gin.Context, progressTx string) (*Progress, error) {
	conn := global.REDISPoll.Get()
	defer conn.Close()
	r, err := conn.Do("get", GetProgressRedisKey(progressTx))
	if err != nil {
		// 处理 Redis 错误
		fmt.Printf("获取进度条:%s 获取缓存异常:%v  \r\n", progressTx, err)
		return nil, err
	}
	value, ok := r.([]byte)
	if !ok {
		// 处理无效的返回值类型
		fmt.Printf("获取进度条:%s 类型转化异常 \r\n", progressTx)
		return nil, err
	}

	var progress Progress
	err = json.Unmarshal(value, &progress)
	if err != nil {
		fmt.Printf("获取进度条:%s 反序列化Progress异常: %v \r\n", progressTx, err)
		return nil, err
	}

	return &progress, nil
}

// 追加完成数量
func (p *Progress) AddSuccessNumber(num int64) {
	if !p.closed {
		p.addSuccessNumberQue <- num
	}
}

// 设置当前进度 --> 这个方法不要并发调用
func (p *Progress) SetProgress(percentage float64) error {
	if p.IsFinalState() {
		// 终状态不处理
		return nil
	}

	p.Status = ProgressIn
	if percentage >= 100 {
		percentage = 100
		p.Message = "SUCCESS"
		p.SuccessNumber = p.TotalNumber
		p.Status = ProgressSuccess
		p.closeAddSuccessNumberQue()
	}
	if percentage-p.PrevPercentage > 0 {
		p.Percentage = percentage
	}
	// 判断上次和当前次间隙
	if percentage-p.PrevPercentage >= ProgressGap || percentage == 100 {
		// fmt.Println(p.ProgressTx, "     PrevPercentage:", p.PrevPercentage, "   percentage:", percentage)
		p.save()
		p.PrevPercentage = percentage
	}

	return nil
}

// 失败处理
func (p *Progress) SetFail(message string) error {
	if p.IsFinalState() {
		// 终状态不处理
		return nil
	}
	p.Status = ProgressFail
	p.Message = message
	p.closeAddSuccessNumberQue()
	p.save()
	return nil
}

// 设置文件下载路径
func (p *Progress) SetFilePath(filepath string) error {
	p.FilePath = filepath
	p.save()
	return nil
}

func (p *Progress) save() error {
	p.redisLock.Lock()
	defer p.redisLock.Unlock()
	progressStr, err := json.Marshal(p)
	if err != nil {
		return err
	}
	_, err = p.redisConn.Do("set", GetProgressRedisKey(p.ProgressTx), progressStr, "EX", ProgressRedisExp)
	return err
}

实现导出接口
service.go

type whiteExport struct {
	req *request.ExportWhiteRequest
	c   *gin.Context
}

// 获取总量
func (w *whiteExport) Total() (int64, error) {
	return model.GetWhiteTotal(w.req.PublicWhite)
}

// 表头
func (w *whiteExport) Headers() []any {
	return []any{"名称"}
}

// 表内容
func (w *whiteExport) GetData(offset int, limit int) ([][]interface{}, error) {
	// 获取数据
	data, err := model.GetData()
	if err != nil {
		return nil, err
	}
	result := make([][]interface{}, len(data))
	for i := 0; i < len(data); i++ {
		result[i] = []interface{}{
			data[i].WhiteName,
		}
	}
	return result, nil
}

func ExportWhiteExcel(c *gin.Context, req *request.ExportWhiteRequest) (*response.ExportResp, error) {
	filepath := excel_export.GetExcelExportFilePath("测试")
	whiteExport := &whiteExport{
		c:   c,
		req: req,
	}
	builder, err := excel_export.NewExcelExportBuilder(c, filepath)
	if err != nil {
		return nil, err
	}

	err = builder.Save(whiteExport)
	if err != nil {
		return nil, err
	}

	return &response.ExportResp{
		Download:   filepath,
		ProgressTx: builder.ProgressTx,
	}, nil
}

csv的我没有封装以后可能封装起来

没封装你参考这个

https://blog.csdn.net/qq_39272466/article/details/131663379?spm=1001.2014.3001.5501

你可能感兴趣的:(golang,excel,开发语言,大数据)