|
|
@@ -5,7 +5,6 @@ import (
|
|
|
"encoding/json"
|
|
|
"errors"
|
|
|
"fmt"
|
|
|
- "fwh-toolbox-server/cache"
|
|
|
"fwh-toolbox-server/domain"
|
|
|
"fwh-toolbox-server/dto"
|
|
|
"fwh-toolbox-server/jobs"
|
|
|
@@ -15,6 +14,7 @@ import (
|
|
|
"time"
|
|
|
|
|
|
"github.com/hibiken/asynq"
|
|
|
+ "go.mongodb.org/mongo-driver/bson/primitive"
|
|
|
"go.uber.org/zap"
|
|
|
)
|
|
|
|
|
|
@@ -33,115 +33,141 @@ func handleBatchDownload(ctx context.Context, task *asynq.Task) error {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
- // 执行主逻辑
|
|
|
- reasons, err := executeBatchDownload(ctx, payload)
|
|
|
-
|
|
|
- // 记录结果
|
|
|
- if err != nil {
|
|
|
- zap.L().Error("[BatchDownload] 任务执行失败", zap.String("id", mainTaskId), zap.String("err", err.Error()))
|
|
|
+ // 执行主逻辑。
|
|
|
+ // 注意:任务终态(complete/failed)由 item 聚合(maybeAggregate)异步派生写入,
|
|
|
+ // 这里只处理"任务根本无法开始"的致命错误 → ProgressError。
|
|
|
+ if err := executeBatchDownload(ctx, payload); err != nil {
|
|
|
+ zap.L().Error("[BatchDownload] 任务执行失败(致命)", zap.String("id", mainTaskId), zap.String("err", err.Error()))
|
|
|
repositories.UpdateBatchDownloadResult(payload.Id, domain.ProgressError, []string{err.Error()})
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
- if len(reasons) == 0 {
|
|
|
- zap.L().Info("[BatchDownload] 任务执行成功", zap.String("id", mainTaskId))
|
|
|
- repositories.UpdateBatchDownloadResult(payload.Id, domain.ProgressComplete, []string{})
|
|
|
- } else {
|
|
|
- zap.L().Warn("[BatchDownload] 任务部分失败", zap.String("id", mainTaskId), zap.Strings("reasons", reasons))
|
|
|
- repositories.UpdateBatchDownloadResult(payload.Id, domain.ProgressFailed, reasons)
|
|
|
- }
|
|
|
-
|
|
|
return nil // 返回 nil 让 Asynq 标记任务为 completed,不触发重试
|
|
|
}
|
|
|
|
|
|
-// executeBatchDownload 执行批量下载逻辑
|
|
|
-func executeBatchDownload(ctx context.Context, payload domain.TaskBatchDownload) ([]string, error) {
|
|
|
+// executeBatchDownload 执行批量下载逻辑。
|
|
|
+// 返回 error 仅表示"任务根本无法开始"的致命错误(→ ProgressError);
|
|
|
+// 进入执行阶段后的所有成败都以 item 形式记录,终态由聚合派生。
|
|
|
+func executeBatchDownload(ctx context.Context, payload domain.TaskBatchDownload) error {
|
|
|
mainTaskId := payload.Id.Hex()
|
|
|
|
|
|
- // 解析时间范围
|
|
|
+ // 解析时间范围(致命错误:早退)
|
|
|
startTime, err := time.Parse(time.DateOnly, payload.TimePeriod[0])
|
|
|
if err != nil {
|
|
|
- return nil, fmt.Errorf("startTime 格式错误: %w", err)
|
|
|
+ return fmt.Errorf("startTime 格式错误: %w", err)
|
|
|
}
|
|
|
endTime, err := time.Parse(time.DateOnly, payload.TimePeriod[1])
|
|
|
if err != nil {
|
|
|
- return nil, fmt.Errorf("endTime 格式错误: %w", err)
|
|
|
+ return fmt.Errorf("endTime 格式错误: %w", err)
|
|
|
}
|
|
|
endTime = endTime.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
|
|
|
if startTime.After(endTime) {
|
|
|
- return nil, errors.New("startTime > endTime")
|
|
|
+ return errors.New("startTime > endTime")
|
|
|
}
|
|
|
|
|
|
// 按天拆分时间段
|
|
|
periods := splitTimePeriods(startTime, endTime)
|
|
|
|
|
|
- // ==================== 阶段1:创建文件下载子任务 ====================
|
|
|
- callFileTasks := buildCallFileTasks(payload, periods, startTime, endTime)
|
|
|
- totalSubTasks := len(callFileTasks)
|
|
|
-
|
|
|
- if totalSubTasks > 0 {
|
|
|
- // 初始化 Redis 计数器(待完成数)
|
|
|
- counterKey := callFileCounterPrefix + mainTaskId
|
|
|
- failuresKey := callFileFailuresPrefix + mainTaskId
|
|
|
-
|
|
|
- cache.Set(ctx, counterKey, totalSubTasks, 2*time.Hour)
|
|
|
- cache.Delete(ctx, failuresKey)
|
|
|
-
|
|
|
- // 入队所有子任务
|
|
|
- for i, cfTask := range callFileTasks {
|
|
|
- taskPayload, _ := json.Marshal(cfTask)
|
|
|
- subTaskID := fmt.Sprintf("callfile-%s-%d", mainTaskId, i)
|
|
|
- t := asynq.NewTask(TaskTypeCallFile, taskPayload,
|
|
|
- asynq.MaxRetry(3),
|
|
|
- asynq.Timeout(35*time.Minute),
|
|
|
- asynq.Queue(QueueDownload),
|
|
|
- asynq.TaskID(subTaskID),
|
|
|
- )
|
|
|
- if _, err := TaskClient.Enqueue(t); err != nil {
|
|
|
- zap.L().Error("[BatchDownload] 子任务入队失败",
|
|
|
- zap.String("id", mainTaskId),
|
|
|
- zap.Int("index", i),
|
|
|
- zap.String("err", err.Error()),
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
+ // ==================== 阶段0:规划所有 item ====================
|
|
|
+ // planItems 枚举所有同步 + 异步工作单元为 pending;构建期查询失败/无数据直接写 failed/skipped。
|
|
|
+ items, asyncTasks := planItems(payload, periods, startTime, endTime)
|
|
|
+
|
|
|
+ if err := repositories.InitBatchDownloadItems(payload.Id, items); err != nil {
|
|
|
+ return fmt.Errorf("初始化 items 失败: %w", err)
|
|
|
}
|
|
|
|
|
|
- // ==================== 阶段2:同步处理其他数据任务 ====================
|
|
|
- if err := executeSyncTasks(ctx, payload, periods); err != nil {
|
|
|
- return nil, err
|
|
|
+ // 边界:若所有 item 在规划期就已是终态(无异步子任务、无同步任务),主动触发一次聚合。
|
|
|
+ doneInit := 0
|
|
|
+ for _, it := range items {
|
|
|
+ if it.Status != domain.ItemPending {
|
|
|
+ doneInit++
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if doneInit == len(items) {
|
|
|
+ maybeAggregate(payload.Id, doneInit, len(items))
|
|
|
+ return nil
|
|
|
}
|
|
|
|
|
|
- // ==================== 阶段3:等待子任务完成 ====================
|
|
|
- if totalSubTasks > 0 {
|
|
|
- failReasons, err := waitForCallFileTasks(ctx, mainTaskId)
|
|
|
- if err != nil {
|
|
|
- return nil, err
|
|
|
+ // ==================== 阶段1:入队异步文件下载子任务 ====================
|
|
|
+ for i, cfTask := range asyncTasks {
|
|
|
+ taskPayload, _ := json.Marshal(cfTask)
|
|
|
+ subTaskID := fmt.Sprintf("callfile-%s-%d", mainTaskId, i)
|
|
|
+ t := asynq.NewTask(TaskTypeCallFile, taskPayload,
|
|
|
+ asynq.MaxRetry(3),
|
|
|
+ asynq.Timeout(35*time.Minute),
|
|
|
+ asynq.Queue(QueueDownload),
|
|
|
+ asynq.TaskID(subTaskID),
|
|
|
+ )
|
|
|
+ if _, err := TaskClient.Enqueue(t); err != nil {
|
|
|
+ // 入队失败:直接把该 item 记为 failed(build),doneCount 照常推进,避免永久 pending 挂起。
|
|
|
+ zap.L().Error("[BatchDownload] 子任务入队失败",
|
|
|
+ zap.String("id", mainTaskId),
|
|
|
+ zap.Int("index", i),
|
|
|
+ zap.String("key", cfTask.ItemKey),
|
|
|
+ zap.String("err", err.Error()),
|
|
|
+ )
|
|
|
+ finishItem(payload.Id, cfTask.ItemKey, domain.ItemFailed, domain.StageBuild, "子任务入队失败: "+err.Error())
|
|
|
}
|
|
|
- return failReasons, nil
|
|
|
}
|
|
|
|
|
|
- return nil, nil
|
|
|
+ // ==================== 阶段2:同步处理其他数据任务 ====================
|
|
|
+ // 同步任务在主任务内联执行,失败记 item failed 而非中止;每个 item 终态都会驱动 maybeAggregate。
|
|
|
+ executeSyncTasks(ctx, payload, periods)
|
|
|
+
|
|
|
+ return nil
|
|
|
}
|
|
|
|
|
|
// callFilePayload 是传递给子任务的 payload 结构
|
|
|
type callFilePayload struct {
|
|
|
MainTaskId string `json:"mainTaskId"`
|
|
|
+ ItemKey string `json:"itemKey"` // 对应 BatchDownloadItem.Key,子任务据此上报结果
|
|
|
TaskInfo domain.BatchDownloadFileTaskInfo `json:"taskInfo"`
|
|
|
}
|
|
|
|
|
|
-// buildCallFileTasks 构建所有需要异步下载的文件任务列表
|
|
|
-func buildCallFileTasks(payload domain.TaskBatchDownload, periods []timePeriod, startTime, endTime time.Time) []callFilePayload {
|
|
|
+// 同步任务的 item fileType 标识(与 executeSyncTasks 内的 key 生成保持一致)
|
|
|
+const (
|
|
|
+ fileTypePowerFlow = "PowerFlow"
|
|
|
+ fileTypeMqttLog = "MqttLog"
|
|
|
+ fileTypeEventAlarm = "EventAlarm"
|
|
|
+ fileTypeAlarmSnapshot = "AlarmSnapshot"
|
|
|
+ fileTypeUpdateLog = "UpdateLog"
|
|
|
+)
|
|
|
+
|
|
|
+// planItems 枚举本次批量下载的所有工作单元:
|
|
|
+// - 返回全部 items(pending / 构建期 failed / skipped)
|
|
|
+// - 返回需要异步入队的文件下载子任务列表(EmsLog / FaultRecord,payload 带 ItemKey)
|
|
|
+//
|
|
|
+// 同步类任务(PowerFlow/MqttLog/EventAlarm/AlarmSnapshot/UpdateLog)只产出 pending item,
|
|
|
+// 由 executeSyncTasks 内联执行并上报。
|
|
|
+func planItems(payload domain.TaskBatchDownload, periods []timePeriod, startTime, endTime time.Time) ([]domain.BatchDownloadItem, []callFilePayload) {
|
|
|
mainTaskId := payload.Id.Hex()
|
|
|
- var tasks []callFilePayload
|
|
|
+ var items []domain.BatchDownloadItem
|
|
|
+ var asyncTasks []callFilePayload
|
|
|
+
|
|
|
+ newItem := func(fileType, sn string, devType int, date, subType string, status domain.ItemStatus, stage, reason string) domain.BatchDownloadItem {
|
|
|
+ return domain.BatchDownloadItem{
|
|
|
+ Key: domain.BuildItemKey(fileType, sn, devType, date, subType),
|
|
|
+ Sn: sn,
|
|
|
+ DevType: devType,
|
|
|
+ FileType: fileType,
|
|
|
+ SubType: subType,
|
|
|
+ Date: date,
|
|
|
+ Status: status,
|
|
|
+ Stage: stage,
|
|
|
+ Reason: reason,
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
for _, sn := range payload.Sn {
|
|
|
- // EmsLog 任务
|
|
|
+ // ---------- EmsLog(异步) ----------
|
|
|
for _, period := range periods {
|
|
|
dateStr := period.Start.Format(time.DateOnly)
|
|
|
for _, logType := range payload.Options.EmsLogs {
|
|
|
- tasks = append(tasks, callFilePayload{
|
|
|
+ key := domain.BuildItemKey(jobs.FileTypeEmsLog, sn, -1, dateStr, logType)
|
|
|
+ items = append(items, newItem(jobs.FileTypeEmsLog, sn, -1, dateStr, logType, domain.ItemPending, "", ""))
|
|
|
+ asyncTasks = append(asyncTasks, callFilePayload{
|
|
|
MainTaskId: mainTaskId,
|
|
|
+ ItemKey: key,
|
|
|
TaskInfo: domain.BatchDownloadFileTaskInfo{
|
|
|
DevType: -1,
|
|
|
Sn: sn,
|
|
|
@@ -153,25 +179,61 @@ func buildCallFileTasks(payload domain.TaskBatchDownload, periods []timePeriod,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // FaultRecord 任务
|
|
|
+ // ---------- FaultRecord(异步) ----------
|
|
|
if payload.Options.FaultRecord {
|
|
|
- frTasks := buildFaultRecordTasks(mainTaskId, sn, startTime, endTime)
|
|
|
- tasks = append(tasks, frTasks...)
|
|
|
+ frItems, frTasks := planFaultRecordItems(mainTaskId, sn, startTime, endTime, newItem)
|
|
|
+ items = append(items, frItems...)
|
|
|
+ asyncTasks = append(asyncTasks, frTasks...)
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- 同步类 ----------
|
|
|
+ if payload.Options.PowerFlow {
|
|
|
+ for _, period := range periods {
|
|
|
+ dateStr := period.Start.Format(time.DateOnly)
|
|
|
+ items = append(items, newItem(fileTypePowerFlow, sn, -1, dateStr, "", domain.ItemPending, "", ""))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for _, logType := range payload.Options.MqttLogs {
|
|
|
+ for _, period := range periods {
|
|
|
+ dateStr := period.Start.Format(time.DateOnly)
|
|
|
+ items = append(items, newItem(fileTypeMqttLog, sn, -1, dateStr, logType, domain.ItemPending, "", ""))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if payload.Options.EventAlarm {
|
|
|
+ for _, period := range periods {
|
|
|
+ dateStr := period.Start.Format(time.DateOnly)
|
|
|
+ items = append(items, newItem(fileTypeEventAlarm, sn, -1, dateStr, "", domain.ItemPending, "", ""))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if payload.Options.AlarmSnapshot {
|
|
|
+ for _, period := range periods {
|
|
|
+ dateStr := period.Start.Format(time.DateOnly)
|
|
|
+ items = append(items, newItem(fileTypeAlarmSnapshot, sn, -1, dateStr, "", domain.ItemPending, "", ""))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if payload.Options.UpdateLog {
|
|
|
+ items = append(items, newItem(fileTypeUpdateLog, sn, -1, "", "", domain.ItemPending, "", ""))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- return tasks
|
|
|
+ return items, asyncTasks
|
|
|
}
|
|
|
|
|
|
-// buildFaultRecordTasks 为单个 SN 构建故障录波下载任务
|
|
|
-func buildFaultRecordTasks(mainTaskId string, sn string, startTime, endTime time.Time) []callFilePayload {
|
|
|
+// planFaultRecordItems 为单个 gateway SN 规划故障录波 item(aGate + 其下 aPower)。
|
|
|
+// 构建期查询失败 → failed(build);无满足时间范围的数据 → skipped;有数据 → pending + 异步 payload。
|
|
|
+func planFaultRecordItems(mainTaskId string, sn string, startTime, endTime time.Time,
|
|
|
+ newItem func(fileType, sn string, devType int, date, subType string, status domain.ItemStatus, stage, reason string) domain.BatchDownloadItem,
|
|
|
+) ([]domain.BatchDownloadItem, []callFilePayload) {
|
|
|
+ var items []domain.BatchDownloadItem
|
|
|
var tasks []callFilePayload
|
|
|
|
|
|
- // 查询 aGate 可召唤列表
|
|
|
- aGateTasks := buildFaultRecordTasksForDev(mainTaskId, 1, sn, startTime, endTime)
|
|
|
- tasks = append(tasks, aGateTasks...)
|
|
|
+ // aGate 可召唤列表
|
|
|
+ frItems, frTasks := planFaultRecordItemsForDev(mainTaskId, 1, sn, startTime, endTime, newItem)
|
|
|
+ items = append(items, frItems...)
|
|
|
+ tasks = append(tasks, frTasks...)
|
|
|
|
|
|
- // 查询 aPower 子设备
|
|
|
+ // aPower 子设备
|
|
|
+ dateRange := startTime.Format(time.DateTime) + "," + endTime.Format(time.DateTime)
|
|
|
siteBaseInfoList, err := repositories.QuerySiteBaseInfoList(dto.SiteBaseInfoPara{
|
|
|
SiteList: []string{sn},
|
|
|
PageSize: 10,
|
|
|
@@ -179,33 +241,39 @@ func buildFaultRecordTasks(mainTaskId string, sn string, startTime, endTime time
|
|
|
})
|
|
|
if err != nil {
|
|
|
zap.L().Error("[BatchDownload] 查询站点基本信息失败", zap.String("sn", sn), zap.String("err", err.Error()))
|
|
|
- return tasks
|
|
|
+ // 站点信息查询失败 → 无法枚举 aPower,记一条 failed(build) 归属该 SN
|
|
|
+ items = append(items, newItem(jobs.FileTypeFaultRecord, sn, 0, dateRange, "", domain.ItemFailed, domain.StageBuild, "查询站点基本信息失败: "+err.Error()))
|
|
|
+ return items, tasks
|
|
|
}
|
|
|
|
|
|
- if siteBaseInfoList != nil && len(*siteBaseInfoList.Result) > 0 {
|
|
|
+ if siteBaseInfoList != nil && siteBaseInfoList.Result != nil && len(*siteBaseInfoList.Result) > 0 {
|
|
|
aPowerSnList := strings.Split((*siteBaseInfoList.Result)[0].FhpSn, ",")
|
|
|
for _, aPowerSn := range aPowerSnList {
|
|
|
aPowerSn = strings.TrimSpace(aPowerSn)
|
|
|
if aPowerSn == "" {
|
|
|
continue
|
|
|
}
|
|
|
- aPowerTasks := buildFaultRecordTasksForDev(mainTaskId, 0, aPowerSn, startTime, endTime)
|
|
|
- tasks = append(tasks, aPowerTasks...)
|
|
|
+ apItems, apTasks := planFaultRecordItemsForDev(mainTaskId, 0, aPowerSn, startTime, endTime, newItem)
|
|
|
+ items = append(items, apItems...)
|
|
|
+ tasks = append(tasks, apTasks...)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- return tasks
|
|
|
+ return items, tasks
|
|
|
}
|
|
|
|
|
|
-// buildFaultRecordTasksForDev 为指定设备构建故障录波下载任务
|
|
|
-func buildFaultRecordTasksForDev(mainTaskId string, devType int, sn string, startTime, endTime time.Time) []callFilePayload {
|
|
|
+// planFaultRecordItemsForDev 为指定设备规划故障录波 item。
|
|
|
+func planFaultRecordItemsForDev(mainTaskId string, devType int, sn string, startTime, endTime time.Time,
|
|
|
+ newItem func(fileType, sn string, devType int, date, subType string, status domain.ItemStatus, stage, reason string) domain.BatchDownloadItem,
|
|
|
+) ([]domain.BatchDownloadItem, []callFilePayload) {
|
|
|
+ dateRange := startTime.Format(time.DateTime) + "," + endTime.Format(time.DateTime)
|
|
|
+
|
|
|
callInfoList, err := repositories.QueryFaultRecordCallableList(devType, sn)
|
|
|
if err != nil {
|
|
|
- zap.L().Error("[BatchDownload] 查询故障录波召唤选项失败", zap.String("sn", sn), zap.String("err", err.Error()))
|
|
|
- return nil
|
|
|
- }
|
|
|
- if len(callInfoList) == 0 {
|
|
|
- return nil
|
|
|
+ zap.L().Error("[BatchDownload] 查询故障录波召唤选项失败", zap.Int("devType", devType), zap.String("sn", sn), zap.String("err", err.Error()))
|
|
|
+ // 构建期查询失败 → failed(build),错误得以上报
|
|
|
+ item := newItem(jobs.FileTypeFaultRecord, sn, devType, dateRange, "", domain.ItemFailed, domain.StageBuild, "查询故障录波召唤选项失败: "+err.Error())
|
|
|
+ return []domain.BatchDownloadItem{item}, nil
|
|
|
}
|
|
|
|
|
|
// 筛选日期范围内的记录
|
|
|
@@ -226,41 +294,42 @@ func buildFaultRecordTasksForDev(mainTaskId string, devType int, sn string, star
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 没有符合日期范围的记录,不创建任务
|
|
|
+ // 无符合日期范围的记录 → skipped(非错误)
|
|
|
if indexList == "" {
|
|
|
- return nil
|
|
|
+ item := newItem(jobs.FileTypeFaultRecord, sn, devType, dateRange, "", domain.ItemSkipped, "", "无满足时间范围的录波数据")
|
|
|
+ return []domain.BatchDownloadItem{item}, nil
|
|
|
}
|
|
|
|
|
|
- return []callFilePayload{{
|
|
|
+ // 有数据 → pending + 异步 payload
|
|
|
+ key := domain.BuildItemKey(jobs.FileTypeFaultRecord, sn, devType, dateRange, indexList)
|
|
|
+ item := newItem(jobs.FileTypeFaultRecord, sn, devType, dateRange, indexList, domain.ItemPending, "", "")
|
|
|
+ task := callFilePayload{
|
|
|
MainTaskId: mainTaskId,
|
|
|
+ ItemKey: key,
|
|
|
TaskInfo: domain.BatchDownloadFileTaskInfo{
|
|
|
DevType: devType,
|
|
|
Sn: sn,
|
|
|
- Date: startTime.Format(time.DateTime) + "," + endTime.Format(time.DateTime),
|
|
|
+ Date: dateRange,
|
|
|
FileType: jobs.FileTypeFaultRecord,
|
|
|
FileSupType: indexList,
|
|
|
},
|
|
|
- }}
|
|
|
+ }
|
|
|
+ return []domain.BatchDownloadItem{item}, []callFilePayload{task}
|
|
|
}
|
|
|
|
|
|
-// executeSyncTasks 同步执行非文件下载类任务(PowerFlow、MqttLogs、EventAlarm 等)
|
|
|
-func executeSyncTasks(ctx context.Context, payload domain.TaskBatchDownload, periods []timePeriod) error {
|
|
|
+// executeSyncTasks 同步执行非文件下载类任务(PowerFlow、MqttLogs、EventAlarm 等)。
|
|
|
+// 每个工作单元成功/失败都通过 finishItem 上报,失败不中止整个任务。
|
|
|
+func executeSyncTasks(ctx context.Context, payload domain.TaskBatchDownload, periods []timePeriod) {
|
|
|
for _, sn := range payload.Sn {
|
|
|
// PowerFlow
|
|
|
if payload.Options.PowerFlow {
|
|
|
for _, period := range periods {
|
|
|
dateStr := period.Start.Format(time.DateOnly)
|
|
|
- res, err := repositories.QuerySitePowerFlow(sn, period.Start)
|
|
|
- if err != nil {
|
|
|
- zap.L().Error("[BatchDownload] 查询功率流数据失败", zap.String("sn", sn), zap.String("date", dateStr), zap.String("err", err.Error()))
|
|
|
- return err
|
|
|
- }
|
|
|
- objName, err := jobs.SaveBatchDownloadPowerFlow(sn, dateStr, *res.Result)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
- if err := repositories.InsertBatchDownloadPathPowerFlow(payload.Id, sn, dateStr, objName); err != nil {
|
|
|
- return err
|
|
|
+ key := domain.BuildItemKey(fileTypePowerFlow, sn, -1, dateStr, "")
|
|
|
+ if err := syncPowerFlow(payload.Id, sn, dateStr, period); err != nil {
|
|
|
+ finishItem(payload.Id, key, domain.ItemFailed, err.stage, err.reason)
|
|
|
+ } else {
|
|
|
+ finishItem(payload.Id, key, domain.ItemSuccess, "", "")
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -268,17 +337,12 @@ func executeSyncTasks(ctx context.Context, payload domain.TaskBatchDownload, per
|
|
|
// MqttLogs
|
|
|
for _, logType := range payload.Options.MqttLogs {
|
|
|
for _, period := range periods {
|
|
|
- logList, err := repositories.QueryMqttLogListOnTimeRange(sn, domain.MqttCmdType(logType), period.Start, period.End)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
dateStr := period.Start.Format(time.DateOnly)
|
|
|
- objName, err := jobs.SaveBatchDownloadMqttLog(sn, logType, dateStr, logList)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
- if err := repositories.InsertBatchDownloadPathMqttLogs(payload.Id, sn, logType, dateStr, objName); err != nil {
|
|
|
- return err
|
|
|
+ key := domain.BuildItemKey(fileTypeMqttLog, sn, -1, dateStr, logType)
|
|
|
+ if err := syncMqttLog(payload.Id, sn, logType, dateStr, period); err != nil {
|
|
|
+ finishItem(payload.Id, key, domain.ItemFailed, err.stage, err.reason)
|
|
|
+ } else {
|
|
|
+ finishItem(payload.Id, key, domain.ItemSuccess, "", "")
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -286,17 +350,12 @@ func executeSyncTasks(ctx context.Context, payload domain.TaskBatchDownload, per
|
|
|
// EventAlarm
|
|
|
if payload.Options.EventAlarm {
|
|
|
for _, period := range periods {
|
|
|
- eventAlarmList, err := repositories.QueryEventOrAlarmListOnTimeRange(sn, period.Start, period.End, repositories.EventAndAlarm)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
dateStr := period.Start.Format(time.DateOnly)
|
|
|
- objName, err := jobs.SaveBatchDownloadEventAlarm(sn, dateStr, eventAlarmList)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
- if err := repositories.InsertBatchDownloadPathEventAlarm(payload.Id, sn, dateStr, objName); err != nil {
|
|
|
- return err
|
|
|
+ key := domain.BuildItemKey(fileTypeEventAlarm, sn, -1, dateStr, "")
|
|
|
+ if err := syncEventAlarm(payload.Id, sn, dateStr, period); err != nil {
|
|
|
+ finishItem(payload.Id, key, domain.ItemFailed, err.stage, err.reason)
|
|
|
+ } else {
|
|
|
+ finishItem(payload.Id, key, domain.ItemSuccess, "", "")
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -305,74 +364,101 @@ func executeSyncTasks(ctx context.Context, payload domain.TaskBatchDownload, per
|
|
|
if payload.Options.AlarmSnapshot {
|
|
|
for _, period := range periods {
|
|
|
dateStr := period.Start.Format(time.DateOnly)
|
|
|
- alarmSnapshotList, err := jobs.GetAlarmSnapshotData(ctx, sn, dateStr)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
- objName, err := jobs.SaveBatchDownloadAlarmSnapshot(sn, dateStr, alarmSnapshotList)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
- if err := repositories.InsertBatchDownloadPathAlarmSnapshot(payload.Id, sn, dateStr, objName); err != nil {
|
|
|
- return err
|
|
|
+ key := domain.BuildItemKey(fileTypeAlarmSnapshot, sn, -1, dateStr, "")
|
|
|
+ if err := syncAlarmSnapshot(ctx, payload.Id, sn, dateStr); err != nil {
|
|
|
+ finishItem(payload.Id, key, domain.ItemFailed, err.stage, err.reason)
|
|
|
+ } else {
|
|
|
+ finishItem(payload.Id, key, domain.ItemSuccess, "", "")
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// UpdateLog
|
|
|
if payload.Options.UpdateLog {
|
|
|
+ key := domain.BuildItemKey(fileTypeUpdateLog, sn, -1, "", "")
|
|
|
if err := executeSyncUpdateLog(sn, payload); err != nil {
|
|
|
- return err
|
|
|
+ finishItem(payload.Id, key, domain.ItemFailed, domain.StageQuery, err.Error())
|
|
|
+ } else {
|
|
|
+ finishItem(payload.Id, key, domain.ItemSuccess, "", "")
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- return nil
|
|
|
}
|
|
|
|
|
|
-// waitForCallFileTasks 等待所有子任务完成,收集失败原因
|
|
|
-func waitForCallFileTasks(ctx context.Context, mainTaskId string) ([]string, error) {
|
|
|
- counterKey := callFileCounterPrefix + mainTaskId
|
|
|
- failuresKey := callFileFailuresPrefix + mainTaskId
|
|
|
-
|
|
|
- ticker := time.NewTicker(30 * time.Second)
|
|
|
- defer ticker.Stop()
|
|
|
-
|
|
|
- timeout := time.After(90 * time.Minute) // 90 分钟总超时
|
|
|
-
|
|
|
- for {
|
|
|
- select {
|
|
|
- case <-ctx.Done():
|
|
|
- return nil, ctx.Err()
|
|
|
- case <-timeout:
|
|
|
- return nil, fmt.Errorf("等待子任务完成超时(90分钟)")
|
|
|
- case <-ticker.C:
|
|
|
- val, err := cache.Get(ctx, counterKey)
|
|
|
- if err != nil {
|
|
|
- // key 不存在或读取失败
|
|
|
- if err.Error() == "redis: nil" {
|
|
|
- // key 不存在,所有子任务已完成
|
|
|
- val = "0"
|
|
|
- } else {
|
|
|
- zap.L().Error("[BatchDownload] 读取计数器失败", zap.String("key", counterKey), zap.String("err", err.Error()))
|
|
|
- continue
|
|
|
- }
|
|
|
- }
|
|
|
+// syncItemError 携带失败阶段与原因,便于 finishItem 记录 stage
|
|
|
+type syncItemError struct {
|
|
|
+ stage string
|
|
|
+ reason string
|
|
|
+}
|
|
|
|
|
|
- remaining, _ := strconv.Atoi(val)
|
|
|
- if remaining <= 0 {
|
|
|
- // 收集失败原因
|
|
|
- failures, err := cache.LRange(ctx, failuresKey, 0, -1)
|
|
|
- if err != nil {
|
|
|
- return nil, fmt.Errorf("读取失败原因列表失败: %w", err)
|
|
|
- }
|
|
|
+func (e *syncItemError) Error() string { return e.reason }
|
|
|
|
|
|
- // 清理
|
|
|
- cache.Delete(ctx, counterKey, failuresKey)
|
|
|
+func newSyncErr(stage string, err error) *syncItemError {
|
|
|
+ return &syncItemError{stage: stage, reason: err.Error()}
|
|
|
+}
|
|
|
|
|
|
- return failures, nil
|
|
|
- }
|
|
|
- }
|
|
|
+// syncPowerFlow 处理单个 PowerFlow 工作单元
|
|
|
+func syncPowerFlow(id primitive.ObjectID, sn, dateStr string, period timePeriod) *syncItemError {
|
|
|
+ res, err := repositories.QuerySitePowerFlow(sn, period.Start)
|
|
|
+ if err != nil {
|
|
|
+ return newSyncErr(domain.StageQuery, err)
|
|
|
}
|
|
|
+ objName, err := jobs.SaveBatchDownloadPowerFlow(sn, dateStr, *res.Result)
|
|
|
+ if err != nil {
|
|
|
+ return newSyncErr(domain.StageSave, err)
|
|
|
+ }
|
|
|
+ if err := repositories.InsertBatchDownloadPathPowerFlow(id, sn, dateStr, objName); err != nil {
|
|
|
+ return newSyncErr(domain.StageSave, err)
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// syncMqttLog 处理单个 MqttLog 工作单元
|
|
|
+func syncMqttLog(id primitive.ObjectID, sn, logType, dateStr string, period timePeriod) *syncItemError {
|
|
|
+ logList, err := repositories.QueryMqttLogListOnTimeRange(sn, domain.MqttCmdType(logType), period.Start, period.End)
|
|
|
+ if err != nil {
|
|
|
+ return newSyncErr(domain.StageQuery, err)
|
|
|
+ }
|
|
|
+ objName, err := jobs.SaveBatchDownloadMqttLog(sn, logType, dateStr, logList)
|
|
|
+ if err != nil {
|
|
|
+ return newSyncErr(domain.StageSave, err)
|
|
|
+ }
|
|
|
+ if err := repositories.InsertBatchDownloadPathMqttLogs(id, sn, logType, dateStr, objName); err != nil {
|
|
|
+ return newSyncErr(domain.StageSave, err)
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// syncEventAlarm 处理单个 EventAlarm 工作单元
|
|
|
+func syncEventAlarm(id primitive.ObjectID, sn, dateStr string, period timePeriod) *syncItemError {
|
|
|
+ eventAlarmList, err := repositories.QueryEventOrAlarmListOnTimeRange(sn, period.Start, period.End, repositories.EventAndAlarm)
|
|
|
+ if err != nil {
|
|
|
+ return newSyncErr(domain.StageQuery, err)
|
|
|
+ }
|
|
|
+ objName, err := jobs.SaveBatchDownloadEventAlarm(sn, dateStr, eventAlarmList)
|
|
|
+ if err != nil {
|
|
|
+ return newSyncErr(domain.StageSave, err)
|
|
|
+ }
|
|
|
+ if err := repositories.InsertBatchDownloadPathEventAlarm(id, sn, dateStr, objName); err != nil {
|
|
|
+ return newSyncErr(domain.StageSave, err)
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// syncAlarmSnapshot 处理单个 AlarmSnapshot 工作单元
|
|
|
+func syncAlarmSnapshot(ctx context.Context, id primitive.ObjectID, sn, dateStr string) *syncItemError {
|
|
|
+ alarmSnapshotList, err := jobs.GetAlarmSnapshotData(ctx, sn, dateStr)
|
|
|
+ if err != nil {
|
|
|
+ return newSyncErr(domain.StageQuery, err)
|
|
|
+ }
|
|
|
+ objName, err := jobs.SaveBatchDownloadAlarmSnapshot(sn, dateStr, alarmSnapshotList)
|
|
|
+ if err != nil {
|
|
|
+ return newSyncErr(domain.StageSave, err)
|
|
|
+ }
|
|
|
+ if err := repositories.InsertBatchDownloadPathAlarmSnapshot(id, sn, dateStr, objName); err != nil {
|
|
|
+ return newSyncErr(domain.StageSave, err)
|
|
|
+ }
|
|
|
+ return nil
|
|
|
}
|
|
|
|
|
|
// ==================== 辅助函数 ====================
|
|
|
@@ -549,3 +635,64 @@ func executeSyncUpdateLog(sn string, payload domain.TaskBatchDownload) error {
|
|
|
}
|
|
|
return repositories.InsertBatchDownloadPathUpdateLog(payload.Id, sn, objName)
|
|
|
}
|
|
|
+
|
|
|
+// ==================== 聚合与完成判定(B2 + 策略C) ====================
|
|
|
+
|
|
|
+// deriveProgressAndSummary 从 items 聚合派生任务终态与摘要 failReasons(策略C:摘要,明细在 items)
|
|
|
+func deriveProgressAndSummary(items []domain.BatchDownloadItem) (domain.Progress, []string) {
|
|
|
+ failed := 0
|
|
|
+ var samples []string
|
|
|
+ for _, it := range items {
|
|
|
+ if it.Status == domain.ItemFailed {
|
|
|
+ failed++
|
|
|
+ if len(samples) < 5 { // 摘要仅保留前若干条样本
|
|
|
+ samples = append(samples, fmt.Sprintf("[%s] sn=%s date=%s stage=%s: %s",
|
|
|
+ it.FileType, it.Sn, it.Date, it.Stage, it.Reason))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if failed == 0 {
|
|
|
+ return domain.ProgressComplete, []string{}
|
|
|
+ }
|
|
|
+
|
|
|
+ summary := append([]string{fmt.Sprintf("共 %d 项失败(详见明细 items)", failed)}, samples...)
|
|
|
+ return domain.ProgressFailed, summary
|
|
|
+}
|
|
|
+
|
|
|
+// maybeAggregate 在 done==total(本任务全部 item 已终态)时聚合派生终态并写库。
|
|
|
+// 由 FinishBatchDownloadItem 的原子性保证只有一个调用会命中 done==total,聚合仅触发一次。
|
|
|
+// done/total 为 -1 表示幂等跳过(重复回调),不处理。
|
|
|
+func maybeAggregate(id primitive.ObjectID, done, total int) {
|
|
|
+ if done < 0 || total < 0 {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if done != total {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ items, err := repositories.GetBatchDownloadItems(id)
|
|
|
+ if err != nil {
|
|
|
+ zap.L().Error("[BatchDownload] 聚合读取 items 失败", zap.String("id", id.Hex()), zap.String("err", err.Error()))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ progress, summary := deriveProgressAndSummary(items)
|
|
|
+ if err := repositories.UpdateBatchDownloadResult(id, progress, summary); err != nil {
|
|
|
+ zap.L().Error("[BatchDownload] 聚合写入结果失败", zap.String("id", id.Hex()), zap.String("err", err.Error()))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ zap.L().Info("[BatchDownload] 任务聚合完成", zap.String("id", id.Hex()), zap.String("progress", string(progress)))
|
|
|
+}
|
|
|
+
|
|
|
+// finishItem 是对 repositories.FinishBatchDownloadItem + maybeAggregate 的封装,
|
|
|
+// 主任务同步任务与子任务共用,保证每个 item 转终态后驱动一次完成检查。
|
|
|
+func finishItem(id primitive.ObjectID, key string, status domain.ItemStatus, stage, reason string) {
|
|
|
+ done, total, err := repositories.FinishBatchDownloadItem(id, key, status, stage, reason)
|
|
|
+ if err != nil {
|
|
|
+ zap.L().Error("[BatchDownload] 更新 item 结果失败",
|
|
|
+ zap.String("id", id.Hex()), zap.String("key", key), zap.String("err", err.Error()))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ maybeAggregate(id, done, total)
|
|
|
+}
|