- 导出统一行数上限:xlsx 超限在拉取阶段截停并报错,CSV 到限截断并在文件末尾写入可见标记 - 全部导出接口 OFFSET 循环翻页改为 keyset 游标(用量日志/计费记录/成员/渠道/钱包流水) - 用量日志导出强制时间窗(开始时间必填、跨度≤31天),校验下沉 common 供管理后台与租户端共用 - 管理后台仪表盘趋势/租户排行/模型分布/小时费用聚合加 5 分钟双层缓存 - 交易流水 Username 筛选改 EXISTS 子查询解除对 JOIN 别名的依赖,COUNT 与列表拆分免背联表 - 管理后台成员列表 COUNT 免联表(筛选条件只引用主表列) - 租户端用量日志列表/统计/导出共用筛选构造器,COUNT 与 SUM 全程剥离联表,钱包流水 COUNT 拆分
90 lines
2.9 KiB
Go
90 lines
2.9 KiB
Go
package export
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/gogf/gf/v2/errors/gerror"
|
|
"github.com/gogf/gf/v2/frame/g"
|
|
)
|
|
|
|
// Column defines one column in the exported file.
|
|
type Column struct {
|
|
Field string // Key in the data map
|
|
Header string // Display header (Chinese)
|
|
Transform func(any) string // Optional value transformer
|
|
}
|
|
|
|
// Config holds the full export configuration.
|
|
type Config struct {
|
|
Format string // "csv" or "xlsx"
|
|
Filename string // Without extension
|
|
Columns []Column // Ordered column definitions
|
|
MaxRows int // Safety cap for Excel (default 100000)
|
|
}
|
|
|
|
// GetMaxRows returns the configured max rows or the default.
|
|
func (c Config) GetMaxRows() int {
|
|
if c.MaxRows <= 0 {
|
|
return 100000
|
|
}
|
|
return c.MaxRows
|
|
}
|
|
|
|
// GenericExport handles format normalization and xlsx/csv branching.
|
|
// queryFn yields data rows — same callback as StreamCSV's queryFn.
|
|
// For xlsx: collects rows from queryFn (capped), then calls WriteExcel.
|
|
// For csv: passes queryFn to StreamCSV for streaming (capped).
|
|
//
|
|
// 行数上限在这里统一强制执行(对拉取过程截停,而不是拉完再检查):
|
|
// 导出的数据源是「无总量保证」的分批循环查询,上限是防止一次导出请求
|
|
// 拖着数据库扫全量大表的最后一道护栏。
|
|
// - xlsx:达到上限直接返回错误(此时还未写出任何响应,能干净地报错),
|
|
// 并且不再继续拉取 —— 旧实现是全部拉进内存后才检查,护栏形同虚设;
|
|
// - csv:边查边流式写出,表头和数据已经发给客户端,超限时无法再改报错误,
|
|
// 只能停止拉取并在文件末尾追加一行可见的截断标记,避免使用者
|
|
// 误把部分数据当作全量。
|
|
func GenericExport(ctx context.Context, config Config, queryFn func(yield func(map[string]any) bool)) error {
|
|
r := g.RequestFromCtx(ctx)
|
|
config.Format = detectFormat(config.Format)
|
|
maxRows := config.GetMaxRows()
|
|
|
|
if config.Format == "xlsx" {
|
|
var (
|
|
data []map[string]any
|
|
overflow bool
|
|
)
|
|
queryFn(func(row map[string]any) bool {
|
|
if len(data) >= maxRows {
|
|
overflow = true
|
|
return false
|
|
}
|
|
data = append(data, row)
|
|
return true
|
|
})
|
|
if overflow {
|
|
return gerror.Newf("数据量超过 %d 行,请缩小筛选范围后重试,或改用 CSV 格式导出", maxRows)
|
|
}
|
|
return WriteExcel(r, config, data)
|
|
}
|
|
|
|
truncated := false
|
|
capped := func(yield func(map[string]any) bool) {
|
|
rowCount := 0
|
|
queryFn(func(row map[string]any) bool {
|
|
if rowCount >= maxRows {
|
|
truncated = true
|
|
return false
|
|
}
|
|
rowCount++
|
|
return yield(row)
|
|
})
|
|
if truncated && len(config.Columns) > 0 {
|
|
yield(map[string]any{
|
|
config.Columns[0].Field: fmt.Sprintf("注意:已达到导出上限 %d 行,结果被截断,请缩小时间范围或筛选条件后分批导出", maxRows),
|
|
})
|
|
}
|
|
}
|
|
return StreamCSV(r, config, capped)
|
|
}
|