增加任务增删改查时对设备任务关联表的维护
This commit is contained in:
		@@ -170,7 +170,14 @@ func initDomainServices(cfg *config.Config, infra *Infrastructure, logger *logs.
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// 计划管理器
 | 
			
		||||
	planService := plan.NewPlanService(planExecutionManager, analysisPlanTaskManager, infra.repos.planRepo, logger)
 | 
			
		||||
	planService := plan.NewPlanService(
 | 
			
		||||
		planExecutionManager,
 | 
			
		||||
		analysisPlanTaskManager,
 | 
			
		||||
		infra.repos.planRepo,
 | 
			
		||||
		infra.repos.deviceRepo,
 | 
			
		||||
		infra.repos.unitOfWork,
 | 
			
		||||
		taskFactory,
 | 
			
		||||
		logger)
 | 
			
		||||
 | 
			
		||||
	return &DomainServices{
 | 
			
		||||
		pigPenTransferManager:   pigPenTransferManager,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								internal/domain/plan/device_id_extractor.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								internal/domain/plan/device_id_extractor.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
package plan
 | 
			
		||||
@@ -2,6 +2,7 @@ package plan
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
 | 
			
		||||
@@ -55,25 +56,31 @@ type Service interface {
 | 
			
		||||
type planServiceImpl struct {
 | 
			
		||||
	executionManager ExecutionManager
 | 
			
		||||
	taskManager      AnalysisPlanTaskManager
 | 
			
		||||
	planRepo         repository.PlanRepository // 新增
 | 
			
		||||
	// deviceRepo       repository.DeviceRepository // 如果需要,新增
 | 
			
		||||
	logger *logs.Logger
 | 
			
		||||
	planRepo         repository.PlanRepository
 | 
			
		||||
	deviceRepo       repository.DeviceRepository
 | 
			
		||||
	unitOfWork       repository.UnitOfWork
 | 
			
		||||
	taskFactory      TaskFactory
 | 
			
		||||
	logger           *logs.Logger
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewPlanService 创建一个新的 Service 实例。
 | 
			
		||||
func NewPlanService(
 | 
			
		||||
	executionManager ExecutionManager,
 | 
			
		||||
	taskManager AnalysisPlanTaskManager,
 | 
			
		||||
	planRepo repository.PlanRepository, // 新增
 | 
			
		||||
	// deviceRepo repository.DeviceRepository, // 如果需要,新增
 | 
			
		||||
	planRepo repository.PlanRepository,
 | 
			
		||||
	deviceRepo repository.DeviceRepository,
 | 
			
		||||
	unitOfWork repository.UnitOfWork,
 | 
			
		||||
	taskFactory TaskFactory,
 | 
			
		||||
	logger *logs.Logger,
 | 
			
		||||
) Service {
 | 
			
		||||
	return &planServiceImpl{
 | 
			
		||||
		executionManager: executionManager,
 | 
			
		||||
		taskManager:      taskManager,
 | 
			
		||||
		planRepo:         planRepo, // 注入
 | 
			
		||||
		// deviceRepo:       deviceRepo, // 注入
 | 
			
		||||
		logger: logger,
 | 
			
		||||
		planRepo:         planRepo,
 | 
			
		||||
		deviceRepo:       deviceRepo,
 | 
			
		||||
		unitOfWork:       unitOfWork,
 | 
			
		||||
		taskFactory:      taskFactory,
 | 
			
		||||
		logger:           logger,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -117,13 +124,40 @@ func (s *planServiceImpl) CreatePlan(planToCreate *models.Plan) (*models.Plan, e
 | 
			
		||||
	}
 | 
			
		||||
	planToCreate.ReorderSteps()
 | 
			
		||||
 | 
			
		||||
	// 3. 调用仓库方法创建计划
 | 
			
		||||
	if err := s.planRepo.CreatePlan(planToCreate); err != nil {
 | 
			
		||||
	// 3. 在调用仓库前,准备好所有数据,包括设备关联
 | 
			
		||||
	for i := range planToCreate.Tasks {
 | 
			
		||||
		taskModel := &planToCreate.Tasks[i]
 | 
			
		||||
		// 使用工厂创建临时领域对象
 | 
			
		||||
		taskResolver, err := s.taskFactory.CreateTaskFromModel(taskModel)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			// 如果一个任务类型不支持,我们可以选择跳过或报错
 | 
			
		||||
			s.logger.Warnf("跳过为任务类型 '%s' 解析设备ID: %v", taskModel.Type, err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		deviceIDs, err := taskResolver.ResolveDeviceIDs()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			// 在事务外解析失败,直接返回错误
 | 
			
		||||
			return nil, fmt.Errorf("为任务 '%s' 提取设备ID失败: %w", taskModel.Name, err)
 | 
			
		||||
		}
 | 
			
		||||
		if len(deviceIDs) > 0 {
 | 
			
		||||
			// 优化:无需查询完整的设备对象,只需构建包含ID的结构体即可建立关联
 | 
			
		||||
			devices := make([]models.Device, len(deviceIDs))
 | 
			
		||||
			for i, id := range deviceIDs {
 | 
			
		||||
				devices[i] = models.Device{Model: gorm.Model{ID: id}}
 | 
			
		||||
			}
 | 
			
		||||
			taskModel.Devices = devices
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 4. 调用仓库方法创建计划,该方法内部会处理事务
 | 
			
		||||
	err := s.planRepo.CreatePlan(planToCreate)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		s.logger.Errorf("%s: 数据库创建计划失败: %v", actionType, err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 4. 创建成功后,调用 manager 确保触发器任务定义存在,但不立即加入待执行队列
 | 
			
		||||
	// 5. 创建成功后,调用 manager 确保触发器任务定义存在,但不立即加入待执行队列
 | 
			
		||||
	if err := s.taskManager.EnsureAnalysisTaskDefinition(planToCreate.ID); err != nil {
 | 
			
		||||
		// 这是一个非阻塞性错误,我们只记录日志,因为主流程(创建计划)已经成功
 | 
			
		||||
		s.logger.Errorf("为新创建的计划 %d 确保触发器任务定义失败: %v", planToCreate.ID, err)
 | 
			
		||||
@@ -203,7 +237,32 @@ func (s *planServiceImpl) UpdatePlan(planToUpdate *models.Plan) (*models.Plan, e
 | 
			
		||||
	planToUpdate.ExecuteCount = 0
 | 
			
		||||
	s.logger.Infof("计划 #%d 被更新,执行计数器已重置为 0。", planToUpdate.ID)
 | 
			
		||||
 | 
			
		||||
	if err := s.planRepo.UpdatePlanMetadataAndStructure(planToUpdate); err != nil {
 | 
			
		||||
	// 在调用仓库前,准备好所有数据,包括设备关联
 | 
			
		||||
	for i := range planToUpdate.Tasks {
 | 
			
		||||
		taskModel := &planToUpdate.Tasks[i]
 | 
			
		||||
		taskResolver, err := s.taskFactory.CreateTaskFromModel(taskModel)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			s.logger.Warnf("跳过为任务类型 '%s' 解析设备ID: %v", taskModel.Type, err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		deviceIDs, err := taskResolver.ResolveDeviceIDs()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("为任务 '%s' 提取设备ID失败: %w", taskModel.Name, err)
 | 
			
		||||
		}
 | 
			
		||||
		if len(deviceIDs) > 0 {
 | 
			
		||||
			// 优化:无需查询完整的设备对象,只需构建包含ID的结构体即可建立关联
 | 
			
		||||
			devices := make([]models.Device, len(deviceIDs))
 | 
			
		||||
			for i, id := range deviceIDs {
 | 
			
		||||
				devices[i] = models.Device{Model: gorm.Model{ID: id}}
 | 
			
		||||
			}
 | 
			
		||||
			taskModel.Devices = devices
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 调用仓库方法更新计划,该方法内部会处理事务
 | 
			
		||||
	err = s.planRepo.UpdatePlanMetadataAndStructure(planToUpdate)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		s.logger.Errorf("%s: 数据库更新计划失败: %v, Plan: %+v", actionType, err, planToUpdate)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -29,4 +29,6 @@ type TaskDeviceIDResolver interface {
 | 
			
		||||
type TaskFactory interface {
 | 
			
		||||
	// Production 根据指定的任务执行日志创建一个任务实例。
 | 
			
		||||
	Production(claimedLog *models.TaskExecutionLog) Task
 | 
			
		||||
	// CreateTaskFromModel 仅根据任务模型创建一个任务实例,用于非执行场景(如参数解析)。
 | 
			
		||||
	CreateTaskFromModel(taskModel *models.Task) (TaskDeviceIDResolver, error)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
package task
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/domain/plan"
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
 | 
			
		||||
@@ -43,3 +45,27 @@ func (t *taskFactory) Production(claimedLog *models.TaskExecutionLog) plan.Task
 | 
			
		||||
		panic("不支持的任务类型") // 显式panic防编译器报错
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateTaskFromModel 实现了 TaskFactory 接口,用于从模型创建任务实例。
 | 
			
		||||
func (t *taskFactory) CreateTaskFromModel(taskModel *models.Task) (plan.TaskDeviceIDResolver, error) {
 | 
			
		||||
	// 这个方法不关心 claimedLog 的其他字段,所以可以构造一个临时的
 | 
			
		||||
	// 它只用于访问那些不依赖于执行日志的方法,比如 ResolveDeviceIDs
 | 
			
		||||
	tempLog := &models.TaskExecutionLog{Task: *taskModel}
 | 
			
		||||
 | 
			
		||||
	switch taskModel.Type {
 | 
			
		||||
	case models.TaskTypeWaiting:
 | 
			
		||||
		return NewDelayTask(t.logger, tempLog), nil
 | 
			
		||||
	case models.TaskTypeReleaseFeedWeight:
 | 
			
		||||
		return NewReleaseFeedWeightTask(
 | 
			
		||||
			tempLog,
 | 
			
		||||
			t.sensorDataRepo,
 | 
			
		||||
			t.deviceRepo,
 | 
			
		||||
			t.deviceService,
 | 
			
		||||
			t.logger,
 | 
			
		||||
		), nil
 | 
			
		||||
	case models.TaskTypeFullCollection:
 | 
			
		||||
		return NewFullCollectionTask(tempLog, t.deviceRepo, t.deviceService, t.logger), nil
 | 
			
		||||
	default:
 | 
			
		||||
		return nil, fmt.Errorf("不支持为类型 '%s' 的任务创建模型实例", taskModel.Type)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@ func GetAllModels() []interface{} {
 | 
			
		||||
		&DeviceTemplate{},
 | 
			
		||||
		&SensorData{},
 | 
			
		||||
		&DeviceCommandLog{},
 | 
			
		||||
		&DeviceTask{},
 | 
			
		||||
 | 
			
		||||
		// Plan & Task Models
 | 
			
		||||
		&Plan{},
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ type DeviceRepository interface {
 | 
			
		||||
	// FindByID 根据主键 ID 查找设备
 | 
			
		||||
	FindByID(id uint) (*models.Device, error)
 | 
			
		||||
 | 
			
		||||
	// FindByIDString 根据字符串形式的主键 ID 查找设备,方便控制器调用
 | 
			
		||||
	// FindByIDString 根据字符串形式的主键 ID 查找设备
 | 
			
		||||
	FindByIDString(id string) (*models.Device, error)
 | 
			
		||||
 | 
			
		||||
	// ListAll 获取所有设备的列表
 | 
			
		||||
@@ -26,7 +26,7 @@ type DeviceRepository interface {
 | 
			
		||||
	// ListAllSensors 获取所有传感器类型的设备列表
 | 
			
		||||
	ListAllSensors() ([]*models.Device, error)
 | 
			
		||||
 | 
			
		||||
	// ListByAreaControllerID 根据区域主控 ID 列出所有子设备。
 | 
			
		||||
	// ListByAreaControllerID 根据区域主控 ID 列出所有子设备
 | 
			
		||||
	ListByAreaControllerID(areaControllerID uint) ([]*models.Device, error)
 | 
			
		||||
 | 
			
		||||
	// FindByDeviceTemplateID 根据设备模板ID查找所有使用该模板的设备
 | 
			
		||||
@@ -40,6 +40,9 @@ type DeviceRepository interface {
 | 
			
		||||
 | 
			
		||||
	// FindByAreaControllerAndPhysicalAddress 根据区域主控ID和物理地址(总线号、总线地址)查找设备
 | 
			
		||||
	FindByAreaControllerAndPhysicalAddress(areaControllerID uint, busNumber int, busAddress int) (*models.Device, error)
 | 
			
		||||
 | 
			
		||||
	// GetDevicesByIDsTx 在指定事务中根据ID列表获取设备
 | 
			
		||||
	GetDevicesByIDsTx(tx *gorm.DB, ids []uint) ([]models.Device, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// gormDeviceRepository 是 DeviceRepository 的 GORM 实现
 | 
			
		||||
@@ -66,6 +69,18 @@ func (r *gormDeviceRepository) FindByID(id uint) (*models.Device, error) {
 | 
			
		||||
	return &device, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetDevicesByIDsTx 在指定事务中根据ID列表获取设备
 | 
			
		||||
func (r *gormDeviceRepository) GetDevicesByIDsTx(tx *gorm.DB, ids []uint) ([]models.Device, error) {
 | 
			
		||||
	var devices []models.Device
 | 
			
		||||
	if len(ids) == 0 {
 | 
			
		||||
		return devices, nil
 | 
			
		||||
	}
 | 
			
		||||
	if err := tx.Where("id IN ?", ids).Find(&devices).Error; err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return devices, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FindByIDString 根据字符串形式的主键 ID 查找设备
 | 
			
		||||
func (r *gormDeviceRepository) FindByIDString(id string) (*models.Device, error) {
 | 
			
		||||
	// 将字符串ID转换为uint64
 | 
			
		||||
 
 | 
			
		||||
@@ -48,6 +48,8 @@ type PlanRepository interface {
 | 
			
		||||
	GetPlansByIDs(ids []uint) ([]models.Plan, error)
 | 
			
		||||
	// CreatePlan 创建一个新的计划
 | 
			
		||||
	CreatePlan(plan *models.Plan) error
 | 
			
		||||
	// CreatePlanTx 在指定事务中创建一个新的计划
 | 
			
		||||
	CreatePlanTx(tx *gorm.DB, plan *models.Plan) error
 | 
			
		||||
	// UpdatePlanMetadataAndStructure 更新计划的元数据和结构,但不包括状态等运行时信息
 | 
			
		||||
	UpdatePlanMetadataAndStructure(plan *models.Plan) error
 | 
			
		||||
	// UpdatePlan 更新计划的所有字段
 | 
			
		||||
@@ -200,62 +202,66 @@ func (r *gormPlanRepository) GetPlanByID(id uint) (*models.Plan, error) {
 | 
			
		||||
 | 
			
		||||
// CreatePlan 创建一个新的计划
 | 
			
		||||
func (r *gormPlanRepository) CreatePlan(plan *models.Plan) error {
 | 
			
		||||
	return r.db.Transaction(func(tx *gorm.DB) error {
 | 
			
		||||
		// 1. 前置校验
 | 
			
		||||
		if plan.ID != 0 {
 | 
			
		||||
			return ErrCreateWithNonZeroID
 | 
			
		||||
		}
 | 
			
		||||
	return r.CreatePlanTx(r.db, plan)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		// 检查是否同时包含任务和子计划
 | 
			
		||||
		if len(plan.Tasks) > 0 && len(plan.SubPlans) > 0 {
 | 
			
		||||
			return ErrMixedContent
 | 
			
		||||
		}
 | 
			
		||||
// CreatePlanTx 在指定事务中创建一个新的计划
 | 
			
		||||
func (r *gormPlanRepository) CreatePlanTx(tx *gorm.DB, plan *models.Plan) error {
 | 
			
		||||
	// 1. 前置校验
 | 
			
		||||
	if plan.ID != 0 {
 | 
			
		||||
		return ErrCreateWithNonZeroID
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
		// 检查是否有重复的执行顺序
 | 
			
		||||
		if err := plan.ValidateExecutionOrder(); err != nil {
 | 
			
		||||
			return fmt.Errorf("计划 (ID: %d) 的执行顺序无效: %w", plan.ID, err)
 | 
			
		||||
		}
 | 
			
		||||
	// 检查是否同时包含任务和子计划
 | 
			
		||||
	if len(plan.Tasks) > 0 && len(plan.SubPlans) > 0 {
 | 
			
		||||
		return ErrMixedContent
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
		// 如果是子计划类型,验证所有子计划是否存在且ID不为0
 | 
			
		||||
		if plan.ContentType == models.PlanContentTypeSubPlans {
 | 
			
		||||
			childIDsToValidate := make(map[uint]bool)
 | 
			
		||||
			for _, subPlanLink := range plan.SubPlans {
 | 
			
		||||
				if subPlanLink.ChildPlanID == 0 {
 | 
			
		||||
					return ErrSubPlanIDIsZeroOnCreate
 | 
			
		||||
				}
 | 
			
		||||
				childIDsToValidate[subPlanLink.ChildPlanID] = true
 | 
			
		||||
	// 检查是否有重复的执行顺序
 | 
			
		||||
	if err := plan.ValidateExecutionOrder(); err != nil {
 | 
			
		||||
		return fmt.Errorf("计划 (ID: %d) 的执行顺序无效: %w", plan.ID, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 如果是子计划类型,验证所有子计划是否存在且ID不为0
 | 
			
		||||
	if plan.ContentType == models.PlanContentTypeSubPlans {
 | 
			
		||||
		childIDsToValidate := make(map[uint]bool)
 | 
			
		||||
		for _, subPlanLink := range plan.SubPlans {
 | 
			
		||||
			if subPlanLink.ChildPlanID == 0 {
 | 
			
		||||
				return ErrSubPlanIDIsZeroOnCreate
 | 
			
		||||
			}
 | 
			
		||||
			childIDsToValidate[subPlanLink.ChildPlanID] = true
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
			var ids []uint
 | 
			
		||||
			for id := range childIDsToValidate {
 | 
			
		||||
				ids = append(ids, id)
 | 
			
		||||
		var ids []uint
 | 
			
		||||
		for id := range childIDsToValidate {
 | 
			
		||||
			ids = append(ids, id)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if len(ids) > 0 {
 | 
			
		||||
			var count int64
 | 
			
		||||
			if err := tx.Model(&models.Plan{}).Where("id IN ?", ids).Count(&count).Error; err != nil {
 | 
			
		||||
				return fmt.Errorf("验证子计划存在性失败: %w", err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if len(ids) > 0 {
 | 
			
		||||
				var count int64
 | 
			
		||||
				if err := tx.Model(&models.Plan{}).Where("id IN ?", ids).Count(&count).Error; err != nil {
 | 
			
		||||
					return fmt.Errorf("验证子计划存在性失败: %w", err)
 | 
			
		||||
				}
 | 
			
		||||
				if int(count) != len(ids) {
 | 
			
		||||
					return ErrNodeDoesNotExist
 | 
			
		||||
				}
 | 
			
		||||
			if int(count) != len(ids) {
 | 
			
		||||
				return ErrNodeDoesNotExist
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
		// 2. 创建根计划
 | 
			
		||||
		// GORM 会自动处理关联的 Tasks (如果 ContentType 是 tasks 且 Task.ID 为 0)
 | 
			
		||||
		if err := tx.Create(plan).Error; err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	// 2. 创建根计划
 | 
			
		||||
	// GORM 会自动处理关联的 Tasks (如果 ContentType 是 tasks 且 Task.ID 为 0),
 | 
			
		||||
	// 以及 Tasks 内部已经填充好的 Devices 关联。
 | 
			
		||||
	if err := tx.Create(plan).Error; err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
		// 3. 创建触发器Task
 | 
			
		||||
		// 关键修改:调用 createPlanAnalysisTask 并处理其返回的 Task 对象
 | 
			
		||||
		_, err := r.createPlanAnalysisTask(tx, plan)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
	// 3. 创建触发器Task
 | 
			
		||||
	// 关键修改:调用 createPlanAnalysisTask 并处理其返回的 Task 对象
 | 
			
		||||
	_, err := r.createPlanAnalysisTask(tx, plan)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdatePlan 更新计划
 | 
			
		||||
@@ -414,9 +420,7 @@ func (r *gormPlanRepository) reconcileTasks(tx *gorm.DB, plan *models.Plan) erro
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(tasksToDelete) > 0 {
 | 
			
		||||
		if err := tx.Delete(&models.Task{}, tasksToDelete).Error; err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		return r.deleteTasksTx(tx, tasksToDelete)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@@ -555,42 +559,43 @@ func (r *gormPlanRepository) flattenPlanTasksRecursive(plan *models.Plan) ([]mod
 | 
			
		||||
func (r *gormPlanRepository) DeleteTask(id int) error {
 | 
			
		||||
	// 使用事务确保操作的原子性
 | 
			
		||||
	return r.db.Transaction(func(tx *gorm.DB) error {
 | 
			
		||||
		return r.deleteTask(tx, id)
 | 
			
		||||
		return r.deleteTasksTx(tx, []int{id})
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// deleteTask 根据ID删除任务
 | 
			
		||||
func (r *gormPlanRepository) deleteTask(tx *gorm.DB, id int) error {
 | 
			
		||||
	// 1. 检查是否有待执行任务引用了这个任务
 | 
			
		||||
// deleteTasksTx 在事务中批量软删除任务,并物理删除其在关联表中的记录
 | 
			
		||||
func (r *gormPlanRepository) deleteTasksTx(tx *gorm.DB, ids []int) error {
 | 
			
		||||
	if len(ids) == 0 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查是否有待执行任务引用了这些任务
 | 
			
		||||
	var pendingTaskCount int64
 | 
			
		||||
	if err := tx.Model(&models.PendingTask{}).Where("task_id = ?", id).Count(&pendingTaskCount).Error; err != nil {
 | 
			
		||||
	if err := tx.Model(&models.PendingTask{}).Where("task_id IN ?", ids).Count(&pendingTaskCount).Error; err != nil {
 | 
			
		||||
		return fmt.Errorf("检查待执行任务时出错: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 如果有待执行任务引用该任务,不能删除
 | 
			
		||||
	if pendingTaskCount > 0 {
 | 
			
		||||
		return fmt.Errorf("无法删除任务(ID: %d),因为存在 %d 条待执行任务引用该任务", id, pendingTaskCount)
 | 
			
		||||
		return fmt.Errorf("无法删除任务,因为存在 %d 条待执行任务引用这些任务", pendingTaskCount)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 2. 检查是否有计划仍在使用这个任务
 | 
			
		||||
	var planCount int64
 | 
			
		||||
	if err := tx.Model(&models.Plan{}).Joins("JOIN tasks ON plans.id = tasks.plan_id").Where("tasks.id = ?", id).Count(&planCount).Error; err != nil {
 | 
			
		||||
		return fmt.Errorf("检查计划引用任务时出错: %w", err)
 | 
			
		||||
	// 因为钩子函数在批量删除中不会被触发, 所以手动删除关联表, 通过批量删除语句优化性能
 | 
			
		||||
 | 
			
		||||
	// 1. 直接、高效地从关联表中物理删除所有相关记录
 | 
			
		||||
	// 这是最关键的优化,避免了不必要的查询和循环
 | 
			
		||||
	if err := tx.Where("task_id IN ?", ids).Delete(&models.DeviceTask{}).Error; err != nil {
 | 
			
		||||
		return fmt.Errorf("清理任务的设备关联失败: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 如果有计划在使用该任务,不能删除
 | 
			
		||||
	if planCount > 0 {
 | 
			
		||||
		return fmt.Errorf("无法删除任务(ID: %d),因为存在 %d 个计划仍在使用该任务", id, planCount)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 3. 执行删除操作
 | 
			
		||||
	result := tx.Delete(&models.Task{}, id)
 | 
			
		||||
	// 2. 对任务本身进行软删除
 | 
			
		||||
	result := tx.Delete(&models.Task{}, ids)
 | 
			
		||||
	if result.Error != nil {
 | 
			
		||||
		return fmt.Errorf("删除任务失败: %w", result.Error)
 | 
			
		||||
		return result.Error
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查是否实际删除了记录
 | 
			
		||||
	if result.RowsAffected == 0 {
 | 
			
		||||
	// 3. 如果是单个删除且未找到记录,则返回错误
 | 
			
		||||
	if len(ids) == 1 && result.RowsAffected == 0 {
 | 
			
		||||
		return gorm.ErrRecordNotFound
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user