package service import ( "errors" "strconv" "time" "gorm.io/gorm" "laic-backend/common" "laic-backend/logger" "laic-backend/model" "laic-backend/tool" ) type WorkflowService struct{} var DefaultWorkflowService = &WorkflowService{} // WorkflowStateIn 工作流状态上报载荷 type WorkflowStateIn struct { CommandID string `json:"commandId"` Type string `json:"type"` TaskID string `json:"taskId"` MissionID string `json:"missionId"` State string `json:"state"` Step string `json:"step"` ResultCode string `json:"resultCode"` } // Upsert 持久化工作流状态(按 command_id 去重更新),并在终态时回写任务执行记录 func (s *WorkflowService) Upsert(dockID, requestID string, in *WorkflowStateIn) { now := time.Now() if requestID != "" { var cmd model.DeviceCommandLog if err := common.DB.Where("dock_id = ? AND request_id = ? AND status IN ?", dockID, requestID, []string{"sent", "acked"}).First(&cmd).Error; err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { logger.ERROR("按 requestId 查询工作流指令失败", err) } return } expected := strconv.FormatInt(cmd.ID, 10) if in.CommandID != "" && in.CommandID != expected { logger.WARN("工作流状态 commandId 与 requestId 不匹配", dockID, requestID) return } in.CommandID = expected } if in.CommandID != "" { var existing model.WorkflowState err := common.DB.Where("dock_id = ? AND command_id = ?", dockID, in.CommandID).First(&existing).Error if errors.Is(err, gorm.ErrRecordNotFound) { id, e := tool.NextID() if e == nil { common.DB.Create(&model.WorkflowState{ ID: id, DockID: dockID, CommandID: in.CommandID, Type: in.Type, TaskID: in.TaskID, MissionID: in.MissionID, State: in.State, Step: in.Step, ResultCode: in.ResultCode, UpdatedAt: now, }) } } else if err == nil { common.DB.Model(&existing).Updates(map[string]any{ "dock_id": dockID, "type": in.Type, "task_id": in.TaskID, "mission_id": in.MissionID, "state": in.State, "step": in.Step, "result_code": in.ResultCode, "updated_at": now, }) } } // 回写 task_execution,优先按 command_id 精确关联,兼容旧设备时只回退到最新未终态记录。 if (in.CommandID != "" || in.TaskID != "") && (in.State == "running" || isTerminalWorkflowState(in.State)) { updates := map[string]any{"status": in.State, "result_code": in.ResultCode} if in.State == "running" { updates["start_time"] = now } else { updates["end_time"] = now updates["start_time"] = gorm.Expr("COALESCE(start_time, ?)", now) } query := common.DB.Model(&model.TaskExecution{}). Where("dock_id = ? AND status NOT IN ('succeeded','failed','cancelled')", dockID) if in.CommandID != "" { query = query.Where("command_id = ?", in.CommandID) } else { query = query.Where("task_id = ?", in.TaskID) var execution model.TaskExecution if err := query.Order("created_at DESC, id DESC").First(&execution).Error; err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { logger.ERROR("查找任务执行记录失败", err) } query = nil } else { query = common.DB.Model(&execution) } } if query != nil { if err := query.Updates(updates).Error; err != nil { logger.ERROR("回写任务执行状态失败", err) } } } // 终态回写轨迹(MySQL trajectory_json + TDengine task_trajectory) if isTerminalWorkflowState(in.State) { DefaultTrajectoryStore.Finalize(dockID, DefaultTelemetryStore.DroneSN(dockID)) } } func isTerminalWorkflowState(state string) bool { switch state { case "succeeded", "failed", "cancelled": return true default: return false } }