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
}
https://blog.csdn.net/qq_39272466/article/details/131663379?spm=1001.2014.3001.5501