All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m3s
6.0 KiB
6.0 KiB
Export Scene Template
Use this reference when adding a new export scene.
Minimal Checklist
- Add
ExportTaskSceneXxxinpkg/constants/constants.go. - Add the scene to
CreateExportTaskRequest.SceneandListExportTaskRequest.Scenevalidation/description. - Create
internal/exporter/<scene>_scene.go. - Register
NewXxxDataSource(db)ininternal/exporter/registry.go. - Update
IsSupportedScene. - Run
gofmton changed Go files. - Run
go build ./internal/exporter/...,go build ./internal/task/..., andgo build ./.... - Manually create a task and validate database rows plus downloaded file.
Skeleton
package exporter
import (
"context"
"strconv"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// XxxDataSource Xxx 导出数据源。
type XxxDataSource struct {
db *gorm.DB
}
// NewXxxDataSource 创建 Xxx 导出数据源。
func NewXxxDataSource(db *gorm.DB) *XxxDataSource {
return &XxxDataSource{db: db}
}
// Scene 返回导出场景编码。
func (s *XxxDataSource) Scene() string {
return constants.ExportTaskSceneXxx
}
// Count 统计 Xxx 导出行数。
func (s *XxxDataSource) Count(ctx context.Context, params ExportParams) (int, error) {
var total int64
query := s.applyFilters(s.db.WithContext(ctx).Model(&model.Xxx{}), params)
if err := query.Count(&total).Error; err != nil {
return 0, err
}
return int(total), nil
}
// Headers 返回 Xxx 导出表头。
func (s *XxxDataSource) Headers(ctx context.Context, params ExportParams) ([]string, error) {
return []string{"ID", "名称", "状态", "店铺ID", "创建时间"}, nil
}
// Fetch 按 offset/limit 查询 Xxx 导出数据。
func (s *XxxDataSource) Fetch(ctx context.Context, params ExportParams, offset, limit int) ([][]string, error) {
if limit <= 0 {
return [][]string{}, nil
}
var items []model.Xxx
query := s.applyFilters(s.db.WithContext(ctx).Model(&model.Xxx{}), params).
Order("id ASC").
Limit(limit).
Offset(offset)
if err := query.Find(&items).Error; err != nil {
return nil, err
}
rows := make([][]string, 0, len(items))
for _, item := range items {
rows = append(rows, []string{
strconv.FormatUint(uint64(item.ID), 10),
item.Name,
strconv.Itoa(item.Status),
formatOptionalUint(item.ShopID),
item.CreatedAt.Format(exportTimeLayout),
})
}
return rows, nil
}
func (s *XxxDataSource) applyFilters(query *gorm.DB, params ExportParams) *gorm.DB {
query = applyExportShopScope(query, params, "shop_id")
if status, ok := filterInt(params.Filters, "status"); ok {
query = query.Where("status = ?", status)
}
if shopID, ok := filterUint(params.Filters, "shop_id"); ok {
query = query.Where("shop_id = ?", shopID)
}
if start, ok := filterTime(params.Filters, "created_at_start"); ok {
query = query.Where("created_at >= ?", start)
}
if end, ok := filterTime(params.Filters, "created_at_end"); ok {
query = query.Where("created_at <= ?", end)
}
return query
}
JOIN Skeleton
Use this shape for multi-table exports:
type xxxExportRow struct {
ID uint
Name string
Status int
ShopID *uint
ExtraName string
CreatedAt time.Time
}
func (s *XxxDataSource) Fetch(ctx context.Context, params ExportParams, offset, limit int) ([][]string, error) {
if limit <= 0 {
return [][]string{}, nil
}
var items []xxxExportRow
query := s.applyFilters(s.db.WithContext(ctx).Table("tb_xxx AS x"), params, "x.").
Select(`
x.id,
x.name,
x.status,
x.shop_id,
COALESCE(e.name, '') AS extra_name,
x.created_at
`).
Joins("LEFT JOIN tb_extra AS e ON e.xxx_id = x.id AND e.deleted_at IS NULL").
Where("x.deleted_at IS NULL").
Order("x.id ASC").
Limit(limit).
Offset(offset)
if err := query.Scan(&items).Error; err != nil {
return nil, err
}
rows := make([][]string, 0, len(items))
for _, item := range items {
rows = append(rows, []string{
strconv.FormatUint(uint64(item.ID), 10),
item.Name,
strconv.Itoa(item.Status),
formatOptionalUint(item.ShopID),
item.ExtraName,
item.CreatedAt.Format(exportTimeLayout),
})
}
return rows, nil
}
func (s *XxxDataSource) applyFilters(query *gorm.DB, params ExportParams, prefix string) *gorm.DB {
query = applyExportShopScope(query, params, prefix+"shop_id")
if status, ok := filterInt(params.Filters, "status"); ok {
query = query.Where(prefix+"status = ?", status)
}
return query
}
If the JOIN multiplies rows, update Count to count the same exported row set. Do not count only the base table unless Fetch also returns one row per base record.
Registry Patch
func NewDefaultRegistry(db *gorm.DB) *Registry {
return NewRegistry(
NewDeviceDataSource(db),
NewIotCardDataSource(db),
NewXxxDataSource(db),
)
}
func IsSupportedScene(scene string) bool {
switch scene {
case constants.ExportTaskSceneDevice,
constants.ExportTaskSceneIotCard,
constants.ExportTaskSceneXxx:
return true
default:
return false
}
}
Manual API Smoke
curl -X POST 'http://localhost:端口/api/admin/export-tasks' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"scene": "xxx",
"format": "csv",
"query": {
"filters": {
"status": 1
}
}
}'
Check database:
SELECT id, scene, format, status, total_rows, total_shards, query_json
FROM tb_export_task
WHERE id = <task_id>;
SELECT shard_no, status, shard_offset, shard_limit, row_count, file_key
FROM tb_export_shard_task
WHERE task_id = <task_id>
ORDER BY shard_no;