init
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m3s

This commit is contained in:
Break
2026-06-16 15:15:50 +08:00
parent 84ab3bad99
commit 1f634eb465
21 changed files with 2776 additions and 41 deletions

View File

@@ -0,0 +1,222 @@
# Export Scene Template
Use this reference when adding a new export scene.
## Minimal Checklist
- Add `ExportTaskSceneXxx` in `pkg/constants/constants.go`.
- Add the scene to `CreateExportTaskRequest.Scene` and `ListExportTaskRequest.Scene` validation/description.
- Create `internal/exporter/<scene>_scene.go`.
- Register `NewXxxDataSource(db)` in `internal/exporter/registry.go`.
- Update `IsSupportedScene`.
- Run `gofmt` on changed Go files.
- Run `go build ./internal/exporter/...`, `go build ./internal/task/...`, and `go build ./...`.
- Manually create a task and validate database rows plus downloaded file.
## Skeleton
```go
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:
```go
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
```go
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
```bash
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:
```sql
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;
```