低空智控平台 后端go
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

372 lines
11 KiB

package service
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"github.com/robfig/cron/v3"
"gorm.io/gorm"
"laic-backend/cache"
"laic-backend/common"
"laic-backend/logger"
"laic-backend/model"
"laic-backend/tool"
"laic-backend/vo"
)
type TaskService struct{}
var DefaultTaskService = &TaskService{}
// GetPage 任务分页列表
func (s *TaskService) GetPage(userID int64, isAdmin bool, req *vo.TaskPageReq) (*common.PageResponse[vo.TaskPlanVO], *common.BusiError) {
db := common.DB.Model(&model.TaskPlan{}).Scopes(withUserFilter(userID, isAdmin))
if req.Status != "" {
db = db.Where("status = ?", req.Status)
}
if req.ScheduleType != "" {
db = db.Where("schedule_type = ?", req.ScheduleType)
}
if req.Keyword != "" {
db = db.Where("name LIKE ?", "%"+req.Keyword+"%")
}
var total int64
if err := db.Count(&total).Error; err != nil {
logger.ERROR("统计任务失败", err)
return nil, common.ErrInternal
}
var tasks []model.TaskPlan
if err := db.Scopes(req.Paginate).Order("created_at DESC, id DESC").Find(&tasks).Error; err != nil {
logger.ERROR("查询任务列表失败", err)
return nil, common.ErrInternal
}
latestByTaskID := make(map[string]*model.TaskExecution, len(tasks))
if len(tasks) > 0 {
taskIDs := make([]string, 0, len(tasks))
for _, task := range tasks {
taskIDs = append(taskIDs, task.ID)
}
var executions []model.TaskExecution
if err := common.DB.Where("task_id IN ?", taskIDs).Order("created_at DESC, id DESC").Find(&executions).Error; err != nil {
logger.ERROR("查询任务最近执行记录失败", err)
return nil, common.ErrInternal
}
for i := range executions {
exec := &executions[i]
if _, exists := latestByTaskID[exec.TaskID]; !exists {
latestByTaskID[exec.TaskID] = exec
}
}
}
list := make([]vo.TaskPlanVO, 0, len(tasks))
for _, task := range tasks {
latest := latestByTaskID[task.ID]
list = append(list, vo.TaskPlanVO{
TaskPlan: task,
PlanStatus: taskPlanStatus(task.Status, latest),
LatestExecution: latest,
})
}
return common.Page(req.Pagination, total, list), nil
}
func taskPlanStatus(status string, latest *model.TaskExecution) string {
if status == "disabled" {
return "paused"
}
if latest != nil && (latest.Status == "pending" || latest.Status == "running") {
return "running"
}
return "pending"
}
// GetDetail 任务详情
func (s *TaskService) GetDetail(userID int64, isAdmin bool, id string) (*model.TaskPlan, *common.BusiError) {
var task model.TaskPlan
if err := common.DB.Scopes(withUserFilter(userID, isAdmin)).First(&task, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, common.ErrTaskNotFound
}
return nil, common.ErrInternal
}
return &task, nil
}
func validateTaskSchedule(scheduleType, scheduleCron string) *common.BusiError {
if scheduleType == "once" {
return nil
}
if scheduleType != "cron" || scheduleCron == "" {
return common.NewBusiError(common.ParamError, "定时任务必须配置执行时间")
}
if _, err := cron.ParseStandard(scheduleCron); err != nil {
return common.NewBusiError(common.ParamError, "定时执行表达式无效")
}
return nil
}
// Create 新增任务
func (s *TaskService) Create(userID int64, req *vo.TaskCreateReq) (*model.TaskPlan, *common.BusiError) {
scheduleType := req.ScheduleType
if scheduleType == "" {
scheduleType = "once"
}
if busiErr := validateTaskSchedule(scheduleType, req.ScheduleCron); busiErr != nil {
return nil, busiErr
}
videoPolicy := req.VideoPolicy
if videoPolicy == "" {
videoPolicy = "raw"
}
now := time.Now()
task := &model.TaskPlan{
ID: strings.ReplaceAll(uuid.New().String(), "-", ""),
UserID: userID,
Name: req.Name,
DockID: req.DockID,
RouteID: req.RouteID,
ScheduleType: scheduleType,
ScheduleCron: req.ScheduleCron,
VideoPolicy: videoPolicy,
Status: "enabled",
CreatedBy: userID,
CreatedAt: now,
UpdatedAt: now,
}
if err := common.DB.Create(task).Error; err != nil {
logger.ERROR("新增任务失败", err)
return nil, common.ErrInternal
}
DefaultScheduler.SyncTask(task.ID)
return task, nil
}
// Update 编辑任务
func (s *TaskService) Update(userID int64, isAdmin bool, id string, req *vo.TaskUpdateReq) *common.BusiError {
var task model.TaskPlan
if err := common.DB.Scopes(withUserFilter(userID, isAdmin)).First(&task, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return common.ErrTaskNotFound
}
return common.ErrInternal
}
scheduleType := task.ScheduleType
if req.ScheduleType != "" {
scheduleType = req.ScheduleType
}
scheduleCron := task.ScheduleCron
if req.ScheduleCron != nil {
scheduleCron = *req.ScheduleCron
}
if scheduleType == "once" {
scheduleCron = ""
}
if busiErr := validateTaskSchedule(scheduleType, scheduleCron); busiErr != nil {
return busiErr
}
if req.Status != "" && req.Status != "draft" && req.Status != "enabled" && req.Status != "disabled" {
return common.NewBusiError(common.ParamError, "任务状态无效")
}
updates := map[string]any{"updated_at": time.Now()}
if req.Name != "" {
updates["name"] = req.Name
}
if req.DockID != "" {
updates["dock_id"] = req.DockID
}
if req.RouteID != nil {
updates["route_id"] = *req.RouteID
}
if req.ScheduleType != "" {
updates["schedule_type"] = req.ScheduleType
}
if req.ScheduleCron != nil {
updates["schedule_cron"] = *req.ScheduleCron
}
if req.ScheduleType != "" && req.ScheduleType == "once" {
updates["schedule_cron"] = ""
}
if req.VideoPolicy != "" {
updates["video_policy"] = req.VideoPolicy
}
if req.Status != "" {
updates["status"] = req.Status
}
if err := common.DB.Model(&task).Updates(updates).Error; err != nil {
logger.ERROR("更新任务失败", err)
return common.ErrInternal
}
DefaultScheduler.SyncTask(id)
return nil
}
// Delete 删除任务
func (s *TaskService) Delete(userID int64, isAdmin bool, id string) *common.BusiError {
res := common.DB.Scopes(withUserFilter(userID, isAdmin)).Delete(&model.TaskPlan{}, "id = ?", id)
if res.Error != nil {
logger.ERROR("删除任务失败", res.Error)
return common.ErrInternal
}
if res.RowsAffected == 0 {
return common.ErrTaskNotFound
}
DefaultScheduler.RemoveTask(id)
return nil
}
// Execute 立即执行任务:落库执行记录 + 下发 workflow.start_task 指令
func (s *TaskService) Execute(userID int64, isAdmin bool, id string) (*vo.TaskExecuteVO, *common.BusiError) {
var task model.TaskPlan
if err := common.DB.Scopes(withUserFilter(userID, isAdmin)).First(&task, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
logger.WARN("执行任务失败,任务不存在:", id)
return nil, common.ErrTaskNotFound
}
logger.ERROR("查询待执行任务失败", err)
return nil, common.ErrInternal
}
if task.Status == "disabled" {
logger.WARN("执行任务失败,任务已禁用:", id)
return nil, common.ErrTaskStateErr
}
online, _ := common.SetMemberExists(cache.OnlineDockSetKey, task.DockID)
if !online {
logger.WARN("执行任务失败,机巢离线:", task.DockID, "任务:", id)
return nil, common.ErrDockOffline
}
commandID, err := tool.NextID()
if err != nil {
return nil, common.ErrInternal
}
execID, err := tool.NextID()
if err != nil {
return nil, common.ErrInternal
}
requestID := strings.ReplaceAll(uuid.New().String(), "-", "")
now := time.Now()
params := map[string]any{
"taskId": task.ID,
"missionId": strings.ReplaceAll(uuid.New().String(), "-", ""),
"executionId": strconv.FormatInt(execID, 10),
"recordOriginalVideo": task.VideoPolicy == "raw",
}
if task.RouteID != 0 {
var waypoints []model.RouteWaypoint
if err := common.DB.Where("route_id = ?", task.RouteID).Order("seq ASC").Find(&waypoints).Error; err != nil {
logger.ERROR("查询航线航点失败", err)
return nil, common.ErrInternal
}
payload := make([]map[string]any, 0, len(waypoints))
for i, wp := range waypoints {
payload = append(payload, map[string]any{
"seq": i,
"longitude": wp.Longitude,
"latitude": wp.Latitude,
"altitude": wp.Altitude,
"speed": wp.Speed,
"yaw": wp.Yaw,
"holdSec": wp.HoldSec,
})
}
params["waypoints"] = payload
}
var originalVideo *model.Video
if task.VideoPolicy == "raw" {
videoID, err := tool.NextID()
if err != nil {
return nil, common.ErrInternal
}
expireAt := now.Add(10 * time.Minute)
originalVideo = &model.Video{
ID: videoID,
UserID: task.UserID,
ExecutionID: execID,
FileName: fmt.Sprintf("execution-%d-original.mp4", execID),
OssKey: fmt.Sprintf("media/%d/execution-%d-original.mp4", videoID, execID),
OssBucket: common.AppConf.OSS.Bucket,
Status: "pending",
UploadExpireAt: &expireAt,
CreatedAt: now,
}
uploadURL, err := common.PresignOSS("PUT", originalVideo.OssKey, 600)
if err != nil {
logger.ERROR("生成原始视频上传地址失败", err)
return nil, common.ErrInternal
}
params["originalVideo"] = map[string]any{
"videoId": strconv.FormatInt(videoID, 10),
"executionId": strconv.FormatInt(execID, 10),
"uploadUrl": uploadURL,
"uploadExpireAt": expireAt.UnixMilli(),
"fileName": originalVideo.FileName,
"contentType": "video/mp4",
}
}
paramsJSON := ""
if b, e := json.Marshal(params); e == nil {
paramsJSON = string(b)
}
cmdLog := &model.DeviceCommandLog{
ID: commandID,
DockID: task.DockID,
CommandType: "workflow.start_task",
Params: paramsJSON,
RequestID: requestID,
TTLMs: 30000,
Status: "sent",
SentAt: &now,
CreatedAt: now,
}
exec := &model.TaskExecution{
ID: execID,
TaskID: task.ID,
CommandID: strconv.FormatInt(commandID, 10),
DockID: task.DockID,
Status: "pending",
TrajectoryJSON: json.RawMessage("[]"),
CreatedAt: now,
}
if err := common.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(cmdLog).Error; err != nil {
return err
}
if err := tx.Create(exec).Error; err != nil {
return err
}
if originalVideo != nil {
return tx.Create(originalVideo).Error
}
return nil
}); err != nil {
logger.ERROR("创建执行记录失败", err)
return nil, common.ErrInternal
}
var drone model.Drone
_ = common.DB.Where("dock_id = ?", task.DockID).First(&drone).Error
DefaultTrajectoryStore.Begin(task.DockID, execID, drone.DroneSN)
if err := DefaultCommandService.publish(cmdLog, params); err != nil {
logger.ERROR("下发任务指令失败", err)
return nil, common.ErrInternal
}
return &vo.TaskExecuteVO{Execution: exec, Command: cmdLog}, nil
}